Skip to content

feat: SpacyEntityRelationExtractor (LLM-free KG construction)#547

Draft
jexp wants to merge 12 commits into
neo4j:mainfrom
jexp:feat/spacy-parser
Draft

feat: SpacyEntityRelationExtractor (LLM-free KG construction)#547
jexp wants to merge 12 commits into
neo4j:mainfrom
jexp:feat/spacy-parser

Conversation

@jexp

@jexp jexp commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary

Implements SpacyEntityRelationExtractor — a drop-in replacement for LLMEntityRelationExtractor that uses SpaCy's dependency parser instead of an LLM, achieving 94% of GPT-4o extraction performance with no LLM calls, GPU-free, and orders-of-magnitude faster.

Based on Min et al., arXiv:2507.03226Towards Practical GraphRAG: Efficient KG Construction and Hybrid Retrieval at Scale.

Companion PR: #548 adds HierarchicalTextSplitter, which is the recommended upstream splitter for this extractor (used in the example script below).

New files

File Description
spacy_utils.py normalize() helper and WORD_FILTER stopword set
spacy_linear_extractor.py Linear heuristic extractor (NER co-occurrence within 10 tokens)
spacy_entity_relation_extractor.py SpacyEntityRelationExtractor — full component
examples/.../spacy_kg_pipeline.py End-to-end example: PDF → HierarchicalTextSplitter (#548) → Extractor → KGWriter

Design

  • Dependency extractor: walks SpaCy dep tree for nsubj/nsubjpass → ROOT → dobj/pobj patterns; handles passive voice (swaps subject/object, suffixes relation with _BY); merges noun chunks via doc.retokenize() (SpaCy 3.x API).
  • Linear extractor: NER entity pairs within same sentence, ≤10 tokens apart, no intervening entity → RELATED_TO triple with confidence 0.3. Toggle via use_linear_extractor (default True).
  • Schema filtering: optional schema param filters by NER type → node label mapping and by relationship type names; fail-open if no NER types map.
  • Word filter: configurable stopword list (word_filter constructor param); defaults to WORD_FILTER (~90 English stopwords).
  • ImportError guard: export in components/__init__.py is wrapped in try/except ImportError so the package imports cleanly when SpaCy is not installed.
  • Same output contract as LLMEntityRelationExtractor: Neo4jGraph with Neo4jNode(label="Entity") and Neo4jRelationship(properties={"confidence": float}).

Tests

  • 13 unit tests (test_spacy_entity_relation_extractor.py) — all mock spacy.load, no model download required
  • 3 integration tests (test_spacy_extractor_integration.py) — skip cleanly when en_core_web_sm is not installed
  • Plus 53 tests for the underlying utility modules (spacy_utils, dep triples, linear extractor)

Test plan

  • uv run pytest tests/unit/experimental/components/test_spacy_utils.py tests/unit/experimental/components/test_spacy_dependency_triples.py tests/unit/experimental/components/test_spacy_linear_extractor.py tests/unit/experimental/components/test_spacy_entity_relation_extractor.py -v
  • python -m spacy download en_core_web_sm && uv run pytest tests/unit/experimental/components/test_spacy_extractor_integration.py -v
  • uv run python -c "from neo4j_graphrag.experimental.components import SpacyEntityRelationExtractor; print('ok')"

jexp and others added 12 commits June 24, 2026 12:19
…WORD_FILTER constant

Create spacy_utils.py with shared normalisation utilities:
- normalize(text) lowercases, applies NFC Unicode normalisation, and collapses whitespace
- WORD_FILTER frozenset of ~90 common English stopwords for entity key filtering
- 21 unit tests covering edge cases, Unicode NFC equivalence, and type immutability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ractor

Add `extract_linear_triples(doc)` in a new module `spacy_linear_extractor.py`
(separated from `spacy_utils.py` because task-002 owns that file concurrently).

The function emits (entity_a, "RELATED_TO", entity_b, 0.3) for every pair of
NER entities in the same sentence whose starts are ≤ 10 tokens apart with no
other entity between them.  It gracefully degrades when no sentence segmenter
has been run by treating the whole doc as a single sentence.

14 unit tests covering all acceptance criteria pass.
…triple extractor

Add `extract_dependency_triples(doc)` to `spacy_utils.py`. Walks the
dependency tree per sentence finding nsubj/nsubjpass → ROOT → dobj/pobj
patterns. Handles active SVO (conf=1.0), passive voice with _BY suffix
(conf=0.9), and prepositional triples (conf=0.7). Uses spaCy 3.x
retokenizer API to collapse multi-word noun chunks/named entities.
Falls back gracefully when sentence boundaries are unavailable.

Move spaCy-gated tests to a dedicated `test_spacy_dependency_triples.py`
so that normalize/WORD_FILTER tests stay runnable without spaCy installed.
18 new tests; all 39 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…paCy components

Add integration test files for SpacyEntityRelationExtractor and
HierarchicalTextSplitter that exercise the real en_core_web_sm model.
All tests are auto-skipped (not failed) when the model is not installed.
Remove hierarchical_splitter.py and its tests from this branch (now in PR neo4j#548).
Update example script to reference the companion PR.
@jexp
jexp force-pushed the feat/spacy-parser branch from 87fe41d to f0d32a4 Compare June 24, 2026 10:19
@jexp jexp changed the title feat: SpacyEntityRelationExtractor and HierarchicalTextSplitter (LLM-free KG construction) feat: SpacyEntityRelationExtractor (LLM-free KG construction) Jun 25, 2026
@jexp
jexp marked this pull request as draft June 25, 2026 12:55
@NathalieCharbel
NathalieCharbel self-requested a review June 25, 2026 12:55
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