Стек ранжирования поиска: BM25, эмбеддинги и реранкинг
Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.
Поиск должен учитывать и точное, и семантическое соответствие. Запрос «wireless headphones» должен находить эти слова, но итоговый порядок может также зависеть от качества продукта, предпочтений пользователя и доступности. Ни один метод ранжирования не умеет одинаково хорошо учитывать все эти сигналы.
В этой статье мы собираем стек поэтапно: retrieval с BM25, плотные эмбеддинги, Reciprocal Rank Fusion, реранкинг cross-encoder и, наконец, listwise-ранжирование с помощью LLM. В репозитории с демо есть запускаемый код для всех этапов на выборке данных Amazon ESCI для продуктового поиска.
Краткое руководство по выбору этапов см. в статье BM25 vs Embeddings vs Rerankers.
Выбирайте этапы по типу сбоя
Production-стек представляет собой воронку, но конкретная конфигурация зависит от запроса и бизнес-контекста.
| Сценарий | Начальный стек | Что валидировать |
|---|---|---|
| Поиск товаров | BM25 + dense retrieval + RRF + cross-encoder | Recall атрибутов, замены, латентность, бизнес-ограничения |
| Поиск по документации | Hybrid retrieval + cross-encoder | Точные идентификаторы, семантические вопросы, фильтры версий |
| Дефлексия обращений в поддержку | Hybrid retrieval + проверки цитат | Recall retrieval, граундинг, отказ от ответа |
| Маркетплейс или объявления | Лексические фильтры + dense retrieval + бизнес-реранкер | Доступность, актуальность, политики, разнообразие продавцов |
| Небольшой внутренний корпус | Базовый BM25, затем реранкер | Оправдывает ли mismatch словаря dense-индекс |
| Поиск в юридической или медицинской сфере с высокими рисками | Retrieval с фокусом на recall и экспертная проверка | Покрытие, происхождение данных, калиброванный отказ |
Начните с BM25 как baseline. Добавляйте dense retrieval, когда mismatch словаря снижает recall. Добавляйте cross-encoder, когда на первой странице есть подходящие кандидаты, но они расположены в неправильном порядке. Подключайте LLM только после того, как сможете позволить себе такую латентность и оценивать решения по ранжированию.
Как мы к этому пришли
Стек проще всего понимать как три слоя. Лексический retrieval находит точные термины, dense retrieval закрывает пробелы в словаре, а реранкеры детально сравнивают наиболее сильных кандидатов.
BM25 и лексический retrieval
Десятилетиями BM25 был стандартом по умолчанию. Это вероятностная модель, которая оценивает документы по частоте терминов запроса в документе, нормируя результат по длине документа и обратной документной частоте (IDF).
BM25 особенно силён, когда намерение выражено буквальными терминами: кодами ошибок, SKU товаров, именами и API-идентификаторами. Его главное ограничение — mismatch словаря. Запрос «cheap laptop» может не найти документ о «budget notebook computer», если в индексируемом тексте нет связки между этими выражениями.
Тем не менее BM25 — надёжный baseline. В таблице лидеров BEIR для запуска BM25 multifield сообщается средний результат 0.429 nDCG@10 на 18 датасетах. Воспроизведение Pyserini использует Lucene multifield-индекс с contents=1.0 и title=1.0, который ищется с помощью --bm25. Это не реализация rank_bm25 из данного демо с простой токенизацией по пробелам. BM25 также превосходит некоторые neural-модели в задачах аргументативного retrieval, например в Touche-2020.
Dense retrieval и эмбеддинги
Энкодеры в стиле BERT сделали dense retrieval практичным. Они отображают запросы и документы в общее векторное пространство, после чего ранжируют кандидатов по функции сходства, например cosine similarity или dot product.
Архитектура bi-encoder (или «two-tower») независимо обрабатывает запрос и документ через отдельные encoder tower и создаёт эмбеддинги фиксированной длины. Векторы документов можно заранее вычислить и проиндексировать офлайн, а затем быстро выполнять retrieval с помощью алгоритмов Approximate Nearest Neighbor (ANN). Теперь «cheap laptop» и «budget notebook» оказываются близко друг к другу в векторном пространстве.
Некоторые bi-encoder используют Siamese architecture, как в Sentence-BERT, где обе стороны разделяют веса. Другие используют отдельные tower для запроса и документа. Pooling, размер вектора, функция сходства и objective обучения — это выбор конкретной модели, а не свойства каждого dense retriever.
Такие модели обучают с помощью contrastive learning, обычно с loss-функцией InfoNCE. Для batch пар (query, positive_document) objective максимизирует sim(query, positive_doc) и минимизирует sim(query, negative_docs). Негативные примеры берутся из позитивных документов других запросов в том же batch (in-batch negatives). Параметр temperature определяет, насколько резко модель должна разделять эти два случая.
Данные обучения часто важнее размерности эмбеддинга. Retrieval-модели учатся на парах query-positive и тщательно отобранных hard negatives: правдоподобных, но нерелевантных документах. В следующем разделе про обучение показано, как SimANS избегает как тривиальных негативов, так и вероятных false negatives.
Цена — representation bottleneck. Bi-encoder сжимает все семантические нюансы в один вектор фиксированного размера, поэтому часто пропускает тонкие взаимодействия между конкретными терминами запроса и конкретными фрагментами документа.
Cross-encoder и LLM
Cross-encoder (Nogueira & Cho, 2019) подаёт запрос и документ вместе в один Transformer как конкатенированную последовательность ([CLS] Query [SEP] Document), поэтому каждый токен запроса может attend к каждому токену документа через полный self-attention. Такое глубокое взаимодействие улавливает нюансы, которые теряются при независимом кодировании.
LLM-реранкинг использует промптованную модель для одновременного сравнения нескольких кандидатов. RankGPT показал сильные zero-shot listwise-результаты с GPT-4 на оценивавшихся бенчмарках, однако стабильность вывода, стоимость и соответствие домену всё ещё требуют отдельного тестирования.
Эти оценки нельзя заранее вычислить для произвольных запросов, поэтому реранкинг выполняется после retrieval. Такая асимметрия стоимости и мотивирует использовать многоэтапную воронку.
Многоэтапная воронка
Запуск дорогого cross-encoder или LLM для миллионов документов нецелесообразен, поэтому современные поисковые стеки используют воронку. Каждый этап сокращает пул кандидатов, а сложность модели возрастает.
Дешёвому retriever также не хватает финальной точности, поэтому воронка применяет каждую модель только там, где её стоимость приемлема.
| Этап | Масштаб входа | Основная цель | Типичные методы | Измерение выхода |
|---|---|---|---|---|
| Retrieval | Корпус или индекс | Recall кандидатов | BM25, bi-encoder | Recall на cutoff кандидатов |
| Pre-ranking | Большой candidate set | Дешёвая фильтрация | Лёгкие модели, правила | Сохранённый recall на миллисекунду |
| Full ranking | Shortlist | Качество top-rank | Cross-encoder, LLM | NDCG/MRR, латентность, стоимость |
| Blending | Финальные списки или слоты | Ограничения и mix | Правила, multi-objective rank | Политики, разнообразие, бизнес-гардрейлы |
Retrieval задаёт потолок, а реранкинг оптимизирует результат внутри него. Если релевантный документ не пережил retrieval, downstream-модели уже не смогут его восстановить.
Демо: пайплайн из пяти этапов
Чтобы сделать это конкретным, я собрал демо search-ranking-stack, которое запускает пайплайн из пяти этапов на бенчмарке продуктового поиска Amazon ESCI. Каждый этап измеряется отдельно, чтобы было видно, откуда именно берутся улучшения.
Пайплайн:
- Разреженный retrieval BM25 — лексический baseline (
rank_bm25) - Retrieval с dense bi-encoder — генерация семантических кандидатов (
all-MiniLM-L6-v2) - Гибридный fusion RRF — fusion разреженных и dense-результатов по позициям
- Реранкинг cross-encoder — попарные оценки релевантности (
ms-marco-MiniLM-L-12-v2) - Listwise-реранкинг с помощью LLM — сравнение финального shortlist по промпту (Ollama, API или локальная модель)
Этапы 1–3 — это этап retrieval воронки (максимизируем recall); этапы 4–5 — этап full ranking (максимизируем precision). В демо отсутствуют pre-ranking и blending. При ~8 500 документах можно позволить себе отправлять все результаты hybrid retrieval напрямую на реранкинг.
Быстрый старт
git clone https://github.com/slavadubrov/search-ranking-stack.git
cd search-ranking-stack
uv sync
# Download and sample ESCI dataset (~2.5GB download, ~5MB sample)
uv run download-data
# Run the full pipeline (without LLM reranking)
uv run run-all
# Run with LLM reranking via Ollama
uv run run-all --llm-mode ollama
Датасет и sampling: Amazon ESCI
Демо использует Amazon Shopping Queries Dataset (ESCI) из KDD Cup 2022 — реальный бенчмарк продуктового поиска с четырёхуровневыми graded labels релевантности:
| Label | Gain | Значение | Пример (запрос: «wireless headphones») |
|---|---|---|---|
| Exact (E) | 3 | Выполняет все требования запроса | Sony WH-1000XM5 Wireless Headphones |
| Substitute (S) | 2 | Функциональная альтернатива | Проводные наушники с Bluetooth-адаптером |
| Complement (C) | 1 | Связанный полезный товар | Чехол для переноски наушников |
| Irrelevant (I) | 0 | Не имеет содержательной связи | USB-кабель для зарядки |
Graded relevance важна, потому что позволяет использовать NDCG (Normalized Discounted Cumulative Gain), который отличает «идеальное» ранжирование от «просто приемлемого». Бинарные метрики оценили бы их одинаково.
Я использовал sample small_version из демо: около 500 запросов, 8 500 товаров и 12 000 judgments. Downloader читает единственный split train из tasksource/esci, фильтрует английскую locale us и small_version == 1, а затем с seed 42 выбирает до 500 уникальных query ID без возвращения. Корпус содержит уникальные товары из выбранных строк с judgments. Это не held-out split и не независимый корпус. Выборка достаточно мала, чтобы запускаться на ноутбуке, но слишком мала и специфична для домена, чтобы обосновывать production-ранжирование. Используйте её для воспроизведения этапов и изучения failure modes; решения о деплое принимайте по held-out-запросам, репрезентативным для реального трафика.
Retrieval: hybrid search
Задача retrieval-слоя — максимизировать recall: добавить в candidate set как можно больше релевантных документов.
BM25: лексический baseline
BM25 оценивает документы по пересечению терминов с запросом, учитывая насыщение term frequency и нормализацию длины документа:
Здесь — обратная документная частота термина , — term frequency в документе , — длина документа, а — средняя длина документа по корпусу. Важны два параметра: (обычно 1.2–2.0) управляет насыщением TF — тем, насколько быстро повторные вхождения перестают приносить дополнительную пользу, — а (обычно 0.75) управляет нормализацией длины документа.
Реализация короткая. Простая токенизация по пробелам с rank_bm25:
# src/search_ranking_stack/stages/s01_bm25.py
from rank_bm25 import BM25Okapi
def run_bm25(data: ESCIData, top_k: int = 100):
doc_ids = list(data.corpus.keys())
tokenized_corpus = [text.lower().split() for text in data.corpus.values()]
bm25 = BM25Okapi(tokenized_corpus)
results = {}
for query_id, query_text in data.queries.items():
scores = bm25.get_scores(query_text.lower().split())
top_indices = np.argsort(scores)[::-1][:top_k]
results[query_id] = {doc_ids[idx]: float(scores[idx]) for idx in top_indices}
return results
BM25 даёт Recall@100 0.741 — 74% релевантных товаров появляются где-либо в top-100. Для чисто лексического метода это неплохо, но 26% релевантных товаров становятся невидимыми для всех downstream-этапов.
Retrieval с dense bi-encoder
Bi-encoder независимо отображает запросы и документы в общее embedding-пространство:
# src/search_ranking_stack/stages/s02_dense.py
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
# Encode corpus once, cache to disk
corpus_embeddings = model.encode(
doc_texts,
batch_size=128,
normalize_embeddings=True, # Cosine sim = dot product
convert_to_numpy=True,
)
# At query time: encode query, compute dot product
query_embeddings = model.encode(query_texts, normalize_embeddings=True)
similarity_matrix = np.dot(query_embeddings, corpus_embeddings.T)
Для нормализованных эмбеддингов cosine similarity сводится к dot product. Демо вычисляет полную матрицу query-by-corpus, потому что 8 500 документов без проблем помещаются в памяти; production-корпус обычно использовал бы approximate-nearest-neighbor index. В этой выборке all-MiniLM-L6-v2 повышает Recall@100 с 0.741 до 0.825.
Как bi-encoder учатся хорошим представлениям
Обучение bi-encoder обычно проходит в два этапа. Сначала модель проходит pre-training на датасетах Natural Language Inference (NLI) и Semantic Textual Similarity (STS), которые формируют общее семантическое понимание. На этом этапе модель учится тому, что «a cat sits on a mat» и «a feline rests on a rug» должны иметь похожие эмбеддинги. Затем её fine-tune-ят на retrieval-специфичных данных, таких как MS MARCO, где модель учится располагать поисковый запрос и релевантный passage ближе друг к другу, чем запрос и нерелевантные passages.
Второй этап зависит от hard negative mining. Случайные негативы — например, документ о кулинарии в паре с запросом о наушниках — тривиально легко отличить, поэтому модель почти ничему на них не учится. Вместо этого текущую модель используют для поиска документов, которые она ранжирует высоко, но которые на самом деле нерелевантны.
Подход SimANS (Simple Ambiguous Negatives Sampling) формализует эту идею. Сначала все документы ранжируются текущим bi-encoder, затем исключаются простые негативы, которые стоят слишком низко, чтобы модель могла на них учиться, и потенциальные false negatives, которые стоят настолько высоко, что могут быть релевантными, но не размечены. Оставшиеся документы из среднего диапазона несут больше всего обучающего сигнала.
# What a training triplet looks like after hard negative mining
training_triplet = {
"query": "wireless noise canceling headphones",
"positive": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones",
"negative": "Sony headphone replacement ear pads", # Hard negative: same brand, related product, but wrong intent
}
# The bi-encoder must learn that "ear pads" is NOT what the user wants,
# even though it shares many tokens with the positive document.
Contrastive loss (InfoNCE) связывает всё это воедино. Для каждого запроса с позитивным документом и набором негативных документов :
Здесь — cosine similarity между эмбеддингами запроса и документа, а — temperature-параметр (обычно 0.05–0.1). Меньшие значения делают loss более чувствительным к hard negatives. По сути, это softmax cross-entropy: нужно повысить similarity позитивной пары относительно всех негативов. Когда мало, даже небольшие различия в similarity создают большие градиенты, заставляя модель проводить более тонкие различия.
Сервинг эмбеддингов bi-encoder в большом масштабе
Архитектурное преимущество bi-encoder — это offline/online split. Эмбеддинги документов вычисляются на этапе построения индекса и сохраняются в vector index. Во время запроса система кодирует запрос и ищет по сохранённым векторам. Латентность зависит от энкодера, hardware, индекса, фильтров и целевого recall, поэтому эти два шага нужно профилировать отдельно.
В демо математика проста: 8 500 документов 384 измерения 4 байта на float = ~13 MB эмбеддингов. На production-масштабе цифры уже не такие скромные: 1 млрд документов с эмбеддингами размерности 768 требуют около 3 TiB хранилища. Здесь пригодятся quantization (сжатие 32-битных float до 8-битных integer), product quantization (разбиение векторов на подпространства) и индексы на базе SSD, такие как DiskANN. Алгоритмы индекса рассмотрены в разделе о dense-vector indexing.
Зачем тестировать hybrid retrieval
Два метода часто ошибаются по-разному. BM25 хорошо подходит для имён собственных, SKU товаров и кодов ошибок. Dense retrieval может закрыть mismatch словаря, например между «cheap laptop» и «budget notebook computer». Польза fusion зависит от того, насколько часто такие комплементарные случаи встречаются в целевом наборе запросов.
Естественный следующий эксперимент — hybrid search: запустить оба retrieval-метода, а затем объединить их ранжированные списки.
Reciprocal rank fusion (RRF)
BM25 и dense retrieval создают score с разным смыслом и разным масштабом. Поэтому линейная комбинация требует калибровки и валидации при каждом изменении retriever или корпуса.
Reciprocal Rank Fusion (Cormack et al., 2009) полностью отбрасывает исходные score и использует только позицию в ранжировании:
Здесь — smoothing constant; 60 — распространённая отправная точка. RRF вознаграждает элементы, которые занимают высокие позиции в нескольких входных списках, не сравнивая их исходные score. Это избавляет от калибровки шкал score, но cutoffs retrieval, веса и всё равно требуют оценки.
Реализация:
# src/search_ranking_stack/stages/s03_hybrid_rrf.py
def reciprocal_rank_fusion(ranked_lists, k=60, top_k=100):
fused_results = {}
for query_id in all_query_ids:
rrf_scores = defaultdict(float)
for results in ranked_lists:
sorted_docs = sorted(results[query_id].items(),
key=lambda x: x[1], reverse=True)
for rank, (doc_id, _score) in enumerate(sorted_docs, start=1):
rrf_scores[doc_id] += 1.0 / (k + rank)
sorted_rrf = sorted(rrf_scores.items(),
key=lambda x: x[1], reverse=True)[:top_k]
fused_results[query_id] = dict(sorted_rrf)
return fused_results
Hybrid RRF достигает Recall@100 0.842 и NDCG@10 0.628, превосходя BM25 (0.585) и Dense (0.611) по отдельности. Документу достаточно хорошо ранжироваться хотя бы одним методом, чтобы пережить fusion.
Реранкинг cross-encoder
При 100 hybrid-кандидатах на запрос можно позволить себе более дорогую модель. Cross-encoder обрабатывает запрос и документ вместе через один Transformer, с полным cross-attention между всеми токенами.
Взаимодействие на уровне токенов
Разница заключается в attention matrix. В bi-encoder attention имеет блочно-диагональную структуру: токены запроса attend только к другим токенам запроса, а токены документа — только к токенам документа. Два представления никогда не встречаются на уровне токенов — они пересекаются только в конце через dot product. Cross-encoder вычисляет полную attention matrix, где каждый токен запроса attend к каждому токену документа и наоборот. Именно этот cross-attention делает возможным глубокое взаимодействие на уровне токенов.
В bi-encoder запрос «apple» кодируется до того, как модель видит какой-либо документ. Cross-encoder видит запрос и кандидата вместе, поэтому может использовать их взаимосвязь на уровне токенов. Это может помочь в таких случаях:
- Отрицание: «headphones that are not wireless». Pooled embedding может недооценить отрицание, тогда как совместное кодирование даёт модели прямое взаимодействие между запросом и документом. Это гипотеза, которую нужно проверить на целевой выборке, а не гарантия.
- Ограничение: «laptop under $500». Совместное кодирование может связать ограничение с ценой в тексте товара, хотя структурированные price-фильтры безопаснее, если соответствующее поле доступно.
Вход cross-encoder форматируется как [CLS] query tokens [SEP] document tokens [SEP]. [CLS] — classification-токен, чьё финальное hidden state подаётся на linear head для получения единого score релевантности. Segment embeddings отличают токены запроса от токенов документа, а [SEP] обозначает границу между сегментами.
Как обучают cross-encoder
Cross-encoder можно обучать на примерах (query, document, relevance_label) с pointwise-, pairwise- или listwise-objective. В примере ниже используется один label релевантности pointwise; это не единственный возможный дизайн обучения.
# Cross-encoder training data format
training_example = {
"query": "wireless headphones",
"document": "Sony WH-1000XM5 Wireless Headphones",
"label": 1.0, # Relevant
}
# Forward pass: [CLS] hidden state → Linear layer → sigmoid → score
# Loss: binary cross-entropy between predicted score and label
Распространённый classifier отображает финальное представление [CLS] в score. Для бинарных labels можно использовать binary cross-entropy; graded relevance допускает regression-, ordinal-, pairwise- или listwise-loss. Выбирайте objective по held-out ranking metrics, а не исходя из предположения, что один objective всегда лучше остальных.
Hard negative mining ещё важнее для cross-encoder, чем для bi-encoder. Cross-encoder дорого обучать: для каждого training example нужен полный forward pass через конкатенированную последовательность, поэтому нельзя тратить вычисления на тривиально простые негативы. Практический рецепт: использовать bi-encoder для retrieval top-K-кандидатов каждого training query, а затем брать hard negatives из определённых диапазонов позиций (например, 10–100). Так cross-encoder получает примеры, где для различения релевантного и нерелевантного действительно требуется глубокое взаимодействие токенов.
# src/search_ranking_stack/stages/s04_cross_encoder.py
from sentence_transformers import CrossEncoder
model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")
def run_cross_encoder(data, hybrid_results, top_k_rerank=50):
reranked_results = {}
for query_id, query_text in data.queries.items():
candidates = list(hybrid_results[query_id].items())[:top_k_rerank]
# Form (query, document) pairs for joint encoding
pairs = []
doc_ids = []
for doc_id, _ in candidates:
doc_text = data.corpus.get(doc_id, "")[:2048]
pairs.append([query_text, doc_text])
doc_ids.append(doc_id)
# Score all pairs with full cross-attention
scores = model.predict(pairs, batch_size=64)
# Rerank by cross-encoder score
scored_docs = sorted(zip(doc_ids, scores),
key=lambda x: x[1], reverse=True)
reranked = {
doc_id: float(score) for doc_id, score in scored_docs
}
# Keep original hybrid candidates at ranks 51--100 for Recall@100.
for doc_id, score in list(hybrid_results[query_id].items())[top_k_rerank:100]:
if doc_id not in reranked:
reranked[doc_id] = float(score) * 0.01
reranked_results[query_id] = reranked
return reranked_results
В записанном запуске демо ms-marco-MiniLM-L-12-v2 выполняет реранкинг 50 кандидатов на запрос и повышает NDCG@10 с 0.628 до 0.645. Измеряйте его латентность на hardware деплоя: на результат влияют модель, длина последовательности, batch size и runtime.
Компромисс между скоростью и качеством
Почему бы не использовать cross-encoder для всего? Потому что score нельзя вычислить заранее. Эмбеддинги документов bi-encoder не зависят от запроса, поэтому их можно вычислить один раз и сохранить. Выход cross-encoder зависит одновременно от запроса и документа. Score релевантности для пары «wireless headphones» и товара Sony возникает из полного cross-attention между конкретными токенами. Его нельзя закэшировать или переиспользовать для другого запроса.
Bi-encoder требует одного кодирования запроса и vector search по заранее вычисленным эмбеддингам документов. Cross-encoder оценивает каждую пару query-document в shortlist, причём стоимость растёт и с числом кандидатов, и с длиной последовательности. Batching помогает, но оценивать 100 000 кандидатов всё равно не имеет смысла; сначала выполняйте retrieval, а затем бенчмарките максимальный shortlist, укладывающийся в целевые показатели качества и латентности.
В демо Recall@100 остаётся равным 0.842 на всём этапе cross-encoder. Реранкинг может изменить порядок результатов, но не добавить документы. Потолок задаётся retrieval.
Listwise-реранкинг с помощью LLM
На финальном этапе демо используется LLM для listwise-реранкинга. Вместо независимой оценки каждого документа модель видит top-10 и возвращает порядок. По примеру RankGPT промпт явно задаёт относительное сравнение, но одновременно добавляет ограничения контекстного окна, position bias, ошибки парсинга и вариативность между запусками.
Listwise-промпт
Шаблон промпта просит LLM учитывать иерархию релевантности ESCI:
# src/search_ranking_stack/stages/s05_llm_rerank.py
def _create_listwise_prompt(query, documents, max_words=200):
n = len(documents)
doc_texts = []
for i, (doc_id, doc_text) in enumerate(documents, start=1):
words = doc_text.split()[:max_words]
doc_texts.append(f"[{i}] {' '.join(words)}")
return (
f"I will provide you with {n} product listings, each indicated by "
f"a numerical identifier [1] to [{n}]. Rank the products based on "
f'their relevance to the search query: "{query}"\n\n'
"Consider:\n"
"- Exact matches should rank highest\n"
"- Substitutes should rank above complements\n"
"- Irrelevant products should rank lowest\n\n"
f"{chr(10).join(doc_texts)}\n\n"
"Output ONLY a comma-separated list of identifiers: [3], [1], [2], ...\n"
"Do not explain your reasoning."
)
Три режима запуска
Демо поддерживает три бэкенда для LLM-реранкинга:
| Режим | Модель | Как запускается |
|---|---|---|
ollama | llama3.2:3b (настраивается) | Локально через Ollama API |
api | claude-haiku-4-5-20251001 | Anthropic API |
local | Qwen/Qwen2.5-1.5B-Instruct | HuggingFace Transformers |
Парсинг и fallback
LLM не обязана соблюдать запрошенную схему, поэтому важны парсинг и fallback-путь:
def _parse_ranking(output: str, n: int) -> list[int] | None:
"""Parse LLM output to extract ranking order."""
matches = re.findall(r"\[(\d+)\]", output)
if not matches:
return None
positions = [int(m) - 1 for m in matches]
# Pad with remaining positions if LLM returned partial output
if len(positions) < n:
seen = set(positions)
for i in range(n):
if i not in seen:
positions.append(i)
return positions[:n]
Если парсинг полностью завершается ошибкой, демо использует порядок cross-encoder. Production-парсер также должен отклонять идентификаторы вне допустимого диапазона и дубликаты, добавлять пропущенных кандидатов в их прежнем порядке, логировать сбой и сравнивать долю fallback с порогом, заданным для запуска.
Результаты на этой выборке ESCI
Эти записанные результаты до этапа с LLM используют small_version демо: английскую locale us
после фильтра small_version == 1, с выборкой до 500 query ID
без возвращения и seed 42, а также корпусом, построенным по товарам с judgments.
Они не являются ни held-out-оценкой, ни независимым корпусом.
Значения MRR используют бинарное правило evaluator демо relevance > 0: результат
Complement, Substitute или Exact считается релевантным, а
Irrelevant — нет. recip_rank оценивает каждый возвращённый список
кандидатов, содержащий до 100 кандидатов; это не MRR@10.
| Этап | NDCG@10 | MRR | Recall@100 | Дельта NDCG |
|---|---|---|---|---|
| BM25 | 0.585 | 0.812 | 0.741 | — |
| Dense Bi-Encoder | 0.611 | 0.808 | 0.825 | +0.026 |
| Hybrid (RRF) | 0.628 | 0.834 | 0.842 | +0.017 |
| + Cross-Encoder | 0.645 | 0.860 | 0.842 | +0.017 |
Ключевые наблюдения
Hybrid search превосходит каждый метод по отдельности. NDCG RRF (0.628) выше, чем у BM25 (0.585) и Dense (0.611). Методы могут ошибаться на разных запросах, и их объединение возвращает документы, которые каждый из них по отдельности пропустил бы.
Recall задаётся на этапе retrieval. Recall@100 остаётся равным 0.842 на всём этапе cross-encoder. Реранкеры меняют порядок, но не добавляют документы. Если нужен более высокий recall, исправляйте retrieval-слой. Значения MRR выше используют то же правило демо: Complement и всё, что выше, считается релевантным.
Результат LLM не приводится. Парсер принимает дубликаты и идентификаторы вне допустимого диапазона, после чего его padding-путь может незаметно изменить список кандидатов. Поэтому предыдущее сравнение с LLM носит иллюстративный характер и не является аудируемым результатом. Повторите эксперимент со строгой валидацией идентификаторов, логированием fallback, несколькими запусками, измерением латентности и стоимости, а также на held-out-наборе домена, прежде чем приписывать какое-либо улучшение реранкеру.
Репозиторий полезен для настройки и изучения кода, но в его README по-прежнему указан некорректный результат + LLM Reranker NDCG@10 0.717. До появления аудируемой LLM-оценки считайте авторитетными таблицу статьи с результатами до LLM и оговорку о LLM выше.
На этой выборке dense retrieval превосходит BM25. Изучите срезы запросов, прежде чем объяснять причину. Mismatch словаря — один из возможных факторов, но на сравнение также влияют формирование выборки, токенизатор, домен обучения модели и поля корпуса.
Оценка: измеряйте то, что важно
В демо используются три метрики, каждая из которых смотрит на ранжирование под своим углом:
NDCG@10 (основная метрика)
Normalized Discounted Cumulative Gain измеряет качество top-10-ранжирования с использованием graded relevance. Метрика поощряет размещение высокорелевантных документов ближе к началу списка с логарифмической скидкой:
Из трёх метрик только NDCG полностью использует четырёхуровневую graded relevance ESCI. Система, поставившая Exact match на позицию 1, получает более высокий score, чем система, поставившая туда Substitute. Поэтому здесь это основная метрика.
MRR (первый результат Complement или выше)
Mean Reciprocal Rank использует позицию первого результата, который считается релевантным. В этом демо evaluator передаёт graded qrels ESCI в pytrec_eval и использует recip_rank для каждого возвращённого списка длиной до 100 кандидатов, поэтому relevance > 0 — бинарный threshold: Complement (1), Substitute (2) и Exact (3) считаются релевантными. Irrelevant (0) — нет. Релевантный результат на позиции 1 даёт reciprocal rank 1.0, на позиции 3 — 0.333. Это MRR по возвращённым спискам кандидатов, а не MRR@10.
Recall@100 (покрытие retrieval)
Recall показывает, какая доля размеченных релевантных документов появляется в top-100. Это метрика candidate ceiling для оцениваемых judgments: реранкер не может добавить документ, пропущенный retrieval, а неполные judgments делают видимый потолок неопределённым.
Индексация dense-векторов за пределами демо
Dense-эмбеддинги становятся полезными на масштабе только после появления индекса Approximate Nearest Neighbor (ANN). Демо использует brute-force cosine similarity, что нормально при ~8 500 документах, но production-системам нужны специализированные индексы.
HNSW (hierarchical navigable small world)
HNSW строит многоуровневый граф: разреженные верхние уровни обеспечивают широкую навигацию, а более плотные нижние уточняют соседство. M управляет связностью графа, а efSearch задаёт компромисс между вычислениями на запрос и recall. Полезные значения зависят от размерности, распределения расстояний, фильтров, реализации и целевого recall.
Операционная особенность — updates и deletions, поскольку графовым индексам может требоваться фоновое восстановление. Поведение зависит от базы данных. Например, в issue Qdrant сообщается о снижении качества filtered search в multi-tenant-конфигурации HNSW. В отчёте измерено среднее precision@100 0.597 ± 0.0541 с фильтром library_id, тогда как exact search в более подробном анализе достиг 100% recall. Изменение payload_m принудительно запустило rebuild и восстановило качество filtered search. Отчёт посвящён tenant-фильтрам, конфигурации HNSW и принудительному rebuild индекса HNSW, а не workload с большим количеством deletions. Воспроизведите целевой churn-профиль и включите в оценку compaction или rebuild.
IVF (inverted file)
IVF-индексы разбивают векторное пространство на кластеры, а затем сканируют nprobe кластеров, ближайших к запросу. Они могут дать полезный компромисс между памятью, временем построения и recall, особенно вместе с compression. Семантика updates и производительность зависят от конкретной реализации, а не только от семейства индекса.
Для экстремального масштаба IVF_RaBitQ (Gao & Long, SIGMOD 2024) сжимает floating-point-векторы до представлений из одного бита. В пространстве высокой размерности знак координаты (+/–) содержит достаточно угловой информации для вычисления similarity.
| Измерение | Граф HNSW | Кластеры IVF |
|---|---|---|
| Управление запросом | efSearch | nprobe |
| Управление построением | Связность и construction beam | Число кластеров и training sample |
| Профиль памяти | Рёбра графа и векторы | Центроиды, списки и сохранённые векторы |
| Поведение при updates | Специфичный для базы repair/cleanup | Специфичное для базы обслуживание списков |
| Оценивать по | Кривой recall-latency-memory-churn | Кривой recall-latency-memory-churn |
В одном case study Uber по поиску доставок снижение параметра поиска на уровне shard с 1 200 до 200 уменьшило заявленную latency на 34%, а CPU — на 17%, при почти неизменном измеренном recall. Универсальный вывод — настраивать кривую recall-cost на production-подобном трафике, а не копировать значение 200.
Опциональные расширения после core pipeline
Когда для retrieval и реранкинга появляются отдельные измерения, несколько расширений становится проще оценивать, не скрывая базовый пайплайн.
Понимание запроса
Query expansion и rewriting могут устранить mismatch словаря ещё до retrieval. Query2doc генерировал pseudo-documents и сообщал об улучшении BM25 в экспериментах на MS MARCO. Expansion также может привнести неправильное intent, поэтому сравнивайте recall и precision на срезах неоднозначных, навигационных запросов и запросов с точными идентификаторами.
Практические паттерны: expansion аббревиатур, enrichment сущностей, декомпозиция sub-query для multi-hop reasoning и RAG-Fusion — генерация нескольких вариантов запроса и объединение результатов через RRF.
Разметка релевантности с помощью LLM
LLM могут создавать черновые labels релевантности, когда человеческих judgments недостаточно. TALEC и работа Pinterest по разметке релевантности представляют два оценённых дизайна. Label от LLM всё равно остаётся output модели: калибруйте его по ослеплённым human judgments, изучайте срезы расхождений и сохраняйте human gold set для regression tests.
Полезные меры контроля:
- rubric с конкретными границами релевантности и примерами;
- ослеплённая human calibration и периодические повторные проверки;
- рандомизация порядка и повторные judgments для нестабильных случаев;
- панели моделей, если дополнительные затраты повышают согласованность; и
- явные проверки на position bias и bias к среднему значению.
Knowledge distillation
Если teacher-LLM даёт прирост, но не укладывается в ограничения сервинга, одним из вариантов становится distillation:
- Использовать мощный LLM (teacher) для реранкинга тысяч training queries
- Обучить небольшой быстрый cross-encoder (student, ~100–200M параметров), который имитирует ranking distribution LLM
- Сравнить student с teacher и baseline по качеству, calibration и стоимости сервинга
InRanker дистиллирует MonoT5-3B в модели на 60M и 220M параметров — уменьшение размера в 50 раз при сопоставимой производительности. Подход Rank-Without-GPT создаёт open-source listwise-реранкеры на 7B, достигающие 97% эффективности GPT-4 благодаря fine-tuning с QLoRA.
Опубликованные результаты compression — это отправная точка, а не ожидаемые production-коэффициенты. Distillation может унаследовать biases teacher и потерять качество на редких срезах запросов, поэтому исходные judgments релевантности нужно сохранять в evaluation loop.
Персонализация и position bias
Общая релевантность имеет пределы. Поиск «apple» должен возвращать iPhone техноэнтузиасту и рецепты с яблоками пользователю, который просматривал кулинарный контент.
Распространённая retrieval-архитектура для персонализации использует two-tower embedding model: query tower кодирует запрос и пользовательский контекст, а item tower — товары и метаданные. Offline/online split поддерживает approximate-nearest-neighbor retrieval; его латентность всё равно зависит от энкодера, индекса, фильтров и системы сервинга.
Эмбеддинги объявлений Airbnb, OmniSearchSage Pinterest и two-tower-системы Uber показывают разные production-дизайны. Их масштаб и заявленные улучшения относятся к этим конкретным системам; переносимый паттерн — offline item tower плюс online query/user tower.
Click data содержит position bias и bias экспозиции. PAL — один из подходов к устранению bias: использовать позицию во время обучения, а при сервинге держать её постоянной. Это не универсальное решение; для другого продукта лучше подойдут randomized interventions, методы inverse propensity и counterfactual evaluation.
Адаптация к домену с помощью synthetic queries
Распространённая ошибка при построении search strategy — считать, что модель, обученная на общих web-данных (например, MS MARCO), хорошо заработает в специализированном домене. Это out-of-domain (OOD) problem.
LLM могут уменьшить, но не устранить bottleneck размеченных данных с помощью Generative Pseudo-Labeling (GPL, InPars):
- Взять доменный корпус документов
- Попросить LLM: «Generate a search query that this document would answer»
- Использовать синтетические пары (query, document) для fine-tuning retriever и reranker
Синтетические пары полезны, когда реальных запросов мало, но они отражают особенности генератора и промпта. Удаляйте дубликаты, фильтруйте неправдоподобные запросы и валидируйте результат на реальном held-out-трафике.
Последовательность экспериментов
Добавляйте сложность только тогда, когда предыдущий этап выявляет измеренный сбой:
Шаг 1 (baseline): реализуйте BM25 или текущую лексическую систему и соберите набор запросов с judgments. Зафиксируйте recall, NDCG, латентность и срезы ошибок.
Шаг 2 (recall кандидатов): тестируйте dense retrieval и fusion, только если baseline пропускает релевантные документы. Настройте cutoff кандидатов с учётом recall и стоимости.
Шаг 3 (precision ранжирования): добавляйте cross-encoder, если правильные кандидаты присутствуют, но стоят в неправильном порядке. Выбирайте размер shortlist по кривой качества и латентности.
Шаг 4 (соответствие домену): выполняйте fine-tuning или distillation только после того, как generic-модели демонстрируют стабильные domain-specific сбои. Реальные held-out judgments храните отдельно от synthetic training data.
Шаг 5 (опциональный дорогой слой): тестируйте listwise- или reasoning-based-реранкинг только тогда, когда его incremental quality сохраняется при повторных запусках и оправдывает латентность, стоимость, privacy и сложность fallback.
Направления исследований, которые нужно оценивать отдельно
Reasoning-реранкеры и search-использующие агенты перспективны, но отвечают на другие вопросы, чем пятиэтапное демо продуктового поиска.
Реранкеры на основе reasoning
Rank1 обучает реранкеры на reasoning traces и показывает сильные результаты на бенчмарке BRIGHT. Эти данные релевантны retrieval с высокой долей reasoning, но не являются прямым прогнозом для продуктового поиска ESCI.
Для юридического или научного поиска сравнивайте reasoning-реранкеры с сильными cross-encoder и listwise-baseline на экспертных judgments, цитатах, латентности и стабильности ошибок.
Агентный поиск
Search-o1 изучает модель, которая выполняет дополнительные поисковые запросы во время multi-hop question answering. Это задача оркестрации — генерация запросов, остановка, использование evidence и оценка ответа, — а не ещё один этап реранкинга. Оценивайте её по корректности end-task и подтверждённости цитатами, а не только по retrieval-метрикам.
Ключевые выводы
-
Рассматривайте стек как последовательность экспериментов. Сначала зафиксируйте лексический baseline и набор запросов с judgments, а уже потом добавляйте dense retrieval, fusion или реранкинг.
-
Отдельно измеряйте recall кандидатов и precision ранжирования. На этой выборке ESCI Recall@100 достигает 0.842 после fusion и остаётся неизменным на обоих этапах реранкинга.
-
Используйте hybrid retrieval, когда ошибки комплементарны. В демо RRF улучшил и Recall@100, и NDCG@10, но другой корпус может не оправдать поддержку двух индексов.
-
Добавляйте cross-encoder, когда shortlist правильный, а порядок неправильный. Число кандидатов выбирайте по измеренной кривой качества и латентности.
-
Считайте LLM-реранкинг опциональным финальным экспериментом. Текущее сравнение LLM в демо иллюстративно, поскольку его парсер не проверяет полную перестановку ID кандидатов. До публикации прироста нужны строгий парсинг, логирование fallback, повторные запуски и учёт стоимости.
-
Разделяйте типы свидетельств. Результат статьи, vendor case study, это демо на ноутбуке и production A/B-тест отвечают на разные вопросы.
Полный код пайплайна находится в репозитории. Клонируйте его, чтобы воспроизвести этапы до LLM или протестировать другие модели и параметры; не считайте устаревшую строку с LLM результатом.
References
Papers
- Reciprocal Rank Fusion — Cormack et al., 2009
- RankGPT: LLMs as Zero-Shot Listwise Rerankers — Sun et al., EMNLP 2023 Outstanding Paper
- Rank1: Reasoning-Based Reranking — Weller et al., COLM 2025
- SCaLR: Self-Calibrated Listwise Reranking — фреймворк self-calibrating listwise-реранкинга
- GCCP: Global-Consistent Comparative Pointwise — устранение calibration в pointwise LLM-ранжировании
- Rank-DistiLLM: Knowledge Distillation for Reranking — Schlatt et al., ECIR 2025
- Query2doc: LLM Query Expansion — Wang et al., EMNLP 2023
- GPL: Generative Pseudo Labeling — адаптация dense retrieval к домену
- InRanker: Distilled Reranker — уменьшение размера в 50 раз при сопоставимой производительности
- Search-o1: Agentic Retrieval — EMNLP 2025
- TALEC: LLM-as-a-Judge for Search — evaluation framework
- BEIR Benchmark — Thakur et al., NeurIPS 2021
- BEIR Leaderboard — baseline
BM25 multifieldи aggregate по 18 датасетам - Sentence-BERT — Reimers & Gurevych, EMNLP 2019
- InfoNCE / CPC — van den Oord et al., 2018
- SimANS: Hard Negative Sampling — Zhou et al., EMNLP 2022
- Passage Reranking with BERT — Nogueira & Cho, 2019
- HNSW — Malkov & Yashunin, 2016
- DiskANN — Subramanya et al., NeurIPS 2019
- RaBitQ — Gao & Long, SIGMOD 2024
- Replacing Judges with Juries — Verga et al., 2024
- RAG-Fusion — Rackauckas, 2024
- InPars — Bonifacio et al., SIGIR 2022
- BRIGHT Benchmark — Su et al., ICLR 2025
- Rank-without-GPT — Zhang et al., ECIR 2025
- Pinterest LLM Search Relevance — Wang et al., 2024
- OmniSearchSage — Agarwal et al., WWW 2024
- PAL: Position-bias Aware Learning — Guo et al., RecSys 2019
Datasets and benchmarks
- Amazon ESCI: Shopping Queries Dataset — KDD Cup 2022
- BEIR: Benchmarking IR — heterogeneous-бенчмарк для zero-shot evaluation
- MTEB: Massive Text Embedding Benchmark — leaderboard эмбеддинг-моделей
- ESCI Paper — Reddy et al., 2022
Models used in the demo
- all-MiniLM-L6-v2 — bi-encoder на 22M параметров
- ms-marco-MiniLM-L-12-v2 — cross-encoder на 33M параметров
- Sentence-Transformers — фреймворк neural retrieval-моделей
Tools and platforms
- rank_bm25 — реализация BM25 на Python
- Pyserini BEIR Reproductions — команды для индекса
bm25-multifieldи evaluation - pytrec_eval — evaluation toolkit TREC
- Elasticsearch — hybrid search с Retrievers API
- Vespa — унифицированный search and recommendation engine
- Weaviate — vector database с hybrid search
- Qdrant — vector database с multi-stage queries
Industry references
- Airbnb Listing Embeddings — Grbovic & Cheng, KDD 2018
- Uber Delivery Search — Uber Engineering, 2025
- Uber Two-Tower Embeddings — Uber Engineering, 2023
- Elastic Rerank — Elastic, 2024
Demo project
- search-ranking-stack — рабочее демо со всем кодом из статьи