Busca semântica híbrida combinando texto e vetores
1. Fundamentos da Busca Semântica e Híbrida
1.1. Diferença entre busca lexical (TF-IDF, BM25) e busca semântica (embeddings)
A busca lexical tradicional, representada por algoritmos como TF-IDF e BM25, opera estritamente por correspondência de termos. Quando um usuário pesquisa "carro elétrico", o sistema busca documentos que contenham exatamente essas palavras, ignorando sinônimos ou conceitos relacionados como "veículo sustentável". A principal limitação é a ausência de compreensão semântica — documentos sobre "veículos movidos a bateria" podem ser ignorados mesmo sendo altamente relevantes.
A busca semântica, por outro lado, utiliza embeddings — representações vetoriais densas geradas por modelos como BERT ou Sentence Transformers. Cada documento e consulta é convertido em um vetor numérico de alta dimensionalidade (tipicamente 384 a 768 dimensões), onde a similaridade é calculada por métricas como cosseno ou produto escalar. Isso permite capturar relações semânticas: "carro elétrico" e "veículo movido a bateria" terão vetores próximos no espaço.
1.2. O conceito de busca híbrida: unindo precisão lexical e compreensão contextual
A busca híbrida combina ambas abordagens para superar as fraquezas individuais. Enquanto a busca lexical é excelente para correspondência exata de termos técnicos (como "Python 3.12" ou "artigo 157"), a busca semântica captura intenções e paráfrases. Um sistema híbrido típico executa ambas as consultas em paralelo e combina os resultados usando algoritmos de fusão.
1.3. Casos de uso típicos: documentos longos, FAQs, e sistemas de recomendação
- Documentos longos: Manuais técnicos onde termos específicos (nomes de componentes) precisam ser encontrados exatamente, mas o contexto geral também importa.
- FAQs: Perguntas frequentes onde a redação exata varia ("Como resetar senha?" vs "Esqueci minha senha").
- Sistemas de recomendação: Produtos onde descrições textuais e características técnicas precisam ser combinadas.
2. Representação de Dados: Texto e Vetores
2.1. Geração de embeddings com modelos modernos
Sentence Transformers oferece modelos otimizados para similaridade semântica. Exemplo de geração:
from sentence_transformers import SentenceTransformer
modelo = SentenceTransformer('all-MiniLM-L6-v2')
documentos = ["Carro elétrico é sustentável", "Veículo movido a bateria"]
embeddings = modelo.encode(documentos)
print(embeddings.shape) # (2, 384)
2.2. Indexação de texto puro para busca lexical
Bibliotecas como Whoosh ou Elasticsearch criam índices invertidos mapeando termos para documentos:
from whoosh.index import create_in
from whoosh.fields import Schema, TEXT
schema = Schema(titulo=TEXT(stored=True), conteudo=TEXT)
ix = create_in("indexdir", schema)
writer = ix.writer()
writer.add_document(titulo="Doc1", conteudo="Carro elétrico sustentável")
writer.commit()
2.3. Armazenamento combinado: bancos vetoriais + índices tradicionais
Sistemas modernos como Weaviate ou Qdrant armazenam ambos os tipos de índice. Exemplo conceitual:
# Estrutura combinada
documento = {
"texto": "Carro elétrico sustentável",
"embedding": [0.12, 0.45, ..., 0.89], # vetor 384 dim
"termos_invertidos": ["carro", "elétrico", "sustentável"]
}
3. Arquitetura do Sistema Híbrido
3.1. Pipeline de ingestão: tokenização, embedding e indexação paralela
Pipeline de ingestão:
1. Documento bruto → Tokenização (lexical)
2. Documento bruto → Modelo embedding → Vetor (semântico)
3. Token → Índice invertido (Whoosh/Elasticsearch)
4. Vetor → Índice vetorial (Pinecone/FAISS)
5. Metadados → Banco relacional (opcional)
3.2. Estratégias de consulta: busca paralela vs. busca sequencial
Na busca paralela, ambas as consultas executam simultaneamente:
Consulta: "Veículo movido a bateria"
→ Busca lexical (BM25): encontra docs com "bateria" e "veículo"
→ Busca semântica (cosseno): encontra docs semanticamente próximos
→ Fusão dos resultados
3.3. Fusão de resultados: rank recíproco (RRF), ponderação linear e aprendizado de ranking
O algoritmo RRF é amplamente usado por sua simplicidade e eficácia:
RRF_score(doc) = Σ 1 / (k + rank_i(doc))
Onde:
- rank_i(doc) = posição do documento na lista i
- k = constante (tipicamente 60)
4. Algoritmos de Combinação e Re-ranking
4.1. Normalização de scores: min-max, softmax e z-score
# Normalização min-max
score_normalizado = (score - min_score) / (max_score - min_score)
# Softmax para probabilidades
prob = exp(score_i) / Σ exp(score_j)
4.2. Re-ranking com modelos cross-encoder para precisão final
Após a fusão inicial, um cross-encoder (como cross-encoder/ms-marco-MiniLM-L-6-v2) avalia pares consulta-documento para reordenar:
from sentence_transformers import CrossEncoder
modelo_rerank = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
pares = [(consulta, doc.texto) for doc in top_20]
scores_rerank = modelo_rerank.predict(pares)
4.3. Ajuste dinâmico de pesos conforme o contexto da consulta
Consultas curtas (1-2 palavras) favorecem busca lexical; consultas longas (frases completas) favorecem semântica:
peso_lexical = max(0.3, 1.0 - len(consulta.split()) / 20)
peso_semantico = 1.0 - peso_lexical
5. Implementação Prática com Exemplos
5.1. Exemplo de código: indexação híbrida com BM25 + embeddings
import numpy as np
from whoosh.index import create_in
from whoosh.fields import Schema, TEXT
from whoosh.qparser import QueryParser
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
# Configuração
schema = Schema(titulo=TEXT(stored=True), conteudo=TEXT(stored=True))
ix = create_in("indice_hibrido", schema)
modelo = SentenceTransformer('all-MiniLM-L6-v2')
# Documentos exemplo
documentos = [
{"titulo": "Doc1", "conteudo": "Carro elétrico é sustentável"},
{"titulo": "Doc2", "conteudo": "Veículo movido a bateria"},
{"titulo": "Doc3", "conteudo": "Motor a combustão polui"}
]
# Indexação
writer = ix.writer()
embeddings_docs = []
for doc in documentos:
writer.add_document(titulo=doc["titulo"], conteudo=doc["conteudo"])
emb = modelo.encode(doc["conteudo"])
embeddings_docs.append(emb)
writer.commit()
embeddings_docs = np.array(embeddings_docs)
5.2. Exemplo de consulta híbrida e fusão RRF
def busca_hibrida(consulta, k=3):
# Busca lexical (BM25)
with ix.searcher() as searcher:
parser = QueryParser("conteudo", ix.schema)
query = parser.parse(consulta)
resultados_lex = searcher.search(query, limit=k)
rank_lex = {hit["titulo"]: i+1 for i, hit in enumerate(resultados_lex)}
# Busca semântica
emb_consulta = modelo.encode(consulta)
similaridades = cosine_similarity([emb_consulta], embeddings_docs)[0]
indices_sem = np.argsort(-similaridades)[:k]
rank_sem = {documentos[i]["titulo"]: idx+1 for idx, i in enumerate(indices_sem)}
# Fusão RRF
todos_titulos = set(rank_lex.keys()) | set(rank_sem.keys())
scores_rrf = {}
for titulo in todos_titulos:
rrf = 0
if titulo in rank_lex:
rrf += 1 / (60 + rank_lex[titulo])
if titulo in rank_sem:
rrf += 1 / (60 + rank_sem[titulo])
scores_rrf[titulo] = rrf
return sorted(scores_rrf.items(), key=lambda x: -x[1])
# Teste
resultado = busca_hibrida("Veículo sustentável")
for titulo, score in resultado:
print(f"{titulo}: {score:.4f}")
5.3. Métricas de avaliação: precisão, recall e NDCG
from sklearn.metrics import ndcg_score
# Exemplo de avaliação
relevancias_reais = [3, 2, 1, 0, 0] # relevância de cada documento
scores_hibridos = [0.95, 0.80, 0.60, 0.30, 0.10]
ndcg = ndcg_score([relevancias_reais], [scores_hibridos])
print(f"NDCG: {ndcg:.4f}")
6. Desafios e Otimizações
6.1. Latência vs. qualidade: trade-offs em sistemas em tempo real
Consultas híbridas podem ser 2-3x mais lentas que buscas isoladas. Otimizações comuns:
- Limitar top-k da busca lexical antes da fusão
- Usar quantização de embeddings (redução de 32-bit para 8-bit)
- Cache de embeddings para consultas frequentes
6.2. Escalabilidade: sharding de índices e caching de embeddings
# Estratégia de sharding por hash do documento
shard_id = hash(doc_id) % NUM_SHARDS
# Cada shard mantém seu próprio índice invertido e vetorial
6.3. Tratamento de consultas ambíguas e vocabulário fora do domínio
Para consultas ambíguas ("Java" pode ser ilha, linguagem ou café):
- Expandir consulta com sinônimos do domínio
- Usar pesos contextuais baseados em histórico do usuário
7. Integração com Agentes de IA e Aplicações Multimodais
7.1. Uso em chatbots: busca híbrida para respostas baseadas em conhecimento interno
Chatbots empresariais combinam BM25 para encontrar documentação técnica exata e embeddings para entender perguntas reformuladas:
def responder_chat(pergunta):
docs_relevantes = busca_hibrida(pergunta, k=5)
contexto = "\n".join([doc.conteudo for doc in docs_relevantes])
# Enviar para LLM: "Com base em: {contexto}, responda: {pergunta}"
7.2. Conexão com ferramentas de RAG (Retrieval-Augmented Generation)
Sistemas RAG modernos (como LangChain) suportam retrievers híbridos nativamente:
from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain.embeddings import HuggingFaceEmbeddings
retriever_lex = BM25Retriever.from_texts(documentos)
retriever_sem = vectorstore.as_retriever()
retriever_hibrido = EnsembleRetriever(
retrievers=[retriever_lex, retriever_sem],
weights=[0.3, 0.7]
)
7.3. Extensão para busca multimodal (texto + imagem)
Modelos como CLIP permitem embeddings multimodais, onde texto e imagem compartilham o mesmo espaço vetorial:
from sentence_transformers import SentenceTransformer
from PIL import Image
modelo_multimodal = SentenceTransformer('clip-ViT-B-32')
emb_texto = modelo_multimodal.encode("Gato preto")
emb_imagem = modelo_multimodal.encode(Image.open("gato.jpg"))
similaridade = cosine_similarity([emb_texto], [emb_imagem])
Referências
- Sentence Transformers Documentation — Documentação oficial da biblioteca para geração de embeddings e modelos pré-treinados para similaridade semântica.
- Elasticsearch: Hybrid Search — Guia oficial sobre implementação de busca híbrida combinando BM25 e kNN no Elasticsearch.
- Pinecone: Hybrid Search Guide — Tutorial prático sobre arquitetura de busca híbrida com bancos vetoriais e índices esparsos.
- LangChain: Ensemble Retriever — Documentação sobre fusão de múltiplos retrievers para sistemas RAG híbridos.
- Weaviate: Hybrid Search — Documentação oficial sobre implementação de busca híbrida com fusão RRF no banco vetorial Weaviate.
- Cross-Encoder for Re-ranking — Tutorial sobre uso de cross-encoders para re-ranking de resultados de busca.
- FAISS: Efficient Similarity Search — Biblioteca de código aberto da Meta para indexação e busca eficiente de embeddings em larga escala.
- NDCG Evaluation Metric — Explicação detalhada da métrica NDCG para avaliação de sistemas de recuperação de informação.