2026年4月: 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)。
キーワード検索:
「EV」 ─── 完全一致 ───▶ ✕ 「電気自動車は…」にはヒットしない
ベクトル検索:
「EV」 ─→ Embedding ─→ [0.12, -0.03, 0.87, ...]
│
同じ空間で意味的に近いものを探す
▼
✓ 「電気自動車は環境にやさしい移動手段です」にヒット
flowchart TB
subgraph keyword["キーワード検索"]
direction LR
k1["検索クエリ「EV」"] --> k2["完全一致"]
k2 --> k3["✕「電気自動車は...」にはヒットしない"]
end
subgraph vector["ベクトル検索"]
direction LR
v1["検索クエリ「EV」"] --> v2["Embedding<br/>[0.12, -0.03, 0.87, ...]"]
v2 --> v3["意味的に近いものを探す"]
v3 --> v4["✓「電気自動車は環境にやさしい移動手段です」にヒット"]
end
keyword ~~~ vector

図1のように、キーワード検索は文字列の一致に依存するのに対して、ベクトル検索はテキストを数値ベクトルに変換し、ベクトル空間上の近さで判断するため、表現の揺れを越えて関連する文書を拾えます。
最近のトレンドは、両者の長所を組み合わせたハイブリッド検索です。本記事では、ベクトル検索の基礎をしっかり固めることに集中し、ハイブリッド検索については「まとめ」で次のステップとして触れます。
ベクトル検索とは何か¶
ベクトル検索は、テキストや画像などのデータを 高次元の数値ベクトル に変換し、ベクトル空間上の距離(コサイン類似度など)で類似度を測る仕組みです。「意味が近いものはベクトルも近い」という性質を利用することで、キーワードの一致に依存しない検索を実現できます。
ベクトル検索システムを構築するには、大きく分けて以下の4つのステップが必要です。
Embedding化(ベクトル化): 検索対象のドキュメント(テキスト、画像など)を、意味内容を表す数値の配列(ベクトル)に変換する。この処理を行うのがEmbeddingモデルと呼ばれる機械学習モデルです
ベクトルの永続化: 生成されたベクトルデータを、後で検索可能な形式で保存する。一般的にはベクトルデータベース(Vector DB)を利用する
クエリのEmbedding化: ユーザーが入力した検索クエリを、同じモデルを使ってベクトルに変換し、クエリベクトルを生成
ベクトルの近傍検索: クエリベクトルと距離が近い(意味が似ている)ドキュメントベクトルを上記2項の永続化されたベクトルデータから探し出す
この4ステップを、「インデックス作成時(事前に1回)」と「検索時(クエリごと)」に分けて図示すると図2のようになります。
【インデックス作成時(事前に1回)】
ドキュメント群(ステップ1) ─→ [Embeddingモデル](ステップ1) ─→ ベクトル群(ステップ1) ─→ [Vector DB]
│ (永続化(ステップ2))
│ 同じモデルを使う
▼
【検索時(クエリごと)】
検索クエリ(ステップ3) ─→ [Embeddingモデル](ステップ3) ─→ クエリベクトル(ステップ3)
│
▼
[Vector DB]
近傍検索(ANN など)(ステップ4)
│
▼
類似ドキュメント
flowchart TB
subgraph index["インデックス作成時(事前に1回)"]
direction LR
subgraph step1["ステップ1: Embedding化(ドキュメント)"]
direction LR
documents["ドキュメント群"] --> embed_docs["Embeddingモデル"]
embed_docs --> doc_vectors["ベクトル群"]
end
subgraph step2["ステップ2: ベクトルの永続化"]
direction LR
vector_db[("Vector DB")]
end
doc_vectors --> vector_db
end
subgraph search["検索時(クエリごと)"]
direction LR
subgraph step3["ステップ3: Embedding化"]
direction LR
query["検索クエリ"] --> embed_query["Embeddingモデル"]
embed_query --> query_vector["クエリベクトル"]
end
subgraph step4["ステップ4: ベクトルの近傍検索"]
direction LR
ann["近傍検索(ANNなど)"] --> results["類似ドキュメント"]
end
query_vector --> vector_db
vector_db --> ann
end
index ~~~ search
embed_docs -. "同じモデルを使う" .- embed_query

図2のポイントは、インデックス作成時と検索時で同じEmbeddingモデルを使うことと、Vector DBが近傍検索の効率化を担うことの2点です。後半のコード例もこの構造に沿って実装していきます。
実務での活用場面¶
ベクトル検索が実務で活用される代表的な場面を挙げます。
RAG(検索拡張生成): LLMに渡すコンテキストとして、質問に意味的に近いドキュメントを検索する
類似商品検索: ECサイトで「この商品に似たもの」を提示する
ドキュメント検索: 社内ナレッジベースから関連する文書を探す
画像検索: テキストで画像を検索したり、画像で似た画像を探したりする
Pythonの役割¶
Embedding生成・データ処理・評価・インフラ連携のすべての工程において、Pythonは事実上の標準言語として機能しています。Hugging Face Transformers、sentence-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 |
|
5.4.1 |
テキストEmbedding生成 |
|
1.5.2 |
ベクトルの保存・類似度検索 |
|
2.4.4 |
パッケージのインストール¶
uv を使う場合:
uv add sentence-transformers duckdb numpy
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モデルでベクトル化し、出力されるベクトルの次元数を比較します。
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()}")
(略)
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を使った計算例を示します。
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つの文をドキュメントとしてベクトル化しておき、あとから入力した短い文(単語でも可)ともっとも意味的に近いものを探します。
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}")
実行結果は以下のようになります(値は環境により多少変動します)。
ベクトルの形状: (4, 768)
検索クエリ: 「EV」
--------------------------------------------------
類似度 0.8149: 電気自動車は環境にやさしい移動手段です
類似度 0.7931: バッテリー駆動の新しい乗用車が増えています
類似度 0.7715: 洗濯機は衣類を洗うための家電です
類似度 0.7397: 今日は良い天気です
「EV」という文字列はどのドキュメントにも含まれていないにもかかわらず、「電気自動車」や「バッテリー駆動の乗用車」が上位に来ています。キーワード一致では実現できない、ベクトル検索ならではの挙動が確認できます。
各ステップのポイントを説明します。
①
SentenceTransformerにモデル名を渡すだけでロードできます。初回実行時はモデルがダウンロードされます(multilingual-e5-baseは約1.1GB)② 検索対象のドキュメントを用意します
③
model.encode()にリストを渡すとまとめてベクトル化されます。normalize_embeddings=Trueを指定すると単位長に正規化され、以降は内積がそのままコサイン類似度になります。戻り値は(ドキュメント数, 次元数)のNumPy配列です④ 検索クエリも同じモデルでベクトル化します
⑤ 行列積
@で全ドキュメントとの類似度を一括計算し、降順に並べて表示します
コード中の "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)。
chunk_size = 200, overlap = 30 で長文を分割する例
位置: 0 170 200 340 370 540
│ │ │ │ │ │
┌─────────────────────┐
│ チャンク0 │ (文字位置 0〜200)
└─────────────────────┘
┌─────────────────────┐
│ チャンク1 │ (文字位置 170〜370)
└─────────────────────┘
┌─────────────────────┐
│ チャンク2 │ (文字位置 340〜540)
└─────────────────────┘
←── 開始位置は step = chunk_size − overlap = 170 ずつ進む ──→
★ 位置 170〜200 の30文字 → チャンク0 の末尾 = チャンク1 の先頭
★ 位置 340〜370 の30文字 → チャンク1 の末尾 = チャンク2 の先頭
境界で文が切れても、前後どちらかのチャンクに完全な形で含まれるようにする狙い
block-beta
columns 10
p0["0-60"] p1["60-120"] p2["120-170"] p3["170-200"] p4["200-260"] p5["260-320"] p6["320-340"] p7["340-370"] p8["370-450"] p9["450-540"]
c0["チャンク0<br/>0〜200"]:4 space:6
space:3 c1["チャンク1<br/>170〜370"]:5 space:2
space:7 c2["チャンク2<br/>340〜540"]:3
space:3 o0["重なり<br/>170〜200"] space:3 o1["重なり<br/>340〜370"] space:2

図3のように、chunk_size 分だけ切り出したら、次のチャンクは overlap 分戻った位置から始める、という動きを繰り返していきます。以下は、この考え方をそのまま実装したシンプルな例です。
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はシンプルな文法と豊富なライブラリが特徴の...
各ステップのポイントです。
①
chunk_text()では、指定した文字数ごとに切り出しつつ、チャンクの境界で意味がぶつ切りになるのを避けるためにオーバーラップ(重なり)を設けています。chunk_sizeとoverlapは、対象文書の性質や利用するモデルの入力上限に合わせて調整してください② チャンクは検索対象のドキュメント扱いなので、
multilingual-e5の流儀に従い"passage: "プレフィックスを付けます③ 検索クエリ側には
"query: "プレフィックスを付けます。同じモデルで両者をベクトル化することで、同一空間上で比較できます④ 正規化済みベクトルでは行列積(
@)だけで全チャンクとのコサイン類似度をまとめて計算できます
この例では固定文字数で素朴に分割していますが、実務では段落や句点単位で区切る、文書の構造(見出し)に沿って分ける、などの工夫がよく使われます。LangChainやLlamaIndexには洗練されたチャンカーが用意されているので、本格的なRAGを構築する際にはそれらも検討してみてください。
【コラム】 外部APIの活用例¶
ローカルモデルではなく、外部APIを使ってEmbeddingを生成することもできます。OpenAI Embeddings APIを使う場合は以下のようになります。
この例を試す場合は、あらかじめ uv add openai または pip install openai で追加してください。
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY環境変数が必要で有料での利用になります
response = client.embeddings.create(
model="text-embedding-3-small",
input="EVはバッテリーで駆動する乗用車です",
)
embedding = response.data[0].embedding
print(f"ベクトルの次元数: {len(embedding)}") # → 1536
【コラム】 Ollamaの活用例¶
Ollama は、ローカル環境でLLMを動かすためのツールです。Embeddingの生成にも対応しており、データをクラウドに送らずに処理できるため、プライバシーが重要な場面で重宝します。Ollamaサーバーを起動した状態で、以下のように利用できます。
この例を試す場合は、あらかじめ uv add ollama または pip install ollama でPythonクライアントを追加してください。また、ollama serve などでローカルのOllamaサーバーを起動し、必要なモデルを ollama pull nomic-embed-text などで取得しておく必要があります。
import ollama
response = ollama.embed(
model="nomic-embed-text",
input="EVはバッテリーで駆動する乗用車です",
)
embedding = response["embeddings"][0]
print(f"ベクトルの次元数: {len(embedding)}") # → 768
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-m3 や jinaai/jina-embeddings-v3、cl-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モデルでは、入力テキストに プレフィックス(接頭辞) を付ける必要があります。たとえば multilingual-e5 シリーズでは、検索クエリには "query: " を、検索対象のドキュメントには "passage: " を先頭に付けます。
query_text = "query: EVの最新動向"
doc_text = "passage: 電気自動車市場は急速に拡大しています"
nomic-ai/nomic-embed-text-v2-moe では "search_query: " と "search_document: " が必要です。検索向けのEmbeddingモデルでは、multilingual-e5 や Nomic 系だけでなく、Ruri、EmbeddingGemma、Qwen のようにクエリ用・文書用のプロンプトや指示文を前提とするものもあります。こうしたプレフィックスや指示文を付け忘れると検索精度が低下するため、モデルごとのドキュメントを必ず確認しましょう。一方で、sentence-transformers/all-MiniLM-L6-v2 のような英語向けの軽量モデルなど、プレフィックスが不要なモデルもあります。どちらの流儀かはHuggingFaceサイトのModel cardタブで確認してから使いましょう。
ベクトルデータベースの種類と選択基準¶
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インデックスの両方をファイルに永続化する設定にしています。
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()
実行結果は以下のようになります(値は環境により多少変動します)。
登録件数: 5
クエリ: 「AIと機械学習の関係」
検索結果:
類似度 0.8482: 機械学習にはPythonがよく使われます
類似度 0.8311: ベクトル検索はAIアプリケーションの基盤技術です
類似度 0.8030: Rustはシステムプログラミング言語です
クエリ「AIと機械学習の関係」に対して、機械学習 を直接含む文が最上位、AI を含むベクトル検索関連の文がその次、という順にランキングできました。キーワード完全一致ではなく意味的な関連性で並んでいることがわかります。
各ステップのポイントを解説します。
① この例では
duckdb.connect("vectors.duckdb")でファイルベースのデータベースを作成します。HNSWインデックスをファイルに永続化したいため、この構成にしています。インメモリで試すだけなら、引数なしのduckdb.connect()でも利用できます補足:
hnsw_enable_experimental_persistenceは、HNSWインデックスをファイルに永続化するための実験的な設定です。DuckDB公式ドキュメントでも実験的機能とされているため、クラッシュ時の復旧手順や制約は事前に確認してください
② 通常のSQLでテーブルを作成します。ベクトルは
FLOAT[次元数]型で定義します(ここではmultilingual-e5-baseの出力に合わせて768次元)。同じスクリプトを繰り返し実行しても重複挿入されないよう、冒頭でDROP TABLE IF EXISTSしてから作り直しています③〜⑤ ドキュメントとそのEmbeddingをSQLのINSERT文で登録します。
multilingual-e5系のモデルに合わせて"passage: "プレフィックスを付け、normalize_embeddings=Trueで単位長に正規化しています⑥ HNSWインデックスを作成します。大規模データでの検索高速化に有効です
⑦
array_cosine_distance()関数でコサイン距離を計算し、距離の近い順に取得します。クエリ側には"query: "プレフィックスを付けてベクトル化します。metric = 'cosine'のHNSWインデックスもこの形で利用されます。表示時には1 - distanceで類似度に戻しています。SQLで書けるため、WHERE句での絞り込みも自在に組み合わせられます
【コラム】 SQLでベクトル検索を行うメリット¶
DuckDBやpgvectorのようにSQLでベクトル検索が書けると、メタデータによるフィルタリング(WHERE句)と類似度検索を1つのクエリで組み合わせられます。たとえば「2024年以降の記事で、このクエリに意味的に近いもの」といった検索が自然に記述できます。SQLに慣れている開発者にとってこのアプローチは非常に取り組みやすいと感じています。
まとめ¶
本記事では、Pythonを使ったベクトル検索パイプラインの構築を、以下の流れで解説しました。
Embedding: テキストを数値ベクトルに変換する仕組みと、
sentence-transformersを使った実装方法を紹介しました。長文を扱うためのチャンキングや、ベクトルの「近さ」を測る指標(コサイン類似度、L2距離、内積)についても整理しましたVector DB: DuckDB(VSS拡張)を使ったローカルでのベクトル保存・検索を実装しました。Qdrant・pgvector・Chroma・sqlite-vecなど、用途に応じた選択肢も比較しました
ベクトル検索はまだまだ進化の途中ですが、道具を選んで組み合わせることで、個人の開発環境でも十分に強力な検索システムを作ることができるようになっています。Embeddingモデルの進化、量子化手法の発展、マルチモーダル対応の拡充など、この分野は変化が非常に速いです。
次のステップ¶
本記事で扱ったベクトル検索パイプラインは、さらに以下の方向に発展させることができます。
RAGパイプラインへの発展: ベクトル検索とLLMを組み合わせ、質問応答システムを構築する
ハイブリッド検索: キーワード検索とベクトル検索を組み合わせ、両者の長所を活かした検索システムを構築する
本番環境でのVector DB運用: Qdrantのクラウドサービスや、pgvectorを使った既存PostgreSQL環境への統合
Embeddingモデルのファインチューニング: 特定ドメインのデータでモデルを調整し、検索精度をさらに向上させる