Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Note
Pencarian Azure AI tersedia melalui portal Azure, REST API, dan Azure SDK. Ini juga mendukung Foundry IQ, lapisan pengetahuan terkelola yang mengubah konten perusahaan menjadi pangkalan pengetahuan yang dapat digunakan kembali dan sadar izin untuk agen di portal Microsoft Foundry.
Pada langkah sebelumnya, Anda menyebarkan situs web yang mendukung pencarian untuk Azure Container Apps. Artikel ini menyoroti langkah-langkah penting yang membuat integrasi pencarian. Anggap saja sebagai contekan untuk mengintegrasikan pencarian ke dalam aplikasi web Anda.
Azure SDK Azure. Search.Documents
API menggunakan Azure SDK untuk Pencarian Azure AI:
- NuGet: Azure. Search.Documents
- Dokumentasi referensi: Pustaka Klien
API mengautentikasi melalui SDK ke API Pencarian Azure AI berbasis cloud dengan menggunakan nama layanan pencarian dan nama indeks. Dalam Azure Container Apps, lingkungan kontainer menyediakan nilai konfigurasi. Identitas terkelola adalah jalur kredensial default.
Autentikasi identitas terkelola
Setiap fungsi Azure di API membuatnya SearchClient melalui kelas bersamaSearchClientFactory, sehingga setiap fungsi mengautentikasi dengan cara yang sama. Secara default, pabrik membangun DefaultAzureCredential dan menggunakannya untuk meminta token untuk Pencarian Azure AI. Di Azure Container Apps, DefaultAzureCredential diselesaikan ke identitas terkelola yang ditetapkan ke aplikasi kontainer.
Metode berikut dari SearchClientFactory.cs membuat kredensial tersebut. Ketika aplikasi kontainer memiliki identitas terkelola yang ditetapkan pengguna, ID klien dari AZURE_CLIENT_ID variabel lingkungan diteruskan ke DefaultAzureCredentialOptions sehingga akuisisi token tidak ambigu.
private static DefaultAzureCredential CreateManagedIdentityCredential()
{
var options = new DefaultAzureCredentialOptions();
if (!string.IsNullOrWhiteSpace(ManagedIdentityClientId))
{
options.ManagedIdentityClientId = ManagedIdentityClientId;
}
return new DefaultAzureCredential(options);
}
Infrastruktur Bicep menetapkan akses identitas terkelola ke bidang data Pencarian Azure AI selama azd up. Penetapan peran ini memungkinkan API mengkueri good-books indeks tanpa menyimpan kunci kueri di lingkungan kontainer.
Resolusi kredensial lokal vs. yang disebarkan
Secara lokal, jika AZURE_CLIENT_ID tidak diatur, DefaultAzureCredential kembali melalui rantai kredensial standarnya dan diselesaikan ke kredensial pengembang yang masuk, seperti akun Azure CLI atau Visual Studio Code yang Anda gunakan untuk masuk. Saat disebarkan ke Azure Container Apps, infrastruktur Bicep diatur AZURE_CLIENT_ID ke ID klien identitas terkelola yang ditetapkan pengguna, sehingga DefaultAzureCredential menargetkan identitas tersebut secara khusus alih-alih menyelesaikan secara ambigu di antara beberapa identitas yang dapat diekspos host.
Untuk menggunakan kunci API, atur USE_KEYLESS_AUTH ke false sebelum penyebaran:
azd env set USE_KEYLESS_AUTH false
azd up
Gunakan autentikasi kunci hanya saat lingkungan Anda memerlukannya.
Pengaturan pengembangan lokal
Untuk pengembangan lokal, file sampel sample.local.settings.json menunjukkan nilai yang diharapkan API. Gunakan pengaturan lokal hanya untuk pengembangan. Dalam Azure Container Apps, konfigurasi penyebaran menyediakan nilai lingkungan kontainer yang setara.
| Setting | Kegunaan | Diperlukan ketika |
|---|---|---|
SearchServiceName |
Nama layanan Pencarian Azure AI. Menggabungkan dengan .search.windows.net untuk membangun URI titik akhir layanan. |
Always |
SearchIndexName |
Nama indeks pencarian yang akan dikueri. Default ke good-books jika tidak diatur. |
Optional |
SEARCH_USE_KEY_AUTH |
Defaultnya adalah false, menggunakan identitas terkelola. Atur ke true untuk menggunakan kunci API alih-alih identitas terkelola. |
Autentikasi kunci opsional |
SearchApiKey |
Kunci admin untuk Pencarian Azure AI. | Diperlukan ketika SEARCH_USE_KEY_AUTH adalah true |
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SearchServiceName": "",
"SearchIndexName": "good-books"
},
"Host": {
"CORS": "*"
}
}
Fungsi: Cari katalog
Search API memproses istilah pencarian dan mencari di seluruh dokumen dalam indeks pencarian, menampilkan daftar kecocokan. Melalui SUGGEST API, string parsial dikirim ke mesin pencari sebagai jenis pengguna. API menyarankan istilah pencarian, seperti judul buku dan penulis, berdasarkan dokumen dalam indeks pencarian dan mengembalikan daftar kecil kecocokan.
Fungsi Azure menarik informasi konfigurasi pencarian dari lingkungan kontainer, membuat klien Pencarian Azure AI, dan memenuhi kueri.
Pemberi saran pencarian, sg, ditentukan dalam file skema yang digunakan selama pengunggahan massal.
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);
}
}
}
Untuk memverifikasi fungsi secara independen, panggil /api/search dengan istilah pencarian di isi permintaan dan konfirmasi respons mencakup dokumen buku yang cocok, jumlah total, dan nilai faset.
Klien: Cari katalog
Halaman Pencarian klien React memanggil search fungsi Azure setiap kali pengguna memasukkan kueri, mengubah filter faset, atau berpindah ke halaman hasil baru. Klien mengirimkan teks pencarian, halaman skip dan top nilai saat ini, dan filter penulis atau bahasa yang dipilih di isi POST ke /api/search. Fungsi mengembalikan daftar dokumen buku yang cocok, jumlah total, dan nilai faset, yang digunakan halaman untuk merender daftar hasil, pager, dan filter faset. Kode berikut dalam \client\src\pages\Search\Search.jsx build yang meminta dan menyimpan respons dalam status komponen:
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>
);
}
Untuk memverifikasi integrasi ini, masukkan istilah pencarian di bilah pencarian situs web dan konfirmasikan bahwa daftar hasil, jumlah hasil, dan faset semuanya diperbarui.
Klien: Saran dari katalog
API fungsi Suggest dipanggil di aplikasi React di \client\src\components\SearchBar\SearchBar.jsx sebagai bagian dari komponen Material UI Autocomplete. Komponen ini menggunakan teks input untuk mencari penulis dan buku yang cocok. Kemudian menampilkan kemungkinan kecocokan tersebut sebagai item yang dapat dipilih di daftar dropdown.
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>
);
}
Untuk memverifikasi integrasi ini, masukkan teks di bilah pencarian situs web dan konfirmasikan bahwa judul dan penulis buku yang cocok muncul di menu dropdown lengkapi otomatis.
Fungsi: Mendapatkan dokumen tertentu
API Pencarian Dokumen mengambil dokumen lengkap untuk satu buku setelah pengguna memilihnya dari hasil pencarian. Fungsi membaca buku id dari string kueri permintaan, menggunakan SearchClientFactory untuk membuat panggilan yang diautentikasi SearchClient, dan untuk GetDocumentAsync mencari kunci tersebut good-books dalam indeks. Ini mengembalikan dokumen yang dihasilkan yang dibungkus dalam LookupOutput objek.
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;
}
}
}
Untuk memverifikasi fungsi Pencarian secara independen, panggil /api/lookup dengan buku id yang valid dan konfirmasi respons mengembalikan dokumen lengkap buku tersebut.
Klien: Mendapatkan dokumen tertentu
Saat pengguna memilih buku dari hasil pencarian, halaman Detail memerlukan dokumen lengkap untuk buku tersebut, termasuk bidang yang tidak ditampilkan dalam daftar ringkasan. Halaman Detail membaca buku id dari parameter rute dan memanggil API Pencarian Dokumen saat /api/lookup komponen dipasang. Ini menyimpan dokumen yang dikembalikan dalam status komponen dan merendernya di tab Hasil dan Data Mentah . Kode berikut dalam \client\src\pages\Details\Details.jsx melakukan pencarian ini selama inisialisasi komponen:
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>
);
}
Untuk memverifikasi integrasi ini, pilih buku dari hasil pencarian dan konfirmasikan bahwa detailnya, termasuk gambar sampul, penulis, dan peringkat, muncul di halaman Detail.
Model C# yang mendukung API
API Azure Functions dan proyek impor massal berbagi sekumpulan kelas model C#. Kelas-kelas ini menentukan badan permintaan yang dikirim klien, seperti teks pencarian, nilai halaman, dan filter. Mereka juga menentukan bentuk respons yang diharapkan klien, seperti hasil pencarian, nilai faset, dan satu dokumen pencarian. Menjaga model ini dalam satu file memastikan titik akhir pencarian, saran, dan pencarian dokumen tetap konsisten dengan harapan klien React. Model berikut, yang ditentukan dalam Models.cs, mendukung fungsi dalam aplikasi ini:
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; }
}
}
Langkah selanjutnya
Untuk terus mempelajari tentang pengembangan Pencarian Azure AI, coba tutorial berikutnya tentang pengindeksan: