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.

TipoEventoQuando
ProduzidoGraphMergeSkillPara cada nó (:Skill) ou grupo criado/atualizado
ProduzidoGraphCreateRelationshipPara cada relação BROADER, NARROWER ou lateral
ProduzidoGraphQueryQuery 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)reuseLevelTipo no grafo
knowledgeknowledge
skill/competencetransversalsoft
skill/competencesector-specific, occupation-specific, cross-sectorhard
OutrosDescartado
Foco em Digital Skills

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 bidirecionais BROADER e NARROWER entre 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:

DatasetRelações criadas
broaderRelationsSkillPillar_pt.csvBROADER / NARROWER entre skills e o pilar de skills da ESCO
skillSkillRelations_pt.csvRelaçõ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.

Serviço idempotente

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

ADRDataDecisão
ADR-0432026Formato de embedding "raw merged newlines" ({label}\n{description}\n{alt_labels}) sem prefixos para evitar ruído lexical