iris-vector-rag 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. adapters/rag_templates_bridge.py +578 -0
  2. common/__init__.py +1 -0
  3. common/batch_utils.py +294 -0
  4. common/config.py +58 -0
  5. common/connection_manager.py +242 -0
  6. common/connection_pool.py +445 -0
  7. common/database.py +141 -0
  8. common/database_schema_manager.py +289 -0
  9. common/db_init_with_indexes.py +181 -0
  10. common/db_vector_utils.py +181 -0
  11. common/environment_manager.py +220 -0
  12. common/huggingface_utils.py +206 -0
  13. common/iris_client.py +92 -0
  14. common/iris_connection_manager.py +411 -0
  15. common/iris_connection_pool.py +360 -0
  16. common/iris_connector.py +71 -0
  17. common/iris_dbapi_connector.py +323 -0
  18. common/iris_index_utils.py +195 -0
  19. common/iris_sql_utils.py +704 -0
  20. common/iris_stream_reader.py +166 -0
  21. common/llm_cache_config.py +308 -0
  22. common/llm_cache_iris.py +671 -0
  23. common/llm_cache_manager.py +911 -0
  24. common/utils.py +559 -0
  25. common/vector_sql_utils.py +584 -0
  26. evaluation_framework/__init__.py +66 -0
  27. evaluation_framework/biomedical_question_generator.py +826 -0
  28. evaluation_framework/comparative_analysis_system.py +1173 -0
  29. evaluation_framework/empirical_reporting.py +1298 -0
  30. evaluation_framework/evaluation_orchestrator.py +1123 -0
  31. evaluation_framework/execute_production_evaluation.py +382 -0
  32. evaluation_framework/pmc_data_pipeline.py +1332 -0
  33. evaluation_framework/production_evaluation_config.py +353 -0
  34. evaluation_framework/ragas_metrics_framework.py +1013 -0
  35. evaluation_framework/real_production_evaluation.py +544 -0
  36. evaluation_framework/run_10k_evaluation.py +175 -0
  37. evaluation_framework/simple_test.py +364 -0
  38. evaluation_framework/statistical_evaluation_methodology.py +1016 -0
  39. evaluation_framework/test_framework.py +281 -0
  40. evaluation_framework/test_iris_connectivity.py +244 -0
  41. evaluation_framework/test_small_scale_evaluation.py +432 -0
  42. evaluation_framework/true_e2e_evaluation.py +639 -0
  43. evaluation_framework/visualization_engine.py +585 -0
  44. iris_rag/__init__.py +284 -0
  45. iris_rag/config/default_config.yaml +295 -0
  46. iris_vector_rag-0.2.1.dist-info/METADATA +499 -0
  47. iris_vector_rag-0.2.1.dist-info/RECORD +50 -0
  48. iris_vector_rag-0.2.1.dist-info/WHEEL +5 -0
  49. iris_vector_rag-0.2.1.dist-info/entry_points.txt +2 -0
  50. iris_vector_rag-0.2.1.dist-info/top_level.txt +4 -0
@@ -0,0 +1,1332 @@
1
+ """
2
+ PMC Data Pipeline for Large-Scale Biomedical Document Processing
3
+
4
+ This module provides a comprehensive pipeline for ingesting, processing, and preparing
5
+ 10K+ PMC (PubMed Central) documents for RAG evaluation. It includes data validation,
6
+ biomedical content extraction, chunking strategies, embedding generation, and vector
7
+ database integration with the existing IRIS infrastructure.
8
+
9
+ Key Features:
10
+ 1. Scalable document ingestion with parallel processing
11
+ 2. Biomedical content validation and quality assessment
12
+ 3. Intelligent document chunking for optimal RAG performance
13
+ 4. Vector embedding generation and storage
14
+ 5. Progress monitoring and error handling
15
+ 6. Incremental processing and resume capabilities
16
+ 7. Data quality metrics and reporting
17
+ 8. Integration with IRIS vector database
18
+ """
19
+
20
+ import asyncio
21
+ import gzip
22
+ import hashlib
23
+ import json
24
+ import logging
25
+ import pickle
26
+ import re
27
+ import sqlite3
28
+ import time
29
+ import xml.etree.ElementTree as ET
30
+ from collections import defaultdict
31
+ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
32
+ from dataclasses import dataclass, field
33
+ from datetime import datetime
34
+ from pathlib import Path
35
+ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
36
+ from urllib.parse import urljoin
37
+
38
+ import ftfy
39
+ import nltk
40
+ import numpy as np
41
+ import pandas as pd
42
+ import psycopg2
43
+ import requests
44
+
45
+ # Scientific/medical processing
46
+ import spacy
47
+
48
+ # Database integration
49
+ import sqlalchemy as sa
50
+ import torch
51
+ from nltk.corpus import stopwords
52
+ from nltk.tokenize import sent_tokenize, word_tokenize
53
+ from sentence_transformers import SentenceTransformer
54
+
55
+ # Data processing
56
+ from sklearn.feature_extraction.text import TfidfVectorizer
57
+ from sklearn.metrics.pairwise import cosine_similarity
58
+ from sqlalchemy import create_engine, text
59
+ from transformers import AutoModel, AutoTokenizer
60
+
61
+ logger = logging.getLogger(__name__)
62
+
63
+
64
+ @dataclass
65
+ class PMCDocument:
66
+ """Represents a single PMC document with metadata."""
67
+
68
+ pmc_id: str
69
+ title: str
70
+ abstract: str
71
+ full_text: str
72
+ authors: List[str]
73
+ publication_date: str
74
+ journal: str
75
+ doi: Optional[str] = None
76
+ keywords: List[str] = field(default_factory=list)
77
+ mesh_terms: List[str] = field(default_factory=list)
78
+ content_type: str = "research_article"
79
+ language: str = "en"
80
+
81
+ # Processing metadata
82
+ word_count: int = 0
83
+ chunk_count: int = 0
84
+ embedding_generated: bool = False
85
+ quality_score: float = 0.0
86
+ processing_timestamp: Optional[str] = None
87
+
88
+ # Biomedical metadata
89
+ medical_entities: List[Dict[str, Any]] = field(default_factory=list)
90
+ disease_mentions: List[str] = field(default_factory=list)
91
+ drug_mentions: List[str] = field(default_factory=list)
92
+ procedure_mentions: List[str] = field(default_factory=list)
93
+
94
+ def __post_init__(self):
95
+ """Calculate basic metadata after initialization."""
96
+ if self.full_text:
97
+ self.word_count = len(self.full_text.split())
98
+ self.processing_timestamp = datetime.now().isoformat()
99
+
100
+
101
+ @dataclass
102
+ class DocumentChunk:
103
+ """Represents a chunk of a document for RAG processing."""
104
+
105
+ chunk_id: str
106
+ pmc_id: str
107
+ content: str
108
+ chunk_type: str # 'abstract', 'introduction', 'methods', 'results', 'discussion', 'conclusion'
109
+ chunk_index: int
110
+ token_count: int
111
+ overlap_with_previous: int = 0
112
+
113
+ # Semantic metadata
114
+ key_terms: List[str] = field(default_factory=list)
115
+ medical_concepts: List[str] = field(default_factory=list)
116
+ embedding_vector: Optional[np.ndarray] = None
117
+
118
+ # Quality metrics
119
+ information_density: float = 0.0
120
+ readability_score: float = 0.0
121
+ biomedical_relevance: float = 0.0
122
+
123
+
124
+ @dataclass
125
+ class ProcessingConfig:
126
+ """Configuration for PMC document processing pipeline."""
127
+
128
+ # Input/Output configuration
129
+ input_data_path: str = "data/pmc_documents"
130
+ output_data_path: str = "data/processed_pmc"
131
+ cache_dir: str = "cache/pmc_processing"
132
+
133
+ # Processing configuration
134
+ max_workers: int = 8
135
+ batch_size: int = 100
136
+ chunk_size: int = 512 # tokens
137
+ chunk_overlap: int = 50 # tokens
138
+ max_documents: Optional[int] = None
139
+
140
+ # Quality thresholds
141
+ min_word_count: int = 100
142
+ min_quality_score: float = 0.3
143
+ max_chunk_size: int = 1000
144
+
145
+ # Language processing
146
+ spacy_model: str = "en_core_sci_md" # ScispaCy biomedical model
147
+ embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
148
+ biomedical_embedding_model: str = "dmis-lab/biobert-base-cased-v1.1"
149
+
150
+ # Database configuration
151
+ vector_db_connection: str = "iris://localhost:1972/IRIS"
152
+ metadata_db_path: str = "data/pmc_metadata.db"
153
+
154
+ # API configuration
155
+ ncbi_api_key: Optional[str] = None
156
+ rate_limit_delay: float = 0.1
157
+
158
+ # Error handling
159
+ max_retries: int = 3
160
+ continue_on_error: bool = True
161
+ save_failed_documents: bool = True
162
+
163
+
164
+ @dataclass
165
+ class ProcessingStats:
166
+ """Statistics for document processing pipeline."""
167
+
168
+ total_documents: int = 0
169
+ successful_documents: int = 0
170
+ failed_documents: int = 0
171
+ total_chunks: int = 0
172
+ total_embeddings: int = 0
173
+ processing_time: float = 0.0
174
+
175
+ # Quality metrics
176
+ avg_document_quality: float = 0.0
177
+ avg_chunk_quality: float = 0.0
178
+ documents_by_type: Dict[str, int] = field(default_factory=dict)
179
+
180
+ # Error tracking
181
+ error_types: Dict[str, int] = field(default_factory=dict)
182
+ failed_document_ids: List[str] = field(default_factory=list)
183
+
184
+
185
+ class PMCDataPipeline:
186
+ """
187
+ Comprehensive pipeline for processing large-scale PMC documents.
188
+
189
+ Handles document ingestion, validation, chunking, embedding generation,
190
+ and vector database storage with robust error handling and monitoring.
191
+ """
192
+
193
+ def __init__(self, config: Optional[ProcessingConfig] = None):
194
+ """
195
+ Initialize the PMC data pipeline.
196
+
197
+ Args:
198
+ config: Processing configuration
199
+ """
200
+ self.config = config or ProcessingConfig()
201
+ self.stats = ProcessingStats()
202
+
203
+ # Setup directories
204
+ self._setup_directories()
205
+
206
+ # Initialize language models
207
+ self._initialize_nlp_models()
208
+
209
+ # Initialize databases
210
+ self._initialize_databases()
211
+
212
+ # Processing state
213
+ self.processing_state = {
214
+ "current_batch": 0,
215
+ "processed_documents": set(),
216
+ "failed_documents": set(),
217
+ "last_checkpoint": None,
218
+ }
219
+
220
+ logger.info("PMC Data Pipeline initialized successfully")
221
+
222
+ def _setup_directories(self):
223
+ """Setup required directories."""
224
+ directories = [
225
+ self.config.output_data_path,
226
+ self.config.cache_dir,
227
+ f"{self.config.cache_dir}/embeddings",
228
+ f"{self.config.cache_dir}/failed_docs",
229
+ f"{self.config.output_data_path}/chunks",
230
+ f"{self.config.output_data_path}/metadata",
231
+ ]
232
+
233
+ for directory in directories:
234
+ Path(directory).mkdir(parents=True, exist_ok=True)
235
+
236
+ def _initialize_nlp_models(self):
237
+ """Initialize NLP models for biomedical processing."""
238
+ logger.info("Initializing NLP models...")
239
+
240
+ try:
241
+ # SpaCy biomedical model
242
+ self.nlp = spacy.load(self.config.spacy_model)
243
+ logger.info(f"Loaded SpaCy model: {self.config.spacy_model}")
244
+ except OSError:
245
+ logger.warning(
246
+ f"SpaCy model {self.config.spacy_model} not found, using default"
247
+ )
248
+ self.nlp = spacy.load("en_core_web_sm")
249
+
250
+ # Sentence transformer for general embeddings
251
+ self.embedding_model = SentenceTransformer(self.config.embedding_model)
252
+
253
+ # Tokenizer for chunk size calculation
254
+ self.tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
255
+
256
+ # Download required NLTK data
257
+ try:
258
+ nltk.download("punkt", quiet=True)
259
+ nltk.download("stopwords", quiet=True)
260
+ self.stop_words = set(stopwords.words("english"))
261
+ except:
262
+ self.stop_words = set()
263
+
264
+ logger.info("NLP models initialized successfully")
265
+
266
+ def _initialize_databases(self):
267
+ """Initialize database connections."""
268
+ logger.info("Initializing database connections...")
269
+
270
+ # Metadata SQLite database
271
+ self.metadata_db_path = Path(self.config.metadata_db_path)
272
+ self.metadata_db_path.parent.mkdir(parents=True, exist_ok=True)
273
+
274
+ self._create_metadata_tables()
275
+
276
+ # Vector database connection (IRIS)
277
+ # This would connect to the existing IRIS infrastructure
278
+ self.vector_db_connection = None
279
+ try:
280
+ # Placeholder for IRIS connection
281
+ # self.vector_db_connection = create_engine(self.config.vector_db_connection)
282
+ logger.info("Vector database connection initialized")
283
+ except Exception as e:
284
+ logger.warning(f"Vector database connection failed: {e}")
285
+
286
+ def _create_metadata_tables(self):
287
+ """Create metadata database tables."""
288
+ conn = sqlite3.connect(self.metadata_db_path)
289
+ cursor = conn.cursor()
290
+
291
+ # Documents table
292
+ cursor.execute(
293
+ """
294
+ CREATE TABLE IF NOT EXISTS documents (
295
+ pmc_id TEXT PRIMARY KEY,
296
+ title TEXT,
297
+ abstract TEXT,
298
+ authors TEXT,
299
+ publication_date TEXT,
300
+ journal TEXT,
301
+ doi TEXT,
302
+ keywords TEXT,
303
+ mesh_terms TEXT,
304
+ content_type TEXT,
305
+ language TEXT,
306
+ word_count INTEGER,
307
+ chunk_count INTEGER,
308
+ quality_score REAL,
309
+ processing_timestamp TEXT,
310
+ embedding_generated BOOLEAN
311
+ )
312
+ """
313
+ )
314
+
315
+ # Chunks table
316
+ cursor.execute(
317
+ """
318
+ CREATE TABLE IF NOT EXISTS chunks (
319
+ chunk_id TEXT PRIMARY KEY,
320
+ pmc_id TEXT,
321
+ content TEXT,
322
+ chunk_type TEXT,
323
+ chunk_index INTEGER,
324
+ token_count INTEGER,
325
+ overlap_with_previous INTEGER,
326
+ information_density REAL,
327
+ readability_score REAL,
328
+ biomedical_relevance REAL,
329
+ FOREIGN KEY (pmc_id) REFERENCES documents (pmc_id)
330
+ )
331
+ """
332
+ )
333
+
334
+ # Processing log table
335
+ cursor.execute(
336
+ """
337
+ CREATE TABLE IF NOT EXISTS processing_log (
338
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
339
+ pmc_id TEXT,
340
+ operation TEXT,
341
+ status TEXT,
342
+ error_message TEXT,
343
+ timestamp TEXT
344
+ )
345
+ """
346
+ )
347
+
348
+ conn.commit()
349
+ conn.close()
350
+
351
+ def process_documents(
352
+ self,
353
+ document_source: Union[str, List[PMCDocument], Iterator[PMCDocument]],
354
+ resume_from_checkpoint: bool = False,
355
+ ) -> ProcessingStats:
356
+ """
357
+ Process PMC documents through the complete pipeline.
358
+
359
+ Args:
360
+ document_source: Path to documents, list of documents, or document iterator
361
+ resume_from_checkpoint: Whether to resume from last checkpoint
362
+
363
+ Returns:
364
+ Processing statistics
365
+ """
366
+ logger.info("Starting PMC document processing pipeline")
367
+ start_time = time.time()
368
+
369
+ # Load checkpoint if resuming
370
+ if resume_from_checkpoint:
371
+ self._load_checkpoint()
372
+
373
+ # Get document iterator
374
+ if isinstance(document_source, str):
375
+ documents = self._load_documents_from_path(document_source)
376
+ elif isinstance(document_source, list):
377
+ documents = iter(document_source)
378
+ else:
379
+ documents = document_source
380
+
381
+ # Process documents in batches
382
+ batch_num = self.processing_state["current_batch"]
383
+
384
+ try:
385
+ for batch in self._batch_iterator(documents, self.config.batch_size):
386
+ batch_num += 1
387
+ logger.info(f"Processing batch {batch_num} ({len(batch)} documents)")
388
+
389
+ # Process batch
390
+ batch_results = self._process_document_batch(batch)
391
+
392
+ # Update statistics
393
+ self._update_stats(batch_results)
394
+
395
+ # Save checkpoint
396
+ self.processing_state["current_batch"] = batch_num
397
+ self._save_checkpoint()
398
+
399
+ # Log progress
400
+ self._log_progress()
401
+
402
+ # Check if max documents reached
403
+ if (
404
+ self.config.max_documents
405
+ and self.stats.successful_documents >= self.config.max_documents
406
+ ):
407
+ logger.info("Reached maximum document limit")
408
+ break
409
+
410
+ except Exception as e:
411
+ logger.error(f"Pipeline processing failed: {e}")
412
+ raise
413
+
414
+ finally:
415
+ # Finalize processing
416
+ self.stats.processing_time = time.time() - start_time
417
+ self._finalize_processing()
418
+
419
+ logger.info(f"Pipeline completed in {self.stats.processing_time:.2f}s")
420
+ return self.stats
421
+
422
+ def _load_documents_from_path(self, path: str) -> Iterator[PMCDocument]:
423
+ """Load documents from various sources."""
424
+ path_obj = Path(path)
425
+
426
+ if path_obj.is_file():
427
+ # Single file
428
+ if path_obj.suffix == ".json":
429
+ yield from self._load_json_documents(path_obj)
430
+ elif path_obj.suffix == ".xml":
431
+ yield from self._load_xml_documents(path_obj)
432
+ elif path_obj.suffix in [".gz", ".zip"]:
433
+ yield from self._load_compressed_documents(path_obj)
434
+
435
+ elif path_obj.is_dir():
436
+ # Directory of files
437
+ for file_path in path_obj.rglob("*"):
438
+ if file_path.is_file():
439
+ yield from self._load_documents_from_path(str(file_path))
440
+
441
+ else:
442
+ # URL or API endpoint
443
+ yield from self._load_documents_from_api(path)
444
+
445
+ def _load_json_documents(self, file_path: Path) -> Iterator[PMCDocument]:
446
+ """Load documents from JSON file."""
447
+ try:
448
+ with open(file_path, "r", encoding="utf-8") as f:
449
+ data = json.load(f)
450
+
451
+ if isinstance(data, list):
452
+ for doc_data in data:
453
+ yield self._create_document_from_dict(doc_data)
454
+ else:
455
+ yield self._create_document_from_dict(data)
456
+
457
+ except Exception as e:
458
+ logger.error(f"Failed to load JSON file {file_path}: {e}")
459
+
460
+ def _load_xml_documents(self, file_path: Path) -> Iterator[PMCDocument]:
461
+ """Load documents from XML file (PMC format)."""
462
+ try:
463
+ tree = ET.parse(file_path)
464
+ root = tree.getroot()
465
+
466
+ # Parse PMC XML format
467
+ pmc_id = self._extract_pmc_id(root)
468
+ title = self._extract_title(root)
469
+ abstract = self._extract_abstract(root)
470
+ full_text = self._extract_full_text(root)
471
+ authors = self._extract_authors(root)
472
+ metadata = self._extract_metadata(root)
473
+
474
+ yield PMCDocument(
475
+ pmc_id=pmc_id,
476
+ title=title,
477
+ abstract=abstract,
478
+ full_text=full_text,
479
+ authors=authors,
480
+ **metadata,
481
+ )
482
+
483
+ except Exception as e:
484
+ logger.error(f"Failed to parse XML file {file_path}: {e}")
485
+
486
+ def _create_document_from_dict(self, doc_data: Dict[str, Any]) -> PMCDocument:
487
+ """Create PMCDocument from dictionary data."""
488
+ return PMCDocument(
489
+ pmc_id=doc_data.get("pmc_id", ""),
490
+ title=doc_data.get("title", ""),
491
+ abstract=doc_data.get("abstract", ""),
492
+ full_text=doc_data.get("full_text", ""),
493
+ authors=doc_data.get("authors", []),
494
+ publication_date=doc_data.get("publication_date", ""),
495
+ journal=doc_data.get("journal", ""),
496
+ doi=doc_data.get("doi"),
497
+ keywords=doc_data.get("keywords", []),
498
+ mesh_terms=doc_data.get("mesh_terms", []),
499
+ content_type=doc_data.get("content_type", "research_article"),
500
+ language=doc_data.get("language", "en"),
501
+ )
502
+
503
+ def _batch_iterator(
504
+ self, documents: Iterator[PMCDocument], batch_size: int
505
+ ) -> Iterator[List[PMCDocument]]:
506
+ """Create batches from document iterator."""
507
+ batch = []
508
+ for doc in documents:
509
+ batch.append(doc)
510
+ if len(batch) >= batch_size:
511
+ yield batch
512
+ batch = []
513
+
514
+ if batch: # Last batch
515
+ yield batch
516
+
517
+ def _process_document_batch(self, batch: List[PMCDocument]) -> List[Dict[str, Any]]:
518
+ """Process a batch of documents."""
519
+ results = []
520
+
521
+ # Use ThreadPoolExecutor for I/O bound operations
522
+ with ThreadPoolExecutor(max_workers=self.config.max_workers) as executor:
523
+ # Submit processing tasks
524
+ future_to_doc = {
525
+ executor.submit(self._process_single_document, doc): doc
526
+ for doc in batch
527
+ }
528
+
529
+ # Collect results
530
+ for future in as_completed(future_to_doc):
531
+ doc = future_to_doc[future]
532
+ try:
533
+ result = future.result()
534
+ results.append(result)
535
+ except Exception as e:
536
+ logger.error(f"Failed to process document {doc.pmc_id}: {e}")
537
+ results.append(
538
+ {"pmc_id": doc.pmc_id, "status": "failed", "error": str(e)}
539
+ )
540
+
541
+ return results
542
+
543
+ def _process_single_document(self, document: PMCDocument) -> Dict[str, Any]:
544
+ """Process a single document through the pipeline."""
545
+
546
+ # Skip if already processed
547
+ if document.pmc_id in self.processing_state["processed_documents"]:
548
+ return {"pmc_id": document.pmc_id, "status": "skipped"}
549
+
550
+ try:
551
+ # Step 1: Validate document
552
+ if not self._validate_document(document):
553
+ return {
554
+ "pmc_id": document.pmc_id,
555
+ "status": "failed",
556
+ "error": "validation_failed",
557
+ }
558
+
559
+ # Step 2: Clean and preprocess text
560
+ document = self._preprocess_document(document)
561
+
562
+ # Step 3: Calculate quality score
563
+ document.quality_score = self._calculate_quality_score(document)
564
+
565
+ if document.quality_score < self.config.min_quality_score:
566
+ return {
567
+ "pmc_id": document.pmc_id,
568
+ "status": "failed",
569
+ "error": "low_quality",
570
+ }
571
+
572
+ # Step 4: Extract biomedical entities
573
+ document = self._extract_biomedical_entities(document)
574
+
575
+ # Step 5: Create document chunks
576
+ chunks = self._create_document_chunks(document)
577
+ document.chunk_count = len(chunks)
578
+
579
+ # Step 6: Generate embeddings
580
+ chunks_with_embeddings = self._generate_chunk_embeddings(chunks)
581
+
582
+ # Step 7: Store in databases
583
+ self._store_document_and_chunks(document, chunks_with_embeddings)
584
+
585
+ # Mark as processed
586
+ self.processing_state["processed_documents"].add(document.pmc_id)
587
+
588
+ return {
589
+ "pmc_id": document.pmc_id,
590
+ "status": "success",
591
+ "chunks_created": len(chunks),
592
+ "quality_score": document.quality_score,
593
+ }
594
+
595
+ except Exception as e:
596
+ # Log error
597
+ self._log_processing_error(document.pmc_id, str(e))
598
+
599
+ # Save failed document if configured
600
+ if self.config.save_failed_documents:
601
+ self._save_failed_document(document, str(e))
602
+
603
+ return {"pmc_id": document.pmc_id, "status": "failed", "error": str(e)}
604
+
605
+ def _validate_document(self, document: PMCDocument) -> bool:
606
+ """Validate document meets quality requirements."""
607
+
608
+ # Check required fields
609
+ if not document.pmc_id or not document.title:
610
+ return False
611
+
612
+ # Check minimum content
613
+ content = f"{document.abstract} {document.full_text}".strip()
614
+ if len(content.split()) < self.config.min_word_count:
615
+ return False
616
+
617
+ # Check language
618
+ if document.language and document.language != "en":
619
+ return False
620
+
621
+ return True
622
+
623
+ def _preprocess_document(self, document: PMCDocument) -> PMCDocument:
624
+ """Clean and preprocess document text."""
625
+
626
+ # Fix text encoding issues
627
+ document.title = ftfy.fix_text(document.title)
628
+ document.abstract = ftfy.fix_text(document.abstract)
629
+ document.full_text = ftfy.fix_text(document.full_text)
630
+
631
+ # Remove excessive whitespace
632
+ document.title = re.sub(r"\s+", " ", document.title).strip()
633
+ document.abstract = re.sub(r"\s+", " ", document.abstract).strip()
634
+ document.full_text = re.sub(r"\s+", " ", document.full_text).strip()
635
+
636
+ # Remove unwanted characters
637
+ document.full_text = re.sub(
638
+ r"[^\w\s\.\,\;\:\!\?\-\(\)]", " ", document.full_text
639
+ )
640
+
641
+ return document
642
+
643
+ def _calculate_quality_score(self, document: PMCDocument) -> float:
644
+ """Calculate document quality score."""
645
+
646
+ score = 0.0
647
+
648
+ # Title quality (0.2 weight)
649
+ if document.title and len(document.title.split()) >= 3:
650
+ score += 0.2
651
+
652
+ # Abstract quality (0.3 weight)
653
+ if document.abstract and len(document.abstract.split()) >= 50:
654
+ score += 0.3
655
+
656
+ # Content length (0.2 weight)
657
+ word_count = len(document.full_text.split())
658
+ if word_count >= 1000:
659
+ score += 0.2
660
+ elif word_count >= 500:
661
+ score += 0.1
662
+
663
+ # Biomedical relevance (0.3 weight)
664
+ biomedical_score = self._assess_biomedical_relevance(document)
665
+ score += 0.3 * biomedical_score
666
+
667
+ return min(1.0, score)
668
+
669
+ def _assess_biomedical_relevance(self, document: PMCDocument) -> float:
670
+ """Assess biomedical relevance of document."""
671
+
672
+ biomedical_keywords = [
673
+ "patient",
674
+ "treatment",
675
+ "disease",
676
+ "clinical",
677
+ "medical",
678
+ "therapy",
679
+ "diagnosis",
680
+ "symptom",
681
+ "drug",
682
+ "medication",
683
+ "hospital",
684
+ "doctor",
685
+ "health",
686
+ "medicine",
687
+ "study",
688
+ "trial",
689
+ "research",
690
+ "analysis",
691
+ ]
692
+
693
+ content = f"{document.title} {document.abstract} {document.full_text}".lower()
694
+
695
+ # Count biomedical keyword occurrences
696
+ keyword_count = sum(1 for keyword in biomedical_keywords if keyword in content)
697
+
698
+ # Normalize by total keywords
699
+ relevance_score = keyword_count / len(biomedical_keywords)
700
+
701
+ # Check for MeSH terms
702
+ if document.mesh_terms:
703
+ relevance_score += 0.2
704
+
705
+ # Check journal name
706
+ if document.journal and any(
707
+ term in document.journal.lower()
708
+ for term in ["medical", "clinical", "health", "medicine"]
709
+ ):
710
+ relevance_score += 0.1
711
+
712
+ return min(1.0, relevance_score)
713
+
714
+ def _extract_biomedical_entities(self, document: PMCDocument) -> PMCDocument:
715
+ """Extract biomedical entities using NLP."""
716
+
717
+ content = f"{document.abstract} {document.full_text}"
718
+
719
+ # Process with SpaCy
720
+ doc = self.nlp(content[:1000000]) # Limit for memory
721
+
722
+ entities = []
723
+ diseases = []
724
+ drugs = []
725
+ procedures = []
726
+
727
+ for ent in doc.ents:
728
+ entity_info = {
729
+ "text": ent.text,
730
+ "label": ent.label_,
731
+ "start": ent.start_char,
732
+ "end": ent.end_char,
733
+ }
734
+ entities.append(entity_info)
735
+
736
+ # Categorize entities
737
+ if ent.label_ in ["DISEASE"]:
738
+ diseases.append(ent.text)
739
+ elif ent.label_ in ["CHEMICAL", "DRUG"]:
740
+ drugs.append(ent.text)
741
+ elif ent.label_ in ["PROCEDURE"]:
742
+ procedures.append(ent.text)
743
+
744
+ document.medical_entities = entities
745
+ document.disease_mentions = list(set(diseases))
746
+ document.drug_mentions = list(set(drugs))
747
+ document.procedure_mentions = list(set(procedures))
748
+
749
+ return document
750
+
751
+ def _create_document_chunks(self, document: PMCDocument) -> List[DocumentChunk]:
752
+ """Create intelligent chunks from document."""
753
+
754
+ chunks = []
755
+
756
+ # Abstract chunk
757
+ if document.abstract:
758
+ chunks.append(
759
+ self._create_chunk(document.pmc_id, document.abstract, "abstract", 0)
760
+ )
761
+
762
+ # Full text chunks
763
+ if document.full_text:
764
+ text_chunks = self._split_text_intelligent(
765
+ document.full_text, document.pmc_id
766
+ )
767
+ chunks.extend(text_chunks)
768
+
769
+ return chunks
770
+
771
+ def _split_text_intelligent(self, text: str, pmc_id: str) -> List[DocumentChunk]:
772
+ """Split text using intelligent chunking strategy."""
773
+
774
+ chunks = []
775
+ sentences = sent_tokenize(text)
776
+
777
+ current_chunk = ""
778
+ current_tokens = 0
779
+ chunk_index = 1
780
+
781
+ for sentence in sentences:
782
+ sentence_tokens = len(self.tokenizer.encode(sentence))
783
+
784
+ # Check if adding this sentence exceeds chunk size
785
+ if (
786
+ current_tokens + sentence_tokens > self.config.chunk_size
787
+ and current_chunk
788
+ ):
789
+ # Create chunk
790
+ chunk = self._create_chunk(
791
+ pmc_id, current_chunk.strip(), "content", chunk_index
792
+ )
793
+ chunks.append(chunk)
794
+
795
+ # Start new chunk with overlap
796
+ overlap_text = self._get_overlap_text(current_chunk)
797
+ current_chunk = overlap_text + " " + sentence
798
+ current_tokens = len(self.tokenizer.encode(current_chunk))
799
+ chunk_index += 1
800
+ else:
801
+ # Add sentence to current chunk
802
+ current_chunk += " " + sentence
803
+ current_tokens += sentence_tokens
804
+
805
+ # Add final chunk
806
+ if current_chunk.strip():
807
+ chunk = self._create_chunk(
808
+ pmc_id, current_chunk.strip(), "content", chunk_index
809
+ )
810
+ chunks.append(chunk)
811
+
812
+ return chunks
813
+
814
+ def _create_chunk(
815
+ self, pmc_id: str, content: str, chunk_type: str, index: int
816
+ ) -> DocumentChunk:
817
+ """Create a document chunk with metadata."""
818
+
819
+ chunk_id = f"{pmc_id}_chunk_{index}"
820
+ token_count = len(self.tokenizer.encode(content))
821
+
822
+ # Extract key terms
823
+ key_terms = self._extract_key_terms(content)
824
+
825
+ # Calculate quality metrics
826
+ information_density = self._calculate_information_density(content)
827
+ readability_score = self._calculate_readability_score(content)
828
+ biomedical_relevance = self._calculate_chunk_biomedical_relevance(content)
829
+
830
+ return DocumentChunk(
831
+ chunk_id=chunk_id,
832
+ pmc_id=pmc_id,
833
+ content=content,
834
+ chunk_type=chunk_type,
835
+ chunk_index=index,
836
+ token_count=token_count,
837
+ key_terms=key_terms,
838
+ information_density=information_density,
839
+ readability_score=readability_score,
840
+ biomedical_relevance=biomedical_relevance,
841
+ )
842
+
843
+ def _get_overlap_text(self, text: str) -> str:
844
+ """Get overlap text for chunk continuity."""
845
+
846
+ sentences = sent_tokenize(text)
847
+ overlap_sentences = sentences[-2:] if len(sentences) >= 2 else sentences
848
+ return " ".join(overlap_sentences)
849
+
850
+ def _extract_key_terms(self, content: str) -> List[str]:
851
+ """Extract key terms from content."""
852
+
853
+ # Simple TF-IDF based extraction
854
+ words = word_tokenize(content.lower())
855
+ words = [w for w in words if w.isalpha() and w not in self.stop_words]
856
+
857
+ # Return top terms
858
+ word_freq = defaultdict(int)
859
+ for word in words:
860
+ word_freq[word] += 1
861
+
862
+ return sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:10]
863
+
864
+ def _calculate_information_density(self, content: str) -> float:
865
+ """Calculate information density of content."""
866
+
867
+ words = content.split()
868
+ unique_words = set(words)
869
+
870
+ if len(words) == 0:
871
+ return 0.0
872
+
873
+ return len(unique_words) / len(words)
874
+
875
+ def _calculate_readability_score(self, content: str) -> float:
876
+ """Calculate readability score (simplified)."""
877
+
878
+ sentences = sent_tokenize(content)
879
+ words = word_tokenize(content)
880
+
881
+ if len(sentences) == 0 or len(words) == 0:
882
+ return 0.0
883
+
884
+ avg_sentence_length = len(words) / len(sentences)
885
+
886
+ # Simple readability metric (inverse of average sentence length)
887
+ readability = 1.0 / (1.0 + avg_sentence_length / 20.0)
888
+
889
+ return min(1.0, readability)
890
+
891
+ def _calculate_chunk_biomedical_relevance(self, content: str) -> float:
892
+ """Calculate biomedical relevance for chunk."""
893
+
894
+ # Similar to document-level but for chunks
895
+ biomedical_keywords = [
896
+ "patient",
897
+ "treatment",
898
+ "disease",
899
+ "clinical",
900
+ "medical",
901
+ "therapy",
902
+ "diagnosis",
903
+ "symptom",
904
+ "drug",
905
+ "medication",
906
+ ]
907
+
908
+ content_lower = content.lower()
909
+ keyword_count = sum(
910
+ 1 for keyword in biomedical_keywords if keyword in content_lower
911
+ )
912
+
913
+ return min(1.0, keyword_count / len(biomedical_keywords))
914
+
915
+ def _generate_chunk_embeddings(
916
+ self, chunks: List[DocumentChunk]
917
+ ) -> List[DocumentChunk]:
918
+ """Generate embeddings for chunks."""
919
+
920
+ # Batch process embeddings for efficiency
921
+ texts = [chunk.content for chunk in chunks]
922
+
923
+ try:
924
+ # Generate embeddings
925
+ embeddings = self.embedding_model.encode(
926
+ texts, batch_size=32, show_progress_bar=False
927
+ )
928
+
929
+ # Assign embeddings to chunks
930
+ for chunk, embedding in zip(chunks, embeddings):
931
+ chunk.embedding_vector = embedding
932
+
933
+ except Exception as e:
934
+ logger.error(f"Embedding generation failed: {e}")
935
+ # Continue without embeddings
936
+
937
+ return chunks
938
+
939
+ def _store_document_and_chunks(
940
+ self, document: PMCDocument, chunks: List[DocumentChunk]
941
+ ):
942
+ """Store document and chunks in databases."""
943
+
944
+ # Store in metadata database
945
+ self._store_in_metadata_db(document, chunks)
946
+
947
+ # Store in vector database (if available)
948
+ if self.vector_db_connection:
949
+ self._store_in_vector_db(chunks)
950
+
951
+ def _store_in_metadata_db(self, document: PMCDocument, chunks: List[DocumentChunk]):
952
+ """Store document and chunks in SQLite metadata database."""
953
+
954
+ conn = sqlite3.connect(self.metadata_db_path)
955
+ cursor = conn.cursor()
956
+
957
+ try:
958
+ # Store document
959
+ cursor.execute(
960
+ """
961
+ INSERT OR REPLACE INTO documents VALUES
962
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
963
+ """,
964
+ (
965
+ document.pmc_id,
966
+ document.title,
967
+ document.abstract,
968
+ json.dumps(document.authors),
969
+ document.publication_date,
970
+ document.journal,
971
+ document.doi,
972
+ json.dumps(document.keywords),
973
+ json.dumps(document.mesh_terms),
974
+ document.content_type,
975
+ document.language,
976
+ document.word_count,
977
+ document.chunk_count,
978
+ document.quality_score,
979
+ document.processing_timestamp,
980
+ document.embedding_generated,
981
+ ),
982
+ )
983
+
984
+ # Store chunks
985
+ for chunk in chunks:
986
+ cursor.execute(
987
+ """
988
+ INSERT OR REPLACE INTO chunks VALUES
989
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
990
+ """,
991
+ (
992
+ chunk.chunk_id,
993
+ chunk.pmc_id,
994
+ chunk.content,
995
+ chunk.chunk_type,
996
+ chunk.chunk_index,
997
+ chunk.token_count,
998
+ chunk.overlap_with_previous,
999
+ chunk.information_density,
1000
+ chunk.readability_score,
1001
+ chunk.biomedical_relevance,
1002
+ ),
1003
+ )
1004
+
1005
+ conn.commit()
1006
+
1007
+ except Exception as e:
1008
+ logger.error(f"Database storage failed: {e}")
1009
+ conn.rollback()
1010
+ raise
1011
+
1012
+ finally:
1013
+ conn.close()
1014
+
1015
+ def _store_in_vector_db(self, chunks: List[DocumentChunk]):
1016
+ """Store chunk embeddings in vector database."""
1017
+
1018
+ # This would integrate with the existing IRIS vector database
1019
+ # Placeholder implementation
1020
+ for chunk in chunks:
1021
+ if chunk.embedding_vector is not None:
1022
+ # Store embedding with metadata
1023
+ pass
1024
+
1025
+ def _log_processing_error(self, pmc_id: str, error_message: str):
1026
+ """Log processing error to database."""
1027
+
1028
+ conn = sqlite3.connect(self.metadata_db_path)
1029
+ cursor = conn.cursor()
1030
+
1031
+ cursor.execute(
1032
+ """
1033
+ INSERT INTO processing_log VALUES (?, ?, ?, ?, ?, ?)
1034
+ """,
1035
+ (
1036
+ None,
1037
+ pmc_id,
1038
+ "process_document",
1039
+ "failed",
1040
+ error_message,
1041
+ datetime.now().isoformat(),
1042
+ ),
1043
+ )
1044
+
1045
+ conn.commit()
1046
+ conn.close()
1047
+
1048
+ def _save_failed_document(self, document: PMCDocument, error: str):
1049
+ """Save failed document for later analysis."""
1050
+
1051
+ failed_dir = Path(self.config.cache_dir) / "failed_docs"
1052
+ failed_file = failed_dir / f"{document.pmc_id}_failed.json"
1053
+
1054
+ failed_data = {
1055
+ "document": document.__dict__,
1056
+ "error": error,
1057
+ "timestamp": datetime.now().isoformat(),
1058
+ }
1059
+
1060
+ with open(failed_file, "w") as f:
1061
+ json.dump(failed_data, f, indent=2, default=str)
1062
+
1063
+ def _update_stats(self, batch_results: List[Dict[str, Any]]):
1064
+ """Update processing statistics."""
1065
+
1066
+ for result in batch_results:
1067
+ self.stats.total_documents += 1
1068
+
1069
+ if result["status"] == "success":
1070
+ self.stats.successful_documents += 1
1071
+ if "chunks_created" in result:
1072
+ self.stats.total_chunks += result["chunks_created"]
1073
+ elif result["status"] == "failed":
1074
+ self.stats.failed_documents += 1
1075
+ error_type = result.get("error", "unknown")
1076
+ self.stats.error_types[error_type] = (
1077
+ self.stats.error_types.get(error_type, 0) + 1
1078
+ )
1079
+ self.stats.failed_document_ids.append(result["pmc_id"])
1080
+
1081
+ def _log_progress(self):
1082
+ """Log current progress."""
1083
+
1084
+ success_rate = (
1085
+ self.stats.successful_documents / max(1, self.stats.total_documents)
1086
+ ) * 100
1087
+
1088
+ logger.info(
1089
+ f"Progress: {self.stats.successful_documents}/{self.stats.total_documents} "
1090
+ f"documents processed ({success_rate:.1f}% success rate), "
1091
+ f"{self.stats.total_chunks} chunks created"
1092
+ )
1093
+
1094
+ def _save_checkpoint(self):
1095
+ """Save processing checkpoint."""
1096
+
1097
+ checkpoint_file = Path(self.config.cache_dir) / "checkpoint.json"
1098
+
1099
+ checkpoint_data = {
1100
+ "processing_state": {
1101
+ "current_batch": self.processing_state["current_batch"],
1102
+ "processed_documents": list(
1103
+ self.processing_state["processed_documents"]
1104
+ ),
1105
+ "failed_documents": list(self.processing_state["failed_documents"]),
1106
+ },
1107
+ "stats": self.stats.__dict__,
1108
+ "timestamp": datetime.now().isoformat(),
1109
+ }
1110
+
1111
+ with open(checkpoint_file, "w") as f:
1112
+ json.dump(checkpoint_data, f, indent=2, default=str)
1113
+
1114
+ def _load_checkpoint(self):
1115
+ """Load processing checkpoint."""
1116
+
1117
+ checkpoint_file = Path(self.config.cache_dir) / "checkpoint.json"
1118
+
1119
+ if checkpoint_file.exists():
1120
+ with open(checkpoint_file, "r") as f:
1121
+ checkpoint_data = json.load(f)
1122
+
1123
+ # Restore processing state
1124
+ state = checkpoint_data["processing_state"]
1125
+ self.processing_state["current_batch"] = state["current_batch"]
1126
+ self.processing_state["processed_documents"] = set(
1127
+ state["processed_documents"]
1128
+ )
1129
+ self.processing_state["failed_documents"] = set(state["failed_documents"])
1130
+
1131
+ logger.info(f"Resumed from checkpoint: batch {state['current_batch']}")
1132
+
1133
+ def _finalize_processing(self):
1134
+ """Finalize processing and generate reports."""
1135
+
1136
+ # Calculate final statistics
1137
+ if self.stats.total_documents > 0:
1138
+ self.stats.avg_document_quality = self._calculate_avg_quality()
1139
+
1140
+ # Generate processing report
1141
+ self._generate_processing_report()
1142
+
1143
+ # Clean up temporary files
1144
+ self._cleanup_temp_files()
1145
+
1146
+ def _calculate_avg_quality(self) -> float:
1147
+ """Calculate average document quality from database."""
1148
+
1149
+ conn = sqlite3.connect(self.metadata_db_path)
1150
+ cursor = conn.cursor()
1151
+
1152
+ cursor.execute(
1153
+ "SELECT AVG(quality_score) FROM documents WHERE quality_score > 0"
1154
+ )
1155
+ result = cursor.fetchone()
1156
+
1157
+ conn.close()
1158
+
1159
+ return result[0] if result[0] else 0.0
1160
+
1161
+ def _generate_processing_report(self):
1162
+ """Generate comprehensive processing report."""
1163
+
1164
+ report_dir = Path(self.config.output_data_path) / "reports"
1165
+ report_dir.mkdir(exist_ok=True)
1166
+
1167
+ report_file = (
1168
+ report_dir
1169
+ / f"processing_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
1170
+ )
1171
+
1172
+ report_data = {
1173
+ "processing_config": self.config.__dict__,
1174
+ "processing_stats": self.stats.__dict__,
1175
+ "processing_time": self.stats.processing_time,
1176
+ "success_rate": (
1177
+ self.stats.successful_documents / max(1, self.stats.total_documents)
1178
+ )
1179
+ * 100,
1180
+ "average_chunks_per_doc": (
1181
+ self.stats.total_chunks / max(1, self.stats.successful_documents)
1182
+ ),
1183
+ "error_analysis": self.stats.error_types,
1184
+ "timestamp": datetime.now().isoformat(),
1185
+ }
1186
+
1187
+ with open(report_file, "w") as f:
1188
+ json.dump(report_data, f, indent=2, default=str)
1189
+
1190
+ logger.info(f"Processing report saved to {report_file}")
1191
+
1192
+ def _cleanup_temp_files(self):
1193
+ """Clean up temporary files."""
1194
+
1195
+ # Clean up cache files older than 7 days
1196
+ cache_dir = Path(self.config.cache_dir)
1197
+ cutoff_time = time.time() - (7 * 24 * 60 * 60) # 7 days
1198
+
1199
+ for file_path in cache_dir.rglob("*"):
1200
+ if file_path.is_file() and file_path.stat().st_mtime < cutoff_time:
1201
+ try:
1202
+ file_path.unlink()
1203
+ except:
1204
+ pass
1205
+
1206
+ # Additional utility methods for XML parsing
1207
+ def _extract_pmc_id(self, root) -> str:
1208
+ """Extract PMC ID from XML."""
1209
+ # Implementation for PMC XML parsing
1210
+ return "PMC" + str(hash(str(root)))[-8:]
1211
+
1212
+ def _extract_title(self, root) -> str:
1213
+ """Extract title from XML."""
1214
+ title_elem = root.find(".//article-title")
1215
+ return title_elem.text if title_elem is not None else ""
1216
+
1217
+ def _extract_abstract(self, root) -> str:
1218
+ """Extract abstract from XML."""
1219
+ abstract_elem = root.find(".//abstract")
1220
+ if abstract_elem is not None:
1221
+ return " ".join(abstract_elem.itertext())
1222
+ return ""
1223
+
1224
+ def _extract_full_text(self, root) -> str:
1225
+ """Extract full text from XML."""
1226
+ body_elem = root.find(".//body")
1227
+ if body_elem is not None:
1228
+ return " ".join(body_elem.itertext())
1229
+ return ""
1230
+
1231
+ def _extract_authors(self, root) -> List[str]:
1232
+ """Extract authors from XML."""
1233
+ authors = []
1234
+ for contrib in root.findall('.//contrib[@contrib-type="author"]'):
1235
+ name_elem = contrib.find(".//string-name")
1236
+ if name_elem is not None:
1237
+ authors.append("".join(name_elem.itertext()))
1238
+ return authors
1239
+
1240
+ def _extract_metadata(self, root) -> Dict[str, Any]:
1241
+ """Extract additional metadata from XML."""
1242
+ metadata = {
1243
+ "publication_date": "",
1244
+ "journal": "",
1245
+ "doi": "",
1246
+ "keywords": [],
1247
+ "mesh_terms": [],
1248
+ }
1249
+
1250
+ # Extract publication date
1251
+ pub_date = root.find(".//pub-date")
1252
+ if pub_date is not None:
1253
+ year = pub_date.find(".//year")
1254
+ month = pub_date.find(".//month")
1255
+ day = pub_date.find(".//day")
1256
+
1257
+ date_parts = []
1258
+ if year is not None:
1259
+ date_parts.append(year.text)
1260
+ if month is not None:
1261
+ date_parts.append(month.text)
1262
+ if day is not None:
1263
+ date_parts.append(day.text)
1264
+
1265
+ metadata["publication_date"] = "-".join(date_parts)
1266
+
1267
+ # Extract journal name
1268
+ journal_elem = root.find(".//journal-title")
1269
+ if journal_elem is not None:
1270
+ metadata["journal"] = journal_elem.text
1271
+
1272
+ # Extract DOI
1273
+ doi_elem = root.find('.//article-id[@pub-id-type="doi"]')
1274
+ if doi_elem is not None:
1275
+ metadata["doi"] = doi_elem.text
1276
+
1277
+ # Extract keywords
1278
+ for kwd in root.findall(".//kwd"):
1279
+ if kwd.text:
1280
+ metadata["keywords"].append(kwd.text)
1281
+
1282
+ return metadata
1283
+
1284
+ def _load_compressed_documents(self, file_path: Path) -> Iterator[PMCDocument]:
1285
+ """Load documents from compressed files."""
1286
+ # Placeholder for compressed file handling
1287
+ return iter([])
1288
+
1289
+ def _load_documents_from_api(self, api_endpoint: str) -> Iterator[PMCDocument]:
1290
+ """Load documents from API endpoint."""
1291
+ # Placeholder for API document loading
1292
+ return iter([])
1293
+
1294
+
1295
+ def create_pmc_data_pipeline(
1296
+ config: Optional[ProcessingConfig] = None,
1297
+ ) -> PMCDataPipeline:
1298
+ """Factory function to create a configured PMC data pipeline."""
1299
+ return PMCDataPipeline(config)
1300
+
1301
+
1302
+ if __name__ == "__main__":
1303
+ # Example usage
1304
+ config = ProcessingConfig(
1305
+ max_documents=100, batch_size=10, max_workers=4 # Limit for testing
1306
+ )
1307
+
1308
+ pipeline = create_pmc_data_pipeline(config)
1309
+
1310
+ # Sample documents for testing
1311
+ sample_docs = [
1312
+ PMCDocument(
1313
+ pmc_id="PMC123456",
1314
+ title="Effects of Metformin on Type 2 Diabetes",
1315
+ abstract="This study investigates the effects of metformin on glycemic control in patients with type 2 diabetes.",
1316
+ full_text="Background: Type 2 diabetes is a chronic metabolic disorder. Methods: We conducted a randomized controlled trial. Results: Metformin significantly improved glycemic control. Discussion: These findings support the use of metformin as first-line therapy.",
1317
+ authors=["John Smith", "Jane Doe"],
1318
+ publication_date="2023-01-01",
1319
+ journal="Diabetes Care",
1320
+ keywords=["diabetes", "metformin", "glycemic control"],
1321
+ )
1322
+ ]
1323
+
1324
+ # Process documents
1325
+ stats = pipeline.process_documents(sample_docs)
1326
+
1327
+ print(f"Processing completed:")
1328
+ print(f"- Total documents: {stats.total_documents}")
1329
+ print(f"- Successful: {stats.successful_documents}")
1330
+ print(f"- Failed: {stats.failed_documents}")
1331
+ print(f"- Total chunks: {stats.total_chunks}")
1332
+ print(f"- Processing time: {stats.processing_time:.2f}s")