O que eu estava prestes a fazer

Precisava de busca semântica sobre conteúdo estruturado. O plano era o padrão: exportar documentos, fragmentar, gerar embeddings, enviar para um banco de vetores, montar um job de sincronização para manter tudo atualizado.

Antes de conectar tudo isso, resolvi verificar se o Sanity tinha alguma coisa relevante nessa direção. Tinha.


O índice de embeddings

O Sanity tem um índice de embeddings próprio que roda direto no seu dataset. Você define quais tipos de documento e campos incluir, ele cuida do chunking e da geração de embeddings, e se mantém sincronizado automaticamente conforme o conteúdo muda.

A configuração é mínima. Algumas linhas no sanity.config.ts:

import { embeddingsIndexDashboard } from "@sanity/embeddings-index-ui"

export default defineConfig({
  // ...
  plugins: [
    embeddingsIndexDashboard(),
  ],
})

Depois você cria um índice pelo painel do Studio ou via CLI, especificando os tipos de documento e quais campos indexar. O índice fica ativo e consultável logo em seguida.


Como consultar

O endpoint de busca recebe uma query em linguagem natural e retorna IDs de documento ranqueados com pontuações de similaridade:

const hits = await client.request({
  url: `/vX/embeddings-index/search/${indexName}`,
  method: "POST",
  body: {
    query: "fluxo de onboarding para contas enterprise",
    maxResults: 5,
  },
})

Formato da resposta:

[
  { "id": "doc-abc123", "score": 0.87 },
  { "id": "doc-def456", "score": 0.81 }
]

Depois você busca o conteúdo completo dos documentos com uma query GROQ usando esses IDs. Duas chamadas, nenhum serviço externo no meio.


O que funciona bem

O índice se mantém atualizado sem nenhuma intervenção. Publique um documento no Sanity e ele fica disponível para busca em segundos.

Combinar os resultados semânticos com filtros GROQ é limpo na prática. Você pega os IDs ranqueados do endpoint semântico e os filtra com GROQ por tipo, status de publicação ou qualquer outro campo:

const docs = await client.fetch(
  `*[_id in $ids && _type == "article" && status == "published"]`,
  { ids: hits.map(h => h.id) }
)

A busca semântica filtra por significado; o GROQ cuida da política. Juntos, tratam a maioria dos cenários de recuperação sem precisar de um query planner separado.


O que observar

A qualidade do índice depende do que você indexa. Se você incluir portable text cru sem chunking em nível de prosa, os resultados para documentos longos podem ser inconsistentes. Obtive resultados melhores indexando campos específicos (título, resumo, parágrafos do corpo) separadamente, em vez de colocar o documento inteiro.

Queries curtas ou ambíguas também são menos confiáveis. Uma busca de duas palavras sem contexto de domínio tende a combinar por similaridade superficial, não por intenção. Uma etapa de expansão de query antes ajuda.


O que eu não esperava

Cada resultado traz o _id do documento. Documentos no Sanity também têm _rev, um hash de revisão que muda a cada edição.

Buscando os dois no momento da consulta, você tem um registro exato de qual versão de qual documento estava em escopo em qualquer busca. Útil para debugar recuperação, e mais útil ainda se você está passando os resultados para um LLM e quer auditar o que o modelo viu de verdade.

Mais sobre isso num post seguinte.