Stack de ranking de pesquisa: BM25, embeddings e reranking

Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.

A pesquisa tem de satisfazer tanto a intenção exata como a semântica. Uma query por «wireless headphones» deve corresponder a essas palavras, mas a ordenação final também pode depender da qualidade do produto, das preferências do utilizador e da disponibilidade. Nenhum método de ranking trata bem todos esses sinais.

Este artigo constrói o stack por etapas: recuperação com BM25, embeddings densos, Reciprocal Rank Fusion, reranking com cross-encoder e, por fim, ranking listwise com um LLM. Um repositório de demonstração complementar contém código executável para estas etapas, aplicado a uma amostra dos dados de pesquisa de produtos Amazon ESCI.

Em resumo: Construa a pesquisa como um conjunto de etapas mensuráveis. Comece com BM25, adicione recuperação densa quando esta melhorar o recall nas suas queries, faça a fusão apenas se os dois recuperadores tiverem erros complementares e faça reranking apenas do conjunto de candidatos compatível com o orçamento de latência. Os resultados pré-LLM registados abaixo são um exemplo completo baseado numa amostra ESCI não reservada para validação. A comparação com LLM do demo é apenas ilustrativa, porque o parser não valida o ranking devolvido; não demonstra que todos os stacks de produção precisam de um LLM online.

Para um guia curto de seleção de etapas, consulte BM25 vs Embeddings vs Rerankers.


Escolha as etapas com base no modo de falha

O stack de produção é um funil, mas o funil adequado depende da query e da área de negócio.

Caso de utilizaçãoStack inicial candidatoValidar
Pesquisa de produtosBM25 + recuperação densa + RRF + cross-encoderRecall de atributos, substituições, latência, restrições de negócio
Pesquisa de documentaçãoRecuperação híbrida + cross-encoderIdentificadores exatos, questões semânticas, filtros de versão
Contenção de pedidos de suporteRecuperação híbrida + verificações de citaçõesRecall da recuperação, grounding, abstention
Marketplace ou anúnciosFiltros lexicais + recuperação densa + reranker de negócioDisponibilidade, atualidade, políticas, diversidade de vendedores
Corpus interno pequenoBaseline BM25, seguido de um rerankerSe a incompatibilidade de vocabulário justifica um índice denso
Pesquisa jurídica ou médica de alto riscoRecuperação orientada para recall e revisão especializadaCobertura, proveniência, abstention calibrada

Comece com BM25 como baseline. Adicione recuperação densa quando a incompatibilidade de vocabulário prejudicar o recall. Adicione um cross-encoder quando a primeira página tiver os candidatos certos pela ordem errada. Adicione um LLM apenas depois de conseguir suportar a latência e avaliar as decisões de ranking.

Como chegámos aqui

O stack é mais fácil de compreender como três camadas. A recuperação lexical encontra termos exatos, a recuperação densa reduz as lacunas de vocabulário e os rerankers comparam detalhadamente os candidatos mais fortes.

BM25 e recuperação lexical

Durante décadas, o BM25 foi a opção predefinida. É um modelo probabilístico que atribui pontuações aos documentos com base na frequência dos termos da consulta no documento, normalizada pelo comprimento do documento e pela frequência inversa nos documentos (IDF).

O BM25 é forte quando os termos literais exprimem a intenção: códigos de erro, SKUs de produtos, nomes e identificadores de API. A sua principal limitação é a incompatibilidade de vocabulário. Uma consulta por «portátil barato» pode não encontrar um documento sobre um «computador portátil económico» quando o texto indexado não fornece nenhuma ponte entre as expressões.

Ainda assim, o BM25 é uma baseline sólida. A tabela de classificação do BEIR apresenta um nDCG@10 médio de 0,429 em 18 conjuntos de dados para a execução BM25 multifield. A reprodução do Pyserini utiliza um índice multifield do Lucene com contents=1.0 e title=1.0, pesquisado com --bm25. Esta não é a implementação rank_bm25 simples, com tokenização por espaços em branco, usada nesta demonstração. O BM25 também supera alguns modelos neuronais em tarefas de recuperação argumentativa, como Touche-2020.

Recuperação densa e embeddings

Os encoders ao estilo do BERT tornaram prática a recuperação densa. Mapeiam consultas e documentos para um espaço vetorial partilhado e, em seguida, ordenam os candidatos com uma função de similaridade, como a similaridade do cosseno ou o produto escalar.

A arquitetura bi-encoder (ou «two-tower») processa a consulta e o documento de forma independente, através de torres de encoding separadas, produzindo embeddings de comprimento fixo. Os vetores dos documentos podem ser pré-calculados e indexados offline, sendo depois recuperados rapidamente através de algoritmos de Approximate Nearest Neighbor (ANN). Assim, «portátil barato» e «computador portátil económico» ficam próximos no espaço vetorial.

Alguns bi-encoders utilizam uma arquitetura siamesa, como no Sentence-BERT, em que ambos os lados partilham os pesos. Outros utilizam torres separadas para a consulta e para o documento. Pooling, dimensão do vetor, função de similaridade e objetivo de treino são escolhas do modelo, não propriedades de todos os recuperadores densos.

Estes modelos são treinados com aprendizagem contrastiva, normalmente utilizando a função de perda InfoNCE. Dado um batch de pares (query, positive_document), o objetivo maximiza sim(query, positive_doc) e minimiza sim(query, negative_docs). Os negativos provêm dos positivos de outras queries no mesmo batch (negativos in-batch). Um parâmetro de temperatura τ\tau controla o grau de separação exigido ao modelo.

Os dados de treino são frequentemente mais importantes do que a dimensão do embedding. Os modelos de recuperação aprendem a partir de pares query-positive e de hard negatives cuidadosamente selecionados: documentos plausíveis, mas não relevantes. A secção de treino mais adiante mostra como o SimANS evita tanto negativos triviais como prováveis falsos negativos.

O custo é o bottleneck da representação. Os bi-encoders comprimem toda a nuance semântica num único vetor de tamanho fixo, pelo que muitas vezes não captam interações de granularidade fina entre termos específicos da consulta e conteúdo específico dos documentos.

Cross-encoders e LLMs

Cross-encoders (Nogueira & Cho, 2019) introduzem a query e o documento em conjunto num Transformer, como uma sequência concatenada ([CLS] Query [SEP] Document), permitindo que cada token da query atenda a todos os tokens do documento através de self-attention completa. Esta interação profunda capta nuances que a codificação independente não deteta.

O reranking com LLMs utiliza um modelo com um prompt para comparar vários candidatos em simultâneo. O RankGPT demonstrou resultados sólidos zero-shot, em modo listwise, com o GPT-4 nos benchmarks avaliados, mas a estabilidade da saída, o custo e a adequação ao domínio continuam a exigir testes separados.

Estas pontuações não podem ser pré-calculadas para queries arbitrárias, pelo que o reranking é colocado depois da retrieval. Esta assimetria de custos motiva o funnel de várias etapas.


O funnel de várias etapas

Executar um cross-encoder ou LLM dispendioso sobre milhões de documentos não é viável, pelo que as stacks de pesquisa modernas utilizam um funnel. Cada etapa reduz o conjunto de candidatos, enquanto aumenta a complexidade do modelo.

Um retriever barato também não oferece precisão final, pelo que o funnel utiliza cada modelo apenas quando o respetivo custo é razoável.

Multi-Stage Ranking FunnelMulti-Stage Ranking Funnel

EtapaEscala de entradaObjetivo principalMétodos típicosMedição à saída
RetrievalCorpus ou índiceRecall de candidatosBM25, bi-encodersRecall no cutoff de candidatos
Pre-rankingConjunto grande de candidatosFiltragem barataModelos leves, regrasRecall retido por milissegundo
Full rankingShortlistQualidade do top rankingCross-encoders, LLMsNDCG/MRR, latência, custo
BlendingListas ou slots finais ordenadosRestrições e misturaRegras, ranking multiobjetivoPolítica, diversidade, guardrails de negócio

A retrieval define o teto e o reranking otimiza dentro desse limite. Se um documento relevante não sobreviver à retrieval, nenhum modelo a jusante poderá recuperá-lo.


A demo: um pipeline de cinco etapas

Para tornar isto concreto, desenvolvi uma demo de search-ranking-stack que executa um pipeline de cinco etapas no benchmark de pesquisa de produtos Amazon ESCI. Cada etapa é medida de forma independente, para que seja possível perceber de onde vêm realmente os ganhos.

Demo Pipeline ArchitectureDemo Pipeline Architecture

O pipeline:

  1. Retrieval sparse com BM25 — baseline lexical (rank_bm25)
  2. Retrieval densa com bi-encoder — geração semântica de candidatos (all-MiniLM-L6-v2)
  3. Fusão híbrida com RRF — fusão baseada no ranking dos resultados sparse e densos
  4. Reranking com cross-encoder — pontuações de relevância pairwise (ms-marco-MiniLM-L-12-v2)
  5. Reranking listwise com LLM — comparação com prompt da shortlist final (Ollama, API ou modelo local)

Os passos 1—3 correspondem à etapa de retrieval do funnel (maximizar o recall); os passos 4—5 correspondem à etapa de full ranking (maximizar a precisão). A demo não inclui pre-ranking nem blending. Com cerca de 8 500 documentos, é possível enviar todos os resultados híbridos diretamente para o reranking.

Início rápido

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

Dataset e amostragem: Amazon ESCI

A demonstração utiliza o Amazon Shopping Queries Dataset (ESCI) da KDD Cup 2022 — um benchmark real de pesquisa de produtos com etiquetas de relevância graduada em quatro níveis:

EtiquetaGanhoSignificadoExemplo (consulta: “wireless headphones”)
Exact (E)3Satisfaz todos os requisitos da consultaSony WH-1000XM5 Wireless Headphones
Substitute (S)2Alternativa funcionalWired headphones with Bluetooth adapter
Complement (C)1Item relacionado e útilHeadphone carrying case
Irrelevant (I)0Não tem uma relação significativaUSB charging cable

A relevância graduada é importante porque permite utilizar NDCG (Normalized Discounted Cumulative Gain), que distingue uma ordenação «perfeita» de uma ordenação «apenas adequada». As métricas binárias atribuem a mesma pontuação a ambas.

Utilizei o small_version da demonstração: cerca de 500 consultas, 8 500 produtos e 12 000 avaliações. O downloader lê a única partição train de tasksource/esci, filtra a localidade inglesa us e small_version == 1 e, em seguida, utiliza a seed 42 para amostrar até 500 IDs de consulta únicos, sem reposição. O corpus contém os produtos únicos das linhas de avaliações selecionadas. Isto não é uma partição de validação nem um corpus independente. A amostra é suficientemente pequena para ser executada num portátil, mas é demasiado pequena e específica do domínio para fundamentar um sistema de ranking em produção. Utilize-a para reproduzir as várias etapas e analisar modos de falha; para tomar decisões de deployment, utilize consultas representativas mantidas de fora.


Retrieval: pesquisa híbrida

A função da camada de retrieval é maximizar o recall: colocar o maior número possível de documentos relevantes no conjunto de candidatos.

BM25: a baseline lexical

O BM25 atribui pontuações aos documentos com base na sobreposição de termos com a consulta, incorporando saturação da frequência dos termos e normalização do comprimento dos documentos:

BM25(q,d)=tqIDF(t)tf(t,d)(k1+1)tf(t,d)+k1(1b+bd/avgdl)\text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{tf(t,d) \cdot (k_1 + 1)}{tf(t,d) + k_1 \cdot (1 - b + b \cdot |d|/\text{avgdl})}

Onde IDF(t)\text{IDF}(t) é a frequência inversa nos documentos do termo tt, tf(t,d)tf(t,d) é a frequência do termo no documento dd, d|d| é o comprimento do documento e avgdl\text{avgdl} é o comprimento médio dos documentos em todo o corpus. Há dois parâmetros importantes: k1k_1 (normalmente 1.2—2.0) controla a saturação da TF — a rapidez com que as ocorrências repetidas deixam de acrescentar valor — e bb (normalmente 0.75) controla a normalização do comprimento dos documentos.

A implementação é curta. Tokenização simples por espaços em branco com 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

O BM25 atinge um Recall@100 de 0.741 — 74% dos produtos relevantes aparecem algures nos 100 primeiros resultados. Não é mau para um método exclusivamente lexical, mas 26% dos itens relevantes ficam invisíveis para todas as etapas seguintes.

Retrieval denso com bi-encoder

O bi-encoder mapeia consultas e documentos de forma independente para um espaço de embeddings partilhado:

# 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)

Com embeddings normalizados, a similaridade do cosseno reduz-se a um produto escalar. A demonstração calcula a matriz completa de consultas por corpus porque 8 500 documentos cabem confortavelmente em memória; num corpus de produção, seria normalmente utilizado um índice de approximate nearest neighbors. Neste exemplo, all-MiniLM-L6-v2 aumenta o Recall@100 de 0,741 para 0,825.

Como os bi-encoders aprendem boas representações

O treino de bi-encoders decorre normalmente em duas fases. Primeiro, o modelo é pré-treinado em conjuntos de dados de Natural Language Inference (NLI) e Semantic Textual Similarity (STS), que lhe ensinam uma compreensão semântica de uso geral. É nessa fase que o modelo aprende que “a cat sits on a mat” e “a feline rests on a rug” devem ter embeddings semelhantes. Depois, é feito fine-tuning com dados específicos de retrieval, como o MS MARCO, onde o modelo aprende que uma consulta e a passagem relevante devem ficar mais próximas entre si do que a consulta e as passagens irrelevantes.

A segunda fase depende de hard negative mining. Negativos aleatórios, como um documento sobre culinária associado a uma consulta sobre auscultadores, são trivialmente fáceis de distinguir, pelo que o modelo aprende pouco com eles. Em vez disso, utiliza-se o próprio modelo atual para encontrar documentos aos quais atribui uma classificação elevada, mas que, na realidade, não são relevantes.

A abordagem SimANS (Simple Ambiguous Negatives Sampling) formaliza este processo. Classifique todos os documentos com o bi-encoder atual e, em seguida, exclua os negativos fáceis, que obtêm uma classificação demasiado baixa para proporcionarem aprendizagem, e os potenciais falsos negativos, que obtêm uma classificação tão elevada que podem ser relevantes, mas não estão anotados como tal. O que resta no intervalo intermédio contém o sinal de treino mais importante.

# 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.

A função de perda contrastiva (InfoNCE) reúne estes elementos. Para cada consulta qq com um documento positivo d+d^+ e um conjunto de documentos negativos {d1,,dn}\{d^-_1, \ldots, d^-_n\}:

L=logesim(q,d+)/τesim(q,d+)/τ+i=1nesim(q,di)/τ\mathcal{L} = -\log \frac{e^{\text{sim}(q, d^+) / \tau}}{e^{\text{sim}(q, d^+) / \tau} + \sum_{i=1}^{n} e^{\text{sim}(q, d^-_i) / \tau}}

Em que sim(q,d)\text{sim}(q, d) é a similaridade do cosseno entre os embeddings da consulta e do documento, e τ\tau é o parâmetro de temperatura (normalmente 0.05—0.1). Valores mais baixos tornam a perda mais sensível a hard negatives. É essencialmente uma softmax cross-entropy: aumentar a similaridade do par positivo relativamente à de todos os negativos. Quando τ\tau é pequeno, mesmo pequenas diferenças na similaridade produzem gradientes elevados, o que força o modelo a estabelecer distinções mais granulares.

Pipeline de treino de um bi-encoderPipeline de treino de um bi-encoder

Disponibilizar embeddings de bi-encoders à escala

A vantagem arquitetural de um bi-encoder é a separação offline/online. Os embeddings dos documentos são calculados no momento da indexação e armazenados num índice vetorial. No momento da consulta, o sistema codifica a consulta e pesquisa esses vetores armazenados. A latência depende do encoder, do hardware, do índice, dos filtros e do objetivo de recall; por isso, avalie os dois passos separadamente.

Na demonstração, os cálculos são modestos: 8 500 documentos ×\times 384 dimensões ×\times 4 bytes por float = aproximadamente 13 MB de embeddings. À escala de produção, os valores deixam de ser modestos: 1 bilião de documentos com embeddings de 768 dimensões requerem aproximadamente 3 TiB de armazenamento. É aqui que entram a quantização (comprimir floats de 32 bits para inteiros de 8 bits), a product quantization (decompor vetores em subespaços) e índices suportados por SSD, como o DiskANN. A secção sobre indexação de vetores densos aborda os algoritmos de indexação.

Pipeline de serving de Bi-EncoderPipeline de serving de Bi-Encoder

Porquê testar a recuperação híbrida

Os dois métodos falham frequentemente de formas diferentes. O BM25 é adequado para nomes próprios, SKUs de produtos e códigos de erro. A recuperação densa consegue resolver incompatibilidades de vocabulário, como «computador portátil barato» versus «computador portátil económico». A utilidade da fusão depende da frequência com que estes casos complementares ocorrem no conjunto de queries alvo.

Uma experiência seguinte comum é a pesquisa híbrida: executar ambos os métodos de recuperação e, em seguida, fundir as listas ordenadas.

Reciprocal rank fusion (RRF)

O BM25 e a recuperação densa produzem scores com significados e escalas diferentes. Por isso, uma combinação linear requer calibração e validação sempre que os retrievers ou o corpus mudam.

Pesquisa híbrida com RRFPesquisa híbrida com RRF

A Reciprocal Rank Fusion (Cormack et al., 2009) ignora completamente os scores brutos e usa apenas a posição no ranking:

RRF(d)=rRankings1k+rank(d,r)\text{RRF}(d) = \sum_{r \in \text{Rankings}} \frac{1}{k + \text{rank}(d, r)}

Aqui, kk é uma constante de suavização; 60 é um valor inicial comum. A RRF atribui maior peso aos itens que aparecem perto do topo em várias listas de entrada, sem comparar os respetivos scores brutos. Evita a calibração da escala dos scores, mas os limites de recuperação, os pesos e kk continuam a exigir avaliação.

A implementação:

# 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

A RRF híbrida atinge Recall@100 de 0.842 e NDCG@10 de 0.628, superando tanto o BM25 (0.585) como o Dense (0.611) isoladamente. Para sobreviverem à fusão, os documentos só precisam de obter uma boa posição num dos métodos.


Reordenação com cross-encoder

Com 100 candidatos híbridos por query, é possível usar um modelo mais dispendioso. O cross-encoder processa a query e o documento em conjunto através de um único Transformer, com cross-attention completa entre todos os tokens.

Bi-Encoder versus Cross-EncoderBi-Encoder versus Cross-Encoder

Interação ao nível dos tokens

A diferença está na matriz de atenção. Num bi-encoder, a atenção é diagonal por blocos: os tokens da query atendem apenas a outros tokens da query, e os tokens do documento atendem apenas a outros tokens do documento. As duas representações nunca se encontram ao nível dos tokens — só intersetam no fim através de um produto escalar. Um cross-encoder calcula a matriz de atenção completa, em que cada token da query atende a todos os tokens do documento e vice-versa. É essa cross-attention que torna possível uma interação profunda ao nível dos tokens.

Arquitetura de atenção do Cross-EncoderArquitetura de atenção do Cross-Encoder

Num bi-encoder, a query «apple» é codificada antes de qualquer documento ser analisado. Um cross-encoder vê a query e o candidato em conjunto, pelo que pode usar a relação entre os respetivos tokens. Isto pode ajudar em casos como:

  • Negação: «auscultadores que não são sem fios». Um embedding com pooling pode atribuir pouco peso à negação, enquanto a codificação conjunta proporciona ao modelo uma interação direta entre a query e o documento. Esta é uma hipótese a validar com um slice específico, não uma garantia.
  • Restrição: «computador portátil abaixo de $500». A codificação conjunta pode relacionar a restrição com um preço no texto do produto, embora os filtros estruturados por preço sejam mais seguros quando o campo está disponível.

A entrada do cross-encoder é formatada como [CLS] query tokens [SEP] document tokens [SEP]. [CLS] é um token de classificação cujo estado oculto final é passado por uma cabeça linear para produzir uma única pontuação de relevância. Os embeddings de segmento distinguem os tokens da consulta dos tokens do documento, e [SEP] assinala a fronteira entre segmentos.

Como são treinados os cross-encoders

Os cross-encoders podem aprender a partir de exemplos (query, document, relevance_label), usando objetivos pointwise, pairwise ou listwise. O exemplo pointwise abaixo utiliza uma única etiqueta de relevância; não é a única abordagem de treino.

# 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

Um classificador comum mapeia a representação final de [CLS] para uma pontuação. As etiquetas binárias podem usar binary cross-entropy; a relevância graduada pode usar perdas de regressão, ordinais, pairwise ou listwise. Escolha com base em métricas de ranking num conjunto de validação separado, em vez de assumir que um objetivo é universalmente melhor.

A mineração de hard negatives é ainda mais importante para cross-encoders do que para bi-encoders A mineração de hard negatives é ainda mais importante para cross-encoders. Os cross-encoders são dispendiosos de treinar — cada exemplo de treino requer uma passagem forward completa pela sequência concatenada —, pelo que não pode desperdiçar computação em negativos trivialmente fáceis. A receita prática: use um bi-encoder para obter os top-K candidatos de cada consulta de treino e, em seguida, extraia hard negatives de intervalos de ranking específicos (por exemplo, ranks 10–100). Assim, o cross-encoder recebe exemplos em que distinguir itens relevantes de irrelevantes exige, de facto, uma interação profunda entre tokens.

# 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

Na execução de demonstração registada, ms-marco-MiniLM-L-12-v2 faz o reranking de 50 candidatos por consulta e aumenta o NDCG@10 de 0.628 para 0.645. Meça a latência no hardware de deployment; o modelo, o comprimento da sequência, o tamanho do batch e o runtime influenciam o resultado.

O compromisso entre velocidade e qualidade

Porque não utilizar cross-encoders para tudo? Porque não é possível pré-calcular as pontuações. Os embeddings dos documentos de um bi-encoder são independentes da consulta, pelo que são calculados uma vez e armazenados. A saída de um cross-encoder depende simultaneamente da consulta e do documento. A pontuação de relevância de “wireless headphones” associada a um produto Sony resulta da cross-attention completa entre esses tokens específicos. Não é possível colocá-la em cache nem reutilizá-la para uma consulta diferente.

Um bi-encoder requer uma codificação da consulta e uma pesquisa vetorial sobre embeddings de documentos pré-calculados. Um cross-encoder atribui uma pontuação a cada par consulta-documento da shortlist, com um custo que aumenta tanto com o número de candidatos como com o comprimento da sequência. O batching ajuda, mas atribuir pontuações a 100 000 candidatos continua a ser o ponto de operação errado; faça primeiro a recuperação e avalie a maior shortlist que cumpra os objetivos de qualidade e latência.

Na demonstração, o Recall@100 mantém-se estável em 0.842 ao longo da etapa do cross-encoder. O reranking pode reordenar resultados, mas não pode adicionar documentos. A recuperação define o limite superior.


Reranking listwise com LLMs

A fase final da demonstração utiliza um LLM para reranking listwise. Em vez de atribuir uma pontuação a cada documento de forma independente, o modelo recebe os 10 primeiros e devolve uma ordenação. Inspirado no RankGPT, o prompt explicita a comparação relativa, mas também introduz limites de contexto, enviesamento posicional, falhas de parsing e variabilidade entre execuções.

Abordagens de reranking com LLMAbordagens de reranking com LLM

O prompt listwise

O template do prompt pede ao LLM que tenha em conta a hierarquia de relevância do 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."
    )

Três modos de execução

A demonstração suporta três backends para o reranking com LLM:

ModoModeloComo é executado
ollamallama3.2:3b (configurável)Localmente através da API do Ollama
apiclaude-haiku-4-5-20251001API da Anthropic
localQwen/Qwen2.5-1.5B-InstructHuggingFace Transformers

Parsing e fallback

Não é garantido que os outputs do LLM sigam o schema solicitado, pelo que o parsing e o fallback são importantes:

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]

Se o parsing falhar completamente, a demonstração recorre à ordenação do cross-encoder. Num parser de produção, também se devem rejeitar identificadores fora do intervalo e duplicados, acrescentar os candidatos omitidos pela ordem anterior, registar a falha e comparar a taxa de fallback com um limiar definido para o lançamento.


Resultados desta amostra ESCI

Estes resultados registados antes do LLM utilizam o small_version da demonstração: o locale inglês us após o filtro small_version == 1, com até 500 IDs de queries amostrados sem reposição, utilizando a seed 42, e um corpus construído a partir dos produtos avaliados para essas queries. Não correspondem a uma avaliação hold-out nem a um corpus independente. Os valores de MRR utilizam a regra binária do avaliador da demonstração, relevance > 0: um resultado Complement, Substitute ou Exact conta como relevante, enquanto um resultado Irrelevant não conta. recip_rank avalia cada lista de candidatos devolvida, que contém até 100 candidatos; não corresponde a MRR@10.

FaseNDCG@10MRRRecall@100Delta de NDCG
BM250.5850.8120.741
Dense Bi-Encoder0.6110.8080.825+0.026
Hybrid (RRF)0.6280.8340.842+0.017
+ Cross-Encoder0.6450.8600.842+0.017

Principais observações

A pesquisa híbrida supera cada método isoladamente. O NDCG do RRF (0.628) é superior ao do BM25 (0.585) e ao do Dense (0.611). Os dois métodos podem falhar em queries diferentes, e combiná-los permite recuperar documentos que qualquer um deles, por si só, não encontraria.

O recall é determinado na recuperação. O Recall@100 mantém-se em 0.842 ao longo da fase do cross-encoder. Os rerankers reordenam os documentos; não acrescentam documentos. Se pretender aumentar o recall, corrija a camada de recuperação. Os valores de MRR acima utilizam a mesma regra global da demonstração: Complement ou superior é relevante.

O resultado do LLM não é reportado. O parser aceita identificadores duplicados e fora do intervalo, e o caminho de padding pode alterar silenciosamente a lista de candidatos. Por isso, a comparação anterior com o LLM é ilustrativa, não um resultado auditável. Repita-a com validação estrita dos identificadores, comportamento de fallback registado, execuções repetidas, medições de latência e custo, e um conjunto de domínios reservado para teste, antes de atribuir qualquer ganho ao reranker.

O repositório complementar é útil para a configuração e o código, mas o README continua a reportar o NDCG@10 inválido de 0,717 em + LLM Reranker. Considere a tabela anterior ao LLM deste artigo e a ressalva acima sobre o LLM como o registo autoritativo, até que esse repositório tenha uma avaliação de LLM auditável.

A recuperação densa supera o BM25 nesta amostra. Inspecione os recortes das queries antes de atribuir a causa. A incompatibilidade de vocabulário é um fator plausível, mas a construção da amostra, o tokenizador, o domínio de treino do modelo e os campos do corpus também afetam a comparação.


Avaliação: medir o que importa

A demonstração usa três métricas, cada uma analisando o ranking de uma perspetiva diferente:

NDCG@10 (métrica principal)

Normalized Discounted Cumulative Gain mede a qualidade do ranking dos 10 primeiros resultados usando relevância graduada. Recompensa a colocação de documentos altamente relevantes no topo, com um desconto logarítmico:

DCG@k=i=1k2reli1log2(i+1)NDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{rel_i} - 1}{\log_2(i + 1)} \qquad \text{NDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Das três, o NDCG é a única métrica que utiliza integralmente a relevância graduada de quatro níveis do ESCI. Um sistema que coloca uma correspondência Exact na posição 1 obtém uma pontuação superior à de um sistema que coloca aí uma Substitute. É por isso que é a métrica principal aqui.

MRR (primeiro resultado Complement ou superior)

Mean Reciprocal Rank usa a posição do primeiro resultado considerado relevante. Nesta demonstração, o avaliador passa os qrels graduados do ESCI a pytrec_eval e usa recip_rank sobre cada lista devolvida com até 100 candidatos, pelo que relevance > 0 é o limiar binário: Complement (1), Substitute (2) e Exact (3) contam como relevantes. Irrelevant (0) não conta. Um resultado relevante na posição 1 dá um reciprocal rank de 1,0; na posição 3, dá 0,333. Este é o MRR sobre as listas de candidatos devolvidas, não o MRR@10.

Recall@100 (cobertura da recuperação)

O recall mede que fração dos documentos relevantes avaliados aparece nos 100 primeiros resultados. É uma métrica do teto de candidatos para os julgamentos avaliados: um reranker não pode adicionar um documento que a recuperação omitiu, enquanto julgamentos incompletos podem tornar incerto o teto aparente.


Indexação de vetores densos para além da demonstração

Os embeddings densos só se tornam úteis à escala quando existe um índice Approximate Nearest Neighbor (ANN). A demonstração usa similaridade de cosseno por força bruta (o que é adequado para cerca de 8500 documentos), mas os sistemas de produção precisam de índices especializados.

HNSW (hierarchical navigable small world)

HNSW constrói um grafo com várias camadas: as camadas superiores, esparsas, permitem uma navegação ampla, enquanto as camadas inferiores, mais densas, refinam a vizinhança. M controla a conectividade do grafo, enquanto efSearch estabelece um compromisso entre o trabalho de consulta e o recall. Os valores úteis dependem da dimensão, da distribuição das distâncias, dos filtros, da implementação e do recall pretendido.

As atualizações e eliminações são uma consideração operacional, porque os índices de grafos podem necessitar de reparação em segundo plano. O comportamento varia consoante a base de dados. Um problema do Qdrant, por exemplo, relata uma degradação da qualidade da pesquisa filtrada numa configuração HNSW multi-tenant. O relatório mediu uma precisão média@100 de 0.597 ± 0.0541 com um filtro library_id, enquanto a pesquisa exata atingiu 100% de recall na sua análise mais aprofundada. Alterar payload_m forçou uma reconstrução que restaurou a qualidade da pesquisa filtrada. O relatório aborda filtros por tenant, a configuração HNSW e uma reconstrução forçada do índice HNSW, não uma carga de trabalho com muitas eliminações. Reproduza o padrão de churn pretendido e inclua o comportamento de compactação ou reconstrução na avaliação.

IVF (ficheiro invertido)

Os índices IVF particionam o espaço vetorial em clusters e, em seguida, pesquisam os nprobe clusters mais próximos da consulta. Podem proporcionar um compromisso útil entre memória, tempo de construção e recall, sobretudo quando combinados com compressão. A semântica e o desempenho das atualizações dependem da implementação, não apenas da família do índice.

Para escalas extremas, IVF-RaBitQ (Gao & Long, SIGMOD 2024) comprime vetores de vírgula flutuante em representações de um único bit. Em espaços de alta dimensionalidade, o sinal (+/-) de uma coordenada contém informação angular suficiente para calcular a similaridade.

DimensãoGrafo HNSWClusters IVF
Controlo da consultaefSearchnprobe
Controlo da construçãoConectividade e beam de construçãoNúmero de clusters e amostra de treino
Perfil de memóriaArestas do grafo e vetoresCentróides, listas e vetores armazenados
Comportamento das atualizaçõesReparação/limpeza específica da base de dadosManutenção de listas específica da base de dados
Avaliar comCurva recall-latência-memória-churnCurva recall-latência-memória-churn

Num estudo de caso da Uber sobre pesquisa de entregas, reduzir um parâmetro de pesquisa ao nível do shard de 1.200 para 200 diminuiu a latência reportada em 34% e o consumo de CPU em 17%, com pouca perda de recall medida. A lição reutilizável é ajustar a curva recall-custo com tráfego semelhante ao de produção, e não copiar o valor 200.


Extensões opcionais após o pipeline principal

Depois de obter medições separadas para a recuperação e o reranking, torna-se mais fácil avaliar várias extensões sem obscurecer o pipeline principal.

Compreensão da consulta

A expansão e a reescrita de consultas podem resolver incompatibilidades de vocabulário antes da recuperação. O Query2doc gerou pseudo-documentos e reportou ganhos no BM25 nas suas experiências com o MS MARCO. A expansão também pode introduzir uma intenção incorreta; por isso, compare recall e precisão em subconjuntos de consultas ambíguas, de navegação e com identificadores exatos.

Padrões práticos: expansão de abreviaturas, enriquecimento de entidades, decomposição em subconsultas para raciocínio multi-hop e RAG-Fusion — gerar várias variantes da consulta e combinar os resultados através de RRF.

Rotulagem de relevância assistida por LLM

Os LLM podem gerar rascunhos de rótulos de relevância quando os julgamentos humanos são escassos. O TALEC e o trabalho da Pinterest sobre rotulagem de relevância apresentam dois desenhos avaliados. Um rótulo produzido por um LLM continua a ser output de um modelo: calibre-o com base em julgamentos humanos cegos, inspecione fatias de discordância e mantenha um conjunto gold humano para testes de regressão.

Controlos úteis incluem:

  • uma rubrica com limites concretos de relevância e exemplos;
  • calibração humana cega e reavaliações periódicas;
  • aleatorização da ordem e julgamentos repetidos para casos instáveis;
  • painéis de modelos quando o custo adicional melhora a concordância; e
  • verificações explícitas de enviesamento por posição e de tendência para a média.

Knowledge distillation

Quando um LLM teacher acrescenta valor, mas não consegue cumprir as restrições de serving, a distillation é uma opção:

  1. Use um LLM poderoso (o teacher) para fazer reranking de milhares de queries de treino
  2. Treine um cross-encoder pequeno e rápido (o student, com ~100M–200M parâmetros) para imitar a distribuição de ranking do LLM
  3. Compare o student com o teacher e com o baseline em qualidade, calibração e custo de serving

O InRanker destila o MonoT5-3B em modelos com 60M e 220M parâmetros — uma redução de 50x no tamanho, com desempenho competitivo. A abordagem Rank-Without-GPT produz rerankers listwise open source de 7B que atingem 97% da eficácia do GPT-4 através de fine-tuning com QLoRA.

Os resultados publicados de compressão são pontos de partida, não rácios esperados em produção. A distillation pode herdar os enviesamentos do teacher e perder qualidade em fatias raras de queries; por isso, mantenha os julgamentos de relevância originais no ciclo de avaliação.


Personalização e enviesamento por posição

A relevância genérica só permite avançar até certo ponto. Uma pesquisa por “apple” deve devolver iPhones a um entusiasta de tecnologia e receitas com maçã a alguém que tenha estado a consultar conteúdos de culinária.

Uma arquitetura de retrieval comum para personalização utiliza um modelo de embeddings de duas torres: a torre da query codifica a query e o contexto do utilizador, enquanto a torre dos itens codifica os itens e os metadados. A separação offline/online suporta retrieval approximate-nearest-neighbor; a latência continua a depender do encoder, do índice, dos filtros e do sistema de serving.

Os embeddings de anúncios da Airbnb, o OmniSearchSage da Pinterest e os sistemas de duas torres da Uber mostram diferentes desenhos de produção. A escala e os ganhos reportados pertencem a esses sistemas; o padrão transferível é a torre de itens offline combinada com uma torre de query/utilizador online.

Os dados de cliques transportam enviesamentos de posição e de exposição. O PAL é uma abordagem de debiasing: usar a posição durante o treino e mantê-la constante durante o serving. Não é uma solução universal; intervenções aleatorizadas, métodos de inverse propensity e avaliação contrafactual podem ser mais adequados para outro produto.


Adaptação ao domínio com queries sintéticas

Um erro comum ao definir uma estratégia de pesquisa é assumir que um modelo treinado com dados gerais da Web (como o MS MARCO) funcionará bem num domínio especializado. Este é o problema out-of-domain (OOD).

Os LLM podem reduzir, mas não eliminar, o bottleneck de dados anotados através de Generative Pseudo-Labeling (GPL, InPars):

  1. Pegue no seu corpus documental específico do domínio
  2. Peça a um LLM para «Gerar uma consulta de pesquisa à qual este documento daria resposta»
  3. Utilize os pares sintéticos (consulta, documento) para fazer fine-tuning do seu retriever e reranker

Os pares sintéticos podem ser úteis quando há poucas consultas reais, mas refletem o gerador e o prompt. Remova duplicados, filtre consultas implausíveis e valide-as com tráfego real separado para avaliação.

Uma sequência de experiências

Adicione complexidade apenas quando a fase anterior revelar uma falha mensurável:

Percurso prático de maturidadePercurso prático de maturidade

Passo 1 (baseline): implemente o BM25 ou o sistema lexical atual e crie um conjunto de consultas avaliadas. Registe recall, NDCG, latência e segmentos de falhas.

Passo 2 (recall de candidatos): teste a recuperação densa e a fusão apenas se o baseline não recuperar documentos relevantes. Ajuste o limite de candidatos tendo em conta o recall e o custo.

Passo 3 (precisão da ordenação): adicione um cross-encoder se os candidatos corretos existirem, mas aparecerem na ordem errada. Escolha o tamanho da shortlist com base numa curva de qualidade-latência.

Passo 4 (adequação ao domínio): faça fine-tuning ou distillation apenas depois de os modelos genéricos revelarem falhas estáveis e específicas do domínio. Mantenha as avaliações reais separadas dos dados de treino sintéticos.

Passo 5 (camada dispendiosa opcional): teste reranking listwise ou baseado em reasoning apenas quando o ganho incremental de qualidade se mantiver em execuções repetidas e justificar a complexidade adicional em termos de latência, custo, privacidade e fallback.


Linhas de investigação a avaliar separadamente

Os rerankers baseados em reasoning e os agentes que utilizam pesquisa são promissores, mas respondem a perguntas diferentes das da demonstração de pesquisa de produtos em cinco fases.

Rerankers baseados em reasoning

Rank1 treina rerankers com traces de reasoning e apresenta resultados fortes no benchmark BRIGHT. Esta evidência é relevante para recuperação intensiva em reasoning, não constituindo uma previsão direta para a pesquisa de produtos ESCI.

Na pesquisa jurídica ou científica, compare rerankers baseados em reasoning com baselines fortes de cross-encoders e listwise, recorrendo a avaliações de especialistas, citações, latência e consistência das falhas.

Pesquisa agentic

O Search-o1 estuda um modelo que faz pesquisas adicionais durante respostas a perguntas multi-hop. Este é um problema de orquestração — geração de consultas, decisão de paragem, utilização de evidência e avaliação da resposta — e não mais uma fase de reranking. Avalie-o pela correção da tarefa final e pelo suporte das citações, não apenas pelas métricas de recuperação.


Principais conclusões

  1. Trate a stack como uma sequência de experiências. Estabeleça um baseline lexical e um conjunto de consultas avaliadas antes de adicionar recuperação densa, fusão ou reranking.

  2. Meça separadamente o recall de candidatos e a precisão da ordenação. Nesta amostra ESCI, o Recall@100 atinge 0,842 após a fusão e mantém-se estável nas duas fases de reranking.

  3. Utilize recuperação híbrida quando os erros forem complementares. A RRF melhorou tanto o Recall@100 como o NDCG@10 na demonstração, mas outro corpus pode não justificar dois índices.

  4. Adicione um cross-encoder quando a shortlist estiver correta, mas a ordem estiver errada. Escolha o número de candidatos com base numa curva de qualidade-latência medida.

  5. Trate o reranking com LLM como uma experiência final opcional. A comparação com LLM da demonstração atual é meramente ilustrativa, porque o respetivo parser não valida uma permutação completa dos IDs dos candidatos. O parsing estrito, os fallbacks registados, as execuções repetidas e o custo devem fazer parte da avaliação antes de reportar qualquer ganho.

  6. Mantenha os tipos de evidência separados. Um resultado de um artigo científico, um case study de um fornecedor, esta demonstração num portátil e um teste A/B em produção respondem a perguntas diferentes.

O código completo do pipeline está no repositório complementar. Faça clone para reproduzir as fases anteriores ao LLM ou testar modelos e parâmetros diferentes; não trate a linha desatualizada do LLM como um resultado.

Referências

Artigos

Conjuntos de dados e benchmarks

Modelos utilizados na demonstração

Ferramentas e plataformas

  • rank_bm25 — Implementação de BM25 em Python
  • Pyserini BEIR Reproductions — comandos de indexação e avaliação de bm25-multifield
  • pytrec_eval — Toolkit de avaliação TREC
  • Elasticsearch — Pesquisa híbrida com a API Retrievers
  • Vespa — Motor unificado de pesquisa e recomendação
  • Weaviate — Base de dados vetorial com pesquisa híbrida
  • Qdrant — Base de dados vetorial com consultas multi-stage

Referências da indústria

Projeto de demonstração