步骤 4 - 探索 .NET 搜索代码
在前面的课程中,你已将搜索添加到静态 Web 应用。 本课重点介绍了建立集成的必要步骤。 如果你正在寻找有关如何将搜索集成到 Web 应用的速查表,本文便介绍了你需要了解的内容。
Azure SDK Azure.Search.Documents
函数应用使用 Azure SDK 进行 Azure AI 搜索:
- NuGet:Azure.Search.Documents
- 参考文档:客户端库
函数应用使用资源名称、资源键和索引名称通过 SDK 向基于云的 Azure AI 搜索 API 进行身份验证。 密码存储在静态 Web 应用设置中,并作为环境变量拉取到 Function 中。
在 local.settings.json 文件中配置机密
在
./api/
创建一个名为local.settings.json
的新文件,并将以下 JSON 对象复制到该文件中。{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "SearchApiKey": "YOUR_SEARCH_QUERY_KEY", "SearchServiceName": "YOUR_SEARCH_RESOURCE_NAME", "SearchIndexName": "good-books" } }
为下方内容更改为你自己的搜索资源值:
- YOUR_SEARCH_RESOURCE_NAME
- YOUR_SEARCH_QUERY_KEY
Azure Function:搜索目录
Search
API 采用搜索词并在搜索索引中的文档之间搜索,并返回匹配项的列表。
Azure Function 拉取搜索配置信息并完成查询。
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using System;
using Azure;
using System.Collections.Generic;
using System.Linq;
namespace FunctionApp_web_search
{
public static class Search
{
private static string searchApiKey = Environment.GetEnvironmentVariable("SearchApiKey", EnvironmentVariableTarget.Process);
private static string searchServiceName = Environment.GetEnvironmentVariable("SearchServiceName", EnvironmentVariableTarget.Process);
private static string searchIndexName = Environment.GetEnvironmentVariable("SearchIndexName", EnvironmentVariableTarget.Process) ?? "good-books";
[FunctionName("search")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
ILogger log)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize<RequestBodySearch>(requestBody);
// Azure AI Search
Uri serviceEndpoint = new Uri($"https://{searchServiceName}.search.azure.cn/");
SearchClient searchClient = new SearchClient(
serviceEndpoint,
searchIndexName,
new AzureKeyCredential(searchApiKey)
);
SearchOptions options = new SearchOptions()
{
Size = data.Size,
Skip = data.Skip,
IncludeTotalCount = true,
Filter= CreateFilterExpression(data.Filters)
};
options.Facets.Add("authors");
options.Facets.Add("language_code");
SearchResults<SearchDocument> response = searchClient.Search<SearchDocument>(data.SearchText, options);
var facetOutput = new Dictionary<String, IList<FacetValue>>();
foreach(var facetResult in response.Facets) {
facetOutput[facetResult.Key] = facetResult.Value
.Select(x => new FacetValue() { value = x.Value.ToString(), count = x.Count })
.ToList();
}
var output = new SearchOutput
{
Count = response.TotalCount,
Results = response.GetResults().ToList(),
Facets = facetOutput
};
return new OkObjectResult(output);
}
public static string CreateFilterExpression(List<SearchFilter> filters)
{
if (filters == null || filters.Count <= 0)
{
return null;
}
List<string> filterExpressions = new List<string>();
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);
}
}
}
客户端:从目录中搜索
通过以下代码在 React 客户端中调用 Azure Function。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import CircularProgress from '@material-ui/core/CircularProgress';
import { useLocation, useHistory } 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();
let history = useHistory();
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;
useEffect(() => {
setIsLoading(true);
setSkip((currentPage-1) * top);
const body = {
q: q,
top: top,
skip: skip,
filters: filters
};
axios.post( '/api/search', body)
.then(response => {
//console.log(JSON.stringify(response.data))
setResults(response.data.results);
setFacets(response.data.facets);
setResultCount(response.data.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(() => {
history.push('/search?q=' + q);
setCurrentPage(1);
setFilters([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [q]);
let postSearchHandler = (searchTerm) => {
//console.log(searchTerm);
setQ(searchTerm);
}
var body;
if (isLoading) {
body = (
<div className="col-md-9">
<CircularProgress />
</div>);
} else {
body = (
<div className="col-md-9">
<Results documents={results} top={top} skip={skip} count={resultCount}></Results>
<Pager className="pager-style" currentPage={currentPage} resultCount={resultCount} resultsPerPage={resultsPerPage} setCurrentPage={setCurrentPage}></Pager>
</div>
)
}
return (
<main className="main main--search container-fluid">
<div className="row">
<div className="col-md-3">
<div className="search-bar">
<SearchBar postSearchHandler={postSearchHandler} q={q}></SearchBar>
</div>
<Facets facets={facets} filters={filters} setFilters={setFilters}></Facets>
</div>
{body}
</div>
</main>
);
}
Azure Function:来自目录的建议
在用户键入内容时,Suggest
API 将使用搜索词,并为搜索索引中的文档建议搜索词(如书籍标题和作者),并返回一个较小的匹配列表。
搜索建议器 sg
在大容量上传期间使用的架构文件中定义。
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using System;
using Azure;
using System.Collections.Generic;
using System.Linq;
namespace FunctionApp_web_search
{
public static class Suggest
{
private static string searchApiKey = Environment.GetEnvironmentVariable("SearchApiKey", EnvironmentVariableTarget.Process);
private static string searchServiceName = Environment.GetEnvironmentVariable("SearchServiceName", EnvironmentVariableTarget.Process);
private static string searchIndexName = Environment.GetEnvironmentVariable("SearchIndexName", EnvironmentVariableTarget.Process) ?? "good-books";
[FunctionName("suggest")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous,"post", Route = null)] HttpRequest req,
ILogger log)
{
// Get Document Id
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var data = JsonSerializer.Deserialize<RequestBodySuggest>(requestBody);
// Azure AI Search
Uri serviceEndpoint = new Uri($"https://{searchServiceName}.search.azure.cn/");
SearchClient searchClient = new SearchClient(
serviceEndpoint,
searchIndexName,
new AzureKeyCredential(searchApiKey)
);
SuggestOptions options = new SuggestOptions()
{
Size = data.Size
};
var suggesterResponse = await searchClient.SuggestAsync<BookModel>(data.SearchText, data.SuggesterName, options);
var response = new Dictionary<string, List<SearchSuggestion<BookModel>>>();
response["suggestions"] = suggesterResponse.Value.Results.ToList();
return new OkObjectResult(response);
}
}
}
客户端:来自目录的建议
建议函数 API 在 \client\src\components\SearchBar\SearchBar.js
中作为组件初始化的一部分在 React 应用中调用:
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import Suggestions from './Suggestions/Suggestions';
import "./SearchBar.css";
export default function SearchBar(props) {
let [q, setQ] = useState("");
let [suggestions, setSuggestions] = useState([]);
let [showSuggestions, setShowSuggestions] = useState(false);
const onSearchHandler = () => {
props.postSearchHandler(q);
setShowSuggestions(false);
}
const suggestionClickHandler = (s) => {
document.getElementById("search-box").value = s;
setShowSuggestions(false);
props.postSearchHandler(s);
}
const onEnterButton = (event) => {
if (event.keyCode === 13) {
onSearchHandler();
}
}
const onChangeHandler = () => {
var searchTerm = document.getElementById("search-box").value;
setShowSuggestions(true);
setQ(searchTerm);
// use this prop if you want to make the search more reactive
if (props.searchChangeHandler) {
props.searchChangeHandler(searchTerm);
}
}
useEffect(_ =>{
const timer = setTimeout(() => {
const body = {
q: q,
top: 5,
suggester: 'sg'
};
if (q === '') {
setSuggestions([]);
} else {
axios.post( '/api/suggest', body)
.then(response => {
console.log(JSON.stringify(response.data))
setSuggestions(response.data.suggestions);
} )
.catch(error => {
console.log(error);
setSuggestions([]);
});
}
}, 300);
return () => clearTimeout(timer);
}, [q, props]);
var suggestionDiv;
if (showSuggestions) {
suggestionDiv = (<Suggestions suggestions={suggestions} suggestionClickHandler={(s) => suggestionClickHandler(s)}></Suggestions>);
} else {
suggestionDiv = (<div></div>);
}
return (
<div >
<div className="input-group" onKeyDown={e => onEnterButton(e)}>
<div className="suggestions" >
<input
autoComplete="off" // setting for browsers; not the app
type="text"
id="search-box"
className="form-control rounded-0"
placeholder="What are you looking for?"
onChange={onChangeHandler}
defaultValue={props.q}
onBlur={() => setShowSuggestions(false)}
onClick={() => setShowSuggestions(true)}>
</input>
{suggestionDiv}
</div>
<div className="input-group-btn">
<button className="btn btn-primary rounded-0" type="submit" onClick={onSearchHandler}>
Search
</button>
</div>
</div>
</div>
);
};
Azure Function:获取特定文档
Lookup
API 接受 ID 并从搜索索引中返回文档对象。
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using System;
using Azure;
namespace FunctionApp_web_search
{
public static class Lookup
{
private static string searchApiKey = Environment.GetEnvironmentVariable("SearchApiKey", EnvironmentVariableTarget.Process);
private static string searchServiceName = Environment.GetEnvironmentVariable("SearchServiceName", EnvironmentVariableTarget.Process);
private static string searchIndexName = Environment.GetEnvironmentVariable("SearchIndexName", EnvironmentVariableTarget.Process) ?? "good-books";
[FunctionName("lookup")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
// Get Document Id
string documentId = req.Query["id"]; ;
// Azure AI Search
Uri serviceEndpoint = new Uri($"https://{searchServiceName}.search.azure.cn/");
SearchClient searchClient = new SearchClient(
serviceEndpoint,
searchIndexName,
new AzureKeyCredential(searchApiKey)
);
var response = await searchClient.GetDocumentAsync<SearchDocument>(documentId);
var output = new LookupOutput
{
Document = response.Value
};
return new OkObjectResult(output);
}
}
}
客户端:获取特定文档
此函数 API 在 \client\src\pages\Details\Detail.js
作为组件初始化的一部分在 React 应用程序中调用:
import React, { useState, useEffect } from "react";
import { useParams } from 'react-router-dom';
import Rating from '@material-ui/lab/Rating';
import CircularProgress from '@material-ui/core/CircularProgress';
import axios from 'axios';
import "./Details.css";
export default function Details() {
let { id } = useParams();
const [document, setDocument] = useState({});
const [selectedTab, setTab] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
// console.log(id);
axios.get('/api/lookup?id=' + id)
.then(response => {
//console.log(JSON.stringify(response.data))
const doc = response.data.document;
setDocument(doc);
setIsLoading(false);
})
.catch(error => {
console.log(error);
setIsLoading(false);
});
}, [id]);
// View default is loading with no active tab
let detailsBody = (<CircularProgress />),
resultStyle = "nav-link",
rawStyle = "nav-link";
if (!isLoading && document) {
// View result
if (selectedTab === 0) {
resultStyle += " active";
detailsBody = (
<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>
);
}
// View raw data
else {
rawStyle += " active";
detailsBody = (
<div className="card-body text-left">
<pre><code>
{JSON.stringify(document, null, 2)}
</code></pre>
</div>
);
}
}
return (
<main className="main main--details container fluid">
<div className="card text-center result-container">
<div className="card-header">
<ul className="nav nav-tabs card-header-tabs">
<li className="nav-item"><button className={resultStyle} onClick={() => setTab(0)}>Result</button></li>
<li className="nav-item"><button className={rawStyle} onClick={() => setTab(1)}>Raw Data</button></li>
</ul>
</div>
{detailsBody}
</div>
</main>
);
}
支持函数应用的 C# 模型
以下模型用于支持此应用中的函数。
using Azure.Search.Documents.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace FunctionApp_web_search
{
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 搜索开发的详细信息,请尝试下一个有关索引的教程: