Полнотекстовый поиск, который уже лежит в SQLite: FTS5

· 2 мин · BadRabbit

Когда агент спрашивает у llm-wiki «какие у нас правила про логирование», нужен поиск. Можно поднять Elasticsearch, а можно открыть документацию SQLite и обнаружить, что всё уже есть. Разбираю расширение FTS5.

Создать индекс

FTS5 — это виртуальная таблица:

CREATE VIRTUAL TABLE pages USING fts5(title, body);

Типы, ограничения и PRIMARY KEY здесь не указываются. Есть неявный rowid, а колонки с именами rowid и rank запрещены.

Искать

SELECT * FROM pages WHERE pages MATCH 'логирование';

Язык запросов богаче, чем кажется:

  • "точная фраза" — фраза целиком;
  • лог* — префиксный поиск;
  • логи AND NOT debug — логические операторы;
  • NEAR(секреты env, 10) — слова на расстоянии не больше 10 токенов;
  • title : секреты — поиск только в заголовке.

Ранжировать

Встроенная функция bm25() даёт релевантность (меньше — лучше), и колонкам можно дать разный вес:

SELECT title FROM pages
WHERE pages MATCH 'секреты'
ORDER BY bm25(pages, 10.0, 1.0);   -- совпадение в заголовке в 10 раз важнее

Ещё быстрее сортировать по скрытой колонке rank.

Показать результат

  • highlight(pages, 1, '<b>', '</b>') — подсветить совпадения;
  • snippet(pages, 1, '<b>', '</b>', '…', 20) — вырезать кусок текста вокруг совпадения.

Готовая выдача поисковика в одном SQL-запросе.

Токенизаторы

  • unicode61 (по умолчанию) — понимает Unicode, убирает диакритику;
  • porter — стемминг, но только для английского;
  • trigram — поиск по любой подстроке, а не по целым словам.

Полезные приёмы

  • Внешние таблицы: FTS5 может индексировать обычную таблицу, не дублируя данные (content='...'), а синхронизация делается триггерами.
  • Префиксные индексы (prefix='2 3') ускоряют поиск по началу слова.
  • detail='none' уменьшает индекс, если не нужны фразы и NEAR.
  • INSERT INTO pages(pages) VALUES('optimize') склеивает индекс в одно дерево.

Мой комментарий

В llm-wiki индекс — производный: его можно удалить и пересобрать из Git в любой момент, поэтому он лежит отдельно от состояния, которое нужно бэкапить. Страницы режутся на куски по заголовкам, и индексируются три колонки: заголовок страницы, заголовок раздела и текст. Для русского porter бесполезен, поэтому стоит unicode61 remove_diacritics 2, а формы слов ловит префиксный запрос вроде логир*. Не морфология, но для правил компании хватает.

Один файл, ноль серверов, BM25 из коробки. Иногда лучшая инфраструктура — та, которой нет.