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.
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_idandtitleare 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, andeventare 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 identifierk(optional, default=20): Number of recommendations (1-100)recent(optional): List of recent user interactionsbrand(optional): Filter recommendations by brandexclusions(optional): List of item IDs to exclude from resultsfilters(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:
-
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)
-
Ranking Stage: Re-score candidates using LightGBM LambdaRank model
- Combines signals: relevance, recency, popularity, diversity
- Learns from interaction patterns (views → adds → purchases)
-
Diversification Stage: Apply MMR to reduce redundancy
- Ensures results aren't all from the same category/brand
- Balances relevance with variety
-
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 embeddingsbatch_size: Batch size for encoding (adjust based on RAM)normalize: L2-normalize embeddings for cosine similaritydevice: "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 itemtau_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 optimizationnum_leaves: Tree complexity (higher = more expressive, risk overfitting)learning_rate: Gradient descent step sizenum_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_sizeinEmbeddingConfig(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_searchinANNConfig(try 32 or 16) - Reduce
candidate_sizesfor 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
timestampvalues are in milliseconds, not seconds - Verify
eventtypes are one of: "view", "add", "purchase" - Increase
candidate_sizesto retrieve more candidates - Adjust
lambda_paramin 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_daysparameter inBrandPopConfig(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:
- Adjust
RankerConfigparameters (num_leaves, learning_rate, etc.) - Add custom features to the ranking stage
- 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 checkstest_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:
- sentence-transformers for text embeddings
- FAISS / hnswlib for ANN search
- LightGBM for learning-to-rank
- Polars for fast data processing
- FastAPI for REST API
- Typer for CLI
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: Read the Docs (coming soon)
Built with best practices, research-backed algorithms, and a focus on practical deployment.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c5d74be6d04e6f2fa25a690f3573ec5ea06a7bff977933688558b6b4d3d3d64
|
|
| MD5 |
40b9938c0cc02e40bd1fc77005b2258f
|
|
| BLAKE2b-256 |
e53f37f6ff9f05c6e8596715ff6b78e656d809ebb6690b1a633aa5d739309174
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89191cf502812451191a8c91204d334c77e6bc8337146a3d681622ef005af2f0
|
|
| MD5 |
deba10d663ffac481427c1e00995bc2a
|
|
| BLAKE2b-256 |
bd4d1bedc6db9817f2974f1a1e1e17e937bae5de426e35a035a833b2c4e40998
|