Полнотекстовый поиск¶
Полнотекстовый поиск — один API на каждом диалекте, где он есть; каждый диалект пишет SQL по-своему. В
ClickHouse через hare его нет: __search и любое выражение поиска дают там UnSupportedError до
отправки SQL.
| PostgreSQL | SQLite | |
|---|---|---|
| Движок | tsvector и tsquery | FTS5 |
| Где ищется | любая текстовая колонка или выражение | поля FullTextIndex, который объявляет модель |
__search |
TO_TSVECTOR(column) @@ PLAINTO_TSQUERY(text), у TSVectorField — сама колонка |
rowid IN (SELECT rowid FROM index WHERE index MATCH query) |
SearchRank |
TS_RANK() / TS_RANK_CD() |
bm25() с обратным знаком, NULL у строки, которая не подходит под запрос |
SearchHeadline |
TS_HEADLINE() |
highlight(), а с max_words — snippet(); NULL у строки, которая не подходит под запрос |
| Конфигурации, лексемы, веса меток | есть | нет |
| Веса полей | через SearchVector(weight=...) |
SearchRank(weights={"title": 10}) |
То, что умеет только один диалект, объявляют два признака Features; вызов, которому нужен признак,
которого нет у соединения, даёт UnSupportedError до отправки SQL:
supports_text_search_configurations(PostgreSQL) — конфигурация поиска (config=),SearchVectorкак самостоятельное значение (аннотация,+, вес), лексемы, веса меток,normalizationиcover_densityуSearchRank, а такжеconfig,min_words,short_word,highlight_allиmax_fragmentsуSearchHeadline;supports_full_text_index(SQLite) —FullTextIndexи веса полей уSearchRank.
from hare.search import SearchHeadline, SearchQuery, SearchRank, SearchType, SearchVector
await Book.objects.filter(description__search="white whale")
await Book.objects.filter(description__search=SearchQuery("white whale", search_type=SearchType.PHRASE))
await (
Book.objects.filter(description__search="white whale")
.annotate(rank=SearchRank("description", "white whale"))
.order_by("-rank")
)
await Book.objects.annotate(excerpt=SearchHeadline("description", "white whale", max_words=20))
В SQLite модель объявляет индекс, по которому идёт поиск:
from hare.dialects.sqlite.indexes import FullTextIndex
class Book(Model):
title = fields.CharField(max_length=200)
description = fields.TextField()
class Meta:
indexes = (FullTextIndex(fields=("title", "description"), tokenizer="porter unicode61"),)
__search¶
field__search=text находит строки, в тексте которых есть каждое слово text (SearchType.PLAIN);
text может быть и SearchQuery.
- PostgreSQL превращает колонку в вектор через
TO_TSVECTOR();TSVectorFieldберётся как есть, со своимconfig. У обычной текстовой колонкиfield__search=SearchQuery(..., config="russian")превращает колонку в вектор сconfigсамого запроса. - SQLite ищет через
FullTextIndexмодели, в который входит поле, только по колонке этого поля. Поле безFullTextIndexили аннотация, которая не является простым полем, даютUnSupportedErrorдо отправки SQL.
SearchQuery¶
| Аргумент | Что задаёт |
|---|---|
value |
Текст поиска, выражение, которое его даёт, или лексемы PostgreSQL (читаются как RAW) |
config |
Конфигурация поиска ("english") — PostgreSQL |
search_type |
Как читается текст — SearchType; другое значение даёт ConfigurationError |
invert |
Отрицание запроса |
SearchType:
| Значение | Как читается текст | PostgreSQL | SQLite |
|---|---|---|---|
PLAIN |
каждое слово, в любом месте | PLAINTO_TSQUERY |
каждое слово — термин FTS5 в кавычках |
PHRASE |
слова подряд, по порядку | PHRASETO_TSQUERY |
одна фраза FTS5 |
WEBSEARCH |
слова, "quoted phrases", or между вариантами, -word для слова, которого не должно быть |
WEBSEARCH_TO_TSQUERY |
то же самое как запрос FTS5; вариант из одних -word (-python, hare or -python) даёт UnSupportedError — FTS5 исключает слова только после слова, которое есть в строках |
RAW |
собственный синтаксис базы как есть | TO_TSQUERY |
синтаксис запросов FTS5 |
В SQLite, кроме RAW, текст передаётся параметром и превращается в запрос FTS5 функцией, которую hare
регистрирует в своих соединениях, — ничего из него не читается как синтаксис FTS5. Текст без единого
слова не находит ни одной строки.
Запросы соединяются через & (подходят оба), | (подходит любой) и ~ (отрицание):
PostgreSQL отрицает любой запрос (!!). FTS5 отрицает запрос только справа от & — SQLite пишет
query & ~query как query NOT query; ~query сам по себе, внутри | или слева от & даёт
UnSupportedError до отправки SQL.
a & b и a | b дают CombinedSearchQuery, который соединяет оба запроса через SearchOperator
(AND, OR); SearchQueryCombinable — база, дающая операторы обоим классам запросов. Запрос в
собственном синтаксисе лексем PostgreSQL — это RawSearchQueryText, который SearchQuery читает как
RAW. Все они лежат в hare.search.
SearchVector¶
Текст для поиска из одного или нескольких полей или выражений, соединённый через пробел.
- Как вектор
SearchRank—SearchVectorиз простых имён полей называет ранжируемые поля на каждом диалекте. - Как самостоятельное значение — это tsvector PostgreSQL (
TO_TSVECTOR(...),SETWEIGHT(...)сweight="A"..."D"— другая буква или значение даютQueryError— или выражением, дающим букву), векторы складываются через+. Нетекстовый источник (число, JSON) приводится к тексту, аCOALESCEне даёт источнику со значением NULL сделать весь вектор NULL. Шифрованное поле даёт ошибку. a + bдвух векторов — этоCombinedSearchVector;SearchVectorCombinable— база, дающая+обоим классам векторов.
SearchRank¶
Насколько каждая строка подходит под запрос, больше — лучше; для аннотации, по которой фильтруют и сортируют.
| Аргумент | Что задаёт |
|---|---|
vector |
Ранжируемое поле, кортеж полей, SearchVector или (PostgreSQL) выражение tsvector — TSVectorField ранжируется как есть |
query |
Текст поиска (PLAIN) или SearchQuery |
weights |
Словарь весов полей — совпадение в более тяжёлом поле выше, у поля, которого нет в словаре, вес 1, каждый вес — конечное число от 0 до 1000000 (SQLite); или веса меток D, C, B, A PostgreSQL последовательностью из четырёх чисел или выражением — другая последовательность даёт ConfigurationError |
normalization |
Битовая маска нормализации по длине текста — PostgreSQL; 32-битное целое число или выражение, другое значение вызывает ConfigurationError |
cover_density |
TS_RANK_CD() вместо TS_RANK() — PostgreSQL |
В SQLite поля должны входить в один FullTextIndex модели, вес должен называть одно из его полей (иначе
ConfigurationError), а строка, которая не подходит под запрос, получает NULL — FTS5 ранжирует только
найденные строки. В PostgreSQL TS_RANK()
даёт такой строке 0 для запроса из одного слова, но ранг чуть выше 0 (1e-20) для запроса из нескольких,
поэтому rank__gt=0 её оставляет — чтобы остались только подходящие строки, фильтруйте по __search.
SearchHeadline¶
SearchHeadline(
expression, query, config=None, start_sel=None, stop_sel=None, max_words=None, min_words=None,
short_word=None, highlight_all=None, max_fragments=None, fragment_delimiter=None,
)
Текст с отмеченными найденными словами, для аннотации.
| Аргумент | Что задаёт |
|---|---|
expression |
Поле, текст которого размечается; в PostgreSQL — и выражение |
query |
Текст поиска (PLAIN) или SearchQuery |
start_sel, stop_sel |
Текст до и после найденного слова — по умолчанию <b> и </b> PostgreSQL; в SQLite обязательны, своих меток у FTS5 нет; не длиннее 1000 символов |
max_words |
Наибольшее число слов фрагмента — от 1; в SQLite не больше 64, и с ним используется snippet() FTS5; в PostgreSQL больше min_words, который по умолчанию 15, — для max_words=3 нужен меньший min_words, иначе PostgreSQL даёт OperationalError |
fragment_delimiter |
Текст между фрагментами — по умолчанию " ... " PostgreSQL; в SQLite обязателен вместе с max_words |
config, min_words, short_word, highlight_all, max_fragments |
Собственные параметры TS_HEADLINE() — PostgreSQL |
В SQLite строка, которая не подходит под запрос, получает NULL, а SearchHeadline без start_sel и
stop_sel или с max_words без fragment_delimiter даёт QueryError до отправки SQL. Число вне
диапазона или метка, которая не является текстом, дают ConfigurationError.
Лексемы (PostgreSQL)¶
from hare.dialects.postgresql.search import Lexeme
SearchQuery(Lexeme("cat", prefix=True, weight="A") & ~Lexeme("dog"))
Lexeme(value, invert=False, prefix=False, weight=None) собирает tsquery из лексем, соединённых через
&, | и ~; значение передаётся параметром. SearchQuery из лексем читается как RAW.
FullTextIndex (SQLite)¶
class FullTextIndex(SeparateTableIndex):
def __init__(
self,
*,
fields: Sequence[str],
name: str | None = None,
tokenizer: str | None = None,
) -> None: ...
Отдельная таблица FTS5 (CREATE VIRTUAL TABLE ... USING fts5(...)), которая берёт текст из таблицы
модели (content=) и ключуется её целочисленным первичным ключом. Триггеры на вставку, обновление и
удаление держат её в согласии с таблицей — индексируется строка, записанная любым клиентом, не только
hare, — а при создании она заполняется уже имеющимися строками. Миграции создают и удаляют её
(AddIndex, RemoveIndex) вместе с триггерами; смена полей или токенизатора заменяет её.
| Аргумент | Что задаёт |
|---|---|
fields |
Индексируемые текстовые поля — непустой список |
name |
Имя таблицы FTS5 — по умолчанию строится из таблицы и полей |
tokenizer |
Параметр tokenize FTS5: "porter unicode61" (основы английских слов), "trigram" (подстроки), ...; при None — unicode61 самого FTS5 |
Модели нужен целочисленный первичный ключ (rowid FTS5); модель без него, пустой список полей, ключ по
убыванию или пустой токенизатор дают ConfigurationError. На соединении без
Features.supports_full_text_index модель с этим индексом даёт ConfigurationError в Hare.init(), а
миграция, которая создаёт или удаляет его, — UnSupportedError до отправки SQL.
Планы запросов¶
Каждое выражение поиска описывает свой план: следующий запрос той же структуры выполняется по плану со своим текстом поиска. Имена полей, тип поиска, веса полей и параметры разметки входят в структуру.