mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-13 18:26:20 +02:00
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
90 lines
2.5 KiB
Python
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",
|
|
}
|