mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
- Fix What's new → link in Info banner (now a proper <a> tag, always clickable) - Replace 4-stat CardGroup on index with inline premium stats row - Convert every <CardGroup>/<Card> block site-wide to markdown bullet lists: content sections → bold-title bullets with sub-bullets, nav cards → [Title](href) — description - Add cursor-animated list item hover effects to custom.css: green inset left border, subtle background tint, marker color change on hover - Affects index, getting-started, quickstart, concepts, modules, faq, architecture, installation, cookbook, glossary, learning-more, explorer-setup, cli-setup, community, contributing-guide, governance, citation, project-license, all integrations pages, and all 20+ reference module pages
380 lines
17 KiB
Markdown
380 lines
17 KiB
Markdown
---
|
||
title: "Split Module"
|
||
description: "Text chunking with recursive, semantic, entity-aware, relation-aware, structural, and sliding window splitting."
|
||
icon: "scissors"
|
||
---
|
||
|
||
**`semantica.split`** breaks documents into chunks that **preserve semantic context**:
|
||
|
||
- Six chunking strategies: recursive, semantic, entity-aware, relation-aware, sliding window, structural
|
||
- `SemanticChunker` uses embedding-based topic-shift detection to split only when content changes
|
||
- `EntityAwareChunker` keeps entity mentions intact across chunk boundaries
|
||
- `RelationAwareChunker` keeps subject-predicate-object triplets within a single chunk
|
||
- Chunking quality directly determines downstream embedding accuracy and entity extraction quality
|
||
|
||
|
||
## Why Chunking Matters
|
||
|
||
Most LLMs and embedding models have fixed context windows. Documents larger than that window must be split. But naive splitting (every 500 characters, regardless of structure) destroys semantic context:
|
||
|
||
- An entity mention like "Apple Inc." split across two chunks loses its context in both
|
||
- A relation triplet like "Steve Jobs founded Apple" split at "Steve Jobs" leaves a dangling subject
|
||
- Embedding a chunk that mixes two unrelated topics produces a centroid vector that matches neither
|
||
|
||
Semantica's chunking methods are designed to avoid these failure modes.
|
||
|
||
## Exported Classes
|
||
|
||
| Class | Role |
|
||
| :--- | :--- |
|
||
| `TextSplitter` | Unified entry point: swap `method=` without changing downstream code |
|
||
| `Chunk` | `{text, start_index, end_index, metadata, id}` |
|
||
| `SemanticChunker` | Embedding-based topic-shift detection: splits only when content actually changes |
|
||
| `StructuralChunker` | Heading/section-based splits using structural text analysis |
|
||
| `EntityAwareChunker` | Prevents named entity mentions from being split across chunk boundaries |
|
||
| `RelationAwareChunker` | Keeps subject-predicate-object triplets intact within a single chunk |
|
||
| `HierarchicalChunker` | Multi-level chunking producing parent/child chunk relationships |
|
||
|
||
**Available `method=` values for `TextSplitter`:**
|
||
|
||
| Method | Best for |
|
||
| :--- | :--- |
|
||
| `recursive` | General text: splits on paragraphs, sentences, words in order |
|
||
| `sentence` | Conversational text, QA |
|
||
| `paragraph` | Long-form text where paragraph integrity matters |
|
||
| `token` | LLM context window enforcement |
|
||
| `semantic_transformer` | Long documents with topic shifts |
|
||
| `entity_aware` | KG extraction pipelines |
|
||
| `relation_aware` | KG pipelines where triplet integrity matters |
|
||
| `structural` | Text with heading/paragraph structure |
|
||
| `sliding_window` | Dense overlap for bi-encoder retrieval |
|
||
|
||
## What You Get
|
||
|
||
- **TextSplitter** — Unified interface for 11 chunking strategies: swap methods without changing downstream code.
|
||
- **Semantic Chunking** — Embedding-based topic shift detection: splits only when the topic actually changes.
|
||
- **Entity-Aware Chunking** — Entity spans never cross chunk boundaries: guaranteed by boundary adjustment.
|
||
- **Relation-Aware Chunking** — Subject–predicate–object triplets kept within a single chunk for KG pipelines.
|
||
- **Chunk Object** — Output dataclass with text, character offsets, optional id, and method-specific metadata.
|
||
|
||
## Quick Start
|
||
|
||
<Steps>
|
||
<Step title="Choose a splitting method">
|
||
```python
|
||
from semantica.split import TextSplitter
|
||
|
||
splitter = TextSplitter(
|
||
method="recursive", # see Splitting Methods table
|
||
chunk_size=1000,
|
||
chunk_overlap=200,
|
||
)
|
||
```
|
||
</Step>
|
||
<Step title="Split raw text">
|
||
```python
|
||
chunks = splitter.split(text)
|
||
|
||
for chunk in chunks:
|
||
print(f" Start: {chunk.start_index}, End: {chunk.end_index}")
|
||
print(f" Method: {chunk.metadata.get('method')}")
|
||
print(f" Preview: {chunk.text[:80]}...")
|
||
```
|
||
</Step>
|
||
<Step title="Or split a document object">
|
||
```python
|
||
# split_documents() accepts any object with a .text attribute,
|
||
# or a plain string: no specific document class required.
|
||
class Doc:
|
||
def __init__(self, text, metadata=None):
|
||
self.text = text
|
||
self.metadata = metadata or {}
|
||
|
||
doc = Doc(text="Annual report content...", metadata={"source": "annual_report.pdf"})
|
||
|
||
splitter = TextSplitter(method="structural")
|
||
chunks = splitter.split_documents([doc])
|
||
|
||
for chunk in chunks:
|
||
print(f" {chunk.text[:80]}...")
|
||
```
|
||
</Step>
|
||
<Step title="Batch-split a list of documents">
|
||
```python
|
||
# split_documents() returns a flat List[Chunk] across all inputs
|
||
all_chunks = splitter.split_documents(docs)
|
||
|
||
for chunk in all_chunks:
|
||
print(chunk.text[:80])
|
||
```
|
||
</Step>
|
||
</Steps>
|
||
|
||
## Splitting Methods
|
||
|
||
| Method | How It Splits | Best For |
|
||
| :------ | :------------- | :-------- |
|
||
| `recursive` | Paragraph → sentence → word (cascading fallback) | General-purpose default |
|
||
| `semantic_transformer` | Embeds sentences, splits at cosine similarity drops | RAG: topic coherence matters |
|
||
| `entity_aware` | Adjusts boundaries so entity spans are never cut | NER pipelines |
|
||
| `relation_aware` | Keeps subject–predicate–object triplets within one chunk | KG construction |
|
||
| `sentence` | Sentence boundary detection (regex, NLTK, spaCy) | Short documents, Q&A |
|
||
| `paragraph` | Paragraph boundary splitting | Long-form articles, reports |
|
||
| `token` | Token count via tiktoken or transformers; hard cutoff | LLM context window prep |
|
||
| `word` | Word count with overlap | Simple token-approximate splits |
|
||
| `character` | Fixed character count with overlap; fastest, no NLP | Simple batch jobs |
|
||
| `sliding_window` | Fixed-size window advancing by stride; configurable overlap | Dense retrieval (ColBERT, DPR) |
|
||
| `structural` | Heading/paragraph structure detection | Text with explicit heading hierarchy |
|
||
| `embedding_semantic` | Embedding similarity boundaries (alias of `semantic_transformer`) | RAG with embedding-based coherence |
|
||
| `hierarchical` | Multi-level section → paragraph → sentence chunking | Multi-granularity retrieval |
|
||
|
||
## Choosing a Strategy
|
||
|
||
Use this decision tree before picking a method:
|
||
|
||
- **Building a KG?** → `relation_aware` (keeps triplets intact), then `entity_aware` for pure NER
|
||
- **RAG system where retrieval quality matters most?** → `semantic_transformer`
|
||
- **Dense overlap for bi-encoder retrieval (ColBERT, DPR)?** → `sliding_window`
|
||
- **Preparing prompts for a fixed-window LLM?** → `token`
|
||
- **Structured text with headings?** → `structural`
|
||
- **Paragraph-level coherence?** → `paragraph` or `sentence`
|
||
- **Fast splitting with no NLP overhead?** → `recursive` or `character`
|
||
|
||
## TextSplitter Constructor
|
||
|
||
```python
|
||
from semantica.split import TextSplitter
|
||
|
||
splitter = TextSplitter(
|
||
method="semantic_transformer", # chunking strategy: see Splitting Methods table
|
||
chunk_size=1000, # target size in characters
|
||
chunk_overlap=200, # character overlap between adjacent chunks
|
||
similarity_threshold=0.7, # cosine similarity cutoff (semantic_transformer only)
|
||
model="all-MiniLM-L6-v2", # sentence-transformers model name (semantic_transformer only)
|
||
ner_method="ml", # NER method (entity_aware only)
|
||
relation_method="ml", # relation extraction method (relation_aware only)
|
||
)
|
||
```
|
||
|
||
| Parameter | Type | Default | Description |
|
||
| :--------- | :---- | :------- | :----------- |
|
||
| `method` | `str \| list[str]` | `"recursive"` | Chunking strategy, or list of methods as fallback chain |
|
||
| `chunk_size` | `int` | `1000` | Target size in **characters** (not tokens: if you were using token-based sizing before, multiply by ~4 to approximate the same boundary) |
|
||
| `chunk_overlap` | `int` | `200` | Character overlap between adjacent chunks |
|
||
| `similarity_threshold` | `float` | `0.7` | Cosine similarity cutoff for `semantic_transformer`: lower = more splits |
|
||
| `model` | `str` | `"all-MiniLM-L6-v2"` | Sentence-transformers model name for `semantic_transformer` |
|
||
| `ner_method` | `str` | `"ml"` | NER method for `entity_aware`: `"pattern"` \| `"regex"` \| `"ml"` \| `"huggingface"` \| `"llm"` |
|
||
| `relation_method` | `str` | `"ml"` | Relation extraction method for `relation_aware`: `"ml"` \| `"llm"` \| `"huggingface"` |
|
||
| `tokenizer` | `str` | `"gpt-4"` | tiktoken model name for `token` method: unrecognised names fall back to `cl100k_base` |
|
||
|
||
<Warning>
|
||
**`chunk_overlap` too small.** Without overlap, a fact that spans a chunk boundary is invisible in both chunks. A 10–20% overlap relative to `chunk_size` is a safe minimum: for `chunk_size=1000`, set `chunk_overlap=100` to `200`.
|
||
</Warning>
|
||
|
||
## Splitting Method Details
|
||
|
||
<Tabs>
|
||
<Tab title="Recursive (default)">
|
||
Tries paragraph breaks first, then sentence boundaries, then word boundaries: falling back only when the chunk exceeds `chunk_size`:
|
||
|
||
```python
|
||
splitter = TextSplitter(method="recursive", chunk_size=1000, chunk_overlap=200)
|
||
chunks = splitter.split(text)
|
||
```
|
||
|
||
**Key behaviours:**
|
||
- Preserves paragraph and sentence structure wherever possible
|
||
- Falls back gracefully: never produces chunks larger than `chunk_size`
|
||
- Overlap ensures context continuity across chunk boundaries
|
||
- Good starting point when you're unsure which method to use
|
||
</Tab>
|
||
<Tab title="Semantic">
|
||
Embeds each sentence using a sentence-transformers model, then splits whenever cosine similarity between consecutive sentences drops below `similarity_threshold`. Each chunk covers one coherent topic:
|
||
|
||
```python
|
||
from semantica.split import TextSplitter
|
||
|
||
splitter = TextSplitter(
|
||
method="semantic_transformer",
|
||
model="all-MiniLM-L6-v2", # any sentence-transformers model name
|
||
similarity_threshold=0.7, # 0.6 = more splits, 0.8 = fewer splits
|
||
chunk_size=800,
|
||
chunk_overlap=0, # not needed: chunks are already coherent
|
||
)
|
||
chunks = splitter.split(text)
|
||
```
|
||
|
||
**Key behaviours:**
|
||
- Requires the `sentence-transformers` package: uses `all-MiniLM-L6-v2` by default, configurable via `model=`
|
||
- Produces variable-length chunks: some topics are short, others long
|
||
- Falls back to sentence splitting if `sentence-transformers` is not installed
|
||
- Slower than `recursive` due to embedding computation; cache embeddings for repeated splits
|
||
|
||
<Tip>
|
||
**Semantic splitting needs enough sentences.** `semantic_transformer` needs several sentences to detect topic shifts. On documents shorter than ~300 words it behaves like `sentence` splitting: use `recursive` instead.
|
||
</Tip>
|
||
</Tab>
|
||
<Tab title="Entity-Aware">
|
||
Runs NER internally, then adjusts chunk boundaries so no entity mention is split across two chunks:
|
||
|
||
```python
|
||
from semantica.split import TextSplitter
|
||
import os
|
||
|
||
# ner_method is passed through to the internal NERExtractor.
|
||
# Use "llm" for highest accuracy, "ml" (default) for speed.
|
||
splitter = TextSplitter(
|
||
method="entity_aware",
|
||
chunk_size=512,
|
||
chunk_overlap=50,
|
||
ner_method="ml", # "pattern" | "regex" | "ml" | "huggingface" | "llm"
|
||
)
|
||
chunks = splitter.split(text)
|
||
|
||
for chunk in chunks:
|
||
print(f" entities in chunk: {chunk.metadata.get('entity_count', 0)}")
|
||
print(f" preview: {chunk.text[:80]}...")
|
||
```
|
||
|
||
**Key behaviours:**
|
||
- NER is run internally: entity extraction happens automatically inside the splitter
|
||
- Entity objects are available in `chunk.metadata["entities"]` for each chunk
|
||
- Chunk sizes vary slightly from `chunk_size`: boundary adjustments are ≤ one sentence
|
||
- Works with all entity types: PERSON, ORGANIZATION, LOCATION, DATE, custom types
|
||
</Tab>
|
||
<Tab title="Relation-Aware">
|
||
Keeps subject–predicate–object triplets within the same chunk: critical for KG pipelines:
|
||
|
||
```python
|
||
from semantica.split import TextSplitter
|
||
|
||
# relation_method is passed through to the internal RelationExtractor.
|
||
# Use "llm" for highest accuracy, "ml" (default) for speed.
|
||
splitter = TextSplitter(
|
||
method="relation_aware",
|
||
chunk_size=512,
|
||
relation_method="ml", # "ml" | "llm" | "huggingface"
|
||
)
|
||
chunks = splitter.split(text)
|
||
|
||
for chunk in chunks:
|
||
print(f" relations in chunk: {chunk.metadata.get('relation_count', 0)}")
|
||
for rel in chunk.metadata.get("relationships", []):
|
||
print(f" {rel}")
|
||
```
|
||
|
||
**Key behaviours:**
|
||
- Relation extraction is run internally: no pre-computed entities or triplets needed
|
||
- Relation objects are available in `chunk.metadata["relationships"]` for each chunk
|
||
- Implies entity-aware behaviour: both entities in a triplet are kept whole too
|
||
- Best used as the split step in a `Parse → Split → Extract → Build KG` pipeline
|
||
</Tab>
|
||
<Tab title="Structural">
|
||
Splits text based on structural analysis of headings and paragraph boundaries. Each heading or paragraph group becomes a chunk boundary:
|
||
|
||
```python
|
||
from semantica.split import TextSplitter
|
||
|
||
splitter = TextSplitter(method="structural")
|
||
chunks = splitter.split(text)
|
||
|
||
for chunk in chunks:
|
||
print(f" {chunk.text[:80]}...")
|
||
print(f" start: {chunk.start_index}, end: {chunk.end_index}")
|
||
```
|
||
|
||
**Key behaviours:**
|
||
- Operates on plain text: no structural document format required
|
||
- Respects heading hierarchy (lines starting with `#` or all-caps headings) and paragraph breaks
|
||
- Uses `max_chunk_size=` parameter instead of the standard `chunk_size=` for maximum size control
|
||
- Falls back to `recursive` if `StructuralChunker` is unavailable
|
||
</Tab>
|
||
</Tabs>
|
||
|
||
## Chunk Schema
|
||
|
||
<AccordionGroup>
|
||
<Accordion title="Chunk dataclass">
|
||
|
||
```python
|
||
@dataclass
|
||
class Chunk:
|
||
text: str # the chunk's text content
|
||
start_index: int # character offset of start in source text
|
||
end_index: int # character offset of end in source text
|
||
metadata: Dict[str, Any] # method-specific fields: see table below
|
||
id: Optional[str] = None # optional chunk identifier
|
||
```
|
||
|
||
</Accordion>
|
||
<Accordion title="Chunk metadata fields">
|
||
|
||
Metadata keys vary by method. Only keys that are actually set by the implementation are listed.
|
||
|
||
| Field | Type | Set by | Description |
|
||
| :----- | :---- | :------ | :----------- |
|
||
| `method` | `str` | all methods | Splitting method that produced this chunk |
|
||
| `chunk_size` | `int` | most methods | Character length of this chunk |
|
||
| `sentence_count` | `int` | `sentence`, `semantic_transformer`, spaCy path | Number of sentences in this chunk |
|
||
| `paragraph_count` | `int` | `paragraph` | Number of paragraphs in this chunk |
|
||
| `word_count` | `int` | `word` | Number of words in this chunk |
|
||
| `token_count` | `int` | `token`; `sentence`/`semantic_transformer` when spaCy is available | Token count: not always present |
|
||
| `entity_count` | `int` | `entity_aware` | Number of entities whose boundaries fall in this chunk |
|
||
| `entities` | `list` | `entity_aware` | Entity objects whose boundaries fall in this chunk |
|
||
| `relation_count` | `int` | `relation_aware` | Number of relation triplets in this chunk |
|
||
| `relationships` | `list` | `relation_aware` | Relation objects in this chunk |
|
||
| `element_count` | `int` | `structural` | Number of structural elements grouped into this chunk |
|
||
| `element_types` | `list[str]` | `structural` | Types of elements: `"heading"`, `"paragraph"`, `"list"`, etc. |
|
||
|
||
</Accordion>
|
||
</AccordionGroup>
|
||
|
||
## Tokenizer Options
|
||
|
||
The `token` method accepts a `tokenizer=` kwarg that is passed to `tiktoken.encoding_for_model()`. The value should be a tiktoken model name. Unrecognised names fall back to `cl100k_base` automatically.
|
||
|
||
| Value | Encoding used |
|
||
| :----- | :------------- |
|
||
| `"gpt-4"` (default) | `cl100k_base` |
|
||
| `"gpt-3.5-turbo"` | `cl100k_base` |
|
||
| `"text-embedding-ada-002"` | `cl100k_base` |
|
||
| Any unrecognised string | Falls back to `cl100k_base` |
|
||
|
||
If `tiktoken` is not installed, the `token` method falls back to splitting by whitespace-separated words.
|
||
|
||
<Warning>
|
||
**Wrong tokenizer.** The `token` method passes the `tokenizer=` value to `tiktoken.encoding_for_model()`. If the model name is not recognised by tiktoken it silently falls back to `cl100k_base`. Pass a valid tiktoken model name (e.g. `"gpt-4"`, `"gpt-3.5-turbo"`) to get deterministic behaviour.
|
||
</Warning>
|
||
|
||
## Pipeline Integration
|
||
|
||
`TextSplitter` can be used standalone or composed manually with other Semantica modules. The example below shows a sequential pattern: parse a file, split the text, then extract entities from each chunk:
|
||
|
||
```python
|
||
from semantica.parse import DocumentParser
|
||
from semantica.split import TextSplitter
|
||
from semantica.semantic_extract import NERExtractor
|
||
|
||
# Parse
|
||
parser = DocumentParser()
|
||
parsed = parser.parse("data/report.pdf") # returns a dict with "full_text" key
|
||
|
||
# Split
|
||
splitter = TextSplitter(method="semantic_transformer", chunk_size=512)
|
||
chunks = splitter.split(parsed["full_text"])
|
||
|
||
# Extract from each chunk
|
||
ner = NERExtractor(method="ml")
|
||
|
||
for chunk in chunks:
|
||
entities = ner.extract(chunk.text)
|
||
print(f" {len(entities)} entities in chunk starting at {chunk.start_index}")
|
||
```
|
||
|
||
For the full pipeline orchestration API, see the [Pipeline reference](pipeline).
|
||
|
||
- [Parse](parse) — Parse documents before chunking: produces sections and metadata.
|
||
- [Embeddings](embeddings) — Embed chunks for vector search and semantic chunking.
|
||
- [Semantic Extract](semantic_extract) — Extract entities and relations from individual chunks.
|
||
- [Pipeline](pipeline) — Integrate splitting as a named pipeline step.
|