こうした場面では通常のテキスト検索では力不足です。実際には次のような状況が起こります。
- ヘルプ記事に使われている表現と異なり、ユーザーが「電源が入らない」と入力したため、サポートポータルが結果を返せない。
- 商品説明にまったく同じ語句が含まれていないため、購入者が「冬に暖かいもの」と検索しても EC サイトで何も見つからない。
- 文書の書き方とユーザーの質問の仕方が異なるため、ナレッジベースが該当文書を取りこぼす。
セマンティック検索はこれを解決します。完全一致ではなく、クエリの意味や意図を理解して検索します。
このチュートリアルでは、Python と無料のnomic-embed-text-v1 埋め込みモデルを使って、MongoDB にセマンティック検索を実装する方法を説明します。
注: nomic-embed-text-v1 はメモリに読み込むと約 0.27 GB です。このチュートリアルのスクリプトを実行する前に、少なくとも 1 GB の空き RAM があることを確認してください。初回実行時は Hugging Face からモデルをダウンロードするため、回線速度に応じて余分な時間がかかります。2 回目以降はローカルキャッシュから読み込まれます。CPU 推論は GPU 推論より大幅に遅くなります。専用 GPU のないマシンでは、モデルは CPU 上で動作し VRAM ではなく RAM を使用するため、埋め込みの生成に時間がかかる場合があります。
このチュートリアルで学べることは次のとおりです。
- ベクトル埋め込みとは何か、そしてどのように意味を表現するか
- MongoDB で埋め込みを生成・保存する方法
- MongoDB の Vector Search インデックスを作成する方法
- ユーザーのクエリをベクトルに変換する方法
$vectorSearchの集計パイプラインを実行し、結果を解釈する方法
このチュートリアルで使用するコードサンプルはすべてGitHub リポジトリにあります。
テキスト検索とセマンティック検索の違い
テキスト検索は、クエリに含まれる語句と完全に一致する語を含む文書を一致させます。セマンティック検索は、語がまったく異なっていても、クエリと同じ意味を持つ文書を見つけます。
次の表は、「heart problems」という検索クエリに対して、それぞれの方式が返す結果の違いを示しています。
| 文書テキスト | テキスト検索 | セマンティック検索 | テキスト検索で返る理由 |
|---|---|---|---|
| "...heart problems in adults..." | Yes | Yes | "heart problems" という語句が完全一致で含まれている |
| "...cardiac conditions and symptoms..." | No | Yes | "heart problems" という語句が完全一致で含まれていない |
| "...risk factors for heart disease..." | No | Yes | "heart problems" という語句が完全一致で含まれていない |
| "...chest pain and shortness of breath..." | No | Yes | "heart problems" という語句が完全一致で含まれていない |
セマンティック検索の基礎概念
MongoDB におけるセマンティック検索では、3 つの概念が中核になります。これらを理解すると、このチュートリアルの各ステップがなぜそのように動作するのかが分かります。
ベクトル埋め込み
埋め込みモデルは、テキストをベクトルと呼ばれる固定長の数列に変換します。ベクトル内の各数値は、意味の次元を表します。
意味が類似する 2 つのテキストは、数値的に近いベクトルを生成します。語を共有していなくても、"cardiac conditions" と "heart problems" はベクトル空間で近くに位置します。この近さがセマンティック検索を可能にします。
このチュートリアルでは、無料・オープンソースでローカルマシン上だけで動作する nomic-embed-text-v1 埋め込みモデルを使用します。初回実行時に Hugging Face から自動的にダウンロードされ、以降はローカルに保存されたものを使用します。モデルは入力 1 件あたり 768 次元の数値を出力します。
ベクトル検索インデックス
ベクトル検索インデックスは、どのフィールドに埋め込みがあり、何次元を想定し、比較にどの類似度関数を使うかを MongoDB に指示します。$vectorSearch クエリを実行する前に、このインデックスを作成する必要があります。
$vectorSearch 集計ステージ
$vectorSearch は検索を実行する集計ステージです。クエリベクトルを受け取り、インデックス化されたフィールドを検索し、セマンティックな類似度で順位付けされた文書を返します。ほかの集計パイプライン同様、$project などのステージと連結して使用します。
それでは始めましょう。
前提条件
開始する前に、次を用意してください。
- Python 3.8 以降がインストールされていること
- MongoDB Atlas アカウントと M0(Free ティア)クラスタが作成済みであること
pymongo4.7 以降、sentence-transformers、einopsの各 Python パッケージがインストールされていること- Python と MongoDB コレクションの基本的な知識
- Atlas の接続文字列(Atlas UI の **Database > Connect > Drivers** に表示)
- スクリプトを実行する前に IP アドレスが Atlas で許可されていること。Atlas UI の **Security > Network Access** で現在の IP アドレスを追加してください。
プロジェクトのセットアップ
プロジェクト用のフォルダを作成し、ターミナルでそのフォルダに移動します。
mkdir mongodb-semantic-search
cd mongodb-semantic-search
プロジェクトの依存関係を分離するため、仮想環境を作成して有効化します。
python -m venv venv
source venv/bin/activate
Windows の場合、次で仮想環境を有効化します。
venv\Scripts\activate
次に、必要なパッケージをインストールします。
pip install pymongo sentence-transformers einops
このチュートリアルでは各ステップごとに 1 つの Python ファイルを作成します。すべてのファイルは mongodb-semantic-search フォルダに配置します。
埋め込みユーティリティファイルを作成する
embedding_utils.py という名前のファイルを作成します。このファイルには、このチュートリアルで使用する 2 つの埋め込み関数を定義します。
from sentence_transformers import SentenceTransformer
# Load the free, open-source embedding model.
# The model downloads from Hugging Face on first run and saves locally.
# trust_remote_code=True is required by this model:
model = SentenceTransformer("nomic-ai/nomic-embed-text-v1", trust_remote_code=True)
def get_embedding(text, precision="float32"):
# Use this function when embedding text you plan to store in MongoDB:
return model.encode(text, precision=precision).tolist()
def get_query_embedding(text, precision="float32"):
# Use this function when embedding a user's search query:
return model.encode(text, precision=precision).tolist()
警告: trust_remote_code=True は、Hugging Face からダウンロードされたモデル固有の Python コードをマシン上で実行できるようにします。Hugging Face 上の当該モデルのソースコードが侵害されたり、悪意ある変更で更新された場合、そのコードが環境内で自動的に実行されます。本番環境でこのパラメータを使用する場合は十分に注意してください。
ベクトル埋め込みを生成・保存する
generate_embeddings.py という名前のファイルを作成します。このファイルは embedding_utils.py から埋め込み関数をインポートし、各文書の text フィールドに対するベクトルを生成して、埋め込みを含む完全な文書を MongoDB に保存します。
from embedding_utils import get_embedding
from pymongo import MongoClient
# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
"mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
appname="devrel-tutorial-python-semantic-search"
)
collection = mongodb_client["sample_db"]["documents"]
# Sample data:
sample_documents = [
{
"title": "MongoDB Atlas",
"text": "MongoDB Atlas is a fully managed cloud database."
},
{
"title": "Vector Search",
"text": "Vector search finds results based on semantic meaning."
},
{
"title": "Nomic AI",
"text": "nomic-embed-text-v1 is a free, open-source embedding model."
},
]
docs_to_insert = []
for doc in sample_documents:
embedding = get_embedding(doc["text"])
docs_to_insert.append({
"title": doc["title"],
"text": doc["text"],
# The vector lives alongside your original data in the same document:
"embedding": embedding
})
# Drop the collection before each run to avoid inserting duplicate documents:
collection.drop()
result = collection.insert_many(docs_to_insert)
print(f"Inserted {len(result.inserted_ids)} documents with embeddings.")
mongodb_client.close()
上記のコードでは、collection.drop() が実行のたびにコレクション内のすべての文書とインデックスを削除します。generate_embeddings.py をこの行なしで再実行すると、重複した文書が挿入され、$vectorSearch が同じ文書を複数回返す原因になります。コレクションを削除するとベクトル検索インデックスも削除されるため、generate_embeddings.py を再実行した際は create_vector_index.py も再度実行する必要があります。
スクリプトを実行します。
python generate_embeddings.py
ターミナルには次のように表示されます。
<All keys matched successfully>
Inserted 3 documents with embeddings.
MongoDB の各文書には、元のテキストと、その 768 次元のベクトルが embedding フィールドに保存されました。ベクトルはデータと同じ文書に共存するため、クエリ時に別ストレージやルックアップは不要です。
MongoDB Vector Search インデックスを作成する
create_vector_index.py という名前のファイルを作成します。このファイルは embedding フィールドに対するベクトル検索インデックスを定義・作成し、コレクションに対して $vectorSearch クエリを実行できるようにします。
インデックス定義には 3 つのフィールドが必要です。
path: 埋め込みを保持するフィールド(このチュートリアルではembedding)numDimensions: モデルの出力次元数と一致させる(nomic-embed-text-v1は768)similarity: 比較関数(このモデルではcosineが適切)
numDimensions の値は、使用する埋め込みモデルと完全に一致していなければなりません。一致しないとインデックスの作成に失敗します。
from pymongo.mongo_client import MongoClient
from pymongo.operations import SearchIndexModel
import time
# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
"mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
appname="devrel-tutorial-python-semantic-search"
)
# Point to the same database and collection you used in the previous step:
database = mongodb_client["sample_db"]
collection = database["documents"]
# Define the vector search index.
# The three required fields tell MongoDB what to index and how to compare vectors:
search_index_model = SearchIndexModel(
definition={
"fields": [
{
"type": "vector",
"path": "embedding", # The field name from generate_embeddings.py
"numDimensions": 768, # nomic-embed-text-v1 always outputs 768 dimensions
"similarity": "cosine" # Recommended similarity function for this model
}
]
},
name="vector_index",
type="vectorSearch"
)
result = collection.create_search_index(model=search_index_model)
print("New search index named " + result + " is building.")
# Poll every five seconds until the index is ready to accept queries:
print("Polling to check if the index is ready. This may take up to a minute.")
predicate = lambda index: index.get("queryable") is True
while True:
indices = list(collection.list_search_indexes(result))
if len(indices) and predicate(indices[0]):
break
time.sleep(5)
print(result + " is ready for querying.")
mongodb_client.close()
スクリプトを実行します。
python create_vector_index.py
ターミナルには次のように表示されます。
New search index named vector_index is building.
Polling to check if the index is ready. This may take up to a minute.
vector_index is ready for querying.
インデックスの構築には最大で 1 分ほどかかります。ポーリングループは 5 秒ごとに確認し、MongoDB がインデックスがクエリ可能であると判断した時点でのみ終了します。「vector_index is ready for querying.」と表示されるまで次のステップに進まないでください。
検索クエリをベクトルに変換する
generate_query_vector.py という名前のファイルを作成します。このファイルは embedding_utils.py の get_query_embedding() 関数をインポートします。generate_query_vector.py を実行して、埋め込みモデルが正しく読み込まれ、768 次元のベクトルを出力することを確認します。
get_query_embedding()関数を定義し、次のステップでインポートして使用します。- モデルが正しく動作しているかを単体で検証できます。
ここでも最初のステップと同じモデルを使用する必要があります。異なるモデルは数値空間が異なるベクトルを生成するため、比較が意味をなさなくなります。
from embedding_utils import get_query_embedding
user_query = "I need an automated, scalable system for serious information storage"
query_vector = get_query_embedding(user_query)
print(f"Query: '{user_query}'")
print(f"Vector dimensions: {len(query_vector)}")
print(f"First 5 values: {query_vector[:5]}")
スクリプトを実行します。
python generate_query_vector.py
次のような出力が得られます。
<All keys matched successfully>
Query: 'I need an automated, scalable system for serious information storage'
Vector dimensions: 768
First 5 values: [0.0025914530269801617, 0.09862980246543884, -0.023092379793524742, -0.0171672236174345, -0.05548065900802612]
上記の出力のうち、Vector dimensions: 768 は、モデルの出力が保存済みの埋め込みと一致していることを示します。
$vectorSearch クエリを実行する
run_vector_search.py という名前のファイルを作成します。このファイルは embedding_utils.py の get_query_embedding() 関数をインポートし、ユーザーの検索語をベクトルに変換します。run_vector_search.py は $vectorSearch の集計パイプラインを実行し、セマンティック類似度で順位付けした結果を出力します。
from embedding_utils import get_query_embedding
from pymongo import MongoClient
# Replace the placeholder with your Atlas connection string:
mongodb_client = MongoClient(
"mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/",
appname="devrel-tutorial-python-semantic-search"
)
collection = mongodb_client["sample_db"]["documents"]
# Generate a query vector from the user's search input:
user_query = "I need an automated, scalable system for serious information storage"
query_vector = get_query_embedding(user_query)
# Define the $vectorSearch aggregation pipeline:
pipeline = [
{
"$vectorSearch": {
"index": "vector_index", # The index created in create_vector_index.py
"path": "embedding", # The field that holds your stored vectors
"queryVector": query_vector, # The vector generated from the user's query
"numCandidates": 150, # How many neighbors MongoDB considers
"limit": 3 # How many results to return
}
},
{
"$project": {
"_id": 0,
"title": 1,
"text": 1,
"score": {
"$meta": "vectorSearchScore" # Relevance score for each result
}
}
}
]
results = collection.aggregate(pipeline)
print(f"\nTop results for query: '{user_query}'\n")
for doc in results:
print(f"Title: {doc['title']}")
print(f"Text: {doc['text']}")
print(f"Score: {doc['score']:.4f}")
print()
mongodb_client.close()
上記のコードでは、2 つのパラメータが検索動作を制御します。
numCandidatesは、最終結果に絞り込む前に MongoDB が最初に検討する候補集合の大きさを設定します。値を大きくすると再現率が向上しますが、わずかに時間がかかります。limitは返される結果数を設定します。このチュートリアルではlimitを 3、numCandidatesを 150 に設定しています。一般的な出発点として、numCandidatesはlimitの 10〜15 倍に設定します。
では、スクリプトを実行します。
python run_vector_search.py
次のような出力が得られます。
<All keys matched successfully>
Top results for query: 'I need an automated, scalable system for serious information storage.'
Title: MongoDB Atlas
Text: MongoDB Atlas is a fully managed cloud database.
Score: 0.7211
Title: Nomic AI
Text: nomic-embed-text-v1 is a free, open-source embedding model.
Score: 0.6818
Title: Vector Search
Text: Vector search finds results based on semantic meaning.
Score: 0.6642
「MongoDB Atlas」は、クエリ「I need an automated, scalable system for serious information storage」と文言を共有していないにもかかわらず、最も高いスコアになっています。両者のベクトルが意味的に近いからです。「…自動化され、スケーラブルな情報保存の仕組み」は、「…フルマネージドのクラウドデータベース」とセマンティックに対応します。同じクエリでテキスト検索を行っても、どの文書にもクエリの語がそのまま現れないため、結果は 0 件となるでしょう。これがセマンティック検索が意図どおりに機能している状態です。
重要ポイント
- MongoDB Vector Search は M0 Free ティアのクラスタで動作します。有料プランは不要です。
- 保存する埋め込みの生成と、クエリベクトルの生成には同じ埋め込みモデルを使用する必要があります。モデルを混在させると結果は意味をなしません。
numDimensionsの値は、インデックス定義において使用する埋め込みモデルの出力サイズと完全に一致させる必要があります。nomic-embed-text-v1は常に 768 次元を出力します。input_type="document"は保存に最適化された埋め込みを生成します。input_type="query"は検索取得に最適化されます。各段階で適切なタイプを使用してください。numCandidatesは、MongoDB が最終的なlimitの結果に絞り込む前に投げかける検索網の広さを制御します。値を大きくすると再現率は上がりますが、クエリ時間が長くなります。vectorSearchScoreはセマンティック類似度によって結果を順位付けします。結果はクエリの語を 1 つも含んでいなくても構いません。スコア範囲はモデルやデータセットにより異なりますが、nomic-embed-text-v1とコサイン類似度の組み合わせでは、次の目安が出発点として有用です。- 0.9 以上: ほぼ同一の意味。文書とクエリはセマンティックにほとんど同じです。
- 0.7〜0.9: 強い関連。文書はクエリの意図に明確に関係しています。
- 0.5〜0.7: 中程度の関連。文書はトピック的には関連していますが、表現や文脈が異なります。
- 0.5 未満: 弱い関連。つながりが薄く、有用でない可能性があります。
このチュートリアルのコードサンプルはすべてGitHub リポジトリで確認できます。
参考資料
- MongoDB Vector Search の概要では、フィルタリングや量子化など MongoDB Vector Search の全機能を解説しています。
- ハイブリッド検索の実行方法では、ベクトル検索と全文検索を 1 つのクエリで組み合わせる方法を紹介します。
- ベクトル埋め込みの作成方法では、Voyage AI、OpenAI、その他のオープンソースモデルプロバイダーの埋め込みモデルを使って、コレクション内のテキストデータのベクトル埋め込みを生成する方法を説明します。
- MongoDB での RAG(Retrieval-Augmented Generation)では、RAG アプリケーションの取得レイヤーとしてセマンティック検索を使用する方法を示します。
FAQs
セマンティック検索を実装するには Atlas の有料プランが必要ですか?
いいえ。このチュートリアルの 4 つのステップはすべて、無料の M0 Free ティア クラスタで実行できます。
保存済み文書とは異なる埋め込みモデルでクエリを作成したらどうなりますか?
結果は意味をなしません。異なるモデルはテキストを異なる数値空間に写像するため、ベクトル同士を比較できなくなります。インデックス作成とクエリの双方で常に同じモデルを使用してください。
`numCandidates` と `limit` の違いは何ですか?
numCandidates は MongoDB が検索時に評価するベクトルの数です。limit は最終的に返す上位結果の数です。numCandidates を高くすると結果の品質は向上しますが、クエリがわずかに遅くなります。一般的な初期設定として、numCandidates を limit の 10〜15 倍にします。
セマンティック検索とテキスト検索を同時に使えますか?
はい。MongoDB はハイブリッド検索をサポートしており、$vectorSearch と $search を 1 つのパイプラインで組み合わせられます。
英語以外の言語でもセマンティック検索は機能しますか?
使用する埋め込みモデルによります。nomic-embed-text-v1 は主に英語テキストで学習されています。多言語のユースケースでは、データに含まれる言語で学習された多言語対応の埋め込みモデルを選択してください。