대규모 언어 모델을 사용하여 Milvus 쿼리 필터 표현식 생성하기

이 튜토리얼에서는 대규모 언어 모델(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)
  • hobby: 사용자 취미(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 시스템을 설정해 보겠습니다. 스크랩한 문서와 사용자 쿼리를 결합하여 구문적으로 올바른 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세 이상
  • 런던, 도쿄 또는 토론토에 거주
  • 쿼리의 시맨틱 컨텍스트와 일치

이 접근 방식은 구조화된 필터링의 정확성과 자연어 입력의 유연성을 결합하여 특정 쿼리 구문에 익숙하지 않은 사용자도 벡터 데이터베이스에 더 쉽게 접근할 수 있도록 합니다.