Kindling is an open-source hybrid recommender system with a Python API and a Rust numerics core. Released under Apache 2.0 on PyPI as kindling-rec. It fuses a closed-form base (EASE for small catalogs, Wilson-normalized cooccurrence for large ones) with auto-gated refinement channels that switch on by the measured shape of the data at fit time, with no training loop, no GPU, and sub-millisecond serving.

Published · v1.0.3

Kindling

A hybrid recommender that catches fast and grows with your data. Point it at a table of interactions and it lights, even with barely anything to burn; feed it more and it gets sharper. One fused, closed-form base score per (user, item), plus refinement channels that switch themselves on by the measured shape of the data. No training loop, no GPU, no neural net, no knobs to babysit. The numerics run in a Rust core.

Apache 2.0 Python 3.11+ Rust core PyPI · kindling-rec
View on GitHub View on PyPI Read the story →

Watch it grow, on real data

Accuracy as a function of how much history the model has seen, from 1% of training data (cold) to 100% (warm). Pick any of the 11 benchmark datasets; the ember line is Kindling. Every point is straight from the repo's warming_*.json runs.

On repeat-regime datasets (grocery, retail) the evaluation credits reorders, and that's where Kindling pulls away from the field. On discovery datasets, popularity is a stubborn cold-start baseline; Kindling's edge shows up as the data warms and it ends on top. The negative results are filed beside the wins in EXPERIMENTS.md.

Closed-form, gated, and honest about it

The fashionable answer to recommendation is a deep model and a training loop. Kindling takes the other bet: shallow, closed-form models, assembled with care and gated per dataset, go toe to toe with the approaches assumed to have made them obsolete. Every channel is closed-form or a counting statistic; every channel is switched on by a measurable property of the data; every gate exists because the ungated version measurably hurt somewhere.

Why it exists

You shouldn't have to be a recommender-systems expert to get a good one running. Kindling does the tuning by reading your data at fit() time and turning on only the machinery that data can actually support. No config flags to guess at.

What you get

One fused base score per (user, item), plus z-normalized refinement channels, plus a boost layer and cold-slots, computed end to end in a Rust core. Sub-millisecond single recommendations; batch runs in parallel with the GIL released.

Channels turn on by regime, not configuration

Every decision below is made from the data at fit() time. You don't pick the model; the shape of your interactions picks it.

Channel Turns on when Does
EASE base catalog ≤ 20k items closed-form item-item base (matrix inversion, in Rust)
cooccurrence base catalog > 20k items Wilson-normalized cooccurrence, scales past EASE
trend timestamps present recency-weighted popularity drift
transitions timestamps present, not a rating-burst sequential last-item → next-item signal
user-CF sparse-history data user-user collaborative signal where it pays
rating-weighting true ratings present weights the base by explicit rating, not just clicks
repeat gate genuine reorder regime (held-out check) promotes repeat consumption; declines fake-repeat
The activation table, condensed. Full gate logic in REFERENCE.md §2.

Installation

pip, from PyPI
pip install kindling-rec
from source, with dev extras
pip install -e ".[dev]"

Runtime dependencies are just numpy / pandas / scipy. The linear algebra that matters (the EASE inversion, the channel blend, the serving path) runs on a pure-Rust core, kindling_core, so there's no PyTorch, no BLAS system dependency, and no GPU. A wheel that imports is a wheel that works. Add ".[dev,bench]" for the benchmark harness.

Quick start

Fit on a table of interactions, then recommend. New and anonymous users are first-class.

Fit and recommend
from kindling import Engine
from kindling.loaders import movielens

# columns: entity_id, item_id, timestamp[, rating]
interactions = movielens.load_1m()

engine = Engine()
engine.fit(interactions)              # reads the data, gates the channels

for rec in engine.recommend(entity_id=42, n=10):
    print(rec.item_id, rec.score, rec.base_kind)

# Many users at once, parallel in the Rust core (GIL released)
batches = engine.recommend_batch([42, 99, 7], n=10)
Cold and anonymous users, no per-user training
# personalized from a handful of seed items
engine.recommend_for_items(item_ids=[101, 205], n=10)

# nothing to go on? clean popularity fallback, same code path
engine.recommend_for_items(item_ids=[], n=10)

Where it stands

Full-ranking NDCG@10, engine defaults. Strongest personalized model on all four headline datasets, and it beats implicit ALS everywhere. No training loop, no GPU.

Dataset NDCG@10 What activates
movielens-1m 0.293 rating-weighted EASE
steam (realistic) 0.066 open-catalog cooccurrence + cold slots
amazon-beauty 0.033 + user-CF channel
amazon-book-chrono 0.032 timestamps activate trend / transitions
Engine-default NDCG@10. The full record, including the negative results, is in EXPERIMENTS.md.
The repeat regime is where it separates

On datasets where people genuinely rebuy and replay (grocery, retail), a held-out gate turns on reorder recommendation. Under repeat-aware evaluation, Kindling pulls away from the field — Dunnhumby 0.48 NDCG@10 versus ~0.05 for every baseline — while correctly declining on fake-repeat data like Steam, where a re-log isn't a repurchase. The gate that turns the win on is the same gate that refuses the false one.

Serving performance

Native engine, measured by bench/final_state_perf.py. The recommend path is pure Rust.

Dataset Fit Single recommend (p50) Batch throughput
movielens-1m 4.2 s 0.17 ms 15.4k recs/s
amazon-beauty 13.1 s 1.21 ms 3.0k recs/s
steam 110 s 5.81 ms 0.8k recs/s
Single recommend dropped from ~200 ms on the earlier Python path to sub-millisecond, with byte-identical rankings.
Single recommend 0.17 ms p50 on movielens-1m, pure-Rust serving path.
Python → Rust ~1000× ~200 ms down to sub-millisecond, rankings byte-identical.
Repeat regime ~10× Dunnhumby 0.48 vs ~0.05 NDCG@10 against every baseline.

Architecture

A thin Python API over a Rust numerics core. Python reads your data, decides the gates, and orchestrates; the core does the linear algebra and the hot serving path.

Python
kindling
Engine.fit/recommend, the data loaders, the gate decisions, and the serving wrapper. Depends on numpy / pandas / scipy only.
Rust
kindling_core
The EASE inversion, the channel blend, the boost layer, cold-slots, and the batch recommend path. GIL released for parallel batches. No BLAS, no GPU.
Serving as an artifact

Persist a fit as a self-contained artifact and serve it with no re-fit: KindlingServer.from_engine(engine).save("artifact/"), then KindlingServer.load("artifact/") in the serving process. A FastAPI example ships behind an optional extra.

Who it's for

Small teams
A good recommender without a research budget
No GPU bill, no training pipeline to maintain, no hyperparameter sweep. Install, fit, serve. The defaults are the tuning.
Cold catalogs
When most users have almost no history
Cold start is the baseline, not a bolt-on. New and anonymous users are served from seed items immediately, popularity only when there's truly nothing to go on.
Reorder businesses
Grocery, retail, anything people rebuy
Repeat consumption is first-class. The repeat gate turns it on where it pays and refuses it where re-logs aren't repurchases.
Honest benchmarking
A measured baseline to beat
A strong, fully-documented closed-form baseline, negative results included, before you spend a quarter on something deeper.

Documentation & citation

License

Apache 2.0, free for commercial, research, and government use. Alpha, and PRs are welcome via GitHub Issues and Pull Requests.

BibTeX
@software{kindling2026,
  author  = {Hoekstra, Robert},
  title   = {Kindling: A Hybrid Recommender that Grows with Your Data},
  year    = {2026},
  url     = {https://github.com/rhoekstr/kindling},
  version = {1.0.3}
}

What Kindling is not

Not deep learning

No neural net, no embeddings to train, no GPU. If you need a two-tower model, this isn't it.

Not novel

A careful assembly of known, closed-form pieces. The contribution is the gating and the honesty, not a new algorithm.

Not state of the art

A solid engineering choice, measured against the field. Strongest personalized model on the test sets, not a leaderboard trophy.

Also from Awry Labs

Gravel takes the same shape to a different problem: a Python API over a fast native core (C++ there, Rust here), closed-form math instead of a training loop. It measures how networks come apart under stress.