Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Note
Pesquisa de IA do Azure está disponível por meio do portal Azure, APIs REST e SDKs do Azure. Ele também sustenta o IQ do Foundry, a camada de conhecimento gerenciado que transforma o conteúdo da empresa em bases de conhecimento reutilizáveis e com reconhecimento de permissão para agentes no portal do Microsoft Foundry.
Na etapa anterior, você implantou o site habilitado para pesquisa para Aplicativos de Contêiner do Azure. Este artigo destaca as etapas essenciais que estabelecem a integração de pesquisa. Pense nisso como uma folha de referência para integrar a pesquisa em seu aplicativo Web.
SDK do Azure Azure.Search.Documents
A API usa o SDK do Azure para Pesquisa de IA do Azure :
- NuGet: Azure. Search.Documents
- Documentação de referência: Biblioteca de Clientes
A API é autenticada por meio do SDK para a API de Pesquisa de IA do Azure baseada em nuvem usando o nome do serviço de pesquisa e o nome do índice. Em Aplicativos de Contêiner do Azure, o ambiente de contêiner fornece os valores de configuração. A identidade gerenciada é o caminho de credencial padrão.
Autenticação de identidade gerenciada
Cada função Azure na API cria-a SearchClient por meio de uma classe compartilhadaSearchClientFactory, de modo que cada função se autentica da mesma maneira. Por padrão, a fábrica cria um DefaultAzureCredential e o usa para solicitar tokens para Pesquisa de IA do Azure . Em Aplicativos de Contêiner do Azure, DefaultAzureCredential resolve para a identidade gerenciada atribuída ao aplicativo de contêiner.
O método a seguir cria SearchClientFactory.cs essa credencial. Quando o aplicativo de contêiner tem uma identidade gerenciada atribuída pelo usuário, a ID do cliente da variável de AZURE_CLIENT_ID ambiente é passada para que a DefaultAzureCredentialOptions aquisição de token não seja ambígua.
private static DefaultAzureCredential CreateManagedIdentityCredential()
{
var options = new DefaultAzureCredentialOptions();
if (!string.IsNullOrWhiteSpace(ManagedIdentityClientId))
{
options.ManagedIdentityClientId = ManagedIdentityClientId;
}
return new DefaultAzureCredential(options);
}
A infraestrutura de Bicep atribui o acesso de identidade gerenciada ao plano de dados Pesquisa de IA do Azure durante azd up. Essa atribuição de função permite que a API consulte o good-books índice sem armazenar uma chave de consulta no ambiente de contêiner.
Resolução de credenciais local versus implantada
Localmente, se AZURE_CLIENT_ID não estiver definido, DefaultAzureCredential retornará pela cadeia de credenciais padrão e resolverá a credencial do desenvolvedor conectado, como a conta de CLI do Azure ou Visual Studio Code que você usou para entrar. Quando implantado para Aplicativos de Contêiner do Azure, a infraestrutura de Bicep é configurada AZURE_CLIENT_ID para a ID do cliente da identidade gerenciada atribuída pelo usuário, portantoDefaultAzureCredential, direciona essa identidade especificamente em vez de resolver de forma ambígua entre várias identidades que um host pode expor.
Em vez disso, para usar chaves de API, defina USE_KEYLESS_AUTH como antes da false implantação:
azd env set USE_KEYLESS_AUTH false
azd up
Use a autenticação de chave somente quando o ambiente exigir.
Configurações de desenvolvimento local
Para desenvolvimento local, o arquivo de exemplo sample.local.settings.json mostra os valores esperados pela API. Use as configurações locais somente para desenvolvimento. Em Aplicativos de Contêiner do Azure, a configuração de implantação fornece os valores de ambiente de contêiner equivalentes.
| Setting | Purpose | Obrigatório quando |
|---|---|---|
SearchServiceName |
Nome do serviço Pesquisa de IA do Azure . Combina com .search.windows.net o URI do ponto de extremidade de serviço. |
Sempre |
SearchIndexName |
Nome do índice de pesquisa a ser consultado. O padrão é se good-books não definido. |
Optional |
SEARCH_USE_KEY_AUTH |
O padrão é false, usa a identidade gerenciada. Defina para true usar uma chave de API em vez de uma identidade gerenciada. |
Autenticação de chave opcional |
SearchApiKey |
Chave de administração para Pesquisa de IA do Azure . | Necessário quando SEARCH_USE_KEY_AUTH é true |
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SearchServiceName": "",
"SearchIndexName": "good-books"
},
"Host": {
"CORS": "*"
}
}
Função: pesquisar o catálogo
A API de Pesquisa usa um termo de pesquisa e o procura nos documentos no índice de pesquisa, retornando uma lista de correspondências. Por meio da API de Sugestão, cadeias de caracteres parciais são enviadas para o mecanismo de pesquisa como os tipos de usuário. A API sugere termos de pesquisa, como títulos de livro e autores, com base em documentos no índice de pesquisa e retorna uma pequena lista de correspondências.
A função Azure extrai as informações de configuração de pesquisa do ambiente de contêiner, cria o cliente Pesquisa de IA do Azure e atende à consulta.
O sugestor de pesquisa, sg, é definido no arquivo de esquema usado durante o upload em massa.
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);
}
}
}
Para verificar a função de forma independente, chame /api/search com um termo de pesquisa no corpo da solicitação e confirme se a resposta inclui documentos de livro correspondentes, uma contagem total e valores de faceta.
Cliente: pesquisar o catálogo
A página Pesquisa do cliente React chama a search função Azure sempre que o usuário insere uma consulta, altera um filtro de faceta ou passa para uma nova página de resultados. O cliente envia o texto de pesquisa, os valores e top a skip página atual e todos os filtros de idioma ou autor selecionados no corpo do POST para /api/search. A função retorna uma lista de documentos de livro correspondentes, uma contagem total e valores de faceta, que a página usa para renderizar a lista de resultados, o pager e os filtros de faceta. O seguinte código em \client\src\pages\Search\Search.jsx builds que solicitam e armazenam a resposta no estado do componente:
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>
);
}
Para verificar essa integração, insira um termo de pesquisa na barra de pesquisa do site e confirme se a lista de resultados, a contagem de resultados e as facetas são atualizadas.
Cliente: sugestões do catálogo
A API de função Sugestão é chamada no aplicativo \client\src\components\SearchBar\SearchBar.jsx React como parte do componente preenchimento automático da interface do usuário do material. Esse componente usa o texto de entrada para pesquisar autores e livros correspondentes. Em seguida, ele exibe essas correspondências possíveis como itens selecionáveis na lista suspensa.
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>
);
}
Para verificar essa integração, insira texto na barra de pesquisa do site e confirme se os títulos e autores de livro correspondentes aparecem na lista suspensa de preenchimento automático.
Função: Obter um documento específico
A API de Pesquisa de Documento recupera o documento completo para um único livro depois que um usuário o seleciona nos resultados da pesquisa. A função lê um livro id da cadeia de caracteres de consulta da solicitação, usa SearchClientFactory para criar um autenticado SearchCliente chama GetDocumentAsync para pesquisar essa chave no good-books índice. Ele retorna o documento resultante encapsulado em um LookupOutput objeto.
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;
}
}
}
Para verificar a função Pesquisa de forma independente, ligue /api/lookup com um livro id válido e confirme se a resposta retorna o documento completo do livro.
Cliente: obter um documento específico
Quando um usuário seleciona um livro nos resultados da pesquisa, a página Detalhes precisa do documento completo para esse livro, incluindo campos não mostrados na lista de resumo. A página Detalhes lê o livro id dos parâmetros de rota e chama a API de Pesquisa de Documento quando /api/lookup o componente é montado. Ele armazena o documento retornado no estado do componente e o renderiza nas guias Resultados e Dados Brutos . O código a seguir \client\src\pages\Details\Details.jsx executa essa pesquisa durante a inicialização do componente:
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>
);
}
Para verificar essa integração, selecione um livro nos resultados da pesquisa e confirme se seus detalhes, incluindo imagem de capa, autores e classificação, aparecem na página Detalhes.
Modelos C# que dão suporte à API
A API Azure Functions e o projeto de importação em massa compartilham um conjunto de classes de modelo C#. Essas classes definem os corpos de solicitação que o cliente envia, como texto de pesquisa, valores de paginação e filtros. Eles também definem as formas de resposta esperadas pelo cliente, como resultados da pesquisa, valores de faceta e um único documento pesquisado. Manter esses modelos em um arquivo garante que os pontos de extremidade de pesquisa, sugestão e pesquisa de documentos permaneçam consistentes com as expectativas do cliente React. Os seguintes modelos, definidos em Models.cs, dão suporte às funções neste aplicativo:
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; }
}
}
Próximas etapas
Para continuar aprendendo sobre Pesquisa de IA do Azure desenvolvimento, experimente este próximo tutorial sobre indexação: