# 📊 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*