npm.io
2.2.0 • Published 6d ago

@fizm/nano-recommender

Licence
MIT
Version
2.2.0
Deps
0
Size
2.0 MB
Vulns
0
Weekly
759

Zero-dependency collaborative filtering engine built for performance.

npm version bundle size license typescript build status

Table of Contents


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-recommender

Install via pnpm:

pnpm add @fizm/nano-recommender

Install via yarn:

yarn add @fizm/nano-recommender
Compatibility 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 NanoRecommender automatically 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 (below wasmMinVectorSize, 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. The content-based engine relies on category and tag metadata matching and runs entirely in pure JS. Developers can also manually override this behavior via wasmStrategy: "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()); // true

Recommendation 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's ratingGiven) influenced the candidate's score.
  • User-Based CF: Triggers showing which similar users (triggerUserId, similarity with target user, and their ratingGiven for the candidate) influenced the prediction.
  • Content-Based Filtering: Triggers showing which items with similar content (triggerItemId and similarity match) 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
  1. Instantiation: Pass a new Worker pointing 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" },
      ),
    );
  2. 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 to NanoRecommenderWorker. 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% test
Running 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);

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. Uses hybrid strategy by default with most-purchased fallbacks.
  • media: Optimizes for watching and clicks, utilizing fast item-based CF with most-viewed fallbacks 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:

  1. Cold Start (0 interactions): Automatically routes to the configured fallback popularity engine.
  2. Sparse Profile (1 to 4 interactions): Routes to content-based (if category/tag metadata is populated) or hybrid to bypass collaborative filtering sparsity limits.
  3. Established Profile (5+ interactions): Routes to user-based if the user shows diverse interest across categories, or item-based if 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. invalid hybridAlpha, negative k, invalid presets).
  • InvalidInteractionError: Thrown when loading interactions containing corrupt data (e.g. missing userId/itemId, NaN ratings, 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 explanationFormatter callback in both the NanoRecommender constructor 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 has wasmMinVectorSize = 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 the content-based strategy 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 maxUserProfileSize limits 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 maxUserProfileSize parameter.

As demonstrated in the benchmark above, setting maxUserProfileSize: 50 successfully 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-recommender uses LSH for User-Based CF and fallback Adaptive Random Co-Rater Sampling for Item-Based CF when enableApproximateSearch is 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., 2 or 3) cuts query candidate spaces aggressively, resulting in sub-100ms speeds at Large scale, while setting it to 1 ensures 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: 1 with lower rows (e.g., 4 or 5) maintains identical accuracy to Exact Search.
  • In ultra-low-latency environments, setting lshMinBandMatches: 2 with 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

Keywords