@@ -38,8 +38,8 @@ def get_embedding_model():
3838celery .config_from_object (settings , namespace = 'CELERY' )
3939
4040TEXT_SPLITTER = RecursiveCharacterTextSplitter (
41- chunk_size = 500 ,
42- chunk_overlap = 50 ,
41+ chunk_size = 2000 ,
42+ chunk_overlap = 200 ,
4343length_function = len ,
4444)
4545
@@ -82,6 +82,8 @@ def dispatch_processing_task(document_id: str):
8282def unpack_zip_task (document_id : str ):
8383 """
8484 The specialist for unpacking zip files.
85+ Extracts files to a persistent temporary location and passes file paths to ingest tasks
86+ to avoid serializing large content in Celery messages.
8587 """
8688 logger .info (f"[UNPACKER] Received job for document_id: { document_id } . Starting to unpack..." )
8789
@@ -99,40 +101,54 @@ def unpack_zip_task(document_id: str):
99101 db .commit ()
100102
101103 file_path = f"./uploads/{ document .id } .zip"
104+
105+
106+ temp_extract_dir = f"./uploads/temp_{ document_id } "
107+ os .makedirs (temp_extract_dir , exist_ok = True )
108+
102109 ingest_tasks = []
103110 try :
104- with tempfile . TemporaryDirectory ( ) as temp_dir :
105- with zipfile . ZipFile ( file_path , 'r' ) as zip_ref :
106- zip_ref . extractall ( temp_dir )
107-
108- logger . info ( f"[UNPACKER] Fanning out tasks for files in { document . filename } " )
109- for filename in os . listdir ( temp_dir ):
110- if filename . endswith ( '.md' ):
111- full_path = os . path . join ( temp_dir , filename )
112- with open ( full_path , 'r' , encoding = 'utf-8' ) as f :
113- content = f . read ()
114-
115- ingest_tasks . append ( ingest_file_task . s ( document_id , content , filename ))
111+ with zipfile . ZipFile ( file_path , 'r' ) as zip_ref :
112+ zip_ref . extractall ( temp_extract_dir )
113+
114+ logger . info ( f"[UNPACKER] Fanning out tasks for files in { document . filename } " )
115+ for filename in os . listdir ( temp_extract_dir ):
116+ if filename . endswith ( '.md' ):
117+ full_path = os . path . join ( temp_extract_dir , filename )
118+
119+ ingest_tasks . append ( ingest_file_from_path_task . s ( document_id , full_path , filename ))
120+
121+ logger . info ( f"[UNPACKER] Created { len ( ingest_tasks ) } ingest tasks for files in temp directory: { temp_extract_dir } " )
122+
116123 except Exception as e :
117124 logger .exception ("[UNPACKER] Failed to unpack or prepare tasks." )
118125 doc_repo .update_status_sync (document , IngestionStatus .FAILED , str (e ))
126+
127+ if os .path .exists (temp_extract_dir ):
128+ import shutil
129+ shutil .rmtree (temp_extract_dir , ignore_errors = True )
119130 return
120131
121132 if not ingest_tasks :
122133 logger .warning (f"[UNPACKER] No .md files found in { document .filename } . Marking as complete." )
123134 doc_repo .update_status_sync (document , IngestionStatus .COMPLETED )
135+
136+ if os .path .exists (temp_extract_dir ):
137+ import shutil
138+ shutil .rmtree (temp_extract_dir , ignore_errors = True )
124139 return
125140
126- callback = finalize_processing_task .s (document_id = document_id )
141+ callback = finalize_processing_task .s (document_id = document_id , temp_dir = temp_extract_dir )
127142 chord (ingest_tasks )(callback )
128143
129144 logger .info (f"[UNPACKER] Dispatched { len (ingest_tasks )} ingest jobs with a finalization callback." )
130145
131146@celery .task
132- def finalize_processing_task (results : list , document_id : str ):
147+ def finalize_processing_task (results : list , document_id : str , temp_dir : str = None ):
133148 """
134149 The callback task that inspects the results of all ingest tasks
135150 and sets the final status for the parent Document.
151+ Also cleans up the temporary directory used for extracted files.
136152 """
137153 logger .info (f"[FINALIZER] All ingest jobs finished for document { document_id } . Analyzing results..." )
138154
@@ -157,6 +173,79 @@ def finalize_processing_task(results: list, document_id: str):
157173 else :
158174 logger .info (f"[FINALIZER] All { len (results )} files ingested successfully for document { document_id } . Setting status to COMPLETED." )
159175 repo .update_status_sync (document , IngestionStatus .COMPLETED )
176+
177+ if temp_dir and os .path .exists (temp_dir ):
178+ try :
179+ import shutil
180+ shutil .rmtree (temp_dir , ignore_errors = True )
181+ logger .info (f"[FINALIZER] Cleaned up temporary directory: { temp_dir } " )
182+ except Exception as e :
183+ logger .warning (f"[FINALIZER] Failed to clean up temp directory { temp_dir } : { e } " )
184+
185+ @celery .task
186+ def ingest_file_from_path_task (document_id : str , file_path : str , original_filename : str ):
187+ """
188+ The specialist for ingesting a single file from a file path.
189+ Reads content from disk to avoid large message serialization.
190+ Uses batched processing to prevent out-of-memory errors.
191+ """
192+ logger .info (f"[INGESTOR] Ingesting file from '{ file_path } ' (original: '{ original_filename } ') for document { document_id } ." )
193+
194+ BATCH_SIZE = 500
195+
196+ try :
197+ if not os .path .exists (file_path ):
198+ raise FileNotFoundError (f"File not found: { file_path } " )
199+
200+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
201+ text = f .read ()
202+
203+ chunks_text = TEXT_SPLITTER .split_text (text )
204+ total_chunks = len (chunks_text )
205+ logger .info (f"[INGESTOR] Split '{ original_filename } ' into { total_chunks } chunks. Processing in batches of { BATCH_SIZE } ..." )
206+
207+ embedding_model = get_embedding_model ()
208+ total_saved = 0
209+
210+ for batch_start in range (0 , total_chunks , BATCH_SIZE ):
211+ batch_end = min (batch_start + BATCH_SIZE , total_chunks )
212+ batch_chunks = chunks_text [batch_start :batch_end ]
213+
214+ logger .info (f"[INGESTOR] Processing batch { batch_start } -{ batch_end } of { total_chunks } chunks..." )
215+
216+
217+ batch_embeddings = list (embedding_model .embed (batch_chunks ))
218+
219+
220+ chunks_to_create = []
221+ for i , text_chunk in enumerate (batch_chunks ):
222+ chunk = Chunk (
223+ document_id = document_id ,
224+ chunk_text = text_chunk ,
225+ embedding = batch_embeddings [i ].tolist (),
226+ chunk_metadata = {
227+ "source_filename" : original_filename ,
228+ "chunk_index" : batch_start + i
229+ }
230+ )
231+ chunks_to_create .append (chunk )
232+
233+
234+ with get_sync_db () as db :
235+ db .bulk_save_objects (chunks_to_create )
236+ db .commit ()
237+
238+ total_saved += len (chunks_to_create )
239+ logger .info (f"[INGESTOR] Saved batch to database. Progress: { total_saved } /{ total_chunks } chunks ({ total_saved * 100 // total_chunks } %)" )
240+
241+ logger .info (f"[INGESTOR] Successfully saved all { total_saved } chunks to the database." )
242+
243+ return {"status" : "SUCCESS" , "filename" : original_filename }
244+
245+ except Exception as e :
246+ logger .exception (f"[INGESTOR] Failed to ingest file '{ original_filename } ' from path '{ file_path } '." , exc_info = True )
247+
248+ return {"status" : "FAILED" , "filename" : original_filename , "error" : str (e )}
160249
161250@celery .task
162251def ingest_file_task (document_id : str , file_content : str , original_filename : str ):
0 commit comments