Skip to content

Commit 2109193

Browse files
authored
Merge pull request #210 from rootcodelabs/llm-469
vector indexer update
2 parents 089949a + 6ec4f20 commit 2109193

4 files changed

Lines changed: 72 additions & 34 deletions

File tree

src/vector_indexer/contextual_processor.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,16 @@ def __init__(
4141

4242
async def process_document(
4343
self, document: ProcessingDocument
44-
) -> List[ContextualChunk]:
44+
) -> tuple[List[ContextualChunk], int]:
4545
"""
4646
Process single document into contextual chunks.
4747
4848
Args:
4949
document: Document to process
5050
5151
Returns:
52-
List of contextual chunks with embeddings
52+
Tuple of (contextual chunks with embeddings, number of chunks
53+
dropped due to context-generation failure)
5354
"""
5455
logger.info(
5556
f"Processing document {document.document_hash} ({len(document.content)} characters)"
@@ -69,11 +70,13 @@ async def process_document(
6970
# Step 3: Create contextual chunks (filter out failed context generations)
7071
contextual_chunks: List[ContextualChunk] = []
7172
valid_contextual_contents: List[str] = []
73+
failed_chunks = 0
7274

7375
for i, (base_chunk, context) in enumerate(
7476
zip(base_chunks, contexts, strict=True)
7577
):
7678
if isinstance(context, Exception):
79+
failed_chunks += 1
7780
self.error_logger.log_context_generation_failure(
7881
document.document_hash, i, str(context), self.config.max_retries
7982
)
@@ -128,7 +131,7 @@ async def process_document(
128131
logger.error(
129132
f"No valid chunks created for document {document.document_hash}"
130133
)
131-
return []
134+
return [], failed_chunks
132135

133136
# Step 4: Create embeddings for all valid contextual chunks
134137
try:
@@ -154,9 +157,10 @@ async def process_document(
154157
raise
155158

156159
logger.info(
157-
f"Successfully processed document {document.document_hash}: {len(contextual_chunks)} chunks"
160+
f"Successfully processed document {document.document_hash}: "
161+
f"{len(contextual_chunks)} chunks ({failed_chunks} dropped)"
158162
)
159-
return contextual_chunks
163+
return contextual_chunks, failed_chunks
160164

161165
except Exception as e:
162166
logger.error(

src/vector_indexer/error_logger.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,17 @@ def log_processing_stats(self, stats: ProcessingStats) -> None:
158158
stats_dict["end_time"] = stats.end_time.isoformat()
159159
stats_dict["duration"] = stats.duration
160160
stats_dict["success_rate"] = stats.success_rate
161+
stats_dict["chunk_success_rate"] = stats.chunk_success_rate
161162

162163
with open(self.config.stats_log_file, "w", encoding="utf-8") as f:
163164
json.dump(stats_dict, f, indent=2)
164165

165166
logger.info(
166167
f"Processing completed - Success rate: {stats.success_rate:.1%}, "
168+
f"Chunk success rate: {stats.chunk_success_rate:.1%}, "
167169
f"Duration: {stats.duration}, "
168170
f"Processed: {stats.documents_processed}/{stats.total_documents} documents, "
169-
f"Chunks: {stats.total_chunks_processed}"
171+
f"Chunks: {stats.total_chunks_processed} ok / {stats.total_chunks_failed} failed"
170172
)
171173
except Exception as e:
172174
logger.error(f"Failed to write stats log: {e}")

src/vector_indexer/main_indexer.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
sys.path.append(str(Path(__file__).parent.parent))
1616

1717
from vector_indexer.config.config_loader import ConfigLoader
18-
from vector_indexer.document_loader import DocumentLoader
18+
from vector_indexer.document_loader import DocumentLoader, DocumentLoadError
1919
from vector_indexer.contextual_processor import ContextualProcessor
2020
from vector_indexer.qdrant_manager import QdrantManager
2121
from vector_indexer.error_logger import ErrorLogger
@@ -169,7 +169,7 @@ async def process_all_documents(self) -> ProcessingStats:
169169

170170
# Process documents with controlled concurrency
171171
semaphore = asyncio.Semaphore(self.config.max_concurrent_documents)
172-
tasks: List[asyncio.Task[tuple[int, str]]] = []
172+
tasks: List[asyncio.Task[tuple[int, str, int]]] = []
173173

174174
for doc_info in documents:
175175
task = asyncio.create_task(
@@ -189,6 +189,9 @@ async def process_all_documents(self) -> ProcessingStats:
189189
chunks_info: Dict[
190190
str, Dict[str, Any]
191191
] = {} # Track chunk counts for metadata update
192+
# Only documents that processed successfully are marked as
193+
# processed in DVC tracking, so failures are retried next run.
194+
processed_documents: List[DocumentInfo] = []
192195
for i, result in enumerate(results):
193196
if isinstance(result, Exception):
194197
doc_info = documents[i]
@@ -200,16 +203,18 @@ async def process_all_documents(self) -> ProcessingStats:
200203
doc_info.document_hash, str(result)
201204
)
202205
else:
203-
# Result should be tuple of (chunk_count, content_hash)
206+
# Result should be tuple of (chunk_count, content_hash, failed_chunks)
204207
doc_info = documents[i]
205208
self.stats.documents_processed += 1
206-
if isinstance(result, tuple) and len(result) == 2:
207-
chunk_count, content_hash = result
209+
processed_documents.append(doc_info)
210+
if isinstance(result, tuple) and len(result) == 3:
211+
chunk_count, content_hash, failed_chunks = result
208212
self.stats.total_chunks_processed += chunk_count
213+
self.stats.total_chunks_failed += failed_chunks
209214
# Track chunk count using content_hash (not directory hash)
210215
chunks_info[content_hash] = {"chunk_count": chunk_count}
211216
logger.info(
212-
f"CHUNK COUNT: Document {doc_info.document_hash[:12]}... (content: {content_hash[:12]}...) -> {chunk_count} chunks"
217+
f"CHUNK COUNT: Document {doc_info.document_hash[:12]}... (content: {content_hash[:12]}...) -> {chunk_count} chunks ({failed_chunks} failed)"
213218
)
214219

215220
# Log the complete chunks_info dictionary
@@ -227,10 +232,10 @@ async def process_all_documents(self) -> ProcessingStats:
227232
# Step 4: Update processed files tracking (even if no new documents processed)
228233
if diff_detector:
229234
try:
230-
# Update metadata for newly processed files
231-
if documents:
235+
# Update metadata for newly processed files (successful only)
236+
if processed_documents:
232237
processed_paths = [
233-
doc.cleaned_txt_path for doc in documents
238+
doc.cleaned_txt_path for doc in processed_documents
234239
]
235240
if processed_paths:
236241
logger.debug(
@@ -290,7 +295,7 @@ async def _process_single_document(
290295
doc_info: DocumentInfo,
291296
qdrant_manager: QdrantManager,
292297
semaphore: asyncio.Semaphore,
293-
) -> tuple[int, str]:
298+
) -> tuple[int, str, int]:
294299
"""
295300
Process a single document with contextual retrieval.
296301
@@ -300,7 +305,9 @@ async def _process_single_document(
300305
semaphore: Concurrency control semaphore
301306
302307
Returns:
303-
tuple: (chunk_count: int, content_hash: str) or Exception on error
308+
tuple: (chunk_count: int, content_hash: str, failed_chunks: int).
309+
Raises on any failure (including load failure or zero usable chunks),
310+
so the document is counted as failed rather than as success.
304311
"""
305312
async with semaphore:
306313
logger.info(f"Processing document: {doc_info.document_hash}")
@@ -310,29 +317,31 @@ async def _process_single_document(
310317
document = self.document_loader.load_document(doc_info)
311318

312319
if not document:
313-
logger.warning(f"Could not load document: {doc_info.document_hash}")
314-
return (0, doc_info.document_hash)
320+
raise DocumentLoadError(
321+
f"Could not load document: {doc_info.document_hash}"
322+
)
315323

316324
# Process document with contextual retrieval
317-
contextual_chunks = await self.contextual_processor.process_document(
318-
document
319-
)
325+
(
326+
contextual_chunks,
327+
failed_chunks,
328+
) = await self.contextual_processor.process_document(document)
320329

321330
if not contextual_chunks:
322-
logger.warning(
323-
f"No chunks created for document: {doc_info.document_hash}"
331+
raise RuntimeError(
332+
f"No chunks created for document: {doc_info.document_hash} "
333+
f"({failed_chunks} chunks failed context generation)"
324334
)
325-
return (0, document.document_hash)
326335

327336
# Store chunks in Qdrant
328337
await qdrant_manager.store_chunks(contextual_chunks)
329338

330339
logger.info(
331340
f"Successfully processed document {doc_info.document_hash}: "
332-
f"{len(contextual_chunks)} chunks"
341+
f"{len(contextual_chunks)} chunks ({failed_chunks} dropped)"
333342
)
334343

335-
return (len(contextual_chunks), document.document_hash)
344+
return (len(contextual_chunks), document.document_hash, failed_chunks)
336345

337346
except Exception as e:
338347
logger.error(f"Error processing document {doc_info.document_hash}: {e}")
@@ -352,10 +361,12 @@ def _log_final_summary(self) -> None:
352361
logger.info(f" • Failed Chunks: {self.stats.total_chunks_failed}")
353362

354363
if self.stats.total_documents > 0:
355-
success_rate = (
356-
self.stats.documents_processed / self.stats.total_documents
357-
) * 100
358-
logger.info(f"Success Rate: {success_rate:.1f}%")
364+
logger.info(f"Success Rate: {self.stats.success_rate * 100:.1f}%")
365+
366+
if self.stats.total_chunks_processed + self.stats.total_chunks_failed > 0:
367+
logger.info(
368+
f"Chunk Success Rate: {self.stats.chunk_success_rate * 100:.1f}%"
369+
)
359370

360371
logger.info(f"Processing Duration: {self.stats.duration}")
361372

@@ -365,6 +376,11 @@ def _log_final_summary(self) -> None:
365376
)
366377
logger.info("Check failure logs for details")
367378

379+
if self.stats.total_chunks_failed > 0:
380+
logger.warning(
381+
f" {self.stats.total_chunks_failed} chunks failed processing"
382+
)
383+
368384
async def run_health_check(self) -> bool:
369385
"""
370386
Run health check on all components.
@@ -617,12 +633,20 @@ async def _execute_cleanup_operations(
617633
return total_deleted
618634

619635
def _cleanup_datasets(self) -> None:
620-
"""Remove datasets folder after processing."""
636+
"""Remove datasets folder contents after processing.
637+
638+
Only the folder's contents are removed, not the folder itself, since
639+
the datasets path is a mounted volume in the container.
640+
"""
621641
try:
622642
datasets_path = Path(self.config.dataset_base_path)
623643
if datasets_path.exists():
624-
shutil.rmtree(str(datasets_path))
625-
logger.info(f"Datasets folder cleaned up: {datasets_path}")
644+
for child in datasets_path.iterdir():
645+
if child.is_dir():
646+
shutil.rmtree(str(child))
647+
else:
648+
child.unlink()
649+
logger.info(f"Datasets folder contents cleaned up: {datasets_path}")
626650
else:
627651
logger.debug(f"Datasets folder does not exist: {datasets_path}")
628652
except Exception as e:

src/vector_indexer/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ def success_rate(self) -> float:
9696
return self.documents_processed / self.total_documents
9797
return 0.0
9898

99+
@property
100+
def chunk_success_rate(self) -> float:
101+
"""Calculate chunk success rate (processed vs processed + failed)."""
102+
total_chunks = self.total_chunks_processed + self.total_chunks_failed
103+
if total_chunks > 0:
104+
return self.total_chunks_processed / total_chunks
105+
return 0.0
106+
99107

100108
class ProcessingError(BaseModel):
101109
"""Error information for failed processing."""

0 commit comments

Comments
 (0)