Dan Levy

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

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

Поиск — это не что-то одно, и семантический поиск не заменяет все остальные его разновидности.

Обзор векторного поиска: сравнение 16 вариантов

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

Как читать это сравнение

База данныхРазвертываниеЛицензияГибридный поискРазреженные векторыИнтерфейс запросовВстроенное мультимодальное получение эмбеддинговИндекс на дискеОграничения на размерность вектораОптимальный вариант
pgvectorСобственный хостинг / управляемый сервис (Supabase, Neon, RDS)OSS (PostgreSQL)Вручную (RRF через SQL)❌✅ Полный SQL❌✅ HNSW на диске16 000 для хранения; 2 000 для индексируемого vectorВы уже используете Postgres; умеренное количество векторов
QdrantСобственный хостинг / CloudApache 2.0✅ Нативный BM25✅ Зрелая поддержка❌ (REST/gRPC)❌✅65 535Фильтруемые запросы в большом масштабе; сложные метаданные
WeaviateСобственный хостинг / CloudBSD 3✅ Нативный BM25 + RRF✅❌ (GraphQL / gRPC)✅ через модули✅65 535Сценарии доступа через GraphQL; встроенная векторизация
PineconeТолько CloudПроприетарная✅ (добавлено в 2024 году)✅❌❌✅ (serverless)20 000Простота управляемого сервиса; отсутствие команды эксплуатации
Milvus / ZillizСобственный хостинг / Cloud (Zilliz)Apache 2.0✅ Нативный✅✅ SQL-подобный (Milvus Query Language)✅✅ DiskANN32 768Масштаб до миллиардов объектов; корпоративный on-premises
ChromaВстраиваемый / собственный хостингApache 2.0❌❌❌❌❌65 535Только локальная разработка и прототипирование
LanceDBВстраиваемый / CloudApache 2.0✅❌✅ SQL через DataFusion✅ Нативный✅ (формат Lance)Не ограниченаEdge / serverless; мультимодальное lakehouse-хранилище
OramaВстраиваемый / CloudApache 2.0✅ Полнотекстовый + векторный❌❌❌❌Зависит от конфигурацииJS/edge-приложения; легковесный поиск по сайту или приложению
TurbopufferТолько Cloud (serverless)Проприетарная✅ BM25 + векторный❌❌❌✅ (объектное хранилище)16 000Мультитенантный SaaS; миллионы пространств имён
ElasticsearchСобственный хостинг / Elastic CloudSSPL / AGPLv3✅ RRF + разреженный ELSER✅ (ELSER)✅ Query DSL❌✅ DiskBBQ4 096Вы уже используете стек Elastic; корпоративный гибридный поиск
OpenSearchСобственный хостинг / управляемый AWSApache 2.0✅ RRF + Neural Search✅✅ Query DSL❌✅ FAISS + HNSW16 000Нативная интеграция с AWS; открытая альтернатива Elastic
VespaСобственный хостинг / CloudApache 2.0✅ Нативный✅ Тензоры / лексическое ранжирование✅ YQL✅ Тензоры✅Фактически не ограниченаСистемы поиска, ранжирования и рекомендаций
ClickHouseСобственный хостинг / CloudApache 2.0Вручную❌✅ Полный SQL❌✅ Колончатый + HNSWЗависит от конфигурацииАналитика и логи с векторным поиском рядом с OLAP
MongoDB AtlasCloud / собственный хостингSSPL✅ Встроенный❌✅ MQL + агрегация❌✅ HNSW8 192Вы уже используете MongoDB; документы и векторы в одной системе
Redis (VSS)Собственный хостинг / Redis CloudRSALv2 / SSPL✅ (RediSearch)✅❌❌❌ Только в RAM32 768Минимальная задержка; векторный поиск на уровне кеша
MarqoCloud / собственный хостингApache 2.0✅❌❌✅ Нативный фокус✅Зависит от конфигурацииСквозной мультимодальный сценарий: изображения + текст + видео

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

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

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

Лексическая и нечеткая половина этой картины — tsvector, pg_trgm, pg_search — разобрана в руководстве Текстовый поиск в Postgres: руководство на 2026 год.


Термины, используемые в этом руководстве

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

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

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

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

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 — не нужно разворачивать, эксплуатировать и синхронизировать данные с отдельным сервисом.

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

Поиск похожих объектов. Связанные товары, похожие обращения в поддержку, дубликаты баг-репортов, статьи, которые могут вам понравиться. «Найди проблемы, похожие на эту» — это поиск ближайших соседей: нужно получить эмбеддинг объекта и найти его геометрических соседей. Важная оговорка: векторный поиск всегда возвращает результаты, даже если ничего действительно похожего нет. Для дедупликации и рекомендаций задавайте минимальный порог схожести (например, cosine similarity ≥ 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 is usually the first index to try for moderate-size datasets
CREATE INDEX documents_embedding_idx
ON documents USING hnsw (embedding vector_cosine_ops);
-- Semantic search query
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).

Где векторный поиск возвращает неправильный ответ


Сочетайте ключевые слова и векторы для смешанных запросов

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

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

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

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

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

Объединяйте результаты с ранжированием через RRF

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

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

Как читать сравнение

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

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

# SPLADE: ~30,000 dims, ~60 non-zero — only relevant vocabulary positions fire
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) позволяет выразить всё это рядом с уже существующими JOIN. Специализированные базы используют JSON-объекты фильтров (Qdrant, Pinecone), DSL запросов (Elasticsearch, Milvus) или GraphQL (Weaviate). Всё это работает; но по мере усложнения логики фильтрации SQL становится привлекательнее.

-- pgvector: vector similarity is just another expression
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: equivalent filter as a Python object — same result, more ceremony
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 raw images, query with text — no external embedding step
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")
# Returns shoe-001 despite zero keyword overlap — CLIP handles the cross-modal match

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

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

Операционные компромиссы, которых не видно в таблице

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

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

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

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

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

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

Выбирайте хранилище по рабочей нагрузке


Не встраивайте идентификаторы и не ждите точного совпадения

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

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

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

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


Стройте поиск вокруг формы запроса

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

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

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