Реалізація Self-Query RAG з метаданими
Self-Query RAG — техніка, при якій LLM аналізує запит користувача та автоматично конструює структурований фільтр за метаданими в доповнення до векторного пошуку. Замість пошуку тільки за семантикою, система точково фільтрує документи за датою, типом, автором, відділом та іншими атрибутами.
Проблема без Self-Query
Без Self-Query запит "політики безпеки, видані в 2024 році" ищет все документи за семантикою "безопасность", не фільтруючи за роком. Користувач отримує змішані результати різних періодів. З Self-Query: LLM видобуває фільтр date >= 2024-01-01 AND doc_type = "security_policy" та застосовує його разом з векторним пошуком.
Реалізація через LangChain SelfQueryRetriever
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Qdrant
# Опис метаданих для LLM
metadata_field_info = [
AttributeInfo(
name="doc_type",
description="Тип документу: contract, regulation, policy, faq, procedure",
type="string",
),
AttributeInfo(
name="department",
description="Відділ або підрозділ: hr, legal, finance, it, security",
type="string",
),
AttributeInfo(
name="year",
description="Рік публікації документу",
type="integer",
),
AttributeInfo(
name="status",
description="Статус документу: active, archived, draft",
type="string",
),
AttributeInfo(
name="author",
description="Автор або відповідальна особа за документ",
type="string",
),
]
document_content_description = "Корпоративна документація компанії: регламенти, політики, договори, процедури"
llm = ChatOpenAI(model="gpt-4o", temperature=0)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vectorstore,
document_contents=document_content_description,
metadata_field_info=metadata_field_info,
enable_limit=True, # Дозволяє LLM вказувати limit у запиті
verbose=True,
)
Приклади Self-Query
# Приклад 1: Фільтр за роком та типом
result = retriever.invoke(
"Які політики безпеки діяли в 2023 році?"
)
# LLM генерує фільтр: {"doc_type": "policy", "department": "security", "year": 2023, "status": "active"}
# Потім виконує векторний пошук з цим фільтром
# Приклад 2: Фільтр за відділом
result = retriever.invoke(
"Покажи регламенти HR відділу"
)
# Фільтр: {"doc_type": "regulation", "department": "hr"}
# Приклад 3: Без фільтра (чистий векторний пошук)
result = retriever.invoke(
"Як підготуватися до аудиту?"
)
# LLM не видобуває структурованих фільтрів — чистий semantic search
Кастомна реалізація Self-Query без LangChain
from pydantic import BaseModel, Field
from typing import Optional
from openai import OpenAI
import json
class SearchFilter(BaseModel):
semantic_query: str = Field(description="Чисто семантична частина для векторного пошуку")
doc_type: Optional[str] = Field(default=None, description="Тип документу")
department: Optional[str] = Field(default=None, description="Відділ")
year_from: Optional[int] = Field(default=None, description="Рік від (включно)")
year_to: Optional[int] = Field(default=None, description="Рік до (включно)")
status: Optional[str] = Field(default=None, description="Статус: active/archived")
def parse_query_to_filter(user_query: str, client: OpenAI) -> SearchFilter:
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": "Видобути структуровані фільтри з запиту користувача для пошуку документів."
}, {
"role": "user",
"content": user_query
}],
response_format=SearchFilter,
temperature=0,
)
return response.choices[0].message.parsed
def self_query_search(user_query: str, vectorstore, top_k: int = 5) -> list:
filter_obj = parse_query_to_filter(user_query, openai_client)
# Будуємо фільтр для Qdrant
qdrant_filter = build_qdrant_filter(filter_obj)
return vectorstore.similarity_search(
filter_obj.semantic_query,
k=top_k,
filter=qdrant_filter,
)
Практичний кейс: корпоративна база знаній з метаданими
Задача: пошуковий асистент для 15 000 внутрішніх документів з метаданими (тип, відділ, рік, статус, автор).
До Self-Query: 42% запитів повертали архівні документи замість актуальних.
Після Self-Query:
- Архівні документи в результатах для "актуальних" запитів: 42% → 3%
- Precision@5: 0.68 → 0.89
- Задоволеність користувачів: +31%
Failure cases: LLM іноді неправильно інтерпретує параметри фільтра при неоднозначних запитах. Рішення — додати confidence threshold та fallback на pure semantic search при низькій впевненості.
Графіки
- Розмітка метаданих документів: 1–3 тижні (залежить від наявності даних)
- Реалізація Self-Query Retriever: 3–5 днів
- Тестування та підбір промпта: 3–5 днів
- Всього: 2–5 тижнів







