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