Skip to content

Commit 9e3cb6a

Browse files
committed
docs: align documentation with public API
1 parent 629e3c9 commit 9e3cb6a

16 files changed

Lines changed: 145 additions & 195 deletions

.formatter.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Used by "mix format"
22
[
3-
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
3+
inputs: ["{mix,.formatter}.exs", "{config,examples,lib,test}/**/*.{ex,exs}"]
44
]

CHANGELOG.md

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,33 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
1313

1414
- Added `Chunx.Chunker.Recursive`, which progressively splits text at
1515
paragraph, sentence, punctuation, whitespace, and token boundaries.
16-
- Added a shared tokenizer boundary with support for native
17-
`Tokenizers.Tokenizer` values and custom tokenizer adapters.
18-
- Added grapheme-safe handling for repeated, overlapping, and byte-level token
19-
offsets.
16+
- Added custom tokenizer adapters through `Chunx.Tokenizer`.
2017
- Added an example script covering the available non-semantic chunkers.
2118
- Added opt-in real-model embedding integration tests, runnable with
2219
`mix test --include integration` or `mix test --only integration`.
23-
- Expanded property testing for reconstruction, offsets, overlap normalization,
24-
semantic metadata, sentence grouping, tokenizer windows, Unicode, and
25-
statistical invariants.
20+
- Added property tests for reconstruction, offsets, overlap, Unicode, tokenizer
21+
windows, and semantic metadata.
2622

2723
### Changed
2824

2925
- Standardized `token_count` across all chunkers to mean content tokens,
3026
excluding tokenizer entries without a byte span.
31-
- Refactored Token, Word, Sentence, Recursive, and Semantic chunking paths to
32-
reduce intermediate collections and repeated traversal.
33-
- Optimized native token counting and ordinary token-window packing while
34-
retaining the hardened Unicode fallback.
35-
- Simplified semantic similarity averaging without changing the adjacent
36-
cosine-similarity model.
27+
- Made tokenizer-derived chunk boundaries grapheme-safe.
28+
- Reduced intermediate allocations and repeated traversal in the chunkers.
3729
- Updated Nx, EXLA, Bumblebee, Scholar, StreamData, and related dependencies.
3830

3931
### Fixed
4032

4133
- Preserved exact byte offsets for repeated text and Unicode content.
42-
- Prevented SentenceChunker from stalling when overlap contains the entire
34+
- Prevented Sentence from stalling when overlap contains the entire
4335
previous chunk.
44-
- Preserved TokenChunker's trailing overlap window while keeping indivisible
36+
- Preserved Token's trailing overlap window while keeping indivisible
4537
graphemes intact.
46-
- Propagated tokenizer failures consistently instead of crashing on failed
47-
encodings.
38+
- Propagated tokenizer failures instead of crashing on failed encodings.
4839
- Rejected malformed tokenizer responses and offsets with tagged errors.
49-
- Prevented SemanticChunker from silently dropping sentences when an embedding
40+
- Prevented Semantic from silently dropping sentences when an embedding
5041
function returns the wrong number of embeddings.
51-
- Corrected sentence grouping and chunk-boundary behavior to preserve all input
52-
text.
42+
- Preserved all input text when grouping sentences and placing chunk boundaries.
5343

5444
## [0.1.0] - 2026-03-04
5545

README.md

Lines changed: 40 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,13 @@
22

33
[![test](https://github.com/preciz/chunx/actions/workflows/test.yml/badge.svg)](https://github.com/preciz/chunx/actions/workflows/test.yml)
44

5-
Chunx is an Elixir library for splitting text into meaningful chunks using various strategies. It's particularly useful for processing large texts for LLMs, semantic search, and other NLP tasks.
6-
7-
## Credit
8-
9-
This library is based on [chonkie-ai/chonkie](https://github.com/chonkie-ai/chonkie)
10-
11-
## Features
12-
13-
- Multiple chunking strategies:
14-
- Token-based chunking
15-
- Word-based chunking
16-
- Sentence-based chunking
17-
- Semantic chunking with embeddings
18-
- Recursive chunking using structural boundaries
19-
20-
- Configurable options for each strategy
21-
- Support for overlapping chunks
22-
- Token count tracking
23-
- Embedding support
5+
Chunx splits text by tokens, words, sentences, document structure, or semantic
6+
similarity. It is an Elixir implementation inspired by
7+
[Chonkie](https://github.com/chonkie-ai/chonkie).
248

259
## Installation
2610

27-
Add `chunx` to your list of dependencies in `mix.exs`:
11+
Add Chunx to `mix.exs`:
2812

2913
```elixir
3014
def deps do
@@ -36,83 +20,66 @@ end
3620

3721
## Usage
3822

39-
### Token-based Chunking
40-
41-
```elixir
42-
{:ok, tokenizer} = Tokenizers.Tokenizer.from_pretrained("gpt2")
43-
{:ok, chunks} = Chunx.Chunker.Token.chunk("Your text here", tokenizer, chunk_size: 512)
44-
```
45-
46-
### Word-based Chunking
23+
All chunkers require a tokenizer. They accept a `Tokenizers.Tokenizer` or a
24+
custom adapter implementing the `Chunx.Tokenizer` behaviour.
4725

4826
```elixir
49-
{:ok, tokenizer} = Tokenizers.Tokenizer.from_pretrained("gpt2")
50-
{:ok, chunks} = Chunx.Chunker.Word.chunk("Your text here", tokenizer, chunk_size: 512)
51-
```
52-
53-
### Sentence-based Chunking
27+
alias Chunx.Chunker.Token
5428

55-
```elixir
5629
{:ok, tokenizer} = Tokenizers.Tokenizer.from_pretrained("gpt2")
57-
{:ok, chunks} = Chunx.Chunker.Sentence.chunk("Your text here", tokenizer)
30+
{:ok, chunks} = Token.chunk("Text to split", tokenizer, chunk_size: 128)
5831
```
5932

60-
### Semantic Chunking
33+
Each returned chunk contains its text, half-open byte offsets into the original
34+
text, and its content-token count. Sentence and Semantic return
35+
`Chunx.SentenceChunk` structs; the other chunkers return `Chunx.Chunk` structs.
6136

62-
```elixir
63-
{:ok, tokenizer} = Tokenizers.Tokenizer.from_pretrained("gpt2")
37+
### Chunkers
6438

65-
# The embedding function must return a list of Nx.Tensor.t()
66-
embedding_fn = fn texts ->
67-
# Your embedding function here
68-
end
39+
| Module | Splitting unit | Overlap |
40+
| --- | --- | --- |
41+
| `Chunx.Chunker.Token` | Token offsets | Token count or fraction |
42+
| `Chunx.Chunker.Word` | Whole words | Token count or fraction |
43+
| `Chunx.Chunker.Sentence` | Whole sentences | Whole sentences within a token budget |
44+
| `Chunx.Chunker.Recursive` | Configured structural levels, then tokens | None |
45+
| `Chunx.Chunker.Semantic` | Sentence-embedding similarity | None |
6946

70-
{:ok, chunks} = Chunx.Chunker.Semantic.chunk("Your text here", tokenizer, embedding_fn)
71-
```
47+
See the [API documentation](https://hexdocs.pm/chunx/) for each module's options
48+
and size-limit exceptions.
7249

73-
### Recursive Chunking
50+
Semantic chunking also requires a function that returns one `Nx.Tensor` for
51+
each input string:
7452

7553
```elixir
76-
{:ok, tokenizer} = Tokenizers.Tokenizer.from_pretrained("gpt2")
77-
{:ok, chunks} = Chunx.Chunker.Recursive.chunk("Your text here", tokenizer)
78-
```
79-
80-
Recursive chunking tries paragraphs, sentences, punctuation, whitespace, and
81-
finally token boundaries until every chunk fits within the configured size.
54+
alias Chunx.Chunker.Semantic
8255

83-
## Configuration
56+
embedding_fun = &MyApp.Embeddings.embed/1
8457

85-
Each chunking strategy accepts various options to customize the chunking behavior:
86-
87-
- `chunk_size`: Maximum number of content tokens per chunk
88-
- `chunk_overlap`: Number or proportion of content tokens shared by consecutive chunks
89-
- `min_sentences_per_chunk`: Minimum number of sentences per sentence-based chunk
90-
- `min_sentences`: Minimum number of sentences per semantic chunk
91-
- `threshold`: Similarity threshold for semantic chunking
92-
- And more...
93-
94-
See the documentation for each chunker module for detailed configuration options.
58+
{:ok, chunks} =
59+
Semantic.chunk("Text to split", tokenizer, embedding_fun,
60+
chunk_size: 128,
61+
threshold: :auto
62+
)
63+
```
9564

9665
## Testing
9766

98-
```elixir
99-
# Run the test suite
67+
Run the regular suite:
68+
69+
```bash
10070
mix test
10171
```
10272

103-
Real-model embedding integration tests are excluded by default because they
104-
download and run a Hugging Face model. Enable them explicitly with:
73+
Embedding integration tests use
74+
`sentence-transformers/all-MiniLM-L6-v2`. They download and run the model, so
75+
they are excluded by default:
10576

10677
```bash
107-
mix test --include integration
78+
mix test --only integration
10879
```
10980

110-
To run only the integration tests, use `mix test --only integration`.
111-
112-
The default model is `sentence-transformers/all-MiniLM-L6-v2`. Override it with
113-
`CHUNX_EMBEDDING_MODEL`, provided the model is supported by Bumblebee's text
114-
embedding serving and `Tokenizers.Tokenizer.from_pretrained/1`.
81+
Use `mix test --include integration` to run both suites together.
11582

11683
## License
11784

118-
[MIT License](LICENSE)
85+
[MIT](LICENSE)

examples/demo.exs

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,29 @@ Mix.install([
33
])
44

55
defmodule Demo do
6-
def run do
7-
{:ok, tokenizer} = Tokenizers.Tokenizer.from_pretrained("gpt2")
8-
9-
text = """
10-
The process of text chunking in RAG applications represents a delicate balance between competing requirements. On one side, we have the need for semantic coherence – ensuring that each chunk maintains meaningful context that can be understood and processed independently. On the other, we must optimize for information density, ensuring that each chunk carries sufficient signal without excessive noise that might impede retrieval accuracy. In this post, we explore the challenges of text chunking in RAG applications and propose a novel approach that leverages recent advances in transformer-based language models to achieve a more effective balance between these competing requirements.
11-
12-
# Heading 1
13-
This is a paragraph with some **bold text** and _italic text_.
14-
15-
## Heading 2
16-
- Bullet point 1
17-
- Bullet point 2 with `inline code`
18-
"""
6+
@text """
7+
Text chunking divides a document into smaller sections. A tokenizer measures
8+
their size. Different chunkers preserve different boundaries.
199
20-
IO.puts("=== Token-based Chunking ===")
21-
{:ok, token_chunks} = Chunx.Chunker.Token.chunk(text, tokenizer, chunk_size: 50, chunk_overlap: 10)
22-
Enum.each(Enum.with_index(token_chunks), fn {chunk, i} ->
23-
IO.puts("Chunk #{i} (Tokens: #{chunk.token_count}):\n#{chunk.text}\n")
24-
end)
10+
Sentence chunking keeps sentences together. Recursive chunking first tries
11+
document structure, then falls back to smaller boundaries.
12+
"""
2513

26-
IO.puts("\n=== Word-based Chunking ===")
27-
{:ok, word_chunks} = Chunx.Chunker.Word.chunk(text, tokenizer, chunk_size: 50, chunk_overlap: 10)
28-
Enum.each(Enum.with_index(word_chunks), fn {chunk, i} ->
29-
IO.puts("Chunk #{i} (Tokens: #{chunk.token_count}):\n#{chunk.text}\n")
30-
end)
14+
def run do
15+
{:ok, tokenizer} = Tokenizers.Tokenizer.from_pretrained("gpt2")
3116

32-
IO.puts("\n=== Sentence-based Chunking ===")
33-
{:ok, sentence_chunks} = Chunx.Chunker.Sentence.chunk(text, tokenizer, chunk_size: 50, chunk_overlap: 10)
34-
Enum.each(Enum.with_index(sentence_chunks), fn {chunk, i} ->
35-
IO.puts("Chunk #{i} (Tokens: #{chunk.token_count}):\n#{chunk.text}\n")
36-
end)
17+
show("Token", Chunx.Chunker.Token.chunk(@text, tokenizer, chunk_size: 20))
18+
show("Word", Chunx.Chunker.Word.chunk(@text, tokenizer, chunk_size: 20))
19+
show("Sentence", Chunx.Chunker.Sentence.chunk(@text, tokenizer, chunk_size: 20))
20+
show("Recursive", Chunx.Chunker.Recursive.chunk(@text, tokenizer, chunk_size: 20))
21+
end
3722

38-
IO.puts("\n=== Recursive Chunking ===")
39-
{:ok, recursive_chunks} = Chunx.Chunker.Recursive.chunk(text, tokenizer, chunk_size: 50)
23+
defp show(name, {:ok, chunks}) do
24+
IO.puts("\n#{name}")
4025

41-
Enum.each(Enum.with_index(recursive_chunks), fn {chunk, i} ->
42-
IO.puts("Chunk #{i} (Tokens: #{chunk.token_count}):\n#{chunk.text}\n")
26+
Enum.each(chunks, fn chunk ->
27+
IO.puts("[#{chunk.start_byte}, #{chunk.end_byte}) (#{chunk.token_count} tokens)")
28+
IO.puts(chunk.text)
4329
end)
4430
end
4531
end

lib/chunx.ex

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
defmodule Chunx do
22
@moduledoc """
3-
Chunk text with different chunking strategies.
3+
Text chunking strategies.
44
5-
Available chunking strategies:
6-
- `Chunx.Chunker.Token` - Splits text into overlapping chunks based on token count
7-
- `Chunx.Chunker.Word` - Splits text into overlapping chunks based on word boundaries
8-
- `Chunx.Chunker.Sentence` - Splits text into overlapping chunks based on sentence boundaries
9-
- `Chunx.Chunker.Semantic` - Splits text into overlapping chunks based on semantic similarity
10-
- `Chunx.Chunker.Recursive` - Recursively splits text using structural boundaries
5+
* `Chunx.Chunker.Token` splits at token offsets.
6+
* `Chunx.Chunker.Word` keeps words intact.
7+
* `Chunx.Chunker.Sentence` keeps sentences intact and supports overlap.
8+
* `Chunx.Chunker.Recursive` tries structural boundaries before tokens.
9+
* `Chunx.Chunker.Semantic` splits where sentence similarity decreases.
1110
"""
1211
end

lib/chunx/chunk.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ defmodule Chunx.Chunk do
2222
embedding: Nx.Tensor.t() | nil
2323
}
2424

25+
@doc "Creates a chunk. Byte offsets use a half-open range."
2526
@spec new(String.t(), non_neg_integer(), non_neg_integer(), pos_integer(), Nx.Tensor.t() | nil) ::
2627
t()
2728
def new(text, start_byte, end_byte, token_count, embedding \\ nil)

lib/chunx/chunker.ex

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule Chunx.Chunker do
22
@moduledoc """
3-
Defines the interface for text chunking strategies.
3+
Defines the shared callback types for chunkers.
44
"""
55

66
alias Chunx.{Chunk, SentenceChunk, Tokenizer}
@@ -9,9 +9,10 @@ defmodule Chunx.Chunker do
99
@type chunk_result :: {:ok, [Chunk.t()] | [SentenceChunk.t()]} | {:error, term()}
1010

1111
@doc """
12-
Splits text into chunks using the given tokenizer. Semantic chunkers receive
13-
an embedding function as the third argument and may receive options as a
14-
fourth argument.
12+
Splits text using a tokenizer.
13+
14+
Semantic chunkers take an embedding function instead of options as the third
15+
argument and accept options as a fourth argument.
1516
1617
## Parameters
1718
* `text` - The text to chunk

lib/chunx/chunker/recursive.ex

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule Chunx.Chunker.Recursive do
22
@moduledoc """
3-
Implements recursive text chunking from coarse document boundaries to tokens.
3+
Splits text through an ordered sequence of structural boundaries.
44
55
Text is split using each configured level in order. Segments that still exceed
66
`:chunk_size` are passed to the next level, while adjacent segments are merged
@@ -41,20 +41,21 @@ defmodule Chunx.Chunker.Recursive do
4141
:tokens
4242
]
4343

44-
@type level :: [String.t()] | :whitespace | :tokens
44+
@type level :: nonempty_list(String.t()) | :whitespace | :tokens
4545
@type chunk_opts :: [
4646
chunk_size: pos_integer(),
47-
levels: [level()]
47+
levels: nonempty_list(level())
4848
]
4949

5050
@default_opts [chunk_size: 512, levels: @default_levels]
5151

5252
@doc """
53-
Recursively splits text into chunks no larger than `:chunk_size` tokens.
53+
Recursively splits text toward the `:chunk_size` target.
5454
5555
## Options
5656
57-
* `:chunk_size` - Maximum number of content tokens per chunk (default: 512).
57+
* `:chunk_size` - Target maximum content-token count (default: 512). An
58+
indivisible grapheme may exceed the target.
5859
* `:levels` - Ordered splitting levels. Each level is a non-empty list of
5960
delimiters, `:whitespace`, or `:tokens`. Token splitting is always used as
6061
a final fallback when custom levels are exhausted.

0 commit comments

Comments
 (0)