DanLevy.net

Поиск по семантическим векторам и другие темы для завоевания друзей и поклонников

Весь ландшафт поиска: точный, нечёткий, семантический, гибридный — и когда стоит комбинировать их все.

Поиск — это не одна вещь, и семантический поиск не заменяет всё остальное.

«Найти пользователя с почтой dan@example.com» и «найди мне статьи об отладке для начинающего инженера» — оба запроса называются поиском, но как инженерные задачи они почти не имеют общего. У первого есть правильный ответ и поиск по индексу за O(log n). У второго правильного ответа нет — есть только релевантность, — и требуется понимание языка, намерения и смысла.

Инженеры, которые убедительнее всего принимают решения о поиске — те, кто выигрывает споры и строит правильную систему, — понимают весь ландшафт. Они знают, какой инструмент взять и почему, и могут объяснить это чётко.

В этой статье рассматривается семантический слой: что на самом деле делает векторный поиск, где он выигрывает и где ему лучше не мешать. Полезный ответ — не «превратите всё в эмбеддинги». Полезный ответ — знать, когда векторы должны стоять рядом с лексическим, нечётким и точным поиском в гибридной архитектуре.

Лексическая и нечёткая половина картины — tsvector, pg_trgm, pg_search — описана в Postgres Text Searching Guide 2026.


Термины: краткий справочник

Векторное представление (embedding) — плотный список чисел с плавающей точкой, выдаваемый моделью; представляет фрагмент текста (или изображение, аудио и т. д.) как точку в многомерном пространстве. Семантически близкий контент оказывается рядом; далёкий — далеко.

Лексический поиск — поиск по точному совпадению слов и токенов. Быстрый, детерминированный, корректный для известных терминов. Не понимает синонимы, перефразировки или эквиваленты на других языках.

Семантический поиск — поиск по смыслу, а не по токенам. Запрос «как обрабатывать таймауты» может найти документ «настройка политик повторных попыток» без единого общего слова, потому что их векторные представления геометрически близки.

Вектор — список чисел. В контексте поиска — вывод модели эмбеддингов. «Векторный поиск» находит векторы, ближайшие к вектору запроса, по геометрическому расстоянию.

FTS (полнотекстовый поиск) — встроенный лексический поиск Postgres на основе tsvector / tsquery. Токенизирует, стеммирует и индексирует текст для запросов по ключевым словам. Силён для прозы и поиска по точным терминам; слеп к смыслу.

BM25 — алгоритм ранжирования для лексического поиска (используется в Elasticsearch, Qdrant и других). Оценивает результаты по частоте термина, взвешенной по редкости термина в корпусе. Лучше голого сопоставления ключевых слов; всё ещё лексический.

HNSW (Hierarchical Navigable Small World) — стандартный индекс приблизительного поиска ближайших соседей для векторного поиска. Строит многоуровневый граф близости для быстрых запросов с высокой полнотой. pgvector, Qdrant, Weaviate и большинство других используют его.

RRF (Reciprocal Rank Fusion) — алгоритм слияния ранжированных списков результатов из нескольких систем поиска. Использует только позицию в ранге — нормализация скоров не нужна. Результат, высоко стоящий в обоих списках (FTS и векторном), получает более сильный комбинированный скор, чем тот, что доминирует только в одном.


Что на самом деле делает семантический поиск

Векторные эмбеддинги преобразуют текст (или изображения, аудио и т. д.) в список чисел — точку в многомерном пространстве. Модель эмбеддингов обучена так, что семантически связанный текст оказывается рядом в этом пространстве. «Собака» и «пёс» оказываются близко. «Бежать марафон» и «запустить Python-скрипт» оказываются далеко друг от друга, несмотря на общее слово.

Поиск похожести в этом пространстве находит документы, чей смысл ближе всего к смыслу запроса, независимо от совпадения слов.

Это означает:

Лексический поиск (tsvector, pg_trgm) ничего из этого не умеет. Он работает со словами и символами, а не со смыслом. Эти инструменты не взаимозаменяемы — они решают разные задачи.


Когда pgvector выигрывает

Построение RAG. Retrieval-Augmented Generation извлекает фрагменты документов, смысл которых ближе всего к вопросу пользователя, а затем передаёт их языковой модели как контекст. Этот шаг извлечения — векторная операция. FTS пропустит перефразировки, синонимы и концептуальные совпадения, которые релевантный фрагмент может выразить иначе. Преимущество pgvector перед отдельным векторным хранилищем: он работает внутри вашего существующего экземпляра Postgres — не нужно разворачивать, обслуживать или синхронизировать отдельный сервис.

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

Поиск похожих элементов. Похожие товары, аналогичные тикеты поддержки, дубликаты баг-репортов, статьи, которые могут понравиться. «Найти задачи, похожие на эту» — это поиск ближайших соседей: встраиваем элемент, находим его геометрических соседей. Важное ограничение: векторный поиск всегда возвращает результаты, даже когда ничего по-настоящему похожего нет. Для дедупликации и рекомендаций фильтруйте по минимальному порогу похожести (например, косинусная похожесть ≥ 0,80), чтобы не выдавать низковероятные совпадения за значимые.

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

Многоязычный поиск. Многоязычные модели эмбеддингов отображают семантически эквивалентный контент на разных языках в близкие векторы. Запрос на испанском «perder peso» может найти английскую статью о «sustainable weight loss habits» — ни одного общего токена, тот же смысл. FTS требует настройки словарей для каждого языка и плохо справляется с кросс-язычными запросами. pg_trgm не зависит от языка, но работает на уровне орфографии, а не смысла.

Настройка pgvector

От установки расширения до запроса по похожести — всё укладывается в несколько SQL-команд:

CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE documents ADD COLUMN embedding vector(1536);
-- HNSW обычно стоит попробовать первым для датасетов среднего размера
CREATE INDEX documents_embedding_idx
ON documents USING hnsw (embedding vector_cosine_ops);
-- Запрос семантического поиска
SELECT id, title, 1 - (embedding <=> $1::vector) AS similarity
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 10;

<=> — косинусное расстояние. 1 - cosine_distance даёт косинусную похожесть (1,0 = идентично, 0,0 = ортогонально). Для ivfflat (более старой, но быстрее строящейся альтернативы) используйте lists = sqrt(row_count) как отправную точку.

Что pgvector обрабатывает плохо


Гибридный поиск: аргумент за оба подхода

Техническая документация — самый очевидный пример, где одного инструмента недостаточно.

Пользователи, ищущие «как настроить таймауты», нуждаются в концептуальном совпадении: статья «Установка политик повторных попыток и лимитов соединений» не содержит общих ключевых слов, но это именно то, что им нужно.

Те же пользователи ищут withRetry(), ECONNRESET и ERR_SOCKET_TIMEOUT. Эти точные строки должны встретиться — семантический поиск может не найти их надёжно, а ложноположительный результат (концептуально похожий, но не тот API) будет откровенно вводить в заблуждение.

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

Решение — гибридный поиск: запустить оба и слить результаты.

Reciprocal Rank Fusion

Reciprocal Rank Fusion (RRF) — стандартный алгоритм объединения ранжированных списков из разных систем поиска. Он не требует нормализации скоров между системами — использует только позиции в ранге. Результат, высоко стоящий в обоих списках, получает более сильный комбинированный скор, чем тот, что доминирует только в одном.

WITH fts_results AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank
FROM documents, to_tsquery('english', $1) query
WHERE search_vector @@ query
LIMIT 50
),
vector_results AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY embedding <=> $2::vector) AS rank
FROM documents
ORDER BY embedding <=> $2::vector
LIMIT 50
),
rrf AS (
SELECT
COALESCE(f.id, v.id) AS id,
COALESCE(1.0 / (60 + f.rank), 0) +
COALESCE(1.0 / (60 + v.rank), 0) AS rrf_score
FROM fts_results f
FULL OUTER JOIN vector_results v ON f.id = v.id
)
SELECT d.id, d.title, rrf.rrf_score
FROM rrf
JOIN documents d ON d.id = rrf.id
ORDER BY rrf_score DESC
LIMIT 10;

60 в знаменателе — константа RRF. Более высокие значения сглаживают разницу позиций; более низкие — усиливают. Значение по умолчанию 60 хорошо работает для большинства типов контента.

RRF позволяет избежать более сложной задачи нормализации ts_rank (логарифмически-частотный скор) относительно косинусного расстояния (геометрическая мера). Они не сопоставимы. RRF спрашивает только: «как высоко этот результат стоял в каждом списке?»

Гибридный поиск с триграммами

Для пользовательского поиска по смешанному контенту — где в одной сессии могут искать имя человека, концепцию или точный термин — трёхстороннее слияние обрабатывает всё:

WITH trgm_results AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY similarity(title, $1) DESC) AS rank
FROM documents
WHERE title % $1
LIMIT 50
),
fts_results AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, to_tsquery('english', $1)) DESC) AS rank
FROM documents
WHERE search_vector @@ to_tsquery('english', $1)
LIMIT 50
),
vector_results AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY embedding <=> $2::vector) AS rank
FROM documents
ORDER BY embedding <=> $2::vector
LIMIT 50
),
rrf AS (
SELECT
COALESCE(t.id, f.id, v.id) AS id,
COALESCE(1.0 / (60 + t.rank), 0) +
COALESCE(1.0 / (60 + f.rank), 0) +
COALESCE(1.0 / (60 + v.rank), 0) AS rrf_score
FROM trgm_results t
FULL OUTER JOIN fts_results f ON t.id = f.id
FULL OUTER JOIN vector_results v ON COALESCE(t.id, f.id) = v.id
)
SELECT d.id, d.title, rrf.rrf_score
FROM rrf
JOIN documents d ON d.id = rrf.id
ORDER BY rrf_score DESC
LIMIT 10;

Это обрабатывает: нечёткие совпадения имён (триграммы), точные совпадения ключевых слов (FTS) и концептуальные запросы (вектор). Одно поисковое поле может обслуживать все три намерения пользователя.


Многослойные гибридные архитектуры

Реальные приложения редко имеют одну поисковую поверхность. У них несколько, каждая со своей потребностью:

ПоверхностьЧто ищут пользователиРекомендуемые слои
Поиск по блогу / документацииКлючевые слова + концепцииFTS + pgvector (RRF)
Поиск по именам пользователей/клиентовИмена с опечаткамиpg_trgm
Поиск товаровНазвания, описания, «похожие на»pg_trgm + FTS + pgvector
Дедупликация тикетов поддержки«Задачи, похожие на эту»Только pgvector
Поиск по SKU/заказамТочные идентификаторыB-tree индекс
RAG по большой базе знанийВопросы на естественном языкеpgvector (фрагментированные документы)
E-commerce «вам может понравиться»Поведенческая + семантическая похожестьpgvector
АвтодополнениеПрефикс, устойчивость к ошибкамpg_trgm

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

Эмпирическое правило

Добавляйте слой, когда появляется режим отказа, который текущий слой не может исправить:


Если всё-таки нужно выделенное векторное хранилище

pgvector обрабатывает множество сценариев до того, как понадобится другая база данных. Грубая граница зависит от количества векторов, настроек индекса, частоты записи, фильтров, оборудования и конкуренции, поэтому относитесь к правилу «до 10 млн векторов» как к начальному предположению для бенчмарка, а не как к ограничению продукта. Когда вы действительно вырастаете за эти рамки — очень высокая конкуренция, очень низкие требования к p99 задержке, миллиарды векторов или серьёзные потребности в мультитенантной изоляции — ландшафт выделенных векторных баз данных широк и заслуживает изучения.

Что на самом деле означают столбцы матрицы

Гибридный поиск означает, что поиск по ключевым словам BM25 и векторная похожесть выполняются в одном запросе, сливаются через RRF. Без него приходится либо выбирать один режим поиска, либо сливать два запроса самостоятельно.

Редкие (sparse) векторы идут дальше, чем BM25. Редкий вектор SPLADE имеет ~30 000 измерений (по одному на термин словаря), ~98% нулей. Ненулевые позиции показывают, какие термины важны и насколько. Запрос «dogs» также взвешивает «canine» и «pet» — точность уровня BM25 плюс расширение терминов внутри векторного индекса. Если этот столбец равен false, вам нужен отдельный слой FTS для запросов по точным терминам.

# SPLADE: ~30 000 измерений, ~60 ненулевых — срабатывают только позиции релевантных слов
def encode_splade(text: str) -> dict:
tokens = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
with torch.no_grad():
output = model(**tokens)
vec = torch.log1p(torch.relu(output.logits)).max(dim=1).values.squeeze()
return {"indices": vec.nonzero().squeeze().tolist(), "values": vec[vec != 0].tolist()}

SQL / SQL-подобный на самом деле касается фильтрации. Векторный поиск без фильтрации — это демо. Вам всё ещё нужны область тенанта, диапазоны дат, разрешения и фильтры по категориям. Полноценный SQL (pgvector, LanceDB) выражает это рядом с вашими существующими соединениями. Специализированные базы данных используют JSON-объекты фильтров (Qdrant, Pinecone), DSL запросов (Elasticsearch, Milvus) или GraphQL (Weaviate). Они работают; SQL становится привлекательнее по мере усложнения логики фильтрации.

-- pgvector: векторная похожесть — просто ещё одно выражение
SELECT id, title, 1 - (embedding <=> $1) AS score
FROM documents
WHERE tenant_id = $2
AND category = ANY($3::text[])
AND created_at > NOW() - INTERVAL '90 days'
ORDER BY embedding <=> $1
LIMIT 10;
# Qdrant: эквивалентный фильтр как Python-объект — тот же результат, больше церемоний
results = client.query_points(
collection_name="documents", query=query_embedding,
query_filter=models.Filter(must=[
models.FieldCondition(key="tenant_id", match=models.MatchValue(value=tenant_id)),
models.FieldCondition(key="category", match=models.MatchAny(any=categories)),
models.FieldCondition(key="created_at", range=models.DatetimeRange(gte=cutoff)),
]),
limit=10,
)

Мультимодальность нативно означает, что база данных поставляет модели эмбеддингов для нетекстового контента. Вы отдаёте ей сырой URL изображения; она сама занимается векторизацией. Большинство баз данных не зависят от эмбеддингов — вы владеете конвейером эмбеддингов. Marqo и Weaviate (через модули CLIP/ImageBind) замыкают этот цикл.

# Marqo: POST сырых изображений, запрос текстом — без внешнего шага эмбеддинга
mq.index("products").add_documents(
[{"id": "shoe-001", "image": "https://cdn.example.com/shoes/001.jpg"}],
tensor_fields=["image"]
)
results = mq.index("products").search(q="lightweight shoes for summer")
# Возвращает shoe-001 несмотря на нулевое совпадение ключевых слов — CLIP обрабатывает кросс-модальное совпадение

Индекс на диске — рычаг стоимости. Индексы HNSW в оперативной памяти могут требовать несколько ГБ RAM на миллион 1536-мерных векторов, если учитывать сырые векторы, накладные расходы графа и метаданные. Альтернативы, работающие с диском (Milvus DiskANN, Elasticsearch DiskBBQ, формат LanceDB Lance, объектное хранилище Turbopuffer), часто жертвуют задержкой запросов ради снижения стоимости инфраструктуры. Для RAG-задач, где задержка модели уже доминирует, этот обмен часто стоит бенчмарка.

Максимальное количество измерений — это миграция, прячущаяся в вашей архитектуре. text-embedding-3-large использует 3072 измерения, Jina v3 может выдавать большие эмбеддинги, а исследовательские модели продолжают двигаться ввысь. Некоторые управляемые сервисы публикуют жёсткие ограничения по измерениям; другие документируют высокие ограничения или отсутствие практического ограничения для типичных моделей эмбеддингов. Проверьте текущую документацию перед принятием решения. Выбирайте что-то с запасом; миграция векторного индекса из-за достижения потолка измерений — болезненный спринт.

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

Ландшафт

База данныхРазвёртываниеЛицензияГибридный поискРедкие векторыSQL / SQL-подобныйМультимодальностьИндекс на дискеМакс. измеренийЗона применения
pgvectorSelf-host / managed (Supabase, Neon, RDS)OSS (PostgreSQL)Вручную (RRF через SQL)✅ Полный SQL✅ HNSW на диске16 000 хранение; 2 000 индексирование vectorУже на Postgres; умеренное количество векторов
QdrantSelf-host / CloudApache 2.0✅ Нативный BM25✅ Зрелая поддержка❌ (REST/gRPC)65 535Фильтрованные запросы в масштабе; сложные метаданные
WeaviateSelf-host / CloudBSD 3✅ Нативный BM25 + RRF❌ (GraphQL / gRPC)✅ через модули65 535Паттерны доступа GraphQL; встроенная векторизация
PineconeТолько CloudПроприетарная✅ (добавлено в 2024)✅ (serverless)20 000Простота управления; нет команды ops
Milvus / ZillizSelf-host / Cloud (Zilliz)Apache 2.0✅ Нативный✅ SQL-подобный (Milvus Query Language)✅ DiskANN32 768Масштаб в миллиарды; enterprise on-prem
ChromaEmbedded / self-hostApache 2.065 535Только локальная разработка и прототипирование
LanceDBEmbedded / CloudApache 2.0✅ SQL через DataFusion✅ Нативная✅ (формат Lance)Без ограниченийEdge / serverless; мультимодальное озеро данных
OramaEmbedded / CloudApache 2.0✅ Полнотекстовый + векторныйЗависитJS/edge приложения; лёгкий поиск по сайту/приложению
TurbopufferТолько Cloud (serverless)Проприетарная✅ BM25 + векторный✅ (объектное хранилище)16 000Multi-tenant SaaS; миллионы namespaces
ElasticsearchSelf-host / Elastic CloudSSPL / AGPLv3✅ RRF + ELSER sparse✅ (ELSER)✅ Query DSL✅ DiskBBQ4 096Уже на стеке Elastic; гибридный enterprise-поиск
OpenSearchSelf-host / AWS managedApache 2.0✅ RRF + Neural Search✅ Query DSL✅ FAISS + HNSW16 000AWS-native; open-source альтернатива Elastic
VespaSelf-host / CloudApache 2.0✅ Нативный✅ Тензоры / лексическое ранжирование✅ YQL✅ ТензорыПрактически без ограниченийПоиск + ранжирование + рекомендательные системы
ClickHouseSelf-host / CloudApache 2.0Вручную✅ Полный SQL✅ Columnar + HNSWЗависитАналитика/логи с векторным поиском рядом с OLAP
MongoDB AtlasCloud / self-hostSSPL✅ Встроенный✅ MQL + aggregation✅ HNSW8 192Уже на MongoDB; документы + векторы в одном
Redis (VSS)Self-host / Redis CloudRSALv2 / SSPL✅ (RediSearch)❌ Только RAM32 768Ультра-низкая задержка; векторный поиск на уровне кэша
MarqoCloud / self-hostApache 2.0✅ Нативный фокусЗависитСквозная мультимодальность: изображение + текст + видео

Несколько вещей, не вошедших в таблицу

Мультитенантность Turbopuffer построена вокруг очень большого количества namespaces. Их публичное позиционирование и истории клиентов подчёркивают рабочие нагрузки с большим количеством tenants, например, крупный корпус Notion. Если каждому пользователю или организации нужна изолированная векторная похожесть, эта архитектура может изменить экономику, но всё равно бенчмаркайте свою собственную форму tenants.

Встроенный режим LanceDB — ближайшая вещь к «SQLite для векторного поиска». Работает в процессе, не требует сервера, работает в Lambda, Cloudflare Workers и edge-средах. Колончатый формат Lance делает встроенную работу практичной в реальном масштабе.

Chroma сильнее всего в dev/test и небольших приложениях. Если вы целитесь в очень крупные корпуса, HA, тяжёлую работу с диском или первоклассный гибридный поиск, оцените production-ориентированное хранилище перед продвижением прототипа в инфраструктуру.

Vespa — это то, к чему обращаются, когда извлечение — только половина продукта. Он объединяет лексическое извлечение, поиск ближайших соседей, тензоры, выражения ранжирования, группировку и онлайн-обслуживание. Эта мощь реальна, как и операционная и модельная сложность. Это подходит командам поиска/рекомендаций больше, чем «добавить семантический поиск в моё CRUD-приложение».

ClickHouse принадлежит к разговору, когда поиск прикреплён к аналитике. Если ваш источник истины — события, логи, трейсы или метрики, ClickHouse хранит векторное расстояние, фильтрацию, агрегацию и серьёзный полнотекстовый индекс в одном SQL-движке. Не специализированная векторная база данных, но часто скучно-правильный ответ для аналитического извлечения.

Редкие векторы — это способ получить качество сопоставления ключевых слов уровня BM25 внутри векторного индекса — без запуска отдельного полнотекстового движка. Qdrant и Elasticsearch имеют особенно зрелые реализации здесь. Если гибридный поиск критичен, а архитектура из двух систем — неприемлема, ищите поддержку редких векторов.

Выбор, когда вы выросли из pgvector


Единственное, чего не стоит делать

Не используйте векторный поиск как нечёткий текстовый поиск для вещей, у которых есть правильные ответы.

«Найти пользователя с почтой dan@example.com» — это не задача векторного поиска. «Найти заказ с ID ORD-12345» — тоже. Создать эмбеддинг для ORD-12345 и искать по косинусной похожести вернёт что-то — но это может быть неправильно. У идентификатора есть правильный ответ. Приблизительное совпадение по идентификатору — это баг.

Векторный поиск возвращает наиболее похожую вещь в вашем датасете, даже когда ничего по-настоящему релевантного нет. Он не знает, когда хорошего ответа не существует. Это нормально для связанных документов. Это серьёзная проблема для точного поиска записей, где уверенный неправильный ответ хуже пустого результата.

То же самое относится и к другой стороне: не используйте FTS для запросов, где пользователь описывает концепцию. «Статьи о принятии трудных решений в условиях неопределённости» не содержит надёжных ключевых слов. FTS вернёт либо шум, либо ничего. Используйте правильный инструмент для формы запроса.


Полная картина

Большинству production-систем поиска нужно больше одного слоя:

Это не конкурирующие инструменты. Они дополняют друг друга. Хорошо построенная поисковая система выбирает правильный слой для каждой формы запроса — а когда формы запросов перекрываются, она запускает несколько слоёв и сливает результаты.

Команды, которые успешно выпускают поисковые функции, понимают весь стек. Те, кто не понимает, берут векторную базу данных, встраивают всё подряд и удивляются, почему точный поиск иногда возвращает неправильную запись.