Files
Priceflow/app/routers/search.py
T
Claude 1116c3a648 feat: Add multi-site product search system with SearXNG integration
This commit adds a comprehensive product search feature that allows users
to search for products across multiple configured e-commerce sites.

Backend changes:
- Add SearXNG service to docker-compose for meta-search capability
- Add SearchSite model and Alembic migration with default sites
- Create searxng_service.py for search queries across sites
- Create light_scraper_service.py for fast HTTP-based extraction
- Create search_service.py orchestrator combining SearXNG + HTTP + Browserless
- Add API endpoints for search (/api/search) with SSE streaming
- Add CRUD endpoints for search sites (/api/search-sites)

Frontend changes:
- Add new /search page with real-time results display
- Create SearchResultCard component for product display
- Add Progress, Checkbox, and Badge UI components
- Update Settings page with search sites management section
- Add navigation link to search page
- Create new modern SVG logo with price-flow gradient design
- Update favicon to SVG format

The search system uses a hybrid approach:
1. SearXNG finds product URLs across configured sites
2. Light HTTP scraper attempts fast extraction
3. Falls back to Browserless for JavaScript-heavy sites
4. Results stream progressively via SSE
2025-11-22 11:03:48 +00:00

90 lines
2.5 KiB
Python

"""
API Router pour la recherche de produits
Utilise Server-Sent Events (SSE) pour le streaming des résultats
"""
import json
import logging
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.database import get_db
from app.services import search_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/search", tags=["search"])
@router.get("")
async def search_products(
q: str = Query(..., min_length=1, description="Terme de recherche"),
sites: str | None = Query(None, description="IDs des sites, séparés par virgule"),
max_results: int = Query(20, ge=1, le=50, description="Nombre max de résultats"),
db: Session = Depends(get_db),
):
"""
Recherche des produits sur les sites configurés.
Retourne un flux SSE avec les résultats au fur et à mesure.
Format des événements SSE:
- event: progress
- data: {"status": "...", "total": N, "completed": N, "results": [...]}
"""
# Parser les IDs de sites
site_ids = None
if sites:
try:
site_ids = [int(s.strip()) for s in sites.split(",") if s.strip()]
except ValueError:
site_ids = None
async def generate():
try:
async for progress in search_service.search_products(
query=q,
db=db,
site_ids=site_ids,
max_results=max_results,
):
# Format SSE
data = progress.model_dump_json()
yield f"event: progress\ndata: {data}\n\n"
except Exception as e:
logger.error(f"Erreur recherche: {e}")
error_data = json.dumps({
"status": "error",
"total": 0,
"completed": 0,
"message": str(e),
"results": [],
})
yield f"event: progress\ndata: {error_data}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@router.get("/health")
async def search_health():
"""Vérifie que le service de recherche est opérationnel"""
from app.services import searxng_service
searxng_ok = await searxng_service.health_check()
return {
"status": "ok" if searxng_ok else "degraded",
"searxng": "ok" if searxng_ok else "unavailable",
}