Coverage for src/qdrant_loader/core/chunking/strategy/markdown/chunk_processor.py: 100%
35 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1"""Chunk processing coordination for markdown strategy."""
3from typing import TYPE_CHECKING, Any
5import structlog
7from qdrant_loader.core.document import Document
8from qdrant_loader.core.text_processing.chunk_enricher import ChunkEnricher
10if TYPE_CHECKING:
11 from qdrant_loader.config import Settings
13logger = structlog.get_logger(__name__)
16class ChunkProcessor:
17 """Handles chunk processing coordination including parallel execution and semantic analysis."""
19 def __init__(self, settings: "Settings"):
20 """Initialize the chunk processor.
22 Args:
23 settings: Configuration settings
24 """
25 self.settings = settings
27 # The shared enricher owns the SemanticAnalyzer (and the enable/enhanced
28 # gating); markdown and docling enrich through the same code path.
29 self._enricher = ChunkEnricher(settings)
31 # Cache for processed chunks to avoid recomputation
32 self._processed_chunks: dict[str, dict[str, Any]] = {}
34 def process_chunk(
35 self, chunk: str, chunk_index: int, total_chunks: int
36 ) -> dict[str, Any]:
37 """Process a single chunk's semantic enrichment.
39 Args:
40 chunk: The chunk to process
41 chunk_index: Index of the chunk
42 total_chunks: Total number of chunks
44 Returns:
45 Dictionary containing enrichment results
46 """
47 logger.debug(
48 "Processing chunk",
49 chunk_index=chunk_index,
50 total_chunks=total_chunks,
51 chunk_length=len(chunk),
52 )
53 results = self._enricher.enrich(chunk, doc_id=f"chunk_{chunk_index}")
54 self._processed_chunks[chunk] = results
55 return results
57 def create_chunk_document(
58 self,
59 original_doc: Document,
60 chunk_content: str,
61 chunk_index: int,
62 total_chunks: int,
63 chunk_metadata: dict[str, Any],
64 skip_nlp: bool = False,
65 ) -> Document:
66 """Create a chunk document with enhanced metadata.
68 Args:
69 original_doc: Original document being chunked
70 chunk_content: Content of the chunk
71 chunk_index: Index of the chunk
72 total_chunks: Total number of chunks
73 chunk_metadata: Chunk-specific metadata
74 skip_nlp: Whether to skip NLP processing
76 Returns:
77 Document representing the chunk
78 """
79 # Create base chunk document
80 chunk_doc = Document(
81 content=chunk_content,
82 title=f"{original_doc.title} - Chunk {chunk_index + 1}",
83 source=original_doc.source,
84 source_type=original_doc.source_type,
85 url=original_doc.url,
86 content_type=original_doc.content_type,
87 metadata=original_doc.metadata.copy(),
88 )
90 # 🔥 FIX: Manually assign chunk ID (following pattern from other strategies)
91 chunk_doc.id = Document.generate_chunk_id(original_doc.id, chunk_index)
93 # Add chunk-specific metadata
94 chunk_doc.metadata.update(chunk_metadata)
95 chunk_doc.metadata.update(
96 {
97 "chunk_index": chunk_index,
98 "total_chunks": total_chunks,
99 "chunk_size": len(chunk_content),
100 "parent_document_id": original_doc.id,
101 "chunking_strategy": "markdown",
102 }
103 )
105 # Perform semantic analysis if not skipped
106 if not skip_nlp:
107 semantic_results = self.process_chunk(
108 chunk_content, chunk_index, total_chunks
109 )
110 chunk_doc.metadata.update(semantic_results)
112 return chunk_doc
114 def estimate_chunk_count(self, content: str) -> int:
115 """Estimate the number of chunks that will be generated.
117 Args:
118 content: The content to estimate chunks for
120 Returns:
121 int: Estimated number of chunks
122 """
123 chunk_size = self.settings.global_config.chunking.chunk_size
125 # Simple estimation: total chars / chunk_size
126 # This is approximate since we split by paragraphs and have overlap
127 estimated = len(content) // chunk_size
129 # Add some buffer for overlap and paragraph boundaries
130 # Apply estimation buffer from configuration
131 buffer_factor = (
132 1.0
133 + self.settings.global_config.chunking.strategies.markdown.estimation_buffer
134 )
135 estimated = int(estimated * buffer_factor)
137 return max(1, estimated) # At least 1 chunk
139 def shutdown(self):
140 """Shutdown and clean up resources."""
141 if getattr(self, "_enricher", None) is not None:
142 self._enricher.shutdown()
144 def __del__(self):
145 """Cleanup on deletion."""
146 self.shutdown()