大規模言語モデルによるMilvusクエリフィルタ式の生成

このチュートリアルでは、大規模言語モデル(Large Language Models: LLM)を用いて、自然言語クエリからMilvusフィルタ式を自動生成する方法を説明します。このアプローチにより、ユーザは複雑なフィルタリング条件を平易な英語で表現することができ、それをMilvusの適切な構文に変換することで、ベクトルデータベースクエリをより利用しやすくすることができます。

Milvusは以下のような高度なフィルタリング機能をサポートしています:

  • 基本的な演算子 ==,!=,>,<,>= のような比較演算子、<=
  • ブール演算子論理演算子:and,or,not のような複雑な条件のための論理演算子
  • 文字列演算like 、その他の文字列関数を使ったパターンマッチング
  • 配列操作array_contains,array_length などを使用した配列フィールドの操作。
  • JSON 操作:特殊な演算子によるJSONフィールドの問い合わせ

LLMとMilvusドキュメントを統合することにより、自然言語によるクエリを理解し、構文的に正しいフィルタ式を生成するインテリジェントなシステムを構築することができます。このチュートリアルでは、様々なフィルタリングシナリオでの有効性を強調しながら、このシステムのセットアッププロセスを説明します。

依存関係と環境

$ pip install --upgrade pymilvus openai requests docling beautifulsoup4
print("Environment setup complete!")

環境変数の設定

埋め込み生成と LLM ベースのフィルター式作成を有効にするために、OpenAI API 認証情報を設定します。'your_openai_api_key' を実際の OpenAI API キーに置き換えてください。

import os
import openai

os.environ["OPENAI_API_KEY"] = "your_openai_api_key"
api_key = os.getenv("OPENAI_API_KEY")

if not api_key:
    raise ValueError("Please set the OPENAI_API_KEY environment variable!")

openai.api_key = api_key
print("API key loaded.")

サンプルコレクションの作成

それでは、ユーザーデータを含むMilvusコレクションのサンプルを作成してみましょう。このコレクションにはスカラーフィールド(フィルタリング用)とベクトル埋め込み(セマンティック検索用)の両方が含まれます。OpenAIのテキスト埋め込みモデルを使用して、ユーザ情報のベクトル表現を生成します。

from pymilvus import MilvusClient, FieldSchema, CollectionSchema, DataType
import os
from openai import OpenAI
import uuid

client = MilvusClient(uri="http://localhost:19530")
openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
embedding_model = "text-embedding-3-small"
embedding_dim = 1536

fields = [
    FieldSchema(
        name="pk",
        dtype=DataType.VARCHAR,
        is_primary=True,
        auto_id=False,
        max_length=100,
    ),
    FieldSchema(name="name", dtype=DataType.VARCHAR, max_length=128),
    FieldSchema(name="age", dtype=DataType.INT64),
    FieldSchema(name="city", dtype=DataType.VARCHAR, max_length=128),
    FieldSchema(name="hobby", dtype=DataType.VARCHAR, max_length=128),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=embedding_dim),
]
schema = CollectionSchema(fields=fields, description="User data embedding example")
collection_name = "user_data_collection"

if client.has_collection(collection_name):
    client.drop_collection(collection_name)
# Strong consistency waits for all loads to complete, adding latency with large datasets
# client.create_collection(
#     collection_name=collection_name, schema=schema, consistency_level="Strong"
# )
client.create_collection(collection_name=collection_name, schema=schema)

index_params = client.prepare_index_params()
index_params.add_index(
    field_name="embedding",
    index_type="IVF_FLAT",
    metric_type="COSINE",
    params={"nlist": 128},
)
client.create_index(collection_name=collection_name, index_params=index_params)

data_to_insert = [
    {"name": "John", "age": 23, "city": "Shanghai", "hobby": "Drinking coffee"},
    {"name": "Alice", "age": 29, "city": "New York", "hobby": "Reading books"},
    {"name": "Bob", "age": 31, "city": "London", "hobby": "Playing chess"},
    {"name": "Eve", "age": 27, "city": "Paris", "hobby": "Painting"},
    {"name": "Charlie", "age": 35, "city": "Tokyo", "hobby": "Cycling"},
    {"name": "Grace", "age": 22, "city": "Berlin", "hobby": "Photography"},
    {"name": "David", "age": 40, "city": "Toronto", "hobby": "Watching movies"},
    {"name": "Helen", "age": 30, "city": "Sydney", "hobby": "Cooking"},
    {"name": "Frank", "age": 28, "city": "Beijing", "hobby": "Hiking"},
    {"name": "Ivy", "age": 26, "city": "Seoul", "hobby": "Dancing"},
    {"name": "Tom", "age": 33, "city": "Madrid", "hobby": "Writing"},
]


def get_embeddings(texts):
    return [
        rec.embedding
        for rec in openai_client.embeddings.create(
            input=texts, model=embedding_model, dimensions=embedding_dim
        ).data
    ]


texts = [
    f"{item['name']} from {item['city']} is {item['age']} years old and likes {item['hobby']}."
    for item in data_to_insert
]
embeddings = get_embeddings(texts)

insert_data = []
for item, embedding in zip(data_to_insert, embeddings):
    item_with_embedding = {
        "pk": str(uuid.uuid4()),
        "name": item["name"],
        "age": item["age"],
        "city": item["city"],
        "hobby": item["hobby"],
        "embedding": embedding,
    }
    insert_data.append(item_with_embedding)

client.insert(collection_name=collection_name, data=insert_data)

print(f"Collection '{collection_name}' has been created and data has been inserted.")

上記のコードは、以下の構造を持つMilvusコレクションを作成します:

  • pk:主キーフィールド (VARCHAR)
  • name: ユーザー名 (VARCHAR)
  • age:ユーザー年齢 (INT64)
  • city:ユーザーの都市 (VARCHAR)
  • 趣味:ユーザーの趣味 (VARCHAR)
  • 埋め込み:ベクトル埋め込み (FLOAT_VECTOR, 1536次元)

11人のサンプルユーザを個人情報と一緒に挿入し、セマンティック検索機能のための埋め込みを生成した。各ユーザの情報は、埋め込まれる前に、名前、場所、年齢、興味を捕らえた記述的なテキストに変換されます。いくつかのサンプルレコードをクエリすることで、コレクションが正常に作成され、期待されるデータが含まれていることを確認してみましょう。

from pymilvus import MilvusClient
import os
from openai import OpenAI

client = MilvusClient(uri="http://localhost:19530")
collection_name = "user_data_collection"

client.load_collection(collection_name=collection_name)

result = client.query(
    collection_name=collection_name,
    filter="",
    output_fields=["name", "age", "city", "hobby"],
    limit=3,
)

for record in result:
    print(record)

Milvusフィルター表現ドキュメントの収集

大規模言語モデルがMilvusのフィルタ式の構文をより理解できるように、関連する公式ドキュメントを提供する必要があります。ここでは、docling ライブラリを使用して、Milvusの公式ウェブサイトからいくつかの主要なページをスクレイピングします。

これらのページには以下の詳細情報が含まれています:

  • ブール演算子 and or, 複雑な論理条件用not
  • 基本的な演算子==,!=,>,<,>= のような比較演算子、<=
  • フィルタリングテンプレート:高度なフィルタリングパターンと構文
  • 文字列マッチング:like およびその他の文字列操作によるパターン・マッチング

このドキュメントは、LLMが正確なフィルター式を生成するための知識ベースとなる。

import docling
from docling.document_converter import DocumentConverter

converter = DocumentConverter()
docs = [
    converter.convert(url)
    for url in [
        "https://milvus.io/docs/boolean.md",
        "https://milvus.io/docs/basic-operators.md",
        "https://milvus.io/docs/filtering-templating.md",
    ]
]

for doc in docs[:3]:
    print(doc.document.export_to_markdown())

ドキュメントのスクレイピングはMilvusのフィルター構文を包括的にカバーしています。この知識ベースにより、LLMは適切な演算子の使い方、フィールドの参照、複雑な条件の組み合わせなど、フィルタ式の構築のニュアンスを理解することができます。

LLMによるフィルター生成

ドキュメントのコンテキストを理解したところで、フィルター式を生成するためにLLMシステムをセットアップしてみましょう。スクレイピングされたドキュメントとユーザークエリを組み合わせて、構文的に正しいMilvusフィルター式を生成する構造化プロンプトを作成します。

このフィルター生成システムでは、以下のような入念に作られたプロンプトを使用します:

  1. コンテキストを提供する:Milvusドキュメントを参照資料として含む。
  2. 制約の設定:LLMが文書化された構文と機能のみを使用することを保証します。
  3. 正確さを強制します:構文的に正しい表現を要求
  4. フォーカスの維持説明なしでフィルター式だけを返す

自然言語クエリでこれをテストし、LLMのパフォーマンスを確認してみよう。

from openai import OpenAI
import json
from IPython.display import display, Markdown

context = "\n".join([doc.document.export_to_markdown() for doc in docs])

prompt = f"""
You are an expert Milvus vector database engineer. Your task is to convert a user's natural language query into a valid Milvus filter expression, using the provided Milvus documentation as your knowledge base.

Follow these rules strictly:
1. Only use the provided documents as your source of knowledge.
2. Ensure the generated filter expression is syntactically correct.
3. If there isn't enough information in the documents to create an expression, state that directly.
4. Only return the final filter expression. Do not include any explanations or extra text.

---
**Milvus Documentation Context:**
{context}

---
**User Query:**
{user_query}

---
**Filter Expression:**
"""

client = OpenAI()


def generate_filter_expr(user_query):
    """
    Generates a Milvus filter expression from a user query using GPT-4o-mini.
    """
    completion = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": prompt},
            {"role": "user", "content": user_query},
        ],
        temperature=0.0,
    )
    return completion.choices[0].message.content


user_query = "Find people older than 30 who live in London, Tokyo, or Toronto"

filter_expr = generate_filter_expr(user_query)

print(f"Generated filter expression: {filter_expr}")

LLMは複数の条件を組み合わせたフィルター式の生成に成功しました:

  • を使った年齢比較>
  • in 演算子を使った複数の都市のマッチング
  • 適切なフィールド参照と構文

これは、LLMのフィルター生成をガイドするために、包括的なドキュメントのコンテキストを提供することの威力を示しています。

生成されたフィルタをテストする

それでは、生成されたフィルター式を実際のMilvus検索オペレーションで使用してテストしてみましょう。セマンティック検索と正確なフィルタリングを組み合わせて、クエリの意図と特定の条件の両方に一致するユーザーを検索します。

from pymilvus import MilvusClient
from openai import OpenAI
import os

client = MilvusClient(uri="http://localhost:19530")
openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

clean_filter = (
    filter_expr.replace("```", "").replace('filter="', "").replace('"', "").strip()
)
print(f"Using filter: {clean_filter}")

query_embedding = (
    openai_client.embeddings.create(
        input=[user_query], model="text-embedding-3-small", dimensions=1536
    )
    .data[0]
    .embedding
)

search_results = client.search(
    collection_name="user_data_collection",
    data=[query_embedding],
    limit=10,
    filter=clean_filter,
    output_fields=["pk", "name", "age", "city", "hobby"],
    search_params={
        "metric_type": "COSINE",
        "params": {"nprobe": 10},
    },
)

print("Search results:")
for i, hits in enumerate(search_results):
    print(f"Query {i}:")
    for hit in hits:
        print(f"  - {hit}")
    print()

結果分析

検索結果は、LLMが生成したフィルタとMilvusベクトル検索がうまく統合されていることを示している。フィルタは以下のユーザーを正しく識別した:

  • 年齢が30歳以上
  • ロンドン、東京、トロントに住んでいる
  • クエリのセマンティックコンテキストにマッチしている

このアプローチは、構造化されたフィルタリングの精度と自然言語入力の柔軟性を組み合わせることで、特定のクエリ構文に慣れていないユーザでもベクトルデータベースをより利用しやすくしている。