File size: 9,657 Bytes
b5246f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
"""
Vocabulary index for corpus-aware query enhancement.

Tracks all unique terms in the document corpus to enable intelligent
synonym expansion that only adds terms actually present in documents.
"""

from typing import Set, Dict, List, Optional
from collections import defaultdict
import re
from pathlib import Path
import json


class VocabularyIndex:
    """
    Maintains vocabulary statistics for intelligent query enhancement.
    
    Features:
    - Tracks all unique terms in document corpus
    - Stores term frequencies for relevance weighting
    - Identifies technical terms and domain vocabulary
    - Enables vocabulary-aware synonym expansion
    
    Performance: 
    - Build time: ~1s per 1000 chunks
    - Memory: ~3MB for 80K unique terms
    - Lookup: O(1) set operations
    """
    
    def __init__(self):
        """Initialize empty vocabulary index."""
        self.vocabulary: Set[str] = set()
        self.term_frequencies: Dict[str, int] = defaultdict(int)
        self.technical_terms: Set[str] = set()
        self.document_frequencies: Dict[str, int] = defaultdict(int)
        self.total_documents = 0
        self.total_terms = 0
        
        # Regex for term extraction
        self._term_pattern = re.compile(r'\b[a-zA-Z][a-zA-Z0-9\-_]*\b')
        self._technical_pattern = re.compile(r'\b[A-Z]{2,}|[a-zA-Z]+[\-_][a-zA-Z]+|\b\d+[a-zA-Z]+\b')
        
    def build_from_chunks(self, chunks: List[Dict]) -> None:
        """
        Build vocabulary index from document chunks.
        
        Args:
            chunks: List of document chunks with 'text' field
            
        Performance: ~1s per 1000 chunks
        """
        self.total_documents = len(chunks)
        
        for chunk in chunks:
            text = chunk.get('text', '')
            
            # Extract and process terms
            terms = self._extract_terms(text)
            unique_terms = set(terms)
            
            # Update vocabulary
            self.vocabulary.update(unique_terms)
            
            # Update frequencies
            for term in terms:
                self.term_frequencies[term] += 1
                self.total_terms += 1
            
            # Update document frequencies
            for term in unique_terms:
                self.document_frequencies[term] += 1
            
            # Identify technical terms
            technical = self._extract_technical_terms(text)
            self.technical_terms.update(technical)
    
    def _extract_terms(self, text: str) -> List[str]:
        """Extract normalized terms from text."""
        # Convert to lowercase and extract words
        text_lower = text.lower()
        terms = self._term_pattern.findall(text_lower)
        
        # Filter short terms
        return [term for term in terms if len(term) > 2]
    
    def _extract_technical_terms(self, text: str) -> Set[str]:
        """Extract technical terms (acronyms, hyphenated, etc)."""
        technical = set()
        
        # Find potential technical terms
        matches = self._technical_pattern.findall(text)
        
        for match in matches:
            # Normalize but preserve technical nature
            normalized = match.lower()
            if len(normalized) > 2:
                technical.add(normalized)
                
        return technical
    
    def contains(self, term: str) -> bool:
        """Check if term exists in vocabulary."""
        return term.lower() in self.vocabulary
    
    def get_frequency(self, term: str) -> int:
        """Get term frequency in corpus."""
        return self.term_frequencies.get(term.lower(), 0)
    
    def get_document_frequency(self, term: str) -> int:
        """Get number of documents containing term."""
        return self.document_frequencies.get(term.lower(), 0)
    
    def is_common_term(self, term: str, min_frequency: int = 5) -> bool:
        """Check if term appears frequently enough."""
        return self.get_frequency(term) >= min_frequency
    
    def is_technical_term(self, term: str) -> bool:
        """Check if term is identified as technical."""
        return term.lower() in self.technical_terms
    
    def filter_synonyms(self, synonyms: List[str], 
                       min_frequency: int = 3,
                       require_technical: bool = False) -> List[str]:
        """
        Filter synonym list to only include terms in vocabulary.
        
        Args:
            synonyms: List of potential synonyms
            min_frequency: Minimum term frequency required
            require_technical: Only include technical terms
            
        Returns:
            Filtered list of valid synonyms
        """
        valid_synonyms = []
        
        for synonym in synonyms:
            # Check existence
            if not self.contains(synonym):
                continue
                
            # Check frequency threshold
            if self.get_frequency(synonym) < min_frequency:
                continue
                
            # Check technical requirement
            if require_technical and not self.is_technical_term(synonym):
                continue
                
            valid_synonyms.append(synonym)
            
        return valid_synonyms
    
    def get_vocabulary_stats(self) -> Dict[str, any]:
        """Get comprehensive vocabulary statistics."""
        return {
            'unique_terms': len(self.vocabulary),
            'total_terms': self.total_terms,
            'technical_terms': len(self.technical_terms),
            'total_documents': self.total_documents,
            'avg_terms_per_doc': self.total_terms / self.total_documents if self.total_documents > 0 else 0,
            'vocabulary_richness': len(self.vocabulary) / self.total_terms if self.total_terms > 0 else 0,
            'technical_ratio': len(self.technical_terms) / len(self.vocabulary) if self.vocabulary else 0
        }
    
    def get_top_terms(self, n: int = 100, technical_only: bool = False) -> List[tuple]:
        """
        Get most frequent terms in corpus.
        
        Args:
            n: Number of top terms to return
            technical_only: Only return technical terms
            
        Returns:
            List of (term, frequency) tuples
        """
        if technical_only:
            term_freq = {
                term: freq for term, freq in self.term_frequencies.items()
                if term in self.technical_terms
            }
        else:
            term_freq = self.term_frequencies
            
        return sorted(term_freq.items(), key=lambda x: x[1], reverse=True)[:n]
    
    def detect_domain(self) -> str:
        """
        Detect document domain from vocabulary patterns.
        
        Returns:
            Detected domain name
        """
        # Domain detection heuristics
        domain_indicators = {
            'embedded_systems': ['microcontroller', 'rtos', 'embedded', 'firmware', 'mcu'],
            'processor_architecture': ['risc-v', 'riscv', 'instruction', 'register', 'isa'],
            'regulatory': ['fda', 'validation', 'compliance', 'regulation', 'guidance'],
            'ai_ml': ['model', 'training', 'neural', 'algorithm', 'machine learning'],
            'software_engineering': ['software', 'development', 'testing', 'debugging', 'code']
        }
        
        domain_scores = {}
        
        for domain, indicators in domain_indicators.items():
            score = sum(
                self.get_document_frequency(indicator) 
                for indicator in indicators
                if self.contains(indicator)
            )
            domain_scores[domain] = score
            
        # Return domain with highest score
        if domain_scores:
            return max(domain_scores, key=domain_scores.get)
        return 'general'
    
    def save_to_file(self, path: Path) -> None:
        """Save vocabulary index to JSON file."""
        data = {
            'vocabulary': list(self.vocabulary),
            'term_frequencies': dict(self.term_frequencies),
            'technical_terms': list(self.technical_terms),
            'document_frequencies': dict(self.document_frequencies),
            'total_documents': self.total_documents,
            'total_terms': self.total_terms
        }
        
        with open(path, 'w') as f:
            json.dump(data, f, indent=2)
    
    def load_from_file(self, path: Path) -> None:
        """Load vocabulary index from JSON file."""
        with open(path, 'r') as f:
            data = json.load(f)
            
        self.vocabulary = set(data['vocabulary'])
        self.term_frequencies = defaultdict(int, data['term_frequencies'])
        self.technical_terms = set(data['technical_terms'])
        self.document_frequencies = defaultdict(int, data['document_frequencies'])
        self.total_documents = data['total_documents']
        self.total_terms = data['total_terms']
    
    def merge_with(self, other: 'VocabularyIndex') -> None:
        """Merge another vocabulary index into this one."""
        # Merge vocabularies
        self.vocabulary.update(other.vocabulary)
        self.technical_terms.update(other.technical_terms)
        
        # Merge frequencies
        for term, freq in other.term_frequencies.items():
            self.term_frequencies[term] += freq
            
        for term, doc_freq in other.document_frequencies.items():
            self.document_frequencies[term] += doc_freq
            
        # Update totals
        self.total_documents += other.total_documents
        self.total_terms += other.total_terms