Como usar o Jev para ordenar os resultados da busca interna do seu produto?

diagrama mostrando o Jev ordenando resultados de busca interna de um produto
Resposta rápida

Busca interna boa é feita em duas etapas: o buscador recupera candidatos, o ranking decide a ordem. O Jev, o System One Model da TypeSafe AI, entra na segunda: você pergunta com uma primitiva Score (array ordenado de níveis, até 10) ou Choice (até 255 opções) e recebe uma decisão tipada com probabilities e confidence de 0 a 1. Normaliza com score / (len(criteria) - 1), combina as dimensões com pesos no seu código e ordena. Todas as perguntas do mesmo state vão numa requisição só, porque a cobrança é por token de input (US$ 0,042 por 1M, saída US$ 0,00).

Sabe aquela busca interna que devolve 40 resultados e o item certo aparece na posição 7?

O buscador não está quebrado, ele só está fazendo o trabalho dele: casar palavra-chave, filtrar, devolver candidatos

Quem decide ordem é outra etapa, e é justamente essa etapa que quase ninguém monta

O Jev é o primeiro modelo público da TypeSafe AI e foi apresentado como um "System One Model": em vez de devolver prosa, ele devolve decisão tipada com probabilidades calibradas

Traduzindo pro nosso problema: dá pra pedir uma nota de relevância item a item e receber um número utilizável, não um parágrafo que você vai ter que parsear na raça

Neste post a gente monta o fluxo inteiro: shortlist, pergunta, normalização, pesos e corte, com o ranking final montado no SEU código 🙂

O que você precisa antes de começar

A parte de ambiente é curta:

  • Python >= 3.10
  • SDK instalado com uv add typesafe-sdk ou pip install typesafe-sdk
  • a variável TYPESAFE_API_KEY no ambiente, que é de onde o cliente do SDK lê a chave

Se tu não quer SDK, a API é um POST direto em https://api.typesafe.ai/v1/systemone, com header Authorization Bearer e Content-Type: application/json

O cliente chama jev-latest por padrão

Esse alias aponta hoje pra versão atual, a jev-1.13.0 (o jev-preview aponta pra mesma coisa), e aqui vai o primeiro tome cuidado do post: jev-latest se move quando sai versão nova, o que pode mudar as respostas

Pra brincar em dev, beleza

Pra ranking em produção, fixa a versão

E o pré-requisito de produto?

Esse é o que mais gente esquece: o Jev não substitui o seu buscador

Curso de Jev: Sistemas Mais Inteligentes, Rápidos e Robustos
Curso Recomendado

Curso de Jev: Sistemas Mais Inteligentes, Rápidos e Robustos

Desenvolva sistemas mais inteligentes, rápidos e robustos com Jev

Você precisa já ter um retriever devolvendo uma shortlist, seja BM25, busca vetorial, Elastic, Postgres full text, o que for

O modelo entra depois, pontuando os candidatos que o retriever trouxe

Recuperar é uma coisa, ordenar é outra, e misturar as duas é o motivo de metade das buscas internas ruins por aí

Passo a passo: ranking da busca interna com o Jev

  1. Gere a shortlist com o buscador que você já tem

Não manda o catálogo inteiro pro modelo, manda o topo do que o retriever achou

Pra ter uma referência de tamanho: o cookbook de re-ranking da TypeSafe monta shortlists BM25 de 30 passagens para 40 consultas do dataset CLERC, e com o re-ranking a acurácia top-1 sai de 5% para 18% e a top-10 de 38% para 62%

O erro comum deste passo: querer ranquear tudo. Ranking é refino, não é recuperação

  1. Monte o state com a consulta e o candidato

O state é o contexto que o modelo enxerga pra responder

A janela do Jev 1.13 é de 32 mil tokens, e esse orçamento vale para o state mais a pergunta mais longa da requisição

# cliente e classes (AsyncTypeSafeClient, Score, Choice, Noul) vêm do typesafe-sdk
# confere os nomes exatos de import e de campo no quickstart oficial

client = AsyncTypeSafeClient()  # lê TYPESAFE_API_KEY do ambiente

def monta_state(consulta, item):
    return f"""Consulta do usuário: {consulta}

Título do resultado: {item['titulo']}
Seção: {item['secao']}
Trecho: {item['trecho'][:1200]}"""

O erro comum deste passo: despejar o documento inteiro no state. Custo é por token de entrada, então documento inteiro é dinheiro queimado em texto que não decide nada

  1. Escreva a pergunta Score com os níveis do baixo pro alto

A primitiva Score usa um array ordenado de descrições de níveis, do extremo baixo ao extremo alto, com até 10 níveis

Três já funciona bem, e honestamente três descritos com clareza rende mais que dez vagos

RELEVANCIA = [
    "não tem nada a ver com a consulta",
    "fala do assunto, mas não responde a dúvida",
    "responde exatamente o que foi perguntado",
]

E como a gente vai combinar mais de uma dimensão lá na frente, já escreve as outras duas agora, no mesmo formato:

ESPECIFICIDADE = [
    "fala do tema de forma genérica",
    "cobre parte do caso específico da consulta",
    "trata exatamente o caso específico que foi perguntado",
]

ATUALIDADE = [
    "descreve um comportamento que não existe mais no produto",
    "está desatualizado em alguns pontos",
    "reflete a versão atual do produto",
]

Repara que são três listas independentes, cada uma julgando UMA coisa só

E todas as três vão virar pergunta na mesma requisição, já já te mostro o porquê

O erro comum deste passo: escrever níveis tipo "ruim, médio, bom". Isso não diz NADA pro modelo nem pro colega que vai mexer depois. Descreve o comportamento observável de cada nível

  1. Agrupe todas as perguntas do mesmo state numa requisição só

Essa é a dica que mais muda a sua conta no fim do mês

Uma chamada com N perguntas dá as mesmas respostas que N chamadas com uma pergunta cada, e a recomendação oficial é mandar junto tudo que compartilha o mesmo state

No cookbook de perguntas paralelas, uma chamada com 13 perguntas sobre o mesmo artigo custou US$ 0,000497 contra US$ 0,006090 de 13 chamadas separadas, ou seja, 12,2x mais barato, e levou 0,27 s contra 2,71 s somados, 10x mais rápido

resposta = await client.system_one(
    state=monta_state(consulta, item),
    relevancia=Score(criteria=RELEVANCIA),
    especificidade=Score(criteria=ESPECIFICIDADE),
    atualidade=Score(criteria=ATUALIDADE),
)

O próprio cookbook de re-ranking avisa isso na cara: ele fez uma pergunta por par só pra ficar didático, e uma aplicação real faria várias perguntas sobre o mesmo par em uma única chamada

O erro comum deste passo: montar um for que dispara uma chamada por dimensão. Mesmo state, mesma requisição

  1. Normalize o score

A resposta de um Score é uma posição na escala, e ela pode cair entre dois níveis

Junto dela vêm dois campos que você vai usar: probabilities, com a probabilidade de cada nível, e confidence, um número de 0 a 1 que resume a forma dessa distribuição

Guarda esse confidence junto do score normalizado, porque é ele que decide lá na frente o que sobe sozinho e o que vai pra revisão

Escala de quatro níveis devolve de 0 a 3, de três níveis devolve de 0 a 2

Pra deixar tudo numa régua de 0 a 1:

def normaliza(score, criteria):
    return score / (len(criteria) - 1)

O erro comum deste passo: somar direto uma escala de 3 níveis com uma de 5 e achar que a segunda é "mais importante". Ela só tem número maior, e o seu ranking vira loteria

  1. Combine as dimensões com pesos no seu código

Aqui entra o padrão de composite scoring: quebra o julgamento em dimensões independentes, normaliza cada uma de 0 a 1 e combina com pesos

O exemplo da doc faz 0,6 × 0,62 + 0,3 × 0,64 + 0,1 × 1,0 = 0,664, arredondado pra 0,66

def score_final(rel, esp, atu):
    return 0.6 * rel + 0.3 * esp + 0.1 * atu

Repara onde os pesos moram: no seu repositório, versionados, revisáveis em PR

É o mesmo raciocínio de quando você usa scores calibrados pra priorizar leads: o modelo julga cada dimensão, a política de negócio fica com você

O erro comum deste passo: pedir pro modelo "dar a nota final considerando tudo". Aí você perde a capacidade de debugar por que o item X subiu

  1. Ordene e aplique o corte
ranqueados = sorted(itens, key=lambda i: i["final"], reverse=True)

CORTE = 0.35  # número seu, calibre numa amostra real de consultas
visiveis = [i for i in ranqueados if i["final"] >= CORTE]

O erro comum deste passo: cortar antes de olhar a distribuição. Roda com uma amostra de consultas reais e escolhe o corte vendo o que some da tela

Usando Choice para ranquear muitos itens em uma requisição só

O fluxo de cima é par a par: uma consulta contra um candidato

Pra catálogo grande existe um caminho mais direto, que é jogar os itens como opções de uma pergunta Choice e deixar o modelo distribuir probabilidade entre eles

Uma pergunta Choice aceita até 255 opções, e cada opção adicionada custa poucos tokens

Por isso a doc recomenda passar a lista completa em vez de mandar um recorte: cortar opção pra "economizar" é economia de centavo que estraga o resultado

E aqui entra a terceira primitiva do Jev, que ainda não apareceu por aqui: a Noul

Pra fechar o combo: Choice escolhe dentro de uma lista de opções, Score dá nota numa escala que você descreve, e Noul devolve a probabilidade de uma afirmação ser verdadeira

No nosso caso a Noul responde uma pergunta bem prática: esse documento responde a consulta, sim ou não?

resposta = await client.system_one(
    state=f"Consulta: {consulta}\n\n" + linhas_numeradas,
    melhor_linha=Choice(options=ids_das_linhas),
    documento_responde=Noul("o documento responde a consulta do usuário"),
)

O cookbook de busca linha a linha mostra exatamente isso: 218 IDs de linha dos Termos de Serviço do GitHub são pontuados contra uma consulta em linguagem natural em uma única requisição, com uma pergunta Choice ranqueando os IDs e uma Noul checando se o documento responde

A função find() de lá devolve a probabilidade de exists e um score de relevância por linha

Ou seja: a Choice te dá o ranking, a Noul te dá o "tem resposta aqui ou não tem?"

Comparando com o fluxo par a par do começo do post: o Score dá uma nota própria pra cada candidato, o que é ótimo quando você quer várias dimensões e um corte por nota

A Choice não dá nota individual, ela distribui probabilidade entre as opções e te devolve a ordem, com todos os itens numa requisição só

Regra de bolso: precisa de nota e de pesos? Score. Precisa só da ordem entre muitos itens, numa tacada só? Choice

E o Noul é uma primitiva versátil, é a mesma que serve pra checar tentativa de jailbreak antes do LLM responder

E quando o catálogo passa de 255 itens?

Particiona em lotes

Ranqueia cada lote, pega o topo de cada um e faz uma rodada final entre os finalistas

E fica de olho na janela de 32 mil tokens: o limite prático não é só a contagem de opções, é o quanto de texto você colocou em cada uma. ID curto mais um título enxuto rende muito mais item por requisição que parágrafo inteiro como opção

Como usar o confidence para decidir o que sobe e o que não aparece

Todo resultado vem com probabilities, a probabilidade de cada nível, e com confidence, um número de 0 a 1 que resume a forma dessa distribuição pra você aplicar um limiar sem fazer a conta na mão

No Score a confiança vem da distribuição entre os níveis

Distribuição mais achatada significa confiança menor, e isso faz todo sentido: se o modelo espalhou probabilidade entre "não tem nada a ver" e "responde exatamente", ele está em cima do muro

A doc sugere um padrão de três faixas, com limiares diferentes conforme o risco da ação:

Faixa de confiança O que a doc recomenda Como aplicar no ranking
Alta agir automaticamente reordena o resultado normalmente
Média seguir com cautela, confirmar ou revisar mantém a posição original ou marca pra revisão
Baixa não agir, mandar pra humano ou outro sistema deixa o ranking do retriever mandar

Repara na lógica do caso de baixa confiança: você não inventa nota, você simplesmente não mexe

O fallback do ranking com Jev é o ranking que você já tinha, e isso deixa o deploy MUITO menos assustador

E por que dá pra confiar nesse número em vez de tratar como enfeite? Porque modelos System One são treinados pra decisão calibrada, com as probabilidades otimizadas contra resultados pra refletir a incerteza (é o que a doc chama de RLCD, reinforcement learning for calibrated decisions)

Somando a isso, as saídas são restritas ao schema definido antes da chamada, então o modelo não consegue devolver valor inválido nem erro de tipo

Nada de json.loads dentro de try/except rezando pra dar certo 😛

Onde esse ranking com Jev faz diferença no produto

O mapa de casos de uso da TypeSafe cita explicitamente pontuar relevância consulta-candidato, reordenar resultados com comparações par a par e selecionar contexto pra pipelines de RAG

Na prática, alguns lugares onde isso rende rápido:

  • Help center e documentação: usuário digita a dúvida com as palavras dele, a doc usa as palavras do produto. Keyword sofre nisso, julgamento de relevância não
  • E-commerce: aqui o composite scoring brilha, porque relevância, disponibilidade e margem são dimensões diferentes e cada uma tem peso diferente na sua régua de negócio
  • Seleção de contexto pra RAG: em vez de empurrar os top 10 do vetorial pro prompt, pontua e manda só o que passou do corte. Menos contexto irrelevante, resposta melhor
  • Reordenação par a par: quando dois candidatos estão empatados no score, uma pergunta comparando os dois resolve o desempate

Um detalhe legal: em todos esses casos o modelo julga, mas a política continua sua. Quer que produto sem estoque nunca apareça no topo? Isso é if no seu código, não é prompt

Custo, latência e os erros que estouram a conta

Sintoma: a conta veio muito mais alta que o esperado

Causa: uma chamada por par, com o state sendo reenviado a cada chamada

O Jev é cobrado apenas por token de entrada, US$ 0,042 por 1M de tokens de input, com saída a US$ 0,00

Output de graça é ótimo, mas input repetido é input pago de novo

Solução: agrupar as perguntas do mesmo state

Os números do cookbook não deixam dúvida: 12,2x mais barato e 10x mais rápido na comparação de 13 perguntas

Sintoma: o ranking mudou sozinho entre um deploy e outro

Causa: jev-latest, que é o padrão do SDK, apontando pra uma versão nova do modelo

O alias se move, e quando ele se move as respostas podem mudar

Solução: fixar a versão em produção (hoje a atual é a jev-1.13.0) e subir versão de propósito, medindo antes e depois

Sintoma: estouro de contexto em alguns itens

Causa: state grande demais pro orçamento de 32 mil tokens, lembrando que esse orçamento cobre o state mais a pergunta mais longa da requisição

Geralmente é aquele item com página gigante que entra na shortlist e arrebenta tudo

Solução: enxugar o state pro trecho que realmente decide. Título, seção e um recorte do corpo costumam bastar pra julgar relevância

Como prevenir isso desde o começo

Define duas coisas antes de escrever a primeira linha:

  • um orçamento por busca: quantos itens você aceita ranquear por consulta
  • um teto de shortlist: 30 candidatos já é uma referência pública de fluxo que funciona

Depois é só monitorar tokens de input por busca, porque é ali que o custo mora

Conclusão

O ganho aqui não vem de trocar de buscador

Vem de separar recuperação de ordenação: o retriever acha os candidatos, o Jev pontua cada um com escala calibrada, e a decisão final é feita com pesos que vivem no seu código, versionados, testáveis e explicáveis pra quem perguntar por que o item subiu

Próximo passo que eu sugiro: pega uma amostra de consultas reais do seu produto, mede top-1 e top-10 com o ranking atual, roda o fluxo e mede de novo

Sem medir antes, você não tem como saber se melhorou ou se só ficou mais caro 😀

E se tu não quer plugar a API própria agora, o Jev também tem página de modelo na Cloudflare Workers AI e no OpenRouter, dá pra testar por onde já estiver na sua stack

Até o próximo post!

Perguntas frequentes

Dá pra usar o Jev sem trocar o buscador que a empresa já tem?

Dá, e é assim que o fluxo funciona. O Jev não faz recuperação, ele pontua os candidatos que o seu retriever (BM25, busca vetorial, Elastic, Postgres full text, o que for) já trouxe. Você mantém o buscador atual e adiciona uma etapa de ranking em cima.

Quanto custa usar o Jev pra ranquear resultados de busca?

O preço é só por token de entrada: US$ 0,042 por 1 milhão de tokens de input, com output a US$ 0,00. O mesmo valor aparece na página do Jev 1.13 no OpenRouter. Como a conta é por token de entrada, montar um state enxuto (sem despejar o documento inteiro) impacta direto no custo.

Quantos itens dá pra pontuar numa única chamada ao Jev?

Depende da primitiva. Uma pergunta Choice aceita até 255 opções na mesma chamada, e no cookbook de busca linha a linha da TypeSafe, 218 IDs de linha dos Termos de Serviço do GitHub foram pontuados numa requisição só, combinando uma pergunta Choice pra ranquear os IDs e uma Noul checando se o documento responde a consulta.

Como saber se posso confiar na nota que o Jev devolveu antes de usar no ranking?

O modelo devolve um campo confidence de 0 a 1 junto com a resposta, calculado a partir da distribuição de probabilidades entre os níveis do Score. A documentação sugere dividir isso em três faixas (alta, média, baixa) e definir o limiar de acordo com o risco da ação, então dá pra usar confidence baixa como sinal pra revisar manualmente um resultado antes de exibir no topo da busca.

O Jev só funciona pela API própria da TypeSafe?

Não, ele também está disponível via Cloudflare Workers AI e no OpenRouter, na página typesafe/jev. Pra quem já usa um desses provedores no stack, dá pra chamar o Jev por ali em vez de bater direto no endpoint da TypeSafe.

Vale a pena mandar uma pergunta por candidato ou agrupar tudo numa chamada?

Agrupar é a recomendação oficial sempre que as perguntas compartilham o mesmo state. No cookbook de perguntas paralelas, 13 perguntas sobre o mesmo artigo numa chamada custaram US$ 0,000497 contra US$ 0,006090 de 13 chamadas separadas, e o tempo caiu de 2,71 s somados pra 0,27 s.



Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 45min

Blog | Mais populares