1use std::sync::Arc;
13
14use parking_lot::RwLock;
15use rustc_hash::FxHashMap;
16
17use crate::dsl::Field;
18use crate::segment::SegmentReader;
19
20pub struct LazyGlobalStats {
26 segments: Vec<Arc<SegmentReader>>,
28 total_docs: u64,
30 sparse_idf_cache: RwLock<FxHashMap<u32, FxHashMap<u32, f32>>>,
32 sparse_total_vectors_cache: RwLock<FxHashMap<u32, u64>>,
34 text_idf_cache: RwLock<FxHashMap<u32, FxHashMap<String, f32>>>,
36 avg_field_len_cache: RwLock<FxHashMap<u32, f32>>,
38 text_df_cache: RwLock<FxHashMap<u32, FxHashMap<Vec<u8>, u64>>>,
41 text_corpus_cache: RwLock<FxHashMap<u32, u64>>,
44}
45
46impl LazyGlobalStats {
47 pub fn new(segments: Vec<Arc<SegmentReader>>) -> Self {
49 let total_docs: u64 = segments.iter().map(|s| s.num_docs() as u64).sum();
50 Self {
51 segments,
52 total_docs,
53 sparse_idf_cache: RwLock::new(FxHashMap::default()),
54 sparse_total_vectors_cache: RwLock::new(FxHashMap::default()),
55 text_idf_cache: RwLock::new(FxHashMap::default()),
56 avg_field_len_cache: RwLock::new(FxHashMap::default()),
57 text_df_cache: RwLock::new(FxHashMap::default()),
58 text_corpus_cache: RwLock::new(FxHashMap::default()),
59 }
60 }
61
62 pub fn text_df(&self, field: Field, term: &[u8]) -> u64 {
67 {
68 let cache = self.text_df_cache.read();
69 if let Some(field_cache) = cache.get(&field.0)
70 && let Some(&df) = field_cache.get(term)
71 {
72 return df;
73 }
74 }
75 let df = self.compute_text_df_bytes(field, term);
76 self.text_df_cache
77 .write()
78 .entry(field.0)
79 .or_default()
80 .insert(term.to_vec(), df);
81 df
82 }
83
84 pub fn text_corpus_size(&self, field: Field) -> u64 {
87 if let Some(&size) = self.text_corpus_cache.read().get(&field.0) {
88 return size;
89 }
90 let size: u64 = self
91 .segments
92 .iter()
93 .map(|segment| segment.text_corpus_size(field) as u64)
94 .sum();
95 self.text_corpus_cache.write().insert(field.0, size);
96 size
97 }
98
99 pub fn text_stats_for(&self, terms: &[(Field, Vec<u8>)]) -> GlobalStats {
104 let mut builder = GlobalStatsBuilder::new();
105 builder.total_docs = self.total_docs;
106 let mut fields_seen: FxHashMap<u32, ()> = FxHashMap::default();
107 for (field, term) in terms {
108 if fields_seen.insert(field.0, ()).is_none() {
109 builder.set_avg_field_len(*field, self.avg_field_len(*field));
110 builder.set_text_corpus_size(*field, self.text_corpus_size(*field));
111 }
112 let df = self.text_df(*field, term);
113 if df > 0 {
114 builder.add_text_df(*field, String::from_utf8_lossy(term).into_owned(), df);
115 }
116 }
117 builder.build(0)
118 }
119
120 #[cfg(feature = "sync")]
121 fn compute_text_df_bytes(&self, field: Field, term: &[u8]) -> u64 {
122 self.segments
123 .iter()
124 .map(|segment| segment.text_doc_freq_sync(field, term).unwrap_or(0) as u64)
125 .sum()
126 }
127
128 #[cfg(not(feature = "sync"))]
129 fn compute_text_df_bytes(&self, _field: Field, _term: &[u8]) -> u64 {
130 0
131 }
132
133 #[inline]
135 pub fn total_docs(&self) -> u64 {
136 self.total_docs
137 }
138
139 pub fn sparse_idf(&self, field: Field, dim_id: u32) -> f32 {
143 {
145 let cache = self.sparse_idf_cache.read();
146 if let Some(field_cache) = cache.get(&field.0)
147 && let Some(&idf) = field_cache.get(&dim_id)
148 {
149 return idf;
150 }
151 }
152
153 let df = self.compute_sparse_df(field, dim_id);
155 let n = self.cached_sparse_n(field);
156 let idf = if df > 0 && n > 0 {
157 (n as f32 / df as f32).ln().max(0.0)
158 } else {
159 0.0
160 };
161
162 {
164 let mut cache = self.sparse_idf_cache.write();
165 cache.entry(field.0).or_default().insert(dim_id, idf);
166 }
167
168 idf
169 }
170
171 pub fn sparse_idf_weights(&self, field: Field, dim_ids: &[u32]) -> Vec<f32> {
176 let mut result = vec![0.0f32; dim_ids.len()];
178 let mut misses: Vec<usize> = Vec::new();
179 {
180 let cache = self.sparse_idf_cache.read();
181 if let Some(field_cache) = cache.get(&field.0) {
182 for (i, &dim_id) in dim_ids.iter().enumerate() {
183 if let Some(&idf) = field_cache.get(&dim_id) {
184 result[i] = idf;
185 } else {
186 misses.push(i);
187 }
188 }
189 } else {
190 misses.extend(0..dim_ids.len());
191 }
192 }
193
194 if misses.is_empty() {
195 return result;
196 }
197
198 let n = self.cached_sparse_n(field);
200
201 let mut new_entries: Vec<(u32, f32)> = Vec::with_capacity(misses.len());
203 for &i in &misses {
204 let dim_id = dim_ids[i];
205 let df = self.compute_sparse_df(field, dim_id);
206 let idf = if df > 0 && n > 0 {
207 (n as f32 / df as f32).ln().max(0.0)
208 } else {
209 0.0
210 };
211 result[i] = idf;
212 new_entries.push((dim_id, idf));
213 }
214
215 {
217 let mut cache = self.sparse_idf_cache.write();
218 let field_cache = cache.entry(field.0).or_default();
219 for (dim_id, idf) in new_entries {
220 field_cache.insert(dim_id, idf);
221 }
222 }
223
224 result
225 }
226
227 fn cached_sparse_n(&self, field: Field) -> u64 {
230 {
232 let cache = self.sparse_total_vectors_cache.read();
233 if let Some(&tv) = cache.get(&field.0) {
234 return tv.max(self.total_docs);
235 }
236 }
237 let tv = self.compute_sparse_total_vectors(field);
239 self.sparse_total_vectors_cache.write().insert(field.0, tv);
240 tv.max(self.total_docs)
241 }
242
243 pub fn text_idf(&self, field: Field, term: &str) -> f32 {
247 {
249 let cache = self.text_idf_cache.read();
250 if let Some(field_cache) = cache.get(&field.0)
251 && let Some(&idf) = field_cache.get(term)
252 {
253 return idf;
254 }
255 }
256
257 let df = self.compute_text_df(field, term);
260 let n = self.text_corpus_size(field) as f32;
261 let df_f = df as f32;
262 let idf = if df > 0 {
263 ((n - df_f + 0.5) / (df_f + 0.5) + 1.0).ln()
264 } else {
265 0.0
266 };
267
268 {
270 let mut cache = self.text_idf_cache.write();
271 cache
272 .entry(field.0)
273 .or_default()
274 .insert(term.to_string(), idf);
275 }
276
277 idf
278 }
279
280 pub fn avg_field_len(&self, field: Field) -> f32 {
282 {
284 let cache = self.avg_field_len_cache.read();
285 if let Some(&avg) = cache.get(&field.0) {
286 return avg;
287 }
288 }
289
290 let mut weighted_sum = 0.0f64;
292 let mut total_weight = 0u64;
293
294 for segment in &self.segments {
295 let avg_len = segment.avg_field_len(field);
296 let doc_count = segment.text_corpus_size(field) as u64;
298 if avg_len > 0.0 && doc_count > 0 {
299 weighted_sum += avg_len as f64 * doc_count as f64;
300 total_weight += doc_count;
301 }
302 }
303
304 let avg = if total_weight > 0 {
305 (weighted_sum / total_weight as f64) as f32
306 } else {
307 1.0
308 };
309
310 {
312 let mut cache = self.avg_field_len_cache.write();
313 cache.insert(field.0, avg);
314 }
315
316 avg
317 }
318
319 fn compute_sparse_df(&self, field: Field, dim_id: u32) -> u64 {
322 let mut df = 0u64;
323 for segment in &self.segments {
324 if let Some(sparse_index) = segment.sparse_indexes().get(&field.0) {
325 df += sparse_index.doc_count(dim_id) as u64;
326 }
327 }
328 df
329 }
330
331 fn compute_sparse_total_vectors(&self, field: Field) -> u64 {
334 let mut total = 0u64;
335 for segment in &self.segments {
336 if let Some(sparse_index) = segment.sparse_indexes().get(&field.0) {
337 total += sparse_index.total_vectors as u64;
338 }
339 }
340 total
341 }
342
343 fn compute_text_df(&self, field: Field, term: &str) -> u64 {
345 self.text_df(field, term.as_bytes())
346 }
347
348 pub fn num_segments(&self) -> usize {
350 self.segments.len()
351 }
352}
353
354impl std::fmt::Debug for LazyGlobalStats {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 f.debug_struct("LazyGlobalStats")
357 .field("total_docs", &self.total_docs)
358 .field("num_segments", &self.segments.len())
359 .field("sparse_cache_fields", &self.sparse_idf_cache.read().len())
360 .field("text_cache_fields", &self.text_idf_cache.read().len())
361 .finish()
362 }
363}
364
365#[derive(Debug)]
369pub struct GlobalStats {
370 total_docs: u64,
372 sparse_stats: FxHashMap<u32, SparseFieldStats>,
374 text_stats: FxHashMap<u32, TextFieldStats>,
376 generation: u64,
378}
379
380#[derive(Debug, Default)]
382pub struct SparseFieldStats {
383 pub doc_freqs: FxHashMap<u32, u64>,
385}
386
387#[derive(Debug, Default)]
389pub struct TextFieldStats {
390 pub doc_freqs: FxHashMap<String, u64>,
392 pub avg_field_len: f32,
394 pub corpus_size: u64,
397}
398
399impl GlobalStats {
400 pub fn new() -> Self {
402 Self {
403 total_docs: 0,
404 sparse_stats: FxHashMap::default(),
405 text_stats: FxHashMap::default(),
406 generation: 0,
407 }
408 }
409
410 #[inline]
412 pub fn total_docs(&self) -> u64 {
413 self.total_docs
414 }
415
416 #[inline]
418 pub fn sparse_idf(&self, field: Field, dim_id: u32) -> f32 {
419 if let Some(stats) = self.sparse_stats.get(&field.0)
420 && let Some(&df) = stats.doc_freqs.get(&dim_id)
421 && df > 0
422 {
423 return (self.total_docs as f32 / df as f32).ln();
424 }
425 0.0
426 }
427
428 pub fn sparse_idf_weights(&self, field: Field, dim_ids: &[u32]) -> Vec<f32> {
430 dim_ids.iter().map(|&d| self.sparse_idf(field, d)).collect()
431 }
432
433 #[inline]
435 pub fn text_idf(&self, field: Field, term: &str) -> f32 {
436 if let Some(stats) = self.text_stats.get(&field.0)
437 && let Some(&df) = stats.doc_freqs.get(term)
438 {
439 let n = if stats.corpus_size > 0 {
440 stats.corpus_size as f32
441 } else {
442 self.total_docs as f32
443 };
444 let df = df as f32;
445 return ((n - df + 0.5) / (df + 0.5) + 1.0).ln();
446 }
447 0.0
448 }
449
450 pub fn text_df(&self, field: Field, term: &str) -> Option<u64> {
452 self.text_stats
453 .get(&field.0)
454 .and_then(|stats| stats.doc_freqs.get(term).copied())
455 }
456
457 pub fn text_corpus_size(&self, field: Field) -> u64 {
459 self.text_stats
460 .get(&field.0)
461 .map_or(0, |stats| stats.corpus_size)
462 }
463
464 pub fn text_fields(&self) -> impl Iterator<Item = (Field, &TextFieldStats)> {
466 self.text_stats
467 .iter()
468 .map(|(id, stats)| (Field(*id), stats))
469 }
470
471 #[inline]
473 pub fn avg_field_len(&self, field: Field) -> f32 {
474 self.text_stats
475 .get(&field.0)
476 .map(|s| s.avg_field_len)
477 .unwrap_or(1.0)
478 }
479
480 #[inline]
482 pub fn generation(&self) -> u64 {
483 self.generation
484 }
485}
486
487impl Default for GlobalStats {
488 fn default() -> Self {
489 Self::new()
490 }
491}
492
493pub struct GlobalStatsBuilder {
495 pub total_docs: u64,
497 sparse_stats: FxHashMap<u32, SparseFieldStats>,
498 text_stats: FxHashMap<u32, TextFieldStats>,
499}
500
501impl GlobalStatsBuilder {
502 pub fn new() -> Self {
504 Self {
505 total_docs: 0,
506 sparse_stats: FxHashMap::default(),
507 text_stats: FxHashMap::default(),
508 }
509 }
510
511 pub fn add_segment(&mut self, reader: &SegmentReader) {
513 self.total_docs += reader.num_docs() as u64;
514
515 }
518
519 pub fn add_sparse_df(&mut self, field: Field, dim_id: u32, doc_count: u64) {
521 let stats = self.sparse_stats.entry(field.0).or_default();
522 *stats.doc_freqs.entry(dim_id).or_insert(0) += doc_count;
523 }
524
525 pub fn add_text_df(&mut self, field: Field, term: String, doc_count: u64) {
527 let stats = self.text_stats.entry(field.0).or_default();
528 *stats.doc_freqs.entry(term).or_insert(0) += doc_count;
529 }
530
531 pub fn set_avg_field_len(&mut self, field: Field, avg_len: f32) {
533 let stats = self.text_stats.entry(field.0).or_default();
534 stats.avg_field_len = avg_len;
535 }
536
537 pub fn set_text_corpus_size(&mut self, field: Field, corpus_size: u64) {
539 let stats = self.text_stats.entry(field.0).or_default();
540 stats.corpus_size = corpus_size;
541 }
542
543 pub fn build(self, generation: u64) -> GlobalStats {
545 GlobalStats {
546 total_docs: self.total_docs,
547 sparse_stats: self.sparse_stats,
548 text_stats: self.text_stats,
549 generation,
550 }
551 }
552}
553
554impl Default for GlobalStatsBuilder {
555 fn default() -> Self {
556 Self::new()
557 }
558}
559
560pub struct GlobalStatsCache {
565 stats: RwLock<Option<Arc<GlobalStats>>>,
567 generation: RwLock<u64>,
569}
570
571impl GlobalStatsCache {
572 pub fn new() -> Self {
574 Self {
575 stats: RwLock::new(None),
576 generation: RwLock::new(0),
577 }
578 }
579
580 pub fn invalidate(&self) {
582 let mut current_gen = self.generation.write();
583 *current_gen += 1;
584 let mut stats = self.stats.write();
585 *stats = None;
586 }
587
588 pub fn generation(&self) -> u64 {
590 *self.generation.read()
591 }
592
593 pub fn get(&self) -> Option<Arc<GlobalStats>> {
595 self.stats.read().clone()
596 }
597
598 pub fn set(&self, stats: GlobalStats) {
600 let mut cached = self.stats.write();
601 *cached = Some(Arc::new(stats));
602 }
603
604 pub fn get_or_compute<F>(&self, compute: F) -> Arc<GlobalStats>
608 where
609 F: FnOnce(&mut GlobalStatsBuilder),
610 {
611 if let Some(stats) = self.get() {
613 return stats;
614 }
615
616 let current_gen = self.generation();
618 let mut builder = GlobalStatsBuilder::new();
619 compute(&mut builder);
620 let stats = Arc::new(builder.build(current_gen));
621
622 let mut cached = self.stats.write();
624 *cached = Some(Arc::clone(&stats));
625
626 stats
627 }
628
629 pub fn needs_rebuild(&self) -> bool {
631 self.stats.read().is_none()
632 }
633
634 pub fn set_stats(&self, stats: GlobalStats) {
636 let mut cached = self.stats.write();
637 *cached = Some(Arc::new(stats));
638 }
639}
640
641impl Default for GlobalStatsCache {
642 fn default() -> Self {
643 Self::new()
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 #[test]
652 fn test_sparse_idf_computation() {
653 let mut builder = GlobalStatsBuilder::new();
654 builder.total_docs = 1000;
655 builder.add_sparse_df(Field(0), 42, 100); builder.add_sparse_df(Field(0), 43, 10); let stats = builder.build(1);
659
660 let idf_42 = stats.sparse_idf(Field(0), 42);
662 let idf_43 = stats.sparse_idf(Field(0), 43);
663
664 assert!(idf_43 > idf_42);
666 assert!((idf_42 - (1000.0_f32 / 100.0).ln()).abs() < 0.001);
667 assert!((idf_43 - (1000.0_f32 / 10.0).ln()).abs() < 0.001);
668 }
669
670 #[test]
671 fn test_text_idf_computation() {
672 let mut builder = GlobalStatsBuilder::new();
673 builder.total_docs = 10000;
674 builder.add_text_df(Field(0), "common".to_string(), 5000);
675 builder.add_text_df(Field(0), "rare".to_string(), 10);
676
677 let stats = builder.build(1);
678
679 let idf_common = stats.text_idf(Field(0), "common");
680 let idf_rare = stats.text_idf(Field(0), "rare");
681
682 assert!(idf_rare > idf_common);
684 }
685
686 #[test]
687 fn test_cache_invalidation() {
688 let cache = GlobalStatsCache::new();
689
690 assert!(cache.get().is_none());
692
693 let stats = cache.get_or_compute(|builder| {
695 builder.total_docs = 100;
696 });
697 assert_eq!(stats.total_docs(), 100);
698
699 assert!(cache.get().is_some());
701
702 cache.invalidate();
704 assert!(cache.get().is_none());
705 }
706}