Pular para o conteúdo

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:

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.

Atenção

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.

Atenção

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.

Nota

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 boolfilter: [...]
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

Fixando o capítulo

Q1: match em assuntos.nome trouxe 301.922 processos; term em assuntos.nome.keyword trouxe 14.723. Por quê?

Q2: A agregação em grau devolveu HTTP 400 com "Fielddata is disabled on [grau]". Qual a correção?

Q3: A agregação de assuntos.codigo mostrou o código 6017 associado a "Dívida Ativa", "Impostos" e "IPTU". O que isso significa?