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:
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ const ConsultorCard = ({ consultor }) => {
|
||||
</>
|
||||
)}
|
||||
<div className="stat">
|
||||
<div className="score-value">{consultor.pontuacao_total}</div>
|
||||
<div className="score-value">{pontuacao.pontuacao_total}</div>
|
||||
<div className="stat-label">Score</div>
|
||||
</div>
|
||||
<div className="expand-icon">{expanded ? '▲' : '▼'}</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,44 +20,65 @@ const Header = ({ total }) => {
|
||||
<h3>Componentes de Pontuação</h3>
|
||||
<div className="criteria-grid">
|
||||
<div className="criteria-section">
|
||||
<h4>A - Coordenação CAPES (máx 450 pts)</h4>
|
||||
<ul>
|
||||
<li>CA: 200 base + 100 tempo + 100 áreas + 20 retorno + 30 bônus</li>
|
||||
<li>CAJ: 150 base + 80 tempo + 100 áreas + 20 retorno + 20 bônus</li>
|
||||
<li>CAJ-MP: 120 base + 60 tempo + 100 áreas + 20 retorno + 15 bônus</li>
|
||||
<li>CAM: 100 base + 50 tempo + 100 áreas + 20 retorno + 10 bônus</li>
|
||||
</ul>
|
||||
<h4>A - Coordenação CAPES</h4>
|
||||
<span className="max-pts">máx 450 pts</span>
|
||||
<table className="criteria-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tipo</th>
|
||||
<th>Base</th>
|
||||
<th>Tempo</th>
|
||||
<th>Bônus</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>CA</td><td>200</td><td>até 100</td><td>30</td></tr>
|
||||
<tr><td>CAJ</td><td>150</td><td>até 80</td><td>20</td></tr>
|
||||
<tr><td>CAJ-MP</td><td>120</td><td>até 60</td><td>15</td></tr>
|
||||
<tr><td>CAM</td><td>100</td><td>até 50</td><td>10</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="criteria-note">+ Áreas (até 100) + Retorno (20)</div>
|
||||
</div>
|
||||
|
||||
<div className="criteria-section">
|
||||
<h4>B - Coordenação Programa PPG (máx 180 pts)</h4>
|
||||
<ul>
|
||||
<li>Base: 70 pts</li>
|
||||
<li>Tempo: 5 pts/ano (máx 50 pts)</li>
|
||||
<li>Programas extras: 20 pts/programa (máx 40 pts)</li>
|
||||
<li>Bônus ativo: 20 pts</li>
|
||||
</ul>
|
||||
<h4>B - Coordenação PPG</h4>
|
||||
<span className="max-pts">máx 180 pts</span>
|
||||
<table className="criteria-table">
|
||||
<tbody>
|
||||
<tr><td>Base</td><td className="pts-value">70 pts</td></tr>
|
||||
<tr><td>Tempo</td><td className="pts-value">5 pts/ano (máx 50)</td></tr>
|
||||
<tr><td>Programas extras</td><td className="pts-value">20 pts/prog (máx 40)</td></tr>
|
||||
<tr><td>Bônus ativo</td><td className="pts-value">20 pts</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="criteria-section">
|
||||
<h4>C - Consultoria (máx 230 pts)</h4>
|
||||
<ul>
|
||||
<li>Ativo: 150 pts base | Histórico: 100 pts base</li>
|
||||
<li>Tempo: 5 pts/ano (máx 50 pts)</li>
|
||||
<li>Eventos: 2 pts/evento (máx 20 pts)</li>
|
||||
<li>Responsável: 5 pts/vez (máx 25 pts)</li>
|
||||
<li>Áreas extras: 10 pts/área (máx 30 pts)</li>
|
||||
</ul>
|
||||
<h4>C - Consultoria</h4>
|
||||
<span className="max-pts">máx 230 pts</span>
|
||||
<table className="criteria-table">
|
||||
<tbody>
|
||||
<tr><td>Base (ativo)</td><td className="pts-value">150 pts</td></tr>
|
||||
<tr><td>Base (histórico)</td><td className="pts-value">100 pts</td></tr>
|
||||
<tr><td>Tempo</td><td className="pts-value">5 pts/ano (máx 50)</td></tr>
|
||||
<tr><td>Eventos</td><td className="pts-value">2 pts/ev (máx 20)</td></tr>
|
||||
<tr><td>Responsável</td><td className="pts-value">5 pts/vez (máx 25)</td></tr>
|
||||
<tr><td>Áreas extras</td><td className="pts-value">10 pts/área (máx 30)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="criteria-section">
|
||||
<h4>D - Premiações (máx 180 pts)</h4>
|
||||
<ul>
|
||||
<li>Premiação: 60 pts</li>
|
||||
<li>Avaliação: 40 pts</li>
|
||||
<li>Inscrição: 20 pts</li>
|
||||
<li>Total máximo: 180 pts</li>
|
||||
</ul>
|
||||
<h4>D - Premiações</h4>
|
||||
<span className="max-pts">máx 180 pts</span>
|
||||
<table className="criteria-table">
|
||||
<tbody>
|
||||
<tr><td>Premiação</td><td className="pts-value">60 pts</td></tr>
|
||||
<tr><td>Avaliação</td><td className="pts-value">40 pts</td></tr>
|
||||
<tr><td>Inscrição</td><td className="pts-value">20 pts</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ const api = axios.create({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 180000,
|
||||
});
|
||||
|
||||
export const rankingService = {
|
||||
|
||||
Reference in New Issue
Block a user