Pesquisando sua localização com Atlas Search e operadores geoespaciais
Avalie esse Tutorial
Ao pensar em pesquisa de texto completo, texto e outros dados de string são provavelmente a primeira coisa que vem à mente. Na verdade, se você tem acompanhado meus tutoriais, deve se lembrar Construir um elemento de formulário de preenchimento automático com Atlas Search e JavaScript ou Exibir visualmente os destaques do Atlas Search com JavaScript e HTML, ambos em exemplos de pesquisa de texto em MongoDB Atlas Search.
A capacidade de usar a pesquisa em linguagem natural em dados de texto é provavelmente um dos casos de uso mais populares, mas há cenários em que você pode precisar restringir ainda mais os resultados.
Digamos que você esteja criando um aplicativo de avaliações de restaurantes como o Yelp ou um sistema de reservas de hospedagem e café da manhã como o Airbnb. Claro, você inserirá algum tipo de critério de texto para o que está procurando, mas também há um aspecto de localização. Por exemplo, se você quiser encontrar um lugar para comer um cheeseburger a uma curta distância de sua localização atual, provavelmente não quer que seus resultados de pesquisa contenham entradas de outro país. Este é um exemplo de uma pesquisa geográfica, onde você gostaria de retornar resultados com base em coordenadas de localização.
Neste tutorial, vamos ver como usar o Atlas Search operadorcomposto para pesquisar com base no texto inserido e dentro de uma determinada área geográfica. Para o texto inserido, usaremos o operador depreenchimento automático e, para o componente geoespacial, usaremos o operadorgeoWithin.
Para ter uma ideia do que queremos realizar, dê uma olhada na imagem animada a seguir:
O exemplo acima será semelhante ao que escrevi no tutorialConstruindo um elemento de formulário de preenchimento automático com Atlas Search e JavaScript, com a exceção de que, desta vez, estamos retornando resultados com base em um local fornecido.
Para este tutorial, usaremos JavaScript e Node.js com o MongoDB Atlas. Existirá um componente de frontend e backend, mas sem muitos requisitos anteriores. Para ter sucesso e acompanhar, você precisará do seguinte:
- MongoDB Atlas (cluster M0+)
- Node.js (15.9.0+)
- O conjunto de dados de amostrasample_airbnb(carregue gratuitamente no Atlas).
As versões acima são apenas as versões que estou usando. Você também pode ter sucesso com uma versão mais antiga do Node.js. Para o MongoDB Atlas, você pode usar um cluster GRÁTIS M0 ou algo mais poderoso.
Usaremos um conjunto de dados de amostra para este exemplo. Você pode saber mais sobre sample_airbnb e outros na documentação.
Vamos construir uma API, mas ela terá um único endpoint. O objetivo desta API é permitir que nosso front-end interaja com o MongoDB.
No seu computador, crie um novo diretório para nosso back-end e execute o seguinte na linha de comando:
1 npm init -y 2 npm install mongodb express cors
Os comandos acima criarão um novo arquivopackage.json e, em seguida, baixarão as dependências do MongoDB e do Express Framework. Como teremos nosso back-end e front-end executando localmente em portas diferentes, a instalação de um pacote de compartilhamento de recursos de origem cruzada (CORS) também é necessária.
Em vez de voltar ao básico com o MongoDB, vamos usar o seguinte código JavaScript padronizado:
1 const { MongoClient } = require("mongodb"); 2 const Express = require("express"); 3 const Cors = require("cors"); 4 5 const app = Express(); 6 7 app.use(Express.json()); 8 app.use(Express.urlencoded({ extended: true })); 9 app.use(Cors()); 10 11 const client = new MongoClient( 12 process.env["ATLAS_URI"], 13 { 14 useUnifiedTopology: true 15 } 16 ); 17 18 var collection; 19 20 app.post("/search", async (request, response, next) => { 21 console.log(JSON.stringify(request.body)); 22 try { 23 let result = await collection.aggregate([/* Search Logic Here */]).toArray(); 24 response.send(result); 25 } catch (e) { 26 response.status(500).send({ message: e.message }); 27 } 28 }); 29 30 app.listen(3000, async () => { 31 try { 32 await client.connect(); 33 collection = client.db("sample_airbnb").collection("listingsAndReviews"); 34 } catch (e) { 35 console.error(e); 36 } 37 });
Adicione o código acima a um arquivo principal.js dentro do seu diretório de projeto. Se você quiser um início rápido para o MongoDB com o Node.js,Lauron Scheefer escreveu uma série de várias partes para que você possa se atualizar.
Há uma coisa a ser observada no código acima:
1 const client = new MongoClient( 2 process.env["ATLAS_URI"], 3 { 4 useUnifiedTopology: true 5 } 6 );
Minhas informações de conexão do MongoDB Atlas estão sendo armazenadas como uma variável de ambiente no meu computador. Embora as variáveis de ambiente sejam a abordagem mais segura, certifique-se de trocá-las pelo que planeja usar.
Com o código padrão fora do caminho, podemos nos concentrar no que importa para este exemplo: o pipeline de agregação para pesquisa em texto e dados geoespaciais. No entanto, antes de começarmos a escrever os estágios do pipeline, precisamos indexar adequadamente nossos dados para pesquisa.
No MongoDB Atlas, selecione a aba Pesquisar de nível superior após escolher um dos seus clusters. Dentro desta aba, selecione o botão Criar Índice que o levará a um assistente de configuração para criar índices de Atlas Search.
Em vez de usar o editor visual para criar um índice, vamos usar o JSON Editor com a seguinte configuração. Forneça sample_airbnb como banco de dados e listingsAndReviews como collection. Você pode copiar e colar a seguinte configuração de índice:
1 { 2 "mappings": { 3 "dynamic": false, 4 "fields": { 5 "address": { 6 "fields": { 7 "location": { 8 "type": "geo" 9 } 10 }, 11 "type": "document" 12 }, 13 "name": [ 14 { 15 "foldDiacritics": false, 16 "maxGrams": 7, 17 "minGrams": 3, 18 "tokenization": "edgeGram", 19 "type": "autocomplete" 20 } 21 ] 22 } 23 } 24 }
Embora o nome do índice não afete sua funcionalidade, vamos chamá-lo depreenchimento automático e referenciá-lo em nosso aplicativo Node.js. Para detalhar o que o índice acima faz, estamos indexando dois campos nos documentos de nossa coleção. O campo
address.location
está sendo indexado como um campo geoespacial, enquanto o campo name
está sendo indexado como um campo de texto de preenchimento automático. Nenhum outro campo em nosso documento poderá ser pesquisado com base nesse índice.Ao final da criação do índice, você deve ter algo parecido com isto:
Então, let's Go back ao nosso código.
Sabemos que nossos resultados de pesquisa devem depender do texto que o usuário fornece e da localização do usuário (como latitude e longitude).
Se quisermos pesquisar apenas com texto, nosso estágio de pipeline de agregação (query) pareceria o seguinte:
1 { 2 "$search": { 3 "index": "autocomplete", 4 { 5 "autocomplete": { 6 "query": "apartment", 7 "path": "name", 8 "fuzzy": { 9 "maxEdits": 2, 10 "prefixLength": 3 11 } 12 } 13 } 14 } 15 }
O estágio acima diz que queremos usar o
autocomplete
índice para nossa pesquisa e queremos usar o autocomplete
operador . Estamos procurando por " apartment " no name
campo e estamos dizendo que estamos permitindo a tolerância a erros de digitação, também conhecida como correspondência difusa.Se quisermos usar o Atlas Search para o Atlas Search dentro de uma área geográfica, o estágio do pipeline de agregação teria a seguinte aparência:
1 { 2 "$search": { 3 "index": "autocomplete", 4 { 5 "geoWithin": { 6 "circle": { 7 "center": { 8 "type": "Point", 9 "coordinates": [-74.0060, 40.7128] 10 }, 11 "radius": 10000 12 }, 13 "path": "address.location" 14 } 15 } 16 } 17 }
O estágio acima diz mais uma vez que estamos usando o índice
autocomplete
, mas estamos usando o operador geoWithin
. Estamos pesquisando dentro de uma área circular onde o ponto central é especificado por uma latitude e uma longitude. Ao trabalhar com GeoJSON como no código acima, a longitude é o primeiro elemento na arraycoordinates
e a latitude é o segundo elemento. Também estamos fornecendo um raio para pesquisar em torno do ponto central.Acabamos de criar dois possíveis aggregation pipeline stages. O problema é que queremos ser eficientes. Não queremos pesquisar texto usando um estágio e, em seguida, aplicar uma faixa geográfica nos resultados em um estágio diferente. Em vez disso, queremos fazer nossas operações
autocomplete
e geoWithin
em uma única query.Podemos fazer isso com o operador
compound
.Para combinar várias operações, podemos alterar nosso código e a lógica do pipeline de agregação para se parecer com o seguinte:
1 app.post("/search", async (request, response, next) => { 2 try { 3 let result = await collection.aggregate([ 4 { 5 "$search": { 6 "index": "autocomplete", 7 "compound": { 8 "must": [ 9 { 10 "autocomplete": { 11 "query": `${request.body.query}`, 12 "path": "name", 13 "fuzzy": { 14 "maxEdits": 2, 15 "prefixLength": 3 16 } 17 } 18 }, 19 { 20 "geoWithin": { 21 "circle": { 22 "center": { 23 "type": "Point", 24 "coordinates": [request.body.position.lng, request.body.position.lat] 25 }, 26 "radius": 10000 27 }, 28 "path": "address.location" 29 } 30 } 31 ] 32 } 33 } 34 } 35 ]).toArray(); 36 response.send(result); 37 } catch (e) { 38 response.status(500).send({ message: e.message }); 39 } 40 });
Observe que estamos incluindo uma matriz
must
no operadorcompound
. Você pode aprender mais sobre cada um dos termos compostos na documentação, mas o operadormust
define quais cláusulas devem corresponder para produzir resultados.Para esclarecer as coisas, estamos dizendo que os resultados devem satisfazer os operadores
autocomplete
e geoWithin
.Agora, você pode executar o aplicativo Node.js e enviar a seguinte carga útil para o endpoint usando uma solicitação POST:
1 { 2 "query": "apartment", 3 "position": { 4 "lng": -74.0060, 5 "lat": 40.7128 6 } 7 }
Considerando os dados que estão no conjunto de dadossample_airbnb, devemos obter resultados em torno de Nova York. No entanto, os dados que recebemos provavelmente são mais do que precisamos. Para limitar a resposta, podemos atualizar nosso pipeline de agregação não apenas para pesquisar, mas também para projetar os campos que queremos em nossa resposta.
Modifique o código para que fique parecido com o seguinte:
1 app.post("/search", async (request, response, next) => { 2 try { 3 let result = await collection.aggregate([ 4 { 5 "$search": { 6 "index": "autocomplete", 7 "compound": { 8 "must": [ 9 { 10 "autocomplete": { 11 "query": `${request.body.query}`, 12 "path": "name", 13 "fuzzy": { 14 "maxEdits": 2, 15 "prefixLength": 3 16 } 17 } 18 }, 19 { 20 "geoWithin": { 21 "circle": { 22 "center": { 23 "type": "Point", 24 "coordinates": [request.body.position.lng, request.body.position.lat] 25 }, 26 "radius": 10000 27 }, 28 "path": "address.location" 29 } 30 } 31 ] 32 } 33 } 34 }, 35 { 36 "$project": { 37 "name": 1, 38 "address": 1, 39 "score": { "$meta": "searchScore" } 40 } 41 } 42 ]).toArray(); 43 response.send(result); 44 } catch (e) { 45 response.status(500).send({ message: e.message }); 46 } 47 });
1 { 2 "$project": { 3 "name": 1, 4 "address": 1, 5 "score": { "$meta": "searchScore" } 6 } 7 }
No estágio
$project
acima , estamos dizendo que queremos apenas os camposname
e address
retornados. Também estamos interessados nos dados de pontuação que retornaram de nossa pesquisa. Por padrão, os dados de pontuação não estariam presentes em nossos resultados. Esses dados podem ser úteis para determinar a qualidade da correspondência.Com a parte de trás fora do caminho, vamos nos concentrar na parte da frente.
Para visualizar o Atlas Search com preenchimento automático , vamos usar jQuery com um pouco de HTML e JavaScript básicos . A biblioteca jQuery será responsável por enviar nossas teclas para a API por meio de uma solicitação HTTP.
Crie outro diretório de projeto que representará o aplicativo de frontend. Dentro desse diretório, crie um arquivoindex.html com a seguinte marcação:
1 2 <html> 3 <head> 4 <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"> 5 <script src="https://code.jquery.com/jquery-1.12.4.js"></script> 6 <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> 7 </head> 8 <body> 9 <div class="ui-widget"> 10 <label for="bnb">Bed and Breakfast [40.7128, -74.0060]:</label><br /> 11 <input id="bnb"> 12 </div> 13 <script> 14 $(document).ready(function () { 15 // Autocomplete logic here... 16 }); 17 </script> 18 </body> 19 </html>
A marcação acima é padrão para começar a usar o jQuery — a exceção é o container
<div>
que tem o campo de entrada. O id
do campo de entrada vai ser importante para quando trabalharmos com jQuery.Então, vamos dar uma olhada na lógica de preenchimento automático do front-end.
1 $(document).ready(function () { 2 $("#bnb").autocomplete({ 3 source: async function (request, response) { 4 let data = await fetch("http://localhost:3000/search", { 5 "method": "POST", 6 "headers": { 7 "content-type": "application/json" 8 }, 9 "body": JSON.stringify({ 10 "query": `${request.term}`, 11 "position": { 12 "lat": 40.7128, 13 "lng": -74.0060 14 } 15 }) 16 }) 17 .then(results => results.json()) 18 .then(results => results.map(result => { 19 return { label: result.name, value: result.name, id: result._id }; 20 })); 21 response(data); 22 }, 23 minLength: 2, 24 select: function (event, ui) { 25 // Further logic here... 26 } 27 }); 28 });
No código acima, estamos utilizando a função
autocomplete
para jQuery no elemento de entrada bnb
. Osource
dos nossos dados a ser mostrado na tela sairá do nosso endpoint da API. Conforme os caracteres são inseridos no campo, uma solicitação POST é feita com a carga útil JSON esperada. Os resultados são então formatados para como o jQuery espera que eles sejam, neste caso, com um campolabel
, value
e id
dentro de um objeto.Como queremos um escopo restrito para este exemplo, não analisaremos a lógica de quando um elemento é selecionado nos resultados do autocompletar retornados. No entanto, apenas o fato de termos o campo
source
nos permitirá mostrar visualmente os resultados do autocompletar à medida que os digitamos.Para executar este exemplo, você precisará servir o back-end e o front-end separadamente. Para o back-end, navegue até o diretório do projeto com sua linha de comando e execute o seguinte:
1 node main.js
O comando acima deve começar a veicular a API na porta 3000. Para atender ao frontend, você precisará do Python ou de uma ferramenta ou pacote compatível como serve, disponível por meio do NPM.
Se o Python estiver disponível, você poderá executar o seguinte a partir do diretório do projeto de frontend:
1 python -m SimpleHTTPServer
O comando acima servirá o front-end na porta 8000.
É claro que o que listei para atender aos seus aplicativos era para desenvolvimento e teste local. Você terá que usar seu bom senso ao implantar seus aplicativos na produção.
Você acabou de ver como usar o operador
compound
do MongoDB Atlas Search para pesquisar com base em texto e em uma área geoespacial, neste caso, um círculo. Por que isso pode ser importante? Imagine precisar procurar um hotel ou restaurante perto de sua localização ou a uma curta distância a seguir, em vez de retornar todas as correspondências possíveis com base em sua entrada de texto. O operadorcompound
permite pesquisar resultados somente se eles corresponderem aos termos compostos fornecidos.Para saber como criar mais no Atlas Search, confira meus outros tutoriais: Criando um elemento de formulário com preenchimento automático com Atlas Search e JavaScript e Mostrando visualmente os destaques do Atlas Search com JavaScript e HTML.