Skip to main content

chunkshop (Python)

Reference implementation of the chunkshop ingest tool. v0.5.0 beta.

New here? Start with the end-to-end tutorial — a guided walkthrough from zero (no Postgres) to a running semantic query.

This file is the field-by-field reference: every CLI flag, every YAML field, the troubleshooting table. Use it alongside the tutorial once you know what you're doing.

For the high-level shape and mermaid diagram, see the top-level README.

Install

From source when you want sample corpora, dev tooling, or unreleased branch work:

git clone https://github.com/yonk-labs/chunkshop && cd chunkshop/python
uv sync --extra dev --extra all-backends

Published package:

pip install 'chunkshop[all-backends]'

As a path dependency from another project:

[tool.uv.sources]
chunkshop = { path = "../chunkshop/python", editable = true }

Optional extras:

Extra What you get
extractors rake-nltk + nltk for the RAKE extractor.
keybert keybert + sentence-transformers for the keybert_phrases extractor.
spacy spacy for the spacy_entities NER extractor.
lang langdetect for the lang_detect extractor.
nlp Umbrella: keybert + spacy + lang in one install.
lede lede>=0.4.5 for summary_embed, query hints, and lede_report document metadata.
lede-spacy lede-spacy>=0.4.5 for optional spaCy-backed lede hint expansion.
sumy sumy + NLTK corpora for the sumy adapter shim (chunkshop.summarizers.sumy).
quantize onnx for on-the-fly quantization scratch.
dev pytest, pytest-asyncio, onnx.

Python ≥ 3.12 required.

Prerequisites

  • One supported target backend: Postgres ≥ 14 + pgvector, MariaDB 11.7+, SQLite + sqlite-vec, or ClickHouse 24.10+.
  • For Postgres, CREATE EXTENSION vector; must succeed in your target DB.
  • Disk space for model cache in ~/.cache/fastembed/ — ~85 MB for int8 bge-base, ~550 MB for nomic.
  • An env var holding your DSN. The target config references it by name, not by value.

Quick run

export CHUNKSHOP_DSN="postgresql://postgres:postgres@localhost:5432/mydb"

# Point at the sample corpus in docs/samples/ for a real end-to-end run:
chunkshop ingest --config ../docs/samples/sample.yaml

# Or copy the template and edit it:
cp src/chunkshop/configs/example-files-to-bge.yaml my-cell.yaml
chunkshop ingest --config my-cell.yaml

Success looks like:

{
  "cell_name": "example_files",
  "docs_processed": 47,
  "chunks_written": 312,
  "wall_seconds": 18.4,
  "error": null
}

CLI

Two subcommands: ingest (one cell) and orchestrate (many cells in parallel).

chunkshop ingest

Runs one YAML end-to-end.

chunkshop ingest --config PATH [--doc-limit N] [--log PATH] [--omp-threads N]
Flag YAML override Purpose
-c, --config — Required. Path to YAML.
--doc-limit runtime.doc_limit Smoke-test mode; stop after N docs.
--log runtime.log_path Append stdout log lines to this file.
--omp-threads runtime.omp_num_threads Cap BLAS/OMP threads before ORT loads.

Exit code: 0 on success, 1 if the cell errored. Stdout = a JSON summary.

chunkshop orchestrate

Runs N cells in parallel as subprocesses.

chunkshop orchestrate (--config-dir DIR | --config PATH [--config PATH ...])
                      [--concurrency N]
                      [--checkpoints "60,120,300,600"]
                      [--timeout SECONDS]
                      [--smoke | --full]
Flag Default Purpose
-d, --config-dir — Run every *.yaml/*.yml in the directory.
-c, --config — Explicit path; repeatable. Mutually exclusive with --config-dir.
--concurrency 4 Max parallel cells (subprocess pool size).
--checkpoints 60,120,300,600 Seconds at which to print a status report.
--timeout 7200 (2h) Overall wall limit; survivors get SIGTERM to their process group.
--smoke off Force doc_limit=1 + concurrency=1. Useful for "does it crash".

Stdout = checkpoint reports during the run, JSON summary at the end.

chunkshop bakeoff

Runs a chunker × embedder matrix against a corpus with hand-written gold queries, scores recall@k + MRR per combo, writes a leaderboard + a runnable recommended.yaml. Config-driven — the matrix lives in YAML, not on the command line.

chunkshop bakeoff --config PATH [--dsn DSN] [--yes] [--keep-schema]
Flag Default Purpose
--config — Path to the bakeoff YAML. Required.
--dsn $CHUNKSHOP_DSN Postgres DSN. Required (env var or flag).
--yes off Bypass the >50-cell matrix confirmation prompt.
--keep-schema off Keep the bakeoff schema after run — useful for debugging.

Outputs land in skill-output/bakeoff/{name}/:

  • results.json — raw per-combo + per-query data.
  • report.md — leaderboard sorted by MRR, per-query detail, statistical- power caveat.
  • recommended.yaml — top combo pre-filled as a runnable chunkshop ingest cell.

Full walkthrough: ../docs/tutorial-bakeoff.md. Recipe card: ../docs/quickstart-bakeoff.md.

YAML reference

Every cell config has five sections plus an optional runtime. Extra keys are rejected (extra="forbid" in pydantic), so typos fail loudly.

cell_name: my_cell
source:   { ... }
chunker:  { ... }
embedder: { ... }
extractor: { ... }   # optional, defaults to {type: none}
target:   { ... }
runtime:  { ... }    # optional, sensible defaults below

source

type Required fields Optional fields
files glob id_from: path | stem | sha1 (default stem), encoding (utf-8)
json_corpus path documents_key (documents), id_field (id), content_field (content), title_field (title)
pg_table dsn_env, schema, table, id_column, content_column title_column, where
http urls or sitemap — (stub today)
s3 bucket prefix (stub today)

chunker

Seven chunkers in three families. Pick one per cell.

Structural — split on headings, paragraphs, or word counts:

type Required Defaults
sentence_aware — doc_type: prose (or code), max_chars: 2000, min_chars: 200
fixed_overlap — window_words: 300, step_words: 150
hierarchy — prefix_heading: true, min_section_chars: 100, max_chars: 2000
neighbor_expand base: (nested chunker) window: 1

Semantic — splits on embedding-drift boundaries (no heading needed):

type Required Defaults
semantic — boundary_model: "sentence-transformers/all-MiniLM-L6-v2-int8", breakpoint_percentile: 95, min_sentences_per_chunk: 3, max_chunk_chars: 2000, sentence_splitter: "naive"

Pass boundary_model: "same" to reuse the cell's main embedder (trades speed for memory). See ../docs/tutorial-semantic.md.

Summary-layer — wrap any base chunker and change what gets embedded vs. what gets stored (summary_embed) or emit fine+coarse rows linked by group_id (hierarchical_summary):

type Required Defaults
summary_embed base:, summarizer: —
hierarchical_summary base:, summarizer:, grouping: grouping: {strategy: fixed_n, n: 5}

The summarizer config is a discriminated union: {mode: external, field: ...} pulls a pre-computed summary from a source document metadata field; {mode: callable, module: "lede.tfidf", function: "summarize", kwargs: {...}} imports lazily at first use; {mode: passthrough} reuses the raw chunk as the summary (baseline). See ../docs/summaries.md and ../docs/tutorial-summaries.md.

Full per-chunker guidance: ../docs/chunkers.md.

embedder

Only fastembed today.

Field Required Default Notes
type yes — Literal fastembed.
model_name yes — e.g. Xenova/bge-base-en-v1.5-int8. See embedders.md.
dim yes — Must match the model. Mismatch fails loudly at first embed.
batch_size no 64 Per-call batch to fastembed.embed.
threads no None None = auto (bad on shared boxes). Set to 4 typically.

extractor

type Fields
none — (default)
rake_keywords top_k: 10, min_chars: 3 (defaults)

RAKE downloads NLTK corpora (stopwords, punkt) on first use to ~/nltk_data/.

target

Field Required Default Notes
dsn_env no AGE_BAKEOFF_PGRG_DSN Name of the env var holding your DSN. Override this to CHUNKSHOP_DSN in your configs.
schema yes — Lowercase ident; must match ^[a-z_][a-z0-9_]*$. Created if missing.
table yes — Same ident rule.
mode no overwrite One of overwrite, append, create_if_missing. See ../docs/tutorial-multi-source.md.
source_tag when mode=append null Ident-safe tag written to every row's source column. Required for append; optional (but recommended) for overwrite/create_if_missing.
promote_metadata no [] List of {path, type} pairs lifting jsonb metadata paths into typed columns. path is lowercased + . → __ for the column name.
force_overwrite no false Bypasses the "refuse to drop a table that holds rows from a foreign source_tag" safety check in overwrite mode.
overwrite no (soft-deprecated) false Legacy boolean. Still honored when mode=overwrite (acts as the DROP+CREATE switch). Prefer the new mode field for new configs.
hnsw no true false for tiny test tables where HNSW is slower than seq scan.

target.documents (Python/Postgres only)

For Postgres targets in the Python implementation, target.documents.enabled: true writes a companion one-row-per-document table next to the chunk table. It is off by default for compatibility.

target:
  type: postgres
  dsn_env: CHUNKSHOP_DSN
  database: mydata
  table: chunks
  documents:
    enabled: true
    table: documents
    store_full_content: true
    store_lede_report: true

Current support boundary:

  • Python + Postgres: supported.
  • Python + SQLite/MariaDB/ClickHouse: rejected at config load.
  • Rust: rejected at config load when documents.enabled: true; Rust does not write the companion document table yet.

Multi-source ingest

Multiple cells can write to the same table by tagging each cell's rows with a source_tag. Cell A creates the table with mode: create_if_missing, Cell B appends with mode: append and its own tag. Queries filter or group by the source column. See ../docs/tutorial-multi-source.md for the end-to-end walkthrough.

target:
  dsn_env: CHUNKSHOP_DSN
  schema: mydata
  table: all_docs
  mode: append
  source_tag: support_tickets

runtime

Field Default Notes
omp_num_threads 1 Sets OMP/MKL/OPENBLAS/NUMEXPR env vars before ORT loads.
doc_limit null Stop after N docs. Smoke-test lever.
log_path null Mirror stdout heartbeats to this file. Parent dirs auto-created.
heartbeat_every 25 Log a progress line every N docs.

Environment variables

Var When chunkshop reads it
$<target.dsn_env> (default AGE_BAKEOFF_PGRG_DSN) At sink construction; must be a valid libpq DSN.
OMP_NUM_THREADS and friends Set by runner before any numpy/ORT import.
HF_HOME / HF_HUB_CACHE Respected by fastembed's downloader if you've moved the cache.

Troubleshooting

"no files matched glob: /path/**/*.md"

Your source.glob didn't match anything. Test it in a shell first:

ls /path/**/*.md | head

Note that chunkshop uses Python's glob.glob(..., recursive=True) — ** only matches across directories when it's its own path component (/foo/**/*.md, not /foo/**.md).

"relation already exists" on second run

target.overwrite is false by default. Either flip it to true (drops + recreates) or drop the table yourself. The ON CONFLICT DO UPDATE in the writer will also happily upsert into an existing table.

"model X produced dim Y, config says dim=Z"

Your YAML's embedder.dim doesn't match the model's output. Look up the right dim in ../docs/embedders.md — bge-small=384, bge-base=768, nomic=768.

"CREATE EXTENSION IF NOT EXISTS vector" fails with permission denied

Your DB role can't create extensions. Ask a superuser to run it once per database:

CREATE EXTENSION IF NOT EXISTS vector;

Then re-run chunkshop — the sink's CREATE EXTENSION IF NOT EXISTS will be a no-op.

"table/schema must match ^[a-z_][a-z0-9_]*$"

chunkshop refuses to interpolate mixed-case or quoted identifiers — SQL injection safety via allowlist. Lowercase your schema and table.

Ingest is slow and my CPU fans are loud

Three knobs. Pick one:

  • Drop embedder.batch_size from 64 to 32 — less memory pressure, slower per-doc.
  • Set embedder.threads: 4 (or 2) — caps ORT's worker pool.
  • If running under orchestrate, reduce --concurrency.

See the thread-tuning table in ../docs/embedders.md.

First run hangs on "downloading model"

Fastembed is pulling the ONNX from HuggingFace. Network / HF outage. Check curl -sI https://huggingface.co/ and your proxy settings. The file lands in ~/.cache/fastembed/<model-name>/.

nltk errors on first rake_keywords run

The extractor downloads stopwords, punkt, punkt_tab into ~/nltk_data/ on first use. Behind a strict firewall? Pre-download once:

import nltk
for r in ("stopwords", "punkt", "punkt_tab"):
    nltk.download(r)

Using chunkshop as a library

from chunkshop import load_config
from chunkshop.runner import run_cell

cfg = load_config("my-cell.yaml")
result = run_cell(cfg)
print(result.docs_processed, result.chunks_written, result.wall_seconds)

Or skip the YAML and build a CellConfig directly — every section is a plain pydantic model.

Tests

cd python
uv run pytest

Most tests are offline. test_embedder_fastembed.py and test_int8_registry.py download the int8 bge-base model on first run and cache it — budget ~85 MB + a few seconds the first time.

Metadata

Release files for chunkshop 0.9.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for chunkshop 0.9.1
File Size Uploaded Provenance
chunkshop-0.9.1.tar.gz 224.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for chunkshop 0.9.1
File Interpreter ABI Platform
chunkshop-0.9.1-py3-none-any.whl Python 3 none any Details

Total release size: 521.3 kB

Release files / chunkshop-0.9.1.tar.gz

Download URL chunkshop-0.9.1.tar.gz
Size 224.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1e733e10faf138fa171dc441cdfee85e5bc7d847e6fd94e1fcbb0f53309b1942
BLAKE2b-256 checksum
How to use checksums
137c93159e3cb547e4370a4cf74b7fbc83685f6f8d14fe66c912e6daa5c75056
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 9, 2026.

Transparency log

Release files / chunkshop-0.9.1-py3-none-any.whl

Download URL chunkshop-0.9.1-py3-none-any.whl
Size 296.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
53e862b77889e896ef170efeed65e33822cc9cbec9cb3f73d5c98dc70e2cef68
BLAKE2b-256 checksum
How to use checksums
61871d8c0fae037761c0a81b973f057d1cf59ae8800b34a923727fb7406e1f3e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 9, 2026.

Transparency log

Security for chunkshop 0.9.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for chunkshop-0.9.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 9, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page