4 - 探索 JavaScript 搜索代码
在前面的课程中,你已将搜索添加到静态 Web 应用。 本课重点介绍了建立集成的必要步骤。 如果你正在寻找有关如何将搜索集成到 JavaScript 应用的备忘单,本文介绍你需要了解的内容。
azure-search-javascript-samples GitHub 存储库中提供了源代码。
Azure SDK @azure/search-documents
函数应用使用 Azure SDK 进行 Azure AI 搜索:
- NPM:@azure/search-documents
- 参考文档:客户端库
函数应用使用资源名称、API 密钥和索引名称通过 SDK 向基于云的 Azure AI 搜索 API 进行身份验证。 密码存储在静态 Web 应用设置中,并作为环境变量拉取到函数中。
在配置文件中配置密码
const CONFIG = {
SearchIndexName: process.env["SearchIndexName"] || "good-books",
SearchApiKey: process.env["SearchApiKey"] || "",
SearchServiceName: process.env["SearchServiceName"] || "",
SearchFacets: process.env["SearchFacets"] || "authors*,language_code",
}
if (!CONFIG.SearchIndexName || !CONFIG.SearchApiKey || !CONFIG.SearchServiceName) throw Error("./config.js::Cognitive Services key is missing");
module.exports = { CONFIG };
Azure Function:搜索目录
搜索 API 采用搜索词并在搜索索引中的文档之间搜索,并返回匹配项的列表。
Azure Function 拉取搜索配置信息并完成查询。
const { SearchClient, AzureKeyCredential } = require("@azure/search-documents");
const { CONFIG } = require("../config");
// Create a SearchClient to send queries
const client = new SearchClient(
`https://` + CONFIG.SearchServiceName + `.search.azure.cn/`,
CONFIG.SearchIndexName,
new AzureKeyCredential(CONFIG.SearchApiKey)
);
// creates filters in odata syntax
const createFilterExpression = (filterList, facets) => {
let i = 0;
let filterExpressions = [];
while (i < filterList.length) {
let field = filterList[i].field;
let value = filterList[i].value;
if (facets[field] === 'array') {
filterExpressions.push(`${field}/any(t: search.in(t, '${value}', ','))`);
} else {
filterExpressions.push(`${field} eq '${value}'`);
}
i += 1;
}
return filterExpressions.join(' and ');
}
// reads in facets and gets type
// array facets should include a * at the end
// this is used to properly create filters
const readFacets = (facetString) => {
let facets = facetString.split(",");
let output = {};
facets.forEach(function (f) {
if (f.indexOf('*') > -1) {
output[f.replace('*', '')] = 'array';
} else {
output[f] = 'string';
}
})
return output;
}
module.exports = async function (context, req) {
try {
// Reading inputs from HTTP Request
let q = (req.query.q || (req.body && req.body.q));
const top = (req.query.top || (req.body && req.body.top));
const skip = (req.query.skip || (req.body && req.body.skip));
const filters = (req.query.filters || (req.body && req.body.filters));
const facets = readFacets(CONFIG.SearchFacets);
// If search term is empty, search everything
if (!q || q === "") {
q = "*";
}
// Creating SearchOptions for query
let searchOptions = {
top: top,
skip: skip,
includeTotalCount: true,
facets: Object.keys(facets),
filter: createFilterExpression(filters, facets)
};
// Sending the search request
const searchResults = await client.search(q, searchOptions);
// Getting results for output
const output = [];
for await (const result of searchResults.results) {
output.push(result);
}
// Logging search results
context.log(searchResults.count);
// Creating the HTTP Response
context.res = {
// status: 200, /* Defaults to 200 */
headers: {
"Content-type": "application/json"
},
body: {
count: searchResults.count,
results: output,
facets: searchResults.facets
}
};
} catch (error) {
context.log.error(error);
// Creating the HTTP Response
context.res = {
status: 400,
body: {
innerStatusCode: error.statusCode || error.code,
error: error.details || error.message
}
};
}
};
客户端:从目录中搜索
通过以下代码在 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:来自目录的建议
在用户键入内容时,建议 API 将使用搜索词,并为搜索索引中的文档建议搜索词(如书籍标题和作者),并返回一个较小的匹配列表。
搜索建议器 sg
在大容量上传期间使用的架构文件中定义。
建议 API 的路由包含在 function.json 绑定中。
const { SearchClient, AzureKeyCredential } = require("@azure/search-documents");
const { CONFIG } = require("../config");
// Create a SearchClient to send queries
const client = new SearchClient(
`https://` + CONFIG.SearchServiceName + `.search.azure.cn/`,
CONFIG.SearchIndexName,
new AzureKeyCredential(CONFIG.SearchApiKey)
);
module.exports = async function (context, req) {
context.log(req);
// Reading inputs from HTTP Request
const q = (req.query.q || (req.body && req.body.q));
const top = (req.query.top || (req.body && req.body.top));
const suggester = (req.query.suggester || (req.body && req.body.suggester));
// Let's get the top 5 suggestions for that search term
const suggestions = await client.suggest(q, suggester, {top: parseInt(top)});
//const suggestions = await client.autocomplete(q, suggester, {top: parseInt(top)});
context.log(suggestions);
context.res = {
// status: 200, /* Defaults to 200 */
headers: {
"Content-type": "application/json"
},
body: { suggestions: suggestions.results}
};
};
客户端:来自目录的建议
建议函数 API 在 \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:获取特定文档
查找 API 接受 ID 并从搜索索引中返回文档对象。
查找 API 的路由包含在 function.json 绑定中。
const { SearchClient, AzureKeyCredential } = require("@azure/search-documents");
const { CONFIG } = require("../config");
// Create a SearchClient to send queries
const client = new SearchClient(
`https://` + CONFIG.SearchServiceName + `.search.azure.cn/`,
CONFIG.SearchIndexName,
new AzureKeyCredential(CONFIG.SearchApiKey)
);
module.exports = async function (context, req) {
// Reading inputs from HTTP Request
const id = (req.query.id || (req.body && req.body.id));
// Returning the document with the matching id
const document = await client.getDocument(id)
context.log(document);
context.res = {
// status: 200, /* Defaults to 200 */
headers: {
"Content-type": "application/json"
},
body: { document: document}
};
};
客户端:获取特定文档
此函数 API 在 \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>
);
}
后续步骤
本教程系列介绍了如何在 JavaScript 中创建和加载搜索索引,并构建了一个 Web 应用,该应用可提供搜索体验,包括搜索栏、分面导航和筛选器、建议、分页和文档查找。
下一步,可以将此示例扩展到多个方向:
- 添加自动完成以获得更多预先输入。
- 添加或修改 Facet 和筛选器。
- 使用 Microsoft Entra ID(而不是基于密钥的身份验证)更改身份验证和授权模型。
- 更改索引方法。 无需将 JSON 推送到搜索索引,而是使用 good-books 数据集预加载 blob 容器,并设置 blob 索引器以引入数据。 了解如何使用索引器,它们在索引过程中提供了更多的数据引入和内容扩充选项。