CLOVER🍀

That was when it all began.

Qdrantのチュートリアルから、「LlamaIndexを使ったマルチテナント(Multitenancy with LlamaIndex)」を試す

これは、なにをしたくて書いたもの?

Qdrantのチュートリアルから、「LlamaIndexを使ったマルチテナント(Multitenancy with LlamaIndex)」を試してみたいと思います。

Multitenancy with LlamaIndex - Qdrant

今回のチュートリアルの狙い

今回扱うチュートリアルはこちらの「LlamaIndexを使ったマルチテナント(Multitenancy with LlamaIndex)」です。

Multitenancy with LlamaIndex - Qdrant

マルチテナントが主題のように見えますが、ここで示したいのはQdrantとLlamaIndexとの統合ですね。

このチュートリアルの内容をざっくり言うと、LlamaIndexの機能でQdrantと統合し、ドキュメント登録とフィルタリング付きの検索を
行う、というのがよいんでしょうね。

Qdrantは様々なフレームワークと統合することができ、そのうちのひとつにLlamaIndexがあります。

LlamaIndex - Qdrant

LlamaIndexは、LLMと外部のデータを接続するためのインターフェースを提供するフレームワークだそうです。RAGのための
フレームワーク、と言ってもよさそうです。

LlamaIndex, Data Framework for LLM Applications

LlamaIndex自体には今回は踏み込みません。

Qdrantは他にも様々なフレームワークとの統合が可能なようです。

そして今回は、QdrantをLlamaIndexのベクトルデータベースとして組み合わせて使うチュートリアルとなっています。

Multitenancy with LlamaIndex - Qdrant

…なのですが、このドキュメント内で使われているLlamaIndexのバージョンが古いようなので、現在のLlamaIndex(0.10.12)に
合わせて読み替えていきます。

環境

今回の環境はこちら。Qdrantは172.17.0.2で動作しているものとします。

$ ./qdrant --version
qdrant 1.7.4

QdrantのWeb UIは0.1.21を使っています。

Qdrant ClientおよびLlamaIndexを使うPython環境。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

Qdrantのチュートリアル「LlamaIndexを使ったマルチテナント(Multitenancy with LlamaIndex)」を試す

それでは、Qdrantのチュートリアル「LlamaIndexを使ったマルチテナント(Multitenancy with LlamaIndex)」をドキュメントに沿って
進めていきます。

Multitenancy with LlamaIndex - Qdrant

必要なライブラリーのインストール。

$ pip3 install qdrant-client llama-index-vector-stores-qdrant llama-index-embeddings-huggingface

インストールしたライブラリーの一覧。

$ pip3 list
Package                            Version
---------------------------------- ----------
aiohttp                            3.9.3
aiosignal                          1.3.1
annotated-types                    0.6.0
anyio                              4.3.0
async-timeout                      4.0.3
attrs                              23.2.0
certifi                            2024.2.2
charset-normalizer                 3.3.2
click                              8.1.7
dataclasses-json                   0.6.4
Deprecated                         1.2.14
dirtyjson                          1.0.8
distro                             1.9.0
exceptiongroup                     1.2.0
filelock                           3.13.1
frozenlist                         1.4.1
fsspec                             2024.2.0
greenlet                           3.0.3
grpcio                             1.62.0
grpcio-tools                       1.62.0
h11                                0.14.0
h2                                 4.1.0
hpack                              4.0.0
httpcore                           1.0.4
httpx                              0.27.0
huggingface-hub                    0.20.3
hyperframe                         6.0.1
idna                               3.6
Jinja2                             3.1.3
joblib                             1.3.2
llama-index-core                   0.10.12
llama-index-embeddings-huggingface 0.1.3
llama-index-vector-stores-qdrant   0.1.3
llamaindex-py-client               0.1.13
MarkupSafe                         2.1.5
marshmallow                        3.20.2
mpmath                             1.3.0
multidict                          6.0.5
mypy-extensions                    1.0.0
nest-asyncio                       1.6.0
networkx                           3.2.1
nltk                               3.8.1
numpy                              1.26.4
nvidia-cublas-cu12                 12.1.3.1
nvidia-cuda-cupti-cu12             12.1.105
nvidia-cuda-nvrtc-cu12             12.1.105
nvidia-cuda-runtime-cu12           12.1.105
nvidia-cudnn-cu12                  8.9.2.26
nvidia-cufft-cu12                  11.0.2.54
nvidia-curand-cu12                 10.3.2.106
nvidia-cusolver-cu12               11.4.5.107
nvidia-cusparse-cu12               12.1.0.106
nvidia-nccl-cu12                   2.19.3
nvidia-nvjitlink-cu12              12.3.101
nvidia-nvtx-cu12                   12.1.105
openai                             1.12.0
packaging                          23.2
pandas                             2.2.0
pillow                             10.2.0
pip                                22.0.2
portalocker                        2.8.2
protobuf                           4.25.3
pydantic                           2.6.1
pydantic_core                      2.16.2
python-dateutil                    2.8.2
pytz                               2024.1
PyYAML                             6.0.1
qdrant-client                      1.7.3
regex                              2023.12.25
requests                           2.31.0
safetensors                        0.4.2
setuptools                         59.6.0
six                                1.16.0
sniffio                            1.3.0
SQLAlchemy                         2.0.27
sympy                              1.12
tenacity                           8.2.3
tiktoken                           0.6.0
tokenizers                         0.15.2
torch                              2.2.1
tqdm                               4.66.2
transformers                       4.38.1
triton                             2.2.0
typing_extensions                  4.9.0
typing-inspect                     0.9.0
tzdata                             2024.1
urllib3                            2.2.1
wrapt                              1.16.0
yarl                               1.9.4

いきなりドキュメントと違うモジュール(llama-index-vector-stores-qdrant、llama-index-embeddings-huggingface)を
インストールしていますが、これはLlmaIndex側のドキュメントを見るとモジュールが変わっていたからですね。

Configuring Settings - LlamaIndex 🦙 v0.10.12

Qdrant Vector Store - LlamaIndex 🦙 v0.10.12

Embeddings - LlamaIndex 🦙 v0.10.12

Defining and Customizing Documents - LlamaIndex 🦙 v0.10.12

また、llama-index-vector-stores-qdrantをインストールするとqdrant-clientも同時にインストールされますが、今回は明示的に
指定することにします。

この結果、作成するプログラムのimport等も元のドキュメントから変更が入ります。

最初は、Qdrantにコレクションを作成してポイントを登録するプログラムを作成します。

add_documents.py

from qdrant_client import QdrantClient
from qdrant_client.models import HnswConfigDiff, PayloadSchemaType
from llama_index.core import Settings, StorageContext, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import Document
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.qdrant import QdrantVectorStore

documents = [
    Document(
        text="LlamaIndex is a simple, flexible data framework for connecting custom data sources to large language models.",
        metadata={
            "library": "llama-index",
        },
    ),
    Document(
        text="LlamaIndex is a framework that can be integrated with Qdrant.",
        metadata={
            "library": "llama-index",
        },
    ),
    Document(
        text="Qdrant is a vector database & vector similarity search engine.",
        metadata={
            "library": "qdrant",
        },
    ),
    Document(
        text="Qdrant is implemented by Rust.",
        metadata={
            "library": "qdrant",
        },
    ),
    Document(
        text="Qdrant has a tutorial on integrating with LlamaIndex.",
        metadata={
            "library": "qdrant",
        },
    ),
]

Settings.embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-small-en-v1.5"
)

Settings.node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=32)

client = QdrantClient("http://172.17.0.2:6333")

vector_store = QdrantVectorStore(
    collection_name="my_collection",
    client=client
)

storage_context = StorageContext.from_defaults(vector_store=vector_store)

VectorStoreIndex.from_documents(
    documents=documents,
    storage_context=storage_context
)

client.create_payload_index(
    collection_name="my_collection",
    field_name="metadata.library",
    field_type=PayloadSchemaType.KEYWORD,
)

client.update_collection(
    collection_name="my_collection",
    hnsw_config=HnswConfigDiff(payload_m=16, m=0),
)

QdrantClientは直接使うところはかなり限定されます。

こちらはQdrantにポイントとして登録するドキュメントです。チュートリアルから、少しサンプルを増やしておきました。

documents = [
    Document(
        text="LlamaIndex is a simple, flexible data framework for connecting custom data sources to large language models.",
        metadata={
            "library": "llama-index",
        },
    ),
    Document(
        text="LlamaIndex is a framework that can be integrated with Qdrant.",
        metadata={
            "library": "llama-index",
        },
    ),
    Document(
        text="Qdrant is a vector database & vector similarity search engine.",
        metadata={
            "library": "qdrant",
        },
    ),
    Document(
        text="Qdrant is implemented by Rust.",
        metadata={
            "library": "qdrant",
        },
    ),
    Document(
        text="Qdrant has a tutorial on integrating with LlamaIndex.",
        metadata={
            "library": "qdrant",
        },
    ),
]

Defining and Customizing Documents - LlamaIndex 🦙 v0.10.12

使用する埋め込みモデルの設定。

Settings.embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-small-en-v1.5"
)

今回はBAAI/bge-small-en-v1.5をHugging Faceからダウンロードして使います。

Embeddings - LlamaIndex 🦙 v0.10.12

次に、ドキュメントをチャンクまたはノードに分割する設定をします。

Settings.node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=32)

Configuring Settings - LlamaIndex 🦙 v0.10.12

QdrantClientを作成し、LlamaIndexで使うベクトルデータベース(QdrantVectorStore)として指定します。

client = QdrantClient("http://172.17.0.2:6333")

vector_store = QdrantVectorStore(
    collection_name="my_collection",
    client=client
)

QdrantVectorStoreをストレージにしてVectorStoreIndex経由でドキュメントを登録します。

storage_context = StorageContext.from_defaults(vector_store=vector_store)

VectorStoreIndex.from_documents(
    documents=documents,
    storage_context=storage_context
)

Qdrant Vector Store - LlamaIndex 🦙 v0.10.12

最後に、metadata.libraryフィールドに対してインデックスを付与して検索を効率化します。また、インデックス全体を検索することが
ないので(※)HNSWグラフをグローバルに構築しないようにします。

※クエリー時に常にmetadata.libraryをフィルター条件似指定します

client.create_payload_index(
    collection_name="my_collection",
    field_name="metadata.library",
    field_type=PayloadSchemaType.KEYWORD,
)

client.update_collection(
    collection_name="my_collection",
    hnsw_config=HnswConfigDiff(payload_m=16, m=0),
)

プログラムができたので、実行します。

$ python3 add_documents.py

QdrantのWeb UIを使って、ドキュメント(Qdrantではポイント)が登録されたことを確認します。

OKですね。

今度は、検索をしてみます。

作成したプログラムはこちら。

search.py

import sys

from llama_index.core.vector_stores.types import FilterOperator, MetadataFilter, MetadataFilters
from qdrant_client import QdrantClient
from llama_index.core import Settings, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.qdrant import QdrantVectorStore

target_library = sys.argv[1]
search_keyword = sys.argv[2]

Settings.embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-small-en-v1.5"
)

Settings.node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=32)

client = QdrantClient("http://172.17.0.2:6333")

vector_store = QdrantVectorStore(
    collection_name="my_collection",
    client=client
)

index = VectorStoreIndex.from_vector_store(
    vector_store=vector_store
)

print(f"target filter: {target_library}")
print(f"search keyword: {search_keyword}")

qdrant_retriver = index.as_retriever(
    filters=MetadataFilters(
        filters=[
            MetadataFilter(
                key="library",
                operator=FilterOperator.EQ,
                value=target_library,
            )
        ]
    )
)

nodes_with_scores = qdrant_retriver.retrieve(search_keyword)

print()
print("results:")

for node in nodes_with_scores:
    print(f"""  document
    id: {node.id_}
    text: {node.text}
    metadata: {node.metadata}
    score: {node.score}""")

検索時にドキュメントを登録した時に指定したメタデータlibraryを指定するのですが、この値と検索キーワードはコマンドライン引数で
指定するようにしました。

target_library = sys.argv[1]
search_keyword = sys.argv[2]

LlamaIndexの設定とQdrantVectorStoreインスタンスの作成。

Settings.embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-small-en-v1.5"
)

Settings.node_parser = SentenceSplitter(chunk_size=512, chunk_overlap=32)

client = QdrantClient("http://172.17.0.2:6333")

vector_store = QdrantVectorStore(
    collection_name="my_collection",
    client=client
)

この後、VectorStoreIndexから検索で使うインデックスを取得するのですが、ここではドキュメントは登録しないので
VectorStoreIndex#from_vector_storeでベクトルデータベース(今回はQdrantVectorStore)のみを指定します。

index = VectorStoreIndex.from_vector_store(
    vector_store=vector_store
)

ここで取得したVectorStoreIndexインスタンスから、VectorStoreIndex#as_retrieverでRetriverのインスタンスを取得するのですが、
この時にフィルターを指定します。ドキュメント登録時にllama-indexqdrantのどちらかを指定しましたが、検索結果に対する絞り込みを
行います。

print(f"target filter: {target_library}")
print(f"search keyword: {search_keyword}")

qdrant_retriver = index.as_retriever(
    filters=MetadataFilters(
        filters=[
            MetadataFilter(
                key="library",
                operator=FilterOperator.EQ,
                value=target_library,
            )
        ]
    )
)

nodes_with_scores = qdrant_retriver.retrieve(search_keyword)

あとは検索キーワードで検索ですね。

結果表示。

print()
print("results:")

for node in nodes_with_scores:
    print(f"""  document
    id: {node.id_}
    text: {node.text}
    metadata: {node.metadata}
    score: {node.score}""")

では、実行してみます。

まずはlibraryqdrantを指定。

$ python3 search.py qdrant 'large language models'
target filter: qdrant
search keyword: large language models

results:
  document
    id: bba4d033-7c06-4869-a5a0-b73410e68ddb
    text: Qdrant is a vector database & vector similarity search engine.
    metadata: {'library': 'qdrant'}
    score: 0.6055155
  document
    id: e7e4ac84-0ef4-48f8-844b-cc2e8b6f97be
    text: Qdrant has a tutorial on integrating with LlamaIndex.
    metadata: {'library': 'qdrant'}
    score: 0.58532107

次は、llama-indexを指定。

$ python3 search.py llama-index 'large language models'
target filter: llama-index
search keyword: large language models

results:
  document
    id: 2a1d4535-001d-4d93-937d-c34f99ea356c
    text: LlamaIndex is a simple, flexible data framework for connecting custom data sources to large language models.
    metadata: {'library': 'llama-index'}
    score: 0.63576734
  document
    id: 7d1c5050-ea41-4f4e-bd29-e7cb7c1b63f1
    text: LlamaIndex is a framework that can be integrated with Qdrant.
    metadata: {'library': 'llama-index'}
    score: 0.5662359

良さそうです。

libraryllama-indexで、キーワードは「qdrant」。

$ python3 search.py llama-index 'qdrant'
target filter: llama-index
search keyword: qdrant

results:
  document
    id: 7d1c5050-ea41-4f4e-bd29-e7cb7c1b63f1
    text: LlamaIndex is a framework that can be integrated with Qdrant.
    metadata: {'library': 'llama-index'}
    score: 0.68183947
  document
    id: 2a1d4535-001d-4d93-937d-c34f99ea356c
    text: LlamaIndex is a simple, flexible data framework for connecting custom data sources to large language models.
    metadata: {'library': 'llama-index'}
    score: 0.5274638

こんなところでしょうか。

おわりに

Qdrantのチュートリアルから、「LlamaIndexを使ったマルチテナント(Multitenancy with LlamaIndex)」を試してみました。

初めてのLlamaIndexでもあったのですが、今回はさらっと流すつもりがQdrantのチュートリアルで使われていたLlamaIndexの内容が
古かったみたいで、結局そこそこLlamaIndexの情報も見ることになってしまいました。まあ、それもよいでしょう。

こうなると、LangChainと組み合わせたりしたくなりますね。