RAG на практике: поиск по документам с LangChain и pgvector
Строим RAG-систему поиска по документам с LangChain и pgvector на PostgreSQL: установка, индексы HNSW vs IVFFlat, код и лучшие практики.
Представьте: у вас 10 000 внутренних документов компании — регламенты, договоры, техническая документация. Сотрудники тратят часы, пытаясь найти нужный пункт. Поиск по ключевым словам не работает — люди спрашивают «как оформить командировку», а документ называется «Положение о служебных поездках».
RAG (Retrieval-Augmented Generation) решает именно эту проблему. Система понимает смысл вопроса, находит релевантные фрагменты из базы знаний и генерирует точный ответ с указанием источника. В этом руководстве мы построим такую систему с нуля — с реальным кодом, PostgreSQL, pgvector и LangChain.
Что такое RAG и почему pgvector
RAG — это архитектурный паттерн, при котором языковая модель не «придумывает» ответ из обучающих данных, а сначала извлекает релевантные документы из внешней базы знаний, а затем генерирует ответ на их основе. Это решает главную проблему LLM — галлюцинации и устаревшие знания.
graph LR
A[Вопрос пользователя] --> B[Модель эмбеддингов]
B --> C[Векторный запрос]
C --> D[(pgvector / PostgreSQL)]
D --> E[Топ-K документов]
E --> F[Контекст + вопрос]
F --> G[LLM Claude / GPT]
G --> H[Ответ с источниками]
Почему pgvector, а не специализированная векторная БД?
Многие команды уже используют PostgreSQL. Добавить расширение pgvector проще, чем поднимать и поддерживать отдельный Pinecone, Weaviate или Qdrant. Для большинства проектов с объёмом до нескольких миллионов документов производительности pgvector вполне достаточно.
| Параметр | pgvector | Pinecone | Weaviate |
|---|---|---|---|
| Инфраструктура | Существующий PostgreSQL | Managed SaaS | Self-hosted / Cloud |
| Стоимость при 1M векторов | ~$0 (сервер уже есть) | от $70/мес | от $25/мес |
| SQL + векторы в одном запросе | Да | Нет | Нет |
| Масштаб (макс. рекомендуемый) | ~10M векторов | Миллиарды | Миллиарды |
| Транзакционность | ACID | Нет | Нет |
langchain-community в отдельный пакет langchain-postgres. С версии 0.0.14 устаревший класс PGVector заменён на PGVectorStore. В этом руководстве используется актуальный API.Установка и подготовка окружения
Зависимости
pip install langchain langchain-postgres langchain-openai \
psycopg[binary] python-dotenv pypdf
Для PostgreSQL с pgvector проще всего использовать Docker:
docker run -d \
--name pgvector-db \
-e POSTGRES_USER=rag_user \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=rag_db \
-p 5432:5432 \
pgvector/pgvector:pg16
Официальный образ pgvector/pgvector идёт с уже установленным расширением. Если используете существующий PostgreSQL, подключите расширение вручную:
CREATE EXTENSION IF NOT EXISTS vector;
Структура проекта
rag-system/
├── .env
├── ingest.py # Загрузка и индексация документов
├── query.py # RAG-запросы
└── documents/ # PDF, TXT, MD-файлы
Файл .env:
OPENAI_API_KEY=sk-...
DATABASE_URL=postgresql+psycopg://rag_user:secret@localhost:5432/rag_db
Индексация документов
Загрузка и разбивка текста
# ingest.py
import os
from pathlib import Path
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_postgres import PGEngine, PGVectorStore
load_dotenv()
# Настройка движка БД
engine = PGEngine.from_connection_string(url=os.environ["DATABASE_URL"])
# Инициализация таблицы (создаётся один раз)
engine.init_vectorstore_table(
table_name="documents",
vector_size=1536, # размер для text-embedding-3-small
)
# Модель эмбеддингов
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Создание векторного хранилища
store = PGVectorStore.create_sync(
engine=engine,
table_name="documents",
embedding_service=embeddings,
)
def load_documents(folder: str):
docs = []
for path in Path(folder).rglob("*"):
if path.suffix == ".pdf":
loader = PyPDFLoader(str(path))
elif path.suffix in (".txt", ".md"):
loader = TextLoader(str(path), encoding="utf-8")
else:
continue
docs.extend(loader.load())
return docs
def ingest(folder: str = "documents"):
raw_docs = load_documents(folder)
print(f"Загружено документов: {len(raw_docs)}")
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ".", " "],
)
chunks = splitter.split_documents(raw_docs)
print(f"Чанков для индексации: {len(chunks)}")
store.add_documents(chunks)
print("Индексация завершена.")
if __name__ == "__main__":
ingest()
chunk_size=1000 с chunk_overlap=200 — хорошая отправная точка. Маленькие чанки (300–500 символов) дают точность, но теряют контекст. Большие (2000+) дают контекст, но снижают точность поиска. Для технических документов с формулами попробуйте chunk_size=500.Создание индекса HNSW
После загрузки документов создайте индекс для ускорения поиска. Подключитесь к PostgreSQL и выполните:
-- HNSW-индекс для косинусного сходства
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Параметры HNSW:
m— количество связей на узел графа (16 — стандарт, больше = точнее, но медленнее сборка)ef_construction— размер динамического списка при построении (64–128 — разумный диапазон)
HNSW обеспечивает в 15× большую пропускную способность по сравнению с IVFFlat при одинаковом уровне полноты (recall 0.998). Для RAG-систем HNSW — выбор по умолчанию.
Сравнение индексов: HNSW vs IVFFlat
Выбор типа индекса напрямую влияет на производительность системы.
| Характеристика | HNSW | IVFFlat |
|---|---|---|
| Скорость поиска (QPS при recall 0.998) | 40.5 | 2.6 |
| Время построения индекса | 4065 сек | 128 сек |
| Память (на 1M векторов) | 729 МБ | 257 МБ |
| Требует предварительной кластеризации | Нет | Да (нужны данные заранее) |
| Поддержка инкрементальных вставок | Хорошая | Деградирует со временем |
| Настройка | 2 параметра | 2 параметра |
RAG-запросы: сборка цепочки
# query.py
import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_postgres import PGEngine, PGVectorStore
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
load_dotenv()
engine = PGEngine.from_connection_string(url=os.environ["DATABASE_URL"])
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
store = PGVectorStore.create_sync(
engine=engine,
table_name="documents",
embedding_service=embeddings,
)
# Ретривер: топ-5 релевантных чанков
retriever = store.as_retriever(
search_type="similarity",
search_kwargs={"k": 5},
)
# Промпт с контекстом
PROMPT_TEMPLATE = """Ты — помощник по работе с документами компании.
Отвечай на вопрос ТОЛЬКО на основе предоставленного контекста.
Если информации недостаточно — так и скажи, не придумывай.
Контекст:
{context}
Вопрос: {question}
Ответ:"""
prompt = ChatPromptTemplate.from_template(PROMPT_TEMPLATE)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def format_docs(docs):
return "\n\n---\n\n".join(
f"[Источник: {d.metadata.get('source', 'неизвестно')}]\n{d.page_content}"
for d in docs
)
# LCEL-цепочка
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
def ask(question: str) -> str:
return rag_chain.invoke(question)
if __name__ == "__main__":
answer = ask("Как оформить заявление на отпуск?")
print(answer)
Вопрос: Как оформить заявление на отпуск?
Ответ: Согласно Положению о предоставлении отпусков (раздел 3.2), заявление подаётся не позднее чем за 14 дней до начала отпуска через систему HR-портал или в бумажном виде на имя непосредственного руководителя. [Источник: hr_policy_2024.pdf]
Продвинутые техники
Гибридный поиск: семантика + ключевые слова
Чистый семантический поиск иногда упускает документы с точными совпадениями (артикулы, имена, коды). Решение — гибридный поиск через tsvector PostgreSQL:
-- Добавляем полнотекстовый поиск к таблице
ALTER TABLE documents ADD COLUMN fts_content tsvector
GENERATED ALWAYS AS (to_tsvector('russian', cmetadata->>'content')) STORED;
CREATE INDEX ON documents USING gin(fts_content);
Затем комбинируем результаты через RRF (Reciprocal Rank Fusion) — суммируем ранги из семантического и полнотекстового поиска.
Фильтрация по метаданным
pgvector поддерживает фильтрацию до векторного поиска:
# Искать только в документах отдела HR
results = store.similarity_search(
query="отпуск",
k=5,
filter={"department": "HR", "year": 2024},
)
Мониторинг качества
Отслеживайте две ключевые метрики:
| Метрика | Что измеряет | Как считать |
|---|---|---|
| Recall@K | Доля релевантных документов в топ-K | Тестовая выборка с размеченными ответами |
| Faithfulness | Соответствие ответа контексту | LLM-as-judge или RAGAs framework |
pip install ragas
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall
# Запускаем оценку на тестовом датасете
results = evaluate(dataset, metrics=[faithfulness, answer_relevancy, context_recall])
print(results)
Заключение
Мы построили полноценную RAG-систему: от загрузки документов через LangChain до поиска по векторному индексу в PostgreSQL и генерации ответов с LLM.
Ключевые выводы:
langchain-postgresсPGVectorStore— актуальный API (старыйPGVectorизlangchain-communityустарел)- HNSW-индекс — выбор по умолчанию для большинства RAG-систем: быстрее, не требует перестройки при добавлении данных
- Размер чанка (1000 символов) и перекрытие (200 символов) — отправная точка, подбирайте под свою задачу
- Гибридный поиск (семантика + FTS) улучшает recall на запросах с конкретными терминами
- Измеряйте качество через RAGAs, а не только субъективную оценку
PostgreSQL с pgvector — прагматичный выбор для команд, которые хотят RAG без дополнительной инфраструктуры. Если ваш объём вырастет до десятков миллионов векторов — рассмотрите миграцию на специализированные решения. До этого момента pgvector справится.
Источники
- https://python.langchain.com/docs/integrations/vectorstores/pgvector/
- https://github.com/langchain-ai/langchain-postgres
- https://aws.amazon.com/blogs/database/optimize-generative-ai-applications-with-pgvector-indexing-a-deep-dive-into-ivfflat-and-hnsw-techniques/
- https://deepwiki.com/langchain-ai/langchain-postgres/3.1-pgvectorstore-(current-implementation)