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:

  1. Busca semântica para obter os _id candidatos.
  2. Query GROQ: *[_id in $ids && _type == "article" && !(_id in path("drafts.**"))]
  3. 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.