LlamaIndex v0.10のQueryEngineを使ってハイブリッド検索をやってみる
2024/05/14
こんにちは。今回は,前回使用したQueryEngineを使ってハイブリッド検索実装にトライします。
今回はこちらのドキュメントをベースに紹介していきます。
はじめに
ここでは,ハイブリッド検索の非常に単純なバージョンを定義する方法を紹介します.
「AND」や「OR」条件を使用して,キーワードルックアップ検索とベクトル検索を組み合わせます.
今回は,日本語に対応させるために同じドキュメントに対して2つのノードを作成します.
設定
LlamaIndexのインストール
!pip install llama-index
sudachiのインストール
今回は日本語トークナイザーにsudachiを使用します.mecabも有名です.
!pip install -q sudachipy
!pip install -q sudachidict_core
import os
os.environ["OPENAI_API_KEY"] = "sk-"
データのダウンロード
トヨタのパンフレットPDFを読み込みます.
import requests
import os
def download_file(url, directory, filename):
# ディレクトリが存在しない場合は作成
if not os.path.exists(directory):
os.makedirs(directory)
# ファイルのパスを構築
file_path = os.path.join(directory, filename)
# ファイルをダウンロード
response = requests.get(url)
response.raise_for_status() # ステータスコードが200以外の場合はエラーを発生させる
# ファイルを保存
with open(file_path, 'wb') as file:
file.write(response.content)
print(f"ファイルがダウンロードされ、{file_path}に保存されました。")
# 使用例
url = 'https://toyota.jp/pages/contents/corollasport/001_p_001/pdf/spec/corollasport_spec_201806.pdf' # ダウンロードしたいファイルのURL
directory = 'data' # 保存したいディレクトリのパス
filename = 'corollasport_spec_old.pdf' # 保存するファイルの名前
download_file(url, directory, filename)
url = 'https://toyota.jp/pages/contents/corollasport/001_p_001/5.0/pdf/spec/corollasport_spec_202404.pdf'
filename = 'corollasport_spec_new.pdf'
download_file(url, directory, filename)
url = 'https://toyota.jp/pages/contents/corollatouring/001_p_001/5.0/pdf/spec/corollatouring_spec_202404.pdf'
filename = 'corollatouring_spec_new.pdf'
download_file(url, directory, filename)
データの読み込み
ドキュメントを読み込みます
# LlamaIndexのインポート
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
# dataフォルダ内の学習データを使い、インデックスを生成する
documents = SimpleDirectoryReader('data').load_data()
ノードの作成
from llama_index.core import Settings
nodes = Settings.node_parser.get_nodes_from_documents(documents)
ストレージコンテキストを作成します
from llama_index.core import StorageContext
# initialize storage context (by default it's in-memory)
storage_context = StorageContext.from_defaults()
storage_context.docstore.add_documents(nodes)
ドキュメントのトークナイズ
ここで,キーワード検索に使用するためにノードを処理します.
# sudachi
from sudachipy import tokenizer
from sudachipy import dictionary
# トークナイザーの設定
tokenizer_obj = dictionary.Dictionary().create()
# トークナイズの関数
def tokenize_text(text):
# モード設定: A - 短い形、B - 中間形、C - 長い形
mode = tokenizer.Tokenizer.SplitMode.C
# トークン化
tokens = tokenizer_obj.tokenize(text, mode)
# 表層形を取得してスペースで結合
tokenized_text = ' '.join([token.surface() for token in tokens])
return tokenized_text
整形に使用する関数の定義
import re
# 日本語の間にあるスペースを削除する
def rm_space(text):
# Regex pattern to match spaces where both sides are Japanese characters using specific Unicode ranges
# Hiragana (U+3040-U+309F), Katakana (U+30A0-U+30FF), and common Kanji (U+4E00-U+9FAF)
return re.sub(r'(?<=[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF])\s+(?=[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF])', '', text)
# 連続するスペースを削除する
def rm_multi_space(text):
return re.sub(r'\s{2,}', ' ', text)
ノードをコピーしてキーワード検索ノードを作成
tnodes = nodes.copy()
for node in tnodes:
node.text = rm_multi_space(tokenize_text(rm_space(node.text)))
tnodes[0].text
同じデータに対するベクトル インデックスとキーワード テーブル インデックスを定義する
from llama_index.core import SimpleKeywordTableIndex, VectorStoreIndex
vector_index = VectorStoreIndex(nodes, storage_context=storage_context)
keyword_index = SimpleKeywordTableIndex(tnodes, storage_context=storage_context)
カスタムレトリーバーの定義
# import QueryBundle
from llama_index.core import QueryBundle
# import NodeWithScore
from llama_index.core.schema import NodeWithScore
# Retrievers
from llama_index.core.retrievers import (
BaseRetriever,
VectorIndexRetriever,
KeywordTableSimpleRetriever,
)
from typing import List
カスタム検索器の定義
class CustomRetriever(BaseRetriever):
"""Custom retriever that performs both semantic search and hybrid search."""
def __init__(
self,
vector_retriever: VectorIndexRetriever,
keyword_retriever: KeywordTableSimpleRetriever,
mode: str = "AND",
) -> None:
"""Init params."""
self._vector_retriever = vector_retriever
self._keyword_retriever = keyword_retriever
if mode not in ("AND", "OR"):
raise ValueError("Invalid mode.")
self._mode = mode
super().__init__()
def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
"""Retrieve nodes given query."""
vector_nodes = self._vector_retriever.retrieve(query_bundle)
keyword_nodes = self._keyword_retriever.retrieve(query_bundle)
vector_ids = {n.node.node_id for n in vector_nodes}
keyword_ids = {n.node.node_id for n in keyword_nodes}
combined_dict = {n.node.node_id: n for n in vector_nodes}
combined_dict.update({n.node.node_id: n for n in keyword_nodes})
if self._mode == "AND":
retrieve_ids = vector_ids.intersection(keyword_ids)
else:
retrieve_ids = vector_ids.union(keyword_ids)
retrieve_nodes = [combined_dict[rid] for rid in retrieve_ids]
return retrieve_nodes
検索器をプラグインに入れるクエリエンジン
from llama_index.core import get_response_synthesizer
from llama_index.core.query_engine import RetrieverQueryEngine
# define custom retriever
vector_retriever = VectorIndexRetriever(index=vector_index, similarity_top_k=2)
keyword_retriever = KeywordTableSimpleRetriever(index=keyword_index)
custom_retriever = CustomRetriever(vector_retriever, keyword_retriever)
# define response synthesizer
response_synthesizer = get_response_synthesizer()
# assemble query engine
custom_query_engine = RetrieverQueryEngine(
retriever=custom_retriever,
response_synthesizer=response_synthesizer,
)
# vector query engine
vector_query_engine = RetrieverQueryEngine(
retriever=vector_retriever,
response_synthesizer=response_synthesizer,
)
# keyword query engine
keyword_query_engine = RetrieverQueryEngine(
retriever=keyword_retriever,
response_synthesizer=response_synthesizer,
)
実行
query_text = "カローラスポーツはどんな車ですか?"
query = tokenize_text(query_text)
response = custom_query_engine.query(query)
print(response)
今回はクエリを入力しても空の回答が返ってきました.
これらのコードはこちらで実行できます.
さらに,キーワード検索をBM25検索器を用いた実装も行いました.
まとめ
CustomRetrieverクラスを作成することで任意のハイブリッド検索器を定義できる
それでは、ご覧いただきありがとうございました。
この記事が気に入ったらサポートをしてみませんか?