awesome-everything EN
↑ Обратно к восхождению

Data engineering

Полнотекстовый поиск: чтение кода и запросов

Суть Читай реальный код сборки индекса, конвейер анализатора, набросок скоринга BM25 и запрос Postgres tsvector, затем предскажи поведение и выбери самый рычажный фикс.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Баги поиска живут в разрыве между тем, какими ты считаешь токены, и тем, что движок на самом деле сохранил. Читай каждый сниппет, предскажи токены или score и выбери фикс, к которому senior потянется первым.

Цель

Отработай цикл, который ты прогоняешь в каждом инциденте поиска: проведи текст через анализатор, представь posting lists, которые он произведёт, прикинь, как BM25 упорядочит кандидатов, и прочитай запрос Postgres tsvector достаточно хорошо, чтобы понять, может ли он вообще использовать индекс.

Сниппет 1 — сборка inverted index

index = {}                      # term -> sorted list of doc ids
def add(doc_id, text):
    for tok in analyze(text):   # tokenize + lowercase + stem
        index.setdefault(tok, [])
        index[tok].append(doc_id)   # append, no dedup, no sort

def search(q):
    lists = [index.get(t, []) for t in analyze(q)]
    return set.intersection(*map(set, lists))   # AND of terms
Викторина

Документ, повторяющий терм (например 'run run run'), добавлен один раз. Что не так с получившимся posting list и каков самый рычажный фикс?

Сниппет 2 — анализатор при индексировании vs при запросе

# index time
def index_analyzer(text):
    return [stem(t) for t in lower(tokenize(text)) if t not in STOPWORDS]

# query time (a different service, written later)
def query_analyzer(text):
    return [t for t in lower(tokenize(text))]   # no stopword drop, no stem
Викторина

Документы проиндексированы через index_analyzer; запросы идут через query_analyzer. Пользователь ищет 'running shoes'. Что произойдёт и каков фикс?

Сниппет 3 — набросок term frequency для BM25

def bm25_tf(tf, doc_len, avg_len, k1=1.2, b=0.75):
    # saturating term frequency with length normalization
    denom = tf + k1 * (1 - b + b * doc_len / avg_len)
    return tf * (k1 + 1) / denom
Викторина

Спам-документ повторяет терм 200 раз; чистый документ имеет его 3 раза при той же длине. Читая эту формулу, какое утверждение верно?

Сниппет 4 — полнотекстовый запрос Postgres

-- column: body text;  index: CREATE INDEX ON docs USING gin(to_tsvector('english', body));
SELECT id, ts_rank(to_tsvector('english', body), q) AS rank
FROM docs, plainto_tsquery('english', 'running shoes') q
WHERE to_tsvector('english', body) @@ q
ORDER BY rank DESC LIMIT 10;
Викторина

Запрос корректен, но делает sequential scan по большой таблице, хотя GIN-индекс существует. Почему и каков фикс?

Итог

Каждый баг поиска читается обратно к токенам и путям доступа: posting list должен дедупить doc id и нести счётчик term-frequency, а не append вслепую; анализаторы при индексировании и при запросе должны быть побайтово одинаковы, иначе совпадения молча исчезают; tf в BM25 насыщается к k1+1, а b нормализует длину, поэтому спам не может доминировать; а запрос Postgres FTS использует свой GIN-индекс, только когда выражение запроса совпадает с индексированным — храни generated tsvector-колонку, а не пересчитывай на строку. Проследи токены, представь posting lists, затем чини структуру до того, как трогать ручку скоринга.

Продолжить восхождение ↑Полнотекстовый поиск: построй и мигрируй поисковый индекс
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.