Loader ESCO
Índice
O que é
O loader-esco é um serviço one-shot responsável por popular o Neo4j com a ontologia ESCO. É executado uma única vez na inicialização do sistema: importa todos os nós de habilidades, gera seus embeddings semânticos via Ollama e constrói as relações hierárquicas entre eles. Ao concluir, encerra automaticamente.
CSVs da ESCO → Parsers → OllamaEmbedder → RPC connector-neo4j → Neo4j
A ingestão é pré-condição para que o Entity Linker funcione: sem os nós (:Skill) indexados com embeddings, o KNN semântico não tem base de busca.
Como Funciona
Fluxo de Funcionamento
Eventos Consumidos e Produzidos
O loader-esco não consome eventos — publica diretamente na fila RPC do connector-neo4j. É o único serviço da arquitetura que não é acionado por eventos de outros serviços.
| Tipo | Evento | Quando |
|---|---|---|
| Produzido | GraphMergeSkill | Para cada nó (:Skill) ou grupo criado/atualizado |
| Produzido | GraphCreateRelationship | Para cada relação BROADER, NARROWER ou lateral |
| Produzido | GraphQuery | Query Cypher livre para a fase de inferência de tipo |
Decisões Abordadas
Serviço one-shot com sys.exit(0) ao concluir: a ontologia ESCO é estática (dataset de 2023) e precisa ser carregada apenas uma vez. Modelar como serviço one-shot em vez de um script avulso permite que o carregamento seja orquestrado pelo Docker Compose com depends_on e que logs e rastreabilidade sigam o mesmo padrão dos demais serviços.
MERGE idempotente com uri como chave: todos os nós são criados via MERGE, não CREATE. Isso garante que reexecutar o loader (por exemplo, após uma falha parcial) não crie duplicatas. O serviço pode ser rodado mais de uma vez com segurança.
Inferência de tipo dos grupos por votação de maioria: grupos temáticos da ESCO (ex.: "Habilidades de TIC") não têm tipo explícito no CSV. Em vez de marcar todos como "unknown" permanentemente, o loader executa uma query Cypher que percorre os descendentes de cada grupo e atribui o tipo mais frequente entre eles. Isso garante que o tipo dos grupos reflita o conteúdo real da subárvore ontológica.
Fases da Ingestão
O serviço executa 4 fases em sequência. Qualquer falha ou sinal de parada interrompe a pipeline.
1. Skills (_load_skills)
Lê o arquivo digitalSkillsCollection_pt.csv e, para cada linha, gera um embedding via Ollama e publica um evento GraphMergeSkill para o connector-neo4j. O parser classifica cada entrada em um dos três tipos antes de persistir:
skillType (ESCO) | reuseLevel | Tipo no grafo |
|---|---|---|
knowledge | — | knowledge |
skill/competence | transversal | soft |
skill/competence | sector-specific, occupation-specific, cross-sector | hard |
| Outros | — | Descartado |
Apenas digitalSkillsCollection_pt.csv está ativo na configuração atual. O arquivo skills_pt.csv (ontologia completa) está comentado em datasetconfig.py — a coleção de habilidades digitais é suficiente para o escopo do TCC.
2. Grupos (_load_groups)
Processa dois datasets em sequência:
skillGroups_pt.csv— cria nós(:Skill)do tipo"unknown"representando os grupos temáticos da ESCO (ex.: "Habilidades de TIC"). O tipo é definido na Fase 4.skillsHierarchy_pt.csv— cria relações bidirecionaisBROADEReNARROWERentre os níveis hierárquicos (0 a 3) da ontologia, ligando skills a seus grupos.
3. Relações (_load_relations)
Cria duas categorias de relações entre skills:
| Dataset | Relações criadas |
|---|---|
broaderRelationsSkillPillar_pt.csv | BROADER / NARROWER entre skills e o pilar de skills da ESCO |
skillSkillRelations_pt.csv | Relações do tipo original do CSV (ex.: ESSENTIAL_FOR, OPTIONAL_FOR) |
4. Inferência de Tipo dos Grupos (_update_skill_groups_type)
Executa uma query Cypher diretamente no Neo4j que corrige os grupos com tipo "unknown":
MATCH (g:Skill {type: 'unknown'})<-[:BROADER*1..]-(s:Skill)
WHERE s.type <> 'unknown'
WITH g, s.type AS stype, count(*) AS cnt
ORDER BY cnt DESC
WITH g, collect(stype)[0] AS majority_type
SET g.type = majority_type
Cada grupo recebe o tipo mais frequente entre os seus descendentes diretos e indiretos. Um grupo de habilidades de programação, por exemplo, recebe "hard" porque a maioria dos seus filhos tem esse tipo.
Geração de Embeddings
O OllamaEmbedder encapsula chamadas à API do Ollama usando o modelo configurado em settings.ollama_embedding (por padrão, embeddinggemma). O texto de entrada segue o formato raw merged newlines (ADR-043):
f"{preferred_label}\n {description}\n {alt_labels}"
Prefixos como "Skill:" ou "Descrição:" são evitados deliberadamente para não introduzir ruído lexical — o modelo opera sobre o conteúdo semântico puro das skills, que estão escritas em português europeu.
Os embeddings têm 768 dimensões e são armazenados como propriedade embedding no nó (:Skill), indexados via HNSW (Hierarchical Navigable Small World) para busca aproximada eficiente.
Arquitetura de Parsers
Cada dataset da ESCO tem estrutura CSV diferente. O serviço usa o Strategy Pattern com uma classe base abstrata DatasetStrategy para isolar a lógica de leitura de cada arquivo:
Os parsers processam os CSVs em chunks de 500 linhas via pandas, evitando carregar arquivos inteiros em memória.
O GraphMergeSkill usa MERGE no Neo4j com a uri como chave — reexecutar o serviço não cria duplicatas. Relações também são criadas com MERGE. É seguro rodar o loader-esco mais de uma vez.
ADR's Relacionadas
| ADR | Data | Decisão |
|---|---|---|
| ADR-043 | 2026 | Formato de embedding "raw merged newlines" ({label}\n{description}\n{alt_labels}) sem prefixos para evitar ruído lexical |