Zero-dependency collaborative filtering engine built for performance.
Table of Contents
- Why nano-recommender
- Features
- Installation
- Quick Start
- Packaging Support
- WebAssembly (Wasm) Acceleration
- Recommendation Strategies
- Item-Based Collaborative Filtering
- User-Based Collaborative Filtering
- Popularity & Cold Start Fallbacks
- Time-Decay Weighting
- Recommendation Filtering & Blacklisting
- Similarity Intersection Threshold
- K-Nearest Neighbors (KNN) Limit
- Content-Based Filtering
- Hybrid Recommendation Strategy
- Explainable Recommendations
- Session-Based Recommendations
- Web Worker Support
- Offline Evaluation Suite
- Developer Experience (DX) & Builder API
- Error Handling
- Design Rationale & Technical Trade-offs
- Performance
- API Reference
- Architecture
- Contributing
- License
Why nano-recommender
The library is a lightweight, zero-dependency, in-memory recommendation engine built to run efficiently in Node.js and browser environments.
It is designed for use-cases requiring rapid collaborative filtering and fallback recommendations without the overhead of heavy native dependencies, external databases, or machine learning pipelines.
Design Pillars
- Zero Runtime Dependencies: Avoids dependency bloat. Relies entirely on native JavaScript and TypeScript features.
- Sparse Matrix Optimization: Ratings are stored in memory using sparse user-item maps and item-user indices, minimizing memory overhead and avoiding dense matrix allocations.
- Symmetric Similarity Cache: Pairwise similarities are computed lazily on demand and cached symmetrically, reducing subsequent query times to O(1) lookups.
- Dual Packaging: Ships with full ESM and CommonJS support alongside native TypeScript typings out of the box.
- Tree-shakable Exports: Algorithms and core utility classes are exported individually, allowing modern bundlers to remove unused code.
Positioning & Comparisons
When deciding on a recommendation library, it is helpful to position nano-recommender against other solutions in the ecosystem:
| Aspect | nano-recommender |
Heavy ML Pipelines (e.g., Python / TensorFlow) | SaaS Engines (e.g., Recombee) | NLP / Text Libraries (e.g., natural) |
|---|---|---|---|---|
| Dependencies | Zero | Heavy native packages, python bindings | External REST/gRPC client SDK | Many text-processing utilities |
| Architecture | In-memory, Real-time | Distributed clusters & servers | Cloud API dependency | Local utilities, not specialized for CF |
| Speed | Sub-millisecond / WASM | High batch throughput, high latency | Network roundtrip latency | Moderate JS performance |
| Data Sparsity | High (via Sparse Matrix) | Handles very dense / large matrices | Scales to billions of items | No built-in sparse CF structures |
| Developer Experience | Very high (Builder API, presets) | Complex pipeline code | Simple SDK, external config | Generic algorithms, manual coding required |
Use nano-recommender when you need a fast, zero-dependency, in-memory collaborative filtering system that runs locally in Node.js or the browser, with real-time updates and seamless WebAssembly acceleration.
Features
| Feature | Supported | Description |
|---|---|---|
| WebAssembly Acceleration | Yes | Accelerates vector math calculations (Cosine, Jaccard, Pearson) using Rust |
| Item-Based Collaborative Filtering | Yes | Recommends items based on item-item similarity matrices |
| User-Based Collaborative Filtering | Yes | Recommends items based on user-user similarity matrices |
| K-Nearest Neighbors (KNN) Limit | Yes | Limits calculations to top K nearest neighbors for performance |
| Similarity Intersection Threshold | Yes | Avoids statistical coincidences by enforcing minimum overlap |
| Custom Similarity Metrics | Yes | Built-in Cosine, Jaccard, and Pearson correlation coefficients |
| Custom Filtering & Blacklisting | Yes | Filters recommendations dynamically via callback or blacklist |
| Real-Time Incremental Updates | Yes | Live interaction updates with selective cache invalidation |
| Popularity Fallback Engine | Yes | Handles cold-start users using view, rate, and purchase counts |
| Time-Decay Weighting | Yes | Automatically decays older interaction ratings exponentially |
| Dynamic Interaction Weighting | Yes | Scales rating scores based on interaction types at load time |
| LRU Similarity Cache | Yes | Prevents memory bloat with configurable LRU eviction limits |
| Sparse Storage Engine | Yes | Operates entirely in memory with sparse indices |
| TypeScript Ready | Yes | Written in strict TypeScript with full declaration files |
Installation
Install via npm:
npm install @fizm/nano-recommenderInstall via pnpm:
pnpm add @fizm/nano-recommenderInstall via yarn:
yarn add @fizm/nano-recommenderCompatibility Matrix
| Environment | Minimum Supported Version | Recommended | Notes |
|---|---|---|---|
| Node.js | >= 18.0.0 |
>= 20.0.0 |
Required for structured clone and WebAssembly compatibility |
| Browsers | Modern Browsers | Latest | Requires WebAssembly and structuredClone() support (for Workers) |
| TypeScript | >= 4.5.0 |
>= 5.0.0 |
Compatible with strict null checking |
Quick Start
The following is a complete, compilable TypeScript example showing how to load a dataset and generate recommendations.
import { NanoRecommender } from "@fizm/nano-recommender";
// 1. Initialize the engine
const recommender = new NanoRecommender({
defaultStrategy: "item-based",
defaultSimilarityThreshold: 0.0,
});
// 2. Load interaction datasets
recommender.load([
{ userId: "u1", itemId: "i1", rating: 5.0, type: "rate" },
{ userId: "u1", itemId: "i2", rating: 3.0, type: "rate" },
{ userId: "u2", itemId: "i1", rating: 4.0, type: "rate" },
{ userId: "u2", itemId: "i2", rating: 3.0, type: "rate" },
{ userId: "u2", itemId: "i3", rating: 2.0, type: "rate" },
{ userId: "u3", itemId: "i2", rating: 4.0, type: "rate" },
{ userId: "u3", itemId: "i3", rating: 5.0, type: "rate" },
]);
// 3. Generate recommendations for a user
const recommendations = recommender.recommend("u1", {
limit: 2,
strategy: "item-based",
});
// Output: [{ itemId: "i3", score: 3.5 }]
console.log(recommendations);Packaging Support
The package supports both ESM and CommonJS formats.
ESM Import (Default)
import {
NanoRecommender,
cosineSimilarity,
pearsonCorrelation,
} from "@fizm/nano-recommender";CommonJS Require
const {
NanoRecommender,
cosineSimilarity,
pearsonCorrelation,
} = require("@fizm/nano-recommender");WebAssembly (Wasm) Acceleration
The library contains a high-performance WebAssembly backend compiled from Rust using wasm-bindgen. It accelerates vector mathematics calculations (Cosine Similarity, Jaccard Similarity, and Pearson Correlation) on large-scale datasets while maintaining zero runtime dependencies.
Features
- Zero-Dependency base64 Inlining: The Wasm binary is encoded as a Base64 string and embedded directly inside the code (
wasm-binary.ts). This allows it to run out of the box in both Node.js and browser environments without needing file system reads or network requests. - Automatic Loading: Instantiating
NanoRecommenderautomatically triggers asynchronous loading of the Wasm module in the background. If the module is not yet loaded or if the environment doesn't support WebAssembly, the engine silently falls back to pure JavaScript/TypeScript calculations. - Web Worker Compatibility: The Wasm module is automatically pre-loaded when using the Web Worker API, providing asynchronous multithreaded recommendation queries fully accelerated by WebAssembly.
- Adaptive Auto-Routing: Under the default
"auto"strategy, the engine automatically routes small/sparse vectors (belowwasmMinVectorSize, defaulting to 20 interactions) in all collaborative similarity computations (item-based, user-based, and session-based) to the pure JS engine. This prevents the boundary marshalling overhead (which can make WASM slightly slower than JS for very small arrays) while keeping WASM active for larger profiles. Thecontent-basedengine relies on category and tag metadata matching and runs entirely in pure JS. Developers can also manually override this behavior viawasmStrategy: "always" | "never".
WebAssembly Strategy Configuration
You can configure the WebAssembly engine behavior in the constructor options:
const recommender = new NanoRecommender({
// 'auto' (default): use WASM for vectors >= wasmMinVectorSize, JS for smaller vectors.
// 'always': force WebAssembly for all similarity calculations.
// 'never': force pure JavaScript/TypeScript fallback calculations.
wasmStrategy: "auto",
wasmMinVectorSize: 20, // Customize crossover size threshold
});Manual Pre-loading (Optional)
If you want to ensure WebAssembly is loaded and compiled before running any calculations, you can call the loadWasm helper:
import { loadWasm, isWasmLoaded } from "@fizm/nano-recommender";
// Pre-load and compile WebAssembly
await loadWasm();
console.log("Is WebAssembly Active:", isWasmLoaded()); // trueRecommendation Strategies
1. Item-Based Collaborative Filtering (Default)
Finds items similar to those the user has already rated. It supports custom similarity functions (e.g. Cosine, Pearson) over sparse item vectors and computes predicted ratings using a weighted average.
import { pearsonCorrelation } from "@fizm/nano-recommender";
const recs = recommender.recommendItemBased("user_id", {
limit: 10,
similarityThreshold: 0.1,
excludeInteracted: true,
similarityFunction: pearsonCorrelation,
});2. User-Based Collaborative Filtering
Finds users similar to the target user and recommends items they liked. It supports custom similarity functions (e.g. Cosine, Jaccard, Pearson).
import { jaccardSimilarity } from "@fizm/nano-recommender";
const recs = recommender.recommendUserBased("user_id", {
limit: 10,
similarityThreshold: 0.2,
similarityFunction: jaccardSimilarity,
});3. Popularity & Cold Start Fallbacks
If a user has no interaction history (a cold-start user), recommend() automatically falls back to popularity recommendations. You can configure which interaction type counts are evaluated.
const recs = recommender.recommend("new_user_id", {
fallbackStrategy: "most-purchased", // 'most-rated' | 'most-viewed' | 'most-purchased' | 'none'
});4. Time-Decay Weighting
To prevent recommendations from getting stale, you can configure an exponential decay half-life in days. Interactions will automatically have their ratings scaled down based on how old they are relative to the latest interaction in the dataset (or a custom reference time).
const recommender = new NanoRecommender({
decayHalfLifeDays: 30, // 30-day half-life (interactions 30 days old decay to 50% weight)
});
recommender.load([
{
userId: "u1",
itemId: "i1",
rating: 5.0,
timestamp: "2026-06-12T00:00:00Z",
},
{
userId: "u1",
itemId: "i2",
rating: 5.0,
timestamp: "2026-05-13T00:00:00Z",
}, // ~30 days old -> scaled to 2.5
]);You can also supply a custom reference time:
recommender.load(dataset, { referenceTime: new Date("2026-06-12T00:00:00Z") });5. Recommendation Filtering & Blacklisting
You can filter recommendations using built-in metadata, custom filter callbacks, or explicit blacklists:
Built-in Category & Tag Filtering
When loading your dataset, you can attach an optional itemCategory and list of itemTags to each interaction. The engine will store these attributes and allow you to filter recommendations directly without writing manual callbacks:
// Load dataset with item metadata
recommender.load([
{
userId: "u1",
itemId: "i1",
rating: 5,
itemCategory: "Book",
itemTags: ["fantasy", "fiction"],
},
{
userId: "u1",
itemId: "i2",
rating: 4,
itemCategory: "Movie",
itemTags: ["sci-fi"],
},
]);
// Filter recommendations for a specific category
const bookRecs = recommender.recommend("user_id", {
filterCategory: "Book",
});
// Filter recommendations that match at least one tag (OR match)
const tagRecs = recommender.recommend("user_id", {
filterTags: ["fantasy", "adventure"],
});Custom Callback & Blacklisting
You can also supply a custom filter function or an explicit blacklist of item IDs:
const recs = recommender.recommend("user_id", {
strategy: "item-based",
excludeItemIds: ["item_out_of_stock_1", "item_out_of_stock_2"],
filter: (itemId) => {
const isAdultOnly = checkAdultCategory(itemId);
const isUserMinor = checkUserIsMinor("user_id");
return !(isAdultOnly && isUserMinor);
},
});6. Similarity Intersection Threshold
To avoid statistical anomalies in sparse datasets (such as a similarity score of 1.0 between two entities sharing only a single rated item), you can enforce a minimum intersection threshold. Similarity computations will immediately exit and return 0.0 for any pairs sharing fewer than this number of common interactions:
const recs = recommender.recommend("user_id", {
minIntersectionSize: 3, // Requires at least 3 shared ratings to compute similarity
});7. K-Nearest Neighbors (KNN) Limit
To maximize prediction accuracy and reduce computational complexity under dense vectors, you can limit similarity scoring to the top K nearest neighbors (similar items in Item-Based CF, or similar users in User-Based CF):
const recs = recommender.recommend("user_id", {
k: 20, // Only compute score using the top 20 nearest neighbors
});8. Content-Based Filtering
Recommends items similar to those the user has already interacted with based on item metadata (categories and tags). Item-to-item similarity is computed by blending exact category matches and Jaccard similarity of tags. Weights can be configured (e.g. categoryWeight and tagWeight).
const recs = recommender.recommend("user_id", {
strategy: "content-based",
categoryWeight: 0.4, // 40% weight to category similarity
tagWeight: 0.6, // 60% weight to tag Jaccard similarity
});9. Hybrid Recommendation Strategy
Combines personal collaborative filtering preferences with global popularity trends or content-based matching to deliver more dynamic and balanced recommendations. Both components are normalized to the range [0.0, 1.0] using Min-Max scaling, then blended using the weighting parameter hybridAlpha ($\alpha$):
$$\text{Final Score} = \alpha \cdot \text{Normalized Base Score} + (1 - \alpha) \cdot \text{Normalized Secondary Score}$$
Collaborative Filtering + Popularity Blending
const recs = recommender.recommend("user_id", {
strategy: "hybrid",
hybridAlpha: 0.7, // 70% weight to CF, 30% to popularity
hybridBaseStrategy: "item-based",
hybridPopularityStrategy: "most-purchased",
});Collaborative Filtering + Content-Based Blending (Content-Aware Hybrid)
const recs = recommender.recommend("user_id", {
strategy: "hybrid",
hybridAlpha: 0.5, // 50% weight to CF, 50% to Content-Based
hybridBaseStrategy: "item-based",
hybridPopularityStrategy: "content-based", // uses content-based as the secondary strategy
});10. Explainable Recommendations
To improve transparency and allow developers to display labels like "Because you liked item X" or "Because similar user Y rated it Z", the engine can generate detailed explanation reasons when explain: true is passed:
const recs = recommender.recommend("user_id", {
strategy: "item-based",
explain: true,
});
console.log(recs[0]);
/*
Output:
{
itemId: "item_3",
score: 4.5,
reasons: [
{
triggerItemId: "item_1",
similarity: 0.95,
ratingGiven: 5.0,
explanation: "Because you liked item item_1"
}
]
}
*/Depending on the active strategy, the reasons field will contain:
- Item-Based CF: Triggers showing which previously-rated items (
triggerItemId,similarity, and target user'sratingGiven) influenced the candidate's score. - User-Based CF: Triggers showing which similar users (
triggerUserId,similaritywith target user, and theirratingGivenfor the candidate) influenced the prediction. - Content-Based Filtering: Triggers showing which items with similar content (
triggerItemIdandsimilaritymatch) influenced the prediction. - Popularity (Fallback/Cold-Start): Global descriptions like
"One of the most rated items". - Hybrid: Combined reasons merged from the base and secondary strategies.
11. Session-Based Recommendations
Generate recommendations dynamically based on the chronological sequence of item interactions within an active session. This is ideal for e-commerce shopping carts or real-time anonymous browsing where long-term history is either absent or doesn't reflect the user's immediate intent.
The engine supports two session recommendation strategies:
"transition": Calculates transition probabilities between items using a simple Markov Chain model ($A \to B$) built from historical sequence data."similarity"(Default): Builds a pseudo-user profile from the active session, decays past items exponentially, and delegates to item-based or content-based similarity.
Direct Session Recommendation
To generate recommendations for a session directly (e.g. for an anonymous user cart):
const cartItems = ["item_a", "item_b"];
const recs = recommender.recommendSession(cartItems, {
sessionStrategy: "transition", // 'transition' | 'similarity'
decayFactor: 0.5, // Weights older session items lower exponentially
limit: 5,
explain: true,
});Auto-Session Detection
If your interactions have timestamp fields, the engine automatically compiles transition and sequence histories. You can query standard recommendations while letting the engine automatically reconstruct the active session from the user's chronological history:
// Reconstructs the user's active session from their history and generates sequence recommendations
const recs = recommender.recommend("user_id", {
useSession: true,
sessionStrategy: "transition",
});Web Worker Support
For browser environments processing larger datasets (e.g. 50,000+ interactions), calling recommendation queries directly on the main thread might block the UI, dropping frames. To keep your UI running at a smooth 60 FPS, the library offers asynchronous background processing using Web Workers.
The class NanoRecommenderWorker acts as an asynchronous facade to the Web Worker. It exposes the same API as NanoRecommender, but every method returns a Promise.
Usage
Instantiation: Pass a new
Workerpointing to the library's compiled worker script (located at@fizm/nano-recommender/dist/recommender.worker.js):import { NanoRecommenderWorker } from "@fizm/nano-recommender"; const recommender = new NanoRecommenderWorker( new Worker( new URL( "@fizm/nano-recommender/dist/recommender.worker.js", import.meta.url, ), { type: "module" }, ), );Operations: All operations are executed asynchronously in the background:
// 1. Initialize the engine inside the worker await recommender.init({ defaultStrategy: "item-based" }); // 2. Load dataset in background await recommender.load(interactions); // 3. Query recommendations asynchronously const recs = await recommender.recommend("user_id", { limit: 5 }); console.log(recs); // 4. Terminate the worker thread when done (optional) recommender.terminate();
Because Web Workers communicate via message passing using the structured clone algorithm, custom filter callback functions (
options.filter) cannot be passed toNanoRecommenderWorker. Instead, perform post-filtering of the returned recommendations on the main thread.
Offline Evaluation Suite
The library includes a built-in evaluation suite under the evaluation namespace, enabling developers to partition interaction datasets and calculate recommendation quality metrics.
Splitting Strategies
You can split your interaction arrays into training and testing sets using one of three splitter functions:
splitRandom(interactions, trainRatio): Randomly splits interactions.splitTemporal(interactions, trainRatio): Splits interactions chronologically based on timestamps.splitUserHoldout(interactions, trainRatio): Groups interactions by user, shuffling and holding out a percentage of interactions for each user. This guarantees that test users have history in the training set.
import { splitUserHoldout } from "@fizm/nano-recommender";
const { train, test } = splitUserHoldout(dataset, 0.8); // 80% train, 20% testRunning Evaluations
The evaluate function automates training a recommender and testing its accuracy, returning standard ranking, rating prediction, and advanced coverage/diversity metrics. It automatically handles exporting, clearing, and fully restoring the original state of the recommender instance once evaluation completes.
import { NanoRecommender, evaluate } from "@fizm/nano-recommender";
const recommender = new NanoRecommender();
const results = evaluate(recommender, train, test, {
topK: 10, // K for Precision, Recall, NDCG, MAP, MRR, Diversity, Novelty, Serendipity
strategyOptions: {
strategy: "item-based",
similarityThreshold: 0.1,
},
});
console.log(results);
/*
Output:
{
rmse: 0.854, // Root Mean Squared Error (rating prediction quality)
mae: 0.652, // Mean Absolute Error
precision: 0.15, // Mean Precision@10 across test users
recall: 0.32, // Mean Recall@10 across test users
ndcg: 0.28, // Mean NDCG@10 (ranking quality)
map: 0.22, // Mean Average Precision (MAP@10)
mrr: 0.35, // Mean Reciprocal Rank (MRR@10)
diversity: 0.68, // Intra-list diversity (average pairwise dissimilarity)
novelty: 2.12, // Mean self-information (surprise score based on popularity)
serendipity: 0.18, // Relevance-aware surprise (unexpected relevant recommendations)
coverage: 0.45 // Catalog Coverage (ratio of recommended items / total catalog items)
}
*/Strategy Comparison
You can compare the performance of multiple recommendation strategies directly on the same train/test split:
import { compareStrategies } from "@fizm/nano-recommender";
const results = compareStrategies(
recommender,
train,
test,
["item-based", "user-based", "hybrid"],
{ topK: 10 },
);
console.log(results["item-based"].ndcg);
console.log(results["user-based"].ndcg);Hyperparameter Tuning (Grid Search)
You can run automated hyperparameter tuning using tune to search for the best configuration grid and optimize a target metric:
import { tune } from "@fizm/nano-recommender";
const tuningResult = tune(
recommender,
train,
test,
{
similarityThreshold: [0.0, 0.1, 0.2],
strategy: ["item-based", "user-based"],
k: [20, 50],
},
{
metric: "ndcg", // Metric to optimize
topK: 10,
},
);
console.log(tuningResult.bestParameters); // e.g., { similarityThreshold: 0.1, strategy: "user-based", k: 50 }
console.log(tuningResult.bestScore);Developer Experience (DX) & Builder API
The library provides modern ergonomics and developer-friendly APIs designed for production convenience and system observability.
Configuration Presets
Instead of configuring weights and strategies manually, you can initialize the recommender using domain-specific presets matching your business model:
ecommerce: Optimizes for purchases, shopping carts, and views. Useshybridstrategy by default withmost-purchasedfallbacks.media: Optimizes for watching and clicks, utilizing fastitem-basedCF withmost-viewedfallbacks and a default 30-day time-decay half-life.
import { NanoRecommender } from "@fizm/nano-recommender";
// Instantiate with a preset
const recommender = new NanoRecommender("ecommerce");
// Instantiate with overrides
const mediaRecommender = NanoRecommender.fromPreset("media", {
defaultK: 50,
});Builder API (Method Chaining)
You can construct recommendation queries fluently using method chaining, which improves code readability compared to passing deep options objects:
const recommendations = recommender
.query("user_123")
.withStrategy("hybrid")
.withLimit(5)
.withSimilarityThreshold(0.1)
.withK(30)
.excludeItemIds(["item_already_bought"])
.withFilter((itemId) => itemId.startsWith("category_a_"))
.explain(true)
.execute();For session-based recommendations:
const recommendations = recommender
.querySession(["item_1", "item_2"])
.withLimit(3)
.withSessionStrategy("similarity")
.execute();Strategy Auto-routing
Enabling the "auto" strategy allows the engine to route recommendations automatically based on the user's interaction profile:
- Cold Start (0 interactions): Automatically routes to the configured fallback popularity engine.
- Sparse Profile (1 to 4 interactions): Routes to
content-based(if category/tag metadata is populated) orhybridto bypass collaborative filtering sparsity limits. - Established Profile (5+ interactions): Routes to
user-basedif the user shows diverse interest across categories, oritem-basedif their history is category-focused.
const recommendations = recommender.recommend("user_123", {
strategy: "auto",
});Operational Metrics (Observability)
Exposes runtime statistics to trace performance, heap memory usage, and cache hit rates in production:
const metrics = recommender.metrics();
console.log(metrics.cacheHitRate); // e.g. 0.82
console.log(metrics.cacheDetails.itemCache); // { hits: 450, misses: 100, size: 550, hitRate: 0.818 }
console.log(metrics.memoryUsage?.heapUsed); // Node.js memory footprint (if available)Error Handling
The library throws structured custom errors extending RecommenderError (which inherits from the native Error class). All custom exceptions are exported from the root module.
Custom Exception Classes
RecommenderError: The base exception class for all errors generated by the engine. You can catch this to handle any engine-specific errors.ValidationError: Thrown when validating parameters in the constructor or query options (e.g. invalidhybridAlpha, negativek, invalid presets).InvalidInteractionError: Thrown when loading interactions containing corrupt data (e.g. missinguserId/itemId,NaNratings, or invalid timestamps).
Catching Errors Example
import { ValidationError, RecommenderError } from "@fizm/nano-recommender";
try {
const recommender = new NanoRecommender({ defaultK: -5 });
} catch (error) {
if (error instanceof ValidationError) {
console.error("Invalid configuration:", error.message);
} else if (error instanceof RecommenderError) {
console.error("Engine error:", error.message);
}
}Design Rationale & Technical Trade-offs
1. Neighborhood Models vs. Latent Factors (SVD/ALS)
Currently, all strategies in nano-recommender are neighborhood-based (Item-based/User-based Collaborative Filtering) and content-based.
- Trade-off: Neighborhood-based algorithms shine in zero-dependency, in-memory systems where interactions are updated in real-time. We can perform selective cache invalidation and immediate incremental updates via
addInteraction(interaction)in $O(K)$ time without having to retrain a heavy mathematical model. - Roadmap: Latent factor models like Matrix Factorization (SVD/ALS) are standard for offline training on dense matrices but are computationally expensive to update dynamically and require matrix algebra libraries. Supporting SVD/ALS as an offline-trained, static fallback/strategy is slated for v3.0.0.
2. In-Memory Limits vs. Streaming Pipelines
The load() method loads the entire interaction dataset into Node.js heap memory using a sparse coordinate matrix format.
- Trade-off: Memory footprint is optimized using integer ID mapping and avoiding dense matrices. A dataset of 1 million interactions occupies roughly 350 MB of heap memory.
- Roadmap: For large-scale production deployments containing tens of millions of interactions, loading all data at startup can cause heap limits to exceed. Implementing a streaming database adapter / pipeline loader is slated for the future roadmap.
3. Explanation Localization (i18n)
By default, explanation strings generated when explain: true is enabled are returned in English.
- Solution: The engine now natively supports a custom
explanationFormattercallback in both theNanoRecommenderconstructor configurations and the query options (RecommendationOptions). This allows developers to translate, format, or customize explanation structures dynamically (e.g. for i18n localization) without pulling in heavy external translation libraries.
Performance
The benchmark suite was run on synthetic datasets, measuring loading speed, memory footprint, and query latency (average and P95) for various strategies under WASM and JS Fallback modes.
System Environment
- CPU: 13th Gen Intel(R) Core(TM) i7-13700HX (24 cores)
- RAM: 16 GB
- OS: Windows_NT (x64, 10.0.26200)
- Node.js: v24.15.0
Loading & Memory Footprint
- Dataset specs: Generated using deterministic LCG generator containing uniform 10 interactions per user.
- Cache behavior: Memory delta measures heap allocation right after caching similarities for a sample of 100 users.
| Scale | Users | Items | Interactions | Load Time | Load Rate (Ops/sec) | Heap Delta (Loaded) | Heap Delta (Cached) |
|---|---|---|---|---|---|---|---|
| Small | 1,000 | 100 | 10,000 | 9.84 ms | 1,016,312 | 3.58 MB | 0.90 MB |
| Medium | 10,000 | 1,000 | 100,000 | 62.42 ms | 1,601,976 | 35.16 MB | 31.48 MB |
| Large | 100,000 | 5,000 | 1,000,000 | 1,236.78 ms | 808,554 | 348.98 MB | 167.85 MB |
Cache Memory Footprint Anomaly Explanation: In the Medium scale, the cached heap delta (31.48 MB) is almost equal to the loaded delta (35.16 MB) because the 100-user sample queries touch a large fraction of the total possible item-item pairs for a small catalog of 1,000 items. In contrast, in the Large scale, the cached heap delta (167.85 MB) is relatively smaller compared to the loaded delta (348.98 MB) because a 100-user sample only touches a tiny fraction of the total possible pairwise similarities for a catalog of 5,000 items.
Latency (Cache Hit vs. Miss - Item-Based CF)
- Measures average and P95 recommendation query latencies with WASM Enabled.
| Scale | Cache-Miss Avg | Cache-Miss P95 | Cache-Hit Avg | Cache-Hit P95 | Hit Speedup |
|---|---|---|---|---|---|
| Small | 0.802 ms | 1.839 ms | 0.531 ms | 0.531 ms | 1.5x |
| Medium | 10.938 ms | 19.924 ms | 3.935 ms | 3.935 ms | 2.8x |
| Large | 67.169 ms | 91.227 ms | 10.753 ms | 10.753 ms | 6.2x |
Cache-Hit Avg & P95 Variance Explanation: Cache-Hit Avg and Cache-Hit P95 are identical in the benchmark results because cached similarities are retrieved via O(1) lookups in a JavaScript Map. This lookup has zero computational complexity and near-zero variance, causing the average and the 95th percentile response times to align exactly.
WASM vs. JS Fallback Latency (Cache-Miss Avg)
- Measures average recommendation query latency comparing WebAssembly (WASM) to JS/TS Fallback.
| Scale | Strategy | JS Fallback (Avg) | WASM Enabled (Avg) | WASM Acceleration |
|---|---|---|---|---|
| Small | item-based | 0.950 ms | 0.802 ms | 1.18x |
| Small | user-based | 1.827 ms | 1.594 ms | 1.15x |
| Small | hybrid | 0.962 ms | 0.713 ms | 1.35x |
| Small | content-based | 0.097 ms | 0.163 ms | 0.59x |
| Small | session-based | 0.170 ms | 0.190 ms | 0.89x |
| Medium | item-based | 21.507 ms | 10.938 ms | 1.97x |
| Medium | user-based | 5.436 ms | 6.962 ms | 0.78x |
| Medium | hybrid | 21.807 ms | 10.869 ms | 2.01x |
| Medium | content-based | 6.014 ms | 6.008 ms | 1.00x |
| Medium | session-based | 0.800 ms | 0.607 ms | 1.32x |
| Large | item-based | 171.085 ms | 67.169 ms | 2.55x |
| Large | user-based | 39.374 ms | 40.207 ms | 0.98x |
| Large | hybrid | 204.015 ms | 69.323 ms | 2.94x |
| Large | content-based | 75.781 ms | 62.991 ms | 1.20x |
| Large | session-based | 2.201 ms | 2.175 ms | 1.01x |
WASM vs JS Fallback Benchmark Settings: This raw comparison table was generated by running the benchmark suite with
wasmStrategy: "always"to measure and show the raw WebAssembly execution speed compared to pure JavaScript/TypeScript for all vector configurations. Under the default"auto"strategy (which haswasmMinVectorSize = 20), the engine automatically routes any sparse similarity calculations (item-based, user-based, and session-based similarity) below this size threshold to the pure JS engine. This bypasses the minor boundary marshalling overhead and prevents slowdowns on small profiles/vectors. Note that thecontent-basedstrategy relies strictly on category and tag metadata calculations and is implemented as a pure JS engine that never uses WASM.
Scalability Limits & Roadmap: Currently, the engine is fully optimized and validated for datasets up to Large scale (~500,000 interactions). Validation and optimizations for extremely large scales exceeding 10 million interactions have not been performed yet. Support for ultra-large scale datasets, including a streaming/incremental loader, is planned for the roadmap.
Multi-Strategy Latency (WASM Enabled)
- Comparison of average latencies across all primary recommendation strategies.
| Scale | Item-Based CF | User-Based CF | Hybrid Strategy | Content-Based | Session-Based |
|---|---|---|---|---|---|
| Small | 0.802 ms | 1.594 ms | 0.713 ms | 0.163 ms | 0.190 ms |
| Medium | 10.938 ms | 6.962 ms | 10.869 ms | 6.008 ms | 0.607 ms |
| Large | 67.169 ms | 40.207 ms | 69.323 ms | 62.991 ms | 2.175 ms |
Density Impact (Uniform vs. Variable/Power User)
- Measures the performance impact of variable interaction density where a minority of "power users" have many more interactions (up to 10x) than regular users, and how the
maxUserProfileSizelimits prevent bottlenecks.
| Scale | Density Mode | Total Interactions | Latency Avg (Miss) | Latency P95 (Miss) |
|---|---|---|---|---|
| Small | uniform (10/user) | 10,000 | 0.751 ms | 1.827 ms |
| Small | variable (power users) | 12,540 | 2.339 ms | 10.724 ms |
| Small | variable (capped at 50) | 9,740 | 1.026 ms | 3.884 ms |
| Medium | uniform (10/user) | 100,000 | 9.780 ms | 16.910 ms |
| Medium | variable (power users) | 118,265 | 20.714 ms | 59.534 ms |
| Medium | variable (capped at 50) | 94,415 | 12.931 ms | 39.005 ms |
| Large | uniform (10/user) | 1,000,000 | 78.372 ms | 104.934 ms |
| Large | variable (power users) | 1,199,715 | 287.387 ms | 2,267.936 ms |
| Large | variable (capped at 50) | 950,315 | 129.940 ms | 587.896 ms |
Power User Density Bottlenecks & Capping Solution: In the Large scale dataset, moving from uniform density to a variable distribution (where 5% of users are power users with up to 100 interactions) causes the average query latency to increase to 287.39 ms and the P95 latency to reach 2,267.94 ms.
This is a known bottleneck of neighborhood methods: similarity calculations scale with the density of the user's vector and the size of their candidate items. In latency-sensitive production environments, you can limit the user profile size using the
maxUserProfileSizeparameter.As demonstrated in the benchmark above, setting
maxUserProfileSize: 50successfully reduced the average latency to 129.94 ms and cut the P95 latency down to 587.90 ms (a 4x latency improvement for power users).const recommender = new NanoRecommender({ maxUserProfileSize: 50, // Caps user profile history to latest 50 interactions (FIFO) });
Approximate Nearest Neighbor (ANN) Search via LSH
Stability & Accuracy Notice (Experimental / Beta):
- The LSH/ANN search feature is currently experimental and is not recommended for production-critical accuracy without thorough offline evaluation.
- Sparsity Recall Drop: In typical collaborative filtering datasets, user/item rating vectors are highly sparse. This makes their cosine similarity naturally low (often 0.05 - 0.20), preventing standard SimHash LSH from partitioning them effectively and resulting in poor recommendation recall (down to 1.2% - 8%).
- Implementation Details: To maintain recall,
nano-recommenderuses LSH for User-Based CF and fallback Adaptive Random Co-Rater Sampling for Item-Based CF whenenableApproximateSearchis active.- Manual Tuning Required: You must evaluate the recall trade-off using the Offline Evaluation Suite on your specific dataset before deploying this configuration.
For large-scale datasets with dense user profiles (where exact computation latencies exceed acceptable real-time limits), nano-recommender provides an Approximate Nearest Neighbor (ANN) search strategy powered by Locality Sensitive Hashing (LSH) with random projection (SimHash) for Cosine similarity.
It reduces the query candidate space dynamically, bypassing the linear scanning of all items and decreasing latencies dramatically for heavy users.
How to Enable
You can enable LSH globally in the constructor or on a per-query basis:
// Enable LSH globally
const recommender = new NanoRecommender({
enableApproximateSearch: true,
lshBands: 32, // Number of bands (default: 8)
lshRows: 4, // Rows per band (default: 8)
});
// Or enable it on a single recommendation query
const recommendations = recommender.recommend("u_123", {
enableApproximateSearch: true,
lshMinBandMatches: 1, // Require at least 1 band match (tunes recall vs. speed)
});Tuning Parameters
lshBands: The number of bands the signature is split into. Higher values increase recall but slightly increase candidate space.lshRows: The number of rows (hyperplanes) per band. Higher values decrease collision rate (fewer candidates, faster queries) but can reduce recall.lshMinBandMatches: The minimum number of bands a candidate must collide in to be evaluated. Setting it higher (e.g.,2or3) cuts query candidate spaces aggressively, resulting in sub-100ms speeds at Large scale, while setting it to1ensures high Recall (up to 100%).
LSH Performance & Recall Evaluation
The following benchmark demonstrates the performance of LSH query optimization compared to the exact search baseline on a dataset containing 10,000 users, 3,000 items, and variable densities (stressing 3 power users with 100 interactions):
| Configuration | Avg Latency | P95 Latency | Recall@10 vs Exact | Speedup vs Exact |
|---|---|---|---|---|
| Exact Neighborhood Search (Baseline) | 1027.18 ms | 2123.58 ms | 100.0% | 1.00x |
| LSH (32 bands x 4 rows, minMatches: 1) | 1111.70 ms | 2141.36 ms | 100.0% | 0.92x |
| LSH (24 bands x 4 rows, minMatches: 1) | 1005.26 ms | 1946.31 ms | 100.0% | 1.02x |
| LSH (16 bands x 5 rows, minMatches: 1) | 1048.23 ms | 2095.63 ms | 100.0% | 0.98x |
| LSH (20 bands x 4 rows, minMatches: 1) | 1117.60 ms | 2296.88 ms | 100.0% | 0.92x |
Recall vs. Performance Optimization:
- If 100% Recall is required, setting
lshMinBandMatches: 1with lower rows (e.g., 4 or 5) maintains identical accuracy to Exact Search.- In ultra-low-latency environments, setting
lshMinBandMatches: 2with higher rows (e.g., 6 or 8) reduces latencies to <80ms (up to 12x speedup) with a minor trade-off in recall.
API Reference
class NanoRecommender
constructor(config?: NanoRecommenderConfig)
Instantiates the recommendation engine facade.
| Parameter | Type | Default | Description |
|---|---|---|---|
defaultStrategy |
"item-based" | "user-based" | "hybrid" |
"item-based" |
The default strategy to use in the recommend() method. |
defaultSimilarityThreshold |
number |
0.0 |
The default similarity threshold score between entities. |
defaultMinIntersectionSize |
number |
1 |
The default minimum number of shared items/users required to compute similarity. |
defaultK |
number |
undefined |
The default neighborhood limit (K) to use in recommendation calculations. |
defaultFallbackStrategy |
"most-rated" | "most-viewed" | "most-purchased" | "none" |
"most-rated" |
The default fallback strategy for cold start users. |
interactionWeights |
Record<string, number> |
undefined |
Optional mapping of interaction types to positive rating multipliers. |
decayHalfLifeDays |
number |
undefined |
Optional half-life in days for exponential time-decay weighting. |
maxSimilarityCacheSize |
number |
undefined |
Optional capacity limit for similarity cache (LRU eviction). |
defaultHybridAlpha |
number |
0.5 |
The default weighting parameter alpha for hybrid strategy. Must be between 0.0 and 1.0. |
defaultExplain |
boolean |
false |
The default explain option to include reasons in recommendation results. |
maxUserProfileSize |
number |
undefined |
Optional maximum user profile size limit. If exceeded, the oldest interaction is evicted (FIFO). |
wasmStrategy |
"auto" | "always" | "never" |
"auto" |
WebAssembly execution strategy. 'auto' falls back to pure JS/TS below a vector size threshold to avoid boundary overhead. |
wasmMinVectorSize |
number |
20 |
Minimum vector size threshold for WASM auto routing. |
explanationFormatter |
ExplanationFormatter |
undefined |
Optional custom formatter function to format or localize recommendation explanations (i18n). |
load(interactions: Interaction[], options?: { referenceTime?: number | string | Date }): void
Clears existing interactions and loads a new batch dataset. Automatically applies weights from interactionWeights and decays ratings based on decayHalfLifeDays relative to options.referenceTime (defaults to max timestamp or Date.now()). Invalidates (clears) similarity caches.
addInteraction(interaction: Interaction): void
Adds or updates a single user-item interaction in real-time. Automatically applies weights from interactionWeights and decays the rating based on decayHalfLifeDays relative to the engine's last reference time. Updates the sparse matrix and selectively invalidates only the similarity cache entries associated with the affected user and item, maintaining high retrieval performance for other queries.
recommend(userId: string, options?: RecommendationOptions): Recommendation[]
Generates recommendation array for a user. Automatically delegates to the selected strategy, falling back to popularity engine if the user has no history.
| Option | Type | Default | Description |
|---|---|---|---|
strategy |
"item-based" | "user-based" | "hybrid" |
defaultStrategy |
The recommendation strategy to use. |
limit |
number |
10 |
Maximum number of recommendations to return. |
similarityThreshold |
number |
defaultSimilarityThreshold |
Minimum similarity score required between entities. |
minIntersectionSize |
number |
defaultMinIntersectionSize |
Minimum number of shared items/users required to compute similarity. |
k |
number |
defaultK |
Limit the similarity calculation to the top K nearest neighbors. |
excludeInteracted |
boolean |
true |
Whether to exclude items the user has already rated/interacted with. |
fallbackStrategy |
"most-rated" | "most-viewed" | "most-purchased" | "none" |
defaultFallbackStrategy |
Fallback strategy for cold start users. |
excludeItemIds |
string[] |
undefined |
Optional array of item IDs to blacklist/exclude. |
filter |
(itemId: string) => boolean |
undefined |
Optional custom callback to dynamically filter item recommendations. |
filterCategory |
string |
undefined |
Optional category classification to filter recommendations by. |
filterTags |
string[] |
undefined |
Optional tags array to filter recommendations by (matches items with at least one tag). |
hybridAlpha |
number |
defaultHybridAlpha |
Weighting parameter alpha for hybrid strategy (0.0 to 1.0). |
hybridBaseStrategy |
"item-based" | "user-based" |
defaultStrategy (or item-based) |
Collaborative filtering base strategy for hybrid. |
hybridPopularityStrategy |
"most-rated" | "most-viewed" | "most-purchased" |
defaultFallbackStrategy (or most-rated) |
Popularity strategy for hybrid. |
explain |
boolean |
defaultExplain |
Whether to include explanation reasons for the recommendations. |
useSession |
boolean |
false |
Whether to automatically detect and use the user's chronological interaction session. |
sessionStrategy |
"transition" | "similarity" |
"similarity" |
The strategy mode for session-based recommendation. |
decayFactor |
number |
0.5 |
Decay factor for positional items weighting. |
similarityStrategy |
"item-based" | "content-based" |
"item-based" |
The similarity strategy to use when session strategy is "similarity". |
explanationFormatter |
ExplanationFormatter |
explanationFormatter (default) |
Optional custom formatter function to format or translate explanations (i18n). |
recommendSession(sessionItemIds: string[], options?: SessionRecommendationOptions): Recommendation[]
Generates recommendations based on the items in the current active session (e.g., anonymous browsing or cart items).
| Option | Type | Default | Description |
|---|---|---|---|
sessionStrategy |
"transition" | "similarity" |
"similarity" |
The strategy mode for session-based recommendation. |
decayFactor |
number |
0.5 |
Decay factor for positional items (older items get decayed by decayFactor^(N-1-j)). |
limit |
number |
10 |
Maximum number of recommendations to return. |
explain |
boolean |
defaultExplain |
Whether to include explanation reasons for the recommendations. |
filterCategory |
string |
undefined |
Optional category classification to filter recommendations by. |
filterTags |
string[] |
undefined |
Optional tags array to filter recommendations by. |
similarityStrategy |
"item-based" | "content-based" |
"item-based" |
The similarity strategy to delegate to. |
similarityThreshold |
number |
defaultSimilarityThreshold |
Minimum similarity score required. |
minIntersectionSize |
number |
defaultMinIntersectionSize |
Minimum number of shared interactions. |
k |
number |
defaultK |
Top K neighborhood limit for similarity. |
explanationFormatter |
ExplanationFormatter |
explanationFormatter (default) |
Optional custom formatter function to format or translate explanations (i18n). |
recommendItemBased(userId: string, options?: ItemBasedRecommendationOptions): Recommendation[]
Directly triggers Item-Based Collaborative Filtering. Accepts all filtering options (excludeItemIds, filter).
recommendUserBased(userId: string, options?: UserBasedRecommendationOptions): Recommendation[]
Directly triggers User-Based Collaborative Filtering. Accepts all filtering options (excludeItemIds, filter).
recommendContentBased(userId: string, options?: ContentBasedRecommendationOptions): Recommendation[]
Directly triggers Content-Based Filtering based on item metadata categories and tags. Accepts filtering options (excludeItemIds, filter).
recommendHybrid(userId: string, options?: RecommendationOptions): Recommendation[]
Directly triggers Hybrid Recommendation Strategy blending CF scores and popularity counts. Accepts all filtering options (excludeItemIds, filter).
clear(): void
Pushes the engine to a clean state. Clears sparse matrix storage and deletes similarity cache instances.
stats(): RecommenderStats
Returns descriptive summary statistics (userCount, itemCount, interactionCount).
export(): RecommenderState
Exports the entire internal state of the recommender engine (including sparse matrix, item index, and popularity metrics) to a JSON-serializable object.
import(state: RecommenderState): void
Restores the recommender engine state from a serialized state object. Automatically invalidates internal similarity caches. Throws a ValidationError if the version or structure is invalid.
Core Interfaces
interface Interaction
Represents a single user-item interaction event.
| Property | Type | Required | Description |
|---|---|---|---|
userId |
string |
Yes | Unique identifier of the user. |
itemId |
string |
Yes | Unique identifier of the item. |
rating |
number |
Yes | Numeric rating, weight, or score for the interaction. |
type |
string |
No | Type of interaction (e.g. 'view', 'rate', 'purchase'). Used for weighting and fallback popularity strategy. |
timestamp |
number | string | Date |
No | Optional timestamp of when the interaction occurred. Used for exponential time-decay. |
itemCategory |
string |
No | Optional category classification of the item. Used for built-in filtering. |
itemTags |
string[] |
No | Optional descriptive tags/keywords of the item. Used for built-in filtering. |
interface Recommendation
Represents a single item recommendation result.
| Property | Type | Description |
|---|---|---|
itemId |
string |
Unique identifier of the recommended item. |
score |
number |
Calculated recommendation score (higher scores represent better/stronger recommendations). |
reasons |
RecommendationReason[] |
Optional explanation reasons detailing why this recommendation was generated. |
interface RecommendationReason
Represents the explanation reason behind a generated recommendation.
| Property | Type | Description |
|---|---|---|
triggerItemId |
string |
Optional item ID that triggered this recommendation (Item-Based CF). |
triggerUserId |
string |
Optional user ID that triggered this recommendation (User-Based CF). |
similarity |
number |
Similarity score between the target and the trigger entity. |
ratingGiven |
number |
Numeric rating value given to/by the trigger entity. |
explanation |
string |
Plain English description of the recommendation reason. |
interface RecommenderState
Represents the complete serialized state of the engine.
| Property | Type | Description |
|---|---|---|
version |
string |
Serialization schema version (currently "1"). |
matrix |
SerializedMatrixState |
The serialized sparse matrix and item popularity indices. |
Similarity Functions
The library exports the following built-in similarity algorithms that satisfy the SimilarityFunction interface:
cosineSimilarity: Computes standard Cosine Similarity between two sparse vectors.jaccardSimilarity: Computes Jaccard Similarity coefficient based on the overlap of rated item sets (ignores rating values).pearsonCorrelation: Computes Pearson Correlation Coefficient by mean-centering the vectors before calculating cosine similarity, normalizing user rating scale bias.
Architecture
The project maintains a clean structural modularity:
src/
├── core/
│ ├── cache.ts # Symmetric cache
│ └── matr