In the previous lessons, you added search to a static web app. This lesson highlights the essential steps that establish integration. If you're looking for a cheat sheet on how to integrate search into your web app, this article explains what you need to know.
Azure SDK Azure.Search.Documents
The Function app uses the Azure SDK for Azure AI Search:
The function app authenticates through the SDK to the cloud-based Azure AI Search API using your resource name, resource key, and index name. The secrets are stored in the static web app settings and pulled in to the function as environment variables.
The Search API takes a search term and searches across the documents in the search index, returning a list of matches. Through the Suggest API, partial strings are sent to the search engine as the user types, suggesting search terms such as book titles and authors across the documents in the search index, and returning a small list of matches.
The Azure function pulls in the search configuration information, and fulfills the query.
The search suggester, sg, is defined in the schema file used during bulk upload.
C#
using Azure;
using Azure.Core.Serialization;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using WebSearch.Models;
using SearchFilter = WebSearch.Models.SearchFilter;
namespaceWebSearch.Function
{
publicclassSearch
{
privatestaticstring searchApiKey = Environment.GetEnvironmentVariable("SearchApiKey", EnvironmentVariableTarget.Process);
privatestaticstring searchServiceName = Environment.GetEnvironmentVariable("SearchServiceName", EnvironmentVariableTarget.Process);
privatestaticstring searchIndexName = Environment.GetEnvironmentVariable("SearchIndexName", EnvironmentVariableTarget.Process) ?? "good-books";
privatereadonly ILogger<Lookup> _logger;
publicSearch(ILogger<Lookup> logger)
{
_logger = logger;
}
[Function("search")]
publicasync Task<HttpResponseData> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
FunctionContext executionContext)
{
string requestBody = awaitnew StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize<RequestBodySearch>(requestBody);
// Azure AI Search
Uri serviceEndpoint = new($"https://{searchServiceName}.search.windows.net/");
SearchClient searchClient = new(
serviceEndpoint,
searchIndexName,
new AzureKeyCredential(searchApiKey)
);
SearchOptions options = new()
{
Size = data.Size,
Skip = data.Skip,
IncludeTotalCount = true,
Filter = CreateFilterExpression(data.Filters)
};
options.Facets.Add("authors");
options.Facets.Add("language_code");
SearchResults<SearchDocument> searchResults = searchClient.Search<SearchDocument>(data.SearchText, options);
var facetOutput = new Dictionary<string, IList<FacetValue>>();
foreach (var facetResult in searchResults.Facets)
{
facetOutput[facetResult.Key] = facetResult.Value
.Select(x => new FacetValue { value = x.Value.ToString(), count = x.Count })
.ToList();
}
// Data to return var output = new SearchOutput
{
Count = searchResults.TotalCount,
Results = searchResults.GetResults().ToList(),
Facets = facetOutput
};
var response = req.CreateResponse(HttpStatusCode.Found);
// Serialize datavar serializer = new JsonObjectSerializer(
new JsonSerializerOptions(JsonSerializerDefaults.Web));
await response.WriteAsJsonAsync(output, serializer);
return response;
}
publicstaticstringCreateFilterExpression(List<SearchFilter> filters)
{
if (filters isnullor { Count: <= 0 })
{
returnnull;
}
List<string> filterExpressions = new();
List<SearchFilter> authorFilters = filters.Where(f => f.field == "authors").ToList();
List<SearchFilter> languageFilters = filters.Where(f => f.field == "language_code").ToList();
List<string> authorFilterValues = authorFilters.Select(f => f.value).ToList();
if (authorFilterValues.Count > 0)
{
string filterStr = string.Join(",", authorFilterValues);
filterExpressions.Add($"{"authors"}/any(t: search.in(t, '{filterStr}', ','))");
}
List<string> languageFilterValues = languageFilters.Select(f => f.value).ToList();
foreach (varvaluein languageFilterValues)
{
filterExpressions.Add($"language_code eq '{value}'");
}
returnstring.Join(" and ", filterExpressions);
}
}
}
Client: Search from the catalog
Call the Azure Function in the React client with the following code.
C#
import React, { useEffect, useState, Suspense } from'react';
import axios from'../../axios.js';
import CircularProgress from'@mui/material/CircularProgress';
import { useLocation, useNavigate } from"react-router-dom";
import Results from'../../components/Results/Results';
import Pager from'../../components/Pager/Pager';
import Facets from'../../components/Facets/Facets';
import SearchBar from'../../components/SearchBar/SearchBar';
import "./Search.css";
export default function Search() {
let location = useLocation();
const navigate = useNavigate();
const [ results, setResults ] = useState([]);
const [ resultCount, setResultCount ] = useState(0);
const [ currentPage, setCurrentPage ] = useState(1);
const [ q, setQ ] = useState(new URLSearchParams(location.search).get('q') ?? "*");
const [ top ] = useState(new URLSearchParams(location.search).get('top') ?? 8);
const [ skip, setSkip ] = useState(new URLSearchParams(location.search).get('skip') ?? 0);
const [ filters, setFilters ] = useState([]);
const [ facets, setFacets ] = useState({});
const [ isLoading, setIsLoading ] = useState(true);
let resultsPerPage = top;
useEffect(() => {
setIsLoading(true);
setSkip((currentPage-1) * top);
const body = {
q: q,
top: top,
skip: skip,
filters: filters
};
axios.post( '/api/search', body)
.then(response => {
console.log(JSON.stringify(response.data))
setResults(response.data.results);
setFacets(response.data.facets);
setResultCount(response.data.count);
setIsLoading(false);
} )
.catch(error => {
console.log(error);
setIsLoading(false);
});
}, [q, top, skip, filters, currentPage]);
// pushing the new search term to history when q is updated// allows the back button to work as expected when coming back from the details page
useEffect(() => {
navigate('/search?q=' + q);
setCurrentPage(1);
setFilters([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [q]);
let postSearchHandler = (searchTerm) => {
//console.log(searchTerm);
setQ(searchTerm);
}
var body;
if (isLoading) {
body = (
<div className="col-md-9">
<CircularProgress />
</div>);
} else {
body = (
<div className="col-md-9">
<Results documents={results} top={top} skip={skip} count={resultCount} query={q}></Results>
<Pager className="pager-style" currentPage={currentPage} resultCount={resultCount} resultsPerPage={resultsPerPage} setCurrentPage={setCurrentPage}></Pager>
</div>
)
}
// filters should be applied across entire result set, // not just within the current pageconst updateFilterHandler = (newFilters) => {
// Reset paging
setSkip(0);
setCurrentPage(1);
// Set filters
setFilters(newFilters);
};
return (
<main className="main main--search container-fluid">
<div className="row">
<div className="search-bar-column col-md-3">
<div className="search-bar">
<SearchBar postSearchHandler={postSearchHandler} query={q}></SearchBar>
</div>
<Facets facets={facets} filters={filters} setFilters={updateFilterHandler}></Facets>
</div>
{body}
</div>
</main>
);
}
Client: Suggestions from the catalog
The Suggest function API is called in the React app at \client\src\components\SearchBar\SearchBar.js as part of the Material UI's Autocomplete component. This component uses the input text to search for authors and books that match the input text then displays those possible matches at selectable items in the drop-down list.