Skip to content

Commit b433bc9

Browse files
committed
spacy-entity-relation-extractor-task-008: add integration tests for SpaCy 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.
1 parent 3e567e1 commit b433bc9

4 files changed

Lines changed: 227 additions & 2 deletions

File tree

.plans/progress-spacy-entity-relation-extractor.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ Files: ~src/neo4j_graphrag/experimental/components/__init__.py, ~src/neo4j_graph
4040
## TASK-010 — 2026-06-24T17:00:00Z
4141
Example script for end-to-end spacy KG pipeline.
4242
Files: +examples/customize/build_graph/components/spacy_kg_pipeline.py
43+
44+
## TASK-008 — 2026-06-24T17:00:00Z
45+
Integration tests for SpacyEntityRelationExtractor and HierarchicalTextSplitter (skip when en_core_web_sm absent).
46+
Files: +tests/unit/experimental/components/test_spacy_extractor_integration.py, +tests/unit/experimental/components/text_splitters/test_hierarchical_splitter_integration.py

.plans/tasks-spacy-entity-relation-extractor.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ tasks:
176176
Use pytest fixtures that skip the test if `en_core_web_sm` is not installed.
177177
Verify that real SpaCy output produces a populated Neo4jGraph and correct
178178
TextChunks respectively.
179-
status: in_progress
179+
status: completed
180180
dependencies:
181181
- task-005
182182
- task-007
@@ -185,7 +185,7 @@ tasks:
185185
- "With model installed, SpacyEntityRelationExtractor.run() returns Neo4jGraph with at least one node and one relationship for a 2-sentence input"
186186
- "With model installed, HierarchicalTextSplitter.run() returns correct chunks for a multi-section document"
187187
- "Confidence values on relationships are floats in [0, 1]"
188-
completed_at: null
188+
completed_at: "2026-06-24T17:00:00Z"
189189

190190
- id: task-009
191191
title: "Export new components from experimental __init__"
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
# #
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
# #
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
# #
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Integration tests for SpacyEntityRelationExtractor using the real en_core_web_sm model.
16+
17+
All tests in this module are automatically skipped when either spaCy or the
18+
``en_core_web_sm`` model is not installed. No mocking is performed — these
19+
tests exercise the full NLP pipeline end-to-end.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import pytest
25+
26+
spacy = pytest.importorskip("spacy")
27+
28+
29+
@pytest.fixture(scope="module")
30+
def nlp(): # type: ignore[return]
31+
try:
32+
return spacy.load("en_core_web_sm")
33+
except OSError:
34+
pytest.skip(
35+
"en_core_web_sm not installed — run: python -m spacy download en_core_web_sm"
36+
)
37+
38+
39+
@pytest.mark.asyncio
40+
async def test_run_returns_populated_graph(nlp) -> None: # noqa: ANN001
41+
"""SpacyEntityRelationExtractor.run() returns a Neo4jGraph with nodes and relationships."""
42+
from neo4j_graphrag.experimental.components.spacy_entity_relation_extractor import (
43+
SpacyEntityRelationExtractor,
44+
)
45+
from neo4j_graphrag.experimental.components.types import TextChunk, TextChunks
46+
47+
extractor = SpacyEntityRelationExtractor(
48+
model="en_core_web_sm",
49+
use_linear_extractor=True,
50+
create_lexical_graph=False,
51+
)
52+
chunks = TextChunks(
53+
chunks=[
54+
TextChunk(
55+
text="Apple acquired Beats Electronics. Tim Cook announced the deal.",
56+
index=0,
57+
)
58+
]
59+
)
60+
result = await extractor.run(chunks=chunks)
61+
62+
assert len(result.nodes) >= 1, "Expected at least one entity node"
63+
assert len(result.relationships) >= 1, "Expected at least one relationship"
64+
65+
66+
@pytest.mark.asyncio
67+
async def test_confidence_values_in_range(nlp) -> None: # noqa: ANN001
68+
"""All relationship confidence values are floats in [0, 1]."""
69+
from neo4j_graphrag.experimental.components.spacy_entity_relation_extractor import (
70+
SpacyEntityRelationExtractor,
71+
)
72+
from neo4j_graphrag.experimental.components.types import TextChunk, TextChunks
73+
74+
extractor = SpacyEntityRelationExtractor(
75+
model="en_core_web_sm",
76+
use_linear_extractor=True,
77+
create_lexical_graph=False,
78+
)
79+
chunks = TextChunks(
80+
chunks=[
81+
TextChunk(
82+
text="Apple acquired Beats Electronics. Tim Cook announced the deal.",
83+
index=0,
84+
)
85+
]
86+
)
87+
result = await extractor.run(chunks=chunks)
88+
89+
for rel in result.relationships:
90+
conf = rel.properties.get("confidence")
91+
assert isinstance(conf, float), f"confidence should be float, got {type(conf)}"
92+
assert 0.0 <= conf <= 1.0, f"confidence out of range [0,1]: {conf}"
93+
94+
95+
@pytest.mark.asyncio
96+
async def test_use_linear_extractor_false_no_related_to(nlp) -> None: # noqa: ANN001
97+
"""use_linear_extractor=False produces no RELATED_TO relationships."""
98+
from neo4j_graphrag.experimental.components.spacy_entity_relation_extractor import (
99+
SpacyEntityRelationExtractor,
100+
)
101+
from neo4j_graphrag.experimental.components.types import TextChunk, TextChunks
102+
103+
extractor = SpacyEntityRelationExtractor(
104+
model="en_core_web_sm",
105+
use_linear_extractor=False,
106+
create_lexical_graph=False,
107+
)
108+
chunks = TextChunks(
109+
chunks=[
110+
TextChunk(
111+
text="Apple acquired Beats Electronics. Tim Cook announced the deal.",
112+
index=0,
113+
)
114+
]
115+
)
116+
result = await extractor.run(chunks=chunks)
117+
118+
related_to_rels = [r for r in result.relationships if r.type == "RELATED_TO"]
119+
assert related_to_rels == [], (
120+
f"Expected no RELATED_TO relationships when use_linear_extractor=False, "
121+
f"but got: {related_to_rels}"
122+
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright (c) "Neo4j"
2+
# Neo4j Sweden AB [https://neo4j.com]
3+
# #
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
# #
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
# #
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Integration tests for HierarchicalTextSplitter using the real en_core_web_sm model.
16+
17+
All tests in this module are automatically skipped when either spaCy or the
18+
``en_core_web_sm`` model is not installed. No mocking is performed — these
19+
tests exercise the full NLP pipeline end-to-end.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import pytest
25+
26+
spacy = pytest.importorskip("spacy")
27+
28+
29+
@pytest.fixture(scope="module")
30+
def nlp(): # type: ignore[return]
31+
try:
32+
return spacy.load("en_core_web_sm")
33+
except OSError:
34+
pytest.skip(
35+
"en_core_web_sm not installed — run: python -m spacy download en_core_web_sm"
36+
)
37+
38+
39+
@pytest.mark.asyncio
40+
async def test_markdown_split_real(nlp) -> None: # noqa: ANN001
41+
"""HierarchicalTextSplitter with markdown strategy returns sequential chunks for a 3-section doc."""
42+
from neo4j_graphrag.experimental.components.text_splitters.hierarchical_splitter import (
43+
HierarchicalTextSplitter,
44+
)
45+
46+
text = (
47+
"# Introduction\n"
48+
"This section introduces the topic and provides background information.\n\n"
49+
"# Methods\n"
50+
"This section describes the experimental methods used in the study.\n\n"
51+
"# Conclusion\n"
52+
"This section summarises the findings and suggests future work.\n"
53+
)
54+
55+
splitter = HierarchicalTextSplitter(
56+
header_strategy="markdown",
57+
max_chunk_size=200,
58+
chunk_overlap=20,
59+
drop_verbless_sentences=False,
60+
)
61+
result = await splitter.run(text)
62+
63+
assert len(result.chunks) >= 2, (
64+
f"Expected at least 2 chunks for a 3-section markdown doc, got {len(result.chunks)}"
65+
)
66+
# Indices must be sequential starting from 0.
67+
for i, chunk in enumerate(result.chunks):
68+
assert chunk.index == i, f"chunk {i} has non-sequential index {chunk.index}"
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_drop_verbless_sentences_real(nlp) -> None: # noqa: ANN001
73+
"""drop_verbless_sentences=True drops verbless fragments using the real SpaCy model."""
74+
from neo4j_graphrag.experimental.components.text_splitters.hierarchical_splitter import (
75+
HierarchicalTextSplitter,
76+
)
77+
78+
# "Overview" is a single-word verbless fragment.
79+
# The second sentence contains a real verb ("covers").
80+
text = "Overview\n\nThis section covers the main concepts of the system in detail."
81+
82+
splitter = HierarchicalTextSplitter(
83+
header_strategy="blank_line",
84+
max_chunk_size=500,
85+
chunk_overlap=0,
86+
drop_verbless_sentences=True,
87+
model="en_core_web_sm",
88+
)
89+
result = await splitter.run(text)
90+
91+
# At least one chunk must survive.
92+
assert len(result.chunks) >= 1, "Expected at least one chunk after filtering"
93+
94+
# The verbless word "Overview" should not appear as a standalone sentence
95+
# in any chunk, while the main sentence content should be present.
96+
all_text = " ".join(chunk.text for chunk in result.chunks)
97+
assert "covers" in all_text, (
98+
"Expected the verbal sentence to survive verbless-sentence filtering"
99+
)

0 commit comments

Comments
 (0)