Files
Priceflow/app/services/duckduckgo_service.py
T
Claude 36415b9cfc feat: Replace SearXNG with DuckDuckGo for product search
- Remove SearXNG container and configuration files
- Add duckduckgo-search library for web search
- Create duckduckgo_service.py with async search support
- Update search_service.py to use DuckDuckGo instead of SearXNG
- Fix BROWSERLESS_URL port to 3012

This eliminates the persistent limiter.toml directory issue and
provides a simpler, more reliable search solution.
2025-11-22 14:38:03 +00:00

168 lines
4.8 KiB
Python

"""
DuckDuckGo Search Service - Recherche de produits via DuckDuckGo
"""
import asyncio
import logging
from urllib.parse import urlparse
from duckduckgo_search import DDGS
logger = logging.getLogger(__name__)
class SearchResult:
"""Résultat de recherche DuckDuckGo"""
def __init__(self, url: str, title: str, snippet: str, source: str):
self.url = url
self.title = title
self.snippet = snippet
self.source = source
def __repr__(self):
return f"SearchResult(url={self.url}, title={self.title[:30]}...)"
async def search(
query: str,
domains: list[str],
max_results: int = 20,
timeout: float = 15.0,
) -> list[SearchResult]:
"""
Effectue une recherche DuckDuckGo ciblée sur les domaines spécifiés.
Args:
query: Terme de recherche (ex: "lutin de noel")
domains: Liste des domaines à cibler (ex: ["amazon.fr", "fnac.com"])
max_results: Nombre maximum de résultats à retourner
timeout: Timeout en secondes
Returns:
Liste de SearchResult avec url, title, snippet, source
"""
if not domains:
logger.warning("Aucun domaine spécifié pour la recherche")
return []
# Nettoyer les domaines (enlever protocole, www, etc.)
clean_domains = [_clean_domain(d) for d in domains]
clean_domains = [d for d in clean_domains if d]
# Construire la requête avec site: pour chaque domaine
site_query = " OR ".join([f"site:{domain}" for domain in clean_domains])
full_query = f"{query} ({site_query})"
logger.info(f"Recherche DuckDuckGo: {full_query}")
try:
# Exécuter la recherche dans un thread séparé (duckduckgo_search est synchrone)
loop = asyncio.get_event_loop()
results = await loop.run_in_executor(
None,
lambda: _do_search(full_query, max_results * 2, timeout),
)
# Filtrer pour ne garder que les domaines ciblés
filtered_results = []
seen_urls = set()
for item in results:
url = item.get("href", "") or item.get("link", "")
if not url:
continue
# Éviter les doublons
if url in seen_urls:
continue
seen_urls.add(url)
# Vérifier que l'URL appartient à un des domaines ciblés
url_domain = _extract_domain(url)
if not any(domain in url_domain for domain in clean_domains):
continue
filtered_results.append(
SearchResult(
url=url,
title=item.get("title", ""),
snippet=item.get("body", "") or item.get("snippet", ""),
source=url_domain,
)
)
if len(filtered_results) >= max_results:
break
logger.info(f"DuckDuckGo: {len(filtered_results)} résultats trouvés")
return filtered_results
except Exception as e:
logger.error(f"Erreur DuckDuckGo: {e}")
return []
def _do_search(query: str, max_results: int, timeout: float) -> list[dict]:
"""Effectue la recherche synchrone DuckDuckGo"""
try:
with DDGS() as ddgs:
results = list(ddgs.text(
query,
region="fr-fr",
safesearch="off",
max_results=max_results,
))
return results
except Exception as e:
logger.error(f"Erreur lors de la recherche DuckDuckGo: {e}")
return []
def _extract_domain(url: str) -> str:
"""Extrait le domaine d'une URL"""
try:
parsed = urlparse(url)
domain = parsed.netloc.lower()
# Retirer www. si présent
if domain.startswith("www."):
domain = domain[4:]
return domain
except Exception:
return ""
def _clean_domain(domain: str) -> str:
"""Nettoie un domaine (enlève protocole, www, slash final, etc.)"""
if not domain:
return ""
domain = domain.strip()
# Si c'est une URL complète, extraire le domaine
if domain.startswith(("http://", "https://")):
domain = _extract_domain(domain)
else:
# Nettoyer le domaine directement
domain = domain.lower()
if domain.startswith("www."):
domain = domain[4:]
# Retirer le slash final
domain = domain.rstrip("/")
return domain
async def health_check() -> bool:
"""Vérifie que DuckDuckGo est accessible"""
try:
loop = asyncio.get_event_loop()
results = await loop.run_in_executor(
None,
lambda: _do_search("test", 1, 5.0),
)
return len(results) > 0
except Exception as e:
logger.warning(f"DuckDuckGo health check failed: {e}")
return False