Векторные базы данных: Боль выбора и радость простоты (pgvector)

06.06.2026 17:00

В 2023 году индустрию накрыла волна интереса к векторным базам данных. Pinecone, Milvus, Qdrant, Weaviate привлекали сотни миллионов долларов инвестиций. Маркетинговые материалы убеждали инженеров: если вы строите AI-продукт, реализуете семантический поиск или RAG (Retrieval-Augmented Generation), вам жизненно необходим специализированный кластер для хранения эмбеддингов. Реляционные базы объявлялись устаревшим наследием, неспособным справиться с многомерными массивами.

Мы поддались этому давлению. На старте разработки корпоративной системы анализа документации команда приняла решение развернуть отдельную векторную базу данных. На бумаге архитектура выглядела современно: микросервисы, четкое разделение ответственности, специализированные хранилища под каждый тип нагрузки. На практике мы собственными руками заложили фундамент для ежедневной операционной боли.

Проблемы начались на этапе синхронизации состояний. Документ — это не просто текст. Это метаданные, права доступа, статусы обработки, связи с другими сущностями. Вся эта реляционная структура закономерно жила в PostgreSQL. Векторные представления текста отправлялись в специализированную БД.

Мгновенно исчезла транзакционная целостность. Операция добавления документа перестала быть атомарной. Если PostgreSQL успешно фиксировал транзакцию, а сетевой вызов к векторной базе отваливался по таймауту, система оставалась в неконсистентном состоянии. Документ существует, но найти его семантическим поиском невозможно.

Обратная ситуация оказалась еще хуже. При удалении документа из реляционной базы запрос на удаление вектора мог потеряться. В результатах поиска начинали всплывать "призраки" — релевантные куски текста, которые вели на несуществующие страницы. Нам пришлось писать фоновые воркеры для сверки состояний, реализовывать паттерн Outbox, добавлять очереди сообщений.

Инфраструктурные расходы тоже росли. Векторные базы данных требовательны к оперативной памяти. Нам пришлось поднять отдельный кластер, настроить для него резервное копирование, интегрировать новые метрики в Prometheus, обучить дежурных инженеров реагировать на специфичные алерты.

Ради чего мы терпели эти архитектурные унижения? Ради двухсот тысяч векторов. Объем данных, который в бинарном виде занимает меньше места, чем кэш браузера. Это был хрестоматийный пример Resume Driven Development — использования модных технологий ради строчки в резюме, в ущерб здравому смыслу и стабильности продукта.

Отрезвление наступило, когда мы начали профилировать задержки. Выяснилось, что сетевой round-trip между сервисами и двойные запросы (сначала найти ID в векторной базе, затем вытащить метаданные из PostgreSQL) съедают больше времени, чем сам поиск по графу.

Мы посмотрели на наш скучный, надежный PostgreSQL и задали очевидный вопрос: почему мы разносим данные, которые должны лежать вместе?

Ответом стало расширение pgvector.

pgvector превращает PostgreSQL в полноценное векторное хранилище. Оно добавляет нативный тип данных vector, операторы вычисления дистанций (L2, косинусное расстояние, внутреннее произведение) и поддерживает индексы для приближенного поиска ближайших соседей (ANN).

Переезд на pgvector позволил удалить тысячи строк кода, отвечающих за синхронизацию. Данные и их векторные представления вернулись в единую транзакционную границу.

BEGIN;
INSERT INTO documents (title, content, author_id) VALUES ('Отчет 2023', 'Текст..', 42) RETURNING id;
-- Генерация эмбеддинга и сохранение происходят в рамках одной транзакции
INSERT INTO document_embeddings (document_id, embedding) VALUES (1, '[0.1, 0.2, ..]');
COMMIT;

Удаление документа теперь каскадно сносит его векторы через обычный ON DELETE CASCADE. Бэкапы снова стали консистентными из коробки. Мониторинг свелся к привычным метрикам Postgres: размеру буферного кэша и статистике попаданий в индекс.

Для обеспечения производительности pgvector предлагает два типа индексов: IVFFlat и HNSW.

IVFFlat (Inverted File Flat) разбивает пространство векторов на кластеры (списки) вокруг центроидов. Он потребляет мало памяти и быстро строится, но требует предварительного накопления данных. Если построить IVFFlat на пустой таблице, центроиды распределятся неоптимально, и recall (полнота поиска) катастрофически упадет.

HNSW (Hierarchical Navigable Small World) — это индустриальный стандарт. Он строит многослойный граф, где верхние слои содержат длинные связи для быстрой навигации, а нижние — плотные локальные кластеры. HNSW в pgvector не требует прогрева данными, поддерживает инкрементальные обновления и обеспечивает высочайший recall. Платой за это выступает увеличенное потребление RAM и более долгое время вставки, но для большинства read-heavy RAG-сценариев это идеальный компромисс.

-- Создание HNSW индекса для косинусного расстояния
CREATE INDEX ON document_embeddings USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

Параметр m определяет максимальное количество связей для каждого узла графа, а ef_construction — размер динамического списка кандидатов при построении. Тюнинг этих параметров позволяет балансировать между скоростью записи, потреблением памяти и точностью поиска.

Решив проблему хранения, мы оптимизировали процесс векторизации.

Генерация эмбеддингов — вычислительно емкая задача. Поднимать собственные GPU-серверы с моделями вроде BGE-m3 или E5-large имеет смысл только при жестких требованиях к изоляции данных (on-premise) или при объемах в миллиарды токенов в сутки. В остальных случаях аренда железа и зарплата MLOps-инженера многократно превышают стоимость API.

Мы перевели векторизацию на RouterAPI. Этот шлюз предоставляет унифицированный доступ к топовым embedding-моделям. Вместо того чтобы управлять зоопарком ключей и писать логику ретраев для разных провайдеров, мы используем один эндпоинт.

Использование моделей класса text-embedding-3-small через RouterAPI обходится в копейки (доли цента за тысячу токенов), при этом обеспечивая размерность вектора 1536 и отличные показатели на бенчмарке MTEB. RouterAPI прозрачно обрабатывает балансировку нагрузки и фоллбеки. Если один апстрим начинает отдавать 429 Too Many Requests, шлюз автоматически переключает запрос на резервный узел.

Реализация на Go стала тривиальной. Мы используем стандартный database/sql и драйвер pgvector-go.

package main

import (
 "bytes"
 "context"
 "database/sql"
 "encoding/json"
 "fmt"
 "net/http"
 "os"

 "github.com/pgvector/pgvector-go"
 _ "github.com/lib/pq"
)

type EmbeddingRequest struct {
 Model string `json:"model"`
 Input string `json:"input"`
}

type EmbeddingResponse struct {
 Data []struct {
 Embedding []float32 `json:"embedding"`
 } `json:"data"`
}

// getEmbedding обращается к RouterAPI для векторизации текста
func getEmbedding(ctx context.Context, text string) ([]float32, error) {
 reqBody := EmbeddingRequest{
 Model: "text-embedding-3-small",
 Input: text,
 }
 jsonData, _ := json.Marshal(reqBody)

 req, err := http.NewRequestWithContext(ctx, "POST", "https://api.routerapi.net/v1/embeddings", bytes.NewBuffer(jsonData))
 if err != nil {
 return nil, err
 }
 
 req.Header.Set("Authorization", "Bearer "+os.Getenv("ROUTERAPI_KEY"))
 req.Header.Set("Content-Type", "application/json")

 client := &http.Client{}
 resp, err := client.Do(req)
 if err != nil {
 return nil, err
 }
 defer resp.Body.Close

 var embResp EmbeddingResponse
 if err := json.NewDecoder(resp.Body).Decode(&embResp); err != nil {
 return nil, err
 }

 if len(embResp.Data) == 0 {
 return nil, fmt.Errorf("empty embedding response")
 }

 return embResp.Data[0].Embedding, nil
}

// searchSimilar выполняет ANN-поиск в PostgreSQL
func searchSimilar(ctx context.Context, db *sql.DB, queryEmbedding []float32, limit int) error {
 // Оператор <=> вычисляет косинусное расстояние. 
 // Сортировка по нему с LIMIT использует HNSW индекс.
 query := `
 SELECT d.title, 1 - (e.embedding <=> $1) AS similarity
 FROM document_embeddings e
 JOIN documents d ON d.id = e.document_id
 ORDER BY e.embedding <=> $1
 LIMIT $2
 `
 
 rows, err := db.QueryContext(ctx, query, pgvector.NewVector(queryEmbedding), limit)
 if err != nil {
 return err
 }
 defer rows.Close

 for rows.Next {
 var title string
 var similarity float64
 if err := rows.Scan(&title, &similarity); err != nil {
 return err
 }
 fmt.Printf("Similarity: %.4f | Title: %s\n", similarity, title)
 }
 return rows.Err
}

Этот подход выдерживает серьезные нагрузки. PostgreSQL с грамотно настроенным shared_buffers и HNSW-индексом легко обслуживает десятки миллионов векторов, сохраняя задержки поиска в пределах 5-10 миллисекунд.

Специализированные векторные базы данных не бесполезны. Если архитектура требует поиска по миллиардам изображений, или RAG-система обрабатывает петабайты неструктурированных корпоративных логов в реальном времени, распределенный кластер Milvus или Qdrant оправдает вложенные усилия. Там в игру вступают механизмы шардирования, квантования векторов (Product Quantization) и разделения вычислительных узлов.

Но для подавляющего большинства проектов добавление новой базы данных в стек — это неоправданный риск. Сложность инфраструктуры должна быть пропорциональна масштабу бизнеса, а не маркетинговым бюджетам вендоров. Пока проект не уперся в физические лимиты одного мощного сервера базы данных, архитектуру следует держать максимально скучной.

Связка PostgreSQL с pgvector для хранения и RouterAPI для дешевой и отказоустойчивой векторизации закрывает 99% потребностей современного AI-продукта. Она позволяет инженерам сфокусироваться на качестве поиска, промпт-инжиниринге и бизнес-логике, а не на отладке распределенных транзакций и мониторинге очередного кластера.

Теги

Ещё по теме