データベース

Firestoreのベクトル検索(KNN)実装手順|インデックス作成・findNearest・料金の数え方

Firestoreは2024年9月5日にK近傍(KNN)ベクトル検索を正式リリース(GA)しました。専用のベクトルデータベースを別途立てなくても、すでにアプリのデータが入っているコレクションに埋め込みベクトルを持たせ、同じクエリ経路で類似検索ができます。ただし埋め込みの次元数と課金の数え方には、実装前に知らないと作り直しになる制約があります。

この記事では、ベクトル値の保存、gcloud でのKNNインデックス作成、findNearest による近傍検索までを公式ドキュメント準拠のコードで示します。さらに、Vertex AIの埋め込みモデルとFirestoreの次元上限が噛み合わない問題と、その回避手順まで扱います。

まとめ

Firestoreのベクトル検索は、実装そのものより「使えるかどうかの前提確認」でつまずきます。着手前に押さえる点は次の4つです。

  • 埋め込みは2048次元まで。Vertex AIの gemini-embedding-001 は既定3072次元なので、そのままでは保存できません。output_dimensionality を1536などへ下げ、さらに手動で正規化します。
  • 課金は返却件数ではなくスキャン量。kNNクエリはインデックスエントリ100件ごとに1読み取りで、通常クエリ(1000件ごと)の10倍の単価です。limit を下げても減るのは返却ドキュメント分だけで、スキャン量に紐づく読み取りは残ります。効くのは事前フィルタです。
  • Standard editionではリアルタイムリスナーが使えないonSnapshot による購読は非対応なので、都度クエリを投げる作りになります。
  • Standard editionで呼べるのはサーバーSDKだけ。Python・Node.js・Go・Javaのみ対応で、Web/Android/iOSのクライアントSDKからは直接叩けません。2026年4月にGAとなったEnterprise editionのPipeline operationsなら、クライアントSDKからも書けます。

以降で、保存・インデックス作成・検索の順に実コードを追い、最後に採用を見送るべき条件を示します。

Standard editionのベクトル検索に残る4つの制約

ベクトル検索がプレビューとして告知されたのは2024年4月3日、GAになったのは同年9月5日です。リリースノートには「You can now use Firestore to perform K-nearest neighbor (KNN) vector searches. Additionally, use Firestore vector searches with inequality filters, retrieve the calculated vector distance, and specify a distance threshold. This feature is generally available (GA).」と記載されています。GAと同時に、不等号フィルタとの併用・距離値の取得・距離しきい値の指定も入りました。プレビュー時代の記事が言う「正式版までに仕様が変わるかもしれない」という留保は、もう当てはまりません。

一方、GA後も残っている制約が4つあります。以下はいずれもStandard edition(従来のクエリAPI)の話で、設計判断に直結します。

制約 内容 影響
次元数 最大2048 3072次元モデルは次元削減が必須
返却件数 最大1,000件(Standard edition限定の制限) 全件走査型の分析用途に不向き
リスナー リアルタイムスナップショット非対応 購読型UIは作れない
対応SDK Python / Node.js / Go / Java モバイル・Webから直接呼べない

とくに4番目は構成を左右します。Standard editionのベクトル検索はサーバーサイドSDKにしか実装されていないため、モバイルアプリから使うならCloud Run functionsやCloud Runのエンドポイントを経由させる必要があります。ベクトル検索そのものの位置づけを整理したい場合は、ベクトル検索とセマンティック検索の違いもあわせて確認してください。

Enterprise editionのPipeline operationsで外れる制約と残るトレードオフ

2026年4月20日のリリースノートに「Firestore Enterprise edition in Native mode and the Pipeline operations interface are now supported at the General Availability (GA) level.」と記載され、Pipeline operationsが正式版になりました。これは前節の制約表を部分的に無効化します。

Pipelinesの findNearest ステージには、Node.jsに加えてWeb・Swift・Kotlin・Java(Android)のサンプルが掲載されています。db.pipeline().collection("cities").findNearest(...) という書き方で、モバイルアプリやブラウザから直接近傍検索を書けるわけです。このステージの公式Limitationsに残っているのは「The maximum supported embedding dimension is 2048.」の1点のみで、1,000件上限の記載はありません。

ただし全部が解決するわけではありません。Pipelines概要の既知の制限には「Pipeline operations don’t yet support existing array-contains & vector index types」とあり、既存のベクトルインデックスを使わずに昇順・降順インデックスで代替するため、「find_nearest expressions will be slower than their existing equivalents」と性能面の注意が明記されています。リアルタイム・オフライン対応も同様に非対応のままです。速度が要件ならStandard editionのベクトルインデックス、クライアントから直接書きたいならPipelines、という切り分けになります。

ベクトル値の保存とKNNインデックス作成のコマンド

ベクトル型フィールドとして書き込む手順

Firestoreは埋め込みを生成しません。Vertex AIなどで作ったベクトルを、専用のベクトル型で書き込みます。配列のまま入れると検索対象になりません。

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

db = firestore.Client()
collection = db.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)

Node.jsでは FieldValue.vector() が同じ役割を担います。

import { Firestore, FieldValue } from "@google-cloud/firestore";

const db = new Firestore();
const coll = db.collection('coffee-beans');

await coll.add({
  name: "Kahawa coffee beans",
  description: "Information about the Kahawa coffee beans.",
  embedding_field: FieldValue.vector([1.0, 2.0, 3.0])
});

単一フィールドのベクトルインデックス作成コマンド

近傍検索にはインデックスが必須です。作らずにクエリを投げると、Firestoreがエラーとともに作成すべき gcloud コマンドを提示します。

gcloud firestore indexes composite create \
--collection-group=collection-group \
--query-scope=COLLECTION \
--field-config field-path=vector-field,vector-config='{"dimension":"1536", "flat": "{}"}' \
--database=database-id

dimension は2048以下の整数、インデックス種別は flat のみです。ここでは後述する gemini-embedding-001 の1536次元に合わせています(公式サンプルの値は1024)。指定した次元と実際に保存するベクトルの次元は一致していなければなりません。あとから埋め込みモデルを変えて次元が変わると、インデックスを作り直すことになります。

事前フィルタを併用する複合インデックスの指定

「赤いラベルの商品だけから近いものを探す」といった絞り込みを先に効かせるには、通常フィールドとベクトルフィールドを1つの複合インデックスにまとめます。

gcloud firestore indexes composite create \
--collection-group=collection-group \
--query-scope=COLLECTION \
--field-config=order=ASCENDING,field-path="color" \
--field-config field-path=vector-field,vector-config='{"dimension":"1536", "flat": "{}"}' \
--database=database-id

事前フィルタは andor の複合フィルタに対応します。後述する課金の観点でも、フィルタでスキャン範囲を狭められるかどうかがコストを分けます。

findNearestによる近傍検索と距離関数の選択基準

基本のKNNクエリと距離しきい値の指定

検索は find_nearest()(Node.jsは findNearest())で行います。次の例は最も近い10件のうち、EUCLIDEAN距離で4.5以内のものだけを返します。

from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

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=10,
    distance_threshold=4.5,
)

for doc in vector_query.stream():
    print(doc.id)

しきい値の向きは距離関数で逆転します。EUCLIDEANとCOSINEは「しきい値以下」を返し、DOT_PRODUCTは「しきい値以上」を返します。ドット積は似ているほど値が大きくなるためです。この向きを取り違えると、結果が0件になるか、逆に絞り込みが効かず limit 件がそのまま返るかのどちらかになります。

距離値そのものを受け取るdistance_result_field

類似度スコアをUIに出したり、RAGの再ランキングに渡したりする場合は、計算済みの距離を出力フィールド名付きで受け取ります。

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

docs = vector_query.stream()
for doc in docs:
    print(f"{doc.id}, Distance: {doc.get('vector_distance')}")

ここに落とし穴があります。select() でフィールドマスクを使って返却フィールドを絞る場合、distance_result_field に指定した名前もマスクに含めないと距離が返ってきません。公式ドキュメントも collection.select(["color", "vector_distance"]) のように距離フィールド名を明示した例を載せています。検索結果の順位づけをスコアに依存させる設計なら、ここは必ず確認しましょう。

3つの距離関数の使い分け

選択肢はEUCLIDEAN・COSINE・DOT_PRODUCTの3つですが、判断基準は単純です。ベクトルが単位正規化されているかどうかで決まります。

正規化済みなら3つとも同じ並び順を返します。この場合、公式ドキュメントは「We recommend using DOT_PRODUCT with unit normalized vectors instead of COSINE distance, which is mathematically equivalent with better performance.」としてDOT_PRODUCTを推奨しています。ただし同じページは「the difference is negligible in most cases」とも書いており、性能に敏感な用途でのチューニング材料という位置づけです。Vertex AIのテキスト埋め込みは既定(3072次元)なら正規化されて返るため、素直に使うならDOT_PRODUCTが第一候補になります。

正規化されていないデータにDOT_PRODUCTを使うのは誤りです。ドット積はベクトルの大きさに引きずられ、そもそも距離を測っていないからです。この場合はCOSINEかEUCLIDEANを試し、用途に合うほうを選びます。正規化されているか判断できないときはCOSINEが安全です。COSINEは正規化を内蔵したDOT_PRODUCTに相当し、距離は0から2の範囲に収まります。

gemini-embedding-001の3072次元がFirestoreに保存できない理由と対処

Vertex AIのテキスト埋め込みモデル gemini-embedding-001 は、既定で3072次元のベクトルを返します。Firestoreの上限は2048次元です。つまり公式が案内するモデルの既定出力を、公式が案内する保存先にそのまま入れられません。この組み合わせは両方の公式ドキュメントに書かれていますが、同じページには載っていないため、実装して初めて気づきがちです。

対処は2段階あります。まず output_dimensionality で次元を下げます。指定できる範囲は128〜3072、推奨値は768・1536・3072なので、推奨値のうち2048以下は1536です。任意値も取れるため、Firestore上限ちょうどの2048を指定しても構いません(公式のMTEBスコアは2048で68.16、1536で68.17とほぼ同等)。

問題は2段階目です。gemini-embedding-001 はMatryoshka Representation Learning(MRL)で学習されており、先頭から切り詰めた短いベクトルも意味を保ちます。ただし公式ドキュメントは「If you are using gemini-embedding-001, you must manually normalize non-3072 dimensions.」と明記しています。切り詰めた時点で単位長ではなくなるため、自分で正規化し直さないといけません

これは机上の注意ではありません。正規化済みの3072次元ベクトルを1536次元へ切り詰めてノルムを測ると、1.0ではなくなります(等方的なベクトルを仮定した期待値は√(1536÷3072)≒0.707。実際のモデル出力はMRLで先頭次元に情報が偏るぶんズレます)。この状態で前節のDOT_PRODUCTを選ぶと、「単位正規化済みベクトルに対して」という前提が崩れ、ベクトルの長さの違いが類似度に混ざります。順位が静かに歪むだけで、エラーは出ません。

import numpy as np
from google import genai
from google.genai.types import EmbedContentConfig
from google.cloud.firestore_v1.vector import Vector

client = genai.Client(vertexai=True, project="YOUR_PROJECT", location="us-central1")
resp = client.models.embed_content(
    model="gemini-embedding-001",
    contents=["Kahawa coffee beans"],
    config=EmbedContentConfig(
        task_type="RETRIEVAL_DOCUMENT",
        output_dimensionality=1536,
    ),
)

v = np.array(resp.embeddings[0].values)
v = v / np.linalg.norm(v)

collection.add({"name": "Kahawa coffee beans", "embedding_field": Vector(v.tolist())})

クライアント生成時の vertexai=True と project/location の指定を省くと、既定ではGemini Developer API側(APIキー認証)に向きます。Vertex AI経由で使うなら明示するか、GOOGLE_GENAI_USE_VERTEXAI 系の環境変数を設定してください。task_type も指定します。保存する文書側は RETRIEVAL_DOCUMENT、検索クエリ側は RETRIEVAL_QUERY という使い分けです。

入力側の上限にも注意が必要です。埋め込みAPIの一般的な制限は1リクエスト250件・合計20,000トークン、1テキストあたり2,048トークンですが、gemini-embedding-001 については「each request can only include a single input text」と明記されています。大量投入はバッチ側で分割しましょう。トークン超過分は既定で黙って切り捨てられるので、検知したいなら auto_truncate(RESTのJSONキーは autoTruncate)を False にしてエラーを出させます。

なお後継の gemini-embedding-2(2026年4月更新)は、3072次元未満を要求した場合も自動で再正規化するため、新規実装でこちらを選ぶなら本節の手動正規化は不要です。ただし task_type パラメータは非対応となり、タスク指示はプロンプトに直接書く方式へ変わりました。両モデルの埋め込み空間には互換性がなく、乗り換えるなら既存データの全再埋め込みが必要です。モデル選定の段階なら、Gemini Embedding 2の料金・仕様とOpenAI比較や、Pythonでembedding(ベクトル化)を生成する方法が判断材料になります。

ベクトル検索の課金がlimitではなくインデックススキャン量で決まる仕組み

Firestoreのベクトル検索でコスト見積もりを外す原因は、ほぼ1点に集約されます。課金対象は返したドキュメント数ではなく、スキャンしたkNNインデックスエントリ数です。(以下はStandard editionの料金体系です。料金ページ自体が「pricing details for Firestore Standard edition」と断っています。)

規定はこうです。通常のクエリはインデックスエントリ1,000件ごとに1読み取りですが、kNNベクトル検索だけは100件ごとに1読み取り。同じスキャン量でも単価が10倍になります。

公式の計算例が分かりやすいので、そのまま追います。limit: 5 のベクトル検索が5件を返し、その過程で1,550件のkNNインデックスエントリを読んだ場合、請求は「返却ドキュメント5読み取り」+「インデックスエントリ 1,550÷100を切り上げて16読み取り」の合計21読み取りです。返却5件に対して21読み取り、つまり4倍以上が課金されています。

項目 通常クエリ kNNベクトル検索
インデックスエントリの課金単位 1,000件ごとに1読み取り 100件ごとに1読み取り
1,550エントリ読んだ場合 2読み取り 16読み取り
返却ドキュメント 1件=1読み取り 1件=1読み取り

なお通常クエリは、範囲フィールドが1つ以下ならインデックスエントリの読み取り自体が課金対象外です。比較表の「2読み取り」は、範囲フィールドを2つ以上使うケースの話だと考えてください。

ここから導かれる実務上の結論は、limit を絞ってもコストはほとんど下がらない、ということです。効くのは事前フィルタです。where() でコレクションの母集団を先に狭めれば、走査するインデックスエントリ自体が減ります。前述の複合インデックスを作る手間は、この課金構造への対策でもあります。

無料枠は1日あたり読み取り50,000・書き込み20,000・保存データ1GiBです。先の例のクエリなら1日約2,380回で無料枠を使い切る計算になります。実際にどれだけインデックスエントリを読んでいるかはQuery Explainで確認できるので、本番投入前に一度測りましょう。ベクトル検索以外の通常クエリまで含めた読み取りコストの削減策は、Firestore Data Bundlesの仕組みも参考になります。

Cloud Run functionsで埋め込みを自動生成する構成

ドキュメントが作成・更新されるたびに埋め込みを作り直す処理は、Firestoreトリガーで動くCloud Run functionに載せるのが公式の想定です。Standard editionのベクトル検索がサーバーサイドSDKにしか実装されていない以上、どのみちサーバー側の実行環境は要ります。

@functions_framework.cloud_event
def store_embedding(cloud_event) -> None:
    """Triggers by a change to a Firestore document."""
    firestore_payload = firestore.DocumentEventData()
    firestore_payload._pb.ParseFromString(cloud_event.data)
    collection_id, doc_id = from_payload(firestore_payload)

    embedding = calculate_embedding(firestore_payload)

    doc = firestore_client.collection(collection_id).document(doc_id)
    doc.set({"embedding_field": embedding}, merge=True)

公式サンプルは payload = firestore_payload._pb.ParseFromString(...) と戻り値を変数に代入していますが、この関数が返すのは読み取ったバイト数(int)です。パース結果はメッセージ側に入るため、上のように firestore_payload を後続へ渡します。サンプルをそのまま写すと、後続の関数にintが渡って壊れます。

この形をそのまま使うと、埋め込みと無関係なフィールドの更新でも再計算が走ります。Node.js版の公式サンプルは、その対策として更新前後の値を比較し、対象フィールドが変わっていなければ即 return する実装になっています。

const previousContent = event.data.before.get("content");
const currentContent = event.data.after.get("content");

// content が変わっていなければ埋め込みを作り直さない
if (previousContent === currentContent) {
  return;
}

const embeddingVector = calculateEmbedding(currentContent);
await event.data.after.ref.update({ embedding: embeddingVector });

この差分チェックを省くと、埋め込みAPIの呼び出し料金とFirestoreの書き込み料金が二重に無駄になります。さらに、書き戻し先のドキュメントが同じトリガー条件に含まれる場合は doc.set() が再びトリガーを引き、更新が連鎖します。公式サンプルがわざわざ「Don’t update the embedding if the content field did not change」とコメントを置いているのは、この判定が省略できないからです。なお、この自動生成パターンの公式サンプルはPythonとNode.jsのみで、GoとJavaのクライアントライブラリでは「Not yet supported」と明示されています。

Firestoreのベクトル検索を採用すべきでない3つの条件

すでにFirestoreを使っているなら、別のベクトルデータベースを増やすより先にこちらを試す価値があります。データの二重管理が要らず、事前フィルタも同じクエリで書けるからです。ただし次の3条件のどれかに当たるなら、素直に別の基盤を選んだほうがよいと考えます。

検索結果をリアルタイム購読したい場合は不可能です。Standard editionのベクトル検索はスナップショットリスナーに非対応で、Enterprise editionのPipeline operationsもリアルタイム・オフライン機能を持ちません。「類似アイテム一覧が自動で更新される画面」は、どちらでも作れないということです。ポーリングで代替すると、インデックススキャンぶんの課金が回数だけ積み上がります。

1回のクエリで1,000件を超える結果が要る場合も外れます。ただしこの上限は公式に「Standard edition limitation only」と括弧書きされており、Enterprise editionへの移行は選択肢に残ります。Standard editionのまま大量候補を取って自前で再ランキングする設計は成立しません。

3072次元の精度をそのまま使いたい場合も向きません。2048次元の壁はEnterprise editionのPipelinesでも同じで、設定では回避できず次元削減が前提になります。埋め込みの表現力を落とせない要件なら、Vertex AI Vector Searchのような専用サービスを検討してください。製品ごとの特性はベクトルデータベースの仕組みと代表的な製品で比較しています。

よくある質問

Firestoreのベクトル検索はまだプレビュー版ですか?

いいえ、2024年9月5日にGA(一般提供)になっています。プレビューとして告知されたのは2024年4月3日で、GA時点で不等号フィルタとの併用、計算済み距離の取得、距離しきい値の指定も同時に利用可能になりました。さらに2026年4月20日には、Enterprise editionのNative modeとPipeline operationsもGAになっています。2024年前半に書かれた解説記事はプレビュー前提のままのことがあるため、仕様の可否はリリースノートで確認してください。

create vector index はどのコマンドで実行しますか?

gcloud firestore indexes composite create--field-config field-path=フィールド名,vector-config='{"dimension":"次元数", "flat": "{}"}' を渡します。次元数は2048以下の整数、インデックス種別は flat のみです。Google Cloudコンソールの「インデックス」→「手動」タブから作ることもでき、Firebase CLIとTerraformにも対応しています。インデックスなしでクエリを投げると、Firestoreが必要なコマンドをエラーメッセージに出してくれます。

Firebaseのクライアントアプリから直接ベクトル検索を呼べますか?

Standard editionの従来クエリAPIでは呼べません。対応しているのはPython・Node.js・Go・Javaのクライアントライブラリだけで、Web・Android・iOS向けのFirebase SDKには実装されていないため、Cloud Run functionsなどを経由させます。一方、2026年4月にGAとなったEnterprise editionのPipeline operationsには、Web・Swift・Kotlin・Java(Android)の findNearest サンプルが公式に掲載されており、クライアントから直接書けます。ただしPipelinesは既存のベクトルインデックスを使わないぶん、検索が遅くなる想定と公式に明記されています。

KNN検索で距離関数はどれを選べばよいですか?

ベクトルが単位正規化されているならDOT_PRODUCTです。公式ドキュメントもCOSINEではなくDOT_PRODUCTを推奨しており、両者は数学的に等価でDOT_PRODUCTのほうが高速です(ただし公式は差がほとんどの場合無視できるとも書いています)。Vertex AIのテキスト埋め込みは既定の3072次元なら正規化されて返ります。output_dimensionality で3072未満に切り詰めた場合、gemini-embedding-001 が返すベクトルは単位長ではありません。切り詰めたなら手動で正規化してからDOT_PRODUCTを使ってください。正規化されていない、あるいは判断がつかないデータではCOSINEを選びます。

ベクトル検索の料金はどのくらいかかりますか?

読み取り課金は「返却したドキュメント数」+「スキャンしたkNNインデックスエントリ数を100で割って切り上げた数」です。公式例では limit: 5 で5件返し1,550エントリを読んだクエリが合計21読み取りとして課金されます。通常クエリは1,000件ごとに1読み取りなので、ベクトル検索は同じスキャン量でも10倍の単価です。無料枠は1日50,000読み取りまで。単価は保存リージョンで変わるうえ、これはStandard editionの体系なので、金額は公式の料金ページで確認してください。

関連記事

資料請求

RELATED POSTS 関連記事