4 – Utforska JavaScript-sökkoden
I föregående lektioner lade du till sökning i en statisk webbapp. Den här lektionen belyser de viktigaste stegen för att upprätta integrering. Om du letar efter ett fuskark om hur du integrerar sökning i din JavaScript-app förklarar den här artikeln vad du behöver veta.
Källkoden är tillgänglig på GitHub-lagringsplatsen azure-search-javascript-samples .
Azure SDK @azure/search-documents
Funktionsappen använder Azure SDK för Azure AI Search:
- NPM: @azure/search-documents
- Referensdokumentation: Klientbibliotek
Funktionsappen autentiserar via SDK:n till det molnbaserade Azure AI Search-API:et med hjälp av resursnamnet, API-nyckeln och indexnamnet. Hemligheterna lagras i inställningarna för den statiska webbappen och hämtas till funktionen som miljövariabler.
Konfigurera hemligheter i en konfigurationsfil
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: Sök i katalogen
Sök-API:et tar en sökterm och söker i dokumenten i sökindexet och returnerar en lista med matchningar.
Azure-funktionen hämtar sökkonfigurationsinformationen och uppfyller frågan.
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
}
}
}
}
});
Klient: Sök från katalogen
Anropa Azure-funktionen i React-klienten med följande kod.
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>
</>
);
}
Klient: Fasetter från katalogen
Den här React-komponenten innehåller söktextrutan och de fasetter som är associerade med sökresultaten. Fasetter måste tänkas ut och utformas som en del av sökschemat när sökdata läses in. Sedan används fasetter i sökfrågan, tillsammans med söktexten, för att tillhandahålla den fasetterade navigeringsupplevelsen.
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>
);
}
Klient: Sidnumrering från katalogen
När sökresultaten expanderar bortom några få (8) ger komponenten @mui/material/TablePagination
sidnumrering över resultaten.
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`
}
/>
);
}
När användaren ändrar sidan skickas det värdet till den överordnade Search.js
sidan från handleChangePage
funktionen. Funktionen skickar en ny begäran till sök-API:et för samma fråga och den nya sidan. API-svaret uppdaterar fasetter, resultat och pager-komponenter.
Azure-funktion: Förslag från katalogen
Api:et Föreslå tar en sökterm medan en användare skriver och föreslår söktermer som boktitlar och författare i dokumenten i sökindexet, vilket returnerar en liten lista med matchningar.
Sökförslagsverktygetsg
, , definieras i schemafilen som används vid massuppladdning.
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
}
}
}
}
});
Klient: Förslag från katalogen
Api:et för funktionen Suggest anropas i React-appen som \src\components\SearchBar\SearchBar.js
en del av komponentinitiering:
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>
);
}
Den här React-komponenten använder komponenten @mui/material/Autocomplete
för att tillhandahålla en söktextruta, som också stöder visningsförslag (med hjälp av renderInput
funktionen). Automatisk komplettering startar när de första tecknen har angetts. När varje nytt tecken anges skickas det som en fråga till sökmotorn. Resultatet visas som en kort lista med förslag.
Den här funktionen för automatisk komplettering är en vanlig funktion, men den här specifika implementeringen har ytterligare ett användningsfall. Kunden kan ange text och välja bland förslagen eller skicka sin angivna text. Indata från förslagslistan samt indata från textrutan måste spåras efter ändringar, vilket påverkar hur formuläret återges och vad som skickas till sök-API:et när formuläret skickas.
Om ditt användningsfall för sökning tillåter att användaren bara väljer bland förslagen, minskar det omfattningen av kontrollens komplexitet men begränsar användarupplevelsen.
Azure-funktion: Hämta specifikt dokument
Uppslags-API:et tar ett ID och returnerar dokumentobjektet från sökindexet.
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
}
}
}
}
});
Klient: Hämta specifikt dokument
Det här funktions-API:et anropas i React-appen som \src\pages\Details\Detail.js
en del av komponentinitiering:
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>
);
}
Om klientappen kan använda förgenererat innehåll är den här sidan en bra kandidat för autogenerering eftersom innehållet är statiskt och hämtas direkt från sökindexet.
Nästa steg
I den här självstudieserien har du lärt dig hur du skapar och läser in ett sökindex i JavaScript, och du har skapat en webbapp som ger en sökupplevelse som innehåller ett sökfält, fasetterad navigering och filter, förslag, sidnumrering och dokumentsökning.
Som ett nästa steg kan du utöka det här exemplet i flera riktningar:
- Lägg till automatisk komplettering för mer typeahead.
- Lägg till eller ändra fasetter och filter.
- Ändra autentiserings- och auktoriseringsmodellen med hjälp av Microsoft Entra-ID i stället för nyckelbaserad autentisering.
- Ändra indexeringsmetodiken. I stället för att skicka JSON till ett sökindex läser du in en blobcontainer med datauppsättningen good-books och konfigurerar en blobindexerare för att mata in data. Om du vet hur du arbetar med indexerare får du fler alternativ för datainmatning och innehållsberikning under indexering.