Полнотекстовый поиск в приложении не требует отдельного сервера. Встроенный в SQLite модуль FTS5 создаёт виртуальную таблицу с инвертированным индексом, умеет ранжировать результаты и возвращать подсвеченные фрагменты. Для небольшого каталога статей или заметок этого достаточно, а база остаётся одним файлом, который легко скопировать.

Когда хватает FTS5

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

  • Индекс хранится рядом с данными и не требует демона.
  • Запросы выполняются в том же соединении, что и обычный SQL.
  • Есть ранжирование BM25 и функция snippet для выдачи фрагментов.
  • Поддерживаются внешние таблицы, чтобы не дублировать текст.

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

Проверка поддержки

Модуль может быть отключён при сборке SQLite. Проверьте опции компиляции и версию библиотеки. Если строки нет, используйте системный sqlite3 или пересоберите зависимость с нужным флагом.

sqlite3 app.db "PRAGMA compile_options;" | grep -i fts5
python3 -c "import sqlite3; print(sqlite3.sqlite_version)"
sqlite3 --version

Python может использовать собственную сборку SQLite, поэтому проверяйте именно окружение приложения. Для большинства современных дистрибутивов FTS5 включён по умолчанию.

Схема с внешним содержимым

Хранить текст дважды не нужно: content=’docs’ указывает на обычную таблицу, а FTS5 держит только индекс. Параметр content_rowid связывает строки с первичным ключом.

CREATE TABLE docs (
  id INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE VIRTUAL TABLE docs_fts USING fts5(
  title,
  body,
  content='docs',
  content_rowid='id',
  tokenize='unicode61 remove_diacritics 2'
);

Токенизатор unicode61 делит текст на слова и убирает диакритику. Для русского языка это разумный выбор: полноценной морфологии он не делает, но приводит регистр и разделители к предсказуемому виду. Porter рассчитан на английский и для смешанных текстов не подходит.

Синхронизация через триггеры

Внешняя таблица не обновляет индекс сама: нужны триггеры на вставку, удаление и изменение. При удалении FTS5 требует старые значения колонок, иначе индекс разойдётся с данными.

CREATE TRIGGER docs_ai AFTER INSERT ON docs BEGIN
  INSERT INTO docs_fts(rowid, title, body)
  VALUES (new.id, new.title, new.body);
END;

CREATE TRIGGER docs_ad AFTER DELETE ON docs BEGIN
  INSERT INTO docs_fts(docs_fts, rowid, title, body)
  VALUES ('delete', old.id, old.title, old.body);
END;

CREATE TRIGGER docs_au AFTER UPDATE ON docs BEGIN
  INSERT INTO docs_fts(docs_fts, rowid, title, body)
  VALUES ('delete', old.id, old.title, old.body);
  INSERT INTO docs_fts(rowid, title, body)
  VALUES (new.id, new.title, new.body);
END;

Без триггеров индекс быстро устареет, а рассинхронизацию покажет integrity-check.

Запросы и ранжирование

Поиск идёт через MATCH: поддерживаются AND, OR, NOT, фразы, префиксы и фильтры по колонкам. bm25 возвращает отрицательное число: чем лучше совпадение, тем оно меньше, поэтому сортировка идёт по возрастанию.

SELECT d.id, d.title,
       snippet(docs_fts, 1, '<b>', '</b>', '…', 16) AS fragment,
       bm25(docs_fts, 1.0, 0.5) AS rank
FROM docs_fts
JOIN docs d ON d.id = docs_fts.rowid
WHERE docs_fts MATCH 'индекс AND (поиск OR sqlite)'
ORDER BY rank
LIMIT 20;

В snippet сначала указывается колонка, затем маркеры подсветки, строка усечения и число токенов. Веса bm25 задают приоритет колонок, а пользовательский запрос нужно экранировать или оборачивать в кавычки.

Фразовый поиск задаётся как MATCH ‘»полнотекстовый поиск»‘, префиксный — MATCH ‘поиск*’, фильтр по колонке — MATCH ‘title:linux’. Скобки помогают комбинировать условия, но сложный запрос труднее отлаживать.

Пример на Python

Параметры в MATCH передаются через заполнитель, как и обычные значения. Не собирайте SQL конкатенацией: это открывает путь к инъекции и ломает запросы с кавычками.

import sqlite3

con = sqlite3.connect('app.db')
con.execute('PRAGMA journal_mode=WAL')

rows = con.execute(
    "SELECT d.title, snippet(docs_fts, 1, '<b>', '</b>', '…', 12) "
    "FROM docs_fts JOIN docs d ON d.id = docs_fts.rowid "
    "WHERE docs_fts MATCH ? ORDER BY bm25(docs_fts) LIMIT ?",
    ('поиск*', 10),
).fetchall()

for title, fragment in rows:
    print(title, fragment)

Если маркеры подсветки вставляются как HTML, результат нужно экранировать при выводе: FTS5 лишь расставляет указанные строки вокруг совпадений.

Обслуживание индекса

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

INSERT INTO docs_fts(docs_fts) VALUES('optimize');
INSERT INTO docs_fts(docs_fts) VALUES('integrity-check');
INSERT INTO docs_fts(docs_fts) VALUES('rebuild');

Optimize запускайте при низкой нагрузке: операция временно увеличивает файл. Integrity-check показывает расхождение индекса и таблицы, rebuild возвращает согласованность.

Подстрочный поиск и ограничения

Обычный unicode61 ищет по словам, поэтому «индекс» не найдётся внутри «индексирование». Если нужен поиск по подстроке, помогает токенизатор trigram, но он требует запросы длиной не меньше трёх символов и занимает больше места.

CREATE VIRTUAL TABLE docs_trgm USING fts5(body, tokenize='trigram');
SELECT rowid FROM docs_trgm WHERE docs_trgm MATCH 'индек';

Trigram иногда ускоряет LIKE ‘%…%’, но не заменяет индекс по словам. Для больших каталогов можно держать два индекса, если интерфейсу нужен поиск по подстроке.

Чек-лист внедрения

  • FTS5 включён в той сборке SQLite, которая работает в приложении.
  • Схема использует внешнюю таблицу и не дублирует текст.
  • Триггеры покрывают вставку, изменение и удаление.
  • Пользовательский запрос передаётся параметром, а не склейкой строк.
  • Настроены веса bm25 и проверена выдача на реальных данных.
  • Есть регламент optimize и integrity-check после массовых операций.

Итог

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

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте