FTS5 — модуль полнотекстового поиска, встроенный в SQLite. Он создаёт виртуальную таблицу, за которой стоит обратный индекс: для каждого слова хранится список документов, где оно встречается. Поиск по такому индексу не требует перебора всех строк, поэтому работает быстро и на сотнях тысяч записей.
Отдельного сервера не нужно — это тот же файл базы данных.
CREATE VIRTUAL TABLE messages_fts USING fts5(
body,
content='messages',
content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
SELECT rowid, bm25(messages_fts) AS score
FROM messages_fts
WHERE messages_fts MATCH 'договор AND подрядчик'
ORDER BY score
LIMIT 20;
Параметр content= делает таблицу внешней: сам текст остаётся в исходной таблице, а в индексе лежат только служебные структуры. Это экономит место, но требует синхронизировать индекс триггерами при вставке и удалении.
#Оговорка про русскую морфологию
Здесь FTS5 подводит новичков чаще всего. Встроенных токенизаторов немного:
| Токенизатор | Что делает | Русский |
|---|---|---|
unicode61 |
режет по не-буквам, приводит регистр | без морфологии |
ascii |
то же, но только для латиницы | не подходит |
porter |
стеммер английского языка | не работает |
trigram |
индексирует тройки символов | поиск по подстроке |
Строка porter — источник главного недоразумения: его добавляют «для морфологии», но это стеммер английского, и на русские окончания он не действует. В результате запрос договор не находит документ со словом договоры, а встреча не находит встречи.
Обходные пути, в порядке роста трудозатрат:
- Префиксный поиск:
MATCH 'договор*'— покрывает окончания, но ловит и лишнее вроде «договорённость». - Индекс префиксов при создании таблицы:
prefix='2 3 4'— ускоряет такой поиск ценой размера индекса. - Токенизатор
trigram— честный поиск по подстроке, индекс заметно больше, ранжирование хуже. - Нормализация до индексации — приведение слов к начальной форме сторонней библиотекой перед записью. Самый качественный и самый трудоёмкий путь.
Развёрнутый разбор с примерами и замерами — в статье про полнотекстовый поиск FTS5 в SQLite.
#Где путают
Заблуждение первое: FTS5 понимает смысл. Не понимает. Он ищет вхождения слов; синоним, перифраз и опечатка ему недоступны. За смысл отвечает векторный поиск, и связка обоих обычно работает лучше каждого по отдельности — как именно, разобрано в разборе гибридного поиска BM25 и векторов.
Заблуждение второе: bm25() возвращает «больше — лучше». В SQLite функция возвращает отрицательное число, и лучший результат — наименьший. Сортировка ORDER BY bm25(t) DESC даёт худшие совпадения первыми, и ошибка выглядит как «поиск находит ерунду». Что означает сама метрика — в справке про BM25.
Заблуждение третье: FTS5 подходит только для маленьких баз. Практический потолок гораздо выше, чем принято думать: узкое место обычно не в объёме, а в частоте записи и в блокировках. Замеры на больших таблицах — в разборе производительности SQLite FTS5 на миллионах строк.
#Частые вопросы
#Нужно ли отдельно строить индекс после наполнения таблицы?
Нет, индекс обновляется при вставке. Но если таблица внешняя, синхронизацию обеспечивают триггеры, и при массовой загрузке их иногда отключают, а индекс перестраивают одной командой rebuild в конце.
#Что делать с опечатками в запросе?
FTS5 их не прощает. Практическое решение — падать в поиск по подстроке, когда точный запрос ничего не вернул, либо держать отдельный индекс на триграммах для нечёткого случая.
#Можно ли искать по фразе целиком?
Да, фраза берётся в двойные кавычки внутри выражения MATCH. Поддерживаются также операторы AND, OR, NOT и поиск рядом стоящих слов через NEAR.
#FTS5 включён в SQLite по умолчанию?
В большинстве сборок — да, но не во всех. Проверяется одной командой: попыткой создать виртуальную таблицу. Если модуль не собран, вы получите ошибку сразу, а не при первом поиске.
Остальные термины — в глоссарии терминов ИИ.