Files
ranking/docs/ranking-queries-implementadas.md
2025-12-13 09:40:40 -03:00

14 KiB

Queries Implementadas - Sistema de Ranking de Consultores CAPES

Documentação das queries efetivamente implementadas no sistema, com regras de pontuação aplicadas.

Visão Geral da Arquitetura

┌─────────────────────────────────────────────────────────────────────┐
│                        FLUXO DE DADOS                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. Elasticsearch (AtuaCAPES)                                       │
│     └─> Query com boost por tipo de atuação                         │
│     └─> Retorna top 1000 candidatos pré-ordenados                   │
│                                                                     │
│  2. Oracle - Opcional                                               │
│     └─> Busca coordenações de PPG por ID_PESSOA                     │
│                                                                     │
│  3. Python (Backend)                                                │
│     └─> Calcula pontuação completa (A + B + C + D)                  │
│     └─> Reordena por pontuação real                                 │
│     └─> Cache por 5 minutos                                         │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Query Principal - Elasticsearch

Objetivo

Buscar candidatos relevantes para o ranking, priorizando por tipo de atuação usando boost.

Query Implementada

{
  "size": 1000,
  "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"}]
}

Explicação dos Boosts

Tipo de Atuação Boost Justificativa
Coordenação de Área de Avaliação 10 Máxima pontuação (base 200 pts)
Histórico de Coordenação de Área 5 Alta pontuação histórica
Premiação Prêmio 3 60 pts por premiação
Consultor 2 Base 150 pts se ativo
Avaliação Prêmio 2 40 pts por avaliação
Histórico de Consultoria 1 Base 100 pts
Inscrição Prêmio 1 20 pts por inscrição

Estatísticas de Candidatos

Tipo Quantidade no ES
Coordenadores de área 522
Consultores 52.551
Premiações 63.799
Total com atuações relevantes 90.482

Query Oracle - Coordenação de Programa (PPG)

Objetivo

Buscar coordenações de programa por ID_PESSOA para calcular o Componente B.

Query Implementada

SELECT
    c.ID_PESSOA,
    c.ID_PROGRAMA_SNPG,
    p.NM_PROGRAMA,
    p.CD_PROGRAMA_PPG,
    p.DS_CONCEITO AS NOTA_PPG,
    p.NM_PROGRAMA_MODALIDADE,
    aa.NM_AREA_AVALIACAO,
    c.DT_INICIO_VIGENCIA,
    c.DT_FIM_VIGENCIA
FROM SUCUPIRA_PAINEL.VM_COORDENADOR c
INNER JOIN SUCUPIRA_PAINEL.VM_PROGRAMA_SUCUPIRA p
    ON c.ID_PROGRAMA_SNPG = p.ID_PROGRAMA
LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_CONHECIMENTO ac
    ON p.ID_AREA_CONHECIMENTO_ATUAL = ac.ID_AREA_CONHECIMENTO
LEFT JOIN SUCUPIRA_PAINEL.VM_AREA_AVALIACAO aa
    ON ac.ID_AREA_AVALIACAO = aa.ID_AREA_AVALIACAO
WHERE c.ID_PESSOA = :id_pessoa
ORDER BY c.DT_INICIO_VIGENCIA

Campos Retornados

Campo Tipo Uso
ID_PROGRAMA_SNPG NUMBER Identificador único do programa
NM_PROGRAMA VARCHAR2 Nome do programa
CD_PROGRAMA_PPG VARCHAR2 Código do PPG
NOTA_PPG VARCHAR2 Nota CAPES (3-7, A)
NM_PROGRAMA_MODALIDADE VARCHAR2 Acadêmico/Profissional
NM_AREA_AVALIACAO VARCHAR2 Área de avaliação
DT_INICIO_VIGENCIA DATE Início da coordenação
DT_FIM_VIGENCIA DATE Fim (null = ativo)

Regras de Pontuação Implementadas

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

Arquivo: backend/src/domain/services/calculador_pontuacao.py

# Apenas coordenações ATIVAS pontuam
coord_atual = next((c for c in coordenacoes if c.periodo.ativo), None)
if not coord_atual:
    return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)

# Valores por tipo
base_map = {"CA": 200, "CAJ": 150, "CAJ-MP": 120, "CAM": 100}
tempo_max_map = {"CA": 100, "CAJ": 80, "CAJ-MP": 60, "CAM": 50}
bonus_atual_map = {"CA": 30, "CAJ": 20, "CAJ-MP": 15, "CAM": 10}

# Cálculo
base = base_map.get(coord_atual.tipo, 0)
anos = coord_atual.periodo.anos_decorridos
tempo = min(int(anos * 10), tempo_max_map.get(coord_atual.tipo, 0))
extras = min(len(coord_atual.areas_adicionais) * 20, 100)
bonus = bonus_atual_map.get(coord_atual.tipo, 0) if coord_atual.periodo.ativo else 0
retorno = 20 if coord_atual.ja_coordenou_antes else 0

Tabela de Pontuação:

Tipo Base Tempo (máx) Bônus Ativo Áreas (máx) Retorno
CA 200 100 (10 pts/ano) 30 100 20
CAJ 150 80 (10 pts/ano) 20 100 20
CAJ-MP 120 60 (10 pts/ano) 15 100 20
CAM 100 50 (10 pts/ano) 10 100 20

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

if not coordenacoes:
    return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)

base = 70
anos_totais = sum(c.periodo.anos_decorridos for c in coordenacoes)
tempo = min(int(anos_totais * 5), 50)

programas_distintos = len({c.id_programa for c in coordenacoes})
extras = min((programas_distintos - 1) * 20, 40)

coord_ativa = any(c.periodo.ativo for c in coordenacoes)
bonus = 20 if coord_ativa else 0

Tabela de Pontuação:

Critério Pontos Máximo
Base (ser coordenador) 70 70
Tempo 5 pts/ano 50
Programas extras 20 pts/programa 40
Bônus ativo 20 20
TOTAL - 180

Componente C - Consultoria (máx 230 pts)

if not consultoria:
    return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)

# Base depende de ter eventos recentes (últimos 2 anos)
base = 150 if consultoria.eventos_recentes > 0 else 100

# Tempo desde primeiro evento
anos = (datetime.now() - consultoria.primeiro_evento).days / 365.25
tempo = min(int(anos * 5), 50)

# Extras
extras_eventos = min(consultoria.total_eventos * 2, 20)
extras_responsavel = min(consultoria.vezes_responsavel * 5, 25)
extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0
extras = extras_eventos + extras_responsavel + extras_areas

Tabela de Pontuação:

Critério Pontos Máximo
Base (ativo/recente) 150 150
Base (histórico) 100 100
Tempo 5 pts/ano 50
Eventos 2 pts/evento 20
Responsável 5 pts/vez 25
Áreas extras 10 pts/área 30
TOTAL - 230

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

if not premiacoes:
    return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)

# Pontos por tipo de premiação
mapa = {
    "Premiação Prêmio": 60,
    "Avaliação Prêmio": 40,
    "Inscrição Prêmio": 20,
}

total_pontos = sum(mapa.get(p.tipo, 0) for p in premiacoes)
total_pontos = min(total_pontos, 180)  # Teto

return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0)

Tabela de Pontuação:

Tipo Pontos Descrição
Premiação Prêmio 60 Recebeu prêmio
Avaliação Prêmio 40 Participou de banca/comissão
Inscrição Prêmio 20 Inscreveu trabalho
Máximo total 180 -

Extração de Dados do Elasticsearch

Coordenações CAPES

def _extrair_coordenacoes_capes(self, atuacoes: List[Dict[str, Any]]) -> List[CoordenacaoCapes]:
    coordenacoes = [
        a for a in atuacoes
        if a.get("tipo") in [
            "Coordenação de Área de Avaliação",
            "Histórico de Coordenação de Área de Avaliação",
        ]
    ]

    # Inferir tipo pelo campo 'nome'
    def _inferir_tipo_coordenacao(coord):
        nome = coord.get("nome", "").lower()
        if "câmara" in nome or "camara" in nome:
            return "CAM"
        elif "mestrado profissional" in nome:
            return "CAJ-MP"
        elif "adjunta" in nome:
            return "CAJ"
        else:
            return "CA"

Consultoria

def _extrair_consultoria(self, atuacoes: List[Dict[str, Any]]) -> Optional[Consultoria]:
    consultorias = [
        a for a in atuacoes
        if a.get("tipo") in ["Consultor", "Histórico de Consultoria"]
    ]

    # Calcula eventos recentes (últimos 2 anos)
    limite_recente = datetime.now() - timedelta(days=730)
    eventos_recentes = sum(1 for d in datas_fim if d >= limite_recente)

    # Extrai áreas únicas
    areas = list({c.get("areaAvaliacao", "N/A") for c in consultorias if c.get("areaAvaliacao")})

    # Conta vezes como responsável
    vezes_responsavel = sum(1 for c in consultorias if c.get("responsavel", False))

Premiações

def _extrair_premiacoes(self, atuacoes: List[Dict[str, Any]]) -> List[Premiacao]:
    premiacoes_data = [
        a for a in atuacoes
        if a.get("tipo") in [
            "Premiação Prêmio",
            "Avaliação Prêmio",
            "Inscrição Prêmio",
        ]
    ]

    for prem in premiacoes_data:
        pontos = self._calcular_pontos_premiacao(prem.get("tipo", ""))
        # tipo vem de atuacoes.tipo
        # nome_premio vem de atuacoes.descricao
        # ano vem de atuacoes.inicio (parse para extrair ano)

Cache de Ranking

Implementação

class RankingCache:
    def __init__(self, ttl_seconds: int = 300):  # 5 minutos
        self.ttl = ttl_seconds
        self._cache: List[Consultor] = []
        self._last_update: Optional[datetime] = None
        self._loading = False
        self._lock = asyncio.Lock()

    def is_valid(self) -> bool:
        if not self._cache or not self._last_update:
            return False
        return (datetime.now() - self._last_update).total_seconds() < self.ttl

Performance

Métrica Valor
Primeira requisição (cold) ~1m34s
Requisições cacheadas ~0.27s
TTL do cache 5 minutos
Candidatos processados 1000

Fluxo Completo de Ranking

async def buscar_ranking(self, limite: int = 100) -> List[Consultor]:
    # 1. Verificar cache
    if _ranking_cache.is_valid():
        return _ranking_cache.get()[:limite]

    async with _ranking_cache._lock:
        # 2. Double-check cache
        if _ranking_cache.is_valid():
            return _ranking_cache.get()[:limite]

        # 3. Buscar candidatos do ES (ordenados por score ES)
        tamanho_busca = max(limite * 3, 1000)
        docs = await self.es_client.buscar_candidatos_ranking(size=tamanho_busca)

        # 4. Construir consultores (calcula pontuação completa)
        consultores = []
        for doc in docs:
            consultor = await self._construir_consultor(doc)
            # _construir_consultor busca dados do Oracle se disponível
            consultores.append(consultor)

        # 5. Reordenar por pontuação real
        consultores_ordenados = sorted(
            consultores, key=lambda c: c.pontuacao_total, reverse=True
        )

        # 6. Cachear resultado
        _ranking_cache.set(consultores_ordenados)

    return consultores_ordenados[:limite]

Limitações Conhecidas

  1. Cobertura de candidatos: Buscamos 1000 candidatos do ES. Se alguém com alta pontuação estiver fora do top 1000 do score ES, não aparecerá.

  2. Oracle opcional: Se Oracle não estiver disponível (TNS error no Docker), Componente B = 0 para todos.

  3. Tipos de coordenação: Inferência pelo campo nome pode não ser 100% precisa.

  4. Datas inconsistentes: ES usa formatos dd/MM/yyyy e yyyy-MM-dd misturados.


Arquivos de Implementação

Arquivo Responsabilidade
backend/src/infrastructure/elasticsearch/client.py Query ES com boost
backend/src/infrastructure/oracle/client.py Query Oracle PPG
backend/src/infrastructure/repositories/consultor_repository_impl.py Cache e orquestração
backend/src/domain/services/calculador_pontuacao.py Regras de pontuação
backend/src/domain/entities/consultor.py Entidades de domínio