Este tópico descreve como usar o tipo geoespacial GEOMETRY e a AI Function do Alibaba Cloud Milvus para executar, na mesma Collection e na mesma consulta, a filtragem geográfica de proximidade e o recall semântico de preferências. Em seguida, aplica-se análise de sentimento e geração de resumos para construir um ranking explicável de serviços locais.
Visão geral da solução
A pergunta "o que comer de bom por perto" é a demanda mais frequente em cenários de serviços locais. Para respondê-la adequadamente, o sistema precisa atender simultaneamente a três camadas de condições:
Proximidade: estabelecimentos dentro de um raio definido a partir da localização atual do usuário ou situados no mesmo bairro comercial.
Preferência: correspondência a demandas semânticas como "restaurante de culinária chinesa adequado para famílias com crianças" ou "cafeteria silenciosa para reuniões de negócios", requisitos que tags não conseguem enumerar com precisão.
Confiabilidade: avaliação baseada em críticas reais, taxa de avaliações positivas e identificação dos pontos fortes e reclamações mais mencionados.
Implementações tradicionais geralmente exigem a integração de três sistemas distintos: banco de dados espacial para filtragem geográfica, banco de dados vetorial para recall semântico e service de NLP para classificação de sentimentos e sumarização. Cada sistema mantém sua própria cópia dos dados, o que torna caras as operações de associação entre sistemas, sincronização e manutenção de consistência. Além disso, uma única consulta precisa encadear múltiplos services e várias rodadas de comunicação pela rede.
O Alibaba Cloud Milvus consolida todas essas capacidades em uma única Collection e uma única operação de busca:
|
Capacidade |
Implementação |
|
Filtragem por alcance geográfico |
Longitude e latitude dos estabelecimentos são armazenadas com o tipo |
|
Busca semântica em avaliações |
O texto das avaliações é vetorizado automaticamente no momento da escrita por |
|
Refinamento de relevância |
Os candidatos recuperados são reordenados conforme a relevância para a consulta usando |
|
Quantificação de reputação |
Utilize |
O ponto mais importante é: a filtragem geográfica e a busca semântica são unificadas em uma única chamada search(). A busca vetorial ANN cuida da "preferência", as funções espaciais no filter cuidam da "proximidade", e ambos os critérios são resolvidos conjuntamente em uma única requisição. O reranking é aplicado em seguida para produzir diretamente os candidatos ao ranking.
As AI Functions utilizadas neste tópico são:
|
Função |
Finalidade |
Uso no ranking |
|
|
Converte automaticamente o texto da avaliação em vetores no momento da escrita, eliminando a necessidade de a aplicação chamar o modelo previamente. |
Permite que demandas semânticas como "restaurante chinês adequado para famílias com crianças" sejam atendidas via busca vetorial. |
|
|
Reordena os candidatos recuperados com base na relevância para a consulta. |
Posiciona no topo do ranking os estabelecimentos que melhor atendem à demanda. |
|
|
Determina se cada avaliação é positiva, negativa ou neutra. |
Calcula a taxa de avaliações positivas de um estabelecimento. |
|
|
Condensa um grande volume de avaliações em um curto resumo. |
Gera o comentário resumido exibido no ranking. |
GEOMETRY e WKT
Se longitude e latitude forem armazenadas como dois campos DOUBLE, o banco de dados os tratará apenas como números, sem capacidade de determinar relações como "qual está dentro de qual" ou "se duas áreas se intersectam". O tipo GEOMETRY foi introduzido no Milvus 2.6 e armazena pontos, linhas e polígonos como objetos geométricos, permitindo operações de relação espacial e aceleração de consultas por meio de índices geoespaciais.
Os valores de um campo GEOMETRY são expressos no formato WKT (Well-Known Text). Os tipos mais comuns são:
|
Tipo WKT |
Exemplo |
Descrição |
Correspondente neste tópico |
|
|
|
Um único ponto |
Longitude e latitude de um estabelecimento |
|
|
|
Uma polilinha |
Trecho de rua ou rota de entrega |
|
|
|
Área fechada cuja primeira e última coordenadas são idênticas |
Bairro comercial ou área de entrega |
|
|
|
Múltiplos pontos |
Várias unidades de uma mesma marca |
|
|
|
Múltiplas áreas |
Diversos distritos administrativos |
A ordem das coordenadas é longitude primeiro e latitude depois, ou seja, POINT(lng lat), inversamente à ordem coloquial "latitude ... longitude ...". Todas as coordenadas neste tópico utilizam o sistema de referência WGS84.
Funções de relação espacial ST_
Funções iniciadas com ST_ determinam a relação espacial entre dois objetos geométricos. Elas são inseridas na expressão filter e retornam verdadeiro ou falso. Este tópico utiliza principalmente as três seguintes:
|
Função |
Descrição |
Uso neste tópico |
|
|
Verifica se A está inteiramente contido em B. |
Determina se um estabelecimento está dentro do polígono de um bairro comercial ou dentro de um raio especificado. |
|
|
Verifica se A contém inteiramente B; inverso de ST_WITHIN. |
Identifica se um bairro comercial contém determinado estabelecimento, útil para análise de atribuição. |
|
|
Verifica se A e B possuem alguma interseção ou contato. |
Avalia se a área de entrega de um estabelecimento intersecta a célula de grade onde o usuário se encontra. |
Outras funções também são suportadas e podem ser usadas conforme necessário:
|
Função |
Descrição |
Cenários |
|
|
Os dois objetos geométricos são exatamente idênticos. |
Deduplicação de geometrias. |
|
|
Apenas as fronteiras se tocam; os interiores não se sobrepõem. |
Verifica se bairros comerciais adjacentes fazem fronteira. |
|
|
Sobreposição parcial, sem que um contenha o outro. |
Detecta se áreas de entrega se cruzam. |
|
|
Interseção do tipo cruzamento. |
Identifica se uma via atravessa um bairro comercial. |
Casos de limite: quando um objeto geométrico cai exatamente sobre a fronteira de um polígono, ST_WITHIN retorna falso e ST_INTERSECTS retorna verdadeiro. Por exemplo, ao aproximar um círculo de raio por um polígono, um estabelecimento localizado exatamente na circunferência não será capturado por ST_WITHIN. Se seu cenário exigir a inclusão dos limites, utilize ST_INTERSECTS. Para MULTIPOINT, basta que um dos pontos esteja na fronteira para que ST_WITHIN retorne falso.
Pré-requisitos
Uma instância do Milvus 2.6 já criada. O tipo
GEOMETRYe a AI Function dependem do kernel 2.6. Após a criação da instância, não é necessário vincular separadamente um service de modelo; basta declarar a Function na Collection.Para acessar a instância pela internet, ative o Public Access na aba Security Configuration da página de detalhes da instância e adicione o IP de saída do cliente à lista de permissões de acesso público.
pymilvus instalado. Os exemplos deste tópico foram validados com pymilvus 3.0.0.
A interface RESTful compartilha a porta 19530 com gRPC. Ao chamar uma interface REST, especifique explicitamente a porta no endereço, por exemplo http://c-xxx.milvus.aliyuncs.com:19530. Se a porta for omitida, a porta 80 será acessada por padrão, causando timeout de conexão.
Procedimento
Prepare o código comum
O código abaixo contém a configuração de conexão, um wrapper para a interface REST e uma função utilitária que aproxima um círculo de raio como um polígono. Ele é compartilhado por todas as etapas subsequentes. Substitua MILVUS_HOST e MILVUS_TOKEN pelas informações da sua instância real.
from __future__ import annotations
import json
import math
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from pymilvus import DataType, Function, FunctionType, MilvusClient
# ==================== Connection configuration ====================
MILVUS_HOST = "http://c-xxx.milvus.aliyuncs.com"
MILVUS_TOKEN = "root:xxx"
# The RESTful interface shares port 19530 with gRPC. The port must be specified explicitly;
# otherwise, port 80 is used by default and the connection times out.
MILVUS_REST_BASE_URL = f"{MILVUS_HOST}:19530"
client = MilvusClient(uri=MILVUS_HOST, token=MILVUS_TOKEN)
collection_name = "shop_reviews"
def post_json(path: str, body: dict[str, Any], timeout: int = 120,
retries: int = 3) -> tuple[int, dict[str, Any]]:
"""Unified wrapper for the REST interfaces of AI_RERANK / AI_SENTIMENT / AI_SUMMARIZE."""
last = None
for _ in range(retries):
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_TOKEN}",
"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
status, data = response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
status, data = exc.code, json.loads(exc.read().decode("utf-8"))
last = (status, data)
if status == 200 and data.get("code") == 0:
return status, data
time.sleep(1)
return last
def circle_to_wkt(lng: float, lat: float, radius_km: float, num_seg: int = 32) -> str:
"""Approximate the circle centered at (lng, lat) with a radius of radius_km as the WKT of a regular polygon.
Note: longitude and latitude are angles. The conversion here is a planar approximation,
and the error grows as the latitude increases."""
d_lat = radius_km / 111.0
d_lng = radius_km / (111.0 * math.cos(math.radians(lat)))
pts = []
for i in range(num_seg + 1):
theta = 2 * math.pi * i / num_seg
pts.append(f"{lng + d_lng * math.cos(theta):.6f} {lat + d_lat * math.sin(theta):.6f}")
return f"POLYGON(({', '.join(pts)}))"
Etapa 1: Crie uma Collection com campo geoespacial e Function de vetorização
O campo location usa DataType.GEOMETRY para armazenar o ponto de coordenadas do estabelecimento. O campo review está associado a uma Function AI_EMBEDDING, que gera automaticamente um vetor de 1024 dimensões no momento da escrita. Adicionalmente, um índice HNSW é criado para o campo vetorial e um índice R-Tree para o campo geoespacial.
# ==================== Step 1: Create the Collection ====================
if client.has_collection(collection_name):
client.drop_collection(collection_name)
schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("shop_id", DataType.INT64)
schema.add_field("shop_name", DataType.VARCHAR, max_length=256)
schema.add_field("category", DataType.VARCHAR, max_length=64)
schema.add_field("location", DataType.GEOMETRY) # Shop longitude and latitude, WKT: POINT(lng lat)
schema.add_field("review", DataType.VARCHAR, max_length=4096) # Original review text, used as the embedding input
schema.add_field("review_embedding", DataType.FLOAT_VECTOR, dim=1024) # Review vector
# Automatically vectorize review at write time
schema.add_function(
Function(
name="embed_review",
function_type=FunctionType.TEXTEMBEDDING,
input_field_names=["review"],
output_field_names=["review_embedding"],
params={
"provider": "aliyun_milvus",
"model_name": "qwen3.7-text-embedding",
"dim": 1024,
"max_client_batch_size": 20,
"max_concurrency": 1,
},
)
)
index_params = client.prepare_index_params()
index_params.add_index(
field_name="review_embedding",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 200},
)
# Create an R-Tree geospatial index for the GEOMETRY field to accelerate spatial queries
index_params.add_index(
field_name="location",
index_type="RTREE",
)
client.create_collection(
collection_name=collection_name,
schema=schema,
index_params=index_params,
)
A dimensão de review_embedding deve corresponder ao parâmetro dim da Function. Criar um índice RTREE para o campo GEOMETRY acelera as consultas espaciais ST_, sendo recomendado quando o volume de dados é elevado.
Etapa 2: Insira dados de estabelecimentos e avaliações
Insira as coordenadas dos estabelecimentos e o texto original das avaliações. O campo review_embedding é gerado automaticamente pela Function.
# ==================== Step 2: Write shop and review data ====================
# review_embedding is generated automatically by the Function; there is no need to pass in vectors manually
rows = [
{"shop_id": 1001, "shop_name": "Bashu Home - Kid-Friendly Sichuan", "category": "Sichuan",
"location": "POINT(116.4612 39.9088)",
"review": "Came with the kids. They have high chairs and small bowls, the Mao Xue Wang is not too spicy, the staff are very patient, and the place is clean."},
{"shop_id": 1001, "shop_name": "Bashu Home - Kid-Friendly Sichuan", "category": "Sichuan",
"location": "POINT(116.4612 39.9088)",
"review": "Crowded on weekends and you have to wait for a table, but the dishes are consistently good. Great for a family meal, and the kids enjoyed it."},
{"shop_id": 1002, "shop_name": "Spicy Jianghu", "category": "Sichuan",
"location": "POINT(116.4501 39.9101)",
"review": "A paradise for lovers of strong flavors, the boiled fish in chili oil is full of flavor, but the space is small and quite noisy, so it is not very suitable for bringing children."},
{"shop_id": 1003, "shop_name": "Quiet Lane Coffee", "category": "Coffee",
"location": "POINT(116.4550 39.9075)",
"review": "A quiet place suitable for talking business, and the pour-over coffee is good, although it has nothing to do with a demand for Sichuan food."},
]
client.insert(collection_name, rows)
client.flush(collection_name)
client.load_collection(collection_name)
Após inserir os dados, chame flush(); caso contrário, uma busca executada imediatamente pode retornar resultado vazio. Somente após load_collection() carregar a Collection na memória é possível realizar buscas.
Etapa 3: Filtre por alcance geográfico
Utilize ST_WITHIN para verificar se um estabelecimento está dentro do polígono de um bairro comercial ou dentro de um círculo aproximado de raio definido. O círculo de raio é aproximado como polígono pela função circle_to_wkt.
# ==================== Step 3: Geographic range filtering ====================
# 3.1 Query the shops inside a business district polygon
cbd_polygon = ("POLYGON((116.448 39.905, 116.465 39.905, 116.465 39.914, "
"116.448 39.914, 116.448 39.905))")
hits = client.query(
collection_name=collection_name,
filter=f"ST_WITHIN(location, '{cbd_polygon}')",
output_fields=["shop_name", "category", "location"],
limit=100,
)
print("Review records inside the business district:")
for h in hits:
print(f" - {h['shop_name']} ({h['category']}) {h['location']}")
# 3.2 Query within a 3 km radius: approximate the circle as a polygon, then check whether the shop falls inside it
center_lng, center_lat = 116.4600, 39.9090
circle = circle_to_wkt(center_lng, center_lat, radius_km=3.0)
nearby = client.query(
collection_name=collection_name,
filter=f"ST_WITHIN(location, '{circle}')",
output_fields=["shop_name", "category", "location"],
limit=200,
)
print(f"Review records within 3 km of the center point ({center_lng}, {center_lat}):")
for n in nearby:
print(f" - {n['shop_name']} ({n['category']}) {n['location']}")
Nos dados de exemplo, os 4 registros de avaliação pertencem a 3 estabelecimentos (Bashu Home possui 2 avaliações), portanto a consulta acima retorna 4 registros. Para obter resultados no nível do estabelecimento, faça a deduplicação por shop_id na aplicação.
A função circle_to_wkt converte longitude e latitude usando uma aproximação planar, cujo erro aumenta conforme a latitude cresce. Esse método é adequado para filtragem aproximada por raio. Se seu cenário exigir precisão nos limites, aumente o número de segmentos em num_seg ou realize uma verificação secundária com distâncias exatas na aplicação.
Etapa 4: Combine busca semântica com filtragem geográfica
Esta é a parte central da solução: unir "proximidade" (funções espaciais no filter) e "preferência" (busca vetorial ANN) em uma única chamada search().
# ==================== Step 4: Fuse semantic retrieval with geographic filtering ====================
# ANN retrieval over review vectors handles "on taste", the spatial function in filter handles "nearby",
# and both are solved jointly in a single request
QUERY = "Kid-friendly Sichuan restaurants nearby"
geo_filter = f"category == 'Sichuan' && ST_WITHIN(location, '{circle}')"
results = client.search(
collection_name=collection_name,
data=[QUERY],
anns_field="review_embedding",
filter=geo_filter,
limit=20,
output_fields=["shop_id", "shop_name", "category", "location", "review"],
)
print(f"Query: {QUERY}")
for rank, hit in enumerate(results[0], 1):
e = hit["entity"]
print(f" {rank}. [similarity {hit['distance']:.4f}] {e['shop_name']} | {e['review'][:24]}")
Um exemplo de resultado retornado é mostrado abaixo. "Bashu Home - Kid-Friendly Sichuan" aparece em primeiro lugar porque trechos de suas avaliações como "high chairs" e "the staff are very patient" têm alta relevância semântica para "kid-friendly":
1. [similarity 0.6926] Bashu Home - Kid-Friendly Sichuan | Came with the kids. They have
2. [similarity 0.6591] Spicy Jianghu | A paradise for lovers of strong flavors, the boil
3. [similarity 0.6091] Bashu Home - Kid-Friendly Sichuan | Crowded on weekends and you
Etapa 5: Use AI_RERANK para refinar a ordenação dos candidatos
A similaridade vetorial reflete proximidade semântica, mas não equivale totalmente a "quão relevante algo é para a consulta". Aplicar AI_RERANK para uma segunda rodada de refinamento nos candidatos recuperados permite mover para o topo os estabelecimentos que melhor correspondem à intenção da consulta. Abaixo são mostrados dois modos de uso: anexando um ranker em search() e chamando a interface REST stateless.
# ==================== Step 5: Rerank candidates with AI_RERANK ====================
RERANK_MODEL = "qwen3-rerank"
# 5.1 Attach a RERANK ranker in search(); after ANN recall, the model performs a second round of fine ranking
reranker = Function(
name="rerank_reviews",
function_type=FunctionType.RERANK,
input_field_names=["review"],
params={
"reranker": "model",
"provider": "aliyun_milvus",
"model_name": RERANK_MODEL,
"queries": [QUERY],
"timeout_sec": 10,
},
)
results = client.search(
collection_name=collection_name,
data=[QUERY],
anns_field="review_embedding",
filter=geo_filter,
limit=20,
output_fields=["shop_id", "shop_name", "review"],
ranker=reranker,
)
for rank, hit in enumerate(results[0], 1):
e = hit["entity"]
print(f" {rank}. [rerank score {hit['distance']:.4f}] {e['shop_name']} | {e['review'][:24]}")
# 5.2 You can also use the stateless REST interface /v2/vectordb/ai/rerank
rerank_docs = [
"Came with the kids. They have high chairs and small bowls, the Mao Xue Wang is not too spicy, the staff are very patient, and the place is clean.",
"A paradise for lovers of strong flavors, the boiled fish in chili oil is full of flavor, but the space is small and quite noisy, so it is not very suitable for bringing children.",
]
status, data = post_json(
"/v2/vectordb/ai/rerank",
{
"model_name": RERANK_MODEL,
"query": QUERY,
"documents": rerank_docs,
"params": {"timeout_sec": 10},
},
)
assert status == 200 and data.get("code") == 0, data
ranked = sorted(data["data"]["output"]["results"],
key=lambda x: x["relevance_score"], reverse=True)
for rank, item in enumerate(ranked, 1):
print(f" {rank}. [relevance {item['relevance_score']:.4f}] {rerank_docs[item['index']][:28]}")
Um exemplo da saída após o refinamento é apresentado a seguir. Ambas as avaliações de "Bashu Home - Kid-Friendly Sichuan" aparecem antes de "Spicy Jianghu":
1. [rerank score 0.6986] Bashu Home - Kid-Friendly Sichuan | Came with the kids. They have
2. [rerank score 0.5389] Bashu Home - Kid-Friendly Sichuan | Crowded on weekends and you
3. [rerank score 0.5214] Spicy Jianghu | A paradise for lovers of strong flavors, the boil
A estrutura de retorno da interface REST é data.output.results, onde cada candidato corresponde a um item contendo index (o índice do candidato no array de entrada documents) e relevance_score (a pontuação de relevância). Ordenar por pontuação em ordem decrescente produz o resultado refinado:
1. [relevance 0.6986] Came with the kids. They have high chairs and small bowls, the Mao Xue Wang is not too spicy, the staff are very patient, and the place is clean.
2. [relevance 0.5214] A paradise for lovers of strong flavors, the boiled fish in chili oil is full of flavor, but the space is small and quite noisy, so it is not very suitable for bringing children.
Na interface REST, documents deve ser um array de strings, query é uma string no nível superior e o parâmetro top_n não é suportado — enviá-lo retorna o erro param [top_n] is not supported. A interface sempre retorna uma pontuação para cada candidato; se precisar truncar os resultados, ordene por pontuação na aplicação e selecione os N primeiros itens manualmente.
O texto do candidato deve abranger todas as dimensões semânticas da consulta. A consulta deste exemplo, "Kid-friendly Sichuan restaurants nearby", contém tanto a dimensão "adequado para crianças" quanto "culinária chinesa", e um texto candidato que omita qualquer uma dessas dimensões perde uma parcela significativa da pontuação. Tome o primeiro candidato como exemplo:
|
Texto do candidato |
Pontuação de relevância |
|
Came with the kids. They have high chairs and small bowls, and the staff are very patient. (Sem informação sobre culinária chinesa) |
0.4723 |
|
Came with the kids. They have high chairs and small bowls, the Mao Xue Wang is not too spicy, and the staff are very patient. |
0.6666 |
|
Came with the kids. They have high chairs and small bowls, the Mao Xue Wang is not too spicy, the staff are very patient, and the place is clean. |
0.6986 |
Simplesmente adicionar o nome de um prato chinês eleva a pontuação de 0.4723 para 0.6666. Em contraste, "A paradise for lovers of strong flavors, the boiled fish in chili oil is full of flavor ... not very suitable for bringing children." pontua na dimensão culinária chinesa por mencionar "boiled fish in chili oil", mantendo sua relevância estável em 0.5214. Portanto, se o primeiro candidato não trouxesse informação sobre culinária chinesa, ficaria posicionado abaixo.
Assim, em cenários reais, o texto do candidato enviado ao reranking deve usar o texto original completo sempre que possível. Não remova informações-chave que possam carregar relevância (como nomes de pratos ou palavras de categoria) apenas para encurtar o tamanho. Se precisar avaliar a eficácia do reranking, utilize o mesmo texto completo usado em produção.
Etapa 6: Calcule a taxa de avaliações positivas e gere um resumo das avaliações
Utilize AI_SENTIMENT para classificar o sentimento de todas as avaliações de um estabelecimento e agregue a taxa de avaliações positivas na aplicação; em seguida, use AI_SUMMARIZE para condensar as avaliações em um comentário resumido.
# ==================== Step 6: Sentiment analysis for the positive review rate + summarization for the comment ====================
shop_id = 1001
reviews = [r["review"] for r in client.query(
collection_name=collection_name,
filter=f"shop_id == {shop_id}",
output_fields=["review"],
limit=1000,
)]
# 6.1 Sentiment classification, aggregated into a positive review rate
status, sent = post_json("/v2/vectordb/ai/sentiment", {
"model_name": "qwen3.7-max",
"texts": reviews,
"params": {"categories": ["positive", "negative", "neutral"], "temperature": 0},
})
assert status == 200 and sent.get("code") == 0, sent
labels = sent["data"]["output"]["outputs"]
valid = [x for x in labels if x] # Empty values count as undetermined and are excluded from the aggregation
positive = sum(1 for x in valid if x == "positive")
good_rate = positive / len(valid) if valid else 0.0
print(f"Positive review rate: {good_rate:.0%} (valid samples {len(valid)}/{len(labels)})")
# 6.2 Generate a summary of the shop's reviews
status, summ = post_json("/v2/vectordb/ai/summarize", {
"model_name": "qwen3.7-max",
"texts": ["\n".join(reviews)],
"params": {"max_words": 40, "temperature": 0,
"prompt": "Summarize the overall reviews of this shop in English, highlighting its signature dishes, the audience it suits, and its environment."},
})
assert status == 200 and summ.get("code") == 0, summ
print(f"Shop summary: {summ['data']['output']['outputs'][0]}")
Tomando shop_id 1001 como exemplo, ambas as avaliações são classificadas como positivas, resultando em uma taxa de avaliações positivas de 100%. O resumo gerado é: "Ambiente limpo, com Mao Xue Wang suave e agradável. Atendimento paciente com talheres infantis disponíveis, adequado para refeições em família com crianças; espere fila nos fins de semana."
A classificação de sentimento pode retornar valor vazio, indicando que o modelo não conseguiu fazer uma determinação. Ao agregar a taxa de avaliações positivas, filtre primeiro os valores vazios para que amostras indeterminadas não sejam contadas no denominador.
Lógica de ordenação do ranking
Reunindo as etapas anteriores, o fluxo completo de geração do ranking é: a filtragem geográfica delimita os candidatos "próximos", a busca semântica identifica os que correspondem à "preferência", o reranking ajusta a ordem por refinamento, e a análise de sentimento combinada com sumarização completa o quadro de "reputação".
As posições finais no ranking podem ser derivadas combinando três sinais:
Sinal geográfico: estar dentro do raio ou do bairro comercial funciona como critério de admissão — estabelecimentos que não atendem são filtrados diretamente; os demais recebem um peso base proporcional à distância.
Relevância semântica: o
relevance_scoreproduzido porAI_RERANK, que mede o quanto as avaliações do estabelecimento correspondem à demanda do usuário.Reputação: a taxa de avaliações positivas agregada por
AI_SENTIMENT.
É possível utilizar uma fusão ponderada no formato w1 · relevance score + w2 · positive review rate + w3 · distance decay, ajustando os pesos conforme os objetivos do negócio. É isso que diferencia esse ranking de uma "simples ordenação por nota": um estabelecimento pode alcançar as primeiras posições gerais devido a uma forte correspondência semântica com "adequado para crianças", combinada com alta taxa de avaliações positivas e menor distância.
Comparação com a solução tradicional multi-sistema:
|
Dimensão |
Solução tradicional (banco espacial + banco vetorial + service de NLP) |
Alibaba Cloud Milvus |
|
Quantidade de sistemas |
3 ou mais, cada um armazenando sua própria cópia dos dados |
1, com fonte única de dados |
|
Fusão de geografia e semântica |
Associação entre sistemas ou múltiplas rodadas de nova busca |
Uma única chamada |
|
Compreensão de avaliações |
Modelos de sentimento e sumarização construídos internamente ou integrados externamente |
|
|
Pipeline de vetorização |
A aplicação chama o embedding primeiro e depois escreve os dados |
Vetorização ocorre no momento da escrita |
|
Consistência e sincronização |
Precisam ser mantidas manualmente |
Naturalmente consistentes dentro de um único banco de dados |
Direções para extensão
Busca multimodal de estabelecimentos: utilize
qwen3-vl-embeddingpara vetorizar fotos de estabelecimentos e pratos, habilitando buscas por imagem como "estabelecimento próximo com este estilo de decoração", que também podem ser combinadas com filtragem geográfica em uma única consulta.Pré-computação offline de reputação: quando um estabelecimento possui grande volume de avaliações, métodos em lote como
AI_BATCHpermitem pré-calcular offline a taxa de avaliações positivas e os resumos, persistindo-os no banco. Consultas ao ranking passam então a ler apenas os resultados, equilibrando custo e latência.Relações espaciais mais ricas: use
ST_INTERSECTSpara verificar se a área de entrega de um estabelecimento intersecta a grade onde o usuário está, eST_CONTAINSpara análise de atribuição de bairro comercial, estendendo a fusão "geografia + IA" da busca de estabelecimentos para agendamento, seleção de pontos e outros cenários.