Embed text-chunker JSON output into PostgreSQL + pgvector for semantic search.
Takes structural chunks (headings, paragraphs, code blocks, math, etc.) from Markdown or LaTeX documents, encodes them with BGE-M3 (1024-dim multilingual embeddings), and stores both the text and vectors in PostgreSQL. Supports semantic similarity search via the query subcommand.
https://github.com/tlkahn/chunk-embed/releases/download/v0.2.0/demo.mp4
Requires Python 3.13+, PostgreSQL with pgvector. Optional: Sentenza for sentence-level splitting.
cd ~/Projects/chunk-embed
uv synccreatedb chunk_embed
psql chunk_embed -c "CREATE EXTENSION IF NOT EXISTS vector;"text-chunker --json chunks document.md | chunk-embed ingest --source document.mdtext-chunker --json chunks document.md > chunks.json
chunk-embed ingest chunks.jsonSource path is inferred from the file argument. Override with --source:
chunk-embed ingest chunks.json --source original/path/to/document.mdNote: --source is required when reading from stdin, since there is no filename to infer from:
# stdin: no filename, so --source is required
text-chunker --json chunks file.md | chunk-embed ingest --source file.md
# without --source, this fails with an error
text-chunker --json chunks file.md | chunk-embed ingest
# → "Error: --source is required when reading from stdin"When passing a file argument, --source defaults to that path automatically:
# --source defaults to "chunks.json"
chunk-embed ingest chunks.jsonBy default, prose chunks (paragraphs, headings, block quotes, etc.) are split into individual sentences via Sentenza before embedding. Each sentence gets its own vector, improving retrieval precision.
# Default: split English text into sentences
text-chunker --json chunks document.md | chunk-embed ingest --source document.md
# Specify language for sentence splitting
chunk-embed ingest chunks.json --lang de
# Disable sentence splitting (embed whole chunks)
chunk-embed ingest chunks.json --no-splitSplittable chunk types: paragraph, block_quote, definition_item, theorem, list_item, heading. Non-splittable types (code_block, math_block, table) pass through unchanged.
If the sentenza binary is not found, splitting is skipped gracefully and chunks are embedded whole.
Parse and embed without writing to the database:
chunk-embed ingest chunks.json --dry-runSearch ingested chunks by semantic similarity:
chunk-embed query "how does authentication work"Output as JSON for programmatic use:
chunk-embed query "error handling patterns" --jsonFilter by source file or chunk type:
chunk-embed query "database schema" --source docs/architecture.md
chunk-embed query "function signatures" --chunk-type code_block --top-k 5Set a minimum similarity threshold:
chunk-embed query "yoga philosophy" --threshold 0.5| Option | Default | Description |
|---|---|---|
INPUT |
stdin | JSON file path, or - for stdin |
--source |
inferred from file | Source path stored as document metadata |
--batch-size |
32 | Embedding batch size |
--database-url |
postgresql://localhost/chunk_embed |
Connection string (env: DATABASE_URL) |
--lang |
en |
ISO 639-1 language code for sentence splitting |
--no-split |
off | Disable sentence splitting |
--dry-run |
off | Skip database write |
| Option | Default | Description |
|---|---|---|
QUERY_TEXT |
(required) | Text to search for |
--top-k |
10 | Number of results to return |
--source |
all | Filter results by source path |
--chunk-type |
all | Filter results by chunk type |
--threshold |
0.0 | Minimum similarity score (-1 to 1) |
--json |
off | Output results as JSON |
--database-url |
postgresql://localhost/chunk_embed |
Connection string (env: DATABASE_URL) |
Expects JSON output from text-chunker --json chunks:
{
"total_chunks": 3,
"mode": "document",
"chunks": [
{
"text": "# Introduction",
"chunk_type": "heading",
"heading_context": ["Introduction"],
"heading_level": 1,
"page_number": null,
"source_line_start": 1,
"source_line_end": 1
}
]
}Supported chunk types: heading, paragraph, list_item, code_block, table, block_quote, definition_item, math_block, theorem.
Running chunk-embed ingest on a document that was previously ingested replaces the old data. The source_path column has a unique constraint — the old document and all its chunks are deleted before inserting the new version.
Two tables:
- documents — one row per ingested source file (source path, mode, chunk count)
- chunks — one row per chunk (text, metadata, 1024-dim embedding vector)
Chunks reference their parent document with ON DELETE CASCADE. An HNSW index on the embedding column enables fast cosine similarity search.
text-chunker (Rust CLI)
│
│ JSON chunks on stdout
▼
chunk-embed (Python CLI)
│
├── parse.py → validate JSON, produce ChunkData objects
├── split.py → sentence splitting via Sentenza CLI
├── embed.py → BGE-M3 encoding via sentence-transformers
├── store.py → pgvector storage + cosine similarity search
├── format.py → human-readable and JSON output formatters
└── cli.py → Click group: ingest + query subcommands
│
▼
PostgreSQL + pgvector
# Fast tests (no DB, no model)
uv run pytest -m "not integration and not slow"
# Integration tests (needs PostgreSQL)
createdb chunk_embed_test
psql chunk_embed_test -c "CREATE EXTENSION IF NOT EXISTS vector;"
uv run pytest -m integration
# Sentenza tests (needs sentenza binary in PATH)
uv run pytest -m sentenza
# Model tests (downloads ~2GB on first run)
uv run pytest -m slow
# All tests
uv run pytestA pre-built .dmg is available from the Releases page. The app is ad-hoc signed (not notarized by Apple), so macOS Gatekeeper will block it by default.
After mounting the .dmg and dragging Chunk Embed into Applications:
- Double-click the app. You will see a dialog: "Chunk Embed" can't be opened because Apple cannot check it for malicious software.
- Open System Settings > Privacy & Security, scroll down — you will see a message about "Chunk Embed" being blocked. Click Open Anyway.
- Alternatively, right-click (or Control-click) the app and choose Open, then click Open in the dialog.
You only need to do this once. After the first launch, macOS remembers your choice.
The app bundles Python and all dependencies. You still need:
- PostgreSQL with the pgvector extension installed
- An internet connection on first launch (to download the BGE-M3 model, ~2 GB)
The Setup tab inside the app checks dependency status and provides install guidance.
MIT