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.
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 recommend0.17 msp50 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.
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.