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*