Fazendo perguntas em Query DSL
Na Unidade 1 a Tool falava SQL. Aqui ela fala Query DSL — a linguagem de consulta do Elasticsearch, escrita em JSON.
Não é preciso aprender Elasticsearch. É preciso aprender cinco construções, e é isso que este capítulo faz. Sem elas você vai escrever consulta no escuro, e — pior — não vai conseguir julgar se o que o agente devolveu está certo.
O esqueleto
Toda consulta é um dicionário com as mesmas peças:
{
"size": 5, # quantos resultados
"query": { ... }, # o filtro
"sort": [{"dataAjuizamento": {"order": "desc"}}], # a ordem
"_source": ["numeroProcesso", "classe.nome"] # quais campos trazer
}
Em SQL isso seria LIMIT, WHERE, ORDER BY e a lista do SELECT. Mesmas quatro perguntas, notação diferente.
_source é a que se esquece com mais frequência e a que mais importa aqui. Sem ela, cada processo volta com a lista inteira de movimentos — que pode ter centenas de entradas. A Unidade 1 já explicou por que isso é caro: a Observation vira texto no prompt. Trazer movimento que ninguém vai ler é queimar a janela de contexto.
1. match_all — sem filtro
{"size": 1, "query": {"match_all": {}}}
Serve para duas coisas: espiar a forma do dado, e contar.
2. term e match — a distinção que decide tudo
Esta é a armadilha do Elasticsearch, e ela tem consequência direta na qualidade da resposta do agente.
Um campo de texto no Elasticsearch é indexado de duas maneiras ao mesmo tempo:
assuntos.nome— texto analisado. Quebrado em palavras, minúsculas, sem acento. Busca por relevância.assuntos.nome.keyword— o texto inteiro, cru, como uma etiqueta. Comparação exata.
Compare os dois na mesma pergunta:
# match no campo analisado {: #match-no-campo-analisado }
consultar({"size": 0, "track_total_hits": True,
"query": {"match": {"assuntos.nome": "Violência Doméstica"}}})
# term no campo keyword {: #term-no-campo-keyword }
consultar({"size": 0, "track_total_hits": True,
"query": {"term": {"assuntos.nome.keyword": "Violência Doméstica Contra a Mulher"}}})
Resultado real:
match : 301922
term : 14723
Vinte vezes mais. E não é que o match seja mais generoso: é que ele quebrou a frase em violência ou doméstica e trouxe tudo que casasse com qualquer uma das duas.
Veja o que o match devolve:
{"numeroProcesso": "00379479320178190000", "nivelSigilo": 0,
"assuntos": [{"codigo": 5560, "nome": "Decorrente de Violência Doméstica"},
{"codigo": 5560, "nome": "Decorrente de Violência Doméstica"},
{"codigo": 5560, "nome": "Decorrente de Violência Doméstica"},
{"codigo": 5560, "nome": "Decorrente de Violência Doméstica"},
{"codigo": 5560, "nome": "Decorrente de Violência Doméstica"}]}
"Decorrente de Violência Doméstica" (código 5560) é um assunto diferente de "Violência Doméstica Contra a Mulher" (código 10949). O match não sabe disso; ele viu as palavras.
Se você entregar um match a um agente e pedir "processos de violência doméstica", ele vai responder com convicção, com números de processo verdadeiros, sobre um assunto que não é o que você perguntou.
Não é alucinação — é a consulta errada, executada corretamente. É o tipo de erro mais difícil de pegar, porque a resposta parece boa.
De quebra, repare no 5560 repetido cinco vezes no mesmo processo. Dado real de tribunal é assim.
3. bool e filter — combinando condições
Para mais de uma condição:
{
"query": {
"bool": {
"filter": [
{"term": {"assuntos.nome.keyword": "Violência Doméstica Contra a Mulher"}},
{"range": {"nivelSigilo": {"lte": 0}}}
]
}
}
}
filter é a lista de condições que todas precisam valer — o AND do SQL.
Existe também must, que faz o mesmo mas calcula pontuação de relevância. Para filtro de dado estruturado, filter é o certo: é mais rápido e não inventa ranking onde não há.
4. sort — a ordem, e a pegadinha do campo de texto
"sort": [{"dataAjuizamento": {"order": "desc"}}]
Parece que resolve, e não resolve. Guarde esta linha: ela é a pegadinha mais cara desta unidade, e vamos desmontá-la em As Tools, agora contra o CNJ.
O motivo, em resumo: dataAjuizamento não tem um formato só. O índice do TJRJ guarda o mesmo campo ora como 20240802145113, ora como 2024-08-02T14:51:13.000Z, e o Elasticsearch ordena os dois numa escala só. Medido: o processo mais antigo do formato compacto é de 1998 e ainda assim ordena acima do processo mais novo do formato ISO, de 2025.
Ou seja, esta linha ordena — só não ordena por data.
Vale registrar o método, porque ele vai se repetir: essa pegadinha não foi descoberta lendo documentação. Foi descoberta rodando um date_histogram e vendo aparecer o ano 2610.
Um campo que você não mediu é um campo em que você não deve confiar, por mais óbvio que o nome dele pareça.
Agora tente ordenar ou agrupar por grau:
consultar({"size": 0, "aggs": {"g": {"terms": {"field": "grau", "size": 10}}}})
HTTP 400: {"error":{"root_cause":[{"type":"illegal_argument_exception",
"reason":"Fielddata is disabled on [grau] in [api_publica_tjrj]. Text fields
are not optimised for operations that require per-document field data like
aggregations and sorting, so these operations are disabled by default.
Please use a keyword field instead. ..."}]}}
Esse erro é um presente. Ele diz o problema (grau é texto analisado), a causa (agregar exige dado por documento) e a solução (use a keyword field instead).
Trocando por grau.keyword:
consultar({"size": 0, "aggs": {"g": {"terms": {"field": "grau.keyword", "size": 10}}}})
{"g": {"buckets": [
{"key": "G1", "doc_count": 15959104},
{"key": "JE", "doc_count": 3951663},
{"key": "G2", "doc_count": 2608822},
{"key": "TR", "doc_count": 545092}]}}
O acervo do TJRJ no DataJud: quase 16 milhões em primeiro grau, 3,9 milhões nos Juizados Especiais, 2,6 milhões em segundo grau, 545 mil nas Turmas Recursais.
Lembre-se de onde essa mensagem de erro veio: do except HTTPError do cnj.py, que lê o corpo da resposta antes de levantar a exceção.
Isso importa duas vezes. Para você, agora, que leu o diagnóstico em vez de "HTTP 400". E para o agente, mais adiante — porque no smolagents a exceção vira Observation, e o modelo lê "Please use a keyword field instead" com a mesma clareza que você.
5. aggs — contar sem trazer
Agregações respondem perguntas sobre o conjunto inteiro sem devolver documento nenhum. Com size: 0, você recebe só os números.
Quantos nomes distintos de assunto existem no acervo do TJRJ?
consultar({"size": 0,
"aggs": {"n": {"cardinality": {"field": "assuntos.nome.keyword"}}}})
{"n": {"value": 2804}}
Quais os mais frequentes?
consultar({"size": 0,
"aggs": {"a": {"terms": {"field": "assuntos.nome.keyword", "size": 5}}}})
{"a": {"buckets": [
{"key": "Dívida Ativa (Execução Fiscal)", "doc_count": 5948694},
{"key": "Impostos", "doc_count": 3103459},
{"key": "Indenização por Dano Moral", "doc_count": 2284732},
{"key": "IPTU/ Imposto Predial e Territorial Urbano", "doc_count": 1797178},
{"key": "Indenização por Dano Material", "doc_count": 1194955}]}}
Esses 2.804 nomes vão virar o catálogo da primeira Tool no próximo capítulo.
Uma armadilha que custa caro: array plano
assuntos é uma lista. E, no índice do DataJud, é uma lista plana — não nested. Isso tem uma consequência que não é óbvia.
Tente descobrir o código de um assunto a partir do nome, agrupando um pelo outro:
consultar({"size": 0,
"aggs": {"c": {"terms": {"field": "assuntos.codigo", "size": 5},
"aggs": {"n": {"terms": {"field": "assuntos.nome.keyword", "size": 3}}}}}})
{"c": {"buckets": [
{"key": 6017, "doc_count": 5950185,
"n": {"buckets": [
{"key": "Dívida Ativa (Execução Fiscal)", "doc_count": 5948694},
{"key": "Impostos", "doc_count": 1509869},
{"key": "IPTU/ Imposto Predial e Territorial Urbano", "doc_count": 489297}]}}]}}
Lido ingenuamente: "o código 6017 se chama Dívida Ativa, ou Impostos, ou IPTU". Nenhum dos três seria seguro afirmar — o 6017 é Dívida Ativa, e os outros dois são assuntos do mesmo processo.
Num array plano, o Elasticsearch perde o pareamento entre posições. Ele sabe que o processo tem os códigos [6017, 5916, 5952] e os nomes ["Dívida Ativa", "Impostos", "IPTU"]; não sabe qual nome vai com qual código.
Consequência de projeto: não vamos resolver nome → código por agregação. Vamos buscar direto pelo nome exato em assuntos.nome.keyword, que é comparação de valor e não depende de pareamento nenhum. É o que a Tool do próximo capítulo faz.
Resumo
| Quero | Uso |
|---|---|
| Tudo, sem filtro | match_all |
| Valor exato | term em campo.keyword |
| Texto livre, por relevância | match em campo — cuidado |
| Várias condições | bool → filter: [...] |
| Faixa numérica ou de data | range |
| Ordenar | sort — só em campos keyword ou numéricos |
| Contar sem trazer | aggs com size: 0 |
| Total verdadeiro | track_total_hits: true |
| Economizar contexto | _source com a lista mínima |