Aperol Spritze de verão com queries geoespaciais e pesquisa vetorial do MongoDB
Avalie esse Tutorial
É verão na cidade de Nova York e você sabe o que isso significa: É a estação do spritz! Não há nada (e eu realmente, 110% não significa nada) melhor do que um spritz fresco de Aperol para terminar um dia tão quente e abafado que o metrô era intercambiável com uma sauna.
Embora eu normalmente adore me aventurar pela cidade em busca do que satisfará perfeitamente meu desejo atual, há certos meses em que me recuso a gastar mais tempo do que o necessário me movendo ao ar livre (olá, onda de calor ?!). À noite, durante o verão de Nova York, estamos descansando - descansando em telhados, terraços e calçadas - onde quer que possamos caber. E com o mínimo de movimento, queremos que nosso Aperol borrife o mais próximo possível. Então, vamos usar consultas geoespaciais do MongoDB, aAtlas Vector Searche a API Google Places para encontrar nossos locais de spritz mais próximos no bairro de West Village, na cidade de Nova York, enquanto usamos a pesquisa semântica para nos ajudar a aproveitar ao máximo nossas consultas.
Neste tutorial, usaremos as várias plataformas listadas acima para encontrar todos os locais que vendem Aperol spritzes no bairro de West Village, na cidade de Nova York, aqueles que correspondem à nossa consulta semântica de estar ao ar livre com serviço rápido (precisamos desses spritzes e precisamos deles AGORA!) e o mais próximo do nosso local de partida.
Antes de começarmos o tutorial, Go examinar algumas das plataformas importantes que usaremos em nossa jornada.
As consultas geoespaciais do MongoDB permitem que você pesquise seu banco de dados com base em localizações geográficas! Isso significa que você pode encontrar locais diferentes, como restaurantes, Parques, Museos, etc. com base apenas em suas coordenadas. Neste tutorial, usaremos as queries geoespaciais do MongoDB para pesquisar os locais de locais que atendem aos spritzes do Aperol que obtivemos da API de locais do Google. Para utilizar queries geoespaciais corretamente com MongoDB, precisaremos garantir que nossos pontos de dados sejam carregados no formato GeoJSON. Mais sobre isso abaixo!
O MongoDB Atlas Vector Search é uma maneira de pesquisar em seu banco de dados semanticamente ou por significado. Isso significa que, em vez de pesquisar com base em palavras-chave específicas ou frases de texto exatas, você pode recuperar resultados mesmo que uma palavra esteja escrita incorretamente ou recuperar resultados com base em sinônimos. Isso se integrará perfeitamente ao nosso tutorial, pois podemos pesquisar as avaliações que recuperamos da nossa API do Google Places e ver quais correspondem mais ao que estamos procurando. Vamos lá!
Para ser bem-sucedido com este tutorial, você precisará de:
- O IDE de sua escolha — este tutorial usa um bloco de notas doGoogle Colib. Sinta-se à vontade para executar seus comandos diretamente de um bloco de anotações.
- Uma chave de API OpenAI - é assim que incorporaremos nossas revisões de localização para que possamos usar o MongoDB Atlas Vector Search!
Depois que seu cluster MongoDB Atlas tiver sido provisionado e você tiver todo o resto anotado em um local seguro, você estará pronto para começar. Certifique-se também de ter permitido "Acesso de qualquer lugar" em seu cluster MongoDB, em "Acesso à rede". Isso não é recomendado para produção, mas é usado neste tutorial para facilitar a referência. Sem isso, você não poderá gravar em seu cluster MongoDB.
Nosso primeiro passo é criar um projeto dentro de nossa conta Google Cloud. Isso é para que possamos garantir o uso da API do Google Places para encontrar todos os locais que servem spritzes Aperol no West Village.
É assim que seu projeto ficará depois de criado. Certifique-se de configurar as informações da sua conta de cobrança no lado esquerdo da tela. Você pode configurar um teste gratuito de $300 em créditos, então, se você estiver experimentando este tutorial, sinta-se à vontade para fazer isso e economizar algum dinheiro!
Depois que sua conta estiver configurada, habilitaremos a API do Google place que vamos usar. Você pode fazer isso por meio do mesmo link para configurar seu projeto do Google Cloud.
Esta é a API que queremos usar:
Pressione o botão Ativar e um pop-up aparecerá com sua chave de API. Armazene-o em algum lugar seguro, pois o usaremos em nosso tutorial! Certifique-se de não perdê-lo ou expô-lo a qualquer lugar.
Com cada solicitação da API de locais, sua chave de API deve ser usada. Você pode encontrar mais informações na documentação.
Uma vez feito isso, podemos começar nosso tutorial.
Agora, vá até o seu bloco de anotações do Google CoLab.
Queremos instalar
googlemaps
e openai
em nosso notebook, pois eles são necessários para nós ao criar este tutorial.1 !pip install googlemaps 2 !pip install openai==0.28
Em seguida, defina e execute suas importações:
1 import googlemaps 2 import getpass 3 import openai
Vamos usar a biblioteca
getpass
para manter nossas chaves API em segredo.Configure-o para sua chave de API do Google e sua chave de API OpenAI:
1 # google API Key 2 google_api_key = getpass.getpass(prompt= "Put in Google API Key here") 3 map_client = googlemaps.Client(key=google_api_key) 4 # openAI API Key 5 openai_api_key = getpass.getpass(prompt= "Put in OpenAI API Key here")
Agora, vamos nos preparar para o sucesso do Vector Search. Primeiro, defina sua chave e, em seguida, estabeleça nossa função de incorporação. Para este tutorial, estamos usando o modelo de incorporação "text-embedding-3-small" da OpenAI. Vamos incorporar as avaliações de nossos locais de spritz para que possamos fazer alguns julgamentos sobre para onde ir!
1 # set your key 2 openai.api_key = openai_api_key 3 4 # embedding model we are using 5 EMBEDDING_MODEL = "text-embedding-3-small" 6 7 # our embedding function 8 def get_embedding(text): 9 response = openai.Embedding.create(input=text, model=EMBEDDING_MODEL) 10 return response['data'][0]['embedding']
Ao usar a Pesquisa nas proximidades em nossa API do Google Places, somos obrigados a configurar três parâmetros: localização, raio e palavra-chave. Para nossa localização, podemos encontrar nossas coordenadas iniciais (bem no meio do West Village) clicando com o botão direito no Google Maps e copiando as coordenadas para nossa área de transferência. Foi assim que obtive as coordenadas mostradas abaixo:
Para o nosso raio, temos que ter em metros. Como não sou muito experiente com medidores, vamos escrever uma pequena função para nos ajudar a fazer essa conversão.
1 # for Google Maps API we need to use a radius in meters. Let's first change our miles to meters 2 def miles_to_meters(miles): 3 return miles * 1609.344
Nossa palavra-chave será apenas o que esperamos encontrar na API do Google Places: Aperol spritzes!
1 middle_of_west_village = (40.73490473393682, -74.00521094160642) 2 search_radius = miles_to_meters(0.4) # West Village is small so just do less than half a mile. 3 spritz_finder = 'aperol spritz'
Podemos então fazer nossa chamada API usando o método
places_nearby
.1 # making the API call using our places_nearby method and our parameters 2 response = map_client.places_nearby( 3 location=middle_of_west_village, 4 radius=search_radius, 5 keyword=spritz_finder 6 )
Antes de Go e imprimirmos nossas localizações, vamos pensar em nosso objetivo final. Queremos realizar algumas coisas antes de inserir nossos documentos em nosso MongoDB Atlas cluster. Queremos:
- Obtenha informações detalhadas sobre nossas localizações, então precisamos fazer outra chamada de API para obter nosso
place_id
, o localname
, nossoformatted_address
,geometry
para nossas coordenadas, algunsreviews
(somente até cinco) e o localrating
. Você pode encontrar mais campos para retornar (se seu coração desejar!) da documentação do Nearby Search. - Incorpore nossas avaliações para cada local usando nossa função de incorporação. Queremos ter a certeza de que temos um campo para estes, para que nossos vetores sejam armazenados em uma array dentro do nosso cluster. Estamos optando por incorporar aqui apenas para facilitar as coisas para nós a longo prazo. Vamos também unir as cinco revisões em uma string para facilitar um pouco as coisas na incorporação.
- Pense em como nossas coordenadas estão configuradas, enquanto criamos um dicionário com todas as informações importantes que queremos retratar.As queries geoespaciais do MongoDB exigemobjetos GeoJSON. Isto significa que precisamos garantir que tenhamos o formato adequado, caso contrário, não poderemos usar nossos operadores de queries geoespaciais posteriormente. Também precisamos ter em mente que a longitude e a latitude são armazenadas em uma array aninhada abaixo
geometry
elocation
dentro da API do Google place. Então, desatualmente, não podemos acessá-lo a partir do nível superior. Precisamos fazer alguma tipoia primeiro. Aqui está um exemplo da saída que copiei da documentação deles mostrando onde a latitude e a longitude estão aninhadas:
1 { 2 "html_attributions": [], 3 "results": 4 [ 5 { 6 "business_status": "OPERATIONAL", 7 "geometry": 8 { 9 "location": { "lat": -33.8587323, "lng": 151.2100055 }, 10 "viewport": 11 { 12 "northeast": 13 { "lat": -33.85739847010727, "lng": 151.2112436298927 }, 14 "southwest": 15 { "lat": -33.86009812989271, "lng": 151.2085439701072 }, 16 },
Com tudo isso em mente, vamos começar!
1 # find information we want: use the Nearby Places documentation to figure out which fields you want 2 spritz_locations = [] 3 for location in response.get('results', []): 4 location_detail = map_client.place( 5 place_id=location['place_id'], fields=['name', 'formatted_address', 'geometry', 'reviews', 'rating'] 6 ) 7 8 9 # these are the specific details we want to be saved as fields in our documents 10 details = location_detail.get('result', {}) 11 12 13 # we want to embed the five reviews so lets extract and join together 14 location_reviews = details.get('reviews', []) 15 store_reviews = [review['text'] for review in location_reviews[:5]] 16 joined_reviews = " ".join(store_reviews) 17 18 19 # generate embedding on your reviews 20 embedding_reviews = get_embedding(joined_reviews) 21 22 23 # we know that the longitude and latitude is nested inside Geometry and Location. 24 # so let's grab it using .get and then format it how we want. 25 geometry = details.get('geometry', {}) 26 location = geometry.get('location', {}) 27 28 29 # both are nested under location so open it up 30 longitude = location.get('lng') 31 latitude = location.get('lat') 32 33 34 location_info = { 35 'name': details.get('name'), 36 'address': details.get('formatted_address'), 37 38 39 # MongoDB geospatial queries require GeoJSON formatting 40 'location': { 41 'type': 'Point', 42 'coordinates': [longitude, latitude] 43 }, 44 'rating': details.get('rating'), 45 'reviews': store_reviews, 46 'embedding': embedding_reviews 47 } 48 spritz_locations.append(location_info)
Vamos imprimir nossa produção e ver quais são nossos locais de spritz no bairro de West Village! Vamos também verificar se temos um campo de incorporação recém-desenvolvido com nossas avaliações incorporadas:
1 # print our spritz information 2 for location in spritz_locations: 3 print(f"Name: {location['name']}, Address: {location['address']}, Coordinates: {location['location']}, Rating: {location['rating']}, Reviews: {location['reviews']}, Embedding: {location['embedding']}")
Então, se eu rolar no meu caderno, posso ver que existem incorporações, mas vou provar que elas estão lá quando inserirmos nossos dados no MongoDB Atlas, pois é um pouco difícil capturá-los em uma única imagem.
Vamos inseri-los usando a biblioteca
pymongo
.Primeiro, vamos instalar
pymongo
.1 # install pymongo 2 !pip install pymongo
Agora, configure nossa conexão MongoDB. Para fazer isso, certifique-se de ter sua string de conexão.
Lembre-se de que você pode nomear seu banco de dados e coleção como quiser, pois ele não será criado até que escrevamos nossos dados. Estou nomeando meu banco de dados "spritz_summer " e minha coleção de "spritz_locations_WV ". Execute o bloco de código abaixo para inserir seus documentos em seu cluster:
1 from pymongo import MongoClient 2 3 # set up your MongoDB connection 4 connection_string = getpass.getpass(prompt= "Enter connection string WITH USER + PASS here") 5 client = MongoClient(connection_string) 6 7 # name your database and collection anything you want since it will be created when you enter your data 8 database = client['spritz_summer'] 9 collection = database['spritz_locations_WV'] 10 11 # insert our spritz locations 12 collection.insert_many(spritz_locations)
Go em frente e verifique se tudo foi escrito corretamente no MongoDB Atlas:
Certifique-se de verificar novamente se o campo de incorporação existe e se é uma matriz de 1536e certifique-se de que suas coordenadas estejam configuradas corretamente da mesma forma que as minhas na imagem.
Ótima pergunta! Como ambos - se estivermos olhando para eles simplesmente de um operador de pipeline de agregação - precisam ser o primeiro estágio em seus pipelines, em vez de fazer um pipeline, podemos fazer uma pequena brecha e criar dois. Mas como vamos decidir qual fazer primeiro?
Quando uso o Google Maps para descobrir aonde ir, normalmente procuro primeiro o que estou desejando e depois vejo a que distância está de onde estou atualmente. Então, vamos manter essa mentalidade e começar com o MongoDB Atlas Vector Search. Mas, eu entendo que, intuitivamente, alguns de vocês podem preferir pesquisar em todos os locais próximos e, em seguida, pesquisar semanticamente (consultas geoespaciais primeiro e depois pesquisa vetorial), então vamos destacar esse método também abaixo.
Temos alguns passos aqui. Nosso primeiro passo é criar um índice de pesquisa do Atlas Vector. Faça isso dentro do MongoDB Atlas seguindo a documentação do Vector Atlas Search.
Lembre-se de que seu índice não é executado em seu script. Ele vive no seu cluster. Você saberá que ele está pronto para Go quando ele ficar verde e for ativado.
1 # create a Vector Search Index so we can use it 2 { 3 "fields": [ 4 { 5 "numDimensions": 1536, 6 "path": "embedding", 7 "similarity": "cosine", 8 "type": "vector" 9 } 10 ] 11 }
Depois de ativado, vamos à pesquisa vetorial!
Então. Digamos que acabei de jantar com meus melhores amigos em nosso restaurante favorito no West Village, Balaboosta. A comida estava ótima, é uma noite de verão, estamos com vontade de borrifar depois do jantar do lado de fora e preferimos nos sentar rapidamente. Vamos ver se conseguimos encontrar um lugar!
Nossa primeira etapa na construção de nosso pipeline é incorporar nossa query. Não podemos comparar texto com vetores; temos que comparar vetores com vetores. Podemos fazer isso com apenas algumas linhas, pois estamos usando o mesmo modelo de incorporação com o qual incorporamos nossas avaliações:
1 # You have to embed your queries just the same way you embedded your documents. 2 # my query 3 query_description = "outdoor seating quick service" 4 5 # we need to embed the query as well, since our documents are embedded 6 query_vector = get_embedding(query_description)
Agora, vamos criar nosso pipeline de agregação. Como usaremos um
$geoNear
Atlas Search em nosso pipeline em seguida, queremos manter os IDs encontrados nesse pipeline de agregação para não pesquisarmos tudo — pesquisamos apenas o tamanho da amostra. Por enquanto, certifique-se de $vectorSearch
que seu esteja no topo!1 spritz_near_me_vector = [ 2 { 3 '$vectorSearch': { 4 'index': 'vector_index', 5 'path': 'embedding', 6 'queryVector': query_vector, 7 'numCandidates': 15, 8 'limit': 5 9 } 10 }, 11 { 12 "$project": { 13 "_id": 1, # we want to keep this in place so we can search again using GeoNear 14 "name": 1, 15 "rating": 1, 16 "reviews": 1 17 #"address": 1, 18 #"location": 1, 19 #"embedding": 1 20 } 21 } 22 ]
Vamos imprimir nossos resultados e ver o que acontece com nossa consulta de “outdoor seating quick service”:
1 spritz_near_me_vector_results = list(collection.aggregate(spritz_near_me_vector)) 2 for result in spritz_near_me_vector_results: 3 print(result)
Temos cinco opções fantásticos! Se Go e ler as avaliações, veremos que elas estão alinhadas com o que estamos procurando. Aqui está um exemplo:
Go salvar os IDs do nosso pipeline acima em uma linha simples para que possamos especificar que queremos usar apenas o operador
$geoNear
nestes cinco:1 # now, we want to take the _ids from our above pipeline so we can use it to geo search 2 spritz_near_me_ids = [result['_id'] for result in spritz_near_me_vector_results] 3 print(spritz_near_me_ids)
Agora que eles estão salvos, podemos construir nosso pipeline
$geoNear
e ver qual dessas opções está mais próxima de nós a partir de nosso ponto de partida, Balaboosta, para que possamos ir até lá.Para descobrir as coordenadas do Balaboosta, cliquei com o botão direito do mouse no Google Maps e salvei as coordenadas e, em seguida, certifiquei-me de que tinha a longitude e a latitude na ordem correta.
Primeiro, crie uma dsphere 2em nosso campo de localização, para que possamos colocar um índice dsphere2 na nossa collection:
1 collection.create_index( { "location" : "2dsphere" } )
Aqui está o pipeline, com nossa query especificando que queremos usar apenas os IDs dos locais que encontramos acima:
1 # use the $geoNear operator to return documents that are at least 100 meters and at most 1000 meters from our specified GeoJSON point. 2 spritz_near_me_geo = [ 3 { 4 "$geoNear": { 5 "near": { 6 "type": "Point", 7 "coordinates": [-74.0059456749148, 40.73781277366724] 8 }, 9 # here we are saying that we only want to use the sample size from above 10 "query": {"_id": {"$in": spritz_near_me_ids}}, 11 "minDistance": 100, 12 "maxDistance": 1000, 13 "spherical": True, 14 "distanceField": "dist.calculated" 15 } 16 }, 17 { 18 "$project": { 19 "_id": 0, 20 "name": 1, 21 "address": 1, 22 "rating": 1, 23 "dist.calculated": 1, 24 #"location": 1, 25 #"embedding": 1 26 } 27 }, 28 { 29 "$limit": 3 30 }, 31 { 32 "$sort": { 33 "dist.calculated": 1 34 } 35 } 36 ]
Vamos imprimir e ver no que dá!
1 spritz_near_me_geo_results = collection.aggregate(spritz_near_me_geo) 2 for result in spritz_near_me_geo_results: 3 print(result)
Parece que o restaurante para o qual estamos indo é Pastis, já que é apenas 182.83 metros (0).1 milhas) de distância. É hora de um Aperol spritzao ar livre!
Para aqueles que preferem mudar as coisas e executar primeiro as consultas geoespaciais e depois incorporar a pesquisa vetorial, aqui está o pipeline:
1 # create a 2dsphere index on our location field 2 collection.create_index({"location": "2dsphere"}) 3 4 # our $geoNear pipeline 5 spritz_near_me_geo = [ 6 { 7 "$geoNear": { 8 "near": { 9 "type": "Point", 10 "coordinates": [-74.0059456749148, 40.73781277366724] 11 }, 12 "minDistance": 100, 13 "maxDistance": 1000, 14 "spherical": True, 15 "distanceField": "dist.calculated" 16 } 17 }, 18 { 19 "$project": { 20 "_id": 1, 21 "dist.calculated": 1 22 } 23 } 24 ] 25 26 # list of ID's and distances so we can use them as our sample size 27 places_ids = list(collection.aggregate(spritz_near_me_geo)) 28 distances = {result['_id']: result['dist']['calculated'] for result in places_ids} # have to create a new dictionary to keep our distances 29 spritz_near_me_ids = [result['_id'] for result in places_ids] 30 # print(spritz_near_me_ids)
Primeiro, crie nosso pipeline
$geoNear
e garanta que você esteja salvando seu places_ids
e o distances
para que possamos carregá-los por meio de nosso pipeline de pesquisa vetorial.Também precisamos reconstruir nosso índice do MongoDB Atlas Vector Search com um caminho "_id " incluído:
1 # our vector search index that was created inside of MongoDB Atlas 2 vector_search_index = { 3 "fields": [ 4 { 5 "numDimensions": 1536, 6 "path": "embedding", 7 "similarity": "cosine", 8 "type": "vector" 9 }, 10 { 11 "type": "filter", 12 "path": "_id" 13 } 14 ] 15 }
Quando estiver ativo e pronto, podemos criar nosso pipeline de pesquisa vetorial:
1 # vector search pipeline 2 spritz_near_me_vector = [ 3 { 4 '$vectorSearch': { 5 'index': 'vector_index', 6 'path': 'embedding', 7 'queryVector': query_vector, 8 'numCandidates': 15, 9 'limit': 3, 10 'filter': {"_id": {'$in': spritz_near_me_ids}} 11 } 12 }, 13 { 14 "$project": { 15 "_id": 1, # we want to keep this in place 16 "name": 1, 17 "rating": 1, 18 "dist.calculated": 1 19 #"reviews": 1 20 # "address": 1, 21 # "location": 1, 22 # "embedding": 1 23 } 24 } 25 ] 26 27 spritz_near_me_vector_results = collection.aggregate(spritz_near_me_vector) 28 for result in spritz_near_me_vector_results: 29 result['dist.calculated'] = distances.get(result['_id']) 30 print(result)
Execute-o e você verá alguns resultados bem semelhantes aos de antes! Deixe um comentário abaixo, informando quais locais apareceram para você como sua saída — estes são os meus:
Como você pode ver, são os mesmos resultados, mas em uma ordem ligeiramente diferente, pois não são mais ordenados por distância.
Neste tutorial, abordamos como usar aAtlas Vector Searche a API do Google Places para encontrar nossos locais de spritz mais próximos no bairro de West Village, na cidade de Nova York, com pesquisa semântica e, em seguida, usamos consultas geoespaciais do MongoDB para descobrir quais locais estavam mais próximos de nós a partir de um ponto de partida específico.
Para obter mais informações sobre queries geoespaciais do MongoDB, visite a documentação localizada acima e, se tiver alguma dúvida ou quiser compartilhar seu trabalho, Junte-se a nós na Comunidade de desenvolvedores do MongoDB.
Principais comentários nos fóruns
joao_schaabjoão schaab2 meses atrás
Graças a este artigo, ele é muito útil. Existe uma maneira de retornar documentos classificados por pontuação e distância sem fazer isso na camada do aplicação ?