Mostrando visualmente os destaques do Atlas Search com JavaScript e HTML
Avalie esse Tutorial
Quandose trata de encontrar palavras ou frases específicas no texto, você provavelmente vai querer usar uma opção de pesquisa em linguagem natural, como a pesquisa de texto completo (FTS). É claro que você poderia criar um conjunto de expressões regulares complicado e difícil de manter para pesquisar no texto, mas essa é uma opção que a maioria dos desenvolvedores não deseja. Isso sem falar que não abrangerá todo o escopo do que um processador de linguagem natural normalmente realiza.
Em um tutorial anterior intitulado Construindo um elemento de formulário de preenchimento automático com Atlas Search e JavaScript, escrevi sobre a pesquisa de receitas à medida que são digitadas no MongoDB Atlas usando o operador
autocomplete
. Embora este tutorial tenha feito o trabalho muito bem, ele não detalhou o que exatamente estava sendo correspondido para qualquer termo específico.Neste tutorial, vamos ver como usar o Atlas Search e trabalhar com os dados de destaque para mostrar visualmente quaisquer correspondências nos termos em um aplicativo voltado para o usuário. O destaque é uma ferramenta poderosa da Pesquisa para permitir que os usuários encontrem o texto exato que desejam em seu contexto adequado.
Para ter uma ideia do que planejamos realizar, dê uma olhada na seguinte imagem animada:
No cenário acima, estamos pesquisando mensagens em uma sala de bate-papo. Quando inserimos um termo no Atlas Search, obtemos as mensagens de bate-papo em resposta, com quaisquer acertos em potencial destacados. Os possíveis acertos podem corresponder exatamente ou podem ter um determinado nível de imprecisão que exploraremos. Neste exemplo específico, o número de respostas destacadas é limitado a cinco.
Antes de entrarmos diretamente na criação do back-end para pesquisa e do front-end para exibição, precisamos ter uma ideia do nosso modelo de dados. Vamos supor que estamos trabalhando com dados de bate-papo do usuário e queremos pesquisar determinadas palavras e frases. Com isso em mente, nossos documentos poderiam ficar assim:
1 { 2 "_id": "mongodb", 3 "messages": [ 4 { 5 "sender": "nraboy", 6 "message": "Hello World" 7 } 8 ] 9 }
O exemplo de documento acima não é o mais realista, mas nos dá alguma coisa. Toda vez que uma nova mensagem é adicionada à sala de bate-papo, ela é anexada ao array
messages
com as informações do remetente associado. Poderíamos tornar isso muito mais complexo, mas não é necessário para este exemplo.O próximo passo é criar um índice de pesquisa padrão em nossa collection de dados. Para este exemplo, usaremos um banco de dados
gamedev
e uma collectionchats
.Embora possamos criar um índice específico para os campos que estamos planejando usar, por questões de simplicidade, a criação de um índice dinâmico padrão será mais do que suficiente. Para fazer isso, basta clicar no botão verde Criar Índice do Atlas Search. Vamos aceitar as configurações padrão e clicar em Criar índice. Isso nos fornecerá o índice padrão com a seguinte configuração:
1 { 2 "mappings": { 3 "dynamic": true 4 } 5 }
O analisador
lucene.standard
é o analisador padrão para o Atlas Search e mais informações sobre ele podem ser encontradas na documentação.Neste exemplo, precisaremos de um back-end para lidar com a interação com o banco de dados para pesquisa. Para manter a pilha consistente neste exemplo, vamos usar o Node.js com algumas dependências comuns.
Crie um novo diretório em seu computador e, na linha de comando, execute o seguinte:
1 npm init -y 2 npm install express mongodb cors --save
Os comandos acima criarão um novo arquivopackage.json e baixarão o Express Framework, o driver Node.js do MongoDB e um middleware de compartilhamento de recursos entre origens que nos permitirá acessar nosso back-end a partir do front-end operando em uma porta diferente.
No mesmo diretório do projeto, crie um arquivomain.js e adicione o seguinte código padrão do Express Framework com MongoDB:
1 const { MongoClient, ObjectID } = require("mongodb"); 2 const Express = require("express"); 3 const Cors = require("cors"); 4 const { request } = require("express"); 5 6 const client = new MongoClient(process.env["ATLAS_URI"]); 7 const server = Express(); 8 9 server.use(Cors()); 10 11 var collection; 12 13 server.get("/search", async (request, response) => { }); 14 15 server.listen("3000", async () => { 16 try { 17 await client.connect(); 18 collection = client.db("gamedev").collection("chats"); 19 } catch (e) { 20 console.error(e); 21 } 22 });
No código acima, estamos importando cada uma de nossas dependências e inicializando o Express Framework, bem como o MongoDB. O
ATLAS_URI
no exemplo acima deve ser armazenado como uma variável de ambiente em seu computador. Você pode obtê-la no painel do MongoDB Atlas e ela terá a seguinte aparência:1 mongodb+srv://<username>:<password>@cluster0-yyarb.mongodb.net/<dbname>?retryWrites=true&w=majority
Claro, não use meu exemplo acima porque você precisará usar suas próprias informações de cluster. Para obter ajuda para começar a usar o MongoDB Atlas, confira meu tutorial anterior sobre o assunto.
Observe a seção do código em que estamos ouvindo as conexões:
1 server.listen("3000", async () => { 2 try { 3 await client.connect(); 4 collection = client.db("gamedev").collection("chats"); 5 } catch (e) { 6 console.error(e); 7 } 8 });
No código acima, estamos nos conectando ao cluster especificado do MongoDB Atlas e estamos obtendo um identificador para a coleção
chats
no banco de dadosgamedev
. Sinta-se à vontade para usar sua própria coleção e nomeação de banco de dados, mas observe que este exemplo seguirá o modelo de dados definido anteriormente no que se refere à pesquisa.Com o boilerplate em vigor, vamos pular para o endpoint
/search
que está atualmente vazio. Em vez disso, vamos querer alterá-lo para o seguinte:1 server.get("/search", async (request, response) => { 2 try { 3 let result = await collection.aggregate([ 4 { 5 "$search": { 6 "text": { 7 "query": `${request.query.term}`, 8 "path": "messages.message", 9 "fuzzy": { 10 "maxEdits": 2 11 } 12 }, 13 "highlight": { 14 "path": "messages.message" 15 } 16 } 17 }, 18 { 19 "$addFields": { 20 "highlights": { 21 "$meta": "searchHighlights" 22 } 23 } 24 } 25 ]).toArray(); 26 response.send(result); 27 } catch (e) { 28 response.status(500).send({ message: e.message }); 29 } 30 });
No código de endpoint acima, estamos criando um pipeline de agregação.
Como planejamos usar o Atlas Search, o operador
$search
precisa ser o primeiro estágio no pipeline. Neste primeiro estágio, estamos pesquisando em torno de um termo fornecido. Em vez de pesquisar o documento inteiro, estamos pesquisando dentro do objeto message
da array messages
. O campofuzzy
com um maxEdits
de 2
define o número de edições de caracteres únicos necessárias para corresponder ao termo de pesquisa especificado. Por exemplo, se inserirmos hlo
, podemos obter um resultado em hello
, onde, como se não tivéssemos definido as informações difusas, um resultado pode não ser encontrado. Mais informações podem ser encontradas na documentação.O segundo estágio do pipeline adicionará os dados de destaque aos resultados antes de serem retornados ao cliente. Os metadados de destaque não fazem parte do documento original, daí a necessidade de adicioná-los usando o operador $meta antes da resposta. Você pode ler mais sobre o operador
$meta
e os metadados que ele pode exibir na documentação. Você também pode usar o operador$meta
em um estágio$project
em vez de $addFields
.Como este é um pipeline de agregação do MongoDB, você pode combinar qualquer número de operadores de agregação, desde que
$search
seja o primeiro no pipeline.Se houver dados na coleção, o aplicativo estará pronto para ser usado.
O próximo passo é exibir os dados de pesquisa na tela. A maior parte do que vem a seguir diz respeito à massagem dos dados em um formato que queremos usar, o que inclui destacar visualmente os dados com marcação HTML.
Vamos precisar criar outro diretório de projeto, desta vez representando o front-end em vez do back-end. Dentro deste novo diretório, crie um arquivoindex.html com a seguinte marcação:
1 2 <html> 3 <head></head> 4 <body> 5 <div> 6 <input id="term" type="text" /> 7 <button type="button" onclick="search()">Search</button> 8 </div> 9 <br /> 10 <div id="output"></div> 11 <script> 12 const search = async () => { 13 let term = document.getElementById("term"); 14 let output = document.getElementById("output"); 15 }; 16 </script> 17 </body> 18 </html>
No código acima, temos um formulário que chama uma função
search
quando o botão é clicado. A partir de agora, a funçãosearch
obtém somente o termo do Atlas Search e faz referência à área onde os resultados do Atlas Search devem ser produzidos.Vamos restringir ainda mais o que a função
search
deve fazer.1 const search = async () => { 2 let term = document.getElementById("term"); 3 let output = document.getElementById("output"); 4 output.innerHTML = ""; 5 let result = await fetch("http://localhost:3000/search?term=" + term.value) 6 .then(response => response.json()); 7 result.forEach(chat => { 8 let messageContainer = document.createElement("div"); 9 messageContainer.innerHTML = `<strong>Chat ${chat._id}</strong>`; 10 chat.messages.forEach(msg => { 11 let message = document.createElement("p"); 12 chat.highlights.forEach(highlight => { 13 let texts = highlight.texts; 14 let replacements = texts.map(text => { 15 if(text.type == "hit") { 16 return "<mark>" + text.value + "</mark>"; 17 } else { 18 return text.value; 19 } 20 }).join(""); 21 let originals = texts.map(text => { 22 return text.value; 23 }).join(""); 24 msg.message = msg.message.replace(originals, replacements); 25 }); 26 message.innerHTML = msg.sender + ": " + msg.message; 27 messageContainer.appendChild(message); 28 }); 29 output.appendChild(messageContainer); 30 }); 31 };
As modificações acima na função podem ser muito para incluir. Vamos detalhar o que está rolando.
Depois de limpar o espaço de saída, estamos fazendo uma solicitação para o back-end:
1 let result = await fetch("http://localhost:3000/search?term=" + term.value) 2 .then(response => response.json());
Os resultados dessa solicitação terão os documentos encontrados, bem como os dados de destaque associados à pesquisa.
A próxima etapa será analisar cada um dos resultados e, em seguida, cada uma das mensagens dos resultados. É aqui que as coisas podem ficar um pouco confusas. O MongoDB retornará dados semelhantes aos seguintes quando se trata de destacar:
1 { 2 "path": "messages.message", 3 "texts": [ 4 { 5 "value": "This is another ", 6 "type": "text" 7 }, 8 { 9 "value": "Hello", 10 "type": "hit" 11 }, 12 { 13 "value": " world example", 14 "type": "text" 15 } 16 ], 17 "score": 0.7454098463058472 18 },
Ele não faz exatamente o realce visual para nós. Em vez disso, ele nos dirá qual termo ou frase teve um sucesso em potencial e o texto adjacente. Com essas informações, precisamos destacar o acerto no JavaScript.
1 let texts = highlight.texts; 2 let replacements = texts.map(text => { 3 if(text.type == "hit") { 4 return "<mark>" + text.value + "</mark>"; 5 } else { 6 return text.value; 7 } 8 }).join(""); 9 let originals = texts.map(text => { 10 return text.value; 11 }).join("");
Aqui, estamos construindo uma string a partir das peças de destaque originais, bem como uma string onde o acerto é envolto em marcação. O objetivo é usar a função
replace
no JavaScript, que exige um termo ou frase de pesquisa, bem como a substituição. Não podemos simplesmente substituir o resultado, porque e se nosso resultado fosse hello
enquanto helloworld
existia no chat sem espaços? A substituição do JavaScript não analisa as palavras de forma natural, portanto, a substituição automática de hello
resultaria em helloworld
sendo destacado incorretamente. É por isso que precisamos trabalhar com os dados adjacentes que o MongoDB retorna.Depois de fazer a substituição do JavaScript pela string original e pela string modificada, podemos prepará-la para a saída com o seguinte:
1 message.innerHTML = msg.sender + ": " + msg.message; 2 messageContainer.appendChild(message);
Como mencionado anteriormente, o front-end está realmente fazendo muitas manipulações visuais usando os dados de resultado e destaque que o back-end criou.
Você acabou de ver como destacar visualmente os resultados de pesquisa na tela usando os dados de destaque retornados com o MongoDB Atlas Search. Embora destacar os resultados da pesquisa com marcação HTML e JavaScript não seja totalmente necessário, é uma ótima maneira de aprender sobre seus dados e como suas pesquisas estão operando.
Para saber mais sobre Atlas Search e criar um formulário de preenchimento automático, vale a pena conferir meu tutorial anterior sobre o tópico.