🐡

LangChain と Qdrant でベクトル検索を行う

2025/01/04に公開

はじめに

RAG を使用したサービスをローカルで開発して、本番環境での使用を想定した場合に LangChain と Qdrant でどのように実装していくかのメモです。

ベースの記事はこちら
https://python.langchain.com/docs/integrations/vectorstores/qdrant/

Qdrant とは

Qdrant(読み方:クワッドラント)は、ベクトル類似性検索エンジンです。追加のペイロードと拡張されたフィルタリングサポートを備えた、ベクトルを保存、検索、管理するための便利な API を備えた、本番環境対応のサービスを提供します。これにより、ニューラルネットワークやセマンティックベースのマッチング、ファセット検索、その他のアプリケーションなど、あらゆる種類の用途に役立ちます。
Qdrant はフィルタリングサポート、Qdrant Cloud が無料で使用できるという点から選びました。

セットアップ

Qdrant を実行する方法にはさまざまなモードがあり、選択したモードによっていくつかのわずかな違いがあります。オプションには以下が含まれます。

  • ローカルモード、サーバーは不要
  • Docker でのデプロイ
  • Qdrant Cloud

インストール手順を参照してください。

pip install -qU langchain-qdrant

初期化

ローカルモード

Python クライアントを使用すると、Qdrant サーバーを実行せずに、ローカルモードで同じコードを実行できます。これは、テストやデバッグ、または少量のベクトルの保存に最適です。埋め込みは、完全にメモリに保持することも、ディスクに永続化することもできます。

インメモリ

テストシナリオや簡単な実験であれば、すべてのデータをメモリ上に保存しておき、クライアントが破棄されたとき(通常はスクリプトやノートブックの終了時)に失われるようにすることもできます。

まずはlangchain-openaiをインストールします。

pip install -qU langchain-openai
import getpass
import os

if not os.environ.get("OPENAI_API_KEY"):
  os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter API key for OpenAI: ")

from langchain_openai import OpenAIEmbeddings

# 埋め込みモデルを選択
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams

# Qdrantクライアントを初期化(メモリ上に保存)
client = QdrantClient(":memory:")

# コレクションを作成
client.create_collection(
    collection_name="demo_collection",
    vectors_config=VectorParams(size=3072, distance=Distance.COSINE),
)

# QdrantVectorStoreを初期化
vector_store = QdrantVectorStore(
    client=client,
    collection_name="demo_collection",
    embedding=embeddings,
)

Qdrant で OpenAI 埋め込みモデルを使用する際に、バイナリ量子化という技術を使うことで、検索結果の品質をあまり損なわずに埋め込みサイズを 32 分の 1 に削減できます。

実験結果によると、以下の OpenAI 埋め込みモデルと次元数で、DBpedia データセットに対して高リコールの検索結果が得られています。

OpenAI 埋め込みモデル 次元数 テストデータセット リコール オーバーサンプリング
text-embedding-3-large 3072 DBpedia 1M 0.9966 3x
text-embedding-3-small 1536 DBpedia 100K 0.9847 3x
text-embedding-3-large 1536 DBpedia 1M 0.9826 3x
text-embedding-ada-002 1536 DbPedia 1M 0.98 4x

この表から、OpenAI 埋め込みモデルとバイナリ量子化を組み合わせることで、高いリコール率を維持しながら、埋め込みの次元数を削減できることがわかります。

ディスクストレージ

Qdrant サーバーを使用しないローカルモードでは、ベクトルをディスクに保存して、データを永続化することもできます。

# データの保存先を指定
client = QdrantClient(path="/tmp/langchain_qdrant")

client.create_collection(
    collection_name="demo_collection",
    vectors_config=VectorParams(size=3072, distance=Distance.COSINE),
)

vector_store = QdrantVectorStore(
    client=client,
    collection_name="demo_collection",
    embedding=embeddings,
)

Docker を使用する場合

本番環境で Qdrant Cloud を使用する関係で URL から接続したい場合や、コンテナごとデプロイしたい場合は Docker を使用します。

まず、Dockerhub から最新の Qdrant イメージをダウンロードします

docker pull qdrant/qdrant

サービスを起動します。

docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
    qdrant/qdrant

デフォルトの設定では、全てのデータは ./qdrant_storage ディレクトリに保存されます。 また、コンテナとホストマシンの両方が見ることができる唯一のディレクトリとなります。

以下で Qdrant にアクセスできます。

  • REST API: localhost:6333
  • Web UI: localhost:6333/dashboard
  • GRPC API: localhost:6334

Qdrant Cloud

インフラストラクチャの管理に煩わされたくない場合は、Qdrant Cloud で完全に管理された Qdrant クラスターを設定することを選択できます。試用版として、無料の 1GB クラスターが永久に付属しています。管理バージョンの Qdrant を使用する場合の主な違いは、デプロイメントへのパブリックアクセスを防ぐために API キーを提供する必要があることです。値は、QDRANT_API_KEY 環境変数に設定することもできます。

url = "<---qdrant cloud cluster url here --->"
api_key = "<---api key here--->"
qdrant = QdrantVectorStore.from_documents(
    docs,
    embeddings,
    url=url,
    prefer_grpc=True,
    api_key=api_key,
    collection_name="my_documents",
)

既存のコレクションを使用する

新しいドキュメントやテキストをロードせずに langchain_qdrant.Qdrant のインスタンスを取得するには、Qdrant.from_existing_collection() メソッドを使用できます。

qdrant = QdrantVectorStore.from_existing_collection(
    embedding=embeddings,
    collection_name="my_documents",
    url="http://localhost:6333",
)

ベクトルストアの管理

ベクトルストアを作成したら、さまざまなアイテムを追加および削除することで、ベクトルストアとやり取りできます。

ベクトルストアにアイテムを追加する

add_documents 関数を使用して、アイテムをベクトルストアに追加できます。

from uuid import uuid4

from langchain_core.documents import Document

document_1 = Document(
    page_content="今日の朝食はチョコレートチップパンケーキとスクランブルエッグでした。",
    metadata={"source": "tweet"},
)

document_2 = Document(
    page_content="明日の天気予報は曇りで、最高気温は62度です。",
    metadata={"source": "news"},
)

document_3 = Document(
    page_content="LangChainでエキサイティングな新しいプロジェクトを構築しています。ぜひチェックしてください!",
    metadata={"source": "tweet"},
)

document_4 = Document(
    page_content="強盗が市の銀行に侵入し、現金100万ドルを盗みました。",
    metadata={"source": "news"},
)

document_5 = Document(
    page_content="うわー!素晴らしい映画でした。また見に行くのが待ちきれません。",
    metadata={"source": "tweet"},
)


documents = [
    document_1,
    document_2,
    document_3,
    document_4,
    document_5,
]
uuids = [str(uuid4()) for _ in range(len(documents))]

vector_store.add_documents(documents=documents, ids=uuids)
APIリファレンス:Document
['c04134c3-273d-4766-949a-eee46052ad32',
 '9e6ba50c-794f-4b88-94e5-411f15052a02',
 'd3202666-6f2b-4186-ac43-e35389de8166',
 '50d8d6ee-69bf-4173-a6a2-b254e9928965',
 'bd2eae02-74b5-43ec-9fcf-09e9d9db6fd3',]

ベクトルストアからアイテムを削除する

vector_store.delete(ids=[uuids[-1]])
True

ベクトルストアのクエリ実行

ベクトルストアが作成され、関連ドキュメントが追加されると、チェーンまたはエージェントの実行中にクエリを実行することが多くなります。

直接クエリを実行する

Qdrant ベクトルストアを使用する最も簡単な方法は、類似性検索を実行することです。内部的には、クエリはベクトル埋め込みにエンコードされ、Qdrant コレクションで類似のドキュメントを見つけるために使用されます。

results = vector_store.similarity_search(
    "LangChainはLLMの操作を簡単にする抽象化を提供します", k=2
)
for res in results:
    print(f"* {res.page_content} [{res.metadata}]")
* LangChainでエキサイティングな新しいプロジェクトを構築しています。ぜひチェックしてください! [{'source': 'tweet', '_id': 'd3202666-6f2b-4186-ac43-e35389de8166', '_collection_name': 'demo_collection'}]
* LangGraphは、状態を持つエージェントアプリケーションを構築するための最高のフレームワークです! [{'source': 'tweet', '_id': '91ed6c56-fe53-49e2-8199-c3bb3c33c3eb', '_collection_name': 'demo_collection'}]

QdrantVectorStore は、類似性検索に 3 つのモードをサポートしています。これらは、クラスを設定するときに retrieval_mode パラメータを使用して構成できます。

  • 高密度ベクトル検索(デフォルト)
  • 低密度ベクトル検索
  • ハイブリッド検索

高密度ベクトル検索

高密度ベクトルのみで検索するには、

  • retrieval_mode パラメータを RetrievalMode.DENSE(デフォルト)に設定する必要があります。
  • 密な埋め込み値を embedding パラメータに提供する必要があります。
from langchain_qdrant import RetrievalMode

qdrant = QdrantVectorStore.from_documents(
    docs,
    embedding=embeddings,
    location=":memory:",
    collection_name="my_documents",
    retrieval_mode=RetrievalMode.DENSE,
)

query = "ケタンジ・ブラウン・ジャクソンについて大統領は何と言いましたか?"
found_docs = qdrant.similarity_search(query)

低密度ベクトル検索

低密度ベクトルのみで検索するには、

  • retrieval_mode パラメータを RetrievalMode.SPARSE に設定する必要があります。
  • スパース埋め込みプロバイダを使用する SparseEmbeddings インターフェースの実装を、sparse_embedding パラメータの値として提供する必要があります。
  • langchain-qdrant パッケージには、すぐに使える FastEmbed ベースの実装が付属しています。

これを使用するには、FastEmbed パッケージをインストールします。

%pip install fastembed
from langchain_qdrant import FastEmbedSparse, RetrievalMode

sparse_embeddings = FastEmbedSparse(model_name="Qdrant/bm25")

qdrant = QdrantVectorStore.from_documents(
    docs,
    sparse_embedding=sparse_embeddings,
    location=":memory:",
    collection_name="my_documents",
    retrieval_mode=RetrievalMode.SPARSE,
)

query = "ケタンジ・ブラウン・ジャクソンについて大統領は何と言いましたか?"
found_docs = qdrant.similarity_search(query)

ハイブリッドベクトル検索

スコア融合を使用して高密度ベクトルと低密度ベクトルによるハイブリッド検索を実行するには、

  • retrieval_mode パラメータを RetrievalMode.HYBRID に設定する必要があります。
  • 密な埋め込み値を embedding パラメータに提供する必要があります。
  • スパース埋め込みプロバイダを使用する SparseEmbeddings インターフェースの実装を、sparse_embedding パラメータの値として提供する必要があります。
  • HYBRID モードでドキュメントを追加した場合は、検索時に任意の検索モードに切り替えることができることに注意してください。高密度ベクトルと低密度ベクトルの両方がコレクションで使用できるためです。
from langchain_qdrant import FastEmbedSparse, RetrievalMode

sparse_embeddings = FastEmbedSparse(model_name="Qdrant/bm25")

qdrant = QdrantVectorStore.from_documents(
    docs,
    embedding=embeddings,
    sparse_embedding=sparse_embeddings,
    location=":memory:",
    collection_name="my_documents",
    retrieval_mode=RetrievalMode.HYBRID,
)

query = "ケタンジ・ブラウン・ジャクソンについて大統領は何と言いましたか?"
found_docs = qdrant.similarity_search(query)

類似性検索を実行して対応するスコアを受け取りたい場合は、次を実行できます。

results = vector_store.similarity_search_with_score(
    query="明日は暑くなるだろうか", k=1
)
for doc, score in results:
    print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]")
* [SIM=0.531834] 明日の天気予報は曇りで、最高気温は62度です。 [{'source': 'news', '_id': '9e6ba50c-794f-4b88-94e5-411f15052a02', '_collection_name': 'demo_collection'}]

QdrantVectorStore で利用可能なすべての検索機能の完全なリストについては、API リファレンスをお読みください。

メタデータフィルタリング

Qdrant には、豊富な型サポートを備えた広範なフィルタリングシステムがあります。similarity_search_with_score メソッドと similarity_search メソッドの両方に追加のパラメータを渡すことで、Langchain でフィルタを使用することも可能です。

from qdrant_client import models

results = vector_store.similarity_search(
    query="世界で最高のサッカー選手は誰ですか?",
    k=1,
    filter=models.Filter(
        should=[
            models.FieldCondition(
                key="page_content",
                match=models.MatchValue(
                    value="現在、世界でトップ10のサッカー選手。"
                ),
            ),
        ]
    ),
)
for doc in results:
    print(f"* {doc.page_content} [{doc.metadata}]")
* 現在、世界でトップ10のサッカー選手。 [{'source': 'website', '_id': 'b0964ab5-5a14-47b4-a983-37fa5c5bd154', '_collection_name': 'demo_collection'}]

リトリーバーに変えてクエリする

また、ベクトルストアをリトリーバーに変換して、チェーンでより簡単に使用することもできます。

retriever = vector_store.as_retriever(search_type="mmr", search_kwargs={"k": 1})
retriever.invoke("銀行から盗むのは犯罪です")
[Document(metadata={'source': 'news', '_id': '50d8d6ee-69bf-4173-a6a2-b254e9928965', '_collection_name': 'demo_collection'}, page_content='強盗が市の銀行に侵入し、現金100万ドルを盗みました。')]

検索拡張生成への利用

このベクトルストアを検索拡張生成(RAG)に使用する方法については、以下のセクションを参照してください。

Qdrant のカスタマイズ

Langchain アプリケーション内で既存の Qdrant コレクションを使用するためのオプションがあります。そのような場合、Qdrant ポイントを Langchain ドキュメントにマッピングする方法を定義する必要がある場合があります。

名前付きベクトル

Qdrant は、名前付きベクトルによるポイントごとの複数のベクトルをサポートしています。外部で作成されたコレクションを操作する場合、または別の名前のベクトルを使用する場合は、その名前を指定することで構成できます。

from langchain_qdrant import RetrievalMode

QdrantVectorStore.from_documents(
    docs,
    embedding=embeddings,
    sparse_embedding=sparse_embeddings,
    location=":memory:",
    collection_name="my_documents_2",
    retrieval_mode=RetrievalMode.HYBRID,
    vector_name="custom_vector",
    sparse_vector_name="custom_sparse_vector",
)

メタデータ

Qdrant は、オプションの JSON ライクなペイロードとともに、ベクトルの埋め込みを保存します。ペイロードはオプションですが、LangChain は埋め込みがドキュメントから生成されると想定しているため、元のテキストを抽出できるようにコンテキストデータを保持します。

デフォルトでは、ドキュメントは次のペイロード構造で保存されます。

{
    "page_content": "Lorem ipsum dolor sit amet",
    "metadata": {
        "foo": "bar"
    }
}

ただし、ページコンテンツとメタデータに異なるキーを使用することもできます。これは、再利用したいコレクションがすでにある場合に役立ちます。

QdrantVectorStore.from_documents(
    docs,
    embeddings,
    location=":memory:",
    collection_name="my_documents_2",
    content_payload_key="my_page_content_key",
    metadata_payload_key="my_meta",
)

Nodejs から利用する場合

https://js.langchain.com/docs/integrations/vectorstores/qdrant/

インストール

npm i @langchain/qdrant @langchain/core @langchain/openai

初期化

import { QdrantVectorStore } from "@langchain/qdrant";
import { OpenAIEmbeddings } from "@langchain/openai";

process.env.QDRANT_URL = "http://localhost:6333";
process.env.QDRANT_API_KEY = "your-api-key";

const embeddings = new OpenAIEmbeddings({
  model: "text-embedding-3-large",
});

// Dockerを起動後作成したコレクション名を指定
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, {
  url: process.env.QDRANT_URL,
  collectionName: "demo_collection",
});

ベクトルストアの管理

ベクトルストアにアイテムを追加する

import type { Document } from "@langchain/core/documents";

const document1: Document = {
  pageContent: "The powerhouse of the cell is the mitochondria",
  metadata: { source: "https://example.com" },
};

const document2: Document = {
  pageContent: "Buildings are made out of brick",
  metadata: { source: "https://example.com" },
};

const document3: Document = {
  pageContent: "Mitochondria are made out of lipids",
  metadata: { source: "https://example.com" },
};

const document4: Document = {
  pageContent: "The 2024 Olympics are in Paris",
  metadata: { source: "https://example.com" },
};

const documents = [document1, document2, document3, document4];

await vectorStore.addDocuments(documents);

Nodejs では addDocuments で Python のように ID を指定することはできないようです。

ベクトルストアからアイテムを削除する

Nodejs からは、ベクトルストアからアイテムを削除することはできません。

ベクトルストアのクエリ実行

直接クエリを実行する

シンプルな類似性検索は、次のように実行できます。

const filter = {
  must: [{ key: "metadata.source", match: { value: "https://example.com" } }],
};

// 第一引数はクエリ、第二引数は取得するドキュメント数、第三引数はmetadataのフィルター
const similaritySearchResults = await vectorStore.similaritySearch(
  "biology",
  2,
  filter
);

for (const doc of similaritySearchResults) {
  console.log(`* ${doc.pageContent} [${JSON.stringify(doc.metadata, null)}]`);
}
* The powerhouse of the cell is the mitochondria [{"source":"https://example.com"}]
* Mitochondria are made out of lipids [{"source":"https://example.com"}]

Qdrant のフィルター構文の詳細については、このページを参照してください。すべての値は metadata. で始まる必要があることに注意してください。

類似性検索を実行して対応するスコアを取得したい場合は、次のように実行できます。

const similaritySearchWithScoreResults =
  await vectorStore.similaritySearchWithScore("biology", 2, filter);

for (const [doc, score] of similaritySearchWithScoreResults) {
  console.log(
    `* [SIM=${score.toFixed(3)}] ${doc.pageContent} [${JSON.stringify(
      doc.metadata
    )}]`
  );
}
* [SIM=0.165] The powerhouse of the cell is the mitochondria [{"source":"https://example.com"}]
* [SIM=0.148] Mitochondria are made out of lipids [{"source":"https://example.com"}]

リトリーバーに変えてクエリする

また、ベクトルストアをリトリーバーに変換して、チェーンでより簡単に使用することもできます。

const retriever = vectorStore.asRetriever({
  // オプションのフィルター
  filter: filter,
  k: 2,
});
await retriever.invoke("biology");
[
  Document {
    pageContent: 'The powerhouse of the cell is the mitochondria',
    metadata: { source: 'https://example.com' },
    id: undefined
  },
  Document {
    pageContent: 'Mitochondria are made out of lipids',
    metadata: { source: 'https://example.com' },
    id: undefined
  }
]

Discussion