Todos os produtos
Search
Central de documentação

Vector Retrieval Service for Milvus:Crie um ranking local de estilo de vida combinando busca geoespacial e AI Functions no Alibaba Cloud Milvus

Última atualização: Aug 13, 2026

O Alibaba Cloud Milvus integra o tipo geoespacial GEOMETRY às AI Functions, permitindo que uma única busca execute filtragem por intervalo geoespacial e recuperação semântica na mesma Collection. Este tópico descreve como executar simultaneamente a filtragem "próximo" e a recuperação "adequado ao gosto", aplicando análise de sentimento e geração de resumo para construir um ranking explicável de estabelecimentos locais.

Visão geral da solução

A pergunta "o que comer perto daqui" é a solicitação mais frequente em cenários de service locais. Para respondê-la adequadamente, o sistema precisa atender a três condições simultaneamente:

  • Proximidade: lojas dentro de um raio definido a partir da localização atual do usuário ou dentro do bairro comercial onde ele se encontra.

  • Adequação ao gosto: solicitações como "um restaurante de Sichuan adequado para famílias" ou "um café tranquilo para conversar sobre negócios", impossíveis de listar com precisão apenas por tags.

  • Confiabilidade: o conteúdo das avaliações reais, a taxa de avaliações positivas e os pontos fortes ou reclamações recorrentes.

    Implementações tradicionais geralmente conectam três sistemas: um banco de dados espacial para filtragem geoespacial, um banco de dados vetorial para recuperação semântica e um service de NLP para classificação de sentimento e sumarização. Cada sistema mantém sua própria cópia dos dados, tornando custosas as junções entre sistemas, a sincronização de dados e a manutenção da consistência. Além disso, uma única consulta precisa encadear vários service e múltiplas idas e vindas na rede.

Dimensão

Solução tradicional (banco de dados espacial + banco de dados vetorial + service de NLP)

Alibaba Cloud Milvus

Número de sistemas

3 ou mais, cada um com sua própria cópia dos dados

1, com uma única source de dados

Fusão geoespacial e semântica

Junções entre sistemas ou múltiplas rodadas de recuperação

Uma única chamada search(): busca ANN + filtragem ST_

Compreensão de avaliações

Modelos de sentimento e sumarização próprios ou externos

AI_SENTIMENT / AI_SUMMARIZE integrados

Caminho de vetorização

A aplicação chama o embedding primeiro e depois grava os dados

Vetorizado na inserção

Consistência e sincronização

Mantidas manualmente

Naturalmente consistentes em um único banco de dados

O Alibaba Cloud Milvus converge essas capacidades em uma única Collection e uma única busca:

Capacidade

Implementação

Filtragem por intervalo geoespacial

Armazene longitude e latitude em um campo GEOMETRY, represente um bairro comercial como POLYGON e inclua funções de relação espacial como ST_WITHIN na expressão filter durante a busca.

Busca semântica em avaliações

O AI_EMBEDDING vetoriza automaticamente o texto da avaliação na inserção, e a busca executa uma consulta ANN contra um vetor de consulta gerado pelo mesmo modelo.

Reordenação por relevância

O AI_RERANK reordena os candidatos recuperados pela relevância em relação à consulta.

Quantificação de reputação

Use o AI_SENTIMENT para classificar o sentimento das avaliações e agregue a taxa de avaliações positivas na aplicação; use o AI_SUMMARIZE para condensar as avaliações de uma loja em um resumo curto.

A filtragem geoespacial e a busca semântica ocorrem em uma única chamada search(): a busca vetorial ANN cuida da "adequação ao gosto" e as funções espaciais no filter cuidam da "proximidade". O sistema resolve ambas conjuntamente em uma única requisição e aplica a reordenação em seguida para produzir diretamente os candidatos do ranking.

Este tópico utiliza as seguintes AI Functions:

Função

O que faz

Papel no ranking

Como é chamada

AI_EMBEDDING

Converte o texto da avaliação em vetores automaticamente na inserção, eliminando a necessidade de a aplicação chamar um modelo previamente.

Permite que solicitações semânticas como "um restaurante de Sichuan adequado para famílias" sejam atendidas por busca vetorial.

Declarada como Function no schema da Collection (FunctionType.TEXTEMBEDDING).

AI_RERANK

Reordena os candidatos recuperados pela relevância em relação à consulta.

Move as lojas que melhor atendem à solicitação para o topo do ranking.

Anexada como ranker na search() (FunctionType.RERANK) ou chamada via endpoint REST /v2/vectordb/ai/rerank.

AI_SENTIMENT

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

Agrega a taxa de avaliações positivas de uma loja.

Chamada via endpoint REST /v2/vectordb/ai/sentiment.

AI_SUMMARIZE

Condensa um grande volume de avaliações em um resumo curto.

Gera o comentário de uma linha exibido no ranking.

Chamada via endpoint REST /v2/vectordb/ai/summarize.

Você pode declarar o AI_EMBEDDING e o AI_RERANK na Collection ou anexá-los a uma busca. Já o AI_SENTIMENT e o AI_SUMMARIZE estão disponíveis apenas pela interface REST.

Pré-requisitos

  • Instância do Milvus 2.6 criada. O tipo GEOMETRY e as AI Functions exigem o kernel 2.6. Após criar a instância, declare a Function diretamente na Collection; não é necessário vincular um service de modelo separadamente.

  • Para acessar a instância pela internet, ative o Public Network 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 o pymilvus 3.0.0.

Limitações e notas de uso

Revise os comportamentos abaixo antes de copiar o código, pois vários deles produzem resultados silenciosos ou vazios em vez de erros:

  • Semântica de limites geoespaciais — Quando um objeto geométrico está exatamente sobre o limite de um polígono, o ST_WITHIN retorna falso e o ST_INTERSECTS retorna verdadeiro. Por exemplo, ao aproximar um círculo de raio por um polígono, o ST_WITHIN não corresponde a uma loja situada exatamente na circunferência. Se a lógica do seu negócio precisar incluir o limite, utilize o ST_INTERSECTS. Para um MULTIPOINT, o ST_WITHIN retorna falso assim que um de seus pontos estiver sobre o limite.

  • Precisão da aproximação de raio — O circle_to_wkt converte longitude e latitude com uma aproximação planar; portanto, o erro aumenta conforme a latitude cresce. Esse método serve para filtragem grosseira por raio. Caso a lógica do seu negócio seja sensível à precisão do limite, aumente a contagem de segmentos num_seg ou verifique novamente a distância exata na aplicação.

  • Porta da interface REST — A interface RESTful compartilha a porta 19530 com o gRPC. Inclua a porta explicitamente no endereço ao chamar a interface REST, por exemplo http://c-xxx.milvus.aliyuncs.com:19530. Se você omitir a porta, a requisição irá para a porta 80 por padrão e a conexão expirará.

  • Parâmetros de rerank via REST — Na interface REST de rerank, documents deve ser um array de strings e query deve ser uma string no nível superior. O parâmetro top_n não é suportado; enviá-lo retorna param [top_n] is not supported. A interface sempre retorna uma pontuação por candidato. Se precisar truncar a lista, ordene pela pontuação e selecione os N primeiros na aplicação.

  • Sentimento indeterminado — O AI_SENTIMENT pode retornar um valor vazio, indicando que o modelo não conseguiu determinar o sentimento. Filtre os valores vazios antes de agregar a taxa de avaliações positivas para que amostras indeterminadas não sejam contadas no denominador.

Procedimento

Execute as etapas em ordem dentro da mesma sessão. Os trechos de código formam um script contínuo: as etapas posteriores reutilizam o cliente, as funções auxiliares e as variáveis definidas nas etapas anteriores (por exemplo, circle da Etapa 3, além de QUERY e geo_filter da Etapa 4). Se copiar uma única etapa isoladamente, defina primeiro suas variáveis dependentes.

Etapa 0: Preparar o código compartilhado (obrigatório)

O código a seguir contém as configurações 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. Todas as etapas subsequentes dependem deste código. Substitua MILVUS_HOST e MILVUS_TOKEN pelas informações da sua própria instância.

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 settings ====================
MILVUS_HOST = "http://c-xxx.milvus.aliyuncs.com"
MILVUS_TOKEN = "root:xxx"
# The RESTful interface shares port 19530 with gRPC. Include the port explicitly,
# otherwise the request goes to port 80 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]]:
    """Shared wrapper for the AI_RERANK / AI_SENTIMENT / AI_SUMMARIZE REST interfaces."""
    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 a regular polygon in WKT.
    Note: longitude and latitude are angles. This conversion is a planar approximation,
    so 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)}))"

O valor de MILVUS_HOST é usado como URI do cliente, enquanto MILVUS_REST_BASE_URL acrescenta a porta 19530 para as chamadas REST nas etapas seguintes. A exigência de porta explícita aplica-se à interface REST.

Etapa 1: Criar uma collection com campo geoespacial e função de embedding

O campo location usa DataType.GEOMETRY para armazenar o ponto de coordenadas de uma loja. O campo review carrega uma função AI_EMBEDDING que gera automaticamente um vetor de 1.024 dimensões no momento da inserção. Simultaneamente, crie um índice HNSW para o campo vetorial e um índice R-Tree para o campo geoespacial.

Aviso

O código abaixo executa drop_collection antes de criar a Collection, o que exclui permanentemente qualquer Collection existente chamada shop_reviews e todos os seus dados. Em uma instância que já contenha dados de produção, use um nome de Collection de teste separado ou remova a ramificação drop_collection.

# ==================== 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)                          # Store coordinates, WKT: POINT(lng lat)
schema.add_field("review", DataType.VARCHAR, max_length=4096)            # Raw review text, used as the embedding input
schema.add_field("review_embedding", DataType.FLOAT_VECTOR, dim=1024)    # Review vector

# Vectorize review automatically at insert 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 speed up 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 valor dim nos parâmetros da Function. Criar um índice RTREE para o campo GEOMETRY acelera as consultas espaciais ST_ e é recomendado quando o volume de dados é grande.

Os valores de model_name neste tópico (qwen3.7-text-embedding aqui, e os modelos de rerank, sentimento e sumarização nas etapas seguintes) são exemplos. Substitua cada um por um modelo disponível na sua própria instância.

Etapa 2: Inserir dados de lojas e avaliações

Insira as coordenadas das lojas e o texto bruto das avaliações. O review_embedding é gerado automaticamente pela Function.

# ==================== Step 2: Insert store and review data ====================
# review_embedding is generated automatically by the Function, so you do not pass vectors manually
rows = [
    {"shop_id": 1001, "shop_name": "Bashu Homestead Family Sichuan", "category": "Sichuan cuisine",
     "location": "POINT(116.4612 39.9088)",
     "review": "Came with my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient, and the place is clean."},
    {"shop_id": 1001, "shop_name": "Bashu Homestead Family Sichuan", "category": "Sichuan cuisine",
     "location": "POINT(116.4612 39.9088)",
     "review": "You wait for a table on weekends, but the dishes are consistent. Good for a family meal, and my kid enjoyed it."},
    {"shop_id": 1002, "shop_name": "Mala Jianghu", "category": "Sichuan cuisine",
     "location": "POINT(116.4501 39.9101)",
     "review": "A paradise for bold flavors, the shuizhuyu is punchy, but the space is small and noisy, so it does not suit kids."},
    {"shop_id": 1003, "shop_name": "Quiet Lane Coffee", "category": "Coffee",
     "location": "POINT(116.4550 39.9075)",
     "review": "Quiet enough to talk business and the pour-over is good, but it has nothing to do with a Sichuan food request."},
]
client.insert(collection_name, rows)
client.flush(collection_name)
client.load_collection(collection_name)

Chame flush() após inserir os dados; caso contrário, uma busca executada imediatamente depois poderá retornar resultados vazios. O load_collection() carrega a Collection na memória, operação obrigatória antes de buscar dados.

Etapa 3: Filtrar por intervalo geoespacial

Escolha o método de intervalo com base no formato da área alvo:

  • Distrito administrativo ou bairro comercial — Represente a área como um POLYGON e use o ST_WITHIN.

  • Área ao redor da localização atual do usuário — Aproxime um círculo de raio como um polígono com o circle_to_wkt e depois use o ST_WITHIN.

  • Lógica que deve incluir o limite ou detectar áreas sobrepostas — Utilize o ST_INTERSECTS, pois o ST_WITHIN não corresponde a uma geometria localizada exatamente no limite.

    O código a seguir mostra tanto o polígono do bairro comercial quanto o círculo de raio. O circle_to_wkt aproxima o círculo de raio como um polígono.

# ==================== Step 3: Geospatial range filtering ====================
# 3.1 Query the stores within 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 within 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 stores fall 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']}")

Os dados de exemplo possuem 4 registros de avaliação em 3 lojas (Bashu Homestead tem 2 avaliações), então ambas as consultas retornam 4 registros. Para gerar uma linha por loja, deduplique por shop_id na aplicação.

Etapa 4: Combinar busca semântica com filtragem geoespacial

Esta etapa funde "proximidade" (funções espaciais no filter) e "adequação ao gosto" (busca vetorial ANN) em uma única chamada search(). Ela reutiliza o circle da Etapa 3.

# ==================== Step 4: Combine semantic search with geospatial filtering ====================
# The ANN search over review vectors handles "on taste", the spatial functions in filter handle "nearby",
# and both are solved jointly in one request
QUERY = "family-friendly Sichuan restaurants nearby"
geo_filter = f"category == 'Sichuan cuisine' && 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]}")

O exemplo a seguir mostra os resultados retornados. "Bashu Homestead Family Sichuan" aparece em primeiro lugar porque suas avaliações mencionam "high chairs" e "the servers are patient", termos semanticamente próximos de "family-friendly":

1. [similarity 0.6926]  Bashu Homestead Family Sichuan | Came with my kid. High c
2. [similarity 0.6591]  Mala Jianghu | A paradise for bold flav
3. [similarity 0.6091]  Bashu Homestead Family Sichuan | You wait for a table on

Etapa 5: Reordenar candidatos com AI_RERANK

A similaridade vetorial reflete proximidade semântica, o que não equivale exatamente à relevância de um resultado para a consulta. Reordenar os candidatos recuperados com o AI_RERANK move as lojas que melhor atendem à intenção da consulta para as primeiras posições. Esta etapa reutiliza QUERY e geo_filter da Etapa 4.

Duas opções estão disponíveis; escolha a adequada para o seu cenário:

  • Reordenar dentro do pipeline de recuperação — Anexe um ranker na search() (usado em 5.1). O modelo reordena os candidatos que a busca ANN recupera da Collection.

  • Reordenar uma lista existente de textos — Chame a interface REST stateless /v2/vectordb/ai/rerank (usado em 5.2). Esse caminho recebe uma lista de textos diretamente e não depende de uma busca.

    As duas subseções abaixo são alternativas, não etapas sequenciais.

# ==================== Step 5: Rerank candidates with AI_RERANK ====================
RERANK_MODEL = "qwen3-rerank"

# 5.1 Attach a RERANK ranker in search(): after ANN recall, the model reranks the candidates
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 my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient, and the place is clean.",
    "A paradise for bold flavors, the shuizhuyu is punchy, but the space is small and noisy, so it does not suit kids.",
]
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]}")

O exemplo a seguir mostra a saída após a reordenação. Ambas as avaliações de "Bashu Homestead Family Sichuan" ficam à frente de "Mala Jianghu":

1. [rerank score 0.6986]  Bashu Homestead Family Sichuan | Came with my kid. High c
2. [rerank score 0.5389]  Bashu Homestead Family Sichuan | You wait for a table on
3. [rerank score 0.5214]  Mala Jianghu | A paradise for bold flav

A interface REST retorna seus resultados em data.output.results, onde cada item corresponde a um candidato e contém index (a posição do candidato no parâmetro de requisição documents) e relevance_score (a pontuação de relevância). Ordenar pela pontuação em ordem decrescente fornece o resultado reordenado:

1. [relevance 0.6986]  Came with my kid. High chairs
2. [relevance 0.5214]  A paradise for bold flavors,

O texto do candidato deve cobrir todas as dimensões semânticas da consulta. A consulta neste exemplo, "family-friendly Sichuan restaurants nearby", abrange tanto a dimensão "family-friendly" quanto a dimensão "Sichuan cuisine". Um candidato que omitir qualquer uma delas perde uma quantidade significativa de pontuação. Tome o primeiro candidato como exemplo:

Texto do candidato

Pontuação de relevância

Came with my kid. High chairs and small bowls are available, the servers are patient. (sem informação sobre culinária de Sichuan)

0.4723

Came with my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient.

0.6666

Came with my kid. High chairs and small bowls are available, the maoxuewang is not too spicy, the servers are patient, and the place is clean.

0.6986

Adicionar o nome de um único prato de Sichuan eleva a pontuação de 0,4723 para 0,6666. Em contraste, "A paradise for bold flavors, the shuizhuyu is punchy ... it does not suit kids." pontua na dimensão culinária de Sichuan porque menciona "shuizhuyu", e sua relevância permanece em 0,5214. Portanto, se o primeiro candidato não tivesse informações sobre culinária de Sichuan, seria classificado em uma posição inferior.

Em produção, envie o texto original completo como candidato para reordenação. Não remova informações-chave que possam carregar relevância, como nomes de pratos ou palavras de categoria, apenas para encurtar o texto. Ao avaliar a qualidade da reordenação, utilize também o mesmo texto completo usado em produção.

Etapa 6: Calcular a taxa de avaliações positivas e gerar resumos de avaliações

Utilize o AI_SENTIMENT para classificar o sentimento de todas as avaliações de uma loja e agregue a taxa de avaliações positivas na aplicação. Em seguida, use o AI_SUMMARIZE para condensar as avaliações em um comentário de uma linha. Esta etapa lê os dados inseridos na Etapa 2.

# ==================== Step 6: Sentiment analysis for the positive-review rate + summary generation ====================
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 Classify sentiment and aggregate the 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 the review summary of the store
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 store in English, highlighting the signature dishes, the diners it suits, and the atmosphere."},
})
assert status == 200 and summ.get("code") == 0, summ
print(f"Store summary: {summ['data']['output']['outputs'][0]}")

Tomando o shop_id 1001 como exemplo: ambas as avaliações são classificadas como positivas, a taxa de avaliações positivas é de 100% e o resumo gerado é "The place is clean and the maoxuewang is mild and palatable. The service is patient and children's tableware is available, so it suits family meals with kids, and you wait for a table on weekends."

A agregação compara cada rótulo com os valores passados no parâmetro de requisição categories. O AI_SENTIMENT também pode retornar um valor vazio quando o modelo não consegue determinar o sentimento; filtre os valores vazios antes de agregar, conforme demonstrado pela lista valid.

Tipos de geometria e funções espaciais

Esta seção descreve o tipo GEOMETRY e as funções de relação espacial ST_ utilizadas no procedimento.

GEOMETRY e WKT

Se você armazenar longitude e latitude como dois campos DOUBLE, o banco de dados os tratará como dois números e não conseguirá determinar "qual está dentro de qual" ou "se duas áreas se interceptam". O GEOMETRY é o tipo geoespacial fornecido pelo Milvus 2.6. Ele armazena pontos, linhas e polígonos como objetos geométricos, o que possibilita operações de relação espacial e permite que um índice geoespacial acelere as consultas.

Os valores de um campo GEOMETRY usam o formato WKT (Well-Known Text). Os tipos comuns são:

Tipo WKT

Exemplo

Significado

Correspondente neste tópico

POINT

POINT(116.4612 39.9088)

Um único ponto

As coordenadas de uma loja

LINESTRING

LINESTRING(116.46 39.90, 116.47 39.91)

Uma polilinha

Um segmento de estrada ou rota de entrega

POLYGON

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

Uma área fechada cujas primeira e última coordenadas são idênticas

Um bairro comercial ou área de entrega

MULTIPOINT

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

Múltiplos pontos

Várias lojas de uma mesma marca

MULTIPOLYGON

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

Múltiplas áreas

Vários distritos administrativos

As coordenadas seguem a ordem longitude primeiro e latitude segundo, ou seja, POINT(lng lat), que é o inverso da ordem falada "latitude ... longitude ...". Todas as coordenadas neste tópico utilizam o sistema de coordenadas WGS84.

Funções de relação espacial ST_

Funções que começam com ST_ determinam a relação espacial entre dois objetos geométricos. Escreva-as em uma expressão filter; elas 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)

Se A está inteiramente dentro de B.

Determina se uma loja está dentro de um polígono de bairro comercial ou dentro de um raio definido.

ST_CONTAINS(A, B)

Se A contém inteiramente B, o inverso de ST_WITHIN.

Determina se um bairro comercial contém uma determinada loja, para análise de atribuição.

ST_INTERSECTS(A, B)

Se A e B têm alguma interseção ou contato.

Determina se a área de entrega de uma loja intercepta a célula de grade onde o usuário está.

As funções a seguir também são suportadas e podem ser usadas conforme necessário:

Função

Descrição

Cenário

ST_EQUALS

Dois objetos geométricos são idênticos.

Deduplicação de geometrias.

ST_TOUCHES

Apenas os limites se tocam e os interiores não se sobrepõem.

Determina se bairros comerciais adjacentes fazem fronteira entre si.

ST_OVERLAPS

Sobreposição parcial sem contenção.

Determina se áreas de entrega se cruzam.

ST_CROSSES

Interseção de travessia.

Determina se uma estrada atravessa um bairro comercial.

Lógica de classificação do ranking

Encadear as etapas anteriores resulta em um fluxo completo de geração de ranking: a filtragem geoespacial restringe os candidatos a "próximos", a busca semântica identifica os "adequados ao gosto", a reordenação ajusta a ordem, e o sentimento somado à sumarização preenche a "reputação". Para produzir uma linha por loja, deduplique os candidatos por shop_id na aplicação.

A posição final pode fundir três sinais:

  • Sinal geoespacial: estar dentro do raio ou do bairro comercial atua como limiar de admissão; lojas que não se qualificam são filtradas diretamente. As lojas qualificadas recebem então um peso base de acordo com a distância.

  • Relevância semântica: o relevance_score retornado pelo AI_RERANK, que mede o quão bem as avaliações de uma loja atendem à solicitação do usuário.

  • Reputação: a taxa de avaliações positivas agregada pelo AI_SENTIMENT.

    Combine esses sinais com uma fusão ponderada no formato w1 · relevance score + w2 · positive-review rate + w3 · distance decay e ajuste os pesos conforme seus objetivos de negócio. Sob essa fusão, uma loja pode alcançar uma alta classificação geral porque corresponde fortemente à semântica "family-friendly" e porque essa correspondência é combinada com uma alta taxa de avaliações positivas e uma distância menor, e não apenas pela avaliação em si.

Extensões adicionais

  • Busca multimodal de lojas: utilize o qwen3-vl-embedding para vetorizar fotos de lojas e pratos, permitindo buscas por imagem como "um lugar próximo com este tipo de decoração". Essa abordagem também pode ser fundida com filtragem geoespacial em uma única busca.

  • Pré-computação offline de reputação: quando uma loja possui um grande volume de avaliações, use métodos em lote como o AI_BATCH para pré-computar offline a taxa de avaliações positivas e o resumo das avaliações e persisti-los. Assim, as consultas do ranking apenas leem os resultados, equilibrando custo e latência.

  • Relações espaciais mais ricas: utilize o ST_INTERSECTS para determinar se a área de entrega de uma loja intercepta a célula de grade onde o usuário está, e use o ST_CONTAINS para análise de atribuição de bairro comercial. Isso estende a fusão "geoespacial + IA" da busca de lojas para despacho, seleção de locais e outras etapas.