O ponto de partida

Precisava de um corpus para um experimento de recuperação com LLM. A pergunta era simples: 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, com fronteiras de documento bem definidas e 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 que o índice de embeddings do Sanity faz

O Sanity tem um recurso de í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 para manter. O índice se mantém atualizado 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 uma query 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 específicos, status de publicação, faixas de data.

Na prática, o pipeline ficou assim:

  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 de fundamentação.

A busca semântica lida com a ambiguidade; o GROQ lida com a política. Juntos, funcionam bem.


O detalhe que eu não tinha considerado

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.

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


O que não funcionou como esperado

A busca semântica é precisa o suficiente para conteúdo bem estruturado. Para queries curtas ou ambíguas, às vezes 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 separado. Vai virar outro post.


Conclusão

Se você já tem conteúdo no Sanity e precisa de uma camada de recuperação para alguma funcionalidade com LLM, o índice de embeddings vale testar antes de ir direto para um banco de vetores dedicado. O padrão híbrido com GROQ é prático. A proveniência via _id/_rev é a parte mais subestimada de toda a stack.

Um CMS com busca semântica funcional, proveniência estruturada e uma camada de consulta composável é mais do que eu esperava quando comecei.