Skip to content

[FEATURE] Ontology-guided extraction and typing - #539

Open
halcoope wants to merge 1 commit into
mainfrom
ontology-guided-extraction
Open

[FEATURE] Ontology-guided extraction and typing#539
halcoope wants to merge 1 commit into
mainfrom
ontology-guided-extraction

Conversation

@halcoope

Copy link
Copy Markdown
Collaborator

Title: [FEATURE] Ontology-guided extraction and typing

Description

Two related capabilities, both opt-in and both driven from an OWL/RDFS ontology you supply.

Extraction guided by a vocabulary. The ontology's entity types, permitted relationships
and attributes are rendered into the extraction prompts, and a deterministic filter then
resolves what the model emitted against the ontology — so a graph names the same idea one
way rather than several. Company rather than Company and CORPORATION; worksFor
rather than WORKS_FOR and EMPLOYED BY.

Typing of extracted values. Values of declared datatype properties are coerced to their
declared XSD type and written onto entity nodes as native graph properties, so
WHERE c.foundedYear < 2000 becomes a query you can run against the graph rather than
something only an LLM can answer from text.

With no ontology configured, the emitted graph queries and the extraction prompts are
byte-identical to a build from before this change.

Changes

Vocabulary

  • New package indexing/extract/ontology/ (8 files, ~3,100 lines): ontology loading and
    structural validation, a plain-data read index that crosses the extraction process
    boundary, prompt rendering, XSD coercion, and the filter.
  • The ontology is rendered into both extraction prompts, so the model is told the
    vocabulary instead of inventing one. Composed into the templates rather than substituted
    into a placeholder, so a no-ontology prompt is unchanged and existing LLM caches stay
    valid.
  • OntologyFilter, a deterministic TransformComponent running after the topic
    extractor: it resolves each emitted name, rewrites resolved names to the authored
    spelling, drops what does not conform, and annotates survivors so the build stage needs no
    ontology knowledge of its own.
  • One setting spans the axis. ontology_authority='off' says nothing; 'align' (the
    default) stores a matched concept under the ontology's name and keeps what the ontology
    does not declare; 'strict' also excludes what does not conform. The level resolves to six
    independent dimensions, each individually overridable, so a single gate can be asked for by
    name.
  • vocabulary_format: 'prose' (default) renders three labelled sections, one per
    output channel; 'turtle' shows the ontology's own source.

Typing

  • Coercion of declared literals to their XSD type: integer families with their declared
    bounds, decimals, booleans, dates, times and URIs. Lenient about lexical form — digit
    grouping, a zero fraction, named-month dates — and strict about meaning: a literal that
    would need interpreting rather thahan stored under a key whose
    name promises a number. Every value that coerces is JSON-serializable, because it travels
    through node metadata into Cypher parameters.
  • **typed_properties chooses where: 'off' (default) writes
    nothing, 'subject' keys it from the property's own local name on the subject entity,
    'complement' writes typed_value / datatype on the value node, 'both' does each to
    its own node.
  • enforce_datatypes makes typing authoritative at 'strict': a literal that does not
    parse as its declared type takes the fact with it, rather than being stored untyped.
  • **The write is additive by construis a separate query, so the
    entity insert is byte-identical at every placement and value, search_str and class
    cannot be disturbed. An ontology declaring a property name the graph model already owns is
    refused at configuration time rath

Also

  • Small edits at the call sites in lexical_graph_index.py, build_pipeline.py,
    entity_graph_builder.py, local_entity_rewrites_graph_builder.py, config.py,
    constants.py, prompts.py and t
  • Unrelated, included here rather than split: pytest's default norecursedirs contains
    build, so tests/unit/indexing/build/ had never been collected in a full-suite or CI
    run. Removing it surfaced six buil passed, plus five tests
    elsewhere already failing on main.es of test changes with
    nothing to do with ontologies.

Problem

Extraction is by default only lightly guided: the LLM is seeded with preferred entity
classifications and asked to prefer are unguided altogether.
That keeps recall high, but the same concept can arrive under several names, which costs
retrieval precision and makes the graph hard to query directly.

Extracted attribute values have the a founding year or a
revenue figure is stored as the textng downstream can filter,
sort or compare on it. There was no way to say "these are the types and relationships I
want", and no way to have a declared datatype land as a native graph property.

Related issue (if any): none

Testing

  • Unit tests added/updated — 641 tests across the ontology and typed-property files.
    Everything is driven from hand-built inputs and a small fixture ontology; no recorded
    model output is checked in.
  • Integration tests added (as appropriate) — none. An env-gated live test was written
    during development and deliberately removed: it required a specific Neptune Analytics
    graph, so it would have been p
  • Existing tests pass (pytest)pre-existing live tests
    gated on NEO4J_TEST_URI / Ss own invocation including --cov-fail-under=56`; total coverage 73.74%. The new package is at 100% of statements
    on six of seven modules and 99.4% on the seventh.
  • Tested manually — end-to-end bAnalytics graph during
    development, with typed properties confirmed present via
    CALL neptune.graph.pg_schema(). Documentation site builds clean.

Checklist

  • Code follows existing style an
  • License headers present on new files — verified against the same check CI runs
  • Documentation updated — new page at
    docs-site/src/content/docs/lextraction.mdx, plus the
    ontology parameter in the Indexing page
  • No breaking changes — the feature is opt-in and defaults to 'off' at every layer;
    typed_properties is delibera, so no ambient
    configuration can turn on graph writes. Enabling an ontology is a re-index boundary
    (normalize_names can change entity.class, which feeds the entity id), and th documented.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@github-actions

Copy link
Copy Markdown

Lexical Graph Coverage Report: The coverage is at unknown% (target: unknown%). Download the HTML report here.

@github-actions

Copy link
Copy Markdown

Lexical Graph Coverage Report: The coverage is at unknown% (target: unknown%). Download the HTML report here.

Lets a user supply an OWL/RDFS ontology as the extraction vocabulary, so that a
graph names the same idea one way rather than several. Opt-in: with no ontology
configured, the emitted graph queries and the extraction prompts are byte-identical
to a build from before this change.

Two mechanisms. The ontology's classes and properties, with their comments, are
rendered into the extraction prompts, so the model is told the vocabulary instead
of inventing one. After extraction, a deterministic transform component resolves
each emitted name against the ontology and - depending on the authority the
ontology is given - rewrites it to the authored spelling, or drops what does not
conform.

One setting spans that axis. `ontology_authority='off'` says nothing; `'align'`,
the default, stores a matched concept under the ontology's name and keeps what the
ontology does not declare; `'strict'` also excludes what does not conform. The
level resolves to six independent dimensions, each individually overridable, so a
single gate can be asked for by name.

Optionally, values of declared datatype properties are coerced to their declared
XSD type and written onto entity nodes as native graph properties, which is what
makes a numeric or date comparison possible in a query. Coercion is lenient about
lexical form - digit grouping, named-month dates - and strict about meaning: a
literal that would need interpreting rather than parsing is refused rather than
stored under a key whose name promises a number.

The new code is one package, indexing/extract/ontology/: loading and validation, a
plain-data read index that crosses the extraction process boundary, prompt
rendering, XSD coercion, and the filter. Everything else is small edits at the call
sites.

Also included, and unrelated: pytest's default norecursedirs contains "build", so
tests/unit/indexing/build/ had never been collected in a full-suite or CI run.
Removing it surfaced six builder test files that no longer passed, plus five tests
elsewhere that were already failing on main.

Documented at docs-site/.../lexical-graph/ontology-guided-extraction.mdx.
@halcoope
halcoope force-pushed the ontology-guided-extraction branch from 3091051 to 4e625b3 Compare September 11, 2026 21:04
@github-actions

Copy link
Copy Markdown

Lexical Graph Coverage Report: The coverage is at 73.7% (target: 80%). Download the HTML report here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant