Skip to main content

CPU-only hybrid recommender (co-vis + ANN + LightGBM) with tiny footprint.

Project description

Pocket-Recs

A CPU-only hybrid recommender system combining research-proven patterns with minimal infrastructure requirements.

CI License Python

Overview

Tired of expensive recommendation services? Want to run recommendations on a $10/month VPS?

Pocket-Recs is a production-ready recommendation engine that brings enterprise-grade ML to small and medium businesses. It scales from tiny shops to mid-sized catalogs (up to 100k items) while running on modest hardware (2 vCPU / 4-8 GB RAM).

No GPU needed. No cloud ML services. Just smart recommendations on a CPU.

It combines:

  • Two-tower retrieval with ANN (FAISS/HNSW) for semantic search
  • Co-visitation patterns with time decay
  • Brand-popularity with recency weighting
  • LightGBM LambdaRank for re-ranking
  • MMR diversification to reduce redundancy

All components are CPU-optimized and use permissive open-source licenses (Apache-2.0, MIT).

When to Use Pocket-Recs

✅ Perfect For:

  • E-commerce shops with 100-100k products
  • Content platforms (blogs, videos, music) with small-to-medium catalogs
  • Startups wanting production-quality recommendations without ML infrastructure
  • Cost-conscious teams ($5-25/month hosting)
  • CPU-only environments (edge, serverless, modest VPS)

❌ Not Ideal For:

  • Mega-scale catalogs (>1M items) - consider specialized vector databases
  • Real-time personalization with <1ms latency requirements
  • Deep learning research requiring custom architectures
  • Cases where GPU acceleration is already available and preferred

🤔 Also Consider:

  • If you have <100 items: Simple collaborative filtering may be enough
  • If you have >1M items: Look into DiskANN, Milvus, or Pinecone
  • If you need real-time learning: Consider online learning frameworks

Key Features

  • Hybrid Multi-Stage Architecture: Retrieval → Ranking → Diversification
  • CPU-Only: No GPU required for training or inference
  • Cost-Efficient: Runs on a $5-10/month VPS
  • Fast: 10-40ms p95 latency for 100k item catalogs
  • Explainable: Returns reason codes for each recommendation
  • Configurable: Extensive configuration options via Python API
  • Production-Ready: REST API, health checks, artifact versioning

Getting Started in 5 Minutes

# 1. Install
pip install pocket-recs[ann,api]

# 2. Generate sample data (or use your own)
python -c "from pocket_recs import Recommender; print('Ready!')"

# 3. Train the model (using example data from examples/ directory)
pocket-recs fit interactions.parquet catalog.csv artifacts/

# 4. Start the API server
pocket-recs serve artifacts/ catalog.csv --port 8000

# 5. Get recommendations
curl -X POST "http://localhost:8000/v1/recommend" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "user_123", "k": 10}'

Installation

# Basic installation (includes core dependencies)
pip install pocket-recs

# Recommended: Install with ANN support (FAISS/HNSW)
pip install pocket-recs[ann]

# Install with API server (FastAPI + uvicorn)
pip install pocket-recs[api]

# Install with ONNX optimization
pip install pocket-recs[onnx]

# Development installation (includes testing tools)
pip install pocket-recs[dev]

# All features (recommended for production)
pip install pocket-recs[ann,api,onnx]

System Requirements:

  • Python 3.9 or higher
  • 2-8 GB RAM (depending on catalog size)
  • No GPU required (CPU-only)

Quick Start

1. Prepare Your Data

You need two files: a catalog (CSV) with your items and interactions (Parquet) with user behavior.

Catalog CSV (catalog.csv):

item_id,brand,category,title,short_desc,price,in_stock
item_1,BrandA,Electronics,Wireless Headphones,Premium noise-canceling,199.99,true
item_2,BrandB,Electronics,USB-C Cable,Fast charging cable,12.99,true
item_3,BrandA,Electronics,Bluetooth Speaker,Portable wireless speaker,79.99,true

Important:

  • item_id and title are required
  • Other fields are optional but improve recommendations
  • Include all items you want to recommend (even if no interactions yet)

Interactions Parquet (interactions.parquet):

import polars as pl

interactions = pl.DataFrame({
    "user_id": ["u1", "u1", "u2", "u2"],
    "item_id": ["item_1", "item_2", "item_1", "item_3"],
    "brand": ["BrandA", "BrandB", "BrandA", "BrandA"],
    "timestamp": [1700000000000, 1700000100000, 1700000200000, 1700000300000],
    "quantity": [1, 1, 2, 1],
    "price": [199.99, 12.99, 199.99, 79.99],
    "event": ["view", "add", "purchase", "view"]
})
interactions.write_parquet("interactions.parquet")

Important:

  • user_id, item_id, timestamp, and event are required
  • Use milliseconds for timestamps (not seconds!)
  • Valid events: "view", "add", "purchase"
  • More data = better recommendations (aim for 1000+ interactions minimum)

2. Train the Model

from pocket_recs import fit

# Run offline training pipeline
artifacts_dir = fit(
    interactions_path="interactions.parquet",
    catalog_path="catalog.csv",
    out_dir="artifacts/"
)

Or via CLI:

pocket-recs fit interactions.parquet catalog.csv artifacts/

3. Generate Recommendations

from pocket_recs import Recommender
from pocket_recs.types import RecommendRequest, Interaction

# Load recommender
rec = Recommender(
    artifacts_dir="artifacts/",
    catalog_path="catalog.csv"
)

# Get recommendations
request = RecommendRequest(
    user_id="u1",
    k=10,
    recent=[
        Interaction(
            user_id="u1",
            item_id="item_1",
            timestamp=1700000000000,
            event="view"
        )
    ]
)

response = rec.recommend(request)

for item in response.items:
    print(f"{item.rank}. {item.item_id} (score: {item.score:.3f})")
    print(f"   Reasons: {', '.join(item.reasons)}")

4. Serve via API

# Start FastAPI server
pocket-recs serve artifacts/ catalog.csv --port 8000

# Or programmatically
from pocket_recs.api import create_app
import uvicorn

app = create_app("artifacts/", "catalog.csv")
uvicorn.run(app, host="0.0.0.0", port=8000)

API Endpoints:

# Get recommendations
curl -X POST "http://localhost:8000/v1/recommend" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u1",
    "k": 10,
    "recent": [
      {"user_id": "u1", "item_id": "item_1", "timestamp": 1700000000000, "event": "view"}
    ],
    "brand": "BrandA",
    "exclusions": ["item_1"]
  }'

# Health check
curl http://localhost:8000/healthz

# Readiness check
curl http://localhost:8000/readyz

Available Request Parameters:

  • user_id (required): User identifier
  • k (optional, default=20): Number of recommendations (1-100)
  • recent (optional): List of recent user interactions
  • brand (optional): Filter recommendations by brand
  • exclusions (optional): List of item IDs to exclude from results
  • filters (optional): Additional custom filters

Visit http://localhost:8000/docs for interactive API documentation.

Architecture

How It Works

Pocket-Recs uses a multi-stage pipeline to generate recommendations:

  1. Retrieval Stage: Quickly fetch 100-200 candidate items from multiple sources

    • Co-visitation: "Users who viewed X also viewed Y" patterns
    • Brand Popularity: Trending items in preferred brands
    • ANN Semantic Search: Items similar to user's recent interests (via embeddings)
  2. Ranking Stage: Re-score candidates using LightGBM LambdaRank model

    • Combines signals: relevance, recency, popularity, diversity
    • Learns from interaction patterns (views → adds → purchases)
  3. Diversification Stage: Apply MMR to reduce redundancy

    • Ensures results aren't all from the same category/brand
    • Balances relevance with variety
  4. Explanation: Attach reason codes to each recommendation

    • "co-visit", "brand-popular", "semantic-match", etc.
    • Helps with debugging and transparency

Architecture Diagram

+--------------------+     +-----------------+      +-------------------+
|  Interactions      |---->|  Sessionize     |----->|  Co-visitation    |
+--------------------+     +-----------------+      +-------------------+
          |                                                  |
          v                                                  v
+--------------------+     +-------------------------+    +------------------+
| Brand Popularity   |     | Text Embeddings         |--->| HNSW/FAISS Index |
+--------------------+     +-------------------------+    +------------------+
          \                 /                                    |
           \               /                                     |
            v             v                                      v
           +-----------------+      +--------------------+
           | Candidate Pool  |      | User Vector        |
           +-----------------+      +--------------------+
                     |                             
                     v
              [Retrieval: ANN + Co-vis + Brand-pop]
                     |
                     v
              [Re-ranking: LightGBM LambdaRank]
                     |
                     v
              [Diversification: MMR]
                     |
                     v
            Top-N Recommendations + Reasons

Performance

Catalog Size Index Size Training Time Inference (p95) RAM Usage Cost/Month
10k items ~15 MB 5-10 min 8-15 ms 2 GB $5-10
50k items ~75 MB 15-30 min 15-25 ms 4 GB $10-15
100k items ~150 MB 30-60 min 20-40 ms 6 GB $15-25

Benchmarks on 2 vCPU / 4-8 GB RAM VPS with sentence-transformers/all-MiniLM-L6-v2

Configuration

All components are highly configurable. Here's a comprehensive example:

from pocket_recs.config import (
    RecommenderConfig,
    EmbeddingConfig,
    ANNConfig,
    CovisConfig,
    BrandPopConfig,
    RankerConfig,
    MMRConfig,
)

config = RecommenderConfig(
    # Text embedding configuration
    embedding=EmbeddingConfig(
        model_name="sentence-transformers/all-MiniLM-L6-v2",
        batch_size=256,
        normalize=True,
        device="cpu",  # Use "cuda" for GPU
    ),
    
    # ANN index configuration
    ann=ANNConfig(
        M=16,                  # HNSW connectivity
        ef_construction=200,   # Build-time search depth
        ef_search=64,          # Query-time search depth
        metric="ip",           # Inner product similarity
    ),
    
    # Co-visitation configuration
    covis=CovisConfig(
        top_k=50,                    # Top items per seed
        tau_ms=15 * 60_000,          # Time decay: 15 minutes
        session_gap_minutes=30,      # Session boundary
    ),
    
    # Brand popularity configuration
    brand_pop=BrandPopConfig(
        half_life_days=7,      # Recency decay half-life
        top_n=100,             # Top items per brand
    ),
    
    # LightGBM ranker configuration
    ranker=RankerConfig(
        objective="lambdarank",
        metric="ndcg",
        ndcg_eval_at=[10],
        num_leaves=63,
        learning_rate=0.05,
        min_data_in_leaf=20,
        num_boost_round=500,
    ),
    
    # MMR diversification configuration
    mmr=MMRConfig(
        lambda_param=0.7,      # Relevance vs diversity (0=diverse, 1=relevant)
    ),
    
    # Candidate pool sizes
    candidate_sizes={
        "covis": 40,
        "brand_pop": 40,
        "ann": 80,
        "item2vec": 40,
    },
)

# Use custom config in training
artifacts_dir = fit(
    interactions_path="interactions.parquet",
    catalog_path="catalog.csv",
    out_dir="artifacts/",
    config=config,
)

# Use custom config in inference
recommender = Recommender(
    artifacts_dir="artifacts/",
    catalog_path="catalog.csv",
    config=config,
)

Configuration Options Explained

EmbeddingConfig: Controls text-to-vector conversion

  • model_name: Hugging Face model for embeddings
  • batch_size: Batch size for encoding (adjust based on RAM)
  • normalize: L2-normalize embeddings for cosine similarity
  • device: "cpu" or "cuda" (training only, inference is CPU)

ANNConfig: Controls approximate nearest neighbor search

  • M: HNSW graph connectivity (higher = better recall, more memory)
  • ef_construction: Build-time search depth (higher = better index quality)
  • ef_search: Query-time search depth (higher = better recall, slower)
  • metric: "ip" (inner product) or "l2" (Euclidean distance)

CovisConfig: Controls co-visitation patterns

  • top_k: Number of related items to keep per seed item
  • tau_ms: Time decay parameter (items viewed together within this window are related)
  • session_gap_minutes: Gap to split sessions (e.g., 30 minutes of inactivity)

BrandPopConfig: Controls brand-level trending items

  • half_life_days: How quickly popularity decays (7 days = last week matters most)
  • top_n: Keep top N items per brand

RankerConfig: Controls LightGBM learning-to-rank model

  • objective: "lambdarank" for ranking optimization
  • num_leaves: Tree complexity (higher = more expressive, risk overfitting)
  • learning_rate: Gradient descent step size
  • num_boost_round: Number of trees

MMRConfig: Controls result diversification

  • lambda_param: 0.0 = maximize diversity, 1.0 = maximize relevance

candidate_sizes: How many candidates to retrieve from each source before ranking

CLI Reference

# Train model
pocket-recs fit <interactions.parquet> <catalog.csv> <output_dir> [--model MODEL_NAME]

# Generate recommendations
pocket-recs recommend <artifacts_dir> <catalog.csv> [--user USER_ID] [--brand BRAND] [--top-k K]

# Start API server
pocket-recs serve <artifacts_dir> <catalog.csv> [--host HOST] [--port PORT] [--reload]

# Show version
pocket-recs version

Data Schema

Catalog (CSV)

Column Type Required Description
item_id string Yes Unique item identifier
title string Yes Product title
brand string No Brand name
category string No Product category
short_desc string No Short description
price float No Product price
in_stock boolean No Stock availability

Interactions (Parquet)

Column Type Required Description
user_id string Yes User identifier
item_id string Yes Item identifier
timestamp int64 Yes Unix timestamp (milliseconds)
event string Yes Event type: view/add/purchase
brand string No Brand at interaction time
quantity int No Quantity (default: 1)
price float No Price at interaction time

Research & References

This project implements patterns from leading recommendation systems research:

  • Two-Tower Retrieval: YouTube DNN architecture (Covington et al., 2016)
  • Self-Attention Sequences: SASRec (Kang & McAuley, 2018), BERT4Rec (Sun et al., 2019)
  • ANN Search: HNSW (Malkov & Yashunin, 2016), FAISS (Johnson et al., 2017)
  • Learning-to-Rank: LambdaRank/LambdaMART (Burges et al., 2006-2010)
  • MMR Diversification: Maximal Marginal Relevance (Carbonell & Goldstein, 1998)
  • Collaborative Filtering: Item2Vec (Barkan & Koenigstein, 2016), SPPMI (Levy & Goldberg, 2014)

See REFERENCES.md for full citations and links.

Roadmap

  • Item2Vec/SPPMI collaborative embeddings
  • Cross-encoder reranking (ONNX quantized)
  • Sequential transformer features (SASRec/BERT4Rec)
  • A/B testing utilities
  • Offline evaluation metrics (NDCG@K, Recall@K, MAP@K)
  • DiskANN support for >1M items
  • OpenVINO INT8 quantization
  • Streamlit demo UI
  • Docker Compose deployment

Troubleshooting

Common Issues

ImportError: No module named 'faiss' or 'hnswlib'

pip install pocket-recs[ann]

FastAPI/uvicorn not found

pip install pocket-recs[api]

Out of Memory during training

  • Reduce batch_size in EmbeddingConfig (try 128 or 64)
  • Use a smaller embedding model (e.g., sentence-transformers/paraphrase-MiniLM-L3-v2)
  • Process data in chunks if you have >1M interactions

Slow recommendations

  • Reduce ef_search in ANNConfig (try 32 or 16)
  • Reduce candidate_sizes for each retrieval source
  • Ensure embeddings are memory-mapped for large catalogs

Poor recommendation quality

  • Ensure you have enough interaction data (at least 1000+ interactions)
  • Check that timestamp values are in milliseconds, not seconds
  • Verify event types are one of: "view", "add", "purchase"
  • Increase candidate_sizes to retrieve more candidates
  • Adjust lambda_param in MMRConfig for more/less diversity

FAISS not available on Windows/ARM

# Use hnswlib-only installation
pip install pocket-recs hnswlib

Frequently Asked Questions (FAQ)

Do I need a GPU?

No! Pocket-Recs is designed to run entirely on CPU. Training and inference both work on modest hardware.

What's the minimum amount of data needed?

  • Minimum: 100+ items, 1000+ interactions, 50+ users
  • Recommended: 1000+ items, 10k+ interactions, 500+ users
  • More data = better recommendations!

How often should I retrain the model?

  • Small catalogs (<10k items): Daily or weekly
  • Medium catalogs (10-50k): Weekly or bi-weekly
  • Large catalogs (50k+): Weekly or monthly
  • Retrain when you notice quality degradation or add many new items

Can I use this for cold-start items (new products)?

Yes! New items with no interactions will still appear in recommendations through:

  • Semantic similarity (text embeddings from title/description)
  • Brand popularity (if the brand has historical data)

How do I handle seasonal or trending items?

  • Use the half_life_days parameter in BrandPopConfig (default: 7 days)
  • Lower values (3-5 days) give more weight to recent trends
  • Higher values (14-30 days) smooth out short-term fluctuations

Can I customize the ranking model?

Yes! You can:

  1. Adjust RankerConfig parameters (num_leaves, learning_rate, etc.)
  2. Add custom features to the ranking stage
  3. Use a different ranker altogether (replace LightGBM with your own model)

What's the difference between recent interactions and the training data?

  • Training data (interactions.parquet): Historical data used to train models offline
  • Recent interactions (in API request): Real-time user behavior used at inference time
  • Recent interactions update the user's profile without retraining

How do I deploy this to production?

See examples/PRODUCTION_README.md and examples/PRODUCTION_API_GUIDE.md for:

  • Docker deployment
  • Load balancing
  • Monitoring and logging
  • A/B testing
  • Artifact versioning

Can I use this with other languages (Java, Node.js, etc.)?

Yes! The REST API allows any language to consume recommendations. You can also:

  • Call the Python API from other languages via process execution
  • Export ONNX models for cross-platform inference (coming soon)

How do I evaluate recommendation quality?

Use the testing scripts in examples/:

  • test_recommendations.py: Basic quality checks
  • test_hybrid_recommendations.py: Compare different strategies
  • Implement offline metrics (NDCG@K, Recall@K) for your dataset

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

Apache License 2.0 - See LICENSE for details.

Citation

If you use Pocket-Recs in your research or production systems, please cite:

@software{pocket_recs,
  title = {Pocket-Recs: CPU-Only Hybrid Recommender System},
  author = {Amjad},
  year = {2025},
  url = {https://github.com/amjad/pocket-recs}
}

Acknowledgments

Built with:

Support


Built with best practices, research-backed algorithms, and a focus on practical deployment.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pocket_recs-0.1.0.tar.gz (56.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pocket_recs-0.1.0-py3-none-any.whl (31.3 kB view details)

Uploaded Python 3

File details

Details for the file pocket_recs-0.1.0.tar.gz.

File metadata

  • Download URL: pocket_recs-0.1.0.tar.gz
  • Upload date:
  • Size: 56.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for pocket_recs-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8c5d74be6d04e6f2fa25a690f3573ec5ea06a7bff977933688558b6b4d3d3d64
MD5 40b9938c0cc02e40bd1fc77005b2258f
BLAKE2b-256 e53f37f6ff9f05c6e8596715ff6b78e656d809ebb6690b1a633aa5d739309174

See more details on using hashes here.

File details

Details for the file pocket_recs-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pocket_recs-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for pocket_recs-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 89191cf502812451191a8c91204d334c77e6bc8337146a3d681622ef005af2f0
MD5 deba10d663ffac481427c1e00995bc2a
BLAKE2b-256 bd4d1bedc6db9817f2974f1a1e1e17e937bae5de426e35a035a833b2c4e40998

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page