NumPyによるSemantic Search
NumPyを使ってクエリのembeddingとドキュメントembeddingの集合のcosine similarityを計算する、純粋なPythonによるsemantic searchシステムを構築します。
「NumPyによるSemantic Search」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
データベースを使わないセマンティック検索
セマンティック検索は、キーワードの一致ではなく意味に基づいて、クエリに最も関連するドキュメントを見つけます。最も簡単な実装では、NumPyを使ってクエリの埋め込みとメモリ上のすべてのドキュメント埋め込みとのコサイン類似度を計算します。外部データベースは必要ありません。
この方法は数万件までのドキュメントで有効に機能し、ベクトルデータベースに投資する前のプロトタイプ作成に適しています。
ドキュメントコーパスの構築
まずドキュメントを収集し、OpenAI APIを使って各ドキュメントにつき1つの埋め込みを生成します。埋め込みは、各行が1つのドキュメントベクトルを表す2次元NumPy配列として保存します。最も近い結果を見つけた後に元の内容を取得できるよう、ドキュメント本文を格納したリストも並行して保持してください。
import numpy as np
from openai import OpenAI
client = OpenAI()
documents = [
'Python is a high-level programming language.',
'NumPy provides fast numerical computing for Python.',
'Embeddings represent text as dense vectors.',
'Cosine similarity measures angle between vectors.',
'RAG combines retrieval with language generation.'
]
response = client.embeddings.create(
model='text-embedding-3-small',
input=documents
)
corpus_embeddings = np.array([item.embedding for item in response.data])
print(f'Corpus shape: {corpus_embeddings.shape}') # (5, 1536)ユーザークエリの埋め込み
ユーザーが検索クエリを送信したら、ドキュメントの埋め込みに使用した同じモデルでクエリを埋め込みます。モデルを混在させると、たとえばドキュメントにはtext-embedding-3-small、クエリにはtext-embedding-3-largeを使うと、異なる空間のベクトルが生成され、意味のない類似度スコアになります。
from openai import OpenAI
import numpy as np
client = OpenAI()
query = 'How do I compute similarity between text?'
response = client.embeddings.create(
model='text-embedding-3-small', # must match corpus model
input=query
)
query_embedding = np.array(response.data[0].embedding)
print(f'Query vector shape: {query_embedding.shape}') # (1536,)NumPyによるコサイン類似度の計算
クエリとすべてのドキュメントとの類似度を1回の処理で求めるには、クエリベクトルとドキュメントベクトルの行列の内積を計算します。OpenAIの埋め込みはどちらもL2正規化されているため、これはすべてのドキュメントに対するコサイン類似度の一括計算と同じです。計算量はO(n * d)で、nはドキュメント数、dは次元数を表します。
import numpy as np
# corpus_embeddings: (n_docs, 1536)
# query_embedding: (1536,)
def semantic_search(query_vec, corpus_vecs):
# Matrix-vector dot product: shape (n_docs,)
similarities = corpus_vecs @ query_vec
return similarities
# Example call (assuming pre-computed embeddings)
# sims = semantic_search(query_embedding, corpus_embeddings)
# print(sims) # array of similarity scores, one per document上位K件の結果のランキングと取得
np.argsortを使って類似度スコアの降順にドキュメントを並べ替え、その後、上位k件のインデックスをスライスで取り出します。これにより最も関連性の高いドキュメントのインデックスが得られるので、並行して保持しているリストから元のテキストを参照できます。
import numpy as np
def get_top_k(query_vec, corpus_vecs, documents, k=3):
similarities = corpus_vecs @ query_vec
# argsort gives ascending order; [::-1] reverses to descending
ranked_indices = np.argsort(similarities)[::-1]
top_k_indices = ranked_indices[:k]
return [
{'text': documents[i], 'score': float(similarities[i])}
for i in top_k_indices
]
# results = get_top_k(query_embedding, corpus_embeddings, documents, k=3)
# for r in results:
# print(f'{r["score"]:.4f}: {r["text"]}')セマンティック検索の完全な例
ここまでの内容をまとめると、コーパスを埋め込み、クエリを埋め込み、類似度を計算し、ランキング順の結果を返します。この一連のパターンは、内部でNumPyの代わりにベクトルデータベースを使う場合でも、すべてのRAG検索ステップの中核となります。
import numpy as np
from openai import OpenAI
client = OpenAI()
docs = [
'Embeddings map text to numerical vectors.',
'Python lists store ordered collections.',
'Cosine similarity compares vector directions.',
'RAG retrieves documents to ground LLM answers.',
'Dictionaries store key-value pairs in Python.'
]
corpus_resp = client.embeddings.create(model='text-embedding-3-small', input=docs)
corpus = np.array([d.embedding for d in corpus_resp.data])
query = 'finding similar text using angles'
q_resp = client.embeddings.create(model='text-embedding-3-small', input=query)
q_vec = np.array(q_resp.data[0].embedding)
scores = corpus @ q_vec
for i in np.argsort(scores)[::-1][:3]:
print(f'{scores[i]:.3f}: {docs[i]}')スコアによるしきい値処理
上位k件の結果がすべて実際に関連しているとは限りません。最も一致する結果であっても、意味的には適合度が低い場合があります。スコアのしきい値を追加して、類似度の低い結果を除外してください。コサイン類似度では通常0.70~0.80程度がしきい値になりますが、実際のクエリを使って対象ドメインに合わせて調整する必要があります。
import numpy as np
def search_with_threshold(query_vec, corpus_vecs, documents, k=5, threshold=0.75):
similarities = corpus_vecs @ query_vec
ranked = np.argsort(similarities)[::-1][:k]
results = []
for i in ranked:
if similarities[i] >= threshold:
results.append({'text': documents[i], 'score': float(similarities[i])})
return results
# Only returns documents above the minimum similarity thresholdNumPy検索のパフォーマンス特性
NumPyによる類似度検索のクエリごとの計算量はO(n * d)です。nはドキュメント数、dは埋め込みの次元数を表します。1536次元の埋め込みの場合:
- 10,000件のドキュメント:最新のCPUでクエリあたり約5ms
- 100,000件のドキュメント:クエリあたり約50ms
- 1,000,000件のドキュメント:約500ms。遅すぎるため、ベクトルデータベースに切り替えてください
NumPyはプロトタイプ作成には非常に優れていますが、近似検索、フィルタリング、永続化は組み込まれていません。
埋め込みのディスクへの永続化
実行するたびに埋め込みを再計算すると、API呼び出しと費用が無駄になります。コーパスが変更された場合にのみ再埋め込みすれば済むように、コーパスの埋め込みとドキュメント本文をディスクに保存してください。
np.saveを使うと埋め込み行列を効率的に保存できます。ドキュメントのリストはJSONとして保存できます。起動時にはAPIを呼び出すのではなく、両方のファイルを読み込んでください。
import numpy as np
import json
# Save
np.save('/tmp/corpus_embeddings.npy', corpus_embeddings)
with open('/tmp/corpus_docs.json', 'w') as f:
json.dump(documents, f)
# Load
corpus_embeddings = np.load('/tmp/corpus_embeddings.npy')
with open('/tmp/corpus_docs.json') as f:
documents = json.load(f)
print(f'Loaded {len(documents)} docs, shape {corpus_embeddings.shape}')新しいドキュメントの段階的な処理
新しいドキュメントが追加されたとき、コーパス全体を再び埋め込む必要はありません。新しいドキュメントだけを埋め込み、np.vstackを使って既存の行列にベクトルを追加します。新しいテキストも同じ順序でドキュメントのリストに追加することを忘れないでください。
import numpy as np
from openai import OpenAI
client = OpenAI()
# Assume these exist from a previous session:
# corpus_embeddings: (n, 1536)
# documents: list of strings
new_docs = ['New document about vector search.']
resp = client.embeddings.create(model='text-embedding-3-small', input=new_docs)
new_vecs = np.array([item.embedding for item in resp.data])
corpus_embeddings = np.vstack([corpus_embeddings, new_vecs])
documents.extend(new_docs)
print(f'Corpus now has {len(documents)} documents')メモリ内検索の制限
NumPyによるセマンティック検索には、専用のベクトルデータベースと比べて大きな制限があります。
- 永続化なし — すべてがRAM上に存在し、再起動すると失われます
- メタデータによるフィルタリングなし — 日付、カテゴリ、作成者で結果を絞り込めません
- 線形スキャンのみ — 近似最近傍インデックスがありません
- 同時アクセス不可 — 複数ユーザー向けの本番環境には適していません
こうした制限があるため、本番のRAGシステムでは専用のベクトルデータベースを使うことになります。
クイックチェック
このレッスンで学んだAIエンジニアリングの概念を理解できているか確認しましょう。
レッスンのまとめ
このレッスンでは、L2正規化された埋め込みに対して行列の内積を計算すると、コーパス全体のコサイン類似度を1回の処理で求められること、np.argsortを反転させると類似度の高い上位k件のドキュメントを取得できること、そしてNumPy検索はプロトタイプに適している一方、永続化とメタデータフィルタリングには対応していないことを学びました。次は、クラスタリングとUMAPを使って、埋め込みの集合からトピックの構造を見つけます。
よくある質問
「NumPyによるSemantic Search」レッスンは無料ですか?
はい。「NumPyによるSemantic Search」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「NumPyによるSemantic Search」で何を学びますか?
NumPyを使ってクエリのembeddingとドキュメントembeddingの集合のcosine similarityを計算する、純粋なPythonによるsemantic searchシステムを構築します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「NumPyによるSemantic Search」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Vector Embeddingとは何か
- OpenAIでEmbeddingを生成する
- NumPyによるSemantic Search
- Embeddingのクラスタリングと可視化