para.AI_ASSUNTOS_CNJ / docs /FLUXOGRAMA.md
Carlexxx
para.AI beta
8a646ad

📊 FLUXOGRAMA.md — Para.AI Assuntos Jurídicos

Fluxo de dados nas pesquisas disponíveis


Fluxo 1 — GET /busca (full-text)

Cliente
  │  GET /busca?q=aposentadoria&ramo=DIREITO+PREVIDENCIÁRIO&size=10
  ▼
routes.busca_get()
  │  valida Query params via FastAPI/Pydantic
  ▼
es_client.buscar(q, ramo, nivel2, nivel3, lei, page, size, com_facets)
  │
  ├─ build_busca_query()
  │    multi_match:
  │      query: "aposentadoria"
  │      fields: [nome_assunto^4, titulo_curto^3, breve_sintese^2,
  │               glossario, classes_path^2, texto_completo]
  │      fuzziness: AUTO · prefix_length: 2
  │    filter: [term{ramo: "DIREITO PREVIDENCIÁRIO"}]
  │    highlight: [nome_assunto, titulo_curto, breve_sintese]
  │    _source.excludes: [texto_completo]
  │    aggs: {por_ramo, por_nivel2, por_nivel3, por_lei, profundidades}
  │    from: 0 · size: 10
  │
  ▼
Elasticsearch 8.12
  │  Analyzer juridico_pt: tokenize → lowercase → asciifolding → stem PT
  │  BM25 scoring com boosts
  │  Agregações bucket
  ▼
builders.build_busca_response(raw, took_ms, page, size)
  │  hits → AssuntoHit(score, Assunto(**src))
  │  highlight injetado em breve_sintese
  │  aggs → Facets(por_ramo, por_nivel2 …)
  ▼
BuscaResponse {total, pagina, tamanho, took_ms, resultados[], facets}
  │  ORJSONResponse
  ▼
Cliente recebe JSON

Fluxo 2 — POST /busca-q (estruturada para LLMs)

LLM / Cliente
  │  POST /busca-q
  │  {q, campos:[{campo,valor}…], modo, topk, operador, retornar[]}
  ▼
routes.busca_q_post()
  │  valida BuscaQRequest (Pydantic)
  │  valida retornar[] ∈ FICHA_CAMPOS_VALIDOS
  ▼
es_client.busca_q(q, campos, modo, topk, operador, …, retornar)
  │
  ├─ _ficha_to_es_source(retornar, incluir_texto_completo)
  │    retornar=[]  → {"excludes":["texto_completo"]}
  │    retornar=[…] → {"includes":[campos_es mapeados]}
  │    "texto" em retornar OR incluir_texto_completo=True
  │              → inclui texto_completo
  │
  ├─ build_busca_q_query(q, campos, modo, topk, …)
  │    must:   multi_match(q) com pesos
  │    should: _clausula_campo(campo, valor, modo) × N
  │      fuzzy  → match com fuzziness AUTO
  │      exato  → term (keyword)
  │    minimum_should_match: 1 (or) | N (and)
  │    _source: ← _ficha_to_es_source()
  │    size: topk
  │
  ▼
Elasticsearch 8.12
  │  score combinado: must (q) + should (campos)
  │  retorna apenas campos _source solicitados
  ▼
builders.build_busca_q_response(raw, …, retornar)
  │  hits → FichaHit(score, campos_matched, _src_to_ficha())
  │
  │  _src_to_ficha():   ← FIX #1 aplicado aqui
  │    want = set(retornar)|{"id"} se retornar else None
  │    _want(campo) = True se want is None OR campo in want
  │    titulo      ← hl("titulo",       "nome_assunto")
  │    introducao  ← hl("introducao",   "breve_sintese")
  │    definicao   ← hl("definicao",    "glossario")
  │    normas      ← src["dispositivos_legais"]
  │    texto       ← src["texto_completo"]
  │                  SE (incluir_texto OR
  │                      (want is not None AND "texto" in want))
  │
  ▼
BuscaQResponse {total, retornados, took_ms, modo, operador, resultados[]}
  │  payload compacto (~2KB)  ← ideal para LLMs
  ▼
LLM recebe fichas estruturadas

Fluxo 3 — GET /autocomplete

Interface (digitação)
  │  GET /autocomplete?q=aposen&size=8
  ▼
routes.autocomplete()
  ▼
es_client.autocomplete(q, size)
  │
  ├─ build_autocomplete_query("aposen", 8)
  │    _source: [nome_assunto, titulo_curto, ramo, classes_nivel2]
  │    bool.should:
  │      match{nome_assunto.autocomplete: "aposen", boost:2}
  │      match{titulo_curto.autocomplete: "aposen",  boost:1}
  │
  ▼
Elasticsearch
  │  Tokenizer edge_ngram: min_gram=2, max_gram=20
  │  "aposentadoria" → ["ap","apo","apos","aposen","aposent",…]
  │  Match: docs cujo prefixo == "aposen"
  │  Rank: boost nome_assunto > titulo_curto
  ▼
Deduplicação (set) + prioriza nome_assunto
  ▼
AutocompleteResponse {sugestoes: ["Aposentadoria", "Aposentadoria Especial", …]}

Fluxo 4 — GET /hierarquia

Cliente
  │  GET /hierarquia
  ▼
routes.hierarquia()
  ▼
es_client.get_hierarquia()
  │
  ├─ Query ES:
  │    size: 0  (sem hits, só agregações)
  │    aggs:
  │      ramos: terms{classes_nivel1, size:25}
  │        nivel2: terms{classes_nivel2, size:30}
  │          nivel3: terms{classes_nivel3, size:30}
  │
  ▼
Elasticsearch — aggregations aninhadas
  ▼
builders.build_hierarquia_response(raw)
  │  ramos_buckets → HierarquiaNo(nome, caminho, total, filhos[])
  │  Para cada ramo → para cada nivel2 → para cada nivel3
  │  Recursão: filhos aninhados
  ▼
HierarquiaResponse {ramos: [HierarquiaNo{nome, caminho, total, filhos[…]}]}

Fluxo 5 — GET /grafo/filhos (drill-down)

Cliente
  │  GET /grafo/filhos?ancestor=Crimes+contra+o+Patrimônio&size=20
  ▼
routes.drill_down(ancestor, size)
  ▼
es_client.drill_down(ancestor, size)
  │
  ├─ Query ES:
  │    size: 20
  │    _source.excludes: [texto_completo, glossario]
  │    query: term{classes_ancestors: "Crimes contra o Patrimônio"}
  │
  ▼
Elasticsearch — filtra por campo classes_ancestors (keyword array)
  ▼
{ancestor, total, filhos:[_source dos hits]}

Fluxo 6 — Inicialização do Sistema

docker-compose up
  ▼
Container app: entrypoint.sh
  │  1. Loop: curl http://elasticsearch:9200/_cluster/health
  │            Aguarda status ≠ red (retry até 60s)
  │  2. Download:
  │      curl -L https://github.com/…/bulk_assuntos.ndjson
  │           -o /app/data/bulk_assuntos.ndjson
  │  3. exec uvicorn app.main:app
  ▼
FastAPI lifespan (main.py)
  │  await es_client.setup_es()
  │    wait_for_es()  ← tenacity retry 10x
  │    Índice existe?
  │      NÃO → create_index() → bulk_index()
  │      SIM → count()
  │        count == 0 → bulk_index()
  │        count > 0  → skip (já indexado)
  ▼
bulk_index()
  │  _iter_bulk_docs(): lê NDJSON, 2 linhas por doc
  │  async_streaming_bulk(chunk_size=500)
  │  Log: "5.184 documentos indexados"
  ▼
API pronta — porta 8000

Última atualização: 24/02/2026 · v1.0.0