4 – Explorer le code de recherche JavaScript
Dans les leçons précédentes, vous avez ajouté la recherche à une application web statique. Cette leçon met en évidence les étapes essentielles qui établissent l’intégration. Si vous recherchez un aide-mémoire sur l’intégration de la recherche dans votre application JavaScript, cet article explique ce que vous devez savoir.
Le code source est disponible dans le référentiel GitHub azure-search-javascript-samples.
Kit de développement logiciel (SDK) Azure @azure/search-documents
L’application de fonction utilise le Kit de développement logiciel (SDK) Azure pour la Recherche Azure AI :
- NPM : @azure/search-documents
- Documentation de référence : Bibliothèque de client
L’application de fonction s’authentifie via le Kit de développement logiciel (SDK) auprès de l’API Recherche Azure AI basée sur le cloud à l’aide du nom de la ressource, de la clé API et du nom de l’index. Les secrets sont stockés dans les paramètres de l’application web statique et extraits dans la fonction en tant que variables d’environnement.
Configurer des secrets dans un fichier de configuration
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 Function : Rechercher dans le catalogue
L’API Recherche cognitive prend un terme de recherche et effectue la recherche parmi les documents de l’index de recherche, en retournant une liste de correspondances.
Azure Functions extrait les informations de configuration de la recherche et exécute la requête.
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 : recherche dans le catalogue
Appelez Azure Function dans le client React à l’aide du code suivant.
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 : Facettes dans le catalogue
Ce composant React inclut la zone de texte de recherche et les facettes associées aux résultats de la recherche. Les facettes doivent être pensées et conçues dans le cadre du schéma de recherche lorsque les données de recherche sont chargées. Ensuite, les facettes sont utilisées dans la requête de recherche, ainsi que le texte de recherche, pour fournir l’expérience de navigation par facettes.
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 : Pagination depuis le catalogue
Lorsque les résultats de la recherche se développent au-delà d’un nombre insignifiant (8), le composant @mui/material/TablePagination
fournit une pagination entre les résultats.
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`
}
/>
);
}
Lorsque l’utilisateur modifie la page, cette valeur est envoyée à la page parente Search.js
à partir de la fonction handleChangePage
. La fonction envoie une nouvelle requête à l’API de recherche pour la même requête et la nouvelle page. La réponse de l’API met à jour les facettes, les résultats et les composants du récepteur de radiomessagerie.
Azure Function : suggestions à partir du catalogue
L’API Suggestion prend un terme de recherche lors de la saisie et suggère des termes de recherche tels que des titres et des auteurs de livres dans l’ensemble des documents de l’index de recherche, renvoyant une petite liste de correspondances.
Le générateur de suggestions de recherche, sg
, est défini dans le fichier de schéma utilisé lors du chargement en bloc.
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 : suggestions à partir du catalogue
L’API de fonction Suggest est appelée dans l’application React au niveau de \src\components\SearchBar\SearchBar.js
dans le cadre de l’initialisation du composant :
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>
);
}
Ce composant React utilise le composant @mui/material/Autocomplete
pour fournir une zone de texte de recherche, qui prend également en charge l’affichage des suggestions (à l’aide de la fonction renderInput
). L’autocomplétion démarre une fois que les premiers caractères sont entrés. À mesure que chaque nouveau caractère est entré, il est envoyé sous forme de requête au moteur de recherche. Les résultats sont affichés sous forme de courte liste de suggestions.
Cette fonctionnalité d’autocomplétion est une fonctionnalité courante, mais cette implémentation spécifique a un cas d’usage supplémentaire. Le client peut entrer du texte et sélectionner parmi les suggestions ou envoyer le texte qu’il a entré. L’entrée de la liste de suggestions ainsi que l’entrée de la zone de texte doivent être suivies pour les modifications, qui ont un impact sur le rendu du formulaire et sur ce qui est envoyé à l’API de recherche lors de l’envoi du formulaire.
Si votre cas d’usage pour la recherche permet à votre utilisateur de sélectionner uniquement parmi les suggestions, cela réduira l’étendue de la complexité du contrôle, mais limitera l’expérience utilisateur.
Azure Function : accéder à un document spécifique
L’API Recherche prend un ID et retourne l’objet de document depuis de l’index de recherche.
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 : accéder à un document spécifique
Cette API de fonction est appelée dans l’application React à l’emplacement \src\pages\Details\Detail.js
dans le cadre de l’initialisation du composant :
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>
);
}
Si votre application cliente peut utiliser du contenu prégénéré, cette page est un bon candidat pour la génération automatique, car le contenu est statique, extrait directement de l’index de recherche.
Étapes suivantes
Dans cette série de tutoriels, vous avez appris à créer et charger un index de recherche en JavaScript, et vous avez créé une application web qui fournit une expérience de recherche incluant une barre de recherche, une navigation à facettes et des filtres, des suggestions, une pagination et une recherche de document.
Comme étape suivante, vous pouvez étendre cet exemple dans plusieurs directions :
- Ajouter autocomplétion pour davantage de saisie semi-automatique.
- Ajouter ou modifier des facettes et des filtres.
- Modifiez le modèle d’authentification et d’autorisation à l’aide de l’ID Microsoft Entra au lieu de l’authentification par clé.
- Modifier la méthodologie d’indexation. Au lieu d’envoyer (push) JSON vers un index de recherche, préchargez un conteneur d’objets blob avec le jeu de données good-books et configurez un indexeur d’objets blob pour ingérer les données. Savoir comment utiliser les indexeurs vous offre plus d’options pour l’ingestion de données et l’enrichissement de contenu pendant l’indexation.
Commentaires
https://aka.ms/ContentUserFeedback.
Bientôt disponible : Tout au long de 2024, nous allons supprimer progressivement GitHub Issues comme mécanisme de commentaires pour le contenu et le remplacer par un nouveau système de commentaires. Pour plus d’informations, consultezEnvoyer et afficher des commentaires pour