Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Note
Azure AI Zoeken is beschikbaar via de Azure-portal, REST API's en Azure-SDK's. Het vormt ook een basis voor Foundry IQ, de beheerde kennislaag die bedrijfsinhoud transformeert in herbruikbare, machtigingsbewuste knowledge bases voor agents in de Microsoft Foundry-portal.
In de vorige stap hebt u de website met zoekmogelijkheden geïmplementeerd om te Azure Container Apps. In dit artikel worden de essentiële stappen beschreven waarmee zoekintegratie tot stand wordt gebracht. U kunt het zien als een cheatsheet voor het integreren van zoekopdrachten in uw web-app.
Azure SDK Azure. Search.Documents
De API maakt gebruik van de Azure SDK voor Azure AI Zoeken:
- NuGet: Azure. Search.Documents
- Referentiedocumentatie: Clientbibliotheek
De API wordt geverifieerd via de SDK voor de cloudgebaseerde Azure AI Zoeken-API met behulp van de naam en indexnaam van de zoekservice. In Azure Container Apps biedt de containeromgeving de configuratiewaarden. Beheerde identiteit is het standaardreferentiepad.
Verificatie van beheerde identiteit
Elke Azure functie in de API maakt de SearchClient functie via een gedeelde SearchClientFactory klasse, zodat elke functie op dezelfde manier wordt geverifieerd. De fabriek bouwt standaard een DefaultAzureCredential en gebruikt deze om tokens voor Azure AI Zoeken aan te vragen. In Azure Container Apps DefaultAzureCredential wordt omgezet in de beheerde identiteit die is toegewezen aan de container-app.
Met de volgende methode wordt SearchClientFactory.cs die referentie gemaakt. Wanneer de container-app een door de gebruiker toegewezen beheerde identiteit heeft, wordt de client-id van de AZURE_CLIENT_ID omgevingsvariabele doorgegeven aan DefaultAzureCredentialOptions zodat tokenverwerving niet dubbelzinnig is.
private static DefaultAzureCredential CreateManagedIdentityCredential()
{
var options = new DefaultAzureCredentialOptions();
if (!string.IsNullOrWhiteSpace(ManagedIdentityClientId))
{
options.ManagedIdentityClientId = ManagedIdentityClientId;
}
return new DefaultAzureCredential(options);
}
De Bicep-infrastructuur wijst de beheerde identiteit tijdens het Azure AI Zoeken gegevensvlak azd uptoe. Met deze roltoewijzing kan de API de good-books index opvragen zonder een querysleutel op te slaan in de containeromgeving.
Lokale versus geïmplementeerde referentieomzetting
Lokaal, als AZURE_CLIENT_ID dit niet isset, DefaultAzureCredential valt u terug via de standaardreferentieketen en wordt deze omgezet in uw aanmeldingsgegevens voor ontwikkelaars, zoals de Azure CLI of Visual Studio Code account dat u hebt gebruikt om u aan te melden. Wanneer deze wordt geïmplementeerd in Azure Container Apps, wordt de Bicep-infrastructuur ingesteld AZURE_CLIENT_ID op de client-id van de door de gebruiker toegewezen beheerde identiteit, zodat DefaultAzureCredential deze identiteit specifiek is gericht op het oplossen van dubbelzinnige identiteiten tussen meerdere identiteiten die een host beschikbaar kan maken.
Als u in plaats daarvan API-sleutels wilt gebruiken, stelt u deze in op USE_KEYLESS_AUTHfalse vóór de implementatie:
azd env set USE_KEYLESS_AUTH false
azd up
Gebruik sleutelverificatie alleen wanneer uw omgeving dit vereist.
Instellingen voor lokale ontwikkeling
Voor lokale ontwikkeling toont het voorbeeldbestand sample.local.settings.json de waarden die de API verwacht. Gebruik alleen lokale instellingen voor ontwikkeling. In Azure Container Apps biedt de implementatieconfiguratie de equivalente waarden voor de containeromgeving.
| Setting | Purpose | Vereist wanneer |
|---|---|---|
SearchServiceName |
Naam van de Azure AI Zoeken-service. Combineert met .search.windows.net het bouwen van de service-eindpunt-URI. |
Altijd |
SearchIndexName |
Naam van de zoekindex die moet worden opzocht. Wordt standaard ingesteld good-books als deze niet is ingesteld. |
Optional |
SEARCH_USE_KEY_AUTH |
De standaardwaarde is onwaar, maakt gebruik van een beheerde identiteit. Ingesteld op true het gebruik van een API-sleutel in plaats van een beheerde identiteit. |
Optionele sleutelverificatie |
SearchApiKey |
Beheerderssleutel voor Azure AI Zoeken. | Verplicht wanneer SEARCH_USE_KEY_AUTHtrue is |
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SearchServiceName": "",
"SearchIndexName": "good-books"
},
"Host": {
"CORS": "*"
}
}
Functie: De catalogus doorzoeken
De zoek-API gebruikt een zoekterm en zoekt in de documenten in de zoekindex en retourneert een lijst met overeenkomsten. Via de Suggestie-API worden gedeeltelijke tekenreeksen naar de zoekmachine verzonden als de gebruikerstypen. De API stelt zoektermen voor, zoals boektitels en auteurs, op basis van documenten in de zoekindex en retourneert een kleine lijst met overeenkomsten.
De Azure functie haalt de zoekconfiguratiegegevens op uit de containeromgeving, maakt de Azure AI Zoeken-client en voldoet aan de query.
De zoeksugger, sgwordt gedefinieerd in het schemabestand dat wordt gebruikt tijdens het bulksgewijs uploaden.
using Azure;
using Azure.Core.Serialization;
using Azure.Identity;
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;
namespace WebSearch.Function
{
public class Search
{
private readonly ILogger<Lookup> _logger;
public Search(ILogger<Lookup> logger)
{
_logger = logger;
}
[Function("search")]
public async Task<HttpResponseData> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
FunctionContext executionContext)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize<RequestBodySearch>(requestBody);
// Azure AI Search (managed identity by default; API key only when SEARCH_USE_KEY_AUTH=true)
SearchClient searchClient = SearchClientFactory.CreateSearchClient();
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 data
var serializer = new JsonObjectSerializer(
new JsonSerializerOptions(JsonSerializerDefaults.Web));
await response.WriteAsJsonAsync(output, serializer);
return response;
}
public static string CreateFilterExpression(List<SearchFilter> filters)
{
if (filters is null or { Count: <= 0 })
{
return null;
}
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 (var value in languageFilterValues)
{
filterExpressions.Add($"language_code eq '{value}'");
}
return string.Join(" and ", filterExpressions);
}
}
}
Als u de functie onafhankelijk wilt controleren, roept /api/search u een zoekterm aan in de hoofdtekst van de aanvraag en bevestigt u dat het antwoord overeenkomende boekdocumenten, een totaalaantal en facetwaarden bevat.
Client: Zoeken in de catalogus
De zoekpagina van de React-client roept de search Azure-functie aan wanneer de gebruiker een query invoert, een facetfilter wijzigt of naar een nieuwe pagina met resultaten gaat. De client verzendt de zoektekst, de huidige pagina's skip en waarden en top eventuele geselecteerde auteurs- of taalfilters in de POST-hoofdtekst naar /api/search. De functie retourneert een lijst met overeenkomende boekdocumenten, een totaalaantal en facetwaarden, die de pagina gebruikt om de resultatenlijst, pager en facetfilters weer te geven. De volgende code in \client\src\pages\Search\Search.jsx builds die aanvraag aanvraagt en het antwoord opslaat in de componentstatus:
import React, { useEffect, useState, Suspense } from 'react';
import fetchInstance from '../../url-fetch';
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;
// Handle page changes in a controlled manner
function handlePageChange(newPage) {
setCurrentPage(newPage);
}
// Calculate skip value and fetch results when relevant parameters change
useEffect(() => {
// Calculate skip based on current page
const calculatedSkip = (currentPage - 1) * top;
// Only update if skip has actually changed
if (calculatedSkip !== skip) {
setSkip(calculatedSkip);
return; // Skip the fetch since skip will change and trigger another useEffect
}
// Proceed with fetch
setIsLoading(true);
const body = {
q: q,
top: top,
skip: skip,
filters: filters
};
fetchInstance('/api/search', { body, method: 'POST' })
.then(response => {
setResults(response.results);
setFacets(response.facets);
setResultCount(response.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) => {
setQ(searchTerm);
}
// filters should be applied across entire result set,
// not just within the current page
const 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-column-container">
<SearchBar postSearchHandler={postSearchHandler} query={q} width={false}></SearchBar>
</div>
<Facets facets={facets} filters={filters} setFilters={updateFilterHandler}></Facets>
</div>
<div className="search-bar-results">
{isLoading ? (
<div className="col-md-9">
<CircularProgress />
</div>
) : (
<div className="search-results-container">
<Results documents={results} top={top} skip={skip} count={resultCount} query={q}></Results>
<Pager className="pager-style" currentPage={currentPage} resultCount={resultCount} resultsPerPage={resultsPerPage} onPageChange={handlePageChange}></Pager>
</div>
)}
</div>
</div>
</main>
);
}
Als u deze integratie wilt controleren, voert u een zoekterm in de zoekbalk van de website in en bevestigt u dat de resultatenlijst, het aantal resultaten en alle facetten worden bijgewerkt.
Client: Suggesties uit de catalogus
De Functie-API Suggest wordt aangeroepen in de React-app \client\src\components\SearchBar\SearchBar.jsx als onderdeel van het onderdeel Voor de materiaalgebruikersinterface automatisch aanvullen. Dit onderdeel gebruikt de invoertekst om te zoeken naar auteurs en boeken die overeenkomen. Vervolgens worden deze mogelijke overeenkomsten weergegeven als selecteerbare items in de vervolgkeuzelijst.
import React, { useState, useEffect } from 'react';
import { TextField, Autocomplete, Button, Box } from '@mui/material';
import fetchInstance from '../../url-fetch';
import './SearchBar.css';
export default function SearchBar({ postSearchHandler, query, width }) {
const [q, setQ] = useState(() => query || '');
const [suggestions, setSuggestions] = useState([]);
const search = (value) => {
postSearchHandler(value);
};
useEffect(() => {
if (q) {
const body = { q, top: 5, suggester: 'sg' };
fetchInstance('/api/suggest', { body, method: 'POST' })
.then(response => {
setSuggestions(response.suggestions.map(s => s.text));
})
.catch(error => {
console.log(error);
setSuggestions([]);
});
}
}, [q]);
const onInputChangeHandler = (event, value) => {
setQ(value);
};
const onChangeHandler = (event, value) => {
setQ(value);
search(value);
};
const onEnterButton = (event) => {
// if enter key is pressed
if (event.key === 'Enter') {
search(q);
}
};
return (
<div
className={width ? "search-bar search-bar-wide" : "search-bar search-bar-narrow"}
>
<Box className="search-bar-box">
<Autocomplete
className="autocomplete"
freeSolo
value={q}
options={suggestions}
onInputChange={onInputChangeHandler}
onChange={onChangeHandler}
disableClearable
renderInput={(params) => (
<TextField
{...params}
id="search-box"
className="form-control rounded-0"
placeholder="What are you looking for?"
onBlur={() => setSuggestions([])}
onClick={() => setSuggestions([])}
onKeyDown={onEnterButton}
/>
)}
/>
<div className="search-button" >
<Button variant="contained" color="primary" onClick={() => {
search(q)
}
}>
Search
</Button>
</div>
</Box>
</div>
);
}
Als u deze integratie wilt controleren, voert u tekst in de zoekbalk van de website in en bevestigt u dat overeenkomende boektitels en auteurs worden weergegeven in de vervolgkeuzelijst voor automatisch aanvullen.
Functie: Specifiek document ophalen
Met de Documentzoek-API wordt het volledige document voor één boek opgehaald nadat een gebruiker het heeft geselecteerd in de zoekresultaten. De functie leest een boek id uit de querytekenreeks van de aanvraag, gebruikt SearchClientFactory om een geverifieerde SearchClientquery te maken en aanroepen GetDocumentAsync om die sleutel in de good-books index op te zoeken. Hiermee wordt het resulterende document geretourneerd dat in een LookupOutput object is verpakt.
using Azure;
using Azure.Core.Serialization;
using Azure.Identity;
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 WebSearch.Models;
namespace WebSearch.Function
{
public class Lookup
{
private readonly ILogger<Lookup> _logger;
public Lookup(ILogger<Lookup> logger)
{
_logger = logger;
}
[Function("lookup")]
public async Task<HttpResponseData> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
FunctionContext executionContext)
{
// Get Document Id
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
string documentId = query["id"].ToString();
// Azure AI Search (managed identity by default; API key only when SEARCH_USE_KEY_AUTH=true)
SearchClient searchClient = SearchClientFactory.CreateSearchClient();
var getDocumentResponse = await searchClient.GetDocumentAsync<SearchDocument>(documentId);
// Data to return
var output = new LookupOutput
{
Document = getDocumentResponse.Value
};
var response = req.CreateResponse(HttpStatusCode.Found);
// Serialize data
var serializer = new JsonObjectSerializer(
new JsonSerializerOptions(JsonSerializerDefaults.Web));
await response.WriteAsJsonAsync(output, serializer);
return response;
}
}
}
Als u de functie Opzoeken onafhankelijk wilt controleren, roept /api/lookup u een geldig boek id aan en bevestigt u dat het volledige document van dat boek wordt geretourneerd.
Client: Specifiek document ophalen
Wanneer een gebruiker een boek selecteert in de zoekresultaten, heeft de pagina Details het volledige document voor dat boek nodig, inclusief velden die niet worden weergegeven in de overzichtslijst. De pagina Details leest het boek id uit de routeparameters en roept de Documentzoek-API aan /api/lookup wanneer het onderdeel wordt toegevoegd. Het geretourneerde document wordt opgeslagen in de onderdeelstatus en wordt weergegeven op de tabbladen Resultaat en Onbewerkte gegevens . Met de volgende code wordt \client\src\pages\Details\Details.jsx deze zoekopdracht uitgevoerd tijdens de initialisatie van onderdelen:
import React, { useState, useEffect } from "react";
import { useParams } from 'react-router-dom';
import Rating from '@mui/material/Rating';
import CircularProgress from '@mui/material/CircularProgress';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
import fetchInstance from '../../url-fetch';
import "./Details.css";
function CustomTabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
className="tab-panel"
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
// Ensure it takes full width
>
{value === index && <Box className="tab-panel-value">{children}</Box>}
</div>
);
}
export default function BasicTabs() {
const { id } = useParams();
const [document, setDocument] = useState({});
const [value, setValue] = React.useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetchInstance('/api/lookup', { query: { id } })
.then(response => {
console.log(JSON.stringify(response))
const doc = response.document;
setDocument(doc);
setIsLoading(false);
})
.catch(error => {
console.log(error);
setIsLoading(false);
});
}, [id]);
const handleChange = (event, newValue) => {
setValue(newValue);
};
if (isLoading || !id || Object.keys(document).length === 0) {
return (
<div className="loading-container">
<CircularProgress />
<p>Loading...</p>
</div>
);
}
return (
<Box className="details-box-parent">
<Box className="details-tab-box-header">
<Tabs value={value} onChange={handleChange} aria-label="book-details-tabs">
<Tab label="Result" />
<Tab label="Raw Data" />
</Tabs>
</Box>
<CustomTabPanel value={value} index={0} className="tab-panel box-content">
<div className="card-body">
<h5 className="card-title">{document.original_title}</h5>
<img className="image" src={document.image_url} alt="Book cover"></img>
<p className="card-text">{document.authors?.join('; ')} - {document.original_publication_year}</p>
<p className="card-text">ISBN {document.isbn}</p>
<Rating name="half-rating-read" value={parseInt(document.average_rating)} precision={0.1} readOnly></Rating>
<p className="card-text">{document.ratings_count} Ratings</p>
</div>
</CustomTabPanel>
<CustomTabPanel value={value} index={1} className="tab-panel">
<div className="card-body text-left card-text details-custom-tab-panel-json-div" >
<pre><code>
{JSON.stringify(document, null, 2)}
</code></pre>
</div>
</CustomTabPanel>
</Box>
);
}
Als u deze integratie wilt controleren, selecteert u een boek in de zoekresultaten en bevestigt u dat de details, inclusief voorafbeelding, auteurs en classificatie, worden weergegeven op de pagina Details.
C#-modellen die ondersteuning bieden voor de API
De Azure Functions-API en het project voor bulksgewijs importeren delen een set C#-modelklassen. Deze klassen definiëren de aanvraaginstanties die de client verzendt, zoals zoektekst, wisselwaarden en filters. Ze definiëren ook de antwoordvormen die de client verwacht, zoals zoekresultaten, facetwaarden en één opgezocht document. Door deze modellen in één bestand te bewaren, zorgt u ervoor dat de zoek-, suggestie- en documentzoekeindpunten consistent blijven met de verwachtingen van de React-client. De volgende modellen, gedefinieerd in Models.cs, ondersteunen de functies in deze app:
using Azure.Search.Documents.Models;
using System.Text.Json.Serialization;
namespace WebSearch.Models
{
public class RequestBodyLookUp
{
[JsonPropertyName("id")]
public string Id { get; set; }
}
public class RequestBodySuggest
{
[JsonPropertyName("q")]
public string SearchText { get; set; }
[JsonPropertyName("top")]
public int Size { get; set; }
[JsonPropertyName("suggester")]
public string SuggesterName { get; set; }
}
public class RequestBodySearch
{
[JsonPropertyName("q")]
public string SearchText { get; set; }
[JsonPropertyName("skip")]
public int Skip { get; set; }
[JsonPropertyName("top")]
public int Size { get; set; }
[JsonPropertyName("filters")]
public List<SearchFilter> Filters { get; set; }
}
public class SearchFilter
{
public string field { get; set; }
public string value { get; set; }
}
public class FacetValue
{
public string value { get; set; }
public long? count { get; set; }
}
class SearchOutput
{
[JsonPropertyName("count")]
public long? Count { get; set; }
[JsonPropertyName("results")]
public List<SearchResult<SearchDocument>> Results { get; set; }
[JsonPropertyName("facets")]
public Dictionary<String, IList<FacetValue>> Facets { get; set; }
}
class LookupOutput
{
[JsonPropertyName("document")]
public SearchDocument Document { get; set; }
}
public class BookModel
{
public string id { get; set; }
public decimal? goodreads_book_id { get; set; }
public decimal? best_book_id { get; set; }
public decimal? work_id { get; set; }
public decimal? books_count { get; set; }
public string isbn { get; set; }
public string isbn13 { get; set; }
public string[] authors { get; set; }
public decimal? original_publication_year { get; set; }
public string original_title { get; set; }
public string title { get; set; }
public string language_code { get; set; }
public double? average_rating { get; set; }
public decimal? ratings_count { get; set; }
public decimal? work_ratings_count { get; set; }
public decimal? work_text_reviews_count { get; set; }
public decimal? ratings_1 { get; set; }
public decimal? ratings_2 { get; set; }
public decimal? ratings_3 { get; set; }
public decimal? ratings_4 { get; set; }
public decimal? ratings_5 { get; set; }
public string image_url { get; set; }
public string small_image_url { get; set; }
}
}
Volgende stap
Als u meer wilt weten over Azure AI Zoeken ontwikkeling, volgt u deze volgende zelfstudie over indexering: