Learning Platform
Глоссарий Troubleshooting
Урок 11.06 · 22 мин
Средний
ftsvssfull-text-searchvector-search

fts и vss: полнотекстовый и векторный поиск

Аналитический SQL отлично отвечает на вопросы «сколько», «когда», «по каким группам». Но есть два типа поиска, которые обычным WHERE-условием решаются плохо: найти документы по смыслу слов и найти объекты, похожие по содержанию. Первое — задача полнотекстового поиска, второе — векторного. DuckDB закрывает обе расширениями fts и vss. Этот урок про то, как они устроены и почему DuckDB способен совмещать поиск с аналитикой в одном запросе.

Почему LIKE — не поиск

Начнём с того, почему для поиска по тексту недостаточно WHERE text LIKE '%запрос%'.

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

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

fts: полнотекстовый поиск

Расширение fts добавляет в DuckDB полнотекстовый поиск. Его сердце — инвертированный индекс. Идея инвертированного индекса проста и мощна: вместо отображения «документ -> его слова» он хранит обратное — «слово -> список документов, где оно встречается». Чтобы найти документы со словом, не надо сканировать все документы — достаточно взять готовый список из индекса.

Инвертированный индекс: от слова к документам
ДокументыИсходные тексты: статьи, описания, отзывы — то, по чему нужно искать
токенизация + стемминг
Слова в базовой формеТекст разбит на слова, слова приведены к корневой форме: 'бежал' и 'бегать' становятся одним токеном
построение индекса
Индекс: слово -> документыДля каждого слова хранится список документов, где оно встречается, с частотами для ранжирования

Индекс создаётся вызовом PRAGMA create_fts_index. После этого появляется функция match_bm25, которая возвращает оценку релевантности документа запросу:

INSTALL fts;
LOAD fts;

-- Построить полнотекстовый индекс по колонке body таблицы articles
PRAGMA create_fts_index('articles', 'id', 'body');

-- Найти и ранжировать статьи по запросу
SELECT id, title, fts_main_articles.match_bm25(id, 'columnar storage engine') AS score
FROM articles
WHERE score IS NOT NULL
ORDER BY score DESC
LIMIT 5;
┌───────┬──────────────────────────────┬────────────────────┐
│  id   │            title             │       score        │
│ int64 │           varchar            │       double       │
├───────┼──────────────────────────────┼────────────────────┤
│   312 │ How columnar engines work    │  4.812...          │
│    87 │ Storage formats compared     │  3.155...          │
│   540 │ Vectorized execution basics  │  1.998...          │
└───────┴──────────────────────────────┴────────────────────┘

Функция называется match_bm25, потому что внутри работает алгоритм ранжирования BM25 — индустриальный стандарт. BM25 оценивает релевантность документа, учитывая две вещи: как часто слово запроса встречается в документе и насколько слово редкое в коллекции в целом (редкое слово важнее частого). Чем выше оценка, тем релевантнее документ — отсюда ORDER BY score DESC.

vss: поиск по векторному сходству

Второй вид поиска — по смыслу содержимого, а не по словам. Эту задачу решают эмбеддинги: ML-модель превращает объект (текст, изображение, товар) в вектор из сотен или тысяч чисел так, что похожие по смыслу объекты получают близкие в пространстве векторы. Тогда «найти похожее» сводится к «найти ближайшие векторы».

Расширение vss (vector similarity search) добавляет в DuckDB поиск ближайших векторов. Эмбеддинги хранятся в колонке типа FLOAT[N] — массив фиксированной длины N (длина вектора). Расстояние между векторами считают функции: array_distance (евклидово расстояние), array_cosine_distance (косинусное), array_inner_product.

INSTALL vss;
LOAD vss;

-- Таблица товаров с эмбеддингами длины 768
-- Найти 5 товаров, ближайших к заданному вектору запроса
SELECT name, array_distance(embedding, [0.12, -0.04, ...]::FLOAT[768]) AS dist
FROM products
ORDER BY dist
LIMIT 5;

Запрос «отсортировать по расстоянию и взять K ближайших» называется K nearest neighbors (KNN), поиск K ближайших соседей. Без индекса он работает честным перебором: расстояние считается до каждого вектора в таблице. Для небольших таблиц это нормально и DuckDB делает это быстро благодаря векторизации.

Для больших коллекций перебор дорог, и vss предоставляет индекс HNSW (Hierarchical Navigable Small World). HNSW — это многослойный граф ближайших соседей: поиск спускается по слоям графа, на каждом шаге приближаясь к цели, и находит почти ближайшие векторы, не перебирая всю коллекцию.

-- Построить HNSW-индекс по колонке эмбеддингов
CREATE INDEX prod_hnsw ON products USING HNSW (embedding);
NOTE

HNSW даёт приближённый ответ — это approximate nearest neighbor search (ANN). Он может пропустить настоящего ближайшего соседа ради скорости. Для рекомендаций и семантического поиска это приемлемо: разница между «самым похожим» и «почти самым похожим» товаром незаметна, а выигрыш в скорости на больших коллекциях огромен. Если же нужен абсолютно точный ответ, используйте честный перебор через ORDER BY array_distance без HNSW-индекса.

Полнотекстовый против векторного поиска
fts: поиск по словамИнвертированный индекс, токенизация, стемминг, ранжирование BM25; ищет документы, содержащие слова запроса
vs
vss: поиск по смыслуЭмбеддинги-векторы, расстояние между векторами, индекс HNSW; ищет объекты, близкие по содержанию
Аспектfts (полнотекстовый)vss (векторный)
Что ищетДокументы со словами запросаОбъекты, близкие по смыслу
Представление данныхИнвертированный индекс по словамВекторы-эмбеддинги FLOAT[N]
Алгоритм ранжированияBM25Расстояние между векторами (KNN)
Индекс для ускоренияИнвертированный индексHNSW (приближённый)
Откуда берётся входСам текстML-модель, считающая эмбеддинги

Почему это в SQL-движке — преимущество

Полнотекстовый и векторный поиск обычно живут в отдельных специализированных системах (поисковые движки, векторные базы данных). Что даёт их наличие прямо в DuckDB?

Главное — поиск совмещается с аналитикой и фильтрами в одном запросе. В отдельной векторной базе вы найдёте похожие товары, а потом отдельным шагом в другой системе отфильтруете их по цене, наличию и категории. В DuckDB это один SQL-запрос: векторный поиск, фильтр по обычным колонкам, джойн со складскими остатками, агрегация — всё вместе, одним планом, без перекладывания данных между системами.

-- Векторный поиск + бизнес-фильтр + джойн в одном запросе
SELECT p.name, p.price, array_distance(p.embedding, :query_vec) AS dist
FROM products p
JOIN stock s ON s.product_id = p.id
WHERE p.price < 100          -- обычный фильтр
  AND s.quantity > 0          -- джойн со складом
ORDER BY dist                 -- ранжирование по векторному сходству
LIMIT 10;

Для умеренных объёмов данных это снимает целый класс инфраструктуры: не нужна отдельная поисковая система, не нужна отдельная векторная база, не нужен пайплайн синхронизации между ними. Поиск становится ещё одной возможностью SQL-движка.

WARNING

fts и vss не делают из DuckDB замену Elasticsearch или специализированной векторной базы для очень больших, постоянно обновляемых поисковых нагрузок с высокой конкурентностью. DuckDB остаётся аналитическим in-process движком: один процесс-писатель, упор на пакетную аналитику. fts и vss блестяще решают задачу «поиск как часть аналитического запроса над умеренным объёмом данных». Для поискового бэкенда веб-сервиса с тысячами запросов в секунду нужна профильная система. Выбирайте инструмент по нагрузке.

Попробуй сам

Обе возможности легко попробовать на учебных данных.

  1. Создайте таблицу articles(id, title, body) с десятком текстов. Выполните INSTALL fts; LOAD fts; и постройте индекс через PRAGMA create_fts_index('articles', 'id', 'body').
  2. Сделайте поиск через match_bm25 по запросу из двух-трёх слов, отсортируйте по score DESC. Затем поищите то же самое через WHERE body LIKE '%слово%' и сравните: LIKE не ранжирует и не находит словоформы.
  3. Создайте таблицу с колонкой FLOAT[3] и несколькими векторами (короткие векторы удобны для ручной проверки). Найдите ближайшие к заданному вектору через ORDER BY array_distance(...) LIMIT 3. Посчитайте пару расстояний вручную и сверьте.
  4. Напишите один запрос, который совмещает поиск (match_bm25 или array_distance) с обычным фильтром по другой колонке и сортировкой. Объясните себе, почему выполнение поиска и фильтра в одном SQL-плане выгоднее, чем два шага в разных системах.
ClickHouse: инвертированный индекс и full-text search
Проверка знанийKnowledge check
Чем задачи расширений fts и vss отличаются друг от друга, и в чём преимущество того, что полнотекстовый и векторный поиск доступны прямо в SQL-движке DuckDB?
ОтветAnswer
Расширения fts и vss решают два разных типа поиска. fts (full-text search) — это полнотекстовый поиск по словам: он находит документы, содержащие слова запроса. Его сердце — инвертированный индекс, который хранит отображение слово -> список документов, где оно встречается, что позволяет не сканировать все документы. fts разбивает текст на слова (токенизация), приводит их к базовой форме (стемминг, благодаря которому 'бежал' и 'бегать' считаются одним словом) и ранжирует документы по релевантности алгоритмом BM25, учитывающим частоту слова в документе и его редкость в коллекции; доступ через функцию match_bm25. vss (vector similarity search) — это поиск по смыслу содержимого через эмбеддинги: ML-модель превращает объект в вектор чисел так, что похожие по смыслу объекты получают близкие векторы, и поиск похожего сводится к поиску ближайших векторов по расстоянию (функции array_distance, array_cosine_distance). Запрос K ближайших соседей (KNN) без индекса работает честным перебором, а для больших коллекций vss даёт индекс HNSW — многослойный граф, который находит приближённо ближайшие векторы без полного перебора. Главное преимущество наличия обоих видов поиска прямо в SQL-движке в том, что поиск совмещается с аналитикой, фильтрами и джойнами в одном запросе и одном плане исполнения. В отдельной поисковой системе или векторной базе пришлось бы сначала выполнить поиск, а потом отдельным шагом в другой системе фильтровать результат по цене, наличию, категории и джойнить со складом. В DuckDB это единый SQL-запрос без перекладывания данных между системами, что для умеренных объёмов снимает целый класс инфраструктуры — отдельную поисковую систему, отдельную векторную базу и пайплайн их синхронизации. Это не замена Elasticsearch или профильной векторной базы для очень больших высококонкурентных поисковых нагрузок, но для поиска как части аналитического запроса над умеренным объёмом данных решение работает отлично.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Почему WHERE text LIKE '%слово%' плохо подходит для полнотекстового поиска, а расширение fts подходит?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7