ziadsameh32 commited on
Commit
6d7282f
·
verified ·
1 Parent(s): b26a481

Update rag/ingest_net.py

Browse files
Files changed (1) hide show
  1. rag/ingest_net.py +30 -290
rag/ingest_net.py CHANGED
@@ -1,11 +1,9 @@
1
  # rag/ingest_net.py
2
-
3
  import uuid
4
  import os
5
  import time
6
- import traceback
7
- from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError
8
- from typing import List, Dict, Any
9
 
10
  from core.books.fetch_url import get_pdf_bytes
11
  from core.books.storage import (
@@ -16,198 +14,81 @@ from core.books.storage import (
16
  )
17
  from schemas.books.sources_schema import DocRaw
18
 
19
- MAX_INGEST_WORKERS = int(os.getenv("MAX_INGEST_WORKERS", "3"))
20
-
21
  MIN_PDF_SIZE_KB = int(os.getenv("MIN_PDF_SIZE_KB", "60"))
22
 
23
- HEAD_PAGES_N = int(os.getenv("HEAD_PAGES_N", "7"))
24
-
25
- FETCH_TIMEOUT_SECONDS = int(os.getenv("FETCH_TIMEOUT_SECONDS", "120"))
26
-
27
  EMBEDDING_MODEL = os.getenv(
28
  "EMBEDDING_MODEL",
29
  "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
30
  )
31
 
 
32
 
33
- def ingest_from_net(
34
- user_id: str,
35
- book_id: str,
36
- sources: List[Dict[str, Any]],
37
- ):
38
- """
39
- Two-phase ingest pipeline.
40
-
41
- Phase A (Concurrent):
42
- - Fetch PDF
43
- - Extract text / OCR
44
- - Preprocess
45
- - Insert raw metadata into DB
46
 
47
- Phase B (Sequential):
48
- - Load embedding model once
49
- - Embed pages
50
- - Upsert to Qdrant
 
 
51
  """
52
-
53
- start_total = time.time()
54
-
55
- print("=" * 80)
56
- print("[INGEST START]")
57
- print(f"user_id={user_id}")
58
- print(f"book_id={book_id}")
59
- print(f"sources_count={len(sources)}")
60
- print("=" * 80)
61
-
62
  init_db()
63
-
64
  if not check_db_health():
65
  raise RuntimeError("Supabase / DB is not reachable")
66
 
67
  collection_name = f"user_{user_id}__book_{book_id}"
68
 
69
- # ============================================================
70
- # Phase A Worker
71
- # ============================================================
72
-
73
  def fetch_and_prepare(src: Dict[str, Any]) -> Dict[str, Any]:
74
-
75
  url = src.get("url")
76
-
77
  doc_id = str(uuid.uuid4())
78
-
79
  start = time.time()
80
-
81
- result = {
82
- "url": url,
83
- "doc_id": doc_id,
84
- "status": "rejected",
85
- "reason": None,
86
- }
87
 
88
  try:
89
-
90
- print(f"\n[FETCH START] {url}")
91
-
92
- # ----------------------------------------------------
93
- # Fetch PDF
94
- # ----------------------------------------------------
95
-
96
  pdf_bytes = get_pdf_bytes(url)
97
-
98
  if not pdf_bytes:
99
  result["reason"] = "no_pdf_or_blocked"
100
  return result
101
 
102
- pdf_size_mb = round(len(pdf_bytes) / (1024 * 1024), 2)
103
-
104
- print(f"[FETCH DONE] {url} | size={pdf_size_mb} MB")
105
-
106
- # ----------------------------------------------------
107
- # Validate size
108
- # ----------------------------------------------------
109
-
110
  if len(pdf_bytes) < MIN_PDF_SIZE_KB * 1024:
111
  result["reason"] = "pdf_too_small"
112
  return result
113
 
114
- # ----------------------------------------------------
115
- # Delayed imports
116
- # ----------------------------------------------------
117
-
118
- from .pdf_text import (
119
- extract_text_pypdf2,
120
- is_text_usable,
121
- )
122
-
123
  from .ocr import mistral_ocr_pdf
124
 
125
- # ----------------------------------------------------
126
- # Extract text
127
- # ----------------------------------------------------
128
-
129
- print(f"[TEXT EXTRACTION START] {url}")
130
-
131
  pages = extract_text_pypdf2(pdf_bytes)
132
-
133
  joined = "\n".join(pages)
134
 
135
- # ----------------------------------------------------
136
- # Text usable?
137
- # ----------------------------------------------------
138
-
139
  if is_text_usable(joined):
140
-
141
  extraction_method = "text"
142
-
143
- print(f"[TEXT EXTRACTION SUCCESS] {url}")
144
-
145
  else:
146
-
147
- print(f"[OCR REQUIRED] {url}")
148
-
149
- # prevent OCR huge PDFs
150
  if len(pdf_bytes) > 10 * 1024 * 1024:
151
-
152
  result["reason"] = "pdf_too_large_for_ocr"
153
-
154
  return result
155
-
156
  try:
157
-
158
- print(f"[OCR START] {url}")
159
-
160
  pages = mistral_ocr_pdf(pdf_bytes)
161
-
162
  extraction_method = "ocr"
163
-
164
- print(f"[OCR DONE] {url}")
165
-
166
  except Exception as e:
167
-
168
- traceback.print_exc()
169
-
170
  result["reason"] = f"ocr_failed:{str(e)}"
171
-
172
  return result
173
 
174
- # ----------------------------------------------------
175
- # Validate extraction
176
- # ----------------------------------------------------
177
-
178
  if not pages:
179
-
180
  result["reason"] = "no_text_extracted"
181
-
182
  return result
183
 
184
- # ----------------------------------------------------
185
- # Preprocessing
186
- # ----------------------------------------------------
187
-
188
- print(f"[PREPROCESS START] {url}")
189
-
190
- from .preprocess import (
191
- normalize_arabic,
192
- drop_common_headers_footers,
193
- )
194
 
195
  language = src.get("language", "ar")
196
-
197
  if language == "ar":
198
-
199
  pages = [normalize_arabic(p) for p in pages]
200
-
201
  pages = drop_common_headers_footers(pages)
202
 
203
  pages_head = pages[:HEAD_PAGES_N]
204
 
205
- print(f"[PREPROCESS DONE] {url}")
206
-
207
- # ----------------------------------------------------
208
- # Create raw doc
209
- # ----------------------------------------------------
210
-
211
  raw_doc = DocRaw(
212
  doc_id=doc_id,
213
  user_id=user_id,
@@ -223,16 +104,9 @@ def ingest_from_net(
223
  error_reason="",
224
  )
225
 
226
- # ----------------------------------------------------
227
- # Insert DB row
228
- # ----------------------------------------------------
229
-
230
- print(f"[DB INSERT START] {url}")
231
-
232
  insert_raw_document(raw_doc)
233
 
234
- print(f"[DB INSERT DONE] {url}")
235
-
236
  result.update(
237
  {
238
  "status": "prepared",
@@ -240,143 +114,45 @@ def ingest_from_net(
240
  "raw_doc": raw_doc,
241
  "pages": pages,
242
  "extraction": extraction_method,
243
- "duration": round(time.time() - start, 2),
244
  }
245
  )
246
-
247
- print(
248
- f"[PREPARED SUCCESS] {url} "
249
- f"| pages={len(pages)} "
250
- f"| duration={round(time.time() - start, 2)}s"
251
- )
252
-
253
  return result
254
 
255
  except Exception as e:
256
-
257
- traceback.print_exc()
258
-
259
  result["reason"] = f"fetch_prepare_failed:{str(e)}"
260
-
261
- print(f"[PREPARED FAILED] {url} | {e}")
262
-
263
  return result
264
 
265
- # ============================================================
266
- # Phase A - Concurrent
267
- # ============================================================
268
-
269
- print("\n" + "=" * 80)
270
- print("[PHASE A START]")
271
- print("=" * 80)
272
-
273
  prepared_items = []
274
-
275
- with ThreadPoolExecutor(
276
- max_workers=MAX_INGEST_WORKERS
277
- ) as executor:
278
-
279
- futures = {
280
- executor.submit(fetch_and_prepare, src): src
281
- for src in sources
282
- }
283
-
284
- for future in as_completed(futures):
285
-
286
- src = futures[future]
287
-
288
- try:
289
-
290
- res = future.result(
291
- timeout=FETCH_TIMEOUT_SECONDS
292
- )
293
-
294
- except TimeoutError:
295
-
296
- res = {
297
- "url": src.get("url"),
298
- "status": "rejected",
299
- "reason": "timeout",
300
- }
301
-
302
- print(
303
- f"[TIMEOUT] {src.get('url')} "
304
- f"after {FETCH_TIMEOUT_SECONDS}s"
305
- )
306
-
307
- except Exception as e:
308
-
309
- traceback.print_exc()
310
-
311
- res = {
312
- "url": src.get("url"),
313
- "status": "rejected",
314
- "reason": f"future_failed:{str(e)}",
315
- }
316
-
317
  prepared_items.append(res)
318
 
319
- print("=" * 80)
320
- print("[PHASE A DONE]")
321
- print("=" * 80)
322
-
323
- # ============================================================
324
- # Load RAG Engine Once
325
- # ============================================================
326
-
327
- print("\n[LOADING EMBEDDING MODEL + QDRANT]")
328
-
329
  from .rag_engine_sources import ArabicBookRAGWithSources
330
 
331
  rag = ArabicBookRAGWithSources(
332
- user_id=user_id,
333
- book_id=book_id,
334
- embedding_model=EMBEDDING_MODEL,
335
  )
336
 
337
- print("[RAG ENGINE READY]")
338
-
339
- # ============================================================
340
- # Phase B - Sequential Qdrant ingest
341
- # ============================================================
342
-
343
- print("\n" + "=" * 80)
344
- print("[PHASE B START]")
345
- print("=" * 80)
346
-
347
  items_out = []
348
-
349
  ingested = 0
350
-
351
  rejected = 0
352
 
353
  for item in prepared_items:
354
-
355
  if item.get("status") != "prepared":
356
-
357
  items_out.append(item)
358
-
359
  rejected += 1
360
-
361
  continue
362
 
363
  doc_id = item["doc_id"]
364
-
365
  pages = item["pages"]
366
-
367
  raw_doc = item["raw_doc"]
368
-
369
  try:
370
-
371
- print(f"\n[QDRANT START] {doc_id}")
372
-
373
- stats = rag.ingest_pages(
374
- pages=pages,
375
- raw_doc=raw_doc,
376
- )
377
-
378
- print(f"[QDRANT DONE] {doc_id}")
379
-
380
  items_out.append(
381
  {
382
  "url": item["url"],
@@ -387,27 +163,13 @@ def ingest_from_net(
387
  "stats": stats,
388
  }
389
  )
390
-
391
  ingested += 1
392
-
393
- # free memory
394
- item["pages"] = None
395
-
396
  except Exception as e:
397
-
398
- traceback.print_exc()
399
-
400
  try:
401
-
402
- mark_raw_status(
403
- doc_id,
404
- "failed",
405
- f"qdrant_failed:{str(e)}",
406
- )
407
-
408
  except Exception:
409
- traceback.print_exc()
410
-
411
  items_out.append(
412
  {
413
  "url": item["url"],
@@ -416,28 +178,6 @@ def ingest_from_net(
416
  "doc_id": doc_id,
417
  }
418
  )
419
-
420
  rejected += 1
421
 
422
- # ============================================================
423
- # Done
424
- # ============================================================
425
-
426
- total_duration = round(
427
- time.time() - start_total,
428
- 2,
429
- )
430
-
431
- print("\n" + "=" * 80)
432
- print("[INGEST FINISHED]")
433
- print(f"ingested={ingested}")
434
- print(f"rejected={rejected}")
435
- print(f"duration={total_duration}s")
436
- print("=" * 80)
437
-
438
- return (
439
- items_out,
440
- ingested,
441
- rejected,
442
- collection_name,
443
- )
 
1
  # rag/ingest_net.py
 
2
  import uuid
3
  import os
4
  import time
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from typing import List, Dict, Any, Tuple
 
7
 
8
  from core.books.fetch_url import get_pdf_bytes
9
  from core.books.storage import (
 
14
  )
15
  from schemas.books.sources_schema import DocRaw
16
 
17
+ MAX_INGEST_WORKERS = int(os.getenv("MAX_INGEST_WORKERS", "6"))
 
18
  MIN_PDF_SIZE_KB = int(os.getenv("MIN_PDF_SIZE_KB", "60"))
19
 
 
 
 
 
20
  EMBEDDING_MODEL = os.getenv(
21
  "EMBEDDING_MODEL",
22
  "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
23
  )
24
 
25
+ HEAD_PAGES_N = int(os.getenv("HEAD_PAGES_N", "7"))
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ def ingest_from_net(user_id: str, book_id: str, sources: List[Dict[str, Any]]):
29
+ """
30
+ Two-phase ingest:
31
+ Phase A (concurrent): fetch PDFs, extract text/OCR, preprocess, insert raw doc to DB.
32
+ Phase B (sequential): load embedder once and ingest pages (encode + upsert) to Qdrant.
33
+ This avoids loading the embedding model many times (huge slow-down).
34
  """
 
 
 
 
 
 
 
 
 
 
35
  init_db()
 
36
  if not check_db_health():
37
  raise RuntimeError("Supabase / DB is not reachable")
38
 
39
  collection_name = f"user_{user_id}__book_{book_id}"
40
 
41
+ # Worker for phase A: fetch + extract + preprocess + insert_raw_document
 
 
 
42
  def fetch_and_prepare(src: Dict[str, Any]) -> Dict[str, Any]:
 
43
  url = src.get("url")
 
44
  doc_id = str(uuid.uuid4())
 
45
  start = time.time()
46
+ result = {"url": url, "doc_id": doc_id, "status": "rejected", "reason": None}
 
 
 
 
 
 
47
 
48
  try:
 
 
 
 
 
 
 
49
  pdf_bytes = get_pdf_bytes(url)
 
50
  if not pdf_bytes:
51
  result["reason"] = "no_pdf_or_blocked"
52
  return result
53
 
 
 
 
 
 
 
 
 
54
  if len(pdf_bytes) < MIN_PDF_SIZE_KB * 1024:
55
  result["reason"] = "pdf_too_small"
56
  return result
57
 
58
+ # delayed imports for optional OCR libs
59
+ from .pdf_text import extract_text_pypdf2, is_text_usable
 
 
 
 
 
 
 
60
  from .ocr import mistral_ocr_pdf
61
 
 
 
 
 
 
 
62
  pages = extract_text_pypdf2(pdf_bytes)
 
63
  joined = "\n".join(pages)
64
 
 
 
 
 
65
  if is_text_usable(joined):
 
66
  extraction_method = "text"
 
 
 
67
  else:
 
 
 
 
68
  if len(pdf_bytes) > 10 * 1024 * 1024:
 
69
  result["reason"] = "pdf_too_large_for_ocr"
 
70
  return result
 
71
  try:
 
 
 
72
  pages = mistral_ocr_pdf(pdf_bytes)
 
73
  extraction_method = "ocr"
 
 
 
74
  except Exception as e:
 
 
 
75
  result["reason"] = f"ocr_failed:{str(e)}"
 
76
  return result
77
 
 
 
 
 
78
  if not pages:
 
79
  result["reason"] = "no_text_extracted"
 
80
  return result
81
 
82
+ # preprocess
83
+ from .preprocess import normalize_arabic, drop_common_headers_footers
 
 
 
 
 
 
 
 
84
 
85
  language = src.get("language", "ar")
 
86
  if language == "ar":
 
87
  pages = [normalize_arabic(p) for p in pages]
 
88
  pages = drop_common_headers_footers(pages)
89
 
90
  pages_head = pages[:HEAD_PAGES_N]
91
 
 
 
 
 
 
 
92
  raw_doc = DocRaw(
93
  doc_id=doc_id,
94
  user_id=user_id,
 
104
  error_reason="",
105
  )
106
 
107
+ # insert raw doc (phase 1)
 
 
 
 
 
108
  insert_raw_document(raw_doc)
109
 
 
 
110
  result.update(
111
  {
112
  "status": "prepared",
 
114
  "raw_doc": raw_doc,
115
  "pages": pages,
116
  "extraction": extraction_method,
117
+ "duration": time.time() - start,
118
  }
119
  )
 
 
 
 
 
 
 
120
  return result
121
 
122
  except Exception as e:
 
 
 
123
  result["reason"] = f"fetch_prepare_failed:{str(e)}"
 
 
 
124
  return result
125
 
126
+ # Phase A: concurrent fetch + prepare
 
 
 
 
 
 
 
127
  prepared_items = []
128
+ with ThreadPoolExecutor(max_workers=MAX_INGEST_WORKERS) as ex:
129
+ futures = {ex.submit(fetch_and_prepare, src): src for src in sources}
130
+ for fut in as_completed(futures):
131
+ res = fut.result()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  prepared_items.append(res)
133
 
134
+ # Phase B: sequential embedding & qdrant upsert with single embedder instance
 
 
 
 
 
 
 
 
 
135
  from .rag_engine_sources import ArabicBookRAGWithSources
136
 
137
  rag = ArabicBookRAGWithSources(
138
+ user_id=user_id, book_id=book_id, embedding_model=EMBEDDING_MODEL
 
 
139
  )
140
 
 
 
 
 
 
 
 
 
 
 
141
  items_out = []
 
142
  ingested = 0
 
143
  rejected = 0
144
 
145
  for item in prepared_items:
 
146
  if item.get("status") != "prepared":
 
147
  items_out.append(item)
 
148
  rejected += 1
 
149
  continue
150
 
151
  doc_id = item["doc_id"]
 
152
  pages = item["pages"]
 
153
  raw_doc = item["raw_doc"]
 
154
  try:
155
+ stats = rag.ingest_pages(pages=pages, raw_doc=raw_doc)
 
 
 
 
 
 
 
 
 
156
  items_out.append(
157
  {
158
  "url": item["url"],
 
163
  "stats": stats,
164
  }
165
  )
 
166
  ingested += 1
 
 
 
 
167
  except Exception as e:
168
+ # mark failed on db
 
 
169
  try:
170
+ mark_raw_status(doc_id, "failed", f"qdrant_failed:{str(e)}")
 
 
 
 
 
 
171
  except Exception:
172
+ pass
 
173
  items_out.append(
174
  {
175
  "url": item["url"],
 
178
  "doc_id": doc_id,
179
  }
180
  )
 
181
  rejected += 1
182
 
183
+ return items_out, ingested, rejected, rag.collection