Zenn
🧚

Vertex AI Vector Search のハイブリッド検索を徹底解説:ベクトル検索の精度課題を克服する方法

2025/02/20に公開

はじめに

こんにちは、クラウドエース 第三開発部の松本です。
普段はデータ基盤や機械学習システムを構築したり、Google Cloud 認定トレーナーとしてトレーニングを提供しています。

近年、RAG(Retrieval-Augmented Generation)などの技術活用において、ベクトル検索を用いた情報検索が行われることが多くなっています。しかし、ベクトル検索だけでは精度に課題が残ることがあります。そこで、従来のキーワード検索を組み合わせることで検索精度を向上させる「ハイブリッド検索」が注目されています。

本記事では、Google Cloud の Vertex AI Vector Search で提供されているハイブリッド検索について解説いたします!

対象読者

  • Vertex AI Vector Search のハイブリッド検索を使用して検索精度を向上させたい方
  • Vertex AI Vector Search を活用したハイブリッド検索の実装方法を知りたい方
  • Google Cloud サービスを活用した検索機能に関心がある方

キーワード検索とセマンティック検索

ハイブリッド検索を理解するために、まずは「キーワード検索」と「セマンティック検索」について解説します。
keyword_and_semantic_search
キーワード検索とセマンティック検索

キーワード検索とは、検索クエリ内のキーワードやフレーズが、検索対象のテキストに含まれているかどうかを基準にして検索する手法です。一般的に、単語の完全一致や部分一致に基づいて検索を行います。この手法は、検索エンジンやデータベースの全文検索機能などで広く利用されています。

セマンティック検索とは、検索クエリの意味や文脈を考慮し、関連性の高い情報を取得する検索手法です。一般的に単語の類似性や文章全体の意味を数値化し、ベクトル空間上で検索を行います。この手法は、ベクトル検索技術や LLM を活用した情報検索システムなどで広く利用されています。(RAG などで活用されるベクトル検索も、セマンティック検索の一種になります。)

それぞれの検索手法には、以下のようなメリット・デメリットがあります。

検索手法 メリット デメリット
キーワード検索 検索クエリと完全一致・部分一致するデータを確実に取得できる。 類義語や異表記に対する対応が難しく、文脈を考慮しないため関連情報を見落とす可能性がある。
セマンティック検索 言葉の意味や文脈を考慮し、類義語や異なる表現にも柔軟に対応できる。 埋め込み(エンベディング)モデルが学習していない単語(マイナーな専門用語や製品番号などの識別子)の検索が難しい。

ハイブリッド検索とは

ハイブリッド検索とは、キーワード検索とセマンティック検索を組み合わせることで、それぞれの強みを活かし、弱点を克服することで、より精度の高い検索を実現するための手法です。

hybrid_search_process
ハイブリッド検索のプロセス

ハイブリッド検索は以下のプロセスによって実行されます。

  1. ユーザーが入力した検索クエリをセマンティック検索とキーワード検索の両方に渡します。
  2. セマンティック検索とキーワード検索が個別に検索を行い、以下の基準に基づいて検索結果ごとにランクを算出します。
    • セマンティック検索:クエリとテキストのベクトル類似度に基づいて順位を決定します。意味的に関連性の高い結果が上位にランク付けされます。
    • キーワード検索:クエリに含まれるキーワードやフレーズの一致度を基準に順位を決定します。完全一致や部分一致の精度に依存して結果がランク付けされます。
  3. それぞれ算出されたランクを融合します。一般的に「RRF(Reciprocal Rank Fusion)」と呼ばれる融合の手法を用います。
  4. RRF によって融合されたランク結果を基に検索結果を出力します。
RRF(Reciprocal Rank Fusion)とは

RRF(Reciprocal Rank Fusion)とは、複数のランキングを融合する手法です。

ハイブリッド検索では、キーワード検索とセマンティック検索の結果がそれぞれ異なるベクトル空間で測定されるため、単純に比較することができません。しかし、RRF を使うことで、これらの検索結果をバランスよく組み合わせることができます。

RRF では、各ランキングリスト内のアイテムの順位に基づき、逆数ランク(例: 1 位なら 1、2 位なら 0.5)を計算し、全リストの逆数ランクを合計してスコアを算出します。最終スコアが高い順に並べ替えることで、異なるソースからの検索結果を公平に統合し、関連性の高いアイテムを上位に配置します。

【例】

アイテム キーワード検索のランキング
(逆数ランク)
セマンティック検索のランキング
(逆数ランク)
合計スコア
A 1 0.25 1.25
B 0.25 1 1.25
C 0.5 0.2 0.7
D 0.2 0.5 0.7
E 0.33 0.0 0.33
F 0.0 0.33 0.33

尚、RRF の詳しい内容については以下の論文が参考になりますので、興味がございましたらご覧ください。

Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods

Vertex AI Vector Search とは

Vertex AI Vector Search とは、Google Cloud の Vertex AI が提供するベクトル検索機能です。Google が運営する研究開発部門である Google Research が開発したベクトル検索技術をベースにしており、高性能な検索が可能です。

https://cloud.google.com/vertex-ai/docs/vector-search/overview?hl=ja

Vertex AI Vector Search は、従来からセマンティック検索をサポートしていましたが、2024 年 5 月からキーワード検索を組み合わせたハイブリッド検索がサポートされ、2024 年 12 月に一般提供(GA)されています。

構成要素

Vertex AI Vector Search の構成要素は以下の通りです。

vertex_ai_vector_search_index
Vertex AI Vector Search の構成要素

  • インデックス: エンベディングベクトルに関する構成ファイルです。検索クエリに対して類似性計算を行うために使用されます。Cloud Storage に保存された embedded_data.json などのエンべディングされたデータを参照し、インデックスを作成します。また、インデックス構成パラメータとして入力ベクトルの次元数、検索アルゴリズム、シャードサイズなどを指定できます。インデックスには以下の更新方法があります。
    • バッチ更新: 定期的に蓄積されたデータを一括更新する方式で、週単位や月単位の更新に適しています。
    • ストリーミング更新: 新しいデータが追加されるたびにストリーミングでインデックスを更新する方式で、新しいデータをリアルタイムで反映させたい場合に適しています。
  • インデックスエンドポイント: 検索リクエストを受け付けて適切なインデックスを通じて検索を実行するためのエントリーポイントです。
  • デプロイされたインデックス: VM 上にデプロイされたインデックスであり、インデックスエンドポイントを通じて利用されます。尚、検索パフォーマンスに影響するデプロイ設定として、マシンタイプや最小・最大レプリカ数があります。

割り当てと上限

デプロイされたインデックス数やノード数などの割り当てと上限があります。詳しくは以下の公式ドキュメントをご参照ください。
https://cloud.google.com/vertex-ai/docs/quotas?hl=ja#vector-search

料金

Vertex AI Vector Search は、デプロイされたインデックスにおける各 VM の 1 ノード時間あたりの料金が発生します。また、インデックスの新規作成や更新(バッチ更新またはストリーミング更新)に対する料金も発生します。詳細については以下の公式ドキュメントをご参照ください。
https://cloud.google.com/vertex-ai/pricing?hl=ja#vectorsearch

実装例

今回は以下のようなサンプルデータの csv ファイルを用意し、これらのテキストに対してハイブリッド検索を行ってみたいと思います。

このデータの特徴は、「商品番号:B123」のように特定の ID が含まれていることであり、このような ID を含む検索クエリに対して、ハイブリッド検索によって ID を正確に捉えつつ、テキストの意味も理解した検索結果を得ることが期待されます。

sample_data.csv
id,content
1,このスタイリッシュなノートパソコン(商品番号:B123)は、ビジネスシーンに最適です。
2,商品番号:D789 のオーブントースターは、様々な調理モードを搭載しています。
3,当店では、スマートフォン(商品番号:A123)の豊富なカラーバリエーションをご用意しております。
4,時短料理にも最適な、オーブントースター(商品番号:D789)は、忙しいあなたをサポートします。
5,商品番号:B123 のノートパソコンは、薄型軽量で持ち運びにも便利です。
6,清潔で快適な空間を、加湿空気清浄機(商品番号:E012)で実現しましょう。
7,革新的な技術を搭載した最新モデルのスマートフォン(商品番号:A123)です。
8,商品番号:C456 の4Kテレビは、高画質映像を迫力の大画面で楽しめます。
9,臨場感あふれる映像体験を提供する、4Kテレビ(商品番号:C456)です。
10,毎日の料理を楽しくする、多機能オーブントースター(商品番号:D789)です。
11,商品番号:E012 の加湿空気清浄機は、花粉やPM2.5を除去します。
12,ご自宅のリビングを映画館に変える、4Kテレビ(商品番号:C456)はいかがですか。
13,商品番号:A123 のスマートフォンは、高性能カメラと大容量バッテリーが魅力です。
14,ハイスペックなノートパソコン(商品番号:B123)で、あなたの創造性を解き放ちましょう。
15,お部屋の空気を快適にする、加湿空気清浄機(商品番号:E012)です。

1. 事前準備

IAM 権限設定

使用するサービスアカウントに対し IAM 権限を設定します。手順については、以下の公式ドキュメントをご参照ください。
(IAM 権限設定は利用環境となる Google Cloud プロジェクトで実施してください。)

  • Vertex AI ユーザー
  • ストレージ管理者
  • Service Usage 管理者

https://cloud.google.com/vertex-ai/docs/vector-search/quickstart?hl=ja#permissions

API 有効化

以降の手順では gcloud CLI を使用します。ローカル環境で実行したい場合は、公式ドキュメントの手順に従いインストールください。(尚、Cloud Shell を利用される場合はインストール不要です。)また、gcloud CLI の操作などを含む詳細については、以下の公式ドキュメントをご参照ください。
https://cloud.google.com/sdk/gcloud?hl=ja

以下のコマンドを実行して、対象の Google Cloud プロジェクトで Compute Engine、Vertex AI、Cloud Storage の API を有効化します。

export PROJECT_ID={project_id} #{project_id} は 利用する環境の Google Cloud プロジェクト ID を指定してください

gcloud services enable compute.googleapis.com aiplatform.googleapis.com storage.googleapis.com --project $PROJECT_ID

Cloud Storage バケット作成

以下のコマンドを実行して、Cloud Storage バケットを作成します。

export LOCATION=us-central1
export BUCKET_URI=gs://$PROJECT_ID-hybridsearch-bucket

gsutil mb -l $LOCATION -p $PROJECT_ID $BUCKET_URI

Python ライブラリのインストール

以下のコマンドを実行して、今回使用する Python ライブラリをインストールします。

pip install google-cloud-aiplatform pandas python-dotenv mecab-python3 scikit-learn

環境変数設定

今回は python-dotenv を使って環境変数を設定します。環境設定ファイルである .env を新規作成して以下を設定します。

.env
PROJECT_ID = "{project_id}" #{project_id} は 利用する環境の Google Cloud プロジェクト ID を指定してください
LOCATION = "us-central1"

2. エンベディングの生成

キーワード検索用のエンベディング生成

ハイブリッド検索におけるキーワード検索では、トークナイズ(文章を単語に分割する)とエンベディング(分割された単語をベクトル変換する)が必要になります。

今回、トークナイズは日本語のトークナイザーの一つである MeCab を使用します。

また、エンベディングで使用する一般的なアルゴリズムとして TF–IDFBM25SPLADE がありますが、今回は TF–IDF を使用します。

まずは、MeCab と TF-IDF の動作を確認してみましょう。

MeCab による日本語のトークナイズ

mecab_tokenizer.py
import MeCab

def mecab_tokenizer(text):
    """日本語テキストを MeCab を用いてトークン化します。

    Args:
        text: トークン化する日本語テキスト

    Returns:
        トークンのリスト
    """
    # MeCab の Tagger(テキストを形態素解析するためのオブジェクト)を初期化
    mecab = MeCab.Tagger()

    # テキストを形態素解析してノードのリストを取得
    node = mecab.parseToNode(text)

    tokens = []

    # トークンを抽出
    while node:
        if node.surface != "":
            tokens.append(node.surface)
        node = node.next
    return tokens

text = "機械学習を活用した検索技術"
output = mecab_tokenizer(text)
print(output)

このコードを実行すると、以下のように日本語の文章を単語に分割することができます。

$ python mecab_tokenizer.py
['機械', '学習', 'を', '活用', 'し', 'た', '検索', '技術']

TF-IDF によるエンベディング

TF-IDF として scikit-learn の TfidfVectorizer クラスを使用します。

text_to_tfidf.py
from sklearn.feature_extraction.text import TfidfVectorizer
from mecab_tokenizer import mecab_tokenizer

def text_to_tfidf(text):
    """
    テキストを TF-IDF ベクトルに変換します。

    Args:
        text: TF-IDF ベクトルに変換するテキスト

    Returns:
        疎なベクトル表現 (辞書型)
        {"values": TF-IDF値のリスト, "dimensions": 次元(単語ID)のリスト}
    """
    # TF-IDF ベクトル化器を初期化
    # tokenizer: トークン化関数として mecab_tokenizer(前述の MeCab) を指定
    # token_pattern: トークンパターンを無効化(tokenizer を使用するため)
    vectorizer = TfidfVectorizer(tokenizer=mecab_tokenizer, token_pattern=None)

    # テキストを TF-IDF ベクトルに変換
    # fit_transform は、テキストをベクトル化すると同時に、ベクトル化器に語彙情報を学習させる
    tfidf_vector = vectorizer.fit_transform([text])

    values = []
    dims = []

    # TF-IDF 値と次元を取り出す
    for i, tfidf_value in enumerate(tfidf_vector.data):
        values.append(float(tfidf_value))
        dims.append(int(tfidf_vector.indices[i]))
    return {"values": values, "dimensions": dims}

text = "機械学習を活用した検索技術"
output = text_to_tfidf(text)
print(output)

このコードを実行すると、以下のようにエンベディングできます。

$ python text_to_tfidf.py
{'values': [0.35355339059327373, 0.35355339059327373, 0.35355339059327373, 0.35355339059327373, 0.35355339059327373, 0.35355339059327373, 0.35355339059327373, 0.35355339059327373], 'dimensions': [6, 3, 2, 7, 0, 1, 5, 4]}

セマンティック検索用のエンベディング生成

セマンティック検索では文章をトークナイズせずにエンべディングベクトルに変換する必要がありますが、今回は Vertex AI の Text Embedding API を用います。

Vertex AI の Text Embedding API の動作も確認してみましょう。

Vertex AI の Text Embedding API によるエンベディング生成

text_to_dense_embedding.py
import os
from dotenv import load_dotenv

import vertexai
from vertexai.language_models import TextEmbeddingModel, TextEmbeddingInput

load_dotenv()

PROJECT_ID = os.environ.get("PROJECT_ID")
LOCATION = os.environ.get("LOCATION")

DENSE_EMBEDDING_MODEL = "textembedding-gecko-multilingual@001"

vertexai.init(project=PROJECT_ID, location=LOCATION)

def text_to_dense_embedding(text, task_type="RETRIEVAL_QUERY"):
    """
    テキストを Vertex AI の Text Embedding API を用いて高密度ベクトルに変換します。

    Args:
        text: 埋め込みベクトルに変換するテキスト
        task_type: 埋め込みタスクのタイプ (デフォルトは "RETRIEVAL_QUERY")

    Returns:
        高密度ベクトル表現 (リスト)
    """
    # 事前学習済みのエンべディングモデルを読み込み
    model = TextEmbeddingModel.from_pretrained(DENSE_EMBEDDING_MODEL)

    # テキストのエンべディングを生成
    input = TextEmbeddingInput(text=text, task_type=task_type)
    return model.get_embeddings([input])[0].values

text = "機械学習を活用した検索技術"
output = text_to_dense_embedding(text)
print(output)

このコードを実行すると、以下のようにエンベディングできます。

$ python text_to_dense_embedding.py
[0.011068560183048248, -0.05315188318490982, ・・・, 0.02082948014140129]

ハイブリッド検索用のエンベディング生成

キーワード検索とセマンティック検索の両方のエンべディング生成方法を確認したところで、それらのコードを以下の embedding.py のようにまとめ、sample_data.csv のデータをもとに、ハイブリッド検索に使用するエンべディングファイルを作成します。

embedding.py
embedding.py
import os
from dotenv import load_dotenv

import pandas as pd
import MeCab
from sklearn.feature_extraction.text import TfidfVectorizer

import vertexai
from vertexai.language_models import TextEmbeddingModel, TextEmbeddingInput

load_dotenv()

PROJECT_ID = os.environ.get("PROJECT_ID")
LOCATION = os.environ.get("LOCATION")

DENSE_EMBEDDING_MODEL = "textembedding-gecko-multilingual@001"
INPUT_FILE = "sample_data.csv"

vertexai.init(project=PROJECT_ID, location=LOCATION)


def mecab_tokenizer(text):
    """日本語テキストを MeCab を用いてトークン化します。

    Args:
        text: トークン化する日本語テキスト

    Returns:
        トークンのリスト
    """
    # MeCab の Tagger(テキストを形態素解析するためのオブジェクト)を初期化
    mecab = MeCab.Tagger()

    # テキストを形態素解析してノードのリストを取得
    node = mecab.parseToNode(text)

    tokens = []

    # トークンを抽出
    while node:
        if node.surface != "":
            tokens.append(node.surface)
        node = node.next
    return tokens


def text_to_tfidf(text):
    """
    テキストを TF-IDF ベクトルに変換します。

    Args:
        text: TF-IDF ベクトルに変換するテキスト

    Returns:
        疎なベクトル表現 (辞書型)
        {"values": TF-IDF値のリスト, "dimensions": 次元(単語ID)のリスト}
    """
    # TF-IDF ベクトル化器を初期化
    # tokenizer: トークン化関数として mecab_tokenizer(前述の MeCab) を指定
    # token_pattern: トークンパターンを無効化(tokenizer を使用するため)
    vectorizer = TfidfVectorizer(tokenizer=mecab_tokenizer, token_pattern=None)

    # テキストを TF-IDF ベクトルに変換
    # fit_transform は、テキストをベクトル化すると同時に、ベクトル化器に語彙情報を学習させる
    tfidf_vector = vectorizer.fit_transform([text])

    values = []
    dims = []

    # TF-IDF 値と次元を取り出す
    for i, tfidf_value in enumerate(tfidf_vector.data):
        values.append(float(tfidf_value))
        dims.append(int(tfidf_vector.indices[i]))
    return {"values": values, "dimensions": dims}


def text_to_dense_embedding(text, task_type="RETRIEVAL_QUERY"):
    """
    テキストを Vertex AI の Text Embedding API を用いて高密度ベクトルに変換します。

    Args:
        text: 埋め込みベクトルに変換するテキスト
        task_type: 埋め込みタスクのタイプ (デフォルトは "RETRIEVAL_QUERY")

    Returns:
        高密度ベクトル表現 (リスト)
    """
    # 事前学習済みのエンべディングモデルを読み込み
    model = TextEmbeddingModel.from_pretrained(DENSE_EMBEDDING_MODEL)

    # テキストのエンべディングを生成
    input = TextEmbeddingInput(text=text, task_type=task_type)
    return model.get_embeddings([input])[0].values


def main():
    df = pd.read_csv(INPUT_FILE, header=0)
    embedded_items = []
    for i, row in df.iterrows():
        embedded_items.append({
            "id": row.id,
            "content": row.content,
            "embedding": text_to_dense_embedding(row.content),
            "sparse_embedding": text_to_tfidf(row.content)
        })
    with open("embedded_items.json", "w") as f:
        f.writelines(f"{i}\n" for i in embedded_items)


if __name__ == "__main__":
    main()

コードを実行すると、エンべディングファイル embedded_items.json が作成されます。

python embedding.py

以下コマンドにて 、作成された embedded_items.json を Cloud Storage バケットへアップロードします。

gsutil cp embedded_items.json $BUCKET_URI

3. インデックスの作成

Vertex AI Vector Search のインデックスを作成します。

まずは、インデックス構成パラメータを設定するための以下メタデータファイルを作成します。

index_metadata.json
{
    "contentsDeltaUri": "gs://{project-id}-hybridsearch-bucket",
    "config": {
        "dimensions": 768,
        "approximateNeighborsCount": 20,
        "shardSize": "SHARD_SIZE_SMALL",
        "algorithm_config": {
            "treeAhConfig": {
                "leafNodeEmbeddingCount": 1000,
                "fractionLeafNodesToSearch": 0.05
            }
        }
    }
}

これらパラメータの設定値については以下の通りです。

  • contentsDeltaUri: インデックスの元データが格納されている Cloud Storage バケットの URI を指定します。
  • config: 以下のインデックス構成を設定します。
    • dimensions: Text Embedding API(text-embedding-gecko モデル)で生成されるベクトルの次元数「768」を指定します。
    • approximateNeighborsCount: 近似最近傍探索で使用する近傍数として、検索精度と速度のバランスを考慮した一般的な値「20」を指定します。
    • shardSize: データセットが比較的小規模であるため、インデックスを分割するシャードサイズとして「SHARD_SIZE_SMALL」を設定します。
    • algorithm_config: 以下の検索アルゴリズムを設定します。
      • treeAhConfig: 検索アルゴリズムとして「Tree-AH」を指定します。(Tree-AH アルゴリズムに関する詳細はこちらをご覧ください。)
        • leafNodeEmbeddingCount: 各リーフノードに対する埋め込みの数として、デフォルト値の「1000」を指定します。
        • fractionLeafNodesToSearch: クエリが検索されるリーフノードのデフォルトの割合として、デフォルト値の「0.05」を指定します。

パラメータ設定に関する詳細は、以下の公式ドキュメントをご参照ください。
https://cloud.google.com/vertex-ai/docs/vector-search/configuring-indexes?hl=ja

以下のコマンドを実行して、インデックスを作成します。

export LOCAL_PATH_TO_METADATA_FILE=index_metadata.json
export INDEX_NAME=hybridsearch-index

gcloud ai indexes create \
    --metadata-file=$LOCAL_PATH_TO_METADATA_FILE \
    --display-name=$INDEX_NAME \
    --region=$LOCATION \
    --project=$PROJECT_ID

gcloud ai indexes create のオプションは以下の通りです。

  • metadata-file: インデックスのメタデータファイルパスを指定します。
  • display-name: インデックスの表示名を指定します。
  • region: インデックスを作成するリージョンを指定します。
  • project: インデックスを作成するプロジェクト ID を指定します。

[Vertex AI Vector Search] > [ベクトル検索]のコンソールを開き、[インデックス]タブを選択すると作成されたインデックスを確認できます。インデックスの ID は後ほど使用するのでメモしておきましょう。(尚、以下の画像では ID をマスキングしています。)

vertex_ai_vector_search_created_index
作成されたインデックス

4. インデックスエンドポイントの作成

以下のコマンドを実行して、インデックスエンドポイントを作成します。

export INDEX_ENDPOINT_NAME=hybridsearch-index-endpoint

gcloud ai index-endpoints create \
    --display-name=$INDEX_ENDPOINT_NAME \
    --public-endpoint-enabled \
    --region=$LOCATION \
    --project=$PROJECT_ID

gcloud ai index-endpoints create のオプションは以下の通りです。

  • display-name: インデックスエンドポイントの表示名を指定します。
  • public-endpoint-enabled: パブリックなエンドポイントを作成する場合に指定します。(パブリック以外にも、VPC ネットワークPrivate Service Connect でのエンドポイント設定も可能です。)
  • region: インデックスエンドポイントを作成するリージョンを指定します。
  • project: インデックスエンドポイントを作成するプロジェクト ID を指定します。

[Vertex AI Vector Search] > [ベクトル検索]のコンソールを開き、[インデックスエンドポイント]タブを選択すると作成されたインデックスエンドポイントを確認できます。
インデックスエンドポイントの ID は後ほど使用するのでメモしておきましょう。(尚、以下の画像では ID をマスキングしています。)

vertex_ai_vector_search_created_index_endpoint
作成されたインデックスエンドポイント

5. インデックスのデプロイ

以下のコマンドを実行して、インデックスをエンドポイントにデプロイします。
(デプロイが完了するまで約 30 分程度かかります。)

export DEPLOYED_INDEX_NAME=deployed-hybridsearch-index
export DEPLOYED_INDEX_ID=deployed_hybridsearch_index
export INDEX_ID={先ほど作成したインデックス ID}
export INDEX_ENDPOINT_ID={先ほど作成したインデックスエンドポイント ID}
export MIN_REPLICA_COUNT=1
export MAX_REPLICA_COUNT=1

gcloud ai index-endpoints deploy-index $INDEX_ENDPOINT_ID \
    --deployed-index-id=$DEPLOYED_INDEX_ID \
    --display-name=$DEPLOYED_INDEX_NAME \
    --index=$INDEX_ID \
    --min-replica-count=$MIN_REPLICA_COUNT \
    --max-replica-count=$MAX_REPLICA_COUNT \
    --region=$LOCATION \
    --project=$PROJECT_ID

gcloud ai index-endpoints deploy-index のオプションは以下の通りです。

  • deploy-index: 作成済みのインデックスエンドポイント ID を指定します。
  • deployed-index-id: デプロイされたインデックスを一意に識別する任意の文字列を指定します。
  • display-name: デプロイされたインデックスの表示名を指定します。
  • index: 作成済みのインデックス ID を指定します。
  • min-replica-count: 最小レプリカ数を指定します。
  • max-replica-count: 最大レプリカ数を指定します。
  • region: インデックスをデプロイするリージョンを指定します。
  • project: インデックスをデプロイするプロジェクト ID を指定します。

6. 検索クエリの実行

以下のコードによりハイブリッド検索のクエリを実行します。
INDEX_ENDPOINT_NAME の値をインデックスエンドポイントの ID に置き換えてください。)

hybrid_search.py
import os
import pandas as pd
from dotenv import load_dotenv

import argparse
import google.cloud.aiplatform as aiplatform

from embedding import text_to_tfidf, text_to_dense_embedding

load_dotenv()

PROJECT_ID = os.environ.get("PROJECT_ID")
LOCATION = os.environ.get("LOCATION")

INDEX_ENDPOINT_NAME={インデックスエンドポイントの ID を指定}
DEPLOYED_HYBRID_INDEX_ID="deployed_hybridsearch_index"

INPUT_FILE = "sample_data.csv"

aiplatform.init(project=PROJECT_ID, location=LOCATION)


def main(query_text):
    """
    指定されたクエリテキストに基づいて、ハイブリッド検索を実行し、結果を表示する。

    Args:
        query_text (str): 検索クエリテキスト。
    """
    # 検索クエリのテキストをエンべディング
    query_dense_emb = text_to_dense_embedding(query_text)
    query_sparse_emb = text_to_tfidf(query_text)

    # ハイブリッド検索用クエリを作成
    hybrid_queries = aiplatform.matching_engine.matching_engine_index_endpoint.HybridQuery(
        dense_embedding=query_dense_emb,
        sparse_embedding_dimensions=query_sparse_emb["dimensions"],
        sparse_embedding_values=query_sparse_emb["values"],
        rrf_ranking_alpha=0.7  # RRF (Rank-biased Reciprocal Fusion) のパラメータ
    )

    # クエリ対象のエンドポイントを指定
    my_index_endpoint = aiplatform.MatchingEngineIndexEndpoint(
        index_endpoint_name=INDEX_ENDPOINT_NAME
    )

    # ハイブリッド検索を実行して結果を取得
    hybrid_resp = my_index_endpoint.find_neighbors(
            deployed_index_id=DEPLOYED_HYBRID_INDEX_ID,
            queries=[hybrid_queries],
            num_neighbors=10
    )

    # サンプルデータと検索結果をマージ
    df = pd.read_csv(INPUT_FILE, header=0)
    for idx, neighbor in enumerate(hybrid_resp[0]):
        id = int(neighbor.id) - 1
        content = df.content[id]
        dense_dist = neighbor.distance if neighbor.distance else 0.0
        sparse_dist = neighbor.sparse_distance if neighbor.sparse_distance else 0.0
        print(f"rank: {idx+1}, content: {content:<9}, dense_dist: {dense_dist:.3f}, sparse_dist: {sparse_dist:.3f}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('query_text', type=str, help='The query text to process')
    args = parser.parse_args()
    main(args.query_text)

このコードの主要な箇所を解説します。

検索クエリのテキストは先ほどと同様に text_to_tfidf(), text_to_dense_embedding() を使用してエンべディングします。

ハイブリッド検索では HybridQuery クラスを使用してクエリを作成します。rrf_ranking_alpha は RRF のパラメータであり、キーワード検索とセマンティック検索の結果のバランスを調整することができます。このパラメータ値は 0 から 1 の間で設定され、以下のように値が大きいほどセマンティック検索の結果が重視されます。

  • rrf_ranking_alpha = 0: キーワード検索の結果のみを使用します。
  • rrf_ranking_alpha = 1: セマンティック検索の結果のみを使用します。
  • 0 < rrf_ranking_alpha < 1: キーワード検索とセマンティック検索の結果に対してバランスをとって使用します。

また、MatchingEngineIndexEndpoint にて指定されたインデックスエンドポイントに接続し、find_neighbors でハイブリッド検索を実行します。

では実際に検索クエリをいくつか実行してみたいと思います!

検索クエリ 1:ID を明確に含むクエリ

まずは ID を明確に含む「商品番号:B123 のノートパソコンを探しています。」という検索クエリを実行してみます。(rrf_ranking_alpha = 0.5 で実行します。)

python hybrid_search.py "商品番号:B123 のノートパソコンを探しています。"
出力結果
rank: 1, content: 商品番号:B123 のノートパソコンは、薄型軽量で持ち運びにも便利です。, dense_dist: 0.923, sparse_dist: 0.858
rank: 2, content: このスタイリッシュなノートパソコン(商品番号:B123)は、ビジネスシーンに最適です。, dense_dist: 0.885, sparse_dist: 0.837
rank: 3, content: 商品番号:A123 のスマートフォンは、高性能カメラと大容量バッテリーが魅力です。, dense_dist: 0.814, sparse_dist: 0.816
rank: 4, content: ハイスペックなノートパソコン(商品番号:B123)で、あなたの創造性を解き放ちましょう。, dense_dist: 0.881, sparse_dist: 0.798
rank: 5, content: 商品番号:D789 のオーブントースターは、様々な調理モードを搭載しています。, dense_dist: 0.710, sparse_dist: 0.816
rank: 6, content: 革新的な技術を搭載した最新モデルのスマートフォン(商品番号:A123)です。, dense_dist: 0.781, sparse_dist: 0.798
rank: 7, content: 清潔で快適な空間を、加湿空気清浄機(商品番号:E012)で実現しましょう。, dense_dist: 0.699, sparse_dist: 0.802
rank: 8, content: 毎日の料理を楽しくする、多機能オーブントースター(商品番号:D789)です。, dense_dist: 0.000, sparse_dist: 0.837
rank: 9, content: 当店では、スマートフォン(商品番号:A123)の豊富なカラーバリエーションをご用意しております。, dense_dist: 0.799, sparse_dist: 0.000
rank: 10, content: 臨場感あふれる映像体験を提供する、4Kテレビ(商品番号:C456)です。, dense_dist: 0.000, sparse_dist: 0.816

こちらの出力結果では、ランキングが高いものから順に出力されています。この結果をみると「B123」を含むテキストが比較的上位(rank: 1, 2, 4)のランクに位置していることが分かるかと思います。

また、dense_dist(セマンティック検索)と sparse_dist (キーワード検索)を見てもそれぞれの値が比較的高く、これらの結果がマージされてランク付けされていることが分かります。

検索クエリ 2:ID なしの意味検索(セマンティック検索の検証)

次に ID を含まないキーワードでの検索として「高性能なノートパソコンが欲しい。」という検索クエリを実行してみます。(rrf_ranking_alpha = 0.5 で実行します。)

python hybrid_search.py "高性能なノートパソコンが欲しい。"
出力結果
rank: 1, content: 商品番号:B123 のノートパソコンは、薄型軽量で持ち運びにも便利です。, dense_dist: 0.807, sparse_dist: 0.649
rank: 2, content: このスタイリッシュなノートパソコン(商品番号:B123)は、ビジネスシーンに最適です。, dense_dist: 0.781, sparse_dist: 0.632
rank: 3, content: 商品番号:A123 のスマートフォンは、高性能カメラと大容量バッテリーが魅力です。, dense_dist: 0.719, sparse_dist: 0.617
rank: 4, content: ハイスペックなノートパソコン(商品番号:B123)で、あなたの創造性を解き放ちましょう。, dense_dist: 0.833, sparse_dist: 0.603
rank: 5, content: 清潔で快適な空間を、加湿空気清浄機(商品番号:E012)で実現しましょう。, dense_dist: 0.686, sparse_dist: 0.636
rank: 6, content: 時短料理にも最適な、オーブントースター(商品番号:D789)は、忙しいあなたをサポートします。, dense_dist: 0.678, sparse_dist: 0.612
rank: 7, content: 商品番号:C456 の4Kテレビは、高画質映像を迫力の大画面で楽しめます。, dense_dist: 0.000, sparse_dist: 0.636
rank: 8, content: 毎日の料理を楽しくする、多機能オーブントースター(商品番号:D789)です。, dense_dist: 0.000, sparse_dist: 0.632
rank: 9, content: 革新的な技術を搭載した最新モデルのスマートフォン(商品番号:A123)です。, dense_dist: 0.710, sparse_dist: 0.000
rank: 10, content: 当店では、スマートフォン(商品番号:A123)の豊富なカラーバリエーションをご用意しております。, dense_dist: 0.702, sparse_dist: 0.000

こちらの出力結果をみると「ノートパソコン」を含むテキストが比較的上位(rank: 1, 2, 4)のランクに位置していることが分かります。

ただし、検索クエリの「高性能なノートパソコン」に対して意味が近い「ハイスペックなノートパソコン」のようなテキストがもう少し上位に来て欲しいので、rrf_ranking_alpha = 0.8(セマンティック検索を重視)に変更して、再度実行してみたいと思います。

出力結果
rank: 1, content: 商品番号:B123 のノートパソコンは、薄型軽量で持ち運びにも便利です。, dense_dist: 0.807, sparse_dist: 0.649
rank: 2, content: ハイスペックなノートパソコン(商品番号:B123)で、あなたの創造性を解き放ちましょう。, dense_dist: 0.833, sparse_dist: 0.603
rank: 3, content: このスタイリッシュなノートパソコン(商品番号:B123)は、ビジネスシーンに最適です。, dense_dist: 0.781, sparse_dist: 0.632
rank: 4, content: 商品番号:A123 のスマートフォンは、高性能カメラと大容量バッテリーが魅力です。, dense_dist: 0.719, sparse_dist: 0.617
rank: 5, content: 清潔で快適な空間を、加湿空気清浄機(商品番号:E012)で実現しましょう。, dense_dist: 0.686, sparse_dist: 0.636
rank: 6, content: 時短料理にも最適な、オーブントースター(商品番号:D789)は、忙しいあなたをサポートします。, dense_dist: 0.678, sparse_dist: 0.612
rank: 7, content: 革新的な技術を搭載した最新モデルのスマートフォン(商品番号:A123)です。, dense_dist: 0.710, sparse_dist: 0.000
rank: 8, content: 当店では、スマートフォン(商品番号:A123)の豊富なカラーバリエーションをご用意しております。, dense_dist: 0.702, sparse_dist: 0.000
rank: 9, content: ご自宅のリビングを映画館に変える、4Kテレビ(商品番号:C456)はいかがですか。, dense_dist: 0.691, sparse_dist: 0.000
rank: 10, content: お部屋の空気を快適にする、加湿空気清浄機(商品番号:E012)です。, dense_dist: 0.682, sparse_dist: 0.000

結果として、「ハイスペックなノートパソコン」が rank: 2 に上昇しました。これにより、意味検索がより重視されたことが分かるかと思います。

検索クエリ 3:関連性の高い製品の検索

最後に、明確な製品名のキーワードが含まれていない検索クエリ「料理が楽しくなる家電を教えて。」を実行してみます。(rrf_ranking_alpha = 0.8 で実行します。)

python hybrid_search.py "料理が楽しくなる家電を教えて。"
出力結果
rank: 1, content: 毎日の料理を楽しくする、多機能オーブントースター(商品番号:D789)です。, dense_dist: 0.785, sparse_dist: 0.671
rank: 2, content: 時短料理にも最適な、オーブントースター(商品番号:D789)は、忙しいあなたをサポートします。, dense_dist: 0.753, sparse_dist: 0.642
rank: 3, content: 商品番号:D789 のオーブントースターは、様々な調理モードを搭載しています。, dense_dist: 0.744, sparse_dist: 0.655
rank: 4, content: 清潔で快適な空間を、加湿空気清浄機(商品番号:E012)で実現しましょう。, dense_dist: 0.705, sparse_dist: 0.667
rank: 5, content: 臨場感あふれる映像体験を提供する、4Kテレビ(商品番号:C456)です。, dense_dist: 0.694, sparse_dist: 0.655
rank: 6, content: 商品番号:C456 の4Kテレビは、高画質映像を迫力の大画面で楽しめます。, dense_dist: 0.657, sparse_dist: 0.667
rank: 7, content: ハイスペックなノートパソコン(商品番号:B123)で、あなたの創造性を解き放ちましょう。, dense_dist: 0.654, sparse_dist: 0.640
rank: 8, content: ご自宅のリビングを映画館に変える、4Kテレビ(商品番号:C456)はいかがですか。, dense_dist: 0.731, sparse_dist: 0.000
rank: 9, content: お部屋の空気を快適にする、加湿空気清浄機(商品番号:E012)です。, dense_dist: 0.706, sparse_dist: 0.000
rank: 10, content: 商品番号:E012 の加湿空気清浄機は、花粉やPM2.5を除去します。, dense_dist: 0.669, sparse_dist: 0.000

こちらの出力結果から、「料理」「家電」といった内容に関連する「オーブントースター」を含むテキストが上位(rank: 1 ~ 3)に位置していることが分かります。

他にも様々な検索パターンが考えられますので、ぜひ多様なクエリを試してみて、ハイブリッド検索の特性を深く理解しながら、最適なパラメータ調整の参考にしていただければと思います!

まとめ

今回は、Google Cloud の Vertex AI Vector Search で提供されているハイブリッド検索について解説しました。ハイブリッド検索は、キーワード検索とセマンティック検索を組み合わせることで、それぞれの強みを活かし弱点を補完して、より高精度な検索を実現することが期待できます。ベクトル検索などのセマンティック検索の精度向上を検討されている方は、ぜひ Vertex AI Vector Search のハイブリッド検索をお試しください!

参考

Discussion

ログインするとコメントできます