LlamaIndex、LangChain、Milvusを使った複数ドキュメントのクエリ
LLMのパワーを解き放つ: LlamaIndex、LangChain、Milvus Vector Databaseで複数の文書を結合し、クエリする
この記事はMLOpsに掲載されたものを許可を得て再掲載しています。
大規模言語モデル(LLM)は個人的なプロジェクトでは人気があるが、実運用ではどのように使えばいいのだろうか?GPTのようなLLMを使ってドキュメントをクエリすることは、すぐに役立つユースケースの1つです。1つの文書にクエリーするだけでは不十分で、複数の文書にクエリーする必要があります。 LlamaIndexとMilvusは、Jupyter Notebookで始めるのに役立ちます。CoLab Notebook](https://colab.research.google.com/drive/10vhji3FPOAm43zAvjOF4dlgxAeqIkHDx?usp=sharing)へのリンクはこちら。
このチュートリアルでは
- 複数ドキュメントのクエリのためのJupyter Notebookのセットアップ
- LlamaIndexを使ったドキュメントクエリーエンジンの構築
- ベクターデータベースの起動
- ドキュメントの収集
- LlamaIndexでドキュメントインデックスを作成する
- ドキュメントに対する分解可能なクエリ
- 非分解クエリとの比較
- LlamaIndex を使った複数ドキュメントのクエリ方法のまとめ
複数ドキュメントのクエリ用にJupyterノートブックをセットアップする
はじめに、ライブラリをセットアップする必要がある。このコードを実行するには7つのライブラリが必要です:llama-index、nltk、milvus、pymilvus、langchain、python-dotenv、openaiである。llama-index、nltk、langchain、openai ライブラリは、LLM に接続してクエリを実行するのに役立つ。
pymilvusとmilvusライブラリは [ベクトルデータベース](https://zilliz.com/learn/what-is-vector-database) 用で、python-dotenvは環境変数の管理用である。また、分解可能なクエリを作成する前にNLTKをセットアップする必要がある。以下のコードは、NLTKからstopwords` モジュールをダウンロードする方法と、よく出るSSLエラーを回避するコードを示している。
python pip install llama-index nltk milvus pymilvus langchain python-dotenv openai インポート nltk インポート ssl
を試す:
create_unverified_https_context = ssl._create_unverified_context except AttributeError: パス else:
ssl._create_default_https_context = _create_unverified_https_context
nltk.download("stopwords")
## LlamaIndex を使ったドキュメントクエリーエンジンの構築
さて、Jupyter Notebookをセットアップしたので、次はドキュメントクエリーエンジンを構築しよう。ここにはたくさんのインポートがある。まず、ベクトル・インデックスとキーワード・テーブル・インデックスの2つのインデックスが必要だ。次に、ディレクトリからデータを読み込むために`SimpleDirectoryReader`が必要だ。
三つ目は、LLMを使う方法である。これには `LLMPredictor` を使う。第四に、サービスコンテキストとストレージコンテキストの2つのコンテキストが必要である。これらは今のところ基本的なLlamaIndexのインポートである。最後に、[LangChain](https://zilliz.com/blog/langchain-ultimate-guide-getting-started)からOpenAIChatツールをインポートする必要がある。
python
from llama_index import (
GPTVectorStoreIndex、
GPTSimpleKeywordTableIndex、
SimpleDirectoryReader、
LLMPredictor、
ServiceContext、
ストレージコンテキスト
)
from langchain.llms.openai import OpenAIChat
ミニLLMアプリケーションをビルドするためにノートブックを準備する最後のステップは、LLMへのアクセスを取得することだ。OpenAIのウェブサイトから取得できるAPIキーを .env ファイルにロードした。load_dotenv()を使って.envファイルをロードし、openaiのapi_key` パラメータにロードしたキーをセットする。
python import os from dotenv import load_dotenv import openai load_dotenv() openai.api_key = os.getenv("OPENAI_API_KEY")
#### ベクターデータベースの起動
ノートブックのセットアップが完了したら、いよいよアプリの動作部分のコードをまとめ始める。ステップ1では、ベクターデータベースを立ち上げる。milvus`のベクターデータベースサーバーとLlamaIndexの`MilvusVectorStore`クラスが必要である。default_server` を起動し、`MilvusVectorStore` オブジェクトにホスト(localhost)とポートを渡して接続する。
python
from llama_index.vector_stores import MilvusVectorStore
from milvus import default_server
default_server.start()
vector_store = MilvusVectorStore(
host = "127.0.0.1"、
ポート = default_server.listen_port
)
ドキュメントの収集
クエリーの対象となる文書を集めよう。この例では、ウィキペディアから大都市について学ぶことにする。トロント、シアトル、サンフランシスコ、シカゴ、ボストン、ワシントンDC、ケンブリッジ(マサチューセッツ州)、ヒューストンに関するウィキペディアの記事を使う。
ウィキペディアのドキュメントを取得するために requests ライブラリを使用する。まず、すべてのリストのタイトルをループするforループを作ることから始める。次に、これらのタイトルそれぞれについて、Wikipedia APIに GET リクエストを送信する。各リクエストには action、format、titles、prop、explaintext の5つのパラメータが必要である。
ウィキペディアの記事のテキストはすべて extract キーの下にある。そこで、すべてのページをまとめて、すべてのテキストを含む1つの変数にロードする。そして、すべてのテキストを手に入れたら、ファイルをローカル・フォルダに書き込み、それぞれに適切なタイトルをつける。
``python wiki_titles = ["Toronto", "Seattle", "San Francisco", "Chicago", "Boston"、 "ワシントンD.C.", "マサチューセッツ州ケンブリッジ", "ヒューストン"]]。
from pathlib import Path
import requests for title in wiki_titles: response = requests.get( 'https://en.wikipedia.org/w/api.php'、 params={ 'action': 'query'、 'format': 'json'、 'titles': タイトル、 'prop':'extracts'、 'explaintext':真、 } ).json() page = next(iter(response['query']['pages'].values())) wiki_text = page['extract'].
data_path = Path('data') if not data_path.exists(): Path.mkdir(data_path)
with open(data_path / f"{title}.txt", 'w') as fp: fp.write(wiki_text)
#### LlamaIndexでドキュメントインデックスを作成する
これですべてのデータが揃ったので、次はLlamaIndexを使って検索用のインデックスを作成します。まず、都市に関するすべての文書を格納する空の辞書を作成します。次に、上記の各タイトルをループして、ファイルを読み込む。
``python
# 全てのwikiドキュメントを読み込む
city_docs = {} とする。
for wiki_title in wiki_titles:
city_docs[wiki_title] = SimpleDirectoryReader(input_files=[f "data/{wiki_title}.txt"]).load_data()
次に、LLM Predictorをインスタンス化します。これにはGPT 3.5 TurboをLangChainのOpenAIChatオブジェクトを通して使います。また、インデックス用にサービスコンテキストとストレージコンテキストの2つのコンテキストを作成する必要があります。サービスコンテキストはLLM予測モデルから、ストレージコンテキストは先ほど作成したMilvusベクトルストアから作成します。
次に、都市インデックスとサマリー用の空の辞書を2つ作成する。もう一度、上で選んだ都市をループする。今回は、先ほど読み込んだ各ドキュメントからGPTVectorStoreIndexオブジェクトを作成し、それを都市インデックスに保存し、メタ記述をサマリー辞書に保存する。
パイソン llm_predictor_chatgpt = LLMPredictor(llm=OpenAIChat(temperature=0, model_name="gpt-3.5-turbo")) service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor_chatgpt) storage_context = StorageContext.from_defaults(vector_store=vector_store)
都市文書インデックスを構築する
city_indices = {} # 都市文書インデックスを構築 index_summaries = {} for wiki_title in wiki_titles: city_indices[wiki_title]は次のようになります。 GPTVectorStoreIndex.from_documents(city_docs[wiki_title]、 service_context=service_context, storage_context=storage_context)
都市の要約テキストを設定
index_summaries[wiki_title] = f"{wiki_title}に関するウィキペディアの記事"
#### ドキュメントに対する分解可能なクエリ
複数のドキュメントを比較対照する鍵は、"分解可能なクエリー "を使うことです。分解可能なクエリとは、より小さな部分に分解できるクエリのことである。まず、LlamaIndexから `ComposableGraph` オブジェクトをインポートする。そして、いくつかのインデックスから分解可能なグラフを作成する。
この例では、キーワードテーブルインデックス、都市インデックスからのインデックスのリスト、そして "summary "インデックスからのインデックスのリストを使用している。また、チャンク内のキーワードの最大数をパラメータで指定することもできる。次に、`DecomposeQueryTransform`オブジェクトをインポートし、先ほど作成したChatGPT LLM Predictorを使用してクエリを変換、分解できるオブジェクトを作成します。
python
from llama_index.indices.composability import ComposableGraph
graph = ComposableGraph.from_indices(
GPTSimpleKeywordTableIndex、
[index for _, index in city_indices.items()]、
[summary for _, summary in index_summaries.items()]、
max_keywords_per_chunk=50
)
from llama_index.indices.query.query_transform.base import DecomposeQueryTransform
decompose_transform = DecomposeQueryTransform(
llm_predictor_chatgpt, verbose=True
)
次に、クエリの変換を行うエンジンを作成します。そのために、まず TransformQueryEngine をインポートします。次に、クエリエンジンのマッピングを保持するために空の辞書が必要です。次に、都市インデックスの各インデックスをループします。
インデックス as_query_engine を使用して、これらのインデックスごとにクエリエンジンを作成する。また、作成したサマリーインデックスを使用して、いくつかの追加情報を追加する。最後に、 TransformQueryEngine オブジェクトと DecomposeQueryTransform オブジェクトに、クエリエンジンとしてのインデックスと追加情報としてのサマリーインデックスを渡して、カスタムクエリエンジンを作成する。
python from llama_index.query_engine.transform_query_engine import TransformQueryEngine custom_query_engines = {} とする。 for index in city_indices.values(): query_engine = index.as_query_engine(service_context=service_context) 変換_extra_info = {'index_summary': index.index_struct.summary}. tranformed_query_engine = TransformQueryEngine(query_engine, decompose_transform、 transform_extra_info=transform_extra_info) custom_query_engines[index.index_id] = tranformed_query_engine
カスタムクエリーエンジンの作成がまだ1セット残っています。上で作成した分解可能グラフのルートを項目として追加し、要約モードで使用します。最後に、上記で作成したカスタムクエリーエンジンの辞書を使用するクエリーエンジンをグラフから作成します。
``python
custom_query_engines[graph.root_index.index_id] = graph.root_index.as_query_engine(
retriever_mode='simple'、
response_mode='tree_summarize'、
service_context=service_context
)
query_engine_decompose = graph.as_query_engine(
custom_query_engines=custom_query_engines,)
これで、これらすべてのドキュメントに対してクエリーを実行する準備ができた。この例では、LLMアプリにシアトル、ヒューストン、トロントの空港を比較対照するよう依頼します。先ほど、クエリの分解を冗長にして、アプリがどのようにクエリを分解しているかを確認できるようにしました。
``python response_chatgpt = query_engine_decompose.query( "シアトル、ヒューストン、トロントの空港を比較対照してください。" ) print(str(response_chatgpt))
下の画像はクエリを分解したものです。シアトル、ヒューストン、トロントの空港について尋ねています。
クエリの分解](https://assets.zilliz.com/Decomposition_of_our_query_97c36cf7b2.png)
下の画像は応答の一部です。完全な回答は、「シアトルにはシアトル・タコマ国際空港という1つの主要空港があり、ヒューストンにはジョージ・ブッシュ・インターコンチネンタル空港とウィリアム・P・ホビー空港という2つの主要空港と、エリントン空港という3つ目の市営空港がある。トロントで最も忙しい空港はトロント・ピアソン国際空港と呼ばれ、ミシサガとの市西部の境界に位置している。トロント・ピアソン国際空港は、カナダとアメリカの近隣都市への限られた商業便と旅客便を運航している。シアトル・タコマ国際空港とジョージ・ブッシュ・インターコンチネンタル空港は主要な国際空港であり、ウィリアム・P・ホビー空港とエリントン空港は小規模で、より地域的な目的地に就航している。トロント・ピアソン国際空港はカナダで最も忙しい空港であり、ユニオン・ピアソン・エクスプレスという列車でユニオン駅に直結している。"
お問い合わせへの回答](https://assets.zilliz.com/Response_to_the_query_6ee9c9b986.png)
#### 非分解クエリとの比較
なぜ、複数のドキュメントをクエリーするために、分解可能なクエリーが必要なのでしょうか?様々なソースからデータを見つけ、収集するには、LLMアプリケーションがクエリを分割し、適切なソースにルーティングする必要があるからです。ロサンゼルスに行ったことのないニューヨーカーにロサンゼルス空港について尋ねたとしよう。彼らは何を答えられるだろうか?ほとんど何もわからないだろう。
もし分解可能なクエリーを使わなかったらどうなるか見てみましょう。以下のコードはカスタム・クエリー・エンジンを作り直したものですが、一つだけ大きな違いがあります。都市インデックスを作成する際に、クエリートランスフォーマー(または必要な余分な「サマリー」情報)を追加しないのです。
python
custom_query_engines = {} とする。
for index in city_indices.values():
query_engine = index.as_query_engine(service_context=service_context)
custom_query_engines[index.index_id] = query_engine
custom_query_engines[graph.root_index.index_id] = graph.root_index.as_query_engine(
retriever_mode='simple'、
response_mode='tree_summarize'、
service_context=service_context
)
query_engine = graph.as_query_engine(
custom_query_engines=custom_query_engines、
)
response_chatgpt = query_engine.query(
"シアトル、ヒューストン、トロントの空港を比較対照してください。"
)
str(response_chatgpt)
このクエリーエンジンでクエリーすると、提供された文脈情報(ドキュメント)には質問に答えるのに十分な情報がないことを伝えるレスポンスが返ってくる。というのも、これを分解しなければ、要するにシアトル空港にしか行ったことのない人に、行ったことのない2つの都市の空港と比較するように頼んでいることになるからだ。
クエリーエンジンのレスポンス](https://assets.zilliz.com/Query_engine_response_c68cb09875.png)
LlamaIndex を使った複数ドキュメントのクエリ方法のまとめ
このチュートリアルでは、"LLM "スタック(LlamaIndex、LangChain、Milvus)を使って、iPython Notebookの複数のドキュメントに対する質問応答アプリを作る方法を学びました。このQ/Aアプリは、分解可能なクエリの概念を使い、LlamaIndexでクエリの分割とルーティングを正しく処理するために、ベクトルストアインデックスとキーワードインデックスをスタックしています。
クエリの分解により、複雑なクエリをよりシンプルで的を絞ったクエリに分解することができる。キーワードインデックスは、キーワード検索によってクエリをルーティングすることを可能にする。最後に、ベクトルストア・インデックスは意味情報を処理することを可能にする。これらすべてを組み合わせることで、多くのソースからの情報を必要とする質問に答えることができる。
まず、トランスフォーマーは質問を単一のデータソースが回答できる単純なクエリーに分割する。次に、キーワードインデックスを使用して単純なクエリを適切なデータソースにルーティングし、ベクトルストアインデックスを使用して質問に回答します。最後に、質問変換器は情報を結合し、元の複雑なクエリに回答します。



