O que é Busca Híbrida?
Busca Híbrida (Hybrid Search) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Combinar busca por palavra-chave e busca vetorial para não perder nem sinônimos nem códigos exatos.
Hybrid Search
Busca Híbrida (Hybrid Search) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Combinar busca por palavra-chave e busca vetorial para não perder nem sinônimos nem códigos exatos.
Combinar busca por palavra-chave e busca vetorial numa única consulta, para não perder nem sinônimos nem termos exatos. Serve para sistemas de busca e RAG em que os usuários misturam linguagem natural com códigos, siglas e nomes próprios.
Dois bibliotecários trabalhando juntos: um conhece o catálogo de cor, o outro entende o que você quis dizer.
Pedro cuida da busca de um e-commerce de peças automotivas. A busca vetorial funcionou bem para “pastilha de freio para carro popular”: achou pastilhas compatíveis com vários modelos. Mas falhou feio em “pastilha 4D0698151”: o vetor de um código de peça não carrega significado, e a busca devolveu pastilhas de outras marcas, nenhuma com aquele código.
A busca antiga, por palavra-chave, acertava o código e errava a linguagem natural. Cada método falha onde o outro acerta, e os clientes escrevem dos dois jeitos, muitas vezes na mesma frase: “pastilha dianteira 4D0698151 para A4”.
O mesmo padrão aparece fora de e-commerce. No chat interno, “VR no home” precisa da busca lexical para as siglas e da vetorial para a paráfrase de trabalho remoto. Numa NF-e, a chave de 44 dígitos só é encontrada por correspondência exata. Um embedding aproxima sentido; ele não memoriza identificadores.
A pergunta que este padrão responde: como uma busca acerta tanto o sentido da frase quanto o termo exato?
Rode as duas buscas sobre o mesmo conjunto de documentos e combine os resultados:
A combinação mais comum é a fusão por posição (Reciprocal Rank Fusion, Cormack et al., 2009): cada documento ganha pontos pela posição em cada lista, e a soma decide a ordem final. Ela não exige calibrar escalas de pontuação diferentes. Filtros exatos (marca, categoria, estoque) se aplicam às duas buscas.
flowchart LR
Q(["1. Consulta"]):::io --> L["2a. Busca lexical"]:::tool
Q --> V["2b. Busca vetorial"]:::tool
I[("Índice com texto e vetores")]:::store --> L
I --> V
L --> F{"3. Fusão por posição"}
V --> F
F --> R["4. Top N combinado"]:::tool
R --> OUT(["5. Resultados ou contexto para o modelo"]):::io
Consulta: “pastilha dianteira 4D0698151 para A4”. Depois de normalizar o código (4d0698151), as duas listas de 50 vêm assim no topo:
lexical vector
1. sku-4D0698151-a (código) 1. sku-outras-pastilhas-a4 (sentido: pastilha + A4)
2. sku-4D0698151-b (código) 2. sku-pastilha-dianteira-gol
3. manual-instalacao-4D06 3. sku-4D0698151-a
RRF com constante 60. O documento na posição 0 ganha 1/60, na posição 2 ganha 1/62. sku-4D0698151-a aparece nas duas listas, soma os dois termos e sobe para o primeiro lugar combinado. As pastilhas “parecidas” do vetor, sem o código, ficam mais abaixo.
A frase mista deixa de ser um empate entre dois sistemas ruins. Cada um contribui com o que sabe, e a fusão promove quem apareceu nos dois.
function hybrid_search(query, filters, k=10):
parallel { # 2
lexical = index.bm25(query, filters, top=50)
vector = index.knn(embed(query), filters, top=50)
}
# 3. Reciprocal Rank Fusion
scores = {}
for rank, doc in enumerate(lexical): scores[doc] += 1 / (60 + rank)
for rank, doc in enumerate(vector): scores[doc] += 1 / (60 + rank)
ranked = sort(scores, desc)
trace.span("hybrid", lexical_top=lexical[:3], vector_top=vector[:3])
return ranked[:k] # 4
Híbrida é a base do RAG quando o vocabulário mistura os dois mundos. Ela não resolve ordenação fina: se o certo está na posição 20 da lista combinada, o próximo passo é Reordenação. Se a consulta chega vaga, o passo anterior é Reescrita de Consulta.
Normalize identificadores antes de indexar e antes de buscar. 4D0-698-151, 4D0698151 e 4d0 698 151 precisam virar o mesmo token. Sem isso, a lexical erra o caso que ela deveria acertar.
category = pastilha e a vetorial não. O resultado combinado vaza produto de outra categoria. O filtro entra nas duas, ou na fusão depois, nunca só numa.| Aspecto | O que acontece | Como aparece no trace |
|---|---|---|
| Chamadas | Duas buscas e um embedding da consulta; nenhuma chamada de modelo de linguagem | span hybrid com as duas listas |
| Latência | A da busca mais lenta, se em paralelo | duração do span |
| Custo | Índice duplicado (texto e vetores) | armazenamento do índice |
| Falha: uma lista domina | Pesos ou escalas mal ajustados | resultados finais iguais a uma das listas |
| Falha: filtros diferentes | Uma busca filtra e a outra não | documentos fora do filtro no resultado |
| Falha: termos com grafia variada | A lexical erra variações (“4D0 698 151”) | consultas com código sem acerto; normalize antes |
{ "query": "pastilha dianteira 4D0698151 para A4", "relevant": ["sku-4D0698151-a", "sku-4D0698151-b"], "type": "mixed" }
Se a híbrida não ganhar da lexical em type=code nem da vetorial em type=natural, a fusão está mal feita (ids, filtros ou normalização), não “o domínio que não combina”.
O span com as duas listas é o que deixa você ver, numa pergunta ruim, se quem falhou foi a lexical, a vetorial ou a fusão.
Filtros de permissão entram nas duas buscas, antes da fusão. Filtrar depois, na lista combinada, é tarde: o ranking já misturou documentos que o usuário não pode ver, e um bug na pós-filtragem vaza trecho.
Conteúdo indexado continua sendo dado não confiável na hora do RAG. Híbrida só muda quem entra na lista, não o fato de que o trecho pode carregar instrução. Veja Prompt injection na prática.
| Prós | Contras |
|---|---|
| Acerta sentido e termos exatos | Dois índices para manter |
| Fusão por posição dispensa calibração | Mais uma etapa para avaliar |
| Funciona bem com vocabulário de domínio | Pesos podem precisar de ajuste por tipo de consulta |
| Base sólida para RAG | Não resolve ordenação fina sem reranking |
As duas abas fazem a busca lexical e a vetorial em paralelo e combinam com Reciprocal Rank Fusion.
Python. Use quando o índice cabe em memória ou você quer entender a fusão sem um motor de busca.
import numpy as np
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
embedder = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
def normalize(text: str) -> list[str]:
return text.lower().replace("-", "").split() # "4D0-698-151" e "4d0698151" ficam iguais
class HybridIndex:
def __init__(self, docs: list[dict]):
self.docs = docs
self.bm25 = BM25Okapi([normalize(d["text"]) for d in docs])
self.vectors = embedder.encode([d["text"] for d in docs], normalize_embeddings=True)
def search(self, query: str, k: int = 10, pool: int = 50, c: int = 60) -> list[dict]:
lexical = np.argsort(-self.bm25.get_scores(normalize(query)))[:pool] # 2a
q = embedder.encode(query, normalize_embeddings=True)
vector = np.argsort(-(self.vectors @ q))[:pool] # 2b
scores: dict[int, float] = {}
for ranking in (lexical, vector): # 3. Reciprocal Rank Fusion
for rank, i in enumerate(ranking):
scores[int(i)] = scores.get(int(i), 0.0) + 1.0 / (c + rank)
best = sorted(scores, key=scores.get, reverse=True)[:k]
return [self.docs[i] for i in best] # 4O que você ganhou: a fusão completa em poucas linhas, fácil de testar no eval. O que você perdeu: em milhões de documentos, use um motor que já tenha as duas buscas.
TypeScript. Use quando os documentos estão no Postgres, com busca de texto e pgvector.
import { Pool } from "pg";
const db = new Pool();
// 2 e 3 numa consulta: as duas buscas como CTEs e a fusão por posição em SQL
export async function hybridSearch(query: string, queryEmbedding: number[], category: string, k = 10) {
const { rows } = await db.query(
`WITH lexical AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(tsv, q) DESC) AS r
FROM products, plainto_tsquery('portuguese', $1) q
WHERE tsv @@ q AND category = $3
ORDER BY r LIMIT 50
),
vector AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $2::vector) AS r
FROM products WHERE category = $3
ORDER BY embedding <=> $2::vector LIMIT 50
)
SELECT p.id, p.title, p.sku,
COALESCE(1.0 / (60 + l.r), 0) + COALESCE(1.0 / (60 + v.r), 0) AS score
FROM products p
LEFT JOIN lexical l ON l.id = p.id
LEFT JOIN vector v ON v.id = p.id
WHERE l.id IS NOT NULL OR v.id IS NOT NULL
ORDER BY score DESC LIMIT $4`,
[query, JSON.stringify(queryEmbedding), category, k],
);
return rows; // 4
}O que você ganhou: um banco só, com filtros iguais nas duas buscas por construção. O que você perdeu: o ajuste fino de cada busca fica limitado ao que o Postgres oferece.
Busca Híbrida (Hybrid Search) é um padrão de projeto de IA para apps com LLM e agentes, do grupo Contexto e conhecimento. Combinar busca por palavra-chave e busca vetorial para não perder nem sinônimos nem códigos exatos.
Nos cenários reais Busca semântica em e-commerce, Chat com a base de conhecimento interna e Assistente jurídico.