Metadata-Version: 2.4
Name: searchrec
Version: 0.1.1
Summary: Plug-in recommendation engine router for FastAPI
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.111
Requires-Dist: aiosqlite>=0.20
Requires-Dist: numpy>=1.26
Requires-Dist: scikit-learn>=1.4
Requires-Dist: scipy>=1.12
Provides-Extra: dev
Requires-Dist: uvicorn[standard]; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Dynamic: license-file

# SearchRec

![PyPI](https://img.shields.io/pypi/v/searchrec)
![CI](https://github.com/dariu5-dev/searchrec/actions/workflows/ci.yml/badge.svg)

A recommendation engine that mounts directly into any FastAPI app. Tracks click sessions, learns what gets browsed together, and serves "you may also like" recommendations with an admin dashboard to visualise the model.

Install into any FastAPI app in under a minute.

## Install

```bash
pip install searchrec
```

## Integration

```python
from fastapi import FastAPI
from searchrec import create_router, SearchRec, searchrec_lifespan
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app):
    async with searchrec_lifespan("./rec.db"):
        yield

app = FastAPI(lifespan=lifespan)
app.include_router(
    create_router(db_path="./rec.db", frontend_prefix="/rec"),
    prefix="/rec",
)
```

Then push your product catalogue once (e.g. at startup or via a management command):

```python
rec = SearchRec(db_path="./rec.db")
await rec.sync_products([
    {"item_id": "shirt_001", "name": "Blue Tee", "category": "Clothing", "price": 29.99},
    {"item_id": "shoe_001",  "name": "Runner X",  "category": "Shoes",    "price": 89.99},
    # ...
])
```

After users have browsed the store, train the model:

```
POST /rec/api/retrain
```

Then recommendations are live:

```
GET /rec/api/recommend/shirt_001
```

### `frontend_prefix` must match `prefix=`

The bundled frontend injects `window.REC_BASE` via a `/config.js` endpoint so all API calls resolve correctly when the router is mounted at a sub-path. The value of `frontend_prefix` must be identical to the `prefix=` argument passed to `include_router`.

```python
# Mounted at /rec — both must say "/rec"
app.include_router(create_router(frontend_prefix="/rec"), prefix="/rec")

# Mounted at root — omit both (default is "")
app.include_router(create_router())
```

### Composable lifespan

If your app already has a lifespan, compose with `searchrec_lifespan`:

```python
from searchrec import searchrec_lifespan

@asynccontextmanager
async def lifespan(app):
    async with searchrec_lifespan("./rec.db"):
        # your own startup work here
        yield
        # your own shutdown work here
```

Alternatively, `create_router` registers a `@router.on_event("startup")` handler as a fallback for apps that don't use a lifespan.

## How it works

Every time a user clicks items in the same session, those items get a +1 in a co-click matrix. After enough sessions, cosine similarity is computed over the matrix to produce ranked recommendations. Similarity scores are cached so serving is a single database lookup with no ML at request time.

```
User clicks -> /api/track (batched)
                  |
           click_events table
                  |
        POST /api/retrain (offline)
        load sessions -> co-click matrix -> cosine similarity -> cache
                  |
        GET /api/recommend/{item_id} -> DB lookup -> top-N items
```

## Configuration

Pass keyword arguments to `create_router`, or build a `SearchRecConfig` object:

```python
from searchrec import create_router, SearchRecConfig

config = SearchRecConfig(
    db_path="./rec.db",
    min_sessions_to_retrain=10,  # minimum sessions before retrain proceeds
    min_coclick=2,               # minimum co-clicks for a pair to count
    top_n=10,                    # recommendations cached per item
    recommend_limit_default=10,  # default ?limit= on /api/recommend
    recommend_limit_max=50,      # maximum ?limit= allowed
    serve_frontend=True,         # set False to disable bundled UI routes
    frontend_prefix="/rec",      # must match prefix= in include_router
)

app.include_router(create_router(config=config), prefix="/rec")
```

## API

| Method | Endpoint | Description |
|---|---|---|
| GET | `/health` | Health check |
| POST | `/api/track` | Ingest a batch of click events (max 100) |
| POST | `/api/retrain` | Run the full recommendation pipeline |
| GET | `/api/recommend/{item_id}` | Top similar items for an item |
| PUT/POST | `/api/items` | Upsert a batch of products |
| DELETE | `/api/items/{item_id}` | Remove a product |
| GET | `/api/items/search?q=` | Search the product catalogue |
| GET | `/api/items/{item_id}` | Single item detail |
| GET | `/api/admin/stats` | Summary stats (clicks, coverage, avg score) |
| GET | `/api/admin/clicks/top` | Top items by click count |
| GET | `/api/admin/clicks/timeseries` | Daily click volume (last N days) |
| GET | `/api/admin/matrix` | Co-click matrix for top N items |
| GET | `/api/admin/export` | Download all recommendations as JSON |
| GET | `/api/admin/export/csv` | Download all recommendations as CSV |

## Bundled UI

When `serve_frontend=True` (the default), the router also serves:

| Route | Description |
|---|---|
| `/` | Store, search products and click to see recommendations |
| `/admin` | Admin dashboard |
| `/style.css` | Shared stylesheet |
| `/tracker.js` | Click event tracker (auto-loaded by the store UI) |
| `/config.js` | Injects `window.REC_BASE` for correct API prefixing |

## Admin dashboard

- **Stats**: total clicks, recommendation coverage, avg similarity score, last retrain time
- **Click volume**: 14-day line chart
- **Co-click matrix**: heatmap of the top 20 most-clicked items, hover for exact counts
- **Top items**: horizontal bar chart ranked by click count
- **Item inspector**: search any product and inspect its recommendations with similarity scores

## Stack

| Layer | Tech |
|---|---|
| Backend | Python, FastAPI, aiosqlite |
| ML | numpy, scikit-learn (cosine similarity) |
| Database | SQLite (WAL mode) |
| Frontend | Vanilla JS, HTML/CSS |
| Charts | Chart.js |

## Demo app

`ecommerce-rec/` contains a standalone demo with a seed script to populate sample products and click sessions:

```bash
cd ecommerce-rec/backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python seed.py
uvicorn main:app --reload
```

## Roadmap

- Phase 2: Word2Vec embeddings on click sequences + FAISS approximate nearest-neighbour search
- Nightly automated retrain (cron)
- User authentication on the admin dashboard
- Native integrations for Next.js, Express, and NestJS
