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)defget_embedding(text, precision="float32"):# Use this function when embedding text you plan to store in MongoDB:return model.encode(text, precision=precision).tolist()defget_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()提供:
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")isTruewhileTrue:
indices =list(collection.list_search_indexes(result))iflen(indices)and predicate(indices[0]):break
time.sleep(5)print(result +" is ready for querying.")
mongodb_client.close()提供:
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.」と表示されるまで次のステップに進まないでください。
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]}")提供:
<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]提供:
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()提供:
<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 件となるでしょう。これがセマンティック検索が意図どおりに機能している状態です。
こうした場面では通常のテキスト検索では力不足です。実際には次のような状況が起こります。
セマンティック検索はこれを解決します。完全一致ではなく、クエリの意味や意図を理解して検索します。
このチュートリアルでは、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 を使用するため、埋め込みの生成に時間がかかる場合があります。このチュートリアルで学べることは次のとおりです。
$vectorSearchの集計パイプラインを実行し、結果を解釈する方法このチュートリアルで使用するコードサンプルはすべてGitHub リポジトリにあります。
テキスト検索とセマンティック検索の違い
テキスト検索は、クエリに含まれる語句と完全に一致する語を含む文書を一致させます。セマンティック検索は、語がまったく異なっていても、クエリと同じ意味を持つ文書を見つけます。
次の表は、「heart problems」という検索クエリに対して、それぞれの方式が返す結果の違いを示しています。
セマンティック検索の基礎概念
MongoDB におけるセマンティック検索では、3 つの概念が中核になります。これらを理解すると、このチュートリアルの各ステップがなぜそのように動作するのかが分かります。
ベクトル埋め込み
埋め込みモデルは、テキストをベクトルと呼ばれる固定長の数列に変換します。ベクトル内の各数値は、意味の次元を表します。
意味が類似する 2 つのテキストは、数値的に近いベクトルを生成します。語を共有していなくても、"cardiac conditions" と "heart problems" はベクトル空間で近くに位置します。この近さがセマンティック検索を可能にします。
このチュートリアルでは、無料・オープンソースでローカルマシン上だけで動作する
nomic-embed-text-v1埋め込みモデルを使用します。初回実行時に Hugging Face から自動的にダウンロードされ、以降はローカルに保存されたものを使用します。モデルは入力 1 件あたり 768 次元の数値を出力します。ベクトル検索インデックス
ベクトル検索インデックスは、どのフィールドに埋め込みがあり、何次元を想定し、比較にどの類似度関数を使うかを MongoDB に指示します。
$vectorSearchクエリを実行する前に、このインデックスを作成する必要があります。$vectorSearch集計ステージ$vectorSearchは検索を実行する集計ステージです。クエリベクトルを受け取り、インデックス化されたフィールドを検索し、セマンティックな類似度で順位付けされた文書を返します。ほかの集計パイプライン同様、$projectなどのステージと連結して使用します。それでは始めましょう。
前提条件
開始する前に、次を用意してください。
pymongo4.7 以降、sentence-transformers、einopsの各 Python パッケージがインストールされていることプロジェクトのセットアップ
プロジェクト用のフォルダを作成し、ターミナルでそのフォルダに移動します。
プロジェクトの依存関係を分離するため、仮想環境を作成して有効化します。
Windows の場合、次で仮想環境を有効化します。
次に、必要なパッケージをインストールします。
このチュートリアルでは各ステップごとに 1 つの Python ファイルを作成します。すべてのファイルは
mongodb-semantic-searchフォルダに配置します。埋め込みユーティリティファイルを作成する
embedding_utils.pyという名前のファイルを作成します。このファイルには、このチュートリアルで使用する 2 つの埋め込み関数を定義します。警告:
trust_remote_code=Trueは、Hugging Face からダウンロードされたモデル固有の Python コードをマシン上で実行できるようにします。Hugging Face 上の当該モデルのソースコードが侵害されたり、悪意ある変更で更新された場合、そのコードが環境内で自動的に実行されます。本番環境でこのパラメータを使用する場合は十分に注意してください。ベクトル埋め込みを生成・保存する
generate_embeddings.pyという名前のファイルを作成します。このファイルはembedding_utils.pyから埋め込み関数をインポートし、各文書の text フィールドに対するベクトルを生成して、埋め込みを含む完全な文書を MongoDB に保存します。上記のコードでは、
collection.drop()が実行のたびにコレクション内のすべての文書とインデックスを削除します。generate_embeddings.pyをこの行なしで再実行すると、重複した文書が挿入され、$vectorSearchが同じ文書を複数回返す原因になります。コレクションを削除するとベクトル検索インデックスも削除されるため、generate_embeddings.pyを再実行した際はcreate_vector_index.pyも再度実行する必要があります。スクリプトを実行します。
ターミナルには次のように表示されます。
MongoDB の各文書には、元のテキストと、その 768 次元のベクトルが
embeddingフィールドに保存されました。ベクトルはデータと同じ文書に共存するため、クエリ時に別ストレージやルックアップは不要です。MongoDB Vector Search インデックスを作成する
create_vector_index.pyという名前のファイルを作成します。このファイルはembeddingフィールドに対するベクトル検索インデックスを定義・作成し、コレクションに対して$vectorSearchクエリを実行できるようにします。インデックス定義には 3 つのフィールドが必要です。
path: 埋め込みを保持するフィールド(このチュートリアルではembedding)numDimensions: モデルの出力次元数と一致させる(nomic-embed-text-v1は768)similarity: 比較関数(このモデルではcosineが適切)numDimensionsの値は、使用する埋め込みモデルと完全に一致していなければなりません。一致しないとインデックスの作成に失敗します。スクリプトを実行します。
ターミナルには次のように表示されます。
インデックスの構築には最大で 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()関数を定義し、次のステップでインポートして使用します。ここでも最初のステップと同じモデルを使用する必要があります。異なるモデルは数値空間が異なるベクトルを生成するため、比較が意味をなさなくなります。
スクリプトを実行します。
次のような出力が得られます。
上記の出力のうち、
Vector dimensions: 768は、モデルの出力が保存済みの埋め込みと一致していることを示します。$vectorSearchクエリを実行するrun_vector_search.pyという名前のファイルを作成します。このファイルはembedding_utils.pyのget_query_embedding()関数をインポートし、ユーザーの検索語をベクトルに変換します。run_vector_search.pyは$vectorSearchの集計パイプラインを実行し、セマンティック類似度で順位付けした結果を出力します。上記のコードでは、2 つのパラメータが検索動作を制御します。
numCandidatesは、最終結果に絞り込む前に MongoDB が最初に検討する候補集合の大きさを設定します。値を大きくすると再現率が向上しますが、わずかに時間がかかります。limitは返される結果数を設定します。このチュートリアルではlimitを 3、numCandidatesを 150 に設定しています。一般的な出発点として、numCandidatesはlimitの 10〜15 倍に設定します。では、スクリプトを実行します。
次のような出力が得られます。
「MongoDB Atlas」は、クエリ「I need an automated, scalable system for serious information storage」と文言を共有していないにもかかわらず、最も高いスコアになっています。両者のベクトルが意味的に近いからです。「…自動化され、スケーラブルな情報保存の仕組み」は、「…フルマネージドのクラウドデータベース」とセマンティックに対応します。同じクエリでテキスト検索を行っても、どの文書にもクエリの語がそのまま現れないため、結果は 0 件となるでしょう。これがセマンティック検索が意図どおりに機能している状態です。
重要ポイント
numDimensionsの値は、インデックス定義において使用する埋め込みモデルの出力サイズと完全に一致させる必要があります。nomic-embed-text-v1は常に 768 次元を出力します。input_type="document"は保存に最適化された埋め込みを生成します。input_type="query"は検索取得に最適化されます。各段階で適切なタイプを使用してください。numCandidatesは、MongoDB が最終的なlimitの結果に絞り込む前に投げかける検索網の広さを制御します。値を大きくすると再現率は上がりますが、クエリ時間が長くなります。vectorSearchScoreはセマンティック類似度によって結果を順位付けします。結果はクエリの語を 1 つも含んでいなくても構いません。スコア範囲はモデルやデータセットにより異なりますが、nomic-embed-text-v1とコサイン類似度の組み合わせでは、次の目安が出発点として有用です。このチュートリアルのコードサンプルはすべてGitHub リポジトリで確認できます。
参考資料