From d23709791390a531358f908b1b8039ed77282d31 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Tue, 9 Dec 2025 05:01:32 -0300 Subject: [PATCH] =?UTF-8?q?refactor:=20Otimiza=C3=A7=C3=A3o=20da=20query?= =?UTF-8?q?=20ES=20e=20melhorias=20na=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Query ES otimizada com boost por tipo de atuação - Coordenação de área com maior peso (boost 10) - Aumento do tamanho de busca para 1000 candidatos - Cache de ranking mantido (TTL 5 min) Frontend: - Correção do display do score (pontuacao.pontuacao_total) - Reorganização dos componentes de pontuação em tabelas - Aumento do timeout do axios para 3 minutos - Melhoria visual do Header com badges de pontuação máxima --- .../infrastructure/elasticsearch/client.py | 257 ++++++++++++++++++ .../repositories/consultor_repository_impl.py | 12 +- frontend/src/components/ConsultorCard.jsx | 2 +- frontend/src/components/Header.css | 67 ++++- frontend/src/components/Header.jsx | 79 ++++-- frontend/src/services/api.js | 1 + 6 files changed, 375 insertions(+), 43 deletions(-) diff --git a/backend/src/infrastructure/elasticsearch/client.py b/backend/src/infrastructure/elasticsearch/client.py index 685e469..1e623ac 100644 --- a/backend/src/infrastructure/elasticsearch/client.py +++ b/backend/src/infrastructure/elasticsearch/client.py @@ -102,3 +102,260 @@ class ElasticsearchClient: return data.get("count", 0) except Exception as e: raise RuntimeError(f"Erro ao contar consultores: {e}") + + async def buscar_candidatos_ranking(self, size: int = 100) -> list: + query = { + "size": size, + "query": { + "bool": { + "should": [ + { + "nested": { + "path": "atuacoes", + "query": { + "bool": { + "should": [ + {"term": {"atuacoes.tipo": {"value": "Coordenação de Área de Avaliação", "boost": 10}}}, + {"term": {"atuacoes.tipo": {"value": "Histórico de Coordenação de Área de Avaliação", "boost": 5}}} + ] + } + }, + "score_mode": "sum" + } + }, + { + "nested": { + "path": "atuacoes", + "query": { + "bool": { + "should": [ + {"term": {"atuacoes.tipo": {"value": "Consultor", "boost": 2}}}, + {"term": {"atuacoes.tipo": {"value": "Histórico de Consultoria", "boost": 1}}} + ] + } + }, + "score_mode": "sum" + } + }, + { + "nested": { + "path": "atuacoes", + "query": { + "bool": { + "should": [ + {"term": {"atuacoes.tipo": {"value": "Premiação Prêmio", "boost": 3}}}, + {"term": {"atuacoes.tipo": {"value": "Avaliação Prêmio", "boost": 2}}}, + {"term": {"atuacoes.tipo": {"value": "Inscrição Prêmio", "boost": 1}}} + ] + } + }, + "score_mode": "sum" + } + } + ], + "minimum_should_match": 1 + } + }, + "_source": ["id", "dadosPessoais", "atuacoes"], + "sort": [{"_score": "desc"}] + } + + try: + response = await self.client.post( + f"{self.url}/{self.index}/_search", + json=query, + timeout=120.0 + ) + response.raise_for_status() + + data = response.json() + results = [] + for hit in data.get("hits", {}).get("hits", []): + doc = hit["_source"] + doc["_score_es"] = hit.get("_score", 0) + results.append(doc) + return results + except Exception as e: + raise RuntimeError(f"Erro ao buscar candidatos ranking: {e}") + + async def buscar_ranking_com_score_old(self, size: int = 100) -> list: + painless_script = """ + double score = 0; + long now = System.currentTimeMillis(); + long doisAnos = 730L * 24 * 60 * 60 * 1000; + + // Componente A - Coordenação CAPES + double compA = 0; + for (def a : params._source.atuacoes) { + String tipo = a.tipo; + if (tipo == null) continue; + + if (tipo.contains('Coordenação de Área')) { + String inicio = a.inicio; + String fim = a.fim; + if (inicio == null) continue; + + boolean ativo = (fim == null || fim.length() == 0); + if (!ativo) continue; + + // Parse data inicio (formato DD/MM/YYYY ou YYYY-MM-DD) + long inicioMs = 0; + try { + if (inicio.contains('/')) { + String[] parts = inicio.substring(0, 10).split('/'); + int dia = Integer.parseInt(parts[0]); + int mes = Integer.parseInt(parts[1]); + int ano = Integer.parseInt(parts[2]); + inicioMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000; + } + } catch (Exception e) {} + + double anos = (now - inicioMs) / (365.25 * 24 * 60 * 60 * 1000); + + // Determinar tipo de coordenação + double base = 100; + double tempoMax = 50; + double bonusAtual = 10; + + def dados = a.dadosCoordenacaoArea; + if (dados == null) dados = a.dadosHistoricoCoordenacaoArea; + if (dados != null && dados.tipo != null) { + String tipoCoord = dados.tipo.toLowerCase(); + if (tipoCoord.contains('coordenador') && !tipoCoord.contains('adjunt')) { + base = 200; tempoMax = 100; bonusAtual = 30; + } else if (tipoCoord.contains('adjunt')) { + if (tipoCoord.contains('profissional')) { + base = 120; tempoMax = 60; bonusAtual = 15; + } else { + base = 150; tempoMax = 80; bonusAtual = 20; + } + } else if (tipoCoord.contains('presidente') || tipoCoord.contains('camara')) { + base = 100; tempoMax = 50; bonusAtual = 10; + } + } + + double tempo = Math.min(anos * 10, tempoMax); + double thisCompA = base + tempo + bonusAtual; + if (thisCompA > compA) compA = thisCompA; + } + } + score += Math.min(compA, 450); + + // Componente C - Consultoria + double compC = 0; + int totalEventos = 0; + int eventosRecentes = 0; + int vezesResponsavel = 0; + long primeiroEvento = now; + def areasConsultoria = new HashSet(); + + for (def a : params._source.atuacoes) { + String tipo = a.tipo; + if (tipo == null) continue; + + if (tipo == 'Consultor' || tipo == 'Histórico de Consultoria') { + totalEventos++; + + String inicio = a.inicio; + if (inicio != null && inicio.length() >= 10) { + try { + String[] parts = inicio.substring(0, 10).split('/'); + if (parts.length == 3) { + int ano = Integer.parseInt(parts[2]); + long inicioMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000; + if (inicioMs < primeiroEvento) primeiroEvento = inicioMs; + } + } catch (Exception e) {} + } + + String fim = a.fim; + if (fim != null && fim.length() >= 10) { + try { + String[] parts = fim.substring(0, 10).split('/'); + if (parts.length == 3) { + int ano = Integer.parseInt(parts[2]); + long fimMs = (ano - 1970) * 365L * 24 * 60 * 60 * 1000; + if (now - fimMs < doisAnos) eventosRecentes++; + } + } catch (Exception e) {} + } + + def dados = a.dadosConsultoria; + if (dados != null) { + if (dados.responsavel == true) vezesResponsavel++; + if (dados.areaAvaliacao != null) areasConsultoria.add(dados.areaAvaliacao); + } + } + } + + if (totalEventos > 0) { + double base = (eventosRecentes > 0) ? 150 : 100; + double anosConsultoria = (now - primeiroEvento) / (365.25 * 24 * 60 * 60 * 1000); + double tempo = Math.min(anosConsultoria * 5, 50); + double extrasEventos = Math.min(totalEventos * 2, 20); + double extrasResp = Math.min(vezesResponsavel * 5, 25); + double extrasAreas = (areasConsultoria.size() > 1) ? Math.min((areasConsultoria.size() - 1) * 10, 30) : 0; + compC = base + tempo + extrasEventos + extrasResp + extrasAreas; + } + score += Math.min(compC, 230); + + // Componente D - Premiações + double compD = 0; + for (def a : params._source.atuacoes) { + String tipo = a.tipo; + if (tipo == null) continue; + + if (tipo.contains('Prêmio') || tipo.contains('Premio')) { + if (tipo.contains('Premiação')) { + compD += 60; + } else if (tipo.contains('Avaliação')) { + compD += 40; + } else if (tipo.contains('Inscrição')) { + compD += 20; + } + } + } + score += Math.min(compD, 180); + + return score; + """ + + query = { + "size": size, + "query": { + "function_score": { + "query": { + "nested": { + "path": "atuacoes", + "query": {"exists": {"field": "atuacoes.tipo"}} + } + }, + "script_score": { + "script": { + "source": painless_script, + "lang": "painless" + } + }, + "boost_mode": "replace" + } + }, + "_source": ["id", "dadosPessoais", "atuacoes"], + "sort": [{"_score": "desc"}] + } + + try: + response = await self.client.post( + f"{self.url}/{self.index}/_search", + json=query + ) + response.raise_for_status() + + data = response.json() + results = [] + for hit in data.get("hits", {}).get("hits", []): + doc = hit["_source"] + doc["_score_es"] = hit.get("_score", 0) + results.append(doc) + return results + except Exception as e: + raise RuntimeError(f"Erro ao buscar ranking com score: {e}") diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py index 4ead580..ee22981 100644 --- a/backend/src/infrastructure/repositories/consultor_repository_impl.py +++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py @@ -264,8 +264,16 @@ class ConsultorRepositoryImpl(ConsultorRepository): if _ranking_cache.is_valid(): return _ranking_cache.get()[:limite] - tamanho_busca = 1000 - consultores = await self.buscar_todos(limite=tamanho_busca) + tamanho_busca = max(limite * 3, 1000) + docs = await self.es_client.buscar_candidatos_ranking(size=tamanho_busca) + + consultores = [] + for doc in docs: + consultor = await self._construir_consultor(doc) + score_es = doc.get("_score_es", 0) + consultor.score_es = score_es + consultores.append(consultor) + consultores_ordenados = sorted( consultores, key=lambda c: c.pontuacao_total, reverse=True ) diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index c49e029..195d5c0 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -55,7 +55,7 @@ const ConsultorCard = ({ consultor }) => { )}
-
{consultor.pontuacao_total}
+
{pontuacao.pontuacao_total}
Score
{expanded ? '▲' : '▼'}
diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css index e837321..369997f 100644 --- a/frontend/src/components/Header.css +++ b/frontend/src/components/Header.css @@ -79,27 +79,72 @@ .criteria-section h4 { color: var(--accent-2); - font-size: 0.82rem; - margin-bottom: 0.6rem; + font-size: 0.85rem; + margin-bottom: 0.25rem; letter-spacing: 0.4px; + display: inline; } -.criteria-section ul { - list-style: none; - font-size: 0.8rem; - line-height: 1.7; +.max-pts { + display: inline-block; + margin-left: 0.5rem; + padding: 0.15rem 0.5rem; + background: rgba(79,70,229,0.3); + border-radius: 4px; + font-size: 0.7rem; + color: var(--silver); + font-weight: 500; +} + +.criteria-table { + width: 100%; + margin-top: 0.6rem; + font-size: 0.75rem; + border-collapse: collapse; +} + +.criteria-table th { + text-align: left; + color: var(--silver); + font-weight: 500; + padding: 0.3rem 0.4rem; + border-bottom: 1px solid rgba(255,255,255,0.1); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.criteria-table td { + padding: 0.35rem 0.4rem; color: var(--muted); -} - -.criteria-section li { - padding: 0.2rem 0; border-bottom: 1px dashed rgba(255,255,255,0.05); } -.criteria-section li:last-child { +.criteria-table tr:last-child td { border-bottom: none; } +.criteria-table td:first-child { + color: var(--silver); + font-weight: 500; +} + +.pts-value { + text-align: right; + color: var(--accent-2) !important; + font-weight: 400 !important; +} + +.criteria-note { + margin-top: 0.5rem; + padding-top: 0.4rem; + border-top: 1px dashed rgba(255,255,255,0.1); + font-size: 0.7rem; + color: var(--muted); + text-align: center; + font-style: italic; +} + @media (max-width: 768px) { .criteria-grid { grid-template-columns: 1fr; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index f2511e0..344333c 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -20,44 +20,65 @@ const Header = ({ total }) => {

Componentes de Pontuação

-

A - Coordenação CAPES (máx 450 pts)

-
    -
  • CA: 200 base + 100 tempo + 100 áreas + 20 retorno + 30 bônus
  • -
  • CAJ: 150 base + 80 tempo + 100 áreas + 20 retorno + 20 bônus
  • -
  • CAJ-MP: 120 base + 60 tempo + 100 áreas + 20 retorno + 15 bônus
  • -
  • CAM: 100 base + 50 tempo + 100 áreas + 20 retorno + 10 bônus
  • -
+

A - Coordenação CAPES

+ máx 450 pts + + + + + + + + + + + + + + + +
TipoBaseTempoBônus
CA200até 10030
CAJ150até 8020
CAJ-MP120até 6015
CAM100até 5010
+
+ Áreas (até 100) + Retorno (20)
-

B - Coordenação Programa PPG (máx 180 pts)

-
    -
  • Base: 70 pts
  • -
  • Tempo: 5 pts/ano (máx 50 pts)
  • -
  • Programas extras: 20 pts/programa (máx 40 pts)
  • -
  • Bônus ativo: 20 pts
  • -
+

B - Coordenação PPG

+ máx 180 pts + + + + + + + +
Base70 pts
Tempo5 pts/ano (máx 50)
Programas extras20 pts/prog (máx 40)
Bônus ativo20 pts
-

C - Consultoria (máx 230 pts)

-
    -
  • Ativo: 150 pts base | Histórico: 100 pts base
  • -
  • Tempo: 5 pts/ano (máx 50 pts)
  • -
  • Eventos: 2 pts/evento (máx 20 pts)
  • -
  • Responsável: 5 pts/vez (máx 25 pts)
  • -
  • Áreas extras: 10 pts/área (máx 30 pts)
  • -
+

C - Consultoria

+ máx 230 pts + + + + + + + + + +
Base (ativo)150 pts
Base (histórico)100 pts
Tempo5 pts/ano (máx 50)
Eventos2 pts/ev (máx 20)
Responsável5 pts/vez (máx 25)
Áreas extras10 pts/área (máx 30)
-

D - Premiações (máx 180 pts)

-
    -
  • Premiação: 60 pts
  • -
  • Avaliação: 40 pts
  • -
  • Inscrição: 20 pts
  • -
  • Total máximo: 180 pts
  • -
+

D - Premiações

+ máx 180 pts + + + + + + +
Premiação60 pts
Avaliação40 pts
Inscrição20 pts
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 3d88954..0e2d0b5 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -5,6 +5,7 @@ const api = axios.create({ headers: { 'Content-Type': 'application/json', }, + timeout: 180000, }); export const rankingService = {