4: Erkunden des JavaScript-Suchcodes
In den vorherigen Lektionen haben Sie einer statischen Web-App eine Suchfunktion hinzugefügt. In dieser Lektion werden die wesentlichen Schritte zum Einrichten der Integration hervorgehoben. Dieser Artikel kann als Spickzettel für die Integration von Suchfunktionen in Ihre JavaScript-App herangezogen werden.
Der Quellcode ist im GitHub-Repository azure-search-javascript-samples verfügbar.
Azure SDK @azure/search-documents
Die Funktions-App verwendet das Azure SDK für Azure KI Search:
- NPM: @azure/search-documents
- Referenzdokumentation: Clientbibliothek
Die Funktions-App authentifiziert sich über das SDK gegenüber der cloudbasierten Azure KI Search-API unter Verwendung Ihres Ressourcennamens, API-Schlüssels und Indexnamens. Die Geheimnisse sind in den Einstellungen der statischen Web-App gespeichert und werden als Umgebungsvariablen in die Funktion gepullt.
Konfigurieren von Geheimnissen in einer Konfigurationsdatei
const CONFIG = {
SearchIndexName: process.env["SearchIndexName"] || "good-books",
SearchApiQueryKey: process.env["SearchApiKey"] || "",
SearchServiceName: process.env["SearchServiceName"] || "",
SearchFacets: process.env["SearchFacets"] || "authors*,language_code",
}
console.log(CONFIG);
if (!CONFIG.SearchIndexName || !CONFIG.SearchApiQueryKey || !CONFIG.SearchServiceName) throw Error("./config.js::Cognitive Services key is missing");
module.exports = { CONFIG };
Azure-Funktion: Durchsuchen des Katalogs
Die Search-API akzeptiert einen Suchbegriff, durchsucht die Dokumente im Suchindex und gibt eine Liste mit Treffern zurück.
Azure Function ruft die Informationen der Suchkonfiguration per Pull ab und führt die Abfrage aus.
const { app } = require('@azure/functions');
const { CONFIG } = require("../lib/config");
const { readFacets, createFilterExpression } = require('../lib/azure-cognitive-search');
const { SearchClient, AzureKeyCredential } = require("@azure/search-documents");
// Create a SearchClient to send queries
const client = new SearchClient(
`https://` + CONFIG.SearchServiceName + `.search.windows.net/`,
CONFIG.SearchIndexName,
new AzureKeyCredential(CONFIG.SearchApiQueryKey)
);
app.http('search', {
methods: ['POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
context.log(`Search request for url "${request.url}"`);
try {
const body = await request.json();
console.log(body);
let q = body.q || "*";
const top = body.top || 5;
const skip = parseInt(body.skip || 0);
const filters = body.filters || undefined;
const facets = readFacets(CONFIG.SearchFacets);
const facetNames = Object.keys(facets);
console.log(facetNames);
const filtersExpression = (filters && facets) ? createFilterExpression(filters, facets) : undefined;
console.log(filtersExpression)
// Creating SearchOptions for query
let searchOptions = {
top: top,
skip: skip,
includeTotalCount: true,
facets: facetNames,
filter: filtersExpression
};
console.log(searchOptions);
// Sending the search request
const searchResults = await client.search(q, searchOptions);
console.log(searchResults);
// Getting results for output
const output = [];
for await (const result of searchResults.results) {
output.push(result);
}
console.log(searchResults)
// Logging search results
context.log(searchResults.count);
return {
headers: {
"Content-type": "application/json"
},
jsonBody: {
count: searchResults.count,
results: output,
resultsCount: output.length,
facets: searchResults.facets,
q,
top,
skip,
filters: filters || ''
}
};
} catch (error) {
return {
status: 500,
jsonBody: {
innerStatusCode: error.statusCode || error.code,
error: error.details || error.message,
stack: error.stack
}
}
}
}
});
Client: Suchen über den Katalog
Rufen Sie mithilfe des folgenden Codes die Azure-Funktion im React-Client auf:
import React, { useState } from "react";
import request from "../api";
import CircularProgress from "@mui/material/CircularProgress";
import { useLocation, useNavigate } from "react-router-dom";
import styled from "@emotion/styled";
import Grid from "@mui/material/Grid";
import Stack from "@mui/material/Stack";
import Container from "@mui/material/Container";
import Results from "../components/Results";
import Pager from "../components/Pager";
import Facets from "../components/Facets/Facets";
import SearchBar from "../components/SearchBar";
import { useQuery } from "@tanstack/react-query";
const StyledPager = styled(Pager)({
marginLeft: "auto",
marginRight: "auto",
maxWidth: "fit-content",
});
const StyledContainer = styled.div`
// Uncomment to debug
// border: 1px solid red;
// Center body with space around
margin: 1rem auto;
margin-top: 5rem;
min-height: 30em;
padding-left: 0px;
padding-right: 0px;
max-width: 98%;
outline: 0px;
display: flex;
`;
const StyledSearchBar = styled(SearchBar)({});
const LeftColumn = styled(Stack)`
width: 30%;
border-right: 1px solid #f0f0f0;
padding: 0 16px 0 16px;
`;
const RightColumn = styled(Container)``;
export default function Search() {
const location = useLocation();
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(
new URLSearchParams(location.search).get("p") ?? 1
);
const [searchTerm, setSearchTerm] = 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({});
let resultsPerPage = top;
function setNavigation(q, p) {
navigate(`/search?q=${q}&p=${p}`);
}
function changeCurrentPage(newPage) {
const newSkip = (newPage - 1) * top;
setNavigation(searchTerm, newPage);
setCurrentPage(newPage);
setSkip(newSkip);
}
const fiveMinutes = 1000 * 60 * 5;
/* eslint-disable no-unused-vars */
const { data, isLoading, dataUpdatedAt, error } = useQuery({
queryKey: ["search", searchTerm, top, skip, currentPage, filters, facets],
//refetchOnMount: false,
//refetchOnWindowFocus: false,
//refetchOnReconnect: false,
enabled: searchTerm !== undefined,
staleTime: fiveMinutes, // time in milliseconds
cacheTime: fiveMinutes,
queryFn: async () => {
setSkip((currentPage - 1) * top);
return request("/api/search", "POST", {
q: searchTerm,
top: top,
skip: (currentPage - 1) * top,
filters: filters,
}).then((response) => {
setFacets(response.facets);
setFilters(response.filters);
return response;
});
},
});
const postSearchHandler = (searchTerm) => {
setNavigation(searchTerm, 1);
setSearchTerm(searchTerm);
setCurrentPage(1);
setSkip(0);
};
const updateFilters = (filters) => {
setFilters(filters);
};
return (
<>
<StyledContainer>
<LeftColumn>
<StyledSearchBar
navigateToSearchPage={postSearchHandler}
defaultTerm={searchTerm}
></StyledSearchBar>
<Facets
facets={facets}
filters={filters}
setFilters={updateFilters}
></Facets>
</LeftColumn>
<RightColumn>
{isLoading ? (
<CircularProgress />
) : (
<Grid container>
<Results
q={searchTerm}
documents={data.results}
top={top}
skip={skip}
count={data.count}
></Results>
<StyledPager
className="pager-style"
currentPage={currentPage}
resultCount={data.count}
resultsPerPage={resultsPerPage}
setCurrentPage={changeCurrentPage}
></StyledPager>
</Grid>
)}
</RightColumn>
</StyledContainer>
</>
);
}
Client: Facetten aus dem Katalog
Diese React-Komponente enthält das Suchtextfeld und die den Suchergebnissen zugeordneten Facetten. Facetten müssen als Teil des Suchschemas durchdacht und gestaltet werden, wenn die Suchdaten geladen werden. Anschließend werden die Facetten in der Suchabfrage zusammen mit dem Suchtext verwendet, um die Facettennavigation bereitzustellen.
import React, { useEffect, useState } from "react";
import { List, Chip } from "@mui/material";
import CheckboxFacet from "./CheckboxFacet";
import styled from "@emotion/styled";
const StyledFacetComponent = styled.div`
border-right: "1px solid #f0f0f0";
height: "100%";
`;
const StyledSelectedFacets = styled.div``;
const StyledFacetList = styled(List)`
margin: "0.25em";
margin-top: "32px !important";
padding-left: "36px !important";
`;
export default function Facets(props) {
const [filters, setFilters] = useState([]);
const [facets, setFacets] = useState({});
useEffect(() => {
setFilters(props.filters);
setFacets(props.facets);
}, [props.filters, props.facets]);
// Change facet name to be more readable
// e.g. "author" -> "Author"
// e.g. "publication_year" -> "Publication Year"
function mapFacetName(facetName) {
facetName =
`${facetName[0].toUpperCase()}${facetName.substring(1)}`.replace(
"_",
" "
) || ``;
return facetName;
}
function addFilter(name, value) {
const newFilters = filters.concat({ field: name, value: value });
props.setFilters(newFilters);
}
function removeFilter(filter) {
const newFilters = filters.filter((item) => item.value !== filter.value);
props.setFilters(newFilters);
}
return (
<StyledFacetComponent>
<StyledSelectedFacets>
<List>
{filters.map((filter, index) => {
return (
<Chip
key={index}
label={`${mapFacetName(filter.field)}: ${filter.value}`}
onDelete={() => removeFilter(filter)}
/>
);
})}
</List>
</StyledSelectedFacets>
<StyledFacetList>
{Object.keys(facets).map((key) => {
return (
<CheckboxFacet
key={key}
name={key}
values={facets[key]}
addFilter={addFilter}
removeFilter={removeFilter}
mapFacetName={mapFacetName}
selectedFacets={filters.filter((f) => f.field === key)}
/>
);
})}
</StyledFacetList>
</StyledFacetComponent>
);
}
Client: Paginierung aus dem Katalog
Wenn die Suchergebnisse über einige wenige (8) hinaus erweitert werden, stellt die @mui/material/TablePagination
-Komponente die Paginierung für die Ergebnisse bereit.
import React from "react";
import TablePagination from "@mui/material/TablePagination";
import styled from "@emotion/styled";
const StyledTablePagination = styled(TablePagination)`
margin: auto;
max-width: fit-content;
border-bottom: 0;
`;
export default function Pager(props) {
const pagesInResultSet = Math.round(props.resultCount / props.resultsPerPage);
const moreThanOnePage = pagesInResultSet > 1;
if (!moreThanOnePage) return <></>;
const handleChangePage = (event, newPage) => {
props.setCurrentPage(newPage + 1);
};
return (
<StyledTablePagination
className="pager"
align="center"
component="div"
count={props.resultCount}
page={props.currentPage - 1} // zero-based control from material ui
onPageChange={handleChangePage}
rowsPerPage={props.resultsPerPage}
rowsPerPageOptions={[props.resultsPerPage]} // don't display b/c there is a single value
showFirstButton={true}
showLastButton={true}
labelDisplayedRows={({ from, to, count }) =>
`${props.currentPage} of ${Math.round(
props.resultCount / props.resultsPerPage
)} pages`
}
/>
);
}
Wenn der Benutzer die Seite ändert, wird dieser Wert von der handleChangePage
-Funktion an die übergeordnete Search.js
-Seite gesendet. Die Funktion sendet eine neue Anforderung an die Such-API für dieselbe Abfrage und die neue Seite. Die API-Antwort aktualisiert die Facetten, Ergebnisse und Pagerkomponenten.
Azure-Funktion: Vorschläge aus dem Katalog
Die Vorschlags-API nimmt einen Suchbegriff während der Eingabe eines Benutzers entgegen und schlägt Suchbegriffe wie Buchtitel und Autoren aus den Dokumenten im Suchindex vor. Dabei wird eine kurze Liste mit Treffern zurückgegeben.
Der Suchvorschlagsfunktion (sg
) wird in der bei Massenuploadvorgängen verwendeten Schemadatei definiert.
const { app } = require('@azure/functions');
const { SearchClient, AzureKeyCredential } = require("@azure/search-documents");
const { CONFIG } = require("../lib/config");
// Create a SearchClient to send queries
const client = new SearchClient(
`https://` + CONFIG.SearchServiceName + `.search.windows.net/`,
CONFIG.SearchIndexName,
new AzureKeyCredential(CONFIG.SearchApiQueryKey)
);
app.http('suggest', {
methods: ['POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
context.log(`Suggester request for url "${request.url}"`);
try {
const body = await request.json();
console.log(`suggest body ${body}`);
let q = body.q;
console.log(`suggest q ${q}`)
const top = body.top;
console.log(`suggest top ${top}`)
const suggester = body.suggester;
console.log(`suggest suggester ${suggester}`)
if(!body || !q || !top || !suggester){
console.log(`No suggester found in body`)
return {
status: 404,
body: "No suggester found"
}
}
// 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);
return {
headers: {
"Content-type": "application/json"
},
jsonBody: {
suggestions: suggestions.results,
q,
top,
suggester
}
}
} catch (error) {
return {
status: 400,
jsonBody: {
innerStatusCode: error.statusCode || error.code,
error: error.details || error.message
}
}
}
}
});
Client: Vorschläge aus dem Katalog
Die Vorschlags-API wird in der React-App unter \src\components\SearchBar\SearchBar.js
im Rahmen der Komponenteninitialisierung aufgerufen:
import React, { useState } from "react";
import request from "../api";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import styled from "@emotion/styled";
import { useQuery } from "@tanstack/react-query";
const StyledContainer = styled.div`
display: flex;
flex-flow: row wrap;
justify-content: space-evenly;
align-items: center;
gap: 1em;
margin-top: 1em;
`;
const StyledAutoComplete = styled(Autocomplete)`
width: 80%;
`;
const StyledButton = styled(Button)`
max-height: 40px;
`;
export default function SearchBar({ navigateToSearchPage, defaultTerm = "" }) {
const [q, setQ] = useState(defaultTerm);
const [suggestions, setSuggestions] = useState([]);
const top = 5;
const suggester = "sg";
/* eslint-disable no-unused-vars */
const { data, isLoading, error } = useQuery({
queryKey: ["suggest", q, top, suggester],
refetchOnMount: true,
enabled: q.length > 0,
queryFn: async () => {
if (q.length > 0) {
return request("/api/suggest", "POST", {
q,
top,
suggester,
}).then((response) => {
let i = 0;
const autoCompleteOptions = response.suggestions.map(
(suggestion) => ({ id: i++, label: suggestion.text })
);
setSuggestions(autoCompleteOptions);
return response;
});
}
},
});
const onFormSubmit = () => {
if (navigateToSearchPage) {
navigateToSearchPage(q);
}
};
function hasLabelValue (option){
return (
option !== undefined &&
option !== null &&
option.label !== undefined &&
option.label !== null
);
}
return (
<StyledContainer onSubmit={onFormSubmit}>
<StyledAutoComplete
key="autocomplete"
freeSolo // accepts both entered text or selected suggestion
autoSelect // text in box selected
autoFocus="autoFocus"
filterOptions={(x) => x}
options={suggestions}
value={q}
noOptionsText="What are you looking for?"
onChange={
(e, value, reason) => setQ(value?.label || "")
}
onInputChange={(e, newValue, reason) => {
if (newValue) {
setQ(newValue);
}
}}
getOptionLabel={(option) => hasLabelValue(option) ? option.label : q}
// set key to force re-render when q changes
renderOption={(props, option) => {
return hasLabelValue(option) ? (
<li {...props} key={option.id}>
{option.label}
</li>
) : (
<li {...props} key={q}>
{q}
</li>
)
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label="What are you looking for?"
onKeyDown={(e) => {
if (e.code.toLowerCase() === "enter" && e.target.value) {
onFormSubmit(e.target.value);
}
}}
/>
)}
/>
<StyledButton
key="styledbutton"
variant="contained"
onClick={onFormSubmit}
>
Search
</StyledButton>
</StyledContainer>
);
}
Diese React-Komponente verwendet die @mui/material/Autocomplete
-Komponente, um ein Suchtextfeld bereitzustellen, das auch das Anzeigen von Vorschlägen (mithilfe der renderInput
-Funktion) unterstützt. AutoVervollständigen beginnt, nachdem die ersten Zeichen eingegeben wurden. Wenn jedes neue Zeichen eingegeben wird, wird es als Abfrage an die Suchmaschine gesendet. Die Ergebnisse werden als kurze Liste von Vorschlägen angezeigt.
Diese AutoVervollständigen-Funktion ist ein häufiges Feature, aber diese spezifische Implementierung hat einen zusätzlichen Anwendungsfall. Der Kunde kann Text eingeben und aus den Vorschlägen auswählen oder den eingegebenen Text übermitteln. Die Eingabe aus der Vorschlagsliste sowie die Eingabe aus dem Textfeld müssen auf Änderungen nachverfolgt werden, was sich darauf auswirkt, wie das Formular gerendert wird und was beim Übermitteln des Formulars an die Such-API gesendet wird.
Wenn Ihr Anwendungsfall für die Suche es Ihrem Benutzer ermöglicht, nur aus den Vorschlägen auszuwählen, reduziert dies den Komplexitätsbereich des Steuerelements, schränkt jedoch die Benutzererfahrung ein.
Azure-Funktion: Abrufen eines bestimmten Dokuments
Die Lookup-API akzeptiert eine ID und gibt das Dokumentobjekt aus dem Suchindex zurück.
const { SearchClient, AzureKeyCredential } = require("@azure/search-documents");
const { app } = require('@azure/functions');
const { CONFIG } = require("../lib/config");
// Create a SearchClient to send queries
const client = new SearchClient(
`https://` + CONFIG.SearchServiceName + `.search.windows.net/`,
CONFIG.SearchIndexName,
new AzureKeyCredential(CONFIG.SearchApiQueryKey)
);
app.http('lookup', {
methods: ['GET'],
authLevel: 'anonymous',
handler: async (request, context) => {
context.log(`Lookup processed request for url "${request.url}"`);
try {
const id = request.query.get('id');
console.log(id);
if (!id) {
return {
status: 404
}
}
const document = await client.getDocument(id);
return { jsonBody: { document: document } };
} catch (error) {
return {
status: 400,
jsonBody: {
innerStatusCode: error.statusCode || error.code,
error: error.details || error.message
}
}
}
}
});
Client: Abrufen eines bestimmten Dokuments
Diese Funktions-API wird in der React-App unter \src\pages\Details\Detail.js
im Rahmen der Komponenteninitialisierung aufgerufen:
import React, { useState } from "react";
import { useParams } from 'react-router-dom';
import CircularProgress from '@mui/material/CircularProgress';
import Container from '@mui/material/Container';
import request from '../api';
import BookDetailsTab from "../components/BookDetail/DetailPage/booktab";
import {
useQuery,
} from '@tanstack/react-query'
export default function Details() {
let { id } = useParams();
const [document, setDocument] = useState({});
const lookupRequest = () => request('/api/lookup?id=' + id, "GET").then(response => {
const doc = response.document;
setDocument(doc);
return response
});
const fiveMinutes = 1000 * 60 * 5;
/* eslint-disable no-unused-vars */
const { data, isLoading, error } = useQuery({
queryKey: ["lookup", id],
queryFn: async() => lookupRequest(),
enabled: id !== undefined,
staleTime: fiveMinutes, // time in milliseconds
cacheTime: fiveMinutes,
//refetchOnMount: false,
//refetchOnWindowFocus: false,
//refetchOnReconnect: false,
});
return (
<Container sx = {{
padding: 4
}}>
{ isLoading ? <CircularProgress /> : <BookDetailsTab document={document}/>}
{ error && <div>{error}</div> }
</Container>
);
}
Wenn Ihre Client-App vorab generierten Inhalt verwenden kann, ist diese Seite ein guter Kandidat für die automatische Generierung, da der Inhalt statisch ist und direkt aus dem Suchindex abgerufen wird.
Nächste Schritte
In dieser Tutorialreihe haben Sie gelernt, wie Sie einen Suchindex in JavaScript erstellen und laden, und Sie haben eine Web-App erstellt, die eine Suchoberfläche mit Suchleiste, Facettennavigation und Filter, Vorschläge, Paginierung und Dokumentsuche bietet.
Im nächsten Schritt können Sie dieses Beispiel in verschiedene Richtungen erweitern:
- Fügen Sie AutoVervollständigen hinzu, um weitere typeahead-Informationen zu erstellen.
- Fügen Sie Facetten und Filter hinzu und ändern Sie diese.
- Ändern Sie das Authentifizierungs- und Autorisierungsmodell, indem Sie Microsoft Entra ID anstelle der schlüsselbasierten Authentifizierung verwenden.
- Ändern Sie die Indizierungsmethode. Anstatt JSON in einen Suchindex zu pushen, laden Sie im Voraus einen Blobcontainer mit dem Good-Books-Dataset, und richten Sie einen Blobindexer ein, um die Daten zu erfassen. Wenn Sie wissen, wie Sie mit Indexern arbeiten, erhalten Sie mehr Optionen für die Datenerfassung und Inhaltsanreicherung während der Indizierung.