Atualização (2026-03-05)
Este post continua correto na direção geral, mas os benchmarks mais recentes mudaram conclusões importantes:
- A recuperação semântica superou a lexical em relevância no domínio.
- Sem gate de confiança, a recuperação semântica apresentou alta taxa de falso positivo fora de domínio.
- Recomendação atual de produção: semântica primária + gate de confiança + fallback lexical.
Resumo público da evidência no benchmark atualizado:
- Conjunto rotulado: 24 queries entre domínio, paráfrase, estresse lexical e controles negativos fora de domínio.
- Qualidade no domínio: recuperação semântica superou lexical em Precision@k, Recall@k, MRR e nDCG.
- Lacuna de robustez: sem gate, a semântica apresentou alta taxa de falso positivo em controles negativos.
- Correção prática: política mista (limiar de score + acordo lexical como fallback) reduziu falsos positivos fora de domínio preservando a maior parte da qualidade semântica.
Precisava de um corpus para um experimento de recuperação com LLM. A pergunta: dado um texto de usuário, o sistema consegue recuperar os documentos certos e o modelo consegue responder com precisão a partir deles?
Já tinha conteúdo estruturado no Sanity (documentação de SaaS B2B, fronteiras de documento bem definidas, campos claros). A dúvida era se conseguia usar o próprio Sanity como backend de recuperação, ou se precisaria exportar tudo para um banco de vetores externo.
Decidi testar antes de assumir a resposta.
O índice de embeddings
O Sanity tem um índice de embeddings próprio. Você configura contra um dataset, define quais tipos de documento e campos indexar, e ele cuida do resto: chunking, embedding e exposição de um endpoint de busca semântica.
Sem banco de vetores separado. Sem job de sincronização pra manter. O índice se atualiza automaticamente.
Consultar parece assim:
const results = await client.request({
url: `/vX/embeddings-index/search/${indexName}`,
method: "POST",
body: { query: "planos de preço para clientes enterprise", maxResults: 5 },
})
O resultado são IDs de documento e pontuações de similaridade. Em seguida, você busca os documentos completos com GROQ filtrando pelos _id.
O padrão híbrido
A parte que funcionou melhor do que eu esperava: combinar a busca semântica com filtros GROQ.
A busca semântica encontra o significado certo. O GROQ filtra pelo escopo certo: tipos de documento, status de publicação, faixas de data.
Na prática:
- Busca semântica para obter os
_idcandidatos. - Query GROQ:
*[_id in $ids && _type == "article" && !(_id in path("drafts.**"))] - Documentos filtrados passam para o LLM como contexto.
A busca semântica lida com a ambiguidade. O GROQ lida com a política. Juntos, funcionam.
O detalhe que importa
Todo documento no Sanity carrega _id e _rev.
O _rev é o hash da revisão. Registra exatamente qual versão do documento o modelo viu no momento da recuperação. Se o documento foi editado entre a recuperação e o usuário ler a resposta, a diferença é detectável.
Pra uma camada de auditoria, isso é genuinamente útil. Você consegue registrar quais IDs e revisões estavam no contexto no momento da inferência e rastrear qualquer resposta até o estado exato do corpus naquele instante.
A maioria dos bancos de vetores não entrega isso de graça. Você precisa construir por cima.
Onde ainda falha
Para queries curtas ou ambíguas, a busca semântica trouxe documentos relacionados de forma frouxa (você entende por que o embedding combinou, mas o conteúdo não era útil de verdade).
A sobreposição lexical entre a resposta e o conteúdo recuperado também se mostrou um sinal fraco para detectar alucinações. Mas isso é um problema para outro post.
Se você já tem conteúdo no Sanity e precisa de uma camada de recuperação pra algum experimento com LLM, vale testar o índice de embeddings vale testar antes de migrar para um banco de vetores dedicado. O padrão híbrido com GROQ é prático. E a proveniência via _id/_rev é a parte mais subestimada de toda a stack.