Что такое semantic ranking на основе embeddings (вторая стадия после ANN)?

Краткий тезис

Semantic ranking на основе embeddings — это второй этап двухстадийного поиска в RAG, который уточняет результаты, полученные на первом этапе с помощью ANN (approximate nearest neighbor). На этом этапе используется более мощная (часто cross-encoder) модель для вычисления семантической близости между запросом и каждым из top-k кандидатов, что даёт более точное ранжирование, но требует больше вычислительных ресурсов. Такой подход позволяет существенно повысить качество retrieval, жертвуя скоростью, и часто применяется в production-системах, где точность критична.


1. Термины: ANN, semantic ranking, embeddings, re-ranking

  • ANN (Approximate Nearest Neighbor) — алгоритм приближённого поиска ближайших соседей в векторном пространстве. Используется на первом этапе для быстрого извлечения кандидатов (обычно top-100–1000) по косинусной близости или L2-расстоянию. Примеры: FAISS, HNSW, ScaNN.
  • Embeddings — векторные представления текста, получаемые с помощью нейросетевых моделей (например, sentence-transformers). На первом этапе обычно используется лёгкая bi-encoder модель (например, all-MiniLM-L6-v2), которая кодирует запрос и документы независимо.
  • Semantic ranking (re-ranking) — процесс переранжирования списка кандидатов с помощью более точной модели, часто cross-encoder, которая обрабатывает пару (запрос, документ) совместно и выдаёт оценку релевантности.
  • Cross-encoder — модель, которая принимает на вход конкатенацию запроса и документа и выдаёт скалярную оценку (например, от 0 до 1). Примеры: cross-encoder/ms-marco-MiniLM-L-6-v2, BAAI/bge-reranker-v2-m3.
  • Bi-encoder — модель, которая кодирует запрос и документ отдельно, а затем сравнивает векторы (косинусное сходство). Быстрее, но менее точна.

2. Зачем нужна вторая стадия после ANN

Первый этап (ANN) решает задачу скорости: из миллионов векторов нужно за миллисекунды найти несколько сотен кандидатов. Однако ANN жертвует точностью:

  • Приближённый поиск может пропустить релевантные документы (низкий recall).
  • Косинусное сходство bi-encoder не всегда хорошо отражает семантическую релевантность (например, запрос «как приготовить пасту» может найти документ про «пасту для зубов» из-за поверхностного совпадения).

Вторая стадия (semantic ranking) решает проблему точности: она берёт top-k от ANN (например, top-100) и заново ранжирует их с помощью более мощной модели. Это позволяет:

  • Поднять релевантные документы наверх.
  • Отсеять ложноположительные срабатывания.
  • Улучшить метрики MRR, NDCG, Recall@k после re-ranking.

3. Как работает semantic ranking на основе embeddings

Процесс состоит из трёх шагов:

  1. Первый этап (ANN):

    • Запрос кодируется bi-encoder'ом в вектор.
    • Вектор ищется в индексе ANN (например, FAISS).
    • Возвращается top-k кандидатов (например, 100).
  2. Второй этап (semantic ranking):

    • Для каждого кандидата из top-k формируется пара (запрос, документ).
    • Cross-encoder обрабатывает каждую пару и выдаёт оценку релевантности (score).
    • Кандидаты сортируются по убыванию score.
    • Возвращается top-m (например, top-10) для подачи в LLM.
  3. Опционально: кэширование:

    • Результаты semantic ranking для часто повторяющихся запросов можно кэшировать (например, в Redis), чтобы избежать повторного дорогого вычисления.

Пример кода (Python с sentence-transformers):

from sentence_transformers import SentenceTransformer, CrossEncoder
import faiss
import numpy as np

# Bi-encoder для ANN
bi_encoder = SentenceTransformer('all-MiniLM-L6-v2')
# Cross-encoder для re-ranking
cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

# Индекс FAISS (предварительно построен)
index = faiss.read_index('documents.index')
documents = [...]  # список текстов документов

def search(query, top_k_ann=100, top_m_rerank=10):
    # Этап 1: ANN
    query_vec = bi_encoder.encode([query])
    distances, indices = index.search(query_vec, top_k_ann)
    candidates = [documents[i] for i in indices[0]]
    
    # Этап 2: Semantic ranking
    pairs = [[query, doc] for doc in candidates]
    scores = cross_encoder.predict(pairs)
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    
    return [doc for doc, score in ranked[:top_m_rerank]]

4. Отличие от первого этапа: bi-encoder vs cross-encoder

ХарактеристикаBi-encoder (ANN)Cross-encoder (semantic ranking)
СкоростьОчень быстрый (миллионы документов за мс)Медленный (сотни пар за мс)
ТочностьСредняя (зависит от модели)Высокая (учитывает контекст пары)
МасштабируемостьХорошая (векторы предвычислены)Плохая (нужно обрабатывать каждую пару)
ПрименениеПервый этап, отсев кандидатовВторой этап, уточнение ранжирования
Пример моделиall-MiniLM-L6-v2cross-encoder/ms-marco-MiniLM-L-6-v2

5. Преимущества двухстадийного подхода

  • Повышение точности: cross-encoder лучше улавливает семантические нюансы (например, отрицание, перефразирование).
  • Снижение шума: отсеиваются нерелевантные документы, которые могли попасть в top-k ANN из-за случайной близости векторов.
  • Гибкость: можно использовать разные модели для разных этапов (лёгкая для скорости, тяжёлая для точности).
  • Улучшение пользовательского опыта: LLM получает более релевантный контекст, ответы становятся точнее.

6. Недостатки и вызовы

  • Latency: добавление второго этапа увеличивает время ответа (обычно на 10–100 мс в зависимости от количества кандидатов и модели).
  • Вычислительные затраты: cross-encoder требует GPU для приемлемой скорости; на CPU может быть слишком медленно.
  • Сложность инфраструктуры: нужно управлять двумя моделями, кэшированием, очередями.
  • Риск overfitting: если re-ranking модель обучена на узком домене, она может хуже работать на новых данных.

7. Стратегии кэширования для semantic ranking

Чтобы снизить latency, результаты re-ranking можно кэшировать:

  • Кэш на уровне запроса: для идентичных запросов возвращаем заранее посчитанный top-m.
  • Кэш на уровне пар (query, document): если одна и та же пара встречается часто, можно сохранить её score.
  • Инвалидация: кэш нужно сбрасывать при обновлении документов или модели.

Пример с Redis:

import redis
import json

cache = redis.Redis(host='localhost', port=6379, db=0)

def cached_rerank(query, candidates):
    key = f"rerank:{hash(query)}"
    cached = cache.get(key)
    if cached:
        return json.loads(cached)
    # Выполняем re-ranking
    pairs = [[query, doc] for doc in candidates]
    scores = cross_encoder.predict(pairs)
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    result = [doc for doc, score in ranked[:10]]
    cache.setex(key, 3600, json.dumps(result))  # TTL 1 час
    return result

8. Метрики оценки semantic ranking

Для оценки качества re-ranking используют те же метрики, что и для retrieval, но считают их после второго этапа:

МетрикаОписаниеЦелевое значение
MRR@kСредний обратный ранг первого релевантного документа>0.8
NDCG@kНормализованный дисконтированный кумулятивный выигрыш (учитывает порядок)>0.9
Recall@kДоля релевантных документов в top-k>0.9
Precision@kДоля релевантных среди top-k>0.7

Сравнение метрик до и после re-ranking показывает прирост (обычно +5–15% по MRR).


9. Связь с Agentic RAG

В Agentic RAG (системах, где агент сам решает, когда и как выполнять поиск) semantic ranking может быть частью инструмента retrieve_and_rerank. Агент может:

  • Вызывать re-ranking только для сложных запросов (где уверенность низкая).
  • Использовать разные re-ranking модели в зависимости от домена.
  • Комбинировать результаты нескольких источников с помощью weighted re-ranking.

10. Альтернативы semantic ranking на основе embeddings

  • LLM-as-a-judge: использовать саму LLM для оценки релевантности (промпт «оцени релевантность документа от 1 до 5»). Медленно, но может быть точнее.
  • Hybrid ranking: комбинировать ANN с BM25 (текстовый поиск) и затем re-ranking.
  • Learning to rank: обучить модель ранжирования на основе фич (например, с помощью LambdaRank).

Пет-проект для закрепления

Задача: Реализовать двухстадийный retrieval с ANN (FAISS) и semantic ranking (cross-encoder) на датасете вопросов-ответов (например, SQuAD или собственный).

Инструменты: Python, FAISS, sentence-transformers, cross-encoder, Flask (для API), Redis (кэш).

Шаги:

  1. Загрузить датасет, разбить на чанки (по 200–500 токенов).
  2. Построить FAISS-индекс с bi-encoder (например, all-MiniLM-L6-v2).
  3. Реализовать API с эндпоинтом /search?query=...&top_k=100&top_m=10.
  4. Добавить re-ranking с cross-encoder (cross-encoder/ms-marco-MiniLM-L-6-v2).
  5. Добавить кэширование результатов re-ranking в Redis.
  6. Сравнить метрики (MRR@10, Recall@10) до и после re-ranking на тестовом наборе запросов.

Ожидаемый результат: Рабочий сервис, который возвращает top-10 релевантных документов с latency < 200 мс (включая re-ranking). Метрики улучшатся на 10–15% по сравнению с одним ANN.


Связь с другими вопросами

ВопросТема
379Что такое ANN (approximate nearest neighbor) и какие алгоритмы используются?
378Какие стратегии chunking'а вы знаете и когда какую применяете?
377Как выбрать embedding-модель для RAG?
376Что такое hybrid search и как его реализовать?
375Какие техники query transformation используются в RAG?
5Как вы оцениваете качество retrieval'а в RAG-системе?

Навигация