backend updates on recommendation system
This commit is contained in:
@@ -7,12 +7,20 @@ Money is returned as a decimal string, never a float.
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from app.electronics.db.connection import connect
|
||||
from app.electronics.db.repository import product_rating_and_reviews
|
||||
from app.electronics import recommend as rec
|
||||
from app.electronics.db.repository import (
|
||||
other_variants,
|
||||
product_rating_and_reviews,
|
||||
rated_products,
|
||||
rating_sources_for,
|
||||
review_sentiment_counts,
|
||||
similar_products,
|
||||
)
|
||||
from app.electronics.reviews import select_reviews
|
||||
|
||||
router = APIRouter(prefix="/elec", tags=["electronics"])
|
||||
@@ -106,7 +114,13 @@ def products(
|
||||
f"LIMIT %(limit)s OFFSET %(offset)s",
|
||||
{**params, "limit": limit, "offset": offset},
|
||||
).fetchall()
|
||||
return {"total": total, "products": [_clean(r) for r in rows]}
|
||||
sources = rating_sources_for(conn, [r["product_id"] for r in rows])
|
||||
out = []
|
||||
for r in rows:
|
||||
rating = _overall_rating(sources[r["product_id"]])
|
||||
out.append({**_clean(r), "rating": rating["value"] if rating else None,
|
||||
"rating_count": rating["count"] if rating else None})
|
||||
return {"total": total, "products": out}
|
||||
|
||||
|
||||
@router.get("/products/{product_id}")
|
||||
@@ -185,6 +199,63 @@ def _breakdown(sources: List[dict]) -> Optional[List[dict]]:
|
||||
for k in ("5", "4", "3", "2", "1")]
|
||||
|
||||
|
||||
@router.get("/products/{product_id}/recommendations")
|
||||
def recommendations(
|
||||
product_id: int,
|
||||
kind: Literal["similar", "better_rated"] = Query("similar", alias="type"),
|
||||
limit: int = Query(rec.MAX_ITEMS, ge=1, le=12),
|
||||
tn_only: bool = False,
|
||||
) -> dict:
|
||||
"""Products to suggest under this one's ratings and reviews (see
|
||||
app/electronics/recommend.py), plus the same model's other variants.
|
||||
type=similar: closest specs; type=better_rated: rated higher, similar price."""
|
||||
with connect() as conn:
|
||||
target = conn.execute(
|
||||
"SELECT product_id, best_price FROM elec.v_brand_catalog WHERE product_id = %s", (product_id,)
|
||||
).fetchone()
|
||||
if not target:
|
||||
raise HTTPException(status_code=404, detail="Product not found or not verified")
|
||||
if kind == "similar":
|
||||
similar = similar_products(conn, product_id, band=rec.SIMILAR_PRICE_BAND, tn_only=tn_only)
|
||||
rated = (rated_products(conn, product_id, band=rec.SIMILAR_PRICE_BAND, tn_only=tn_only)
|
||||
if len(similar) < rec.MIN_MATCHES else [])
|
||||
else:
|
||||
similar = []
|
||||
rated = rated_products(conn, product_id, band=rec.BETTER_PRICE_BAND, tn_only=tn_only)
|
||||
ids = {product_id} | {c["product_id"] for c in similar + rated}
|
||||
ratings = {pid: _overall_rating(src) for pid, src in rating_sources_for(conn, list(ids)).items()}
|
||||
|
||||
def with_rating(c: dict) -> dict:
|
||||
r = ratings.get(c["product_id"])
|
||||
return {**c, "best_price": _float(c.get("best_price")),
|
||||
"rating": r["value"] if r else None, "rating_count": r["count"] if r else None}
|
||||
|
||||
if kind == "similar":
|
||||
picked = rec.recommend(with_rating(dict(target)), [with_rating(c) for c in similar],
|
||||
[with_rating(c) for c in rated], limit)
|
||||
else:
|
||||
sentiment = review_sentiment_counts(conn, [c["product_id"] for c in rated])
|
||||
picked = rec.better_rated(
|
||||
with_rating(dict(target)),
|
||||
[{**with_rating(c), "sentiment": sentiment.get(c["product_id"])} for c in rated], limit)
|
||||
cards = {r["product_id"]: r for r in conn.execute(
|
||||
"SELECT product_id, brand, display_name, ram_gb, storage_gb, image_url, best_price, best_price_site "
|
||||
"FROM elec.v_brand_catalog WHERE product_id = ANY(%s)", ([c["product_id"] for c in picked],)
|
||||
)}
|
||||
variants = other_variants(conn, product_id)
|
||||
items = [
|
||||
{**_clean(cards[c["product_id"]]), "rating": c["rating"], "rating_count": c["rating_count"],
|
||||
"basis": c["basis"], "reason": c["reason"]}
|
||||
for c in picked if c["product_id"] in cards
|
||||
]
|
||||
return {"product_id": product_id, "type": kind, "items": items,
|
||||
"other_variants": [_clean(v) for v in variants]}
|
||||
|
||||
|
||||
def _float(value: Any) -> Optional[float]:
|
||||
return None if value is None else float(value)
|
||||
|
||||
|
||||
@router.get("/products/{product_id}/price-history")
|
||||
def price_history(product_id: int) -> List[dict]:
|
||||
with connect() as conn:
|
||||
|
||||
Reference in New Issue
Block a user