|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: AI-Powered Smart Searching in SfAutocomplete Control | Syncfusion® |
| 4 | +description: Learn about how to implement AI-powered smart search using Syncfusion® .NET MAUI Autocomplete control. |
| 5 | +platform: maui |
| 6 | +control: SfAutocomplete |
| 7 | +documentation: ug |
| 8 | +--- |
| 9 | + |
| 10 | +# Implementing AI-Powered Smart Search in .NET MAUI Autocomplete |
| 11 | + |
| 12 | +This document will walk you through the implementation of an advanced search functionality in the Syncfusion [.NET MAUI Autocomplete](https://help.syncfusion.com/cr/maui/Syncfusion.Maui.Inputs.SfAutocomplete.html) control. The example leverages the power of Azure OpenAI for an intelligent, AI-driven search experience. |
| 13 | + |
| 14 | +## Integrating Azure OpenAI with your .NET MAUI App |
| 15 | + |
| 16 | +First, ensure you have access to [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/overview) and have created a deployment in the Azure portal. |
| 17 | + |
| 18 | +If you don’t have access, please refer to the [create and deploy Azure OpenAI service](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/create-resource?pivots=web-portal) guide to set up a new account. |
| 19 | + |
| 20 | +Note down the deployment name, endpoint URL, and API key. |
| 21 | + |
| 22 | +we’ll use the [Azure.AI.OpenAI](https://www.nuget.org/packages/Azure.AI.OpenAI/1.0.0-beta.12) NuGet package from the [NuGet Gallery](https://www.nuget.org/). So, before getting started, install the Azure.AI.OpenAI NuGet package in your .NET MAUI app. |
| 23 | + |
| 24 | +In your base service class (AzureBaseService), initialize the OpenAIClient. Replace the Endpoint, DeploymentName, Key with actual values from your Azure OpenAI resource. |
| 25 | + |
| 26 | +This creates a chat client using your endpoint, API key, and deployment name. It’s stored in the Client property for use in other methods. |
| 27 | + |
| 28 | +ComboBoxAzureAIService use this Client to send prompts and receive completions. |
| 29 | + |
| 30 | +In the `GetCompletion` method, we will construct the prompt and send it to the Azure OpenAI Service. The ChatHistory helps maintain context but is cleared for each new prompt in this implementation to ensure each search is independent. |
| 31 | + |
| 32 | +{% tabs %} |
| 33 | +{% highlight c# %} |
| 34 | + |
| 35 | +// AzureBaseService.cs |
| 36 | + public abstract class AzureBaseService |
| 37 | + { |
| 38 | + internal const string Endpoint = "YOUR_END_POINT_NAME"; |
| 39 | + |
| 40 | + internal const string DeploymentName = "DEPLOYMENT_NAME"; |
| 41 | + |
| 42 | + internal const string Key = "API_KEY"; |
| 43 | + |
| 44 | + public AzureBaseService() |
| 45 | + { |
| 46 | + } |
| 47 | + |
| 48 | + /// <summary> |
| 49 | + /// To get the Azure open ai kernal method |
| 50 | + /// </summary> |
| 51 | + private void GetAzureOpenAIKernal() |
| 52 | + { |
| 53 | + try |
| 54 | + { |
| 55 | + var client = new AzureOpenAIClient(new Uri(Endpoint), new AzureKeyCredential(Key)).AsChatClient(modelId: DeploymentName); |
| 56 | + this.Client = client; |
| 57 | + } |
| 58 | + catch (Exception) |
| 59 | + { |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + } |
| 64 | + |
| 65 | +{% endhighlight %} |
| 66 | + |
| 67 | +{% endtabs %} |
| 68 | + |
| 69 | +{% tabs %} |
| 70 | +{% highlight c# %} |
| 71 | + |
| 72 | +//AzureAIService.cs |
| 73 | + |
| 74 | +public class AzureAIService : AzureBaseService |
| 75 | + { |
| 76 | + /// <summary> |
| 77 | + /// Gets a completion response from the AzureAI service based on the provided prompt. |
| 78 | + /// </summary> |
| 79 | + /// <param name="prompt"></param> |
| 80 | + /// <param name="cancellationToken"></param> |
| 81 | + /// <returns></returns> |
| 82 | + public async Task<string> GetCompletion(string prompt, CancellationToken cancellationToken) |
| 83 | + { |
| 84 | + ChatHistory = string.Empty; |
| 85 | + if(ChatHistory != null) |
| 86 | + { |
| 87 | + ChatHistory = ChatHistory + "You are a filtering assistant."; |
| 88 | + ChatHistory = ChatHistory + prompt; |
| 89 | + try |
| 90 | + { |
| 91 | + if (Client != null) |
| 92 | + { |
| 93 | + cancellationToken.ThrowIfCancellationRequested(); |
| 94 | + var chatresponse = await Client.CompleteAsync(prompt); |
| 95 | + return chatresponse.ToString(); |
| 96 | + } |
| 97 | + } |
| 98 | + catch (RequestFailedException ex) |
| 99 | + { |
| 100 | + // Log the error message and rethrow the exception or handle it appropriately |
| 101 | + Debug.WriteLine($"Request failed: {ex.Message}"); |
| 102 | + throw; |
| 103 | + } |
| 104 | + catch (Exception ex) |
| 105 | + { |
| 106 | + // Handle other potential exceptions |
| 107 | + Debug.WriteLine($"An error occurred: {ex.Message}"); |
| 108 | + throw; |
| 109 | + } |
| 110 | + } |
| 111 | + return ""; |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | +{% endhighlight %} |
| 116 | + |
| 117 | +{% endtabs %} |
| 118 | + |
| 119 | +## Implementing custom filtering in .NET MAUI Autocomplete |
| 120 | + |
| 121 | +The [.NET MAUI Autocomplete](https://help.syncfusion.com/cr/maui/Syncfusion.Maui.Inputs.SfAutocomplete.html) control allows you to apply custom filter logic to suggest items based on your specific filter criteria by utilizing the `FilterBehavior` property, which is the entry point for our smart search logic. |
| 122 | + |
| 123 | +**Step 1:** Let’s create a new business model to search country names. Refer to the following code example. |
| 124 | + |
| 125 | +{% tabs %} |
| 126 | +{% highlight c# %} |
| 127 | + |
| 128 | +// Model.cs |
| 129 | + |
| 130 | +public class CountryModel |
| 131 | +{ |
| 132 | + public string? Name { get; set; } |
| 133 | +} |
| 134 | + |
| 135 | +//ViewModel.cs |
| 136 | + |
| 137 | +internal class CountryViewModel : INotifyPropertyChanged |
| 138 | + { |
| 139 | + private ObservableCollection<CountryModel> countries; |
| 140 | + |
| 141 | + public ObservableCollection<CountryModel> Countries |
| 142 | + { |
| 143 | + get { return countries; } |
| 144 | + set { countries = value; OnPropertyChanged(nameof(Countries)); } |
| 145 | + } |
| 146 | + public CountryViewModel() |
| 147 | + { |
| 148 | + countries = new ObservableCollection<CountryModel> |
| 149 | + { |
| 150 | + new CountryModel { Name = "Afghanistan" }, |
| 151 | + new CountryModel { Name = "Akrotiri" }, |
| 152 | + new CountryModel { Name = "Albania" }, |
| 153 | + new CountryModel { Name = "Algeria" }, |
| 154 | + new CountryModel { Name = "American Samoa" }, |
| 155 | + new CountryModel { Name = "Andorra" }, |
| 156 | + new CountryModel { Name = "Angola" }, |
| 157 | + new CountryModel { Name = "Anguilla" }, |
| 158 | + .... |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + public event PropertyChangedEventHandler? PropertyChanged; |
| 163 | + |
| 164 | + private void OnPropertyChanged(string propertyName) |
| 165 | + { |
| 166 | + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | +{% endhighlight %} |
| 171 | + |
| 172 | +{% endtabs %} |
| 173 | + |
| 174 | +**Step 2:** Connecting the Custom Filter to Azure OpenAI |
| 175 | + |
| 176 | +Implement the `GetMatchingItemsAsync` method from the interface. This method is the heart of the custom filter. It is invoked every time the text in the [Autocomplete](https://help.syncfusion.com/cr/maui/Syncfusion.Maui.Inputs.SfAutocomplete.html) control changes. |
| 177 | + |
| 178 | +The logic within [Autocomplete](https://help.syncfusion.com/cr/maui/Syncfusion.Maui.Inputs.SfAutocomplete.html) intelligently decides whether to perform an online AI search based on the availability of Azure credentials. |
| 179 | + |
| 180 | +To get accurate and structured results from the AI, we must provide a detailed prompt. This is constructed inside the |
| 181 | +`FilterCountriesUsingAzureAI` method. |
| 182 | + |
| 183 | +The `FilterCountriesUsingAzureAI` method uses prompt engineering to instruct the AI on how to filter the results, including asking it to handle spelling mistakes and providing the response in a clean format. |
| 184 | + |
| 185 | +{% tabs %} |
| 186 | +{% highlight c# %} |
| 187 | + |
| 188 | +//CustomFilter.cs |
| 189 | + |
| 190 | +public class CustomFilter : IAutocompleteFilterBehavior |
| 191 | + { |
| 192 | + private readonly AzureAIService _azureAIService; |
| 193 | + public ObservableCollection<CountryModel> Countries { get; set; } |
| 194 | + public ObservableCollection<CountryModel> FilteredCountries { get; set; } = new ObservableCollection<CountryModel>(); |
| 195 | + private CancellationTokenSource? _cancellationTokenSource; |
| 196 | + private SoundexAndLevensteinDistance soundexAndLevensteinDistance; |
| 197 | + |
| 198 | + public CustomFilter() |
| 199 | + { |
| 200 | + _azureAIService = new AzureAIService(); |
| 201 | + Countries = new ObservableCollection<CountryModel>(); |
| 202 | + _cancellationTokenSource = new CancellationTokenSource(); |
| 203 | + soundexAndLevensteinDistance = new SoundexAndLevensteinDistance(); |
| 204 | + } |
| 205 | + |
| 206 | + /// <summary> |
| 207 | + /// Finds matching items using the typed text |
| 208 | + /// </summary> |
| 209 | + /// <param name="source"></param> |
| 210 | + /// <param name="filterInfo"></param> |
| 211 | + /// <returns></returns> |
| 212 | + public async Task<object?> GetMatchingItemsAsync(SfAutocomplete source, AutocompleteFilterInfo filterInfo) |
| 213 | + { |
| 214 | + if (string.IsNullOrEmpty(filterInfo.Text)) |
| 215 | + { |
| 216 | + _cancellationTokenSource?.Cancel(); |
| 217 | + FilteredCountries.Clear(); |
| 218 | + return await Task.FromResult(FilteredCountries); |
| 219 | + } |
| 220 | + |
| 221 | + Countries = (ObservableCollection<CountryModel>)source.ItemsSource; |
| 222 | + |
| 223 | + // If the API key is not provided, perform an offline search using Soundex and Levenshtein algorithms. |
| 224 | + if (!AzureBaseService.IsCredentialValid) |
| 225 | + { |
| 226 | + foreach (CountryModel country in Countries) |
| 227 | + { |
| 228 | + soundexAndLevensteinDistance.FilterItemsBySoundexAndLevenshtein(filterInfo.Text, country.Name!); |
| 229 | + } |
| 230 | + var filteredItems = soundexAndLevensteinDistance.GetOrder(); |
| 231 | + |
| 232 | + return await Task.FromResult(filteredItems); |
| 233 | + } |
| 234 | + |
| 235 | + string listItems = string.Join(", ", Countries!.Select(c => c.Name)); |
| 236 | + |
| 237 | + // Join the first five items with newline characters for demo output template for AI |
| 238 | + string outputTemplate = string.Join("\n", Countries.Take(5).Select(c => c.Name)); |
| 239 | + |
| 240 | + //The cancellationToken was used for cancelling the API request if user types continuously |
| 241 | + _cancellationTokenSource?.Cancel(); |
| 242 | + _cancellationTokenSource = new CancellationTokenSource(); |
| 243 | + var cancellationToken = _cancellationTokenSource.Token; |
| 244 | + |
| 245 | + //Passing the User Input, ItemsSource, Reference output and CancellationToken |
| 246 | + var filterCountries = await FilterCountriesUsingAzureAI(filterInfo.Text, listItems, outputTemplate, cancellationToken); |
| 247 | + |
| 248 | + return await Task.FromResult(filterCountries); |
| 249 | + } |
| 250 | + |
| 251 | + /// <summary> |
| 252 | + /// Filters country names based on user input using Azure AI. |
| 253 | + /// </summary> |
| 254 | + /// <param name="userInput"></param> |
| 255 | + /// <param name="itemsList"></param> |
| 256 | + /// <param name="outputTemplate"></param> |
| 257 | + /// <param name="cancellationToken"></param> |
| 258 | + /// <returns></returns> |
| 259 | + public async Task<ObservableCollection<CountryModel>> FilterCountriesUsingAzureAI(string userInput, string itemsList, string outputTemplate, CancellationToken cancellationToken) |
| 260 | + { |
| 261 | + if (!string.IsNullOrEmpty(userInput)) |
| 262 | + { |
| 263 | + var prompt = $"Filter the list items based on the user input using character Starting with and Phonetic algorithms like Soundex or Damerau-Levenshtein Distance. " + |
| 264 | + $"The filter should ignore spelling mistakes and be case insensitive. " + |
| 265 | + $"Return only the filtered items with each item in new line without any additional content like explanations, Hyphen, Numberings and - Minus sign. Ignore the content 'Here are the filtered items or similar things' " + |
| 266 | + $"Only return items that are present in the List Items. " + |
| 267 | + $"Ensure that each filtered item is returned in its entirety without missing any part of its content. " + |
| 268 | + $"Arrange the filtered items that starting with the user input's first letter are at the first index, followed by other matches. " + |
| 269 | + $"Examples of filtering behavior: " + |
| 270 | + $" userInput: a, filter the items starting with A " + |
| 271 | + $" userInput: b, filter items starting with B " + |
| 272 | + $" userInput: c, filter items starting with C " + |
| 273 | + $" userInput: d, filter items starting with D " + |
| 274 | + $" userInput: e, filter items starting with E " + |
| 275 | + $" userInput: f, filter items starting with F " + |
| 276 | + $" userInput: i, filter items starting with I " + |
| 277 | + $" userInput: z, filter items starting with Z " + |
| 278 | + $" userInput: l, filter items starting with L " + |
| 279 | + $" userInput: q, filter items starting with Q " + |
| 280 | + $" userInput: o, filter items starting with O " + |
| 281 | + $" userInput: in, filter items starting with In " + |
| 282 | + $" userInput: pa, filter items starting with Pa " + |
| 283 | + $" userInput: em, filter items starting with Em " + |
| 284 | + $"The example data are for reference, dont provide these as output. Filter the item from list items properly" + |
| 285 | + $"Here is the User input: {userInput}, " + |
| 286 | + $"List of Items: {itemsList}" + |
| 287 | + $"If no items found, return \"Empty\" " + |
| 288 | + $"Dont use 'Here are the filtered items:' in the output. Check this demo output template, you should return output like this: {outputTemplate} "; |
| 289 | + |
| 290 | + var completion = await _azureAIService.GetCompletion(prompt, cancellationToken); |
| 291 | + |
| 292 | + var filteredCountryNames = completion.Split('\n').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).ToList(); |
| 293 | + |
| 294 | + if (FilteredCountries.Count > 0) |
| 295 | + FilteredCountries.Clear(); |
| 296 | + FilteredCountries.AddRange( |
| 297 | + Countries |
| 298 | + .Where(i => filteredCountryNames.Any(item => i.Name!.StartsWith(item)))); |
| 299 | + } |
| 300 | + return FilteredCountries; |
| 301 | + } |
| 302 | + |
| 303 | + } |
| 304 | + |
| 305 | +{% endhighlight %} |
| 306 | + |
| 307 | +{% endtabs %} |
| 308 | + |
| 309 | +**Step:3** Applying Custom Filtering to AutoComplete |
| 310 | + |
| 311 | +Applying custom filtering to the [Autocomplete](https://help.syncfusion.com/cr/maui/Syncfusion.Maui.Inputs.SfAutocomplete.html) control by using the `FilterBehavior` property. |
| 312 | + |
| 313 | +{% tabs %} |
| 314 | +{% highlight xaml %} |
| 315 | + |
| 316 | + <editors:SfAutocomplete x:Name="autoComplete" |
| 317 | + DropDownPlacement="Bottom" |
| 318 | + MaxDropDownHeight="200" |
| 319 | + TextSearchMode="Contains" |
| 320 | + DisplayMemberPath="Name" |
| 321 | + TextMemberPath="Name" |
| 322 | + ItemsSource="{Binding Countries}"> |
| 323 | + <editors:SfAutocomplete.FilterBehavior> |
| 324 | + <local:CustomFilter/> |
| 325 | + </editors:SfAutocomplete.FilterBehavior> |
| 326 | + </editors:SfAutocomplete> |
| 327 | + |
| 328 | +{% endhighlight %} |
| 329 | + |
| 330 | +{% endtabs %} |
| 331 | + |
| 332 | +The following image demonstrates the output of the above AI-based search using a custom filtering sample. |
| 333 | + |
| 334 | + |
| 335 | + |
| 336 | +You can find the complete sample from this [link](https://github.com/SyncfusionExamples/Smart-AI-Searching-using-.NET-MAUI-Autocomplete). |
| 337 | + |
| 338 | +By combining a powerful AI-driven online search with a robust you can create a truly smart and reliable search experience in your .NET MAUI applications. |
0 commit comments