joel commited on
Commit
491c975
·
1 Parent(s): b009395

maj app pour recup les data sur le db

Browse files
Files changed (1) hide show
  1. app.py +118 -204
app.py CHANGED
@@ -81,201 +81,124 @@ class StatsResponse(BaseModel):
81
  # SEARCH ENGINE - Recherche locale optimisée
82
  # ============================================================================
83
 
84
- class LocalSearchEngine:
85
- """Moteur de recherche local ultra-rapide avec fuzzy matching"""
86
-
87
- def __init__(self, documents_file: Path):
88
- self.documents_file = documents_file
89
- self.documents = []
90
- self.load_documents()
91
-
92
- def load_documents(self):
93
- """Charge les documents depuis le fichier JSON"""
94
- if self.documents_file.exists():
95
- try:
96
- with open(self.documents_file, 'r', encoding='utf-8') as f:
97
- self.documents = json.load(f)
98
- logger.info(f"✅ {len(self.documents)} documents chargés")
99
- except Exception as e:
100
- logger.error(f"Erreur chargement documents: {e}")
101
- self.documents = []
102
- else:
103
- self.documents = []
104
-
105
- def reload(self):
106
- """Recharge les documents (après scraping)"""
107
- self.load_documents()
108
 
109
- def fuzzy_match(self, text: str, query: str, threshold: float = 0.6) -> bool:
110
- """Fuzzy matching simple basé sur la distance de Levenshtein"""
111
- text = text.lower()
112
- query = query.lower()
113
-
114
- # Recherche exacte d'abord
115
- if query in text:
116
- return True
117
-
118
- # Fuzzy matching pour les mots individuels
119
- query_words = query.split()
120
- text_words = text.split()
121
-
122
- for q_word in query_words:
123
- for t_word in text_words:
124
- # Calcul de similarité simple
125
- if len(q_word) < 3:
126
- if q_word == t_word:
127
- return True
128
- else:
129
- # Tolérance aux fautes pour mots > 3 caractères
130
- if self._similarity(q_word, t_word) >= threshold:
131
- return True
132
-
133
- return False
134
 
135
- def _similarity(self, s1: str, s2: str) -> float:
136
- """Calcule la similarité entre deux chaînes (0-1)"""
137
- if s1 == s2:
138
- return 1.0
139
-
140
- # Distance de Levenshtein simplifiée
141
- len_s1, len_s2 = len(s1), len(s2)
142
- if abs(len_s1 - len_s2) > 2:
143
- return 0.0
144
-
145
- # Compte les caractères communs
146
- common = sum(1 for a, b in zip(s1, s2) if a == b)
147
- max_len = max(len_s1, len_s2)
148
-
149
- return common / max_len if max_len > 0 else 0.0
150
 
151
- def search(
152
  self,
153
  query: str,
154
  pays: Optional[str] = None,
155
  langue: Optional[str] = None,
156
- limit: int = 10,
157
  fuzzy: bool = True
158
  ) -> List[Dict[str, Any]]:
159
- """Recherche dans les documents avec scoring"""
160
 
161
- results = []
162
- query_lower = query.lower()
163
 
164
- for doc in self.documents:
165
- score = 0.0
166
-
167
- # Filtres
168
- if pays and doc.get('pays') != pays:
169
- continue
170
- if langue and doc.get('langue') != langue:
171
- continue
172
 
173
- # Scoring
174
- titre = doc.get('titre', '')
175
- texte = doc.get('texte', '')
 
 
 
 
 
176
 
177
- if fuzzy:
178
- # Recherche permissive
179
- if self.fuzzy_match(titre, query):
180
- score += 10.0 # Boost titre
181
- if self.fuzzy_match(texte, query):
182
- score += 5.0
183
- else:
184
- # Recherche exacte
185
- if query_lower in titre.lower():
186
- score += 10.0
187
- if query_lower in texte.lower():
188
- score += 5.0
189
 
190
- # Boost par pertinence
191
- if 'tags' in doc:
192
- for tag in doc.get('tags', []):
193
- if query_lower in tag.lower():
194
- score += 3.0
195
-
196
- if score > 0:
197
- results.append({
198
- **doc,
199
- '_score': score
200
- })
201
-
202
- # Tri par score décroissant
203
- results.sort(key=lambda x: x['_score'], reverse=True)
204
-
205
- return results[:limit]
206
-
207
- def get_stats(self) -> Dict[str, Any]:
208
- """Retourne les statistiques de la base"""
209
- pays_count = {}
210
- langues_count = {}
211
- sources_count = {}
212
-
213
- for doc in self.documents:
214
- # Pays
215
- pays = doc.get('pays', 'Inconnu')
216
- pays_count[pays] = pays_count.get(pays, 0) + 1
217
 
218
- # Langues
219
- langue = doc.get('langue', 'Inconnu')
220
- langues_count[langue] = langues_count.get(langue, 0) + 1
221
-
222
- # Sources
223
- source_url = doc.get('source_url', '')
224
- if source_url:
225
- domain = source_url.split('/')[2] if len(source_url.split('/')) > 2 else 'Inconnu'
226
- sources_count[domain] = sources_count.get(domain, 0) + 1
227
-
228
- return {
229
- 'total_documents': len(self.documents),
230
- 'pays': pays_count,
231
- 'langues': langues_count,
232
- 'sources': sources_count,
233
- 'derniere_mise_a_jour': datetime.now().isoformat()
234
- }
235
-
236
- # Instance globale du moteur de recherche
237
- search_engine = LocalSearchEngine(DOCUMENTS_FILE)
 
 
 
 
 
 
 
238
 
239
  # ============================================================================
240
  # API ENDPOINTS
241
  # ============================================================================
242
 
243
-
244
-
245
  @app.get("/api/health")
246
  async def health():
247
  """Health check"""
 
 
 
 
 
 
 
248
  return {
249
- "status": "healthy",
250
- "documents_loaded": len(search_engine.documents),
251
  "timestamp": datetime.now().isoformat()
252
  }
253
 
254
  @app.post("/api/search", response_model=SearchResponse)
255
  async def api_search(request: SearchRequest):
256
- """
257
- Endpoint de recherche principal
258
-
259
- **Paramètres:**
260
- - query: Texte à rechercher
261
- - pays: Filtrer par pays (optionnel)
262
- - langue: Filtrer par langue (optionnel)
263
- - limit: Nombre de résultats (défaut: 10)
264
- - fuzzy: Recherche permissive avec tolérance aux fautes (défaut: true)
265
-
266
- **Exemple:**
267
- ```json
268
- {
269
- "query": "économie togo",
270
- "pays": "Togo",
271
- "limit": 20,
272
- "fuzzy": true
273
- }
274
- ```
275
- """
276
  start_time = datetime.now()
277
 
278
- results = search_engine.search(
279
  query=request.query,
280
  pays=request.pays,
281
  langue=request.langue,
@@ -300,11 +223,7 @@ async def api_search_get(
300
  limit: int = Query(10, ge=1, le=100, description="Nombre de résultats"),
301
  fuzzy: bool = Query(True, description="Recherche permissive")
302
  ):
303
- """
304
- Endpoint de recherche (GET)
305
-
306
- **Exemple:** `/api/search?q=économie&pays=Togo&limit=20`
307
- """
308
  request = SearchRequest(
309
  query=q,
310
  pays=pays,
@@ -316,16 +235,8 @@ async def api_search_get(
316
 
317
  @app.get("/api/stats", response_model=StatsResponse)
318
  async def api_stats():
319
- """
320
- Retourne les statistiques de la base de données
321
-
322
- **Retourne:**
323
- - total_documents: Nombre total de documents
324
- - pays: Répartition par pays
325
- - langues: Répartition par langue
326
- - sources: Répartition par source
327
- """
328
- stats = search_engine.get_stats()
329
  return StatsResponse(**stats)
330
 
331
  @app.get("/api/documents")
@@ -333,16 +244,15 @@ async def api_documents(
333
  skip: int = Query(0, ge=0),
334
  limit: int = Query(10, ge=1, le=100)
335
  ):
336
- """
337
- Retourne la liste des documents (paginée)
338
-
339
- **Paramètres:**
340
- - skip: Nombre de documents à sauter
341
- - limit: Nombre de documents à retourner
342
- """
343
- documents = search_engine.documents[skip:skip+limit]
344
  return {
345
- "total": len(search_engine.documents),
346
  "skip": skip,
347
  "limit": limit,
348
  "documents": documents
@@ -350,31 +260,36 @@ async def api_documents(
350
 
351
  @app.get("/api/documents/{doc_id}")
352
  async def api_document_by_id(doc_id: str):
353
- """Retourne un document par son ID"""
354
- for doc in search_engine.documents:
355
- if doc.get('id') == doc_id:
356
- return doc
357
- raise HTTPException(status_code=404, detail="Document non trouvé")
 
 
 
 
 
 
 
 
 
358
 
359
  @app.post("/api/reload")
360
  async def api_reload():
361
- """Recharge les documents depuis le fichier (après scraping)"""
362
- search_engine.reload()
363
- return {
364
- "status": "success",
365
- "documents_loaded": len(search_engine.documents)
366
- }
367
 
368
  # ============================================================================
369
  # GRADIO INTERFACE
370
  # ============================================================================
371
 
372
- def gradio_search(query: str, pays: str, langue: str, fuzzy: bool):
373
- """Fonction de recherche pour Gradio"""
374
  if not query:
375
  return "⚠️ Veuillez entrer une requête de recherche"
376
 
377
- results = search_engine.search(
378
  query=query,
379
  pays=pays if pays != "Tous" else None,
380
  langue=langue if langue != "Toutes" else None,
@@ -392,11 +307,10 @@ def gradio_search(query: str, pays: str, langue: str, fuzzy: bool):
392
  titre = doc.get('titre', 'Sans titre')
393
  texte = doc.get('texte', '')[:200] + "..."
394
  pays_doc = doc.get('pays', 'Inconnu')
395
- source = doc.get('source_url', '')
396
  score = doc.get('_score', 0)
397
 
398
  output += f"### {i}. {titre}\n"
399
- output += f"**Pays:** {pays_doc} | **Score:** {score:.1f}\n\n"
400
  output += f"{texte}\n\n"
401
  output += f"[🔗 Source]({source})\n\n"
402
  output += "---\n\n"
 
81
  # SEARCH ENGINE - Recherche locale optimisée
82
  # ============================================================================
83
 
84
+ # ============================================================================
85
+ # SEARCH ENGINE - Recherche MongoDB Native
86
+ # ============================================================================
87
+
88
+ from db.mongo_connector import db as mongo_db
89
+
90
+ class MongoSearchEngine:
91
+ """Moteur de recherche connecté directement à MongoDB"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
+ def __init__(self):
94
+ self.collection = mongo_db["documents"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ async def reload(self):
97
+ """Pas nécessaire avec MongoDB (temps réel)"""
98
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ async def search(
101
  self,
102
  query: str,
103
  pays: Optional[str] = None,
104
  langue: Optional[str] = None,
105
+ limit: int = 20,
106
  fuzzy: bool = True
107
  ) -> List[Dict[str, Any]]:
108
+ """Recherche dans MongoDB avec Regex (Simple & Efficace sans Atlas Search)"""
109
 
110
+ filter_query = {}
 
111
 
112
+ # Filtres exacts
113
+ if pays and pays != "Tous":
114
+ filter_query["pays"] = pays
115
+ if langue and langue != "Toutes":
116
+ filter_query["langue"] = langue
 
 
 
117
 
118
+ # Recherche texte (Regex insensible à la case)
119
+ if query:
120
+ regex_pattern = {"$regex": query, "$options": "i"}
121
+ filter_query["$or"] = [
122
+ {"titre": regex_pattern},
123
+ {"texte": regex_pattern},
124
+ {"tags": regex_pattern}
125
+ ]
126
 
127
+ try:
128
+ cursor = self.collection.find(filter_query).limit(limit).sort("date", -1)
129
+ results = await cursor.to_list(length=limit)
 
 
 
 
 
 
 
 
 
130
 
131
+ # Conversion ObjectId -> str
132
+ for doc in results:
133
+ if '_id' in doc:
134
+ doc['_id'] = str(doc['_id'])
135
+ # Ajout d'un score fictif pour compatibilité frontend
136
+ doc['_score'] = 1.0
137
+
138
+ return results
139
+ except Exception as e:
140
+ logger.error(f"Erreur recherche MongoDB: {e}")
141
+ return []
142
+
143
+ async def get_stats(self) -> Dict[str, Any]:
144
+ """Retourne les statistiques agrégées depuis MongoDB"""
145
+ try:
146
+ total = await self.collection.count_documents({})
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ pipeline_pays = [{"$group": {"_id": "$pays", "count": {"$sum": 1}}}]
149
+ pays_data = await self.collection.aggregate(pipeline_pays).to_list(length=100)
150
+ pays_count = {d["_id"]: d["count"] for d in pays_data if d["_id"]}
151
+
152
+ pipeline_langue = [{"$group": {"_id": "$langue", "count": {"$sum": 1}}}]
153
+ langue_data = await self.collection.aggregate(pipeline_langue).to_list(length=100)
154
+ langues_count = {d["_id"]: d["count"] for d in langue_data if d["_id"]}
155
+
156
+ # Pour les sources, on fait une estimation ou on extrait le domaine
157
+ # Ici simplifié : on compte juste les total
158
+ sources_count = {"MongoDB": total}
159
+
160
+ return {
161
+ 'total_documents': total,
162
+ 'pays': pays_count,
163
+ 'langues': langues_count,
164
+ 'sources': sources_count,
165
+ 'derniere_mise_a_jour': datetime.now().isoformat()
166
+ }
167
+ except Exception as e:
168
+ logger.error(f"Erreur stats MongoDB: {e}")
169
+ return {
170
+ 'total_documents': 0, 'pays': {}, 'langues': {}, 'sources': {}, 'derniere_mise_a_jour': None
171
+ }
172
+
173
+ # Instance globale
174
+ search_engine = MongoSearchEngine()
175
 
176
  # ============================================================================
177
  # API ENDPOINTS
178
  # ============================================================================
179
 
 
 
180
  @app.get("/api/health")
181
  async def health():
182
  """Health check"""
183
+ try:
184
+ count = await search_engine.get_stats()
185
+ status = "healthy"
186
+ except:
187
+ status = "db_error"
188
+ count = {"total_documents": 0}
189
+
190
  return {
191
+ "status": status,
192
+ "documents_loaded": count["total_documents"],
193
  "timestamp": datetime.now().isoformat()
194
  }
195
 
196
  @app.post("/api/search", response_model=SearchResponse)
197
  async def api_search(request: SearchRequest):
198
+ """Endpoint de recherche principal (MongoDB)"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  start_time = datetime.now()
200
 
201
+ results = await search_engine.search(
202
  query=request.query,
203
  pays=request.pays,
204
  langue=request.langue,
 
223
  limit: int = Query(10, ge=1, le=100, description="Nombre de résultats"),
224
  fuzzy: bool = Query(True, description="Recherche permissive")
225
  ):
226
+ """Endpoint de recherche (GET)"""
 
 
 
 
227
  request = SearchRequest(
228
  query=q,
229
  pays=pays,
 
235
 
236
  @app.get("/api/stats", response_model=StatsResponse)
237
  async def api_stats():
238
+ """Retourne les statistiques de la base MongoDB"""
239
+ stats = await search_engine.get_stats()
 
 
 
 
 
 
 
 
240
  return StatsResponse(**stats)
241
 
242
  @app.get("/api/documents")
 
244
  skip: int = Query(0, ge=0),
245
  limit: int = Query(10, ge=1, le=100)
246
  ):
247
+ """Retourne la liste des documents (paginée)"""
248
+ cursor = search_engine.collection.find({}).skip(skip).limit(limit)
249
+ documents = await cursor.to_list(length=limit)
250
+ for doc in documents:
251
+ if '_id' in doc: doc['_id'] = str(doc['_id'])
252
+
253
+ total = await search_engine.collection.count_documents({})
 
254
  return {
255
+ "total": total,
256
  "skip": skip,
257
  "limit": limit,
258
  "documents": documents
 
260
 
261
  @app.get("/api/documents/{doc_id}")
262
  async def api_document_by_id(doc_id: str):
263
+ """Retourne un document par son ID (champ 'id' ou '_id')"""
264
+ doc = await search_engine.collection.find_one({"id": doc_id})
265
+ if not doc:
266
+ # Essai avec ObjectId
267
+ try:
268
+ from bson import ObjectId
269
+ doc = await search_engine.collection.find_one({"_id": ObjectId(doc_id)})
270
+ except: pass
271
+
272
+ if not doc:
273
+ raise HTTPException(status_code=404, detail="Document non trouvé")
274
+
275
+ if '_id' in doc: doc['_id'] = str(doc['_id'])
276
+ return doc
277
 
278
  @app.post("/api/reload")
279
  async def api_reload():
280
+ """Endpoint dummy pour compatibilité"""
281
+ return {"status": "success", "message": "MongoDB is real-time"}
 
 
 
 
282
 
283
  # ============================================================================
284
  # GRADIO INTERFACE
285
  # ============================================================================
286
 
287
+ async def gradio_search(query: str, pays: str, langue: str, fuzzy: bool):
288
+ """Fonction de recherche pour Gradio (Async wrapper)"""
289
  if not query:
290
  return "⚠️ Veuillez entrer une requête de recherche"
291
 
292
+ results = await search_engine.search(
293
  query=query,
294
  pays=pays if pays != "Tous" else None,
295
  langue=langue if langue != "Toutes" else None,
 
307
  titre = doc.get('titre', 'Sans titre')
308
  texte = doc.get('texte', '')[:200] + "..."
309
  pays_doc = doc.get('pays', 'Inconnu')
 
310
  score = doc.get('_score', 0)
311
 
312
  output += f"### {i}. {titre}\n"
313
+ output += f"**Pays:** {pays_doc}\n\n"
314
  output += f"{texte}\n\n"
315
  output += f"[🔗 Source]({source})\n\n"
316
  output += "---\n\n"