fix(ontology): retain data properties for normalized class names (#1171)

* fix(ontology): retain properties for normalized class names

* perf(ontology): precompute normalized class lookup
This commit is contained in:
Guofang.Tang
2026-08-27 13:32:14 +05:00
committed by GitHub
parent 5d54919804
commit 23baf21d5a
2 changed files with 84 additions and 12 deletions
+43 -12
View File
@@ -197,23 +197,22 @@ class PropertyGenerator:
self, entities: List[Dict[str, Any]], classes: List[Dict[str, Any]], **options self, entities: List[Dict[str, Any]], classes: List[Dict[str, Any]], **options
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Infer data properties from entity attributes.""" """Infer data properties from entity attributes."""
# Group entities by type # Group entities by their inferred class so normalized class names remain
entity_types = defaultdict(list) # aligned with the class definitions emitted by ClassInferrer.
class_entities = defaultdict(list)
class_lookup = self._build_class_type_lookup(classes)
for entity in entities: for entity in entities:
entity_type = entity.get("type") or entity.get("entity_type", "Entity") entity_type = entity.get("type") or entity.get("entity_type", "Entity")
entity_types[entity_type].append(entity) class_def = self._find_class_for_entity_type(entity_type, class_lookup)
if not class_def:
continue
class_name = class_def["name"]
class_entities[class_name].append(entity)
# Extract data properties for each class # Extract data properties for each class
properties = [] properties = []
for entity_type, type_entities in entity_types.items(): for class_name, type_entities in class_entities.items():
# Find corresponding class
class_def = next(
(cls for cls in classes if cls["name"] == entity_type), None
)
if not class_def:
continue
# Extract data properties # Extract data properties
data_props = self._extract_data_properties(type_entities) data_props = self._extract_data_properties(type_entities)
@@ -233,7 +232,7 @@ class PropertyGenerator:
else None, else None,
"label": normalized_name, "label": normalized_name,
"comment": f"Data property for {prop_name}", "comment": f"Data property for {prop_name}",
"domain": [entity_type], "domain": [class_name],
"range": prop_type, "range": prop_type,
"metadata": {"inferred_from": prop_name}, "metadata": {"inferred_from": prop_name},
} }
@@ -242,6 +241,38 @@ class PropertyGenerator:
return properties return properties
def _build_class_type_lookup(
self, classes: List[Dict[str, Any]]
) -> Dict[str, Dict[str, Any]]:
"""Build a lookup for raw, normalized, and recorded source type names."""
lookup: Dict[str, Dict[str, Any]] = {}
for class_def in classes:
class_name = class_def.get("name")
if class_name:
lookup.setdefault(str(class_name), class_def)
lookup.setdefault(
self.naming_conventions.normalize_class_name(str(class_name)),
class_def,
)
inferred_from = class_def.get("metadata", {}).get("inferred_from")
if inferred_from is not None:
lookup.setdefault(str(inferred_from), class_def)
lookup.setdefault(
self.naming_conventions.normalize_class_name(str(inferred_from)),
class_def,
)
return lookup
def _find_class_for_entity_type(
self, entity_type: Any, class_lookup: Dict[str, Dict[str, Any]]
) -> Optional[Dict[str, Any]]:
"""Find a class using the precomputed type lookup."""
raw_type = str(entity_type)
normalized_type = self.naming_conventions.normalize_class_name(raw_type)
return class_lookup.get(raw_type) or class_lookup.get(normalized_type)
def _extract_data_properties( def _extract_data_properties(
self, entities: List[Dict[str, Any]] self, entities: List[Dict[str, Any]]
) -> Dict[str, str]: ) -> Dict[str, str]:
@@ -0,0 +1,41 @@
from semantica.ontology.class_inferrer import ClassInferrer
from semantica.ontology.ontology_generator import OntologyGenerator
from semantica.ontology.property_generator import PropertyGenerator
def _entities():
return [
{
"id": "e1",
"type": "software engineer",
"name": "Alice",
"email": "alice@example.org",
},
{
"id": "e2",
"type": "software engineer",
"name": "Bob",
"email": "bob@example.org",
},
]
def test_property_generator_matches_normalized_class_names():
entities = _entities()
classes = ClassInferrer().infer_classes(entities)
properties = PropertyGenerator().infer_properties(entities, [], classes)
email = next(prop for prop in properties if prop["name"] == "email")
assert email["domain"] == ["SoftwareEngineer"]
assert email["range"] == "xsd:string"
def test_ontology_pipeline_emits_data_properties_for_normalized_types():
ontology = OntologyGenerator().generate_ontology(
{"entities": _entities(), "relationships": []}
)
email = next(prop for prop in ontology["properties"] if prop["name"] == "email")
assert email["domain"] == ["SoftwareEngineer"]
assert email["range"] == "xsd:string"