Note
Azure AI 検索は、Azure ポータル、REST API、およびAzure SDKから使用できます。 また、Foundry IQ は、エンタープライズ コンテンツを、Microsoft Foundry ポータルのエージェントの再利用可能なアクセス許可に対応したナレッジ ベースに変換するマネージド ナレッジ レイヤーです。
前の手順では、検索が有効な Web サイトをAzure Container Appsにデプロイしました。 この記事では、検索統合を確立する重要な手順について説明します。 検索を Web アプリに統合するためのチート シートと考えてください。
Azure SDK Azure。Search.Documents
API は、Azure AI 検索にAzure SDKを使用します。
- NuGet: Azure。Search.Documents
- リファレンス ドキュメント: クライアント ライブラリ
API は、検索サービス名とインデックス名を使用して、SDK を介してクラウドベースの Azure AI 検索 API に対して認証を行います。 Azure Container Appsでは、コンテナー環境によって構成値が提供されます。 マネージド ID は既定の資格情報パスです。
マネージド ID 認証
API の各Azure関数は、共有SearchClientFactory クラスを介してSearchClientを作成するため、すべての関数が同じ方法で認証されます。 既定では、ファクトリはDefaultAzureCredentialをビルドし、それを使用してAzure AI 検索のトークンを要求します。 Azure Container Appsでは、DefaultAzureCredentialはコンテナー アプリに割り当てられたマネージド ID に解決されます。
SearchClientFactory.csからの次のメソッドは、その資格情報を作成します。 コンテナー アプリにユーザー割り当てマネージド ID がある場合、トークンの取得があいまいでないように、 AZURE_CLIENT_ID 環境変数のクライアント ID が DefaultAzureCredentialOptions に渡されます。
private static DefaultAzureCredential CreateManagedIdentityCredential()
{
var options = new DefaultAzureCredentialOptions();
if (!string.IsNullOrWhiteSpace(ManagedIdentityClientId))
{
options.ManagedIdentityClientId = ManagedIdentityClientId;
}
return new DefaultAzureCredential(options);
}
Bicep インフラストラクチャは、azd up中にマネージド ID アクセスを Azure AI 検索 データ プレーンに割り当てます。 このロールの割り当てにより、API はコンテナー環境にクエリ キーを格納せずに、 good-books インデックスに対してクエリを実行できます。
ローカルとデプロイされた資格情報の解決
ローカルでは、AZURE_CLIENT_IDが設定されていない場合、DefaultAzureCredentialは標準の資格情報チェーンを経由してフォールバックし、サインインに使用したAzure CLIやVisual Studio Code アカウントなど、サインインしている開発者の資格情報に解決されます。 Azure Container Appsにデプロイすると、Bicep インフラストラクチャはユーザー割り当てマネージド ID のクライアント ID にAZURE_CLIENT_IDを設定するため、DefaultAzureCredentialは、ホストが公開できる複数の ID 間であいまいに解決するのではなく、その ID を特にターゲットにします。
代わりに API キーを使用するには、デプロイ前に USE_KEYLESS_AUTH を false に設定します。
azd env set USE_KEYLESS_AUTH false
azd up
キー認証は、環境で必要な場合にのみ使用します。
ローカル開発設定
ローカル開発の場合、サンプル sample.local.settings.json ファイルには、API が期待する値が表示されます。 開発にのみローカル設定を使用します。 Azure Container Appsでは、デプロイ構成によって同等のコンテナー環境の値が提供されます。
| Setting | Purpose | 必要な場合 |
|---|---|---|
SearchServiceName |
Azure AI 検索 サービスの名前。
.search.windows.netと組み合わせて、サービス エンドポイント URI を構築します。 |
いつも |
SearchIndexName |
クエリを実行する検索インデックスの名前。 既定値は、設定されていない場合は good-books されます。 |
Optional |
SEARCH_USE_KEY_AUTH |
既定値は false で、マネージド ID を使用します。 マネージド ID の代わりに API キーを使用するように true に設定します。 |
オプションのキー認証 |
SearchApiKey |
Azure AI 検索の管理者キー。 |
SEARCH_USE_KEY_AUTH が true されている場合は必須 |
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"SearchServiceName": "",
"SearchIndexName": "good-books"
},
"Host": {
"CORS": "*"
}
}
関数: カタログを検索する
Search API は検索語句を受け取り、検索インデックス内のすべてのドキュメントから探して、一致項目の一覧を返します。 Suggest API を使用すると、部分的な文字列がユーザーの入力時に検索エンジンに送信されます。 この API は、検索インデックス内のドキュメントに基づいて書籍のタイトルや作成者などの検索用語を提案し、一致の小さなリストを返します。
Azure関数は、コンテナー環境から検索構成情報を取得し、Azure AI 検索 クライアントを作成して、クエリを実行します。
検索サジェスターsgは、一括アップロードの際に使用されるスキーマファイルに定義されています。
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);
}
}
}
関数を個別に検証するには、要求本文で検索語句を使用して /api/search を呼び出し、応答に一致する書籍ドキュメント、合計カウント、ファセット値が含まれていることを確認します。
クライアント: カタログを検索する
React クライアントの検索ページは、ユーザーがクエリを入力したり、ファセット フィルターを変更したり、結果の新しいページに移動したりするたびに、search Azure関数を呼び出します。 クライアントは、検索テキスト、現在のページの skip と top 値、POST 本文で選択した作成者または言語フィルターを /api/searchに送信します。 この関数は、一致する書籍ドキュメント、合計数、およびファセット値のリストを返します。この値は、ページが結果リスト、ポケットベル、ファセット フィルターをレンダリングするために使用します。
\client\src\pages\Search\Search.jsxの次のコードは、応答を要求し、コンポーネントの状態で格納するビルドです。
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>
);
}
この統合を確認するには、Web サイトの検索バーに検索語句を入力し、結果一覧、結果数、ファセットがすべて更新されていることを確認します。
クライアント: カタログからの提案
Suggest 関数 API は、Material UI オートコンプリート コンポーネントの一部として、\client\src\components\SearchBar\SearchBar.jsxの React アプリで呼び出されます。 このコンポーネントでは、入力テキストを使用して、一致する作成者と書籍を検索します。 その後、一致する可能性のある項目がドロップダウン リストに選択可能な項目として表示されます。
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>
);
}
この統合を確認するには、Web サイトの検索バーにテキストを入力し、オートコンプリート ドロップダウンに一致する書籍のタイトルと作成者が表示されることを確認します。
関数: 特定のドキュメントを取得する
Document Lookup API は、ユーザーが検索結果から選択した後、1 つの書籍の完全なドキュメントを取得します。 この関数は、要求のクエリ文字列から書籍 id を読み取り、 SearchClientFactory を使用して認証された SearchClientを作成し、 GetDocumentAsync を呼び出して good-books インデックスでそのキーを検索します。
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;
}
}
}
Lookup 関数を個別に検証するには、有効な書籍idを使用して/api/lookupを呼び出し、応答でその書籍の完全なドキュメントが返されることを確認します。
クライアント: 特定のドキュメントを取得する
ユーザーが検索結果から書籍を選択すると、[詳細] ページには、要約リストに表示されないフィールドを含む、その書籍の完全なドキュメントが必要です。 [詳細] ページでは、ルート パラメーターから書籍 id が読み取られ、コンポーネントがマウントされたときにドキュメント参照 API が /api/lookup 経由で呼び出されます。 返されたドキュメントがコンポーネントの状態で格納され、[ 結果 ] タブと [ 生データ ] タブに表示されます。
\client\src\pages\Details\Details.jsxの次のコードは、コンポーネントの初期化中にこの参照を実行します。
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>
);
}
この統合を確認するには、検索結果から書籍を選択し、表紙画像、作成者、評価などの詳細が [詳細] ページに表示されることを確認します。
API をサポートする C# モデル
Azure Functions API と一括インポート プロジェクトは、一連の C# モデル クラスを共有します。 これらのクラスは、検索テキスト、ページング値、フィルターなど、クライアントが送信する要求本文を定義します。 また、検索結果、ファセット値、1 つの検索ドキュメントなど、クライアントが期待する応答図形も定義します。 これらのモデルを 1 つのファイルに保持することで、検索、提案、ドキュメントの検索エンドポイントが React クライアントの期待と一貫性を保ちます。
Models.csで定義されている次のモデルは、このアプリの関数をサポートしています。
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; }
}
}
次のステップ
Azure AI 検索開発について引き続き学習するには、インデックス作成に関するこの次のチュートリアルをお試しください。