4 - Explorar o código de pesquisa do JavaScript
Nas lições anteriores, você adicionou a pesquisa a um aplicativo Web estático. Esta lição destaca as etapas essenciais que estabelecem a integração. Se você estiver procurando uma folha de referências para integrar a pesquisa ao seu aplicativo JavaScript, este artigo explicará o que você precisa saber.
O código-fonte está disponível no repositório GitHub azure-search-javascript-samples.
SDK do Azure @azure/search-documents
O aplicativo de funções usa o SDK do Azure para Azure AI Search:
- NPM: @azure/search-documents
- Documentação de referência: Biblioteca de clientes
O aplicativo de funções faz a autenticação por meio do SDK na API do Azure AI Search baseada em nuvem usando o nome do recurso, a chave da API e o nome do índice. Os segredos são armazenados nas configurações do aplicativo Web estático e inseridos na função como variáveis de ambiente.
Configurar segredos em um arquivo de configuração
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 };
Função do Azure: pesquisar o catálogo
A API de Pesquisa usa um termo de pesquisa e o procura nos documentos no índice de pesquisa, retornando uma lista de correspondências.
A Função do Azure efetua pull das informações de configuração de pesquisa e preenche a consulta.
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
}
}
}
}
});
Cliente: pesquisar no catálogo
Chame a Função do Azure no cliente do React com o código a seguir.
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>
</>
);
}
Cliente: facetas do catálogo
Esse componente React inclui a caixa de texto de pesquisa e as facetas associadas aos resultados da pesquisa. As facetas precisam ser pensadas e projetadas como parte do esquema de pesquisa quando os dados de pesquisa são carregados. Em seguida, as facetas são usadas na consulta de pesquisa, juntamente com o texto de pesquisa, para fornecer a experiência de navegação facetada.
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>
);
}
Cliente: Paginação do catálogo
Quando os resultados da pesquisa se expandem além de alguns triviais (8), o componente @mui/material/TablePagination
fornece a paginação entre os resultados.
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`
}
/>
);
}
Quando o usuário altera a página, esse valor é enviado para a página pai Search.js
da função handleChangePage
. A função envia uma nova solicitação à API de pesquisa para a mesma consulta e a nova página. A resposta da API atualiza as facetas, os resultados e os componentes do pager.
Função do Azure: sugestões do catálogo
A API de Sugestão usa um termo de pesquisa enquanto um usuário digita um texto e sugere termos de pesquisa, como títulos de livros e autores nos documentos do índice de pesquisa, retornando uma pequena lista de correspondências.
O sugestor de pesquisa, sg
, é definido no arquivo de esquema usado durante o upload em massa.
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
}
}
}
}
});
Cliente: sugestões do catálogo
A API de função de Sugerir é chamada no aplicativo React em \src\components\SearchBar\SearchBar.js
como parte da inicialização do componente:
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>
);
}
Esse componente React usa o componente @mui/material/Autocomplete
para fornecer uma caixa de texto de pesquisa, que também dá suporte à exibição de sugestões (usando a funçãorenderInput
). O preenchimento automático é iniciado depois que os primeiros caracteres são inseridos. À medida que cada novo caractere é inserido, ele é enviado como uma consulta para o mecanismo de pesquisa. Os resultados são exibidos como uma pequena lista de sugestões.
Essa funcionalidade de preenchimento automático é um recurso comum, mas essa implementação específica tem um caso de uso adicional. O cliente pode inserir texto e selecionar entre as sugestões ou enviar o texto inserido. A entrada da lista de sugestões, bem como a entrada da caixa de texto, devem ser controladas quanto a alterações, que afetam como o formulário é renderizado e o que é enviado à API de pesquisa quando o formulário é enviado.
Se o seu caso de uso para pesquisa permitir que o usuário selecione apenas entre as sugestões, isso reduzirá o escopo de complexidade do controle, mas limitará a experiência do usuário.
Função do Azure: obter um documento específico
A API Lookup usa uma ID e retorna o objeto de documento do índice de pesquisa.
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
}
}
}
}
});
Cliente: obter um documento específico
Essa API de função é chamada no aplicativo React em \src\pages\Details\Detail.js
como parte da inicialização do componente:
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>
);
}
Se o aplicativo cliente puder usar conteúdo pré-gerado, essa página será uma boa candidata à geração automática porque o conteúdo é estático, extraído diretamente do índice de pesquisa.
Próximas etapas
Nesta série de tutoriais, você aprendeu a criar e carregar um índice de pesquisa no JavaScript e criou um aplicativo Web que fornece uma experiência de pesquisa que inclui uma barra de pesquisa, faceted navigation e filtros, sugestões, paginação e pesquisa de documentos.
Como próxima etapa, você pode estender este exemplo em várias direções:
- Adicionar preenchimento automático para mais digitação antecipada.
- Adicionar ou modificar facetas e filtros.
- Alterar o modelo de autenticação e autorização usando o Microsoft Entra ID em vez da autenticação baseada em chave.
- Alterar a metodologia de indexação. Em vez de enviar JSON por push para um índice de pesquisa, pré-carregar um contêiner de blob com o conjunto de dados good-books e configurar um indexador de blob para ingerir os dados. Saber como trabalhar com indexadores oferece mais opções para ingestão de dados e enriquecimento de conteúdo durante a indexação.