Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Note
Recherche Azure AI est disponible via le portail Azure, les API REST et les SDK Azure. Il sous-tend également Foundry IQ, la couche de connaissances managée qui transforme le contenu d’entreprise en bases de connaissances réutilisables et prenant en charge les autorisations pour les agents dans le portail Microsoft Foundry.
À l’étape précédente, vous avez déployé le site web activé pour la recherche sur Azure Container Apps. Cet article met en évidence les étapes essentielles qui établissent l’intégration de la recherche. Considérez-le comme une aide-mémoire pour intégrer la recherche dans votre application web.
Azure.Search.Documents du SDK Azure
L’API utilise les Kit de développement logiciel (SDK) Azure pour Recherche Azure AI :
- NuGet : Azure.Search.Documents
- Documentation de référence : Bibliothèque cliente
L’API s’authentifie via le Kit de développement logiciel (SDK) à l’API Recherche Azure AI basée sur le cloud à l’aide du nom du service de recherche et du nom d’index. Dans Azure Container Apps, l’environnement de conteneur fournit les valeurs de configuration. L’identité managée est le chemin d’accès d’informations d’identification par défaut.
Authentification d’identité managée
Chaque fonction Azure dans l’API crée son SearchClient par le biais d’une classe partagéeSearchClientFactory, de sorte que chaque fonction s’authentifie de la même façon. Par défaut, la fabrique génère une DefaultAzureCredential fabrique et l’utilise pour demander des jetons pour Recherche Azure AI. Dans Azure Container Apps, DefaultAzureCredential résout l’identité managée affectée à l’application conteneur.
La méthode suivante à partir de SearchClientFactory.cs crée ces informations d’identification. Lorsque l’application conteneur a une identité managée affectée par l’utilisateur, l’ID client de la variable d’environnement est passé pour DefaultAzureCredentialOptions que l’acquisition AZURE_CLIENT_ID de jetons ne soit pas ambiguë.
private static DefaultAzureCredential CreateManagedIdentityCredential()
{
var options = new DefaultAzureCredentialOptions();
if (!string.IsNullOrWhiteSpace(ManagedIdentityClientId))
{
options.ManagedIdentityClientId = ManagedIdentityClientId;
}
return new DefaultAzureCredential(options);
}
L’infrastructure Bicep attribue l’accès d’identité managée au plan de données Recherche Azure AI pendant azd up. Cette attribution de rôle permet à l’API d’interroger l’index good-books sans stocker de clé de requête dans l’environnement de conteneur.
Résolution des informations d’identification locales et déployées
Localement, s’il AZURE_CLIENT_ID n’est pas défini, DefaultAzureCredential revient dans sa chaîne d’informations d’identification standard et se résout par vos informations d’identification de développeur connectées, telles que le compte Azure CLI ou Visual Studio Code que vous avez utilisé pour vous connecter. Lorsqu'elle est déployée sur Azure Container Apps, l'infrastructure Bicep est définie AZURE_CLIENT_ID sur l'ID client de l'identité managée affectée par l'utilisateur, afin DefaultAzureCredential de cibler cette identité spécifiquement au lieu de résoudre ambiguëment entre plusieurs identités qu'un hôte peut exposer.
Pour utiliser des clés API à la place, définissez-les USE_KEYLESS_AUTHfalse avant le déploiement :
azd env set USE_KEYLESS_AUTH false
azd up
Utilisez l’authentification par clé uniquement lorsque votre environnement l’exige.
Paramètres de développement local
Pour le développement local, l’exemple sample.local.settings.json de fichier affiche les valeurs attendues par l’API. Utilisez uniquement les paramètres locaux pour le développement. Dans Azure Container Apps, la configuration du déploiement fournit les valeurs d’environnement de conteneur équivalentes.
| Setting | Purpose | Obligatoire quand |
|---|---|---|
SearchServiceName |
Nom du service Recherche Azure AI. Combine avec .search.windows.net pour générer l’URI du point de terminaison de service. |
Toujours |
SearchIndexName |
Nom de l’index de recherche à interroger. La valeur par défaut est si elle n’est good-books pas définie. |
Optional |
SEARCH_USE_KEY_AUTH |
La valeur par défaut est false, utilise l’identité managée. Définissez cette option pour true utiliser une clé API au lieu d’une identité managée. |
Authentification par clé facultative |
SearchApiKey |
Clé d’administration pour Recherche Azure AI. | Obligatoire lorsque SEARCH_USE_KEY_AUTH est true |
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SearchServiceName": "",
"SearchIndexName": "good-books"
},
"Host": {
"CORS": "*"
}
}
Fonction : rechercher dans le catalogue
L’API Recherche cognitive prend un terme de recherche et effectue la recherche parmi les documents de l’index de recherche, en retournant une liste de correspondances. Par le biais de l’API Suggest, des chaînes partielles sont envoyées au moteur de recherche en tant que types d’utilisateurs. L’API suggère des termes de recherche, tels que des titres de livre et des auteurs, basés sur des documents dans l’index de recherche et retourne une petite liste de correspondances.
La fonction Azure extrait les informations de configuration de recherche à partir de l’environnement conteneur, crée le client Recherche Azure AI et répond à la requête.
Le générateur de suggestions de recherche, sg, est défini dans le fichier de schéma utilisé lors du chargement en bloc.
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);
}
}
}
Pour vérifier la fonction indépendamment, appelez /api/search avec un terme de recherche dans le corps de la requête et confirmez que la réponse inclut des documents de livre correspondants, un nombre total et des valeurs de facette.
Client : Rechercher dans le catalogue
La page De recherche du client React appelle la search fonction Azure chaque fois que l'utilisateur entre une requête, modifie un filtre de facettes ou passe à une nouvelle page de résultats. Le client envoie le texte de recherche, les valeurs et top les valeurs de skip la page active, ainsi que tous les filtres d’auteur ou de langue sélectionnés dans le corps POST à /api/search. La fonction retourne une liste de documents de livres correspondants, un nombre total et des valeurs de facette, que la page utilise pour afficher les filtres de la liste des résultats, du pagineur et des facettes. Le code suivant dans les \client\src\pages\Search\Search.jsx builds qui demande et stocke la réponse dans l’état du composant :
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>
);
}
Pour vérifier cette intégration, entrez un terme de recherche dans la barre de recherche du site web et vérifiez que la liste des résultats, le nombre de résultats et les facettes de toutes les mises à jour.
Client : suggestions à partir du catalogue
L’API de fonction Suggest est appelée dans l’application \client\src\components\SearchBar\SearchBar.jsx React dans le cadre du composant de saisie semi-automatique de l’interface utilisateur matériel. Ce composant utilise le texte d’entrée pour rechercher des auteurs et des livres qui correspondent. Il affiche ensuite ces correspondances possibles comme éléments sélectionnables dans la liste déroulante.
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>
);
}
Pour vérifier cette intégration, entrez du texte dans la barre de recherche du site web et vérifiez que les titres et auteurs correspondants apparaissent dans la liste déroulante de saisie semi-automatique.
Fonction : Obtenir un document spécifique
L’API Recherche de document récupère le document complet d’un seul livre une fois qu’un utilisateur l’a sélectionné dans les résultats de la recherche. La fonction lit un livre id à partir de la chaîne de requête de la requête, utilise SearchClientFactory pour créer une clé authentifiée SearchClientet appelle GetDocumentAsync pour rechercher cette clé dans l’index good-books . Elle retourne le document obtenu encapsulé dans un LookupOutput objet.
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;
}
}
}
Pour vérifier la fonction Lookup indépendamment, appelez /api/lookup avec un livre id valide et confirmez que la réponse retourne le document complet de ce livre.
Client : accéder à un document spécifique
Lorsqu’un utilisateur sélectionne un livre dans les résultats de la recherche, la page Détails a besoin du document complet pour ce livre, y compris les champs non affichés dans la liste récapitulative. La page Détails lit le livre id à partir des paramètres d’itinéraire et appelle l’API Recherche de document lors /api/lookup du montage du composant. Il stocke le document retourné dans l’état du composant et le restitue dans les onglets Résultats et Données brutes . Le code suivant effectue \client\src\pages\Details\Details.jsx cette recherche pendant l’initialisation des composants :
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>
);
}
Pour vérifier cette intégration, sélectionnez un livre dans les résultats de recherche et vérifiez que ses détails, y compris l’image de couverture, les auteurs et l’évaluation, apparaissent sur la page Détails.
Modèles C# qui prennent en charge l’API
L’API Azure Functions et le projet d’importation en bloc partagent un ensemble de classes de modèle C#. Ces classes définissent les corps de requête envoyés par le client, tels que le texte de recherche, les valeurs de pagination et les filtres. Ils définissent également les formes de réponse attendues par le client, telles que les résultats de recherche, les valeurs de facettes et un document à recherche unique. La conservation de ces modèles dans un fichier garantit que les points de terminaison de recherche, de suggestion et de recherche de document restent cohérents avec les attentes du client React. Les modèles suivants, définis dans Models.cs, prennent en charge les fonctions de cette application :
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; }
}
}
Étape suivante
Pour continuer à découvrir Recherche Azure AI développement, essayez ce tutoriel suivant sur l’indexation :