Firestoreのベクトル検索機能を使ってみたい

2025/01/18に公開

はじめに(簡単に自己紹介)

勤務先がGoogle Cloudのパートナーとなり、それに伴い、私自身もGoogle Cloudを勉強している最中です。いくつか資格も取得しましたが、業務へ活用していくためには、実際に製品やサービスを使ってみることが大切だなと感じています。

そこで、座学から業務活用へステップアップするためにGoogle Cloudに関連するトピックについて実際に手を動かして学んでみたいというコンセプトで記事を投稿していきたいと思います。
(アプトプットの機会も少ないため、Zennを活用してみたいと思います。)

やりたいこと

今回はFirestoreのベクトル検索機能を使ってみたいと思います。
Firestoreはサーバレスなドキュメントデータベースです。一般的なアプリケーションのデータ管理以外に、生成AIアプリケーション向けにベクトル検索機能が用意されています。
https://cloud.google.com/firestore?hl=ja

Google Cloudで提供されているベクトル検索やRAG機能は複数あります。簡単にRAGを構築したいのであれば、CloudStorageに格納したファイルの中身を検索できるフルマネージドなVertexAIのRAG機能などがありますが、細かな設定ができない点がボトルネックになることもあります。
今回のFirestoreを使う方法は、もう少しカスタマイズをしたい場合に利用できる選択肢の1つという位置付けだと思います。
https://cloud.google.com/use-cases/retrieval-augmented-generation?hl=ja

ポイントになる部分を簡単にご紹介

ベクトル検索とは

「ベクトル検索」で調べると、多くの記事が出てきますが、文字だけで説明されても難しくてわかりませんよね…
そこで、ここでは正確な説明を省いて、図を使ってざっくりと理解してみたいと思います。

ベクトル検索は似たもの検索です。ベクトルを使って類似度(似ている度合い)を計算します。ベクトルと言われると拒否反応が出る方もおられるかもしれませんが、意識せずとも私たちの頭の中では、以下のようにベクトル検索に近いことを行っています。

例:珈琲の類似度検索
(私は珈琲について詳しくないのですが)珈琲豆の種類を比較するときには苦味や酸味といった珈琲に共通する特徴を使ってマッピングすることで比較をすると思います。
図にすると下のようなイメージでしょうか。例えば「珈琲Aは苦味が強く酸味が弱い、一方で珈琲Bは苦味が弱く酸味が少し強い」というように特徴を整理できます。

さらに、お互いの近さを測ることで類似度も比較できることに気づいたでしょうか。例えば「珈琲Cは苦味が少ないが酸味があるため、珈琲Aよりも珈琲Bに似ている」といった感じです。
この近い・遠いの概念は人間であれば頭の中で無意識に計算できてしまいますが、コンピュータ上ではどのようにこの概念を再現すれば良いでしょうか。これをベクトルを使って再現します。

再現の流れは以下のようになります。

  1. 珈琲豆を苦味・酸味という特徴でマッピングしたように文章をマッピングします。このとき文章はベクトルに変換されます。(これをエンべディングと呼びます。)言語は複雑なためエンべディングに使う特徴数(ベクトルの次元数)も大きくなります。
  2. 1で生成されたベクトルをデータベースに保存します。(今回はFirestoreを使ってベクトルを保存します。)
  3. 検索したい文章に対してもエンべディングを行い、2でデータベースに保存したベクトルとの近さを計算することで、類似する意味の文章を見つけ出します。(今回は検索もFirestoreの機能を使います。)

実践編

では、上記の流れをpythonを使って作成していきましょう。
作成にあたり以下のサイトを参考にしました。
https://cloud.google.com/firestore/docs/solutions/generative-ai-index?hl=ja
https://github.com/GoogleCloudPlatform/python-docs-samples/blob/bde17f7657aeb730f99ffecd64d52a03c16c77f1/firestore/cloud-client/vector_search.py#L19-L30

1. ベクトルエンべディングを生成する

文章をベクトルに変換します。

2. Firestoreにエンべディングを保存する

1で生成したベクトルをFirestoreに保存します。
以下のコードでは、サンプルコードのベクトルを使用したため3次元ですが、VertexAI Text embedding APIを利用すると768次元のベクトルが生成されます。

store_vectors.py
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector

firestore_client = firestore.Client()
    collection = firestore_client.collection("coffee-beans")
    doc = {
        "name": "Kahawa coffee beans",
        "description": "Information about the Kahawa coffee beans.",
        "embedding_field": Vector([0.18332680, 0.24160706, 0.3416704]),
    }
    collection.add(doc)

"embedding_field": Vectorに生成したベクトルを挿入します。
Firestore上に保存されたことを確認してみましょう。

1つのデータだけでは検索できないので、追加してみましょう。
データはGemini先生に作ってもらいました。

上記コードのdoc={}部分を書き換えます。

doc = {
"name": "Moka Java blend",
"description": "A classic blend of Moka and Java beans, known for its balanced flavor profile.",
"embedding_field": Vector([0.452, 0.123, 0.789]),
}
doc = {
"name": "Blue Mountain coffee",
"description": "High-quality coffee beans from the Blue Mountains of Jamaica, known for its smooth and mild taste.",
"embedding_field": Vector([0.05, 0.67, 0.23]),
}

これで3つのデータをFirestoreに保存しました。

3. ベクターインデックスを生成する

ベクトル検索を行うためにはベクターインデックスを作成する必要があるようです。gcloudコマンドを使って作成します。

gcloud firestore indexes composite create --project={YOUR PROJECT ID} --collection-group=coffee-beans --query-scope=COLLECTION --field-config=vector-config='{"dimension":"3","flat": "{}"}',field-path=embedding_field

--project=dimension等はご自身の環境に合わせて変更をお願いします。

4. ベクトル検索を実行する

ここまででベクトル検索の準備は整いました。
以下のコードでベクトル検索をしてみましょう。

vector_search.py
from google.cloud import firestore
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

db = firestore.Client()
collection = db.collection("coffee-beans")

vector_query = collection.find_nearest(
    vector_field="embedding_field",
    query_vector=Vector([0.3416704, 0.18332680, 0.24160706]),
    distance_measure=DistanceMeasure.EUCLIDEAN,
    limit=5,
    distance_result_field="vector_distance",
)

docs = vector_query.stream()

for doc in docs:
    print(f"{doc.id} => {doc.to_dict()}")

query_vector=に検索したいベクトルを挿入します。
検索結果を見てみましょう。

axyGKsSH5ODqRzBuFsFC => {'vector_distance': 0.19616818391009075, 'description': 'Information about the Kahawa coffee beans.', 'embedding_field': Vector<0.1833268, 0.24160706, 0.3416704>, 'name': 'Kahawa coffee beans'}
TBrKWSIx7caP4M4A2Lp0 => {'vector_distance': 0.5616502240685423, 'description': 'A classic blend of Moka and Java beans, known for its balanced flavor profile.', 'embedding_field': Vector<0.452, 0.123, 0.789>, 'name': 'Moka Java blend'}
RYkeQmfGwnNIY16wWiCx => {'vector_distance': 0.5675007926657404, 'description': 'High-quality coffee beans from the Blue Mountains of Jamaica, known for its smooth and mild taste.', 'embedding_field': Vector<0.05, 0.67, 0.23>, 'name': 'Blue Mountain coffee'}

類似している(ベクトル間の距離が近い)順に
①Kahawa coffee beans
②Moka Java blend
③Blue Mountain coffee
となりました。
ご興味があれば、検索するときのベクトルを変えてみて、順位が変化するかも試してみてください。

まとめ

今回はFiresotreのベクトル検索機能を試してみました。
基本的にはVertexAIのRAG機能を利用し、要件や要望が満たせない場合に今回のようなベクトル検索機能を利用していく形になると思います。

Discussion