В 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-продукта. Она позволяет инженерам сфокусироваться на качестве поиска, промпт-инжиниринге и бизнес-логике, а не на отладке распределенных транзакций и мониторинге очередного кластера.