How to Implement Entity-Aware Chunking for GraphRAG in semantica.split
Use the TextSplitter class in semantica.split to create a custom chunking method that detects named entities and aligns chunk boundaries to entity spans, ensuring no entity is split across chunks for optimal GraphRAG retrieval.
GraphRAG pipelines require chunks that preserve semantic coherence around named entities—splitting "Albert Einstein" across two chunks destroys relationship extraction quality. The semantica.split module provides an extensible architecture through the TextSplitter class that lets you implement custom strategies without modifying core library code. This guide shows you how to build, register, and deploy an entity-aware chunker that keeps entities intact while feeding metadata to your knowledge graph.
Understanding the TextSplitter Architecture
The TextSplitter class acts as a dispatcher that delegates actual splitting logic to registered functions. Understanding this flow is essential before adding custom behavior.
Core Dispatcher in splitter.py
The central class lives in semantica/split/splitter.py starting at line 54. When you call split(), the dispatcher looks up the concrete implementation via get_split_method(method_name) at line 144, then executes it with supplied options at line 152. This design means you never modify TextSplitter itself—you register new methods externally.
Method Registration in methods.py
Available strategies are stored in the SPLIT_METHODS dictionary inside semantica/split/methods.py. The get_split_method function retrieves entries from this registry. To add entity-aware functionality, you will import your new function into this file and add it to the dictionary.
Implementing the Entity-Aware Split Method
Create a new module file at semantica/split/entity_aware.py. This file contains the entity_aware_split function and helper logic for entity extraction and boundary alignment.
Creating entity_aware.py
The implementation extracts entities first, then builds chunks that never split an entity span. It returns standard Chunk objects from semantica/split/semantic_chunker.py with metadata tracking which entities appear in each chunk.
# semantica/split/entity_aware.py
from typing import List
from .semantic_chunker import Chunk
from ..utils.logging import get_logger
logger = get_logger("entity_aware")
def _extract_entities(text: str, ner_method: str = "spacy") -> List[dict]:
"""
Return entities as [{'text': str, 'start': int, 'end': int}].
Supports spaCy, HuggingFace, or LLM-based extraction via ner_method.
"""
if ner_method == "spacy":
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp(text)
return [{"text": ent.text, "start": ent.start_char, "end": ent.end_char}
for ent in doc.ents]
raise ValueError(f"Unsupported ner_method: {ner_method}")
def entity_aware_split(
text: str,
*,
chunk_size: int = 1000,
chunk_overlap: int = 200,
ner_method: str = "spacy",
**kwargs,
) -> List[Chunk]:
"""
Split text into chunks that keep each recognized entity together.
"""
logger.debug("Running entity-aware splitter")
entities = _extract_entities(text, ner_method=ner_method)
# Map character positions to entity indices
entity_map = {}
for i, ent in enumerate(entities):
for pos in range(ent["start"], ent["end"]):
entity_map[pos] = i
chunks: List[Chunk] = []
start = 0
while start < len(text):
# Provisional end based on chunk_size
end = min(start + chunk_size, len(text))
# If end falls inside an entity, extend to entity boundary
while end < len(text) and end in entity_map:
end += 1
# Record which entities appear in this slice
ent_ids = {entity_map[pos] for pos in range(start, end) if pos in entity_map}
metadata = {"entity_ids": list(ent_ids)}
chunk_text = text[start:end]
chunks.append(Chunk(content=chunk_text, metadata=metadata))
# Advance with overlap
start = max(end - chunk_overlap, 0)
logger.info(f"Created {len(chunks)} entity-aware chunks")
return chunks
Aligning Chunk Boundaries to Entity Spans
The critical logic occurs in the boundary-adjustment loop. If the naive chunk_size endpoint lands inside an entity, the code increments end until it clears the entity boundary. This guarantees entity integrity—no named entity straddles two chunks. Each resulting Chunk object carries an entity_ids list in its metadata dictionary, enabling downstream GraphRAG components to link chunks to specific knowledge graph nodes.
Registering and Configuring the Method
After implementing the logic, you must expose it to TextSplitter through the registration system and provide default configuration values.
Adding to SPLIT_METHODS
Edit semantica/split/methods.py to import and register your function:
# semantica/split/methods.py
from .entity_aware import entity_aware_split
SPLIT_METHODS = {
# ... existing methods ...
"entity_aware": entity_aware_split,
}
Now get_split_method("entity_aware") returns your function, making it available to TextSplitter instantiations.
Setting Default Options
Default parameters are read from the split configuration system (see line 47 in splitter.py). Add an entry to semantica/split/config.yaml (or extend the existing config object) to define sensible defaults:
entity_aware:
chunk_size: 1000
chunk_overlap: 200
ner_method: spacy
The TextSplitter constructor merges these values into the options dict via _load_method_configs (lines 106-113 in splitter.py), allowing users to override them at runtime.
Usage Examples
Once registered, you can invoke entity-aware chunking through the standard TextSplitter interface.
Basic Entity-Aware Chunking
Instantiate the splitter with method="entity_aware" and pass the text:
from semantica.split import TextSplitter
splitter = TextSplitter(
method="entity_aware",
chunk_size=1200,
chunk_overlap=250,
ner_method="spacy",
)
text = "Albert Einstein was born in Ulm. He developed the theory of relativity..."
chunks = splitter.split(text)
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}: {chunk.content}")
print(f"Entities: {chunk.metadata['entity_ids']}")
Fallback Chains for Reliability
TextSplitter supports fallback chains starting at line 139 in splitter.py. If the entity-aware method fails (e.g., missing spaCy model), it automatically tries the next method:
splitter = TextSplitter(method=["entity_aware", "recursive"])
If entity_aware_split raises an exception, the dispatcher catches it and attempts the recursive splitter instead.
Integrating with GraphRAG Pipelines
Feed the chunk metadata into your knowledge graph to enable entity-to-chunk retrieval:
from semantica.split import TextSplitter
splitter = TextSplitter(method="entity_aware", ner_method="spacy")
chunks = splitter.split(long_document)
# Store in vector database and link to graph
for chunk in chunks:
vector_id = vector_store.add_document(chunk.content)
for entity_id in chunk.metadata["entity_ids"]:
graph.create_edge(entity_id=entity_id, chunk_id=vector_id)
This creates bidirectional links between graph entities and text chunks, allowing GraphRAG queries to retrieve contextually relevant passages via entity traversal.
Summary
- Architecture:
TextSplitterinsemantica/split/splitter.pydelegates to registered functions viaget_split_method, enabling custom strategies without core code changes. - Implementation: Create
semantica/split/entity_aware.pywithentity_aware_splitthat extracts entities first, then adjusts chunk boundaries to avoid splitting entity spans. - Registration: Import the function into
semantica/split/methods.pyand add it to theSPLIT_METHODSdictionary to expose it via themethod="entity_aware"parameter. - Configuration: Define defaults in
config.yamlor pass options directly toTextSplitter; the class merges these through_load_method_configs. - GraphRAG Integration: Each returned
Chunkcontains anentity_idsmetadata field that maps directly to knowledge graph nodes, enabling entity-aware retrieval.
Frequently Asked Questions
What is entity-aware chunking and why does it matter for GraphRAG?
Entity-aware chunking ensures that named entities (people, organizations, locations) never straddle chunk boundaries. In GraphRAG, if an entity like "New York City" is split across chunks, relationship extraction fails and graph queries return incomplete context. By aligning chunk boundaries to entity spans, you preserve the semantic units that graph databases index.
How does TextSplitter handle method failures?
TextSplitter implements fault tolerance through fallback chains. When you pass a list to the method parameter (e.g., method=["entity_aware", "recursive"]), the code loops through options starting at line 139 in splitter.py. If entity_aware_split raises an exception—such as a missing NER library—the dispatcher automatically tries the next method in the list.
Can I use different NER backends with entity_aware_split?
Yes. The _extract_entities helper function accepts a ner_method parameter that supports multiple backends. The example implementation includes spaCy via spacy.load("en_core_web_sm"), but you can extend the conditional logic to support HuggingFace transformers, Azure AI Language, or LLM-based extraction by adding elif branches that return the standard {"text": str, "start": int, "end": int} format.
How do I link chunks to a knowledge graph after splitting?
Each Chunk object returned by entity_aware_split contains a metadata dictionary with an entity_ids key listing the indices of entities present in that chunk. Iterate through the chunks and use these IDs to create edges in your graph database (Neo4j, Amazon Neptune, etc.) connecting the entity nodes to the chunk vector IDs stored in your retrieval system. This enables traversal from entities to their full textual context during GraphRAG inference.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →