perf(ranking): otimizar paginação usando Oracle direto
- Remover carregamento de 350k registros em memória no startup - Refatorar endpoints para buscar dados direto do Oracle: - ranking_paginado - buscar_por_nome - ranking_estatisticas - obter_posicao_ranking - Adicionar healthcheck leve no Oracle com start_period de 60s - Corrigir start-ngrok.sh para subir todos os containers - Adicionar domínio ngrok-free.dev no vite.config.js
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import html
|
||||
import re
|
||||
import unicodedata
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
@@ -10,6 +11,42 @@ from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
LATIN1_BYTE_MAP = {
|
||||
192: 'À', 193: 'Á', 194: 'Â', 195: 'Ã', 196: 'Ä',
|
||||
199: 'Ç',
|
||||
200: 'È', 201: 'É', 202: 'Ê', 203: 'Ë',
|
||||
204: 'Ì', 205: 'Í', 206: 'Î', 207: 'Ï',
|
||||
209: 'Ñ',
|
||||
210: 'Ò', 211: 'Ó', 212: 'Ô', 213: 'Õ', 214: 'Ö',
|
||||
217: 'Ù', 218: 'Ú', 219: 'Û', 220: 'Ü',
|
||||
224: 'à', 225: 'á', 226: 'â', 227: 'ã', 228: 'ä',
|
||||
231: 'ç',
|
||||
232: 'è', 233: 'é', 234: 'ê', 235: 'ë',
|
||||
236: 'ì', 237: 'í', 238: 'î', 239: 'ï',
|
||||
241: 'ñ',
|
||||
242: 'ò', 243: 'ó', 244: 'ô', 245: 'õ', 246: 'ö',
|
||||
249: 'ù', 250: 'ú', 251: 'û', 252: 'ü',
|
||||
}
|
||||
|
||||
|
||||
def corrigir_encoding(texto: str) -> str:
|
||||
if not texto:
|
||||
return texto
|
||||
|
||||
def substituir_byte(match):
|
||||
byte_val = int(match.group(1))
|
||||
return LATIN1_BYTE_MAP.get(byte_val, match.group(0))
|
||||
|
||||
pattern = r'(?<=[\w\u00C0-\u00FF])(\d{3})(?=[\w\u00C0-\u00FF])'
|
||||
resultado = texto
|
||||
for _ in range(5):
|
||||
novo = re.sub(pattern, substituir_byte, resultado)
|
||||
if novo == resultado:
|
||||
break
|
||||
resultado = novo
|
||||
return resultado
|
||||
|
||||
|
||||
def normalizar_texto(texto: str) -> str:
|
||||
if not texto:
|
||||
return ""
|
||||
@@ -41,7 +78,7 @@ from ..schemas.ranking_schema import (
|
||||
SugerirConsultoresResponseSchema,
|
||||
AreaAvaliacaoSchema,
|
||||
)
|
||||
from .dependencies import get_repository, get_ranking_store, get_processar_job, get_es_client
|
||||
from .dependencies import get_repository, get_ranking_store, get_processar_job, get_es_client, get_ranking_oracle_repo
|
||||
from ...infrastructure.elasticsearch.client import ElasticsearchClient
|
||||
from ...application.jobs.job_status import job_status
|
||||
|
||||
@@ -168,35 +205,43 @@ async def ranking_paginado(
|
||||
size: int = Query(default=50, ge=1, le=1000, description="Tamanho da página (máx 1000)"),
|
||||
ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"),
|
||||
selos: Optional[str] = Query(default=None, description="Filtrar por selos (separados por vírgula)"),
|
||||
store = Depends(get_ranking_store),
|
||||
oracle_repo = Depends(get_ranking_oracle_repo),
|
||||
):
|
||||
if not store.is_ready():
|
||||
import json as json_lib
|
||||
|
||||
if not oracle_repo:
|
||||
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
||||
|
||||
total = oracle_repo.contar_total(filtro_ativo=ativo)
|
||||
if total == 0:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
||||
)
|
||||
|
||||
filtro_selos = [s.strip() for s in selos.split(",") if s.strip()] if selos else None
|
||||
total, entries = store.get_page(page=page, size=size, filtro_ativo=ativo, filtro_selos=filtro_selos)
|
||||
|
||||
consultores = oracle_repo.buscar_paginado(page=page, size=size, filtro_ativo=ativo)
|
||||
total_pages = (total + size - 1) // size
|
||||
|
||||
consultores_schema = []
|
||||
for e in entries:
|
||||
d = e.detalhes
|
||||
for c in consultores:
|
||||
try:
|
||||
d = json_lib.loads(c.json_detalhes) if isinstance(c.json_detalhes, str) else c.json_detalhes or {}
|
||||
except (json_lib.JSONDecodeError, TypeError):
|
||||
d = {}
|
||||
|
||||
tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d)
|
||||
consultores_schema.append(
|
||||
ConsultorRankingResumoSchema(
|
||||
id_pessoa=e.id_pessoa,
|
||||
nome=e.nome,
|
||||
posicao=e.posicao,
|
||||
pontuacao_total=float(e.pontuacao_total),
|
||||
bloco_a=float(e.bloco_a),
|
||||
bloco_b=float(e.bloco_b),
|
||||
bloco_c=float(e.bloco_c),
|
||||
bloco_d=float(e.bloco_d),
|
||||
ativo=e.ativo,
|
||||
anos_atuacao=float(e.anos_atuacao),
|
||||
id_pessoa=c.id_pessoa,
|
||||
nome=c.nome,
|
||||
posicao=c.posicao,
|
||||
pontuacao_total=float(c.pontuacao_total),
|
||||
bloco_a=float(c.componente_a),
|
||||
bloco_b=float(c.componente_b),
|
||||
bloco_c=float(c.componente_c),
|
||||
bloco_d=float(c.componente_d),
|
||||
ativo=c.ativo,
|
||||
anos_atuacao=float(c.anos_atuacao),
|
||||
tipos_atuacao=tipos_atuacao,
|
||||
coordenador_ppg=bool(d.get("coordenador_ppg", False)),
|
||||
consultoria=d.get("consultoria"),
|
||||
@@ -225,15 +270,12 @@ async def ranking_paginado(
|
||||
async def buscar_por_nome(
|
||||
nome: str = Query(..., min_length=3, description="Nome (ou parte) para buscar"),
|
||||
limit: int = Query(default=5, ge=1, le=20, description="Limite de resultados"),
|
||||
store = Depends(get_ranking_store),
|
||||
oracle_repo = Depends(get_ranking_oracle_repo),
|
||||
):
|
||||
if not store.is_ready():
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
||||
)
|
||||
if not oracle_repo:
|
||||
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
||||
|
||||
resultados = store.buscar_por_nome(nome=nome, limit=limit)
|
||||
resultados = oracle_repo.buscar_por_nome(nome=nome, limit=limit)
|
||||
return [
|
||||
ConsultaNomeSchema(
|
||||
id_pessoa=r["ID_PESSOA"],
|
||||
@@ -247,53 +289,20 @@ async def buscar_por_nome(
|
||||
|
||||
@router.get("/ranking/estatisticas", response_model=EstatisticasRankingSchema)
|
||||
async def ranking_estatisticas(
|
||||
store = Depends(get_ranking_store),
|
||||
oracle_repo = Depends(get_ranking_oracle_repo),
|
||||
):
|
||||
if not store.is_ready():
|
||||
if not oracle_repo:
|
||||
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
||||
|
||||
total = oracle_repo.contar_total()
|
||||
if total == 0:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
||||
)
|
||||
|
||||
total = store.total()
|
||||
ativos = store.total(filtro_ativo=True)
|
||||
inativos = total - ativos
|
||||
entries = store.get_page(page=1, size=total)[1] if total else []
|
||||
totais = [e.pontuacao_total for e in entries]
|
||||
distribuicao = []
|
||||
if total:
|
||||
buckets = [
|
||||
("800+", lambda x: x >= 800),
|
||||
("600-799", lambda x: 600 <= x < 800),
|
||||
("400-599", lambda x: 400 <= x < 600),
|
||||
("200-399", lambda x: 200 <= x < 400),
|
||||
("0-199", lambda x: x < 200),
|
||||
]
|
||||
for faixa, pred in buckets:
|
||||
qtd = sum(1 for x in totais if pred(x))
|
||||
distribuicao.append(
|
||||
{
|
||||
"faixa": faixa,
|
||||
"quantidade": qtd,
|
||||
"percentual": round((qtd * 100.0 / total), 2) if total else 0,
|
||||
}
|
||||
)
|
||||
|
||||
estatisticas = {
|
||||
"total_consultores": total,
|
||||
"total_ativos": ativos,
|
||||
"total_inativos": inativos,
|
||||
"ultima_atualizacao": store.last_update.isoformat() if store.last_update else None,
|
||||
"pontuacao_media": (sum(totais) / total) if total else 0,
|
||||
"pontuacao_maxima": max(totais) if totais else 0,
|
||||
"pontuacao_minima": min(totais) if totais else 0,
|
||||
"media_componentes": {
|
||||
"a": (sum(e.bloco_a for e in entries) / total) if total else 0,
|
||||
"b": (sum(e.bloco_b for e in entries) / total) if total else 0,
|
||||
"c": (sum(e.bloco_c for e in entries) / total) if total else 0,
|
||||
"d": (sum(e.bloco_d for e in entries) / total) if total else 0,
|
||||
},
|
||||
}
|
||||
estatisticas = oracle_repo.obter_estatisticas()
|
||||
distribuicao = oracle_repo.obter_distribuicao()
|
||||
|
||||
return EstatisticasRankingSchema(
|
||||
total_consultores=estatisticas.get("total_consultores", 0),
|
||||
@@ -333,16 +342,13 @@ async def processar_ranking(
|
||||
@router.get("/ranking/posicao/{id_pessoa}", response_model=PosicaoRankingSchema)
|
||||
async def obter_posicao_ranking(
|
||||
id_pessoa: int,
|
||||
store = Depends(get_ranking_store),
|
||||
oracle_repo = Depends(get_ranking_oracle_repo),
|
||||
):
|
||||
if not store.is_ready():
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.",
|
||||
)
|
||||
if not oracle_repo:
|
||||
raise HTTPException(status_code=503, detail="Oracle não configurado")
|
||||
|
||||
entry = store.get_by_id(id_pessoa)
|
||||
total = store.total()
|
||||
total = oracle_repo.contar_total()
|
||||
entry = oracle_repo.buscar_por_id(id_pessoa)
|
||||
|
||||
if not entry:
|
||||
return PosicaoRankingSchema(
|
||||
@@ -365,10 +371,10 @@ async def obter_posicao_ranking(
|
||||
posicao=entry.posicao,
|
||||
total_consultores=total,
|
||||
pontuacao_total=float(entry.pontuacao_total),
|
||||
bloco_a=float(entry.bloco_a),
|
||||
bloco_b=float(entry.bloco_b),
|
||||
bloco_c=float(entry.bloco_c),
|
||||
bloco_d=float(entry.bloco_d),
|
||||
bloco_a=float(entry.componente_a),
|
||||
bloco_b=float(entry.componente_b),
|
||||
bloco_c=float(entry.componente_c),
|
||||
bloco_d=float(entry.componente_d),
|
||||
ativo=entry.ativo,
|
||||
encontrado=True,
|
||||
)
|
||||
@@ -531,14 +537,14 @@ async def sugerir_consultores(
|
||||
|
||||
for area in dados.get("areaConhecimentoPos", []):
|
||||
if area.get("nome"):
|
||||
area_nome = html.unescape(area["nome"])
|
||||
area_nome = corrigir_encoding(html.unescape(area["nome"]))
|
||||
areas_conhecimento.add(area_nome)
|
||||
area_norm = normalizar_texto(area["nome"])
|
||||
if tema_norm in area_norm or any(p in area_norm for p in tema_palavras if len(p) > 3):
|
||||
motivos_match.add(f"Area: {area_nome}")
|
||||
area_aval = area.get("areaAvaliacao", {})
|
||||
if area_aval and area_aval.get("nome"):
|
||||
aval_nome = html.unescape(area_aval["nome"])
|
||||
aval_nome = corrigir_encoding(html.unescape(area_aval["nome"]))
|
||||
areas_avaliacao.add(aval_nome)
|
||||
aval_norm = normalizar_texto(area_aval["nome"])
|
||||
if tema_norm in aval_norm or any(p in aval_norm for p in tema_palavras if len(p) > 3):
|
||||
@@ -546,7 +552,7 @@ async def sugerir_consultores(
|
||||
|
||||
for pesq in dados.get("areaPesquisa", []):
|
||||
if pesq.get("descricao"):
|
||||
pesq_desc = html.unescape(pesq["descricao"])
|
||||
pesq_desc = corrigir_encoding(html.unescape(pesq["descricao"]))
|
||||
linhas_pesquisa.add(pesq_desc)
|
||||
pesq_norm = normalizar_texto(pesq["descricao"])
|
||||
if tema_norm in pesq_norm or any(p in pesq_norm for p in tema_palavras if len(p) > 3):
|
||||
|
||||
Reference in New Issue
Block a user