Todos os produtos
Search
Central de documentação

Vector Retrieval Service for Milvus:Crie um ranking de serviços locais com o Alibaba Cloud Milvus combinando busca geoespacial e AI Function

Última atualização: Oct 19, 2026

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 GEOMETRY, bairros comerciais são representados por POLYGON e funções de relação espacial como ST_WITHIN são incluídas na expressão filter durante a busca.

Busca semântica em avaliações

O texto das avaliações é vetorizado automaticamente no momento da escrita por AI_EMBEDDING, e a busca ANN é executada no momento da consulta usando o vetor gerado pelo mesmo modelo.

Refinamento de relevância

Os candidatos recuperados são reordenados conforme a relevância para a consulta usando AI_RERANK.

Quantificação de reputação

Utilize AI_SENTIMENT para classificar o sentimento das avaliações e agregar a taxa de avaliações positivas na aplicação, e use AI_SUMMARIZE para condensar as avaliações de um estabelecimento em um breve resumo.

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

AI_EMBEDDING

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.

AI_RERANK

Reordena os candidatos recuperados com base na relevância para a consulta.

Posiciona no topo do ranking os estabelecimentos que melhor atendem à demanda.

AI_SENTIMENT

Determina se cada avaliação é positiva, negativa ou neutra.

Calcula a taxa de avaliações positivas de um estabelecimento.

AI_SUMMARIZE

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

POINT

POINT(116.4612 39.9088)

Um único ponto

Longitude e latitude de um estabelecimento

LINESTRING

LINESTRING(116.46 39.90, 116.47 39.91)

Uma polilinha

Trecho de rua ou rota de entrega

POLYGON

POLYGON((116.448 39.905, ..., 116.448 39.905))

Área fechada cuja primeira e última coordenadas são idênticas

Bairro comercial ou área de entrega

MULTIPOINT

MULTIPOINT((116.46 39.90), (116.47 39.91))

Múltiplos pontos

Várias unidades de uma mesma marca

MULTIPOLYGON

MULTIPOLYGON(((...)),((...)))

Múltiplas áreas

Diversos distritos administrativos

Nota

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

ST_WITHIN(A, B)

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.

ST_CONTAINS(A, B)

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.

ST_INTERSECTS(A, B)

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

ST_EQUALS

Os dois objetos geométricos são exatamente idênticos.

Deduplicação de geometrias.

ST_TOUCHES

Apenas as fronteiras se tocam; os interiores não se sobrepõem.

Verifica se bairros comerciais adjacentes fazem fronteira.

ST_OVERLAPS

Sobreposição parcial, sem que um contenha o outro.

Detecta se áreas de entrega se cruzam.

ST_CROSSES

Interseção do tipo cruzamento.

Identifica se uma via atravessa um bairro comercial.

Aviso

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 GEOMETRY e 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.

Nota

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,
)
Nota

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)
Nota

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.

Nota

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.
Nota

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.

Nota

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."

Nota

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_score produzido por AI_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 search(): busca ANN + filtragem ST_

Compreensão de avaliações

Modelos de sentimento e sumarização construídos internamente ou integrados externamente

AI_SENTIMENT / AI_SUMMARIZE nativos

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-embedding para 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_BATCH permitem 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_INTERSECTS para verificar se a área de entrega de um estabelecimento intersecta a grade onde o usuário está, e ST_CONTAINS para 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.