
前回の記事では、Elasticsearchを用いたBM25ベースの全文検索手法について解説しました。
今回はより高度な検索体験を提供するために、ベクトル(Embedding)検索について詳しく説明します。
これにより、ユーザーの意図や文脈を考慮した検索が可能となり、RAG(Retrieval-Augmented Generation)システムの精度向上が期待できます。
1. ベクトル(Embedding)検索とは?
ベクトル検索は、テキストや画像などのデータを高次元の数値ベクトルに変換し、その類似性を計算することで、意味的に関連する情報を検索する手法です。
従来のキーワードベースの検索とは異なり、文脈や意味を考慮した検索が可能となります。
たとえば、
「坊っちゃんが学校へ行った」という文と「生徒が登校した」という文は、
キーワードが異なっていても、ベクトル空間上では近い位置に配置されるため、類似した意味を持つ文として検索されます。
これが、意味検索の強みです。
Elasticsearchでは、dense_vector
フィールドを使用してベクトルを格納し、k近傍法(kNN)などのアルゴリズムを用いて類似度検索を実現します。
2. BM25との比較
BM25は、キーワードの出現頻度や文書の長さを考慮して関連度を計算するアルゴリズムであり、正確なキーワードマッチングに優れています。
一方、ベクトル検索は、文脈や意味の類似性を考慮するため、異なる表現でも関連性の高い情報を取得できます。
以下に、BM25とベクトル検索の主な特徴を比較した表を示します。
特徴 | BM25 | ベクトル検索(Embedding) |
---|---|---|
検索手法 | キーワードベース | 意味・文脈ベース |
類似度の計算 | 単語の出現頻度と逆文書頻度 | コサイン類似度やユークリッド距離 |
同義語の対応 | 同義語(Synonym)辞書が必要 ❌ | (何も考える必要がない) ✅ |
処理速度 | 高速 ✅ | 普通 ❌ |
適用分野 | 正確なキーワード検索 | 意味的な検索、質問応答、推薦システム |
BM25は高速で正確なキーワード検索に適していますが、ベクトル検索はユーザーの意図や文脈を考慮した柔軟な検索が可能です。
どちらにもメリット・デメリットがありますが、
0.5 * BM25 + 0.5 * EmScore
のようにして重み付けをして組み合わせることもできます。
これを (BM25とEmbeddingの)Hybrid Search と呼びます。
3. Embeddingの計算方法(OpenAI API)
ベクトル検索を行うには、まず文やクエリを「ベクトル」に変換する必要があります。
以下は、OpenAIの text-embedding-3-small
を使ってベクトルを取得する方法です
import openai
import os
openai.api_key = os.getenv("OPENAI_API_KEY")
def get_embedding(text: str) -> list[float]:
response = openai.embeddings.create(
input=text,
model="text-embedding-3-small"
)
return response.data[0].embedding
取得したベクトルは、Elasticsearchに格納することで意味検索に利用できます。
4. 実践例:elasticsearch-dslでEmbedding検索
ここでは、Pythonの elasticsearch-dsl
を使って、Embedding検索を実装します。
4.1 インデックスの作成
まず、ベクトル格納用の dense_vector
フィールドを含むマッピングを新しく定義します。
from elasticsearch_dsl import Document, Text, DenseVector, connections
connections.create_connection(hosts=["localhost"])
class BotchanLineWithEmbedding(Document):
line = Text(analyzer=ja_analyzer)
line_embedding = DenseVector(dims=1536)
line_number = Integer()
class Index:
name = INDEX_NAME
4.2 インデックス登録
Embeddingを取得し、各文書に格納します。
docs = []
with open(TEXT_DATA_PATH, "r") as f:
for i, line in tqdm(enumerate(f)):
line = line.strip()
if len(line) > 0:
emb = get_embedding(line)
doc = BotchanLineWithEmbedding(meta={"id": i}, line=line, line_number=i, line_embedding=emb)
docs.append(doc.to_dict(include_meta=True))
bulk(es, docs, index=INDEX_NAME)
4.3 検索クエリの実行
次に、ユーザーの検索クエリに対してベクトルを生成し、cosine類似度を使って検索します。
from elasticsearch_dsl import Search
from elasticsearch_dsl import connections
client = connections.create_connection(hosts=["http://localhost:9200"], timeout=20)
query_text = "激おこぷんぷん丸"
query_vector = get_embedding(query_text)
s = Search(using=client, index="botchan").query(
"script_score",
query={"match_all": {}},
script={
"source": "cosineSimilarity(params.query_vector, 'line_embedding')",
"params": {"query_vector": query_vector}
}
)
response = s.execute()
for hit in response:
print(f"{hit.meta.score:.2f}: {hit.line}")
実行結果は以下のようになります:
0.31: おやじはちっともおれを可愛かわいがってくれなかった。 ... 乱暴で乱暴で ...
激おこですね…😡
おわりに
ここまではElasticSearchの基礎的な使い方、環境構築の方法を紹介してきました。
また、キーワードの精度と意味検索の柔軟性を両立させることで、RAGのための性能向上が期待できることも分かりました。
次回はいよいよ、langchainを使ったRAGを実践してみましょう。
お疲れ様でした!
コメント