refactor: Otimização da query ES e melhorias na UI

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
This commit is contained in:
Frederico Castro
2025-12-09 05:01:32 -03:00
parent 9e6ba459a8
commit d237097913
6 changed files with 375 additions and 43 deletions

View File

@@ -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}")

View File

@@ -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
)