Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Note
Azure AI Vyhledávač je k dispozici prostřednictvím portálu Azure, rozhraní REST API a Sady Azure SDK. Podporuje také Foundry IQ, spravovanou znalostní vrstvu, která transformuje podnikový obsah na opakovaně použitelné znalostní báze s podporou oprávnění pro agenty na portálu Microsoft Foundry.
V předchozím kroku jste nasadili web s podporou vyhledávání do Azure Container Apps. Tento článek popisuje základní kroky, které vytvářejí integraci vyhledávání. Představte si to jako tahák pro integraci vyhledávání do webové aplikace.
Azure SDK Azure. Search.Documents
Rozhraní API používá Azure SDK pro Azure AI Vyhledávač:
- NuGet: Azure. Search.Documents
- Referenční dokumentace: Klientská knihovna
Rozhraní API se ověřuje prostřednictvím sady SDK v cloudovém Azure AI Vyhledávač API pomocí názvu vyhledávací služby a názvu indexu. V Azure Container Apps poskytuje prostředí kontejneru hodnoty konfigurace. Spravovaná identita je výchozí cesta k přihlašovacím údajům.
Ověřování spravovaných identit
Každá Azure funkce v rozhraní API vytvoří svou SearchClient prostřednictvím sdílené SearchClientFactory třídy, takže každá funkce se ověřuje stejným způsobem. Ve výchozím nastavení objekt pro vytváření sestaví DefaultAzureCredential tokeny a použije ho k vyžádání tokenů pro Azure AI Vyhledávač. V Azure Container Apps DefaultAzureCredential se přeloží na spravovanou identitu přiřazenou k aplikaci kontejneru.
Následující metoda z SearchClientFactory.cs tohoto pověření vytvoří. Pokud má aplikace kontejneru spravovanou identitu přiřazenou uživatelem, předá DefaultAzureCredentialOptions se ID klienta z AZURE_CLIENT_ID proměnné prostředí, aby získání tokenu nebylo nejednoznačné.
private static DefaultAzureCredential CreateManagedIdentityCredential()
{
var options = new DefaultAzureCredentialOptions();
if (!string.IsNullOrWhiteSpace(ManagedIdentityClientId))
{
options.ManagedIdentityClientId = ManagedIdentityClientId;
}
return new DefaultAzureCredential(options);
}
Infrastruktura Bicep přiřazuje spravované identitě přístup k rovině dat Azure AI Vyhledávač během azd up. Toto přiřazení role umožňuje rozhraní API dotazovat good-books index bez uložení klíče dotazu do prostředí kontejneru.
Místní versus nasazené řešení přihlašovacích údajů
Místně, pokud AZURE_CLIENT_ID je nenasazená sada, DefaultAzureCredential se vrátí do standardního řetězce přihlašovacích údajů a přeloží se na přihlašovací údaje přihlášeného vývojáře, jako je Azure CLI nebo účet Visual Studio Code, který jste použili k přihlášení. Při nasazení do Azure Container Apps se infrastruktura Bicep nastaví AZURE_CLIENT_ID na ID klienta spravované identity přiřazené uživatelem, takže DefaultAzureCredential cílí na tuto identitu konkrétně místo nejednoznačnosti mezi několika identitami, které může hostitel vystavit.
Pokud chcete místo toho použít klíče rozhraní API, nastavte USE_KEYLESS_AUTH před nasazením false :
azd env set USE_KEYLESS_AUTH false
azd up
Ověřování pomocí klíče používejte jenom v případech, kdy ho vaše prostředí vyžaduje.
Místní nastavení vývoje
V případě místního vývoje ukázkový sample.local.settings.json soubor zobrazuje hodnoty, které rozhraní API očekává. Používejte pouze místní nastavení pro vývoj. V Azure Container Apps poskytuje konfigurace nasazení ekvivalentní hodnoty prostředí kontejneru.
| Setting | Purpose | Požadováno v případě, že |
|---|---|---|
SearchServiceName |
Název služby Azure AI Vyhledávač. Kombinuje s sestavením .search.windows.net identifikátoru URI koncového bodu služby. |
Vždy |
SearchIndexName |
Název indexu vyhledávání, který se má dotazovat. Výchozí hodnota je good-books , pokud není nastavená. |
Optional |
SEARCH_USE_KEY_AUTH |
Výchozí hodnota je false, používá spravovanou identitu. Nastavte na true použití klíče rozhraní API místo spravované identity. |
Volitelné ověřování pomocí klíče |
SearchApiKey |
Klíč správce pro Azure AI Vyhledávač | Vyžadováno, když je SEARCH_USE_KEY_AUTHtrue |
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SearchServiceName": "",
"SearchIndexName": "good-books"
},
"Host": {
"CORS": "*"
}
}
Funkce: Vyhledávání v katalogu
Rozhraní API pro vyhledávání přebírá hledaný termín a hledá v dokumentech v indexu vyhledávání a vrací seznam shod. Prostřednictvím rozhraní API pro návrhy se částečné řetězce odesílají do vyhledávacího webu jako typy uživatelů. Rozhraní API navrhuje hledané termíny, jako jsou názvy knih a autoři, na základě dokumentů v indexu vyhledávání a vrací malý seznam shod.
Funkce Azure načítá informace o konfiguraci vyhledávání z prostředí kontejneru, vytvoří klienta Azure AI Vyhledávač a splní dotaz.
Navrhovatel hledání sg je definován v souboru schématu použitém při hromadném nahrání.
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);
}
}
}
Pokud chcete funkci ověřit nezávisle, zavolejte hledaný /api/search termín v textu požadavku a potvrďte, že odpověď obsahuje odpovídající dokumenty knihy, celkový počet a omezující hodnoty.
Klient: Vyhledávání v katalogu
Vyhledávací stránka klienta React volá search funkci Azure pokaždé, když uživatel zadá dotaz, změní filtr omezující vlastnosti nebo se přesune na novou stránku výsledků. Klient odešle hledaný text, hodnoty a top hodnoty aktuální stránky skip a všechny vybrané filtry autora nebo jazyka v textu POST do /api/search. Funkce vrátí seznam odpovídajících dokumentů knihy, celkový počet a omezující vlastnosti, které stránka používá k vykreslení seznamu výsledků, pageru a fasetových filtrů. Následující kód v \client\src\pages\Search\Search.jsx buildech, které požadují a ukládají odpověď ve stavu komponenty:
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>
);
}
Pokud chcete tuto integraci ověřit, zadejte hledaný termín na panelu hledání webu a potvrďte, že se seznam výsledků, počet výsledků a omezující vlastnosti všechny aktualizace aktualizují.
Klient: Návrhy z katalogu
Rozhraní API funkce Navrhnout se volá v aplikaci \client\src\components\SearchBar\SearchBar.jsx React jako součást komponenty Automatické dokončování uživatelského rozhraní materiálu. Tato komponenta používá vstupní text k vyhledávání autorů a knih, které odpovídají. Potom tyto možné shody zobrazí jako položky, které se dají vybrat, v rozevíracím seznamu.
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>
);
}
Pokud chcete tuto integraci ověřit, zadejte text do vyhledávacího panelu webu a ověřte, že se v rozevíracím seznamu automatického dokončování zobrazí odpovídající názvy knih a autoři.
Funkce: Získání konkrétního dokumentu
Rozhraní API pro vyhledávání dokumentů načte celý dokument pro jednu knihu poté, co ho uživatel vybere z výsledků hledání. Funkce čte knihu id z řetězce dotazu požadavku, používá SearchClientFactory k vytvoření ověřeného SearchClientklíče a volání GetDocumentAsync k vyhledání tohoto klíče v indexu good-books . Vrátí výsledný dokument zabalený v objektu LookupOutput .
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;
}
}
}
Pokud chcete funkci Lookup ověřit nezávisle, zavolejte /api/lookup platnou knihu id a potvrďte, že odpověď vrátí celý dokument knihy.
Klient: Získání konkrétního dokumentu
Když uživatel vybere knihu z výsledků hledání, stránka Podrobnosti potřebuje úplný dokument pro danou knihu, včetně polí, která nejsou zobrazena v seznamu souhrnů. Stránka Podrobnosti čte knihu id z parametrů trasy a volá rozhraní API pro vyhledávání dokumentů, když /api/lookup se komponenta připojí. Uloží vrácený dokument do stavu komponenty a vykreslí ho na kartách Výsledek a Nezpracovaná data . Následující kód provede \client\src\pages\Details\Details.jsx toto vyhledávání během inicializace komponent:
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>
);
}
Pokud chcete tuto integraci ověřit, vyberte ve výsledcích hledání knihu a ověřte, že se na stránce Podrobnosti zobrazí její podrobnosti, včetně titulních obrázků, autorů a hodnocení.
Modely jazyka C#, které podporují rozhraní API
Rozhraní AZURE FUNCTIONS API a projekt hromadného importu sdílejí sadu tříd modelů jazyka C#. Tyto třídy definují těla požadavků, které klient odesílá, například hledaný text, stránkovací hodnoty a filtry. Definují také obrazce odpovědí, které klient očekává, například výsledky hledání, omezující vlastnosti a jeden prohledáný dokument. Udržování těchto modelů v jednom souboru zajišťuje, aby hledání, návrhy a koncové body vyhledávání dokumentů zůstaly v souladu s očekáváními klienta React. Následující modely definované v Models.cstéto aplikaci podporují funkce:
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; }
}
}
Další krok
Pokud chcete pokračovat ve studiu Azure AI Vyhledávač vývoje, vyzkoušejte tento další kurz o indexování: