Metadata-Version: 2.4
Name: searchrec
Version: 0.1.0
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

An e-commerce recommendation engine built on item-item collaborative filtering. Tracks user click sessions, learns what gets browsed together, and serves "you may also like" recommendations — with an admin dashboard to visualize the model.

Install into any FastAPI app in under a minute.

## Install

```bash
pip install git+https://github.com/dariu5-dev/searchrec.git
```

## 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 you pass 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 is cached to a database so serving is just a lookup — 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, 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
