# 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 ```json { "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 ```sql 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` ```python # 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) ```python 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) ```python 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) ```python 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python 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 |