diff --git a/cookbook/advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb b/cookbook/advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb index 3a2afcfd..392cdc28 100644 --- a/cookbook/advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb +++ b/cookbook/advanced/13_Manual_Ontology_Snowflake_Mapping.ipynb @@ -105,48 +105,7 @@ "id": "cell-4", "metadata": {}, "outputs": [], - "source": [ - "BASE_URI = \"https://example.com/hr/\"\n", - "\n", - "# Your ontology — designed by you, not inferred by Semantica.\n", - "ontology: Dict[str, Any] = {\n", - " \"name\": \"EmploymentDomainOntology\",\n", - " \"uri\": f\"{BASE_URI}EmploymentDomainOntology\",\n", - " \"namespace\": {\"base_uri\": BASE_URI},\n", - "\n", - " # You decide the class taxonomy\n", - " \"classes\": [\n", - " {\"name\": \"Person\", \"uri\": f\"{BASE_URI}Person\"},\n", - " {\"name\": \"Organization\", \"uri\": f\"{BASE_URI}Organization\"},\n", - " {\"name\": \"Role\", \"uri\": f\"{BASE_URI}Role\"},\n", - " # EmploymentEvent is a reification node.\n", - " # It connects Person + Organization + Role and carries salary/date context.\n", - " {\"name\": \"EmploymentEvent\", \"uri\": f\"{BASE_URI}EmploymentEvent\"},\n", - " ],\n", - "\n", - " # You decide every property — type, domain, range, cardinality\n", - " \"properties\": [\n", - " # Datatype properties\n", - " {\"name\": \"name\", \"type\": \"datatype\", \"domain\": \"Person\", \"range\": \"string\", \"required\": True},\n", - " {\"name\": \"legalName\", \"type\": \"datatype\", \"domain\": \"Organization\", \"range\": \"string\", \"required\": True},\n", - " {\"name\": \"title\", \"type\": \"datatype\", \"domain\": \"Role\", \"range\": \"string\", \"required\": True},\n", - " {\"name\": \"startDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n", - " {\"name\": \"endDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n", - " {\"name\": \"salary\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"decimal\"},\n", - "\n", - " # Object properties — reification spokes (required)\n", - " {\"name\": \"employee\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Person\", \"required\": True},\n", - " {\"name\": \"employer\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Organization\", \"required\": True},\n", - " {\"name\": \"role\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Role\", \"required\": True},\n", - "\n", - " # Shortcut edges — direct person→org / person→role without traversing the event node\n", - " {\"name\": \"worksFor\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Organization\"},\n", - " {\"name\": \"hasRole\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Role\"},\n", - " ],\n", - "}\n", - "\n", - "ontology" - ] + "source": "BASE_URI = \"https://example.com/hr/\"\n\n# Your ontology — designed by you, not inferred by Semantica.\nontology: Dict[str, Any] = {\n \"name\": \"EmploymentDomainOntology\",\n \"uri\": f\"{BASE_URI}EmploymentDomainOntology\",\n \"namespace\": {\"base_uri\": BASE_URI},\n\n # You decide the class taxonomy\n \"classes\": [\n {\"name\": \"Person\", \"uri\": f\"{BASE_URI}Person\"},\n {\"name\": \"Organization\", \"uri\": f\"{BASE_URI}Organization\"},\n {\"name\": \"Role\", \"uri\": f\"{BASE_URI}Role\"},\n # EmploymentEvent is a reification node.\n # It connects Person + Organization + Role and carries salary/date context.\n {\"name\": \"EmploymentEvent\", \"uri\": f\"{BASE_URI}EmploymentEvent\"},\n ],\n\n # Each property carries a full URI so TripletStore stores it as hr:\n # rather than the default urn:property:.\n # This ensures SPARQL queries using PREFIX hr: match what is actually stored.\n \"properties\": [\n # Datatype properties\n {\"name\": \"name\", \"uri\": f\"{BASE_URI}name\", \"type\": \"datatype\", \"domain\": \"Person\", \"range\": \"string\", \"required\": True},\n {\"name\": \"legalName\", \"uri\": f\"{BASE_URI}legalName\", \"type\": \"datatype\", \"domain\": \"Organization\", \"range\": \"string\", \"required\": True},\n {\"name\": \"title\", \"uri\": f\"{BASE_URI}title\", \"type\": \"datatype\", \"domain\": \"Role\", \"range\": \"string\", \"required\": True},\n {\"name\": \"startDate\", \"uri\": f\"{BASE_URI}startDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n {\"name\": \"endDate\", \"uri\": f\"{BASE_URI}endDate\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"date\"},\n {\"name\": \"salary\", \"uri\": f\"{BASE_URI}salary\", \"type\": \"datatype\", \"domain\": \"EmploymentEvent\", \"range\": \"decimal\"},\n\n # Object properties — reification spokes (required)\n {\"name\": \"employee\", \"uri\": f\"{BASE_URI}employee\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Person\", \"required\": True},\n {\"name\": \"employer\", \"uri\": f\"{BASE_URI}employer\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Organization\", \"required\": True},\n {\"name\": \"role\", \"uri\": f\"{BASE_URI}role\", \"type\": \"object\", \"domain\": \"EmploymentEvent\", \"range\": \"Role\", \"required\": True},\n\n # Shortcut edges — direct person→org / person→role without traversing the event node\n {\"name\": \"worksFor\", \"uri\": f\"{BASE_URI}worksFor\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Organization\"},\n {\"name\": \"hasRole\", \"uri\": f\"{BASE_URI}hasRole\", \"type\": \"object\", \"domain\": \"Person\", \"range\": \"Role\"},\n ],\n}\n\nontology" }, { "cell_type": "markdown", @@ -186,29 +145,7 @@ "id": "cell-6", "metadata": {}, "outputs": [], - "source": [ - "assoc_builder = AssociativeClassBuilder()\n", - "\n", - "employment_assoc = assoc_builder.create_associative_class(\n", - " name=\"EmploymentEvent\",\n", - " connects=[\"Person\", \"Organization\", \"Role\"],\n", - " temporal=True, # adds startDate / endDate handling\n", - " properties={\n", - " \"startDate\": \"xsd:date\",\n", - " \"endDate\": \"xsd:date\",\n", - " \"salary\": \"xsd:decimal\",\n", - " },\n", - ")\n", - "\n", - "validation_result = assoc_builder.validate_associative_class(employment_assoc)\n", - "\n", - "print(\"AssociativeClass structure:\")\n", - "print(f\" name: {employment_assoc.get('name')}\")\n", - "print(f\" connects: {employment_assoc.get('connects')}\")\n", - "print(f\" temporal: {employment_assoc.get('temporal')}\")\n", - "print(f\" properties: {list(employment_assoc.get('properties', {}).keys())}\")\n", - "print(f\"\\nValidation passed: {validation_result}\")" - ] + "source": "assoc_builder = AssociativeClassBuilder()\n\nemployment_assoc = assoc_builder.create_associative_class(\n name=\"EmploymentEvent\",\n connects=[\"Person\", \"Organization\", \"Role\"],\n temporal=True, # adds startDate / endDate handling\n properties={\n \"startDate\": \"xsd:date\",\n \"endDate\": \"xsd:date\",\n \"salary\": \"xsd:decimal\",\n },\n)\n\nvalidation_result = assoc_builder.validate_associative_class(employment_assoc)\n\n# AssociativeClass is a dataclass — use attribute access, not .get()\nprint(\"AssociativeClass structure:\")\nprint(f\" name: {employment_assoc.name}\")\nprint(f\" connects: {employment_assoc.connects}\")\nprint(f\" temporal: {employment_assoc.temporal}\")\nprint(f\" properties: {list(employment_assoc.properties.keys())}\")\nprint(f\"\\nValidation passed: {validation_result}\")" }, { "cell_type": "markdown", @@ -309,67 +246,7 @@ "id": "cell-10", "metadata": {}, "outputs": [], - "source": [ - "def map_rows_to_kg(rows: List[Dict[str, Any]]) -> Dict[str, Any]:\n", - " entities: Dict[str, Dict[str, Any]] = {}\n", - " relationships: List[Dict[str, Any]] = []\n", - "\n", - " for row in rows:\n", - " # Stable, deterministic node IDs derived from business keys\n", - " person_id = f\"person:{row['EMPLOYEE_ID']}\"\n", - " org_id = f\"org:{row['ORG_ID']}\"\n", - " role_id = f\"role:{row['ROLE_ID']}\"\n", - " # Event ID includes all three participants + start date so that\n", - " # a re-hired employee gets a distinct event node, not an overwrite.\n", - " event_id = f\"employment:{row['EMPLOYEE_ID']}:{row['ORG_ID']}:{row['START_DATE']}\"\n", - "\n", - " # Entities — \"type\" must match a class name from Step 1\n", - " entities[person_id] = {\n", - " \"id\": person_id,\n", - " \"type\": \"Person\",\n", - " \"properties\": {\"name\": row[\"EMPLOYEE_NAME\"]},\n", - " }\n", - " entities[org_id] = {\n", - " \"id\": org_id,\n", - " \"type\": \"Organization\",\n", - " \"properties\": {\"legalName\": row[\"ORG_NAME\"]},\n", - " }\n", - " entities[role_id] = {\n", - " \"id\": role_id,\n", - " \"type\": \"Role\",\n", - " \"properties\": {\"title\": row[\"ROLE_TITLE\"]},\n", - " }\n", - " # Reification node — carries the n-ary context\n", - " entities[event_id] = {\n", - " \"id\": event_id,\n", - " \"type\": \"EmploymentEvent\",\n", - " \"properties\": {\n", - " \"startDate\": row[\"START_DATE\"],\n", - " \"endDate\": row[\"END_DATE\"], # None = still employed\n", - " \"salary\": row[\"SALARY\"],\n", - " },\n", - " }\n", - "\n", - " relationships.extend([\n", - " # Shortcut edges — fast SPARQL when context is not needed\n", - " {\"source\": person_id, \"target\": org_id, \"type\": \"worksFor\"},\n", - " {\"source\": person_id, \"target\": role_id, \"type\": \"hasRole\"},\n", - " # Reification spokes — full context via the event node\n", - " {\"source\": event_id, \"target\": person_id, \"type\": \"employee\"},\n", - " {\"source\": event_id, \"target\": org_id, \"type\": \"employer\"},\n", - " {\"source\": event_id, \"target\": role_id, \"type\": \"role\"},\n", - " ])\n", - "\n", - " return build_kg([{\"entities\": list(entities.values()), \"relationships\": relationships}])\n", - "\n", - "\n", - "kg = map_rows_to_kg(rows)\n", - "print(f\"Entities built: {len(kg.get('entities', []))}\")\n", - "print(f\"Relationships built: {len(kg.get('relationships', []))}\")\n", - "\n", - "sample = next((e for e in kg[\"entities\"] if e[\"type\"] == \"EmploymentEvent\"), None)\n", - "print(f\"\\nSample EmploymentEvent node: {sample}\")" - ] + "source": "def map_rows_to_kg(rows: List[Dict[str, Any]]) -> Dict[str, Any]:\n entities: Dict[str, Dict[str, Any]] = {}\n relationships: List[Dict[str, Any]] = []\n\n for row in rows:\n # Stable, deterministic node IDs derived from business keys\n person_id = f\"person:{row['EMPLOYEE_ID']}\"\n org_id = f\"org:{row['ORG_ID']}\"\n role_id = f\"role:{row['ROLE_ID']}\"\n # Event ID includes all three participants + start date so that\n # a re-hired employee gets a distinct event node, not an overwrite.\n event_id = f\"employment:{row['EMPLOYEE_ID']}:{row['ORG_ID']}:{row['START_DATE']}\"\n\n # Entities — \"type\" must match a class name from Step 1\n entities[person_id] = {\n \"id\": person_id,\n \"type\": \"Person\",\n \"properties\": {\"name\": row[\"EMPLOYEE_NAME\"]},\n }\n entities[org_id] = {\n \"id\": org_id,\n \"type\": \"Organization\",\n \"properties\": {\"legalName\": row[\"ORG_NAME\"]},\n }\n entities[role_id] = {\n \"id\": role_id,\n \"type\": \"Role\",\n \"properties\": {\"title\": row[\"ROLE_TITLE\"]},\n }\n\n # Reification node — filter out None values so TripletStore does not\n # stringify None as the literal \"None\" for open-ended employment.\n event_props = {\n \"startDate\": row[\"START_DATE\"],\n \"endDate\": row[\"END_DATE\"],\n \"salary\": row[\"SALARY\"],\n }\n entities[event_id] = {\n \"id\": event_id,\n \"type\": \"EmploymentEvent\",\n \"properties\": {k: v for k, v in event_props.items() if v is not None},\n }\n\n # Full URIs for relationship types so TripletStore stores hr:\n # instead of the default urn:property:, keeping SPARQL consistent.\n relationships.extend([\n # Shortcut edges — fast SPARQL when context is not needed\n {\"source\": person_id, \"target\": org_id, \"type\": f\"{BASE_URI}worksFor\"},\n {\"source\": person_id, \"target\": role_id, \"type\": f\"{BASE_URI}hasRole\"},\n # Reification spokes — full context via the event node\n {\"source\": event_id, \"target\": person_id, \"type\": f\"{BASE_URI}employee\"},\n {\"source\": event_id, \"target\": org_id, \"type\": f\"{BASE_URI}employer\"},\n {\"source\": event_id, \"target\": role_id, \"type\": f\"{BASE_URI}role\"},\n ])\n\n return build_kg([{\"entities\": list(entities.values()), \"relationships\": relationships}])\n\n\nkg = map_rows_to_kg(rows)\nprint(f\"Entities built: {len(kg.get('entities', []))}\")\nprint(f\"Relationships built: {len(kg.get('relationships', []))}\")\n\nsample = next((e for e in kg[\"entities\"] if e[\"type\"] == \"EmploymentEvent\"), None)\nprint(f\"\\nSample EmploymentEvent node: {sample}\")" }, { "cell_type": "markdown", @@ -555,4 +432,4 @@ ] } ] -} +} \ No newline at end of file