Implement a Blazor full text search using Azure Cognitive Search

This article shows how to implement a full text search in Blazor using Azure Cognitive Search. The search results are returned using paging and the search index can be created, deleted from a Blazor application.

Code: https://github.com/damienbod/AspNetCoreAzureSearch

Posts in this series

Creating the Blazor App

The Blazor application was created using Visual Studio. The application requires an API which will be used to access, request the Azure Cognitive search service. We do not want to access the Azure Cognitive Search service directly from the WASM application because the free version requires an API key (or the paid versions can use an API key) and an API key cannot be stored safely in a SPA. The WASM app will only use its backend in the same domain and can be secured as required. The trusted backend can forward requests to other APIs, in our case to the Azure Cognitive search.

Creating an ASP.NET Core hosted Blazor application is slightly hidden. Once you select the Blazor WASM as your UI in Visual Studio, you need to select the ASP.NET Core hosted checkbox in the second step. This could probably be created using the dotnet new command as well, maybe someone knows how to do this.

The template creates three projects, a client, a server and a shared project. The Blazor application can be started from Visual Studio using the Server project. If you start the Client project, the API calls will not work. The Startup class is configured to use Blazor and the Azure Cognitive search client providers. The Azure.Search.Documents was added to the project file.

To setup the Search services, please refer to this blog, or the offical docs.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace BlazorAzureSearch.Server
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<SearchProviderIndex>();
            services.AddScoped<SearchProviderPaging>();
            services.AddScoped<SearchProviderAutoComplete>();

            services.AddHttpClient();

            services.AddControllersWithViews();
            services.AddRazorPages();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseWebAssemblyDebugging();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseBlazorFrameworkFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
                endpoints.MapFallbackToFile("index.html");
            });
        }
    }
}

Implement the Server APIs

The Blazor Server project implements two APIs to support the Blazor WASM API calls. One API for the Azure Cognitive index management and one API for the search. The Search paging API provides two methods which can request a search using paging. The API takes the returned Azure.Search.Documents results and maps the results to a POCO for the WASM UI.

using BlazorAzureSearch.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorAzureSearch.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class SearchPagingController : ControllerBase
    {
        private readonly SearchProviderPaging _searchProvider;
        private readonly ILogger<SearchAdminController> _logger;

        public SearchPagingController(SearchProviderPaging searchProvider,
        ILogger<SearchAdminController> logger)
        {
            _searchProvider = searchProvider;
            _logger = logger;
        }

        [HttpGet]
        public async Task<SearchData> Get(string searchText)
        {
            SearchData model = new SearchData
            {
                SearchText = searchText
            };

            await _searchProvider.QueryPagingFull(model, 0, 0).ConfigureAwait(false);

            return model;
        }

        [HttpPost]
        [Route("Paging")]
        public async Task<SearchDataDto> Paging([FromBody] SearchDataDto searchDataDto)
        {
            int page;

            switch (searchDataDto.Paging)
            {
                case "prev":
                    page = searchDataDto.CurrentPage - 1;
                    break;

                case "next":
                    page = searchDataDto.CurrentPage + 1;
                    break;

                default:
                    page = int.Parse(searchDataDto.Paging);
                    break;
            }

            int leftMostPage = searchDataDto.LeftMostPage;

            SearchData model = new SearchData
            {
                SearchText = searchDataDto.SearchText,
                LeftMostPage = searchDataDto.LeftMostPage,
                PageCount = searchDataDto.PageCount,
                PageRange = searchDataDto.PageRange,
                Paging = searchDataDto.Paging,
                CurrentPage = searchDataDto.CurrentPage
            };

            await _searchProvider.QueryPagingFull(model, page, leftMostPage).ConfigureAwait(false);

           
            var results = new SearchDataDto
            {
                SearchText = model.SearchText,
                LeftMostPage = model.LeftMostPage,
                PageCount = model.PageCount,
                PageRange = model.PageRange,
                Paging = model.Paging,
                CurrentPage = model.CurrentPage,
                Results = new SearchResultItems
                {
                   PersonCities = new List<PersonCityDto>(),
                   TotalCount = model.PersonCities.TotalCount.GetValueOrDefault()
                }
            };

            var docs =  model.PersonCities.GetResults().ToList();
            foreach(var doc in docs)
            {
                results.Results.PersonCities.Add(new PersonCityDto
                {
                    CityCountry = doc.Document.CityCountry,
                    FamilyName = doc.Document.FamilyName,
                    Github = doc.Document.Github,
                    Id = doc.Document.Id,
                    Info = doc.Document.Info,
                    Metadata = doc.Document.Metadata,
                    Mvp = doc.Document.Mvp,
                    Name = doc.Document.Name,
                    Twitter = doc.Document.Twitter,
                    Web = doc.Document.Web
                });
            }

            return results;
        }
    }
}

The SearchProviderPaging provider implements the Azure Cognitive Search service client using the Azure SDK nuget package. This class uses the user secrets and sets the search configuration. The paging was implemented based on the offical documentation samples for paging.

using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using BlazorAzureSearch.Shared;
using Microsoft.Extensions.Configuration;
using System;
using System.Threading.Tasks;

namespace BlazorAzureSearch.Server
{
    public class SearchProviderPaging
    {
        private readonly SearchClient _searchClient;
        private readonly string _index;

        public SearchProviderPaging(IConfiguration configuration)
        {
            _index = configuration["PersonCitiesIndexName"];

            Uri serviceEndpoint = new Uri(configuration["PersonCitiesSearchUri"]);
            AzureKeyCredential credential = new AzureKeyCredential(configuration["PersonCitiesSearchApiKey"]);
            _searchClient = new SearchClient(serviceEndpoint, _index, credential);
        }

        public async Task QueryPagingFull(SearchData model, int page, int leftMostPage)
        {
            var pageSize = 4;
            var maxPageRange = 7;
            var pageRangeDelta = maxPageRange - pageSize;

            var options = new SearchOptions
            {
                Skip = page * pageSize,
                Size = pageSize,
                IncludeTotalCount = true,
                QueryType = SearchQueryType.Full
            }; // options.Select.Add("Name"); // add this explicitly if all fields are not required

            model.PersonCities = await _searchClient.SearchAsync<PersonCity>(model.SearchText, options).ConfigureAwait(false);
            model.PageCount = ((int)model.PersonCities.TotalCount + pageSize - 1) / pageSize;
            model.CurrentPage = page;
            if (page == 0)
            {
                leftMostPage = 0;
            }
            else if (page <= leftMostPage)
            {
                leftMostPage = Math.Max(page - pageRangeDelta, 0);
            }
            else if (page >= leftMostPage + maxPageRange - 1)
            {
                leftMostPage = Math.Min(page - pageRangeDelta, model.PageCount - maxPageRange);
            }
            model.LeftMostPage = leftMostPage;
            model.PageRange = Math.Min(model.PageCount - leftMostPage, maxPageRange);
        }
    }
}

The SearchAdminController implements the API for the search administration Blazor UI. This API was created just for the Blazor view UI. The API makes it possible to create or delete an index, or add data to the index. The status of the index can also be queried.

using Azure.Search.Documents.Indexes.Models;
using BlazorAzureSearch.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorAzureSearch.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class SearchAdminController : ControllerBase
    {
        private readonly SearchProviderIndex _searchProviderIndex;
        private readonly ILogger<SearchAdminController> _logger;

        public SearchAdminController(SearchProviderIndex searchProviderIndex,
            ILogger<SearchAdminController> logger)
        {
            _searchProviderIndex = searchProviderIndex;
            _logger = logger;
        }

        [HttpGet]
        [Route("IndexStatus")]
        public async Task<IndexStatus> IndexStatus()
        {
            var indexStatus = await _searchProviderIndex.GetIndexStatus().ConfigureAwait(false);
            return new IndexStatus
            {
                IndexExists = indexStatus.Exists,
                DocumentCount = indexStatus.DocumentCount
            };
        }

        [HttpPost]
        [Route("DeleteIndex")]
        public async Task<IndexResult> DeleteIndex([FromBody] string indexName)
        {
            var deleteIndex = new IndexResult();
            if (string.IsNullOrEmpty(indexName))
            {
                deleteIndex.Messages = new List<AlertViewModel> {
                    new AlertViewModel("danger", "no indexName defined", "Please provide the index name"),
                };
                return deleteIndex;
            }

            try
            {
                await _searchProviderIndex.DeleteIndex(indexName).ConfigureAwait(false);

                deleteIndex.Messages = new List<AlertViewModel> {
                    new AlertViewModel("success", "Index Deleted!", "The Azure Search Index was successfully deleted!"),
                };
                var indexStatus = await _searchProviderIndex.GetIndexStatus().ConfigureAwait(false);
                deleteIndex.Status.IndexExists = indexStatus.Exists;
                deleteIndex.Status.DocumentCount = indexStatus.DocumentCount;
                return deleteIndex;
            }
            catch (Exception ex)
            {
                deleteIndex.Messages = new List<AlertViewModel> {
                    new AlertViewModel("danger", "Error deleting index", ex.Message),
                };
                return deleteIndex;
            }
        }

        [HttpPost]
        [Route("AddData")]
        public async Task<IndexResult> AddData([FromBody]string indexName)
        {
            var addData = new IndexResult();
            if (string.IsNullOrEmpty(indexName))
            {
                addData.Messages = new List<AlertViewModel> {
                    new AlertViewModel("danger", "no indexName defined", "Please provide the index name"),
                };
                return addData;
            }
            try
            {
                PersonCityData.CreateTestData();
                await _searchProviderIndex.AddDocumentsToIndex(PersonCityData.Data).ConfigureAwait(false);
                addData.Messages = new List<AlertViewModel>{
                    new AlertViewModel("success", "Documented added", "The Azure Search documents were uploaded! The Document Count takes n seconds to update!"),
                };
                var indexStatus = await _searchProviderIndex.GetIndexStatus().ConfigureAwait(false);
                addData.Status.IndexExists = indexStatus.Exists;
                addData.Status.DocumentCount = indexStatus.DocumentCount;
                return addData;
            }
            catch (Exception ex)
            {
                addData.Messages = new List<AlertViewModel> {
                    new AlertViewModel("danger", "Error adding documents", ex.Message),
                };
                return addData;
            }
        }

        [HttpPost]
        [Route("CreateIndex")]
        public async Task<IndexResult> CreateIndex([FromBody] string indexName)
        {
            var createIndex = new IndexResult();
            if (string.IsNullOrEmpty(indexName))
            {
                createIndex.Messages = new List<AlertViewModel> {
                    new AlertViewModel("danger", "no indexName defined", "Please provide the index name"),
                };
                return createIndex;
            }

            try
            {
                await _searchProviderIndex.CreateIndex().ConfigureAwait(false);
                createIndex.Messages = new List<AlertViewModel>  {
                    new AlertViewModel("success", "Index created", "The Azure Search index was created successfully!"),
                };
                var indexStatus = await _searchProviderIndex.GetIndexStatus().ConfigureAwait(false);
                createIndex.Status.IndexExists = indexStatus.Exists;
                createIndex.Status.DocumentCount = indexStatus.DocumentCount;
                return createIndex;
            }
            catch (Exception ex)
            {
                createIndex.Messages = new List<AlertViewModel> {
                    new AlertViewModel("danger", "Error creating index", ex.Message),
                };
                return createIndex;
            }

        }
    }
}

The SearchProviderIndex implements the management APIs for the Azure Cognitive search. The provider uses the Azure.Search.Documents Azure SDK package as well as the REST API directly to access the Azure Cognitive services. The Azure.Search.Documents Azure SDK provides no easy way to query the document count and the status of the index, so the REST API was used directly.

using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using Azure.Search.Documents.Models;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace BlazorAzureSearch.Server
{
    public class SearchProviderIndex
    {
        private readonly SearchIndexClient _searchIndexClient;
        private readonly SearchClient _searchClient;
        private readonly IConfiguration _configuration;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly string _index;

        public SearchProviderIndex(IConfiguration configuration, IHttpClientFactory httpClientFactory)
        {
            _configuration = configuration;
            _httpClientFactory = httpClientFactory;
            _index = configuration["PersonCitiesIndexName"];

            Uri serviceEndpoint = new Uri(configuration["PersonCitiesSearchUri"]);
            AzureKeyCredential credential = new AzureKeyCredential(configuration["PersonCitiesSearchApiKey"]);

            _searchIndexClient = new SearchIndexClient(serviceEndpoint, credential);
            _searchClient = new SearchClient(serviceEndpoint, _index, credential);

        }

        public async Task CreateIndex()
        {
            FieldBuilder bulder = new FieldBuilder();
            var definition = new SearchIndex(_index, bulder.Build(typeof(PersonCity)));
            definition.Suggesters.Add(new SearchSuggester(
                "personSg", new string[] { "Name", "FamilyName", "Info", "CityCountry" }
            ));

            await _searchIndexClient.CreateIndexAsync(definition).ConfigureAwait(false);
        }

        public async Task DeleteIndex(string indexName)
        {
            await _searchIndexClient.DeleteIndexAsync(indexName).ConfigureAwait(false);
        }

        public async Task<(bool Exists, long DocumentCount)> GetIndexStatus()
        {
            try
            {
                var httpClient = _httpClientFactory.CreateClient();
                httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue
                {
                    NoCache = true,
                };
                httpClient.DefaultRequestHeaders.Add("api-key", _configuration["PersonCitiesSearchApiKey"]);

                var uri = $"{_configuration["PersonCitiesSearchUri"]}/indexes/{_index}/docs/$count?api-version=2020-06-30";
                var data = await httpClient.GetAsync(uri).ConfigureAwait(false);
                if (data.StatusCode == System.Net.HttpStatusCode.NotFound)
                {
                    return (false, 0);
                }
                var payload = await data.Content.ReadAsStringAsync().ConfigureAwait(false);
                return (true, int.Parse(payload));
            }
            catch
            {
                return (false, 0);
            }
        }

        public async Task AddDocumentsToIndex(List<PersonCity> personCities)
        {
            var batch = IndexDocumentsBatch.Upload(personCities);
            await _searchClient.IndexDocumentsAsync(batch).ConfigureAwait(false);
        }
    }
}

Implement the Blazor UI

The Blazor WASM client project implements two razor views, one for the administration of the index and one for the paging search. The Blazor razor files contains both the template code as well as the code behind. This is the default. The navigation names, routes were changed but otherwise the UI is the same as the default Blazor templates from ASP.NET Core.

The search results are displayed in a list and calls the code behind methods which call the APIs of the Blazor Server project.

@page "/searchpaging"
@using BlazorAzureSearch.Shared
@inject HttpClient Http
@inject NavigationManager NavManager

<EditForm Model="@SearchData" class="centerMiddle">
    <div class="searchBoxForm">
        <InputText @bind-Value="SearchData.SearchText" class="searchBox"></InputText>
        <input class="searchBoxSubmit" @onclick="@(e => SearchPager(0.ToString(), SearchData.SearchText))">
    </div>
</EditForm>

@if (Loading)
{
    <div class="spinner d-flex align-items-center justify-content-center fixedSpinner" >
        <div class="spinner-border text-success" role="status">
            <span class="sr-only">Loading...</span>
        </div>
    </div>
} 

@if (SearchData.Results.PersonCities != null)
{
    <p class="sampleText centerMiddle">
        Found @SearchData.Results.TotalCount Documents
    </p>

    var results = SearchData.Results.PersonCities;

    @for (var i = 0; i < results.Count; i++)
    {
<div>
    <b><span><a href="@results[i].Web">@results[i].Name @results[i].FamilyName</a>: @results[i].CityCountry &nbsp;</span></b>
    @if (!string.IsNullOrEmpty(results[i].Twitter))
    {
        <a href="@results[i].Twitter"><img src="/images/socialTwitter.png" /></a>
    }
    @if (!string.IsNullOrEmpty(results[i].Github))
    {
        <a href="@results[i].Github"><img src="/images/github.png" /></a>
    }
    @if (!string.IsNullOrEmpty(results[i].Mvp))
    {
        <a href="@results[i].Mvp"><img src="/images/mvp.png" width="24" /></a>
    }
    <br />
    <em><span>@results[i].Metadata</span></em><br />
    <textarea class="infotext">@results[i].Info</textarea>
    <br />
</div>
    }
}

<div class="container">
    <div class="row">
        <div class="col">
            @if (SearchData.PageCount > 1)
            {
                <table class="col">
                    <tr class="col">
                        <td>
                            @if (SearchData.CurrentPage > 0)
                            {
                                <p class="pageButton">
                                    <button class="btn btn-link"
                                            @onclick="@(e => SearchPager(0.ToString(), SearchData.SearchText))">
                                        |<
                                    </button>
                                </p>
                            }
                            else
                            {
                                <p class="pageButtonDisabled">|&lt;</p>
                            }
                        </td>

                        <td>
                            @if (SearchData.CurrentPage > 0)
                            {
                                var prev = "prev";
                                <p class="pageButton">
                                    <button class="btn btn-link" @onclick="@(e => SearchPager(prev, SearchData.SearchText))"><</button>
                                </p>
                            }
                            else
                            {
                                <p class="pageButtonDisabled">&lt;</p>
                            }
                        </td>

                        @for (var pn = SearchData.LeftMostPage; pn < SearchData.LeftMostPage + SearchData.PageRange; pn++)
                        {
                            <td>
                                @if (SearchData.CurrentPage == pn)
                                {
                                    <p class="pageSelected">@(pn + 1)</p>
                                }
                                else
                                {
                                    <p class="pageButton">
                                        @{
                                            var p1 = SearchData.PageCount - 1;
                                            var plink = pn.ToString();
                                        }
                                        <button class="btn btn-link"
                                                @onclick="@(e => SearchPager(plink, SearchData.SearchText))">
                                            @(pn + 1)
                                        </button>
                                    </p>
                                }
                            </td>

                        }

                        <td>
                            @if (SearchData.CurrentPage < SearchData.PageCount - 1)
                            {

                                <p class="pageButton">
                                    @{
                                        var p1 = SearchData.PageCount - 1;
                                        var next = "next";
                                    }
                                    <button class="btn btn-link"
                                            @onclick="@(e => SearchPager(next, SearchData.SearchText))">
                                        >
                                    </button>
                                </p>
                            }
                            else
                            {
                                <p class="pageButtonDisabled">&gt;</p>
                            }
                        </td>

                        <td>
                            @if (SearchData.CurrentPage < SearchData.PageCount - 1)
                            {
                                <p class="pageButton">
                                    @{var p7 = SearchData.PageCount - 1;}
                                    <button class="btn btn-link"
                                            @onclick="@(e => SearchPager(p7.ToString(), SearchData.SearchText))">
                                        >|
                                    </button>
                                </p>
                            }
                            else
                            {
                                <p class="pageButtonDisabled">&gt;|</p>
                            }
                        </td>
                    </tr>
                </table>
            }
        </div>
   
    </div>

</div>

The code behind implements the OnInitializedAsync so the search can be started from a simple GET using query string parameters. The Search method uses the UI values and sends the API call requests to the APIs in the Blazor server project.

@code {

    private bool Loading { get; set; } = false;
    private SearchDataDto SearchData { get; set; } = new SearchDataDto();

    private int PageNo { get; set; }

    protected override async Task OnInitializedAsync()
    {
        var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
        if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("paging", out var queryParamPaging))
        {
            SearchData.Paging = queryParamPaging;
        }
        if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("SearchText", out var queryParamSearchText))
        {
            SearchData.SearchText = queryParamSearchText;
        }

        if (!string.IsNullOrEmpty(queryParamSearchText) ||
            !string.IsNullOrEmpty(queryParamPaging))
        {
            await Search();
        }
    }

    private async Task SearchPager(string paging, string searchText)
    {
        SearchData.Paging = paging.ToString();
        SearchData.SearchText = searchText;
        await Search();
    }

    private async Task Search()
    {
        Loading = true;
        int page;

        switch (SearchData.Paging)
        {
            case "prev":
                page = PageNo - 1;
                break;

            case "next":
                page = PageNo + 1;
                break;

            default:
                page = int.Parse(SearchData.Paging);
                break;
        }

        int leftMostPage = SearchData.LeftMostPage;

        var searchData = new SearchDataDto
        {
            SearchText = SearchData.SearchText,
            CurrentPage = SearchData.CurrentPage,
            PageCount = SearchData.PageCount,
            LeftMostPage = SearchData.LeftMostPage,
            PageRange = SearchData.PageRange,
            Paging = SearchData.Paging
        };

        var response = await Http.PostAsJsonAsync<SearchDataDto>("api/SearchPaging/Paging", searchData);
        response.EnsureSuccessStatusCode();
        string responseBody = await response.Content.ReadAsStringAsync();

        var searchDataResult = System.Text.Json.JsonSerializer.Deserialize<SearchDataDto>(responseBody);

        PageNo = page;
        SearchData = searchDataResult;
        Loading = false;
    }

}

The Blazor Search Admin view can create or delete the index. Data can be added and the status of the index is displayed. The UI is implemented using Bootstrap 4 css.

@page "/searchadmin"
@using BlazorAzureSearch.Shared
@inject HttpClient Http

<div class="jumbotron jumbotron-fluid">
    <div class="container">
        <h1 class="display-4">Index: @IndexName</h1>
        <p class="lead">Exists: <span class="badge badge-secondary">@IndexExists</span>  Documents Count: <span class="badge badge-light">@DocumentCount</span> </p>
    </div>
</div>

@if (Loading)
{
    <div class="spinner d-flex align-items-center justify-content-center fixedSpinner">
        <div class="spinner-border text-success" role="status">
            <span class="sr-only">Loading...</span>
        </div>
    </div>
}

<div class="card-deck">
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">Create index: @IndexName</h5>
            <p class="card-text">Click to create a new index in Azure Cognitive search called @IndexName.</p>
        </div>
        <div class="card-footer text-center">
            <button class="btn btn-primary col-sm-6" @onclick="CreateIndex">
                Create
            </button>
        </div>
    </div>
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">Add Documents to index: @IndexName</h5>
            <p class="card-text">Add documents to the Azure Cognitive search index: @IndexName.</p>
        </div>
        <div class="card-footer text-center">
            <button class="btn btn-primary col-sm-6" @onclick="AddData">
                Add
            </button>
        </div>
    </div>
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">Delete index: @IndexName</h5>
            <p class="card-text">Delete Azure Cognitive search index: @IndexName.</p>
        </div>
        <div class="card-footer text-center">
            <button type="submit" class="btn btn-danger col-sm-6" @onclick="DeleteIndex">
                Delete
            </button>
        </div>
    </div>
</div>

<br />

@if (Messages != null)
{
    @foreach (var msg in Messages)
    {
        <div class="alert alert-@msg.AlertType alert-dismissible fade show" role="alert">
            <strong>@msg.AlertTitle</strong> @msg.AlertMessage
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
    }
}

The OnInitializedAsync method gets the status of the index. The other methods are used to prepare the data and forward the calls to the Server API.

@code {
    private bool Loading { get; set; } = false;
    private List<AlertViewModel> Messages = null;
    private string IndexName { get; set; } = "personcities";
    private bool IndexExists { get; set; }
    private long DocumentCount { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Console.WriteLine("On Init");

        Loading = true;
        var status = await Http.GetFromJsonAsync<IndexStatus>("api/SearchAdmin/IndexStatus");
        IndexExists = status.IndexExists;
        DocumentCount = status.DocumentCount;
        Loading = false;
    }

    private async Task DeleteIndex()
    {
        Loading = true;
        var response = await Http.PostAsJsonAsync<string>("api/SearchAdmin/DeleteIndex", IndexName);
        response.EnsureSuccessStatusCode();
        string responseBody = await response.Content.ReadAsStringAsync();

        var deleteIndex = System.Text.Json.JsonSerializer.Deserialize<IndexResult>(responseBody);

        Messages = deleteIndex.Messages;
        if (Messages.Count > 0 && Messages[0].AlertType == "success")
        {
            IndexExists = deleteIndex.Status.IndexExists;
            DocumentCount = deleteIndex.Status.DocumentCount;
        }
        Loading = false;
        Console.WriteLine($"DocumentCount: {DocumentCount}");
    }

    private async Task AddData()
    {
        Loading = true;
        var response = await Http.PostAsJsonAsync<string>("api/SearchAdmin/AddData", IndexName);
        response.EnsureSuccessStatusCode();
        string responseBody = await response.Content.ReadAsStringAsync();

        var addData = System.Text.Json.JsonSerializer.Deserialize<IndexResult>(responseBody);

        Messages = addData.Messages;
        if (Messages.Count > 0 && Messages[0].AlertType == "success")
        {
            IndexExists = addData.Status.IndexExists;
            DocumentCount = addData.Status.DocumentCount;
        }
        Loading = false;
        Console.WriteLine($"DocumentCount: {DocumentCount}");
    }

    private async Task CreateIndex()
    {
        try
        {
            Loading = true;
            var response = await Http.PostAsJsonAsync<string>("api/SearchAdmin/CreateIndex", IndexName);
            response.EnsureSuccessStatusCode();
            string responseBody = await response.Content.ReadAsStringAsync();

            var createIndex = System.Text.Json.JsonSerializer.Deserialize<IndexResult>(responseBody);

            Messages = createIndex.Messages;
            if (Messages.Count > 0 && Messages[0].AlertType == "success")
            {
                IndexExists = createIndex.Status.IndexExists;
                DocumentCount = createIndex.Status.DocumentCount;
            }
        }
        finally
        {
            Loading = false;
            Console.WriteLine($"DocumentCount: {DocumentCount}");
        }
    }
}

When the application is run, the application search works by clicking the search button. I haven’t figured out how to bind an onenter event in an input text box in Blazor but this should be pretty easy.

The Search Admin view looks as follows and can edit the data as required.

Running the code yourself

To try this yourself, just clone the github repo and create an Azure Cognitive Search service. Add your keys to the user secrets in the Server project and test away. I’m pretty new to Blazor, so send your PRs or add issues if you see ways of improving this.

{
  "PersonCitiesSearchUri": "--url--",
  "PersonCitiesSearchApiKey": "--secret--",
  "PersonCitiesIndexName": "personcities"
}

Links

https://docs.microsoft.com/en-us/aspnet/core/blazor

https://docs.microsoft.com/en-us/azure/search

https://docs.microsoft.com/en-us/azure/search/search-what-is-azure-search

https://docs.microsoft.com/en-us/rest/api/searchservice/

https://github.com/Azure-Samples/azure-search-dotnet-samples/

https://channel9.msdn.com/Shows/AI-Show/Azure-Cognitive-Search-Deep-Dive-with-Debug-Sessions

https://channel9.msdn.com/Shows/AI-Show/Azure-Cognitive-Search-Whats-new-in-security

https://azure.github.io/azure-sdk/releases/latest/index.html

https://chrissainty.com/working-with-query-strings-in-blazor/

2 comments

  1. […] Implement a Blazor full text search using Azure Cognitive Search (Damien Bowden) […]

  2. […] Implement a Blazor full text search using Azure Cognitive Search – Damien Bowden […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: