すべてのプロダクト
Search
ドキュメントセンター

Vector Retrieval Service for Milvus:Alibaba Cloud Milvus で地理空間検索と AI 機能を組み合わせるローカルライフスタイルリーダーボードの構築

最終更新日:Aug 14, 2026

Alibaba Cloud Milvus は、GEOMETRY 地理空間タイプと AI Functions を組み合わせ、1 つのコレクション上で地理空間範囲フィルタリングとセマンティックリコールの両方を単一の検索で実行します。このトピックでは、近傍フィルタリングと「好みに合った」リコールを同時に実行し、その上に感情分析と要約生成を重ねて、説明可能なローカルライフスタイルリーダーボードを構築する方法について説明します。

ソリューション概要

「近くで美味しいものは何か」は、ローカルライフスタイルのシナリオで最も頻繁に寄せられるリクエストです。このリクエストに的確に対応するには、システムが同時に次の 3 つの条件を満たす必要があります:

  • 近隣:ユーザーの現在地から特定の半径以内、またはユーザーがいる商業地区内にある店舗。

  • 好み:「家族向けの四川料理店」や「商談ができる静かなカフェ」など、タグで正確に列挙できないリクエスト。

  • 信頼性:実際のレビューの内容、高評価率の高さ、繰り返し言及される長所や不満点。

    従来の実装では通常、地理空間フィルタリング用の空間データベース、セマンティックリコール用のベクトルデータベース、感情分類と要約用の NLP サービスという 3 つのシステムを組み合わせます。各システムは独自のデータコピーを保持するため、システム間の結合、データ同期、一貫性の維持にコストがかかり、1 つのクエリで複数のサービスを連鎖させ、数回のネットワークラウンドトリップが必要になります。

ディメンション

従来のソリューション (空間データベース + ベクトルデータベース + NLP サービス)

Alibaba Cloud Milvus

システム数

3 つ以上、それぞれが独自のデータコピーを保持

1 つ、単一のデータソース

地理空間とセマンティックの融合

システム間の結合または複数のリコールラウンド

1 回の search():近似最近傍 (ANN) 検索 + ST_ フィルタリング

レビューの理解

自社構築または外部の感情分析・要約モデル

組み込みの AI_SENTIMENT / AI_SUMMARIZE

ベクトル化パス

アプリケーションが最初に埋め込みを呼び出し、その後データを書き込む

挿入時にベクトル化

一貫性と同期

手動での管理

単一データベース内で自然に一貫性が保たれます

Alibaba Cloud Milvus は、これらの機能を 1 つのコレクションと 1 回の検索に集約します:

機能

実装

地理空間範囲フィルタリング

経度と緯度を GEOMETRY フィールドに格納し、商業地区を POLYGON として表現し、検索時に ST_WITHIN などの空間関係関数を filter 式に記述します。

レビューに対するセマンティック検索

レビューテキストは、挿入時に AI_EMBEDDING によって自動的にベクトル化され、検索では同じモデルで生成されたクエリベクトルに対して近似最近傍 (ANN) クエリが実行されます。

関連性による再ランキング

AI_RERANK を使用して、リコールされた候補をクエリとの関連性に基づいて並べ替えます。

評判の定量化

AI_SENTIMENT を使用してレビューの感情を分類し、アプリケーション側で高評価率を集計します。また、AI_SUMMARIZE を使用して店舗のレビューを短い要約に圧縮します。

地理空間フィルタリングとセマンティック検索は、1 回の search() 呼び出しで実行されます。近似最近傍 (ANN) ベクトル検索が「好み」を、filter 内の空間関数が「近隣」を処理し、両方が 1 つのリクエスト内でまとめて解決されます。さらに、その上で再ランキングが適用され、リーダーボードの候補が直接生成されます。

このトピックでは、以下の AI 関数を使用します:

関数

機能

リーダーボードでの役割

呼び出し方法

AI_EMBEDDING

挿入時にレビューテキストを自動的にベクトルに変換するので、アプリケーションは最初にモデルを呼び出す必要がありません。

「家族向けの四川料理店」のようなセマンティックリクエストをベクトル検索でマッチングできるようにします。

コレクションスキーマで関数 (FunctionType.TEXTEMBEDDING) として宣言します。

AI_RERANK

リコールされた候補をクエリとの関連性に基づいて並べ替えます。

リクエストに最も適合する店舗をリーダーボードの最上位に移動させます。

search() でランカー (FunctionType.RERANK) としてアタッチするか、REST エンドポイント /v2/vectordb/ai/rerank を介して呼び出します。

AI_SENTIMENT

レビューが肯定的、否定的、または中立的であるかを判断します。

店舗の高評価率を集計します。

REST エンドポイント /v2/vectordb/ai/sentiment を介して呼び出します。

AI_SUMMARIZE

多数のレビューを短い要約に圧縮します。

リーダーボードに表示される一行コメントを生成します。

REST エンドポイント /v2/vectordb/ai/summarize を介して呼び出します。

AI_EMBEDDING と AI_RERANK はコレクションに宣言するか、検索にアタッチできますが、AI_SENTIMENT と AI_SUMMARIZE は REST エンドポイントを介してのみ利用可能です。

前提条件

  • Milvus 2.6 インスタンスが作成されます。GEOMETRY 型と AI 機能には 2.6 カーネルが必要です。インスタンスを作成した後、モデルサービスを別途バインドする必要はなく、コレクションで直接関数を宣言します。

  • インターネット経由でインスタンスにアクセスするには、インスタンス詳細ページの [Security Configuration] タブで [Public Network Access] が有効化されており、クライアントエグレス IP アドレスがパブリックアクセスホワイトリストに追加されている必要があります。

  • pymilvus がインストールされている必要があります。このトピックの例は、pymilvus 3.0.0 で検証済みです。

制約と使用上の注意

コードをコピーする前に、次の動作を確認してください。いくつかはエラーではなく、結果を返さなかったり、空の結果を返したりします:

  • 地理空間境界のセマンティクス — ジオメトリオブジェクトがポリゴンの境界上にちょうど位置する場合、 ST_WITHIN は false を返し、 ST_INTERSECTS は true を返します。たとえば、半径の円をポリゴンで近似した場合、円周上にちょうど位置する店舗は ST_WITHIN では一致しません。ビジネスロジックで境界を含める必要がある場合は、代わりに ST_INTERSECTS を使用してください。 MULTIPOINT の場合は、点のうち 1 つでも境界上に位置すると、 ST_WITHIN は false を返します。

  • 半径近似の精度 — circle_to_wkt は平面近似で経度と緯度を変換するため、緯度が高くなるほど誤差が大きくなります。このメソッドは、粗い半径フィルターに適しています。ビジネスロジックが境界の精度に敏感な場合は、 num_seg のセグメント数を増やすか、アプリケーション側で正確な距離を再度検証してください。

  • RESTインターフェイスのポート — RESTfulインターフェイスは gRPC とポート 19530 を共有するため、RESTインターフェイスを呼び出す際は、アドレスにポートを明示的に含める必要があります。例: http://c-xxx.milvus.aliyuncs.com:19530。ポートを省略すると、リクエストはデフォルトでポート 80 に送信され、接続がタイムアウトします。

  • RESTリランクのパラメーター — RESTリランクのインターフェイスでは、 documents は文字列の配列である必要があり、 query はトップレベルの文字列である必要があります。 top_n パラメーターはサポートされていません。指定すると param [top_n] is not supported が返されます。このインターフェイスは候補ごとに 1 つのスコアを常に返すため、リストを切り捨てる必要がある場合は、スコアでソートし、アプリケーション側で上位 N 件を取得してください。

  • 未確定のセンチメント — AI_SENTIMENT は空の値を返す場合があります。これは、モデルがセンチメントを判定できなかったことを意味します。肯定的レビュー率を集計する前に空の値をフィルターし、未確定のサンプルが分母に含まれないようにしてください。

手順

同じセッション内で、ステップを順番に実行します。コードスニペットは 1 つの連続したスクリプトを構成しており、後のステップでは前のステップで定義されたクライアント、ヘルパー関数、変数を再利用します (例:ステップ 3 の circle、ステップ 4 の QUERY と geo_filter)。単一のステップのみをコピーする場合は、まずアップストリームの変数を定義してください。

ステップ 0: 共有コードの準備 (必須)

次のコードには、接続設定、REST インターフェイスのラッパー、および円をポリゴンとして近似するユーティリティ関数が含まれています。以降のすべてのステップはこのコードに依存するため、MILVUS_HOST と MILVUS_TOKEN を、お使いのインスタンスの情報に置き換えてください。

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

# ==================== 接続設定 ====================
MILVUS_HOST = "http://c-xxx.milvus.aliyuncs.com"
MILVUS_TOKEN = "root:xxx"
# RESTful インターフェイスは gRPC とポート 19530 を共有します。ポートを明示的に含めてください。
# そうしないと、リクエストはデフォルトでポート 80 に送信され、接続がタイムアウトします。
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]]:
    """AI_RERANK / AI_SENTIMENT / AI_SUMMARIZE の REST インターフェイス向けの共有ラッパーです。"""
    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:
    """(lng, lat) を中心とし、半径 radius_km の円を、WKT の正多角形として近似します。
    注: 経度と緯度は角度です。この変換は平面近似のため、緯度が高くなるほど誤差が大きくなります。"""
    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)}))"

MILVUS_HOST の値はクライアント URI として使用します。一方、MILVUS_REST_BASE_URL は、後続のステップで REST を呼び出すためにポート 19530 を付加します。ポートの明示が必要なのは REST インターフェイス です。

ステップ1:地理空間フィールドと埋め込み関数を持つコレクションの作成

location フィールドでは DataType.GEOMETRY を使用して店舗の座標点を格納します。review フィールドでは、AI_EMBEDDING 関数によって、挿入時に 1,024 次元のベクトルが自動的に生成されます。同時に、ベクトルフィールドには HNSW インデックスを、地理空間フィールドには R-Tree インデックスを作成します。

警告

次のコードでは、コレクションを作成する前に drop_collection を実行します。これにより、shop_reviews という名前の既存のコレクションとそのすべてのデータが完全に削除されます。すでにビジネスデータを保持しているインスタンスでは、別のテストコレクション名を使用するか、drop_collection の分岐を削除してください。

# ==================== ステップ1:コレクションの作成 ====================
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)                          # 座標を格納、WKT:POINT(lng lat)
schema.add_field("review", DataType.VARCHAR, max_length=4096)            # 生のレビューテキスト、埋め込みの入力として使用
schema.add_field("review_embedding", DataType.FLOAT_VECTOR, dim=1024)    # レビューベクトル

# 挿入時にレビューを自動的にベクトル化
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},
)
# GEOMETRY フィールドに R-Tree 地理空間インデックスを作成して空間クエリを高速化
index_params.add_index(
    field_name="location",
    index_type="RTREE",
)
client.create_collection(
    collection_name=collection_name,
    schema=schema,
    index_params=index_params,
)

review_embedding の次元は、関数パラメーターの dim 値と一致させる必要があります。GEOMETRY フィールドに RTREE インデックスを作成すると、ST_ 空間クエリが高速化され、データ量が多い場合に推奨されます。

このトピックの model_name の値 (ここでは qwen3.7-text-embedding、および後の手順で示すリランキング、感情分析、要約の各モデル) は一例です。それぞれをご自身のインスタンスで利用可能なモデルに置き換えてください。

ステップ 2:店舗とレビューのデータの挿入

店舗の座標とレビューの原文を入力します。review_embedding は、関数によって自動的に生成されます。

# ==================== ステップ 2:店舗とレビューのデータの挿入 ====================
# review_embedding は関数によって自動的に生成されるため、手動でベクトルを渡す必要はありません
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)

データを挿入した後は flush() を呼び出してください。そうしないと、直後に実行した検索で空の結果が返される場合があります。load_collection() はコレクションをメモリにロードします。この操作は、コレクションを検索する前に必要です。

ステップ 3:地理空間範囲によるフィルタリング

対象エリアの形状に基づいて範囲の指定方式を選択します。

  • 行政区画またはビジネス地区 — エリアを POLYGON として表現し、ST_WITHIN を使用します。

  • ユーザーの現在地周辺のエリア — circle_to_wkt を使用して指定した半径の円をポリゴンとして近似し、ST_WITHIN を使用します。

  • 境界を含める、または重なり合うエリアを検出する必要があるロジック — 代わりに ST_INTERSECTS を使用します。境界上に正確に位置するジオメトリは、ST_WITHIN では一致しないためです。

    次のコードは、ビジネス地区のポリゴンと、circle_to_wkt によってポリゴンとして近似された半径の円の両方を示しています。

# ==================== ステップ 3: 地理空間範囲のフィルタリング ====================
# 3.1 ビジネス地区ポリゴン内の店舗をクエリ
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("ビジネス地区内のレビューレコード:")
for h in hits:
    print(f"  - {h['shop_name']} ({h['category']})  {h['location']}")

# 3.2 半径 3 km 内をクエリ: 円をポリゴンとして近似し、店舗がその内部に入るかを確認
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"中心点 ({center_lng}, {center_lat}) から 3 km 以内のレビューレコード:")
for n in nearby:
    print(f"  - {n['shop_name']} ({n['category']})  {h['location']}")

サンプルデータには 3 つの店舗にわたる 4 件のレビューレコードがあります (Bashu Homestead には 2 件のレビューがあります)。そのため、いずれのクエリも 4 件のレビューレコードを返します。店舗ごとに 1 行を出力するには、アプリケーション側で shop_id による重複排除を行います。

ステップ 4:セマンティック検索と地理空間フィルタリングの組み合わせ

このステップでは、「近く」 (filter 内の空間関数) と「好みに合った」 (ANN ベクトル検索) を単一の search() 呼び出しに統合します。ステップ 3 の circle を再利用します。

# ==================== ステップ 4:セマンティック検索と地理空間フィルタリングの組み合わせ ====================
# レビューベクトルに対する ANN 検索が「好みに合った」を扱い、フィルター内の空間関数が「近く」を扱うことで、
# 両方が 1 つのリクエストで同時に解決されます
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}")
for rank, hit in enumerate(results[0], 1):
    e = hit["entity"]
    print(f"  {rank}. [類似度 {hit['distance']:.4f}]  {e['shop_name']} | {e['review'][:24]}")

次の例は、検索結果を示しています。「Bashu Homestead Family Sichuan」が 1 位にランクされています。これは、そのレビューに「ハイチェア」や「店員は親切」といった、「ファミリーフレンドリー」と意味的に近い内容が含まれているためです。

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

ステップ 5:AI_RERANK による候補のリランキング

ベクトル類似度は意味的な近さを反映しますが、クエリに対する結果の関連性と完全に一致するとは限りません。AI_RERANK を使用してリコールされた候補をリランキングすると、クエリの意図により適合する店舗が上位にランク付けされます。このステップでは、ステップ 4 の QUERY と geo_filter を再利用します。

次の 2 つのオプションがあります。シナリオに応じていずれかを選択してください:

  • 検索パイプライン内でのリランキング — search() にランカーをアタッチします (5.1 で使用)。モデルは、近似最近傍検索でコレクションから取得した候補をリランキングします。

  • 既存のテキストリストのリランキング — ステートレスな REST インターフェイス /v2/vectordb/ai/rerank を呼び出します (5.2 で使用)。このパスはテキストリストを直接受け取り、検索に依存しません。

    以下の 2 つのサブセクションは代替手段であり、連続したステップではありません。

# ==================== 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
# 5.1 search() に RERANK ランカーをアタッチします:ANN リコールの後、モデルは候補をリランキングします
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
# 5.2 ステートレスな REST インターフェイス /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]}")

次の例は、リランキング後の出力を示しています。リランキングを有効にしてsearch()を呼び出すと、リランキングスコアがdistanceフィールドに返されます。この値は大きいほど関連性が高いことを示します。この例では、"Bashu Homestead Family Sichuan" の 2 件のレビューはいずれも "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

REST インターフェイスは、結果を data.output.results に返します。各項目は 1 つの候補に対応し、index (documents リクエストパラメーターにおける候補の位置) と relevance_score (関連性スコア) を含みます。スコアで降順にソートすると、リランキング後の結果を取得できます:

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

候補テキストは、クエリのすべての意味的ディメンションを網羅する必要があります。 この例のクエリ "family-friendly Sichuan restaurants nearby" は、「family-friendly」のディメンションと「Sichuan cuisine」のディメンションの両方を含みます。いずれか一方が欠ける候補は、スコアが大きく低下します。最初の候補を例に見てみましょう:

候補テキスト

関連性スコア

Came with my kid. High chairs and small bowls are available, the servers are patient. (四川料理に関する情報なし)

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

四川料理の料理名を 1 つ追加するだけで、スコアは 0.4723 から 0.6666 に上がります。一方、"A paradise for bold flavors, the shuizhuyu is punchy ... it does not suit kids." は、"shuizhuyu" に言及しているため四川料理のディメンションでスコアを獲得し、関連性は 0.5214 のままです。そのため、最初の候補に四川料理の情報がなければ、より下位にランク付けされます。

したがって本番環境では、リランキング用の候補として元の全文テキストを送信し、テキストを短くするためだけに、料理名やカテゴリーワードなど関連する可能性のある重要な情報を削除しないでください。リランキングの品質を評価する際も、オンラインで使用するものと同じ全文テキストを使用してください。

ステップ 6:好評率の計算とレビューの要約生成

AI_SENTIMENT を使用して、店舗のすべてのレビューの感情を分類し、アプリケーション側で好評率を集約します。次に、AI_SUMMARIZE を使用してレビューを 1 行のコメントに要約します。このステップでは、ステップ 2 で挿入されたデータを読み取ります。

# ==================== ステップ 6:好評率のための感情分析 + 要約生成 ====================
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 感情を分類し、好評率を集約する
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]              # 空の値は未確定とみなし、集約から除外する
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 店舗のレビューの要約を生成する
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]}")

shop_id 1001 を例にとると、両方のレビューが肯定的と分類され、好評率は 100% となり、生成された要約は「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.」となります。

集約では、各ラベルが categories リクエストパラメーターで渡された値と照合されます。AI_SENTIMENT は、モデルが感情を判断できない場合に空の値を返すこともあります。valid リストに示されているように、集約する前に空の値をフィルターしてください。

ジオメトリ型と空間関数

このセクションでは、手順で使用する GEOMETRY 型と ST_ 空間関係関数について説明します。

GEOMETRY と WKT

経度と緯度を 2 つの DOUBLE フィールドとして保存すると、データベースはそれらを 2 つの数値として扱うため、「どちらがどちらの内側にあるか」や「2 つのエリアが交差しているか」を判定できません。GEOMETRY は、Milvus 2.6 が提供する地理空間データ型です。点、線、ポリゴンをジオメトリオブジェクトとして保存することで、空間関係を演算でき、地理空間インデックスによってクエリを高速化することもできます。

GEOMETRY フィールドの値には、WKT (Well-Known Text) 形式を使用します。一般的な型は次のとおりです:

WKT 型

例

意味

このトピックでの例

POINT

POINT(116.4612 39.9088)

単一の点

1 店舗の座標

LINESTRING

LINESTRING(116.46 39.90, 116.47 39.91)

ポリライン

道路区間、または配送ルート

POLYGON

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

最初と最後の座標が一致する閉じた領域

商圏、または配送エリア

MULTIPOINT

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

複数の点

同一ブランドの複数店舗

MULTIPOLYGON

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

複数の領域

複数の行政区画

座標は、経度が先、緯度が後の順序、つまり POINT(lng lat) です。これは、一般的な呼び順である「緯度、経度」とは逆です。このトピックの座標はすべて WGS84 座標系を使用します。

ST_ 空間関係関数

ST_ で始まる関数は、2 つのジオメトリオブジェクト間の空間関係を判定します。これらは filter 式に記述し、true または false を返します。このトピックでは主に次の 3 つを使用します:

関数

説明

このトピックでの使用方法

ST_WITHIN(A, B)

A が B の内部に完全に含まれる場合に true を返します。

店舗が商圏のポリゴン内にあるか、または指定した半径内にあるかを判定します。

ST_CONTAINS(A, B)

A が B を完全に包含する場合に true を返します (ST_WITHIN の逆)。

帰属分析のために、商圏が特定の店舗を含むかどうかを判定します。

ST_INTERSECTS(A, B)

A と B が交差または接触する場合に true を返します。

店舗の配送エリアが、ユーザーがいるグリッドセルと交差しているかどうかを判定します。

次の関数もサポートされており、必要に応じて使用できます:

関数

説明

シナリオ

ST_EQUALS

2 つのジオメトリオブジェクトが同一である場合に true を返します。

ジオメトリの重複排除。

ST_TOUCHES

境界のみが接触し、内部はオーバーラップしない場合に true を返します。

隣接する商圏同士が境界を接しているかどうかを判定します。

ST_OVERLAPS

互いに包含せず、部分的にオーバーラップする場合に true を返します。

配送エリアが重なっているかどうかを判定します。

ST_CROSSES

ジオメトリオブジェクトが互いに交差する場合に true を返します。

道路が商圏と交差しているかどうかを判定します。

Leaderboard ranking logic

Chaining the preceding steps gives one complete leaderboard generation flow: geospatial filtering narrows the candidates down to "nearby", semantic search matches "on taste", reranking adjusts the order, and sentiment plus summarization fill in the "reputation". To produce one row per store, deduplicate the candidates by shop_id on the application side.

The final position can fuse three signals:

  • Geospatial signal: whether a store falls within the radius or the business district acts as the admission threshold, and stores that do not qualify are filtered out directly. Qualified stores then get a base weight according to distance.

  • Semantic relevance: the relevance_score returned by AI_RERANK, which measures how well a store's reviews fit the user's request.

  • Reputation: the positive-review rate aggregated by AI_SENTIMENT.

    You can combine these signals with a weighted fusion in the form of w1 · relevance score + w2 · positive-review rate + w3 · distance decay, and tune the weights to your business goals. Under this fusion, a store can rank high overall because it strongly matches the "family-friendly" semantics and because that match is combined with a high positive-review rate and a shorter distance, rather than by rating alone.

さらなる拡張

  • マルチモーダル店舗検索: qwen3-vl-embedding を使用して店舗や料理の写真をベクトル化することで、「このような内装の近くの店舗」といった画像ベースの店舗検索が可能になります。この検索は、単一の検索内で地理空間フィルタリングと融合させることもできます。

  • 評判のオフライン事前計算: 店舗に大量のレビューがある場合、AI_BATCH などのバッチ方式を使用して高評価レビュー率とレビューサマリーをオフラインで事前計算し、永続化します。これにより、リーダーボードクエリは結果を読み取るだけで済むようになり、コストとレイテンシーのバランスを取ることができます。

  • より豊富な空間関係: ST_INTERSECTS を使用して店舗の配達エリアがユーザーのいるグリッドセルと交差するかを判断し、ST_CONTAINS を商圏の帰属分析に使用して、「地理空間 + AI」の融合を店舗の検索から配送、立地選定などのステップまで拡張します。