Python Monthly Topics

Pythonによるベクトル検索の基礎と実践 ~Embedding⁠Vector DB~

寺田 学@terapyonです。2026年4月の「Python Monthly Topics」は、Pythonを使ったベクトル検索の基礎と実践について紹介します。

「EV」と検索しても「電気自動車」がヒットしない ―キーワード検索の限界を感じたことはないでしょうか。ベクトル検索は、この「意味の近さ」を数値で表現し、より直感的な検索を実現する技術です。近年、LLMや生成AIの普及とともに、RAG(Retrieval-Augmented Generation)の中核技術として実務への導入が急速に進んでいます。

本記事では、Pythonを使ってベクトル検索パイプラインを一から構築する方法を解説します。テキストのEmbedding生成から始まり、ベクトルデータベースへの保存・検索の実装を、段階的に紹介していきます。

はじめに ―ベクトル検索の概要とPythonのエコシステム

キーワード検索の限界

従来のキーワード検索(Keyword Search)は、文字列の一致を基準に結果を返します。⁠EV」で検索しても「電気自動車」「テスラ」はヒットしませんし、⁠スマートフォン」で検索しても「携帯電話」「スマホ」が同じ意味だとは認識できません。一方、ベクトル検索(Semantic Search)は意味の類似性に基づいて結果を返す仕組みです。両者の違いを以下の表にまとめます。

特徴 キーワード検索 ベクトル検索
マッチング キーワードの一致 意味の類似性
得意なこと 固有名詞、型番、完全一致 概念検索、表記揺れ吸収、曖昧な検索
苦手なこと 同義語(辞書が必要⁠⁠、表記揺れ 特定キーワードの厳密な検索

両者の違いを「EV」で検索した場合で図示してみます(図1)

図1 キーワード検索とベクトル検索の違い
図1 キーワード検索とベクトル検索の違い

図1のように、キーワード検索は文字列の一致に依存するのに対して、ベクトル検索はテキストを数値ベクトルに変換し、ベクトル空間上の近さで判断するため、表現の揺れを越えて関連する文書を拾えます。

最近のトレンドは、両者の長所を組み合わせたハイブリッド検索です。本記事では、ベクトル検索の基礎をしっかり固めることに集中し、ハイブリッド検索については「まとめ」で次のステップとして触れます。

ベクトル検索とは何か

ベクトル検索は、テキストや画像などのデータを高次元の数値ベクトルに変換し、ベクトル空間上の距離(コサイン類似度など)で類似度を測る仕組みです。⁠意味が近いものはベクトルも近い」という性質を利用することで、キーワードの一致に依存しない検索を実現できます。

ベクトル検索システムを構築するには、大きく分けて以下の4つのステップが必要です。

  1. Embedding化(ベクトル化):検索対象のドキュメント(テキスト、画像など)を、意味内容を表す数値の配列(ベクトル)に変換する。この処理を行うのがEmbeddingモデルと呼ばれる機械学習モデルです
  2. ベクトルの永続化:生成されたベクトルデータを、後で検索可能な形式で保存する。一般的にはベクトルデータベース(Vector DB)を利用する
  3. クエリのEmbedding化:ユーザーが入力した検索クエリを、同じモデルを使ってベクトルに変換し、クエリベクトルを生成
  4. ベクトルの近傍検索:クエリベクトルと距離が近い(意味が似ている)ドキュメントベクトルを上記2項の永続化されたベクトルデータから探し出す

この4ステップを、⁠インデックス作成時(事前に1回⁠⁠」と「検索時(クエリごと⁠⁠」に分けて図示すると図2のようになります。

図2 ベクトル検索の4ステップ
図2 ベクトル検索の4ステップ

図2のポイントは、インデックス作成時と検索時で同じEmbeddingモデルを使うことと、Vector DBが近傍検索の効率化を担うことの2点です。後半のコード例もこの構造に沿って実装していきます。

実務での活用場面

ベクトル検索が実務で活用される代表的な場面を挙げます。

  • RAG(検索拡張生成):LLMに渡すコンテキストとして、質問に意味的に近いドキュメントを検索する
  • 類似商品検索:ECサイトで「この商品に似たもの」を提示する
  • ドキュメント検索:社内ナレッジベースから関連する文書を探す
  • 画像検索:テキストで画像を検索したり、画像で似た画像を探したりする

Pythonの役割

Embedding生成・データ処理・評価・インフラ連携のすべての工程において、Pythonは事実上の標準言語として機能しています。Hugging Face Transformerssentence-transformers、各種Vector DBのPythonクライアントなど、エコシステムが急速に充実しており、少ないコードで本格的なパイプラインを構築できます。

図2の4ステップを、本記事で実際に使う具体的なライブラリ・モデル・DBにあてはめると、各ステップと技術の対応は以下のようになります。他の選択肢については後半で解説しています。

ステップ 本記事で使う技術 他の選択肢
1. Embedding化(ドキュメント) sentence-transformers+multilingual-e5(768次元) OpenAI Embeddings API, Ollama, FastEmbed
2. ベクトルの永続化 DuckDB+VSS拡張 Qdrant, pgvector, LanceDB
3. Embedding化(クエリ) (1と同じモデルを使用)
4. 近傍検索 HNSW(VSS拡張が内蔵) IVF, PQ など

このように、本記事ではEmbeddingに sentence-transformersのmultilingual-e5、Vector DBにはDuckDBのVSS(Vector Similarity Search)拡張を使って、ローカル環境で完結するパイプラインを組み立てていきます。VSS拡張は、DuckDBにベクトル距離計算やHNSWインデックスなどの類似度検索機能を追加する公式拡張です。

動作環境

本記事のコードは以下の環境で動作確認しています。

項目 バージョン 備考
Python 3.13.13
sentence-transformers 5.4.1 テキストEmbedding生成
duckdb 1.5.2 ベクトルの保存・類似度検索
NumPy 2.4.4

パッケージのインストール

uvを使う場合:

uvを使ったインストール
uv add sentence-transformers duckdb numpy

pipを使う場合:

pipを使ったインストール
pip install sentence-transformers duckdb numpy

テキストデータのベクトル化(Embedding)手法

Embeddingとは

Embedding(エンベディング)とは、単語・文・画像などのデータを固定長の数値ベクトルに変換する処理のことです。この変換を行うのがEmbeddingモデルと呼ばれる専用の機械学習モデルです。テキストを入力として受け取り、たとえば384次元や768次元、1024次元といった固定長の数値の配列を出力します。

Embeddingモデルにはさまざまな種類があり、モデルごとに意味の捉え方が異なります。英語に強いもの、日本語を含む多言語に対応したもの、特定ドメインに特化したものなど、目的に応じた選択が必要です。また、出力するベクトルの次元数もモデルによって異なり、次元数が大きいほど意味の表現力は高くなりますが、その分ストレージや計算コストも増えます。Embeddingモデルの選択については後半の「Embeddingモデル選択のポイント」で詳しく解説します。

もうひとつ注意すべきなのが入力テキストの長さ制限です。従来のBERTベースのモデルは、256〜512トークン程度を上限とするものが多く、それを超える文章は切り捨てられてしまいます。長い文書を扱う場合は、適切な長さに分割(チャンキング)してからEmbeddingする必要があります。最近のLLMベースのEmbeddingモデルでは入力長の制限が大幅に緩和されているものもありますが、モデルごとに対応が異なるため、利用前に仕様を確認しておくことが重要です。

たとえば、⁠EV」⁠乗用車」⁠洗濯機」という3つの単語をEmbeddingすると、⁠EV」「乗用車」のベクトルは近い位置に、⁠EV」「洗濯機」は離れた位置に配置されます。この「意味が近いものはベクトルも近い」という性質がベクトル検索の根幹です。

以下のコードでは、同じテキストを2つのEmbeddingモデルでベクトル化し、出力されるベクトルの次元数を比較します。

Embeddingの実行(embedding_dimensions.py)
from sentence_transformers import SentenceTransformer


text = "EVはバッテリーで駆動する乗用車です"
model_names = [
    "intfloat/multilingual-e5-base",
    "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
]

for model_name in model_names:
    model = SentenceTransformer(model_name)
    embedding = model.encode(text, normalize_embeddings=True)

    print(model_name)
    print(f"  ベクトルの形状: {embedding.shape}")
    print(f"  先頭5要素: {embedding[:5].round(4).tolist()}")
Embeddingの実行結果(抜粋)
(略⁠⁠

intfloat/multilingual-e5-base
  ベクトルの形状: (768,)
  先頭5要素: [0.02539999969303608, 0.03759999945759773, 0.012799999676644802, 0.04989999905228615, 0.03620000183582306]

⁠略⁠⁠

sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
  ベクトルの形状: (384,)
  先頭5要素: [0.032499998807907104, 0.10040000081062317, -0.0210999995470047, 0.012000000104308128, 0.07150000333786011]

このあと、ベクトル同士の近さの測り方や、Embeddingを使った検索の実装を順に見ていきます。ここでは、モデルによってベクトルの次元数が異なる点に注目してください。

ベクトルの「近さ」を測る指標

ベクトル検索では、2つのベクトルがどれだけ「近い」かを数値で測る必要があります。代表的な指標を3つ紹介します。

指標 測るもの 値の範囲 特徴
コサイン類似度 ベクトルの向き(角度) -1〜1 AI分野で最もよく使われる
ユークリッド距離(L2距離) ベクトル間の直線距離 0〜∞ 直感的だが、高次元では差が出にくい
内積(ドット積) ベクトルの向きと大きさ -∞〜∞ 正規化済みベクトルではコサイン類似度と同じ結果

コサイン類似度はベクトルの「向き」の近さを測ります。値が1に近いほど類似度が高く、0に近いほど無関係です。ベクトルの長さ(大きさ)に影響されないため、AI分野で最も好まれる指標です。

ユークリッド距離(L2距離)は点と点の直線距離で、直感的にわかりやすい指標です。ただし、数百〜数千次元の高次元空間では、どの点を選んでも距離の差がほとんどなくなる「次元の呪い」と呼ばれる現象が起きるため、直線距離よりも角度(コサイン類似度)のほうが有効に働く場面が多くなります。

内積(ドット積)は計算がシンプルで高速です。ただし、内積の結果がコサイン類似度と一致するのは、ベクトルL2正規化(長さを1に揃える)した場合に限ります。モデルやライブラリによって扱いは異なり、OpenAI Embeddings APIのように正規化済みベクトルを返すものもあれば、sentence-transformersのようにnormalize_embeddings=Trueで正規化を明示できます。正規化済みベクトル同士では、内積がコサイン類似度と完全に一致するため、大規模データでの検索では内積を使ったほうが計算効率が良くなります。

NumPyを使った計算例を示します。

NumPyによる類似度計算の例(similarities.py)
from typing import Any
import numpy as np

# コサイン類似度
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# L2距離(ユークリッド距離)
def l2_distance(a: np.ndarray, b: np.ndarray) -> np.floating[Any]:
    return np.linalg.norm(a - b)

# 内積(正規化済みベクトル向け)
def dot_similarity(a: np.ndarray, b: np.ndarray) -> float:
    return np.dot(a, b)

Embedding化の方法

Embedding化の方法は大きく分けて3つあります。

  • PythonからローカルでEmbeddingモデルを動かす方法(例:sentence-transformers
  • 外部APIを使ってEmbeddingを生成する方法(例:OpenAI Embeddings API)
  • OllamaのEmbedding機能を使う方法(ローカルで完結するが、Ollamaサーバーの起動が必要)

3つのアプローチの特徴を以下の表にまとめます。

項目 ローカルモデル(sentence-transformers) 外部API(OpenAI等) Ollama
コスト 無料(計算リソースのみ) 従量課金 無料(計算リソースのみ)
プライバシー ◎(データが外部に出ない) △(データがクラウドに送信される) ◎(データが外部に出ない)
セットアップ pip installのみ APIキーが必要 Ollamaサーバーの起動が必要
扱いやすさ ◎(Pythonのみで完結) ◎(APIなので言語を問わず扱いやすい)
オフライン対応
精度 モデルによる 高品質 モデルによる
日本語対応 モデルによる モデルによる

プロトタイピングや個人プロジェクトではsentence-transformersのローカルモデルから始めるのが手軽です。

ここでは、PythonからローカルでEmbeddingモデルを動かす方法を中心に解説します。外部APIやOllamaは後述するコラムで紹介します。

sentence-transformersによるローカル実行

sentence-transformersは、テキストのEmbedding生成に特化したPythonライブラリです。Hugging Face上の多数のモデルを利用でき、ローカル環境で完結するため、プライバシーやコストの面でも安心して使えます。

以下のコード例では、日本語を含む多言語に対応したEmbeddingモデルintfloat/multilingual-e5-baseを使って簡単な検索シナリオを実装します。あらかじめ4つの文をドキュメントとしてベクトル化しておき、あとから入力した短い文(単語でも可)ともっとも意味的に近いものを探します。

sentence-transformersによるEmbedding生成と検索 (text_encoding.py)
from sentence_transformers import SentenceTransformer

# ① モデルのロード(初回はHugging Faceからダウンロードされる)
model = SentenceTransformer("intfloat/multilingual-e5-base")

# ② 検索対象のドキュメント
documents = [
    "電気自動車は環境にやさしい移動手段です",
    "バッテリー駆動の新しい乗用車が増えています",
    "今日は良い天気です",
    "洗濯機は衣類を洗うための家電です",
]

# ③ ドキュメントをベクトル化
# multilingual-e5 系のモデルでは検索対象に "passage: " プレフィックスを付ける
doc_embeddings = model.encode(
    [f"passage: {doc}" for doc in documents],
    normalize_embeddings=True,
)
print(f"ベクトルの形状: {doc_embeddings.shape}")
# → ベクトルの形状: (4, 768)

# ④ 短いクエリで検索する(検索側には "query: " プレフィックス)
query = "EV"
query_embedding = model.encode(f"query: {query}", normalize_embeddings=True)

# ⑤ 全ドキュメントとの類似度を一括計算(正規化済みなので内積=コサイン類似度)
similarities = doc_embeddings @ query_embedding
ranked = sorted(zip(similarities, documents), reverse=True)

print(f"\n検索クエリ: 「{query}」")
print("-" * 50)
for sim, doc in ranked:
    print(f"類似度 {sim:.4f}: {doc}")

実行結果は以下のようになります(値は環境により多少変動します⁠⁠。

Embedding生成と検索の実行結果の例
ベクトルの形状: (4, 768)

検索クエリ: 「EV」
--------------------------------------------------
類似度 0.8149: 電気自動車は環境にやさしい移動手段です
類似度 0.7931: バッテリー駆動の新しい乗用車が増えています
類似度 0.7715: 洗濯機は衣類を洗うための家電です
類似度 0.7397: 今日は良い天気です

「EV」という文字列はどのドキュメントにも含まれていないにもかかわらず、⁠電気自動車」「バッテリー駆動の乗用車」が上位に来ています。キーワード一致では実現できない、ベクトル検索ならではの挙動が確認できます。

各ステップのポイントを説明します。

  1. SentenceTransformerにモデル名を渡すだけでロードできます。初回実行時はモデルがダウンロードされますmultilingual-e5-baseは約1.1GB)
  2. 検索対象のドキュメントを用意します
  3. model.encode()にリストを渡すとまとめてベクトル化されます。normalize_embeddings=Trueを指定すると単位長に正規化され、以降は内積がそのままコサイン類似度になります。戻り値は(ドキュメント数, 次元数)のNumPy配列です
  4. 検索クエリも同じモデルでベクトル化します
  5. 行列積@で全ドキュメントとの類似度を一括計算し、降順に並べて表示します

コード中の"passage: " / "query: "multilingual-e5系のモデルで推奨されるプレフィックスです。モデルごとに付け方が異なるので、詳しくは後述のコラム「モデルによって異なるプレフィックスに注意」を参照してください。

デバイス(GPU / CPU / MPS)の指定

SentenceTransformer は、計算に使うデバイスを device 引数で指定できます。指定しなければGPUの有無などから自動で選ばれますが、環境に合わせて明示しておくと挙動が安定します。

デバイスの指定例
# NVIDIA GPU(CUDA)
model = SentenceTransformer("intfloat/multilingual-e5-base", device="cuda")

# Apple Silicon(M1〜M4 など)のMPS
model = SentenceTransformer("intfloat/multilingual-e5-base", device="mps")

# CPU
model = SentenceTransformer("intfloat/multilingual-e5-base", device="cpu")

筆者はNVIDIA GPU環境でdevice="cuda"を使っています。大量のドキュメントを一括でEmbeddingする用途ではGPUの有無が処理時間の大きな差につながります。数百〜数千件程度のデータであれば、device="cpu"でも実用的な速度で動作します。Apple Silicon搭載Macではdevice="mps"を指定するとPyTorchのMetal Performance Shadersバックエンドが使え、CPUよりも高速に動作します。

なお、GPUを使えない環境で大量のテキストを扱う場合は、ONNX RuntimeベースのFastEmbedという選択肢もあります。PyTorchより軽量に扱えるケースがあり、サーバーサイドやCI環境での運用に向いています。

長文をチャンクに分けてEmbeddingする

Embeddingモデルには入力長の上限があります。multilingual-e5-baseの場合は512トークン(日本語でおおむね数百文字程度)が上限で、それを超える文章はモデル側で切り捨てられてしまいます。また、長い文書を1つのベクトルに押し込めると話題が混ざって意味がぼやけ、検索精度が落ちる原因にもなります。

そこで実務では、長文を一定の長さのチャンク(断片)に分割してからEmbeddingする方法がよく使われます。固定文字数で切り出しつつ、境界で意味がぶつ切りになるのを避けるために少しだけ重なり(オーバーラップ)を持たせるのが基本形です(図3)

図3 固定文字数+オーバーラップによるチャンキング
図3 固定文字数+オーバーラップによるチャンキング

図3のように、chunk_size分だけ切り出したら、次のチャンクはoverlap分戻った位置から始める、という動きを繰り返していきます。以下は、この考え方をそのまま実装したシンプルな例です。

長文をチャンクに分けてEmbeddingする(chunk_encoding.py)
from sentence_transformers import SentenceTransformer


def chunk_text(text: str, chunk_size: int = 200, overlap: int = 30) -> list[str]:
    """長文を指定された文字数のチャンクに分割する(オーバーラップあり)。"""
    chunks = []
    step = chunk_size - overlap
    for start in range(0, len(text), step):
        chunk = text[start:start + chunk_size]
        if chunk:
            chunks.append(chunk)
        if start + chunk_size >= len(text):
            break
    return chunks


long_text = (
    "Pythonはシンプルな文法と豊富なライブラリが特徴の汎用プログラミング言語です。"
    "Webアプリケーション開発、データ分析、機械学習、スクリプティングなど、"
    "幅広い用途で使われています。"
    "近年は、生成AIやベクトル検索の分野でも事実上の標準言語として定着しました。"
    "Hugging Face Transformersやsentence-transformersといったライブラリを使えば、"
    "少ないコードでEmbeddingの生成や類似度検索を実装できます。"
    "ベクトル検索はRAG(検索拡張生成)の基盤技術として、"
    "社内ドキュメント検索やチャットボットの文脈提供などに応用されています。"
)

# ① 長文をチャンクに分割
chunks = chunk_text(long_text)
print(f"チャンク数: {len(chunks)}")
for i, chunk in enumerate(chunks):
    print(f"  [{i}] ({len(chunk)}文字) {chunk[:30]}...")

# ② 各チャンクをpassageとしてEmbedding化
model = SentenceTransformer("intfloat/multilingual-e5-base")
chunk_embeddings = model.encode(
    [f"passage: {chunk}" for chunk in chunks],
    normalize_embeddings=True,
)

# ③ クエリを同じモデルでEmbedding化(こちらは "query: " プレフィックス)
query = "RAGで使われる技術は何ですか"
query_embedding = model.encode(f"query: {query}", normalize_embeddings=True)

# ④ 各チャンクとの類似度を計算し、上位を表示
similarities = chunk_embeddings @ query_embedding  # 正規化済みなので行列積=コサイン類似度
ranked = sorted(enumerate(similarities), key=lambda x: x[1], reverse=True)

print(f"\nクエリ: 「{query}」")
print("-" * 50)
for rank, (idx, sim) in enumerate(ranked[:3], 1):
    print(f"{rank}位 類似度 {sim:.4f}: {chunks[idx]}")

実行結果の一部を抜粋すると、以下のようになります。

長文をチャンクに分けた実行結果の例(抜粋)
チャンク数: 2
  [0] (200文字) Pythonはシンプルな文法と豊富なライブラリが特徴の汎用プ...
  [1] (115文字) ormersといったライブラリを使えば、少ないコードでEmb...

(略)

クエリ: 「RAGで使われる技術は何ですか」
--------------------------------------------------
1位 類似度 0.8328: ormersといったライブラリを使えば、少ないコードで...
2位 類似度 0.7798: Pythonはシンプルな文法と豊富なライブラリが特徴の...

各ステップのポイントです。

  1. chunk_text()では、指定した文字数ごとに切り出しつつ、チャンクの境界で意味がぶつ切りになるのを避けるためにオーバーラップ(重なり)を設けています。chunk_sizeoverlapは、対象文書の性質や利用するモデルの入力上限に合わせて調整してください
  2. チャンクは検索対象のドキュメント扱いなので、multilingual-e5の流儀に従い"passage: "プレフィックスを付けます
  3. 検索クエリ側には"query: "プレフィックスを付けます。同じモデルで両者をベクトル化することで、同一空間上で比較できます
  4. 正規化済みベクトルでは行列積@だけで全チャンクとのコサイン類似度をまとめて計算できます

この例では固定文字数で素朴に分割していますが、実務では段落や句点単位で区切る、文書の構造(見出し)に沿って分ける、などの工夫がよく使われます。LangChainやLlamaIndexには洗練されたチャンカーが用意されているので、本格的なRAGを構築する際にはそれらも検討してみてください。

Embeddingモデル選択のポイント

テキスト用Embeddingモデルは大きくBERTベースとLLMベースに分かれます。まずは筆者の視点で代表的なモデルを以下にまとめます。そのあとで、各モデルの性能を客観的に比較するための指標として、本セクション後半の「モデル選定の参考になるベンチマーク(MTEB⁠⁠」でMTEBリーダーボードの見方も紹介します。

モデル ベース 次元数 入力上限 日本語 特徴
sentence-transformers/all-MiniLM-L6-v2 BERT 384 256トークン 軽量・英語向け、学習用途に最適
intfloat/multilingual-e5-base BERT 768 512トークン 多言語対応、バランスが良い
intfloat/multilingual-e5-large BERT 1024 512トークン 多言語高精度、筆者のメイン
BAAI/bge-m3 BERT 1024 8192トークン 多言語・長文対応、Dense/Sparse/ColBERTのハイブリッド検索対応
cl-nagoya/ruri-v3-310m ModernBERT 768 8192トークン ◎◎ 日本語特化、長文対応、JMTEB高水準
jinaai/jina-embeddings-v3 BERT+LoRA 1024 8192トークン 多言語・長文対応、タスク別LoRAアダプタで用途に応じた精度調整が可能
nomic-ai/nomic-embed-text-v1.5 BERT 768 8192トークン × 英語専用、長文対応、ログ分析などに便利
nomic-ai/nomic-embed-text-v2-moe LLM(MoE) 768 512トークン 多言語対応、Matryoshka対応
google/embeddinggemma-300m LLM 768 2048トークン 小型・多言語対応、Matryoshka対応
Qwen/Qwen3-Embedding-0.6B LLM 1024 32768トークン 長文対応、高精度

モデルは「何でも同じ」ではありません。日本語対応・入力長・次元数など、モデルによって得意不得意が大きく異なります。用途に合ったモデルを選ぶことが検索品質に直結します。

BERT系(ModernBERTを含む)は、軽量でCPU環境でも運用が可能です。入力長は256〜512トークンが上限のものが多いですが、実績が豊富で安定しています。BAAI/bge-m3jinaai/jina-embeddings-v3cl-nagoya/ruri-v3-310mのように、8192トークン級の長文に対応したものもあります。

LLMベースは入力長の制限が大幅に緩和されており、表現力も高い傾向があります。ただしモデルサイズの幅が広く、大きめのモデルで実用的な速度を出すにはGPU環境がほぼ前提になります。一方で、google/embeddinggemma-300mのような小型モデルであれば、CPU環境でも用途次第で実用的に扱えます。

表中に出てくるMatryoshka対応は、1つのモデルから得たベクトルの先頭から指定した次元数分だけ切り出しても意味を保つ ように学習されたモデルを指します。たとえば、768次元で学習されたモデルでも、先頭256次元だけを使って検索できるため、保存容量や計算コストを大きく減らせます。精度と効率のトレードオフを後からコード側で調整できるのが利点です。名前は入れ子構造のマトリョーシカ人形に由来します。

モデル選定の参考になるベンチマーク(MTEB)

上述の表は筆者の視点でよく使われるモデルを整理したものですが、Embeddingモデルの世界は進歩が速く、新しいモデルも次々と登場します。客観的な比較指標として便利なのが、MTEB(Massive Text Embedding Benchmark)のリーダーボードです。MTEBはEmbeddingモデルを検索(Retrieval)分類(Classification)クラスタリング(Clustering)意味的類似度(STS)など複数のタスクで評価するベンチマーク群で、Hugging Face上で誰でも閲覧できます。ベンチマークを選ぶと、上位モデルのランキングとともに、モデルサイズTotal Parameters⁠・ベクトル次元数Embedding Dimensions⁠・入力上限Max Tokensといった実用情報を一覧できます。

ベクトル検索用途では、画面左メニューのRetrieval「RTEB Multilingual」を開くのがおすすめです。検索タスクに特化した多言語ベンチマークで、日本語を含む言語でのRetrieval性能を俯瞰できるため、モデル候補を絞り込むときの出発点として便利です。

ただしリーダーボードはあくまで参考程度に使うのが適切です。MTEBは結果の提出ベースで更新されるため、すべてのモデルや派生バリアントが載っているわけではありません。本記事の表に挙げたモデルの中にも、ベンチマークによっては掲載されていないものがあります。また、スコアは公開データセットに対する平均的な性能であり、実際のドキュメントやクエリでの精度と一致するとは限りません。用途によっては、上位ではないモデルでも十分な性能が出ることが多いので、気になった候補を数モデルに絞ったら、自分の用途に沿った評価データを数十〜数百件用意して比較してから採用するのが確実です。

ベクトルデータベースの種類と選択基準

Embeddingによってテキストが数値ベクトルに変換できるようになったら、次は大量のベクトルを効率よく保存・検索する仕組みが必要になります。ここからは、その役割を担うベクトルデータベースの選び方と、DuckDBを使った具体的な実装を見ていきます。

なぜ通常のDBでは不十分か

生成したEmbeddingベクトルを保存するだけなら、通常のRDBMSやファイルでも問題ありません。しかし、⁠クエリベクトルに最も近いベクトルを探す」という類似度検索を行う場合、通常のRDBMSでは全件スキャンが必要になります。

たとえば10万件のベクトルがあり、各ベクトルが768次元(float32)だとすると、合計で約300MB(10万件×768次元×4バイト)のデータ転送が必要でI/O負荷にもなります。さらに1回の検索で10万回の類似度計算が必要です。小規模なデータでは問題ありませんが、データ量が増えると現実的な応答速度を維持できなくなります。

ベクトルデータベース(Vector DB)は、この高次元ベクトルの類似度検索を効率的に行うために設計された専用のデータストアです。全件スキャンではなく、ANN(Approximate Nearest Neighbor: 近似最近傍探索) と呼ばれる仕組みを内部で用いることで、多少の精度と引き換えに大規模データでも高速な検索を実現します。代表的なアルゴリズムには HNSW(Hierarchical Navigable Small World⁠⁠、IVF(Inverted File Index⁠⁠、PQ(Product Quantization)などがあり、Vector DBごとに対応状況が異なります。ANNアルゴリズム自体の詳細は次号(2026年5月)で取り上げる予定です。

代表的なベクトルデータベース

ベクトルデータベースには、専用のVector DB、既存RDBMSの拡張、ローカル/組み込み向けのものなどさまざまな選択肢があります。筆者はローカルでの実験やデータ分析にはDuckDB、本番運用にはQdrantを使い分けています。

DB 分類 用途 セットアップ SQLで操作 フィルタリング Pythonクライアント
DuckDB ローカル/組み込み ローカル実験・分析 pip installのみ(ファイルベース) ◎(SQL) ◎(duckdb)
Qdrant 専用Vector DB 中〜大規模・本番運用 Docker推奨
Chroma 専用Vector DB プロトタイピング pip installのみ
pgvector RDBMSの拡張 既存PostgreSQL環境への追加 PostgreSQL必要 ◎(SQL) ○(psycopg2等)
sqlite-vec RDBMSの拡張 軽量・組み込み pip installのみ
LanceDB ローカル/組み込み MLパイプライン・ローカル運用 pip installのみ(ファイルベース) △(SQLライクフィルタ)

DuckDBはファイルベースの列指向データベースで、VSS(Vector Similarity Search)拡張を使うことでベクトル検索が可能になります。SQLでベクトル演算(コサイン類似度など)が扱えるのが大きな特徴で、セットアップも手軽です。筆者はローカルでのデータ分析や一時的なベクトル処理に日常的に利用しています。ただし、HNSWインデックスの永続化は執筆時点では実験的機能のため、利用時は設定と注意事項を確認してください。

QdrantはRust製の高性能なVector DBで、本番運用を想定して設計されています。メタデータフィルタリング(⁠⁠2024年以降かつカテゴリがニュース」のような絞り込み)が強力で、Pythonクライアントも直感的に使えます。筆者が本番環境でメインに使っているVector DBです。利用にはDockerでのサーバー起動が必要ですが、クラウドサービスも提供されています。

Chromaは軽量でセットアップが容易なVector DBで、プロトタイピングに向いています。

pgvectorはPostgreSQLの拡張機能で、既存のPostgreSQL環境にそのまま追加できます。SQLでベクトル検索を記述でき、既存資産を活かせるのが強みです。

sqlite-vec はSQLiteにベクトル検索機能を追加する拡張で、軽量な組み込み用途に適しています。sqlite-vss の後継として開発が進められており、導入しやすさの面でも扱いやすい選択肢です。

LanceDBは、Lanceという列指向フォーマットをベースにした組み込み型のVector DBです。pip installで導入でき、ファイルベースで動く手軽さはDuckDBやsqlite-vecに近いですが、ML/AIワークフローを意識して設計されており、マルチモーダルデータの管理やデータのバージョニングに対応しているのが特徴です。ローカルでのプロトタイピングから大規模データまで一貫して扱えます。

DuckDBを使った基本的なベクトル保存⁠検索

DuckDBのVSS拡張を使ったベクトルの保存と検索の基本的な流れを示します。ここではファイルベースのDuckDBを使い、ベクトルデータとHNSWインデックスの両方をファイルに永続化する設定にしています。

DuckDBを使ったベクトル保存・検索 (duckdb_vss_search.py)
import duckdb
from sentence_transformers import SentenceTransformer

# ① DuckDBの初期化とVSS拡張のロード
con = duckdb.connect("vectors.duckdb")
con.execute("INSTALL vss; LOAD vss;")
# HNSWインデックスをファイルに永続化するための実験的設定
con.execute("SET hnsw_enable_experimental_persistence = true")

# ② テーブルの作成(multilingual-e5-base は768次元)
# スクリプトを複数回実行しても重複しないよう、最初にテーブルを作り直す
con.execute("DROP TABLE IF EXISTS documents")
con.execute("""
    CREATE TABLE documents (
        id INTEGER,
        content TEXT,
        embedding FLOAT[768]
    )
""")

# ③ 保存するドキュメントの準備
documents = [
    "Pythonは汎用プログラミング言語です",
    "機械学習にはPythonがよく使われます",
    "Rustはシステムプログラミング言語です",
    "ベクトル検索はAIアプリケーションの基盤技術です",
    "データベースはデータを永続化するシステムです",
]

# ④ Embeddingの生成(上記text_encoding.pyと同じ)
model = SentenceTransformer("intfloat/multilingual-e5-base")
embeddings = model.encode(
    [f"passage: {doc}" for doc in documents],
    normalize_embeddings=True,
)

# ⑤ ドキュメントとEmbeddingをテーブルに追加
for i, (doc, emb) in enumerate(zip(documents, embeddings)):
    con.execute(
        "INSERT INTO documents VALUES (?, ?, ?)",
        [i, doc, emb.tolist()],
    )
print(f"登録件数: {con.execute('SELECT COUNT(*) FROM documents').fetchone()[0]}")

# ⑥ HNSWインデックスの作成
con.execute("""
    CREATE INDEX IF NOT EXISTS idx_documents_embedding
    ON documents USING HNSW (embedding) WITH (metric = 'cosine')
""")

# ⑦ クエリによる類似検索(検索側は "query: " プレフィックス)
query = "AIと機械学習の関係"
query_embedding = model.encode(
    f"query: {query}", normalize_embeddings=True
).tolist()

results = con.execute("""
    SELECT content, array_cosine_distance(embedding, ?::FLOAT[768]) AS distance
    FROM documents
    ORDER BY distance ASC
    LIMIT 3
""", [query_embedding]).fetchall()

print(f"\nクエリ: 「{query}」")
print("検索結果:")
for content, distance in results:
    similarity = 1 - distance
    print(f"  類似度 {similarity:.4f}: {content}")

con.close()

実行結果は以下のようになります(値は環境により多少変動します⁠⁠。

DuckDBによる検索結果の例
登録件数: 5

クエリ: 「AIと機械学習の関係」
検索結果:
  類似度 0.8482: 機械学習にはPythonがよく使われます
  類似度 0.8311: ベクトル検索はAIアプリケーションの基盤技術です
  類似度 0.8030: Rustはシステムプログラミング言語です

クエリ「AIと機械学習の関係」に対して、機械学習を直接含む文が最上位AIを含むベクトル検索関連の文がその次、という順にランキングできました。キーワード完全一致ではなく意味的な関連性で並んでいることがわかります。

各ステップのポイントを解説します。

  1. この例ではduckdb.connect("vectors.duckdb")でファイルベースのデータベースを作成します。HNSWインデックスをファイルに永続化したいため、この構成にしています。インメモリで試すだけなら、引数なしのduckdb.connect()でも利用できます
    • 補足:hnsw_enable_experimental_persistenceは、HNSWインデックスをファイルに永続化するための実験的な設定です。DuckDB公式ドキュメントでも実験的機能とされているため、クラッシュ時の復旧手順や制約は事前に確認してください
  2. 通常のSQLでテーブルを作成します。ベクトルはFLOAT[次元数]型で定義します(ここではmultilingual-e5-baseの出力に合わせて768次元⁠⁠。同じスクリプトを繰り返し実行しても重複挿入されないよう、冒頭でDROP TABLE IF EXISTSしてから作り直しています
  3. 〜⑤ ドキュメントとそのEmbeddingをSQLのINSERT文で登録します。multilingual-e5系のモデルに合わせて"passage: "プレフィックスを付けnormalize_embeddings=Trueで単位長に正規化しています
  4. HNSWインデックスを作成します。大規模データでの検索高速化に有効です
  5. array_cosine_distance()関数でコサイン距離を計算し、距離の近い順に取得します。クエリ側には"query: "プレフィックスを付けてベクトル化します。metric = 'cosine'のHNSWインデックスもこの形で利用されます。表示時には1 - distanceで類似度に戻しています。SQLで書けるため、WHERE句での絞り込みも自在に組み合わせられます

まとめ

本記事では、Pythonを使ったベクトル検索パイプラインの構築を、以下の流れで解説しました。

  • Embedding:テキストを数値ベクトルに変換する仕組みと、sentence-transformersを使った実装方法を紹介しました。長文を扱うためのチャンキングや、ベクトルの「近さ」を測る指標(コサイン類似度、L2距離、内積)についても整理しました
  • Vector DB:DuckDB(VSS拡張)を使ったローカルでのベクトル保存・検索を実装しました。Qdrant・pgvector・Chroma・sqlite-vecなど、用途に応じた選択肢も比較しました

ベクトル検索はまだまだ進化の途中ですが、道具を選んで組み合わせることで、個人の開発環境でも十分に強力な検索システムを作ることができるようになっています。Embeddingモデルの進化、量子化手法の発展、マルチモーダル対応の拡充など、この分野は変化が非常に速いです。

次のステップ

本記事で扱ったベクトル検索パイプラインは、さらに以下の方向に発展させることができます。

  • RAGパイプラインへの発展:ベクトル検索とLLMを組み合わせ、質問応答システムを構築する
  • ハイブリッド検索:キーワード検索とベクトル検索を組み合わせ、両者の長所を活かした検索システムを構築する
  • 本番環境でのVector DB運用:Qdrantのクラウドサービスや、pgvectorを使った既存PostgreSQL環境への統合
  • Embeddingモデルのファインチューニング:特定ドメインのデータでモデルを調整し、検索精度をさらに向上させる

参考リンク

おすすめ記事

記事・ニュース一覧