1use std::fmt::Debug;
7
8#[cfg(feature = "native")]
9mod segment_manager;
10#[cfg(feature = "native")]
11pub use segment_manager::SegmentManager;
12#[cfg(feature = "native")]
13pub(crate) use segment_manager::SegmentOperationGuard;
14
15#[derive(Debug, Clone)]
17pub struct SegmentInfo {
18 pub id: String,
20 pub num_docs: u32,
22}
23
24#[derive(Debug, Clone)]
26pub struct MergeCandidate {
27 pub segment_ids: Vec<String>,
29}
30
31pub trait MergePolicy: Send + Sync + Debug {
35 fn find_merges(&self, segments: &[SegmentInfo]) -> Vec<MergeCandidate>;
38
39 fn has_severe_backlog(&self, _segments: &[SegmentInfo]) -> bool {
47 false
48 }
49
50 fn clone_box(&self) -> Box<dyn MergePolicy>;
52
53 fn max_segment_docs(&self) -> Option<u32> {
56 None
57 }
58}
59
60impl Clone for Box<dyn MergePolicy> {
61 fn clone(&self) -> Self {
62 self.clone_box()
63 }
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct NoMergePolicy;
69
70impl MergePolicy for NoMergePolicy {
71 fn find_merges(&self, _segments: &[SegmentInfo]) -> Vec<MergeCandidate> {
72 Vec::new()
73 }
74
75 fn clone_box(&self) -> Box<dyn MergePolicy> {
76 Box::new(self.clone())
77 }
78}
79
80#[derive(Debug, Clone)]
96pub struct TieredMergePolicy {
97 pub segments_per_tier: usize,
99 pub max_merge_at_once: usize,
102 pub tier_factor: f64,
104 pub tier_floor: u32,
106 pub max_merged_docs: u32,
108
109 pub floor_segment_docs: u32,
113 pub oversized_threshold: f64,
116 pub min_growth_ratio: f64,
120 pub budget_trigger: bool,
124 pub scored_selection: bool,
127 pub max_segment_docs: u32,
130}
131
132impl Default for TieredMergePolicy {
133 fn default() -> Self {
134 Self {
135 segments_per_tier: 10,
136 max_merge_at_once: 10,
137 tier_factor: 10.0,
138 tier_floor: 1000,
139 max_merged_docs: 5_000_000,
140 floor_segment_docs: 1000,
141 oversized_threshold: 0.5,
142 min_growth_ratio: 0.0,
143 budget_trigger: false,
144 scored_selection: false,
145 max_segment_docs: 10_000_000,
146 }
147 }
148}
149
150impl TieredMergePolicy {
151 pub fn new() -> Self {
153 Self::default()
154 }
155
156 pub fn aggressive() -> Self {
162 Self {
163 segments_per_tier: 3,
164 max_merge_at_once: 10,
165 tier_factor: 10.0,
166 tier_floor: 500,
167 max_merged_docs: 10_000_000,
168 max_segment_docs: 10_000_000,
169 ..Default::default()
170 }
171 }
172
173 pub fn large_scale() -> Self {
181 Self {
182 segments_per_tier: 10,
183 max_merge_at_once: 24,
189 tier_factor: 10.0,
190 tier_floor: 50_000,
191 max_merged_docs: 5_000_000,
195 floor_segment_docs: 50_000,
196 oversized_threshold: 0.5,
197 min_growth_ratio: 0.5,
198 budget_trigger: true,
199 scored_selection: true,
200 max_segment_docs: 5_000_000,
201 }
202 }
203
204 pub fn bulk_indexing() -> Self {
209 Self {
210 segments_per_tier: 20,
211 max_merge_at_once: 20,
212 tier_factor: 10.0,
213 tier_floor: 100_000,
214 max_merged_docs: 50_000_000,
215 floor_segment_docs: 100_000,
216 oversized_threshold: 0.5,
217 min_growth_ratio: 0.75,
218 budget_trigger: true,
219 scored_selection: true,
220 max_segment_docs: 50_000_000,
221 }
222 }
223}
224
225impl TieredMergePolicy {
226 fn effective_max_docs(&self) -> u32 {
229 self.max_merged_docs.min(self.max_segment_docs)
230 }
231
232 fn compute_ideal_segment_count(&self, total_docs: u64) -> usize {
236 if total_docs == 0 {
237 return 0;
238 }
239 let floor = self.floor_segment_docs.max(1) as f64;
240 let num_tiers = ((total_docs as f64 / floor).max(1.0))
242 .log(self.tier_factor)
243 .ceil() as usize;
244 let num_tiers = num_tiers.max(1);
245 num_tiers * self.segments_per_tier
246 }
247
248 fn eligible_segments<'a>(&self, segments: &'a [SegmentInfo]) -> Vec<&'a SegmentInfo> {
249 let effective_max = self.effective_max_docs();
250 let oversized_limit = (effective_max as f64 * self.oversized_threshold) as u64;
251 segments
252 .iter()
253 .filter(|segment| (segment.num_docs as u64) <= oversized_limit || oversized_limit == 0)
254 .collect()
255 }
256
257 fn score_candidate(&self, group: &[usize], sorted: &[&SegmentInfo]) -> f64 {
262 let floor = self.floor_segment_docs.max(1) as f64;
263 let mut total_floored = 0.0f64;
264 let mut largest_floored = 0.0f64;
265 for &idx in group {
266 let floored = (sorted[idx].num_docs as f64).max(floor);
267 total_floored += floored;
268 if floored > largest_floored {
269 largest_floored = floored;
270 }
271 }
272 if total_floored == 0.0 {
273 return f64::MAX;
274 }
275 let skew = largest_floored / total_floored;
276 skew * total_floored.powf(0.05)
277 }
278
279 fn passes_min_growth(&self, group: &[usize], sorted: &[&SegmentInfo]) -> bool {
282 if self.min_growth_ratio <= 0.0 || group.len() < 2 {
283 return true;
284 }
285 let largest = group
286 .iter()
287 .map(|&i| sorted[i].num_docs as u64)
288 .max()
289 .unwrap_or(0);
290 let total: u64 = group.iter().map(|&i| sorted[i].num_docs as u64).sum();
291 total as f64 >= (1.0 + self.min_growth_ratio) * largest as f64
292 }
293
294 fn find_merges_greedy(&self, sorted: &[&SegmentInfo]) -> Vec<MergeCandidate> {
296 let mut candidates = Vec::new();
297 let mut used = vec![false; sorted.len()];
298 let max_ratio = self.tier_factor as u64;
299 let effective_max = self.effective_max_docs() as u64;
300
301 let mut start = 0;
302 loop {
303 while start < sorted.len() && used[start] {
304 start += 1;
305 }
306 if start >= sorted.len() {
307 break;
308 }
309
310 let mut group = vec![start];
311 let mut total_docs: u64 = sorted[start].num_docs as u64;
312
313 for j in (start + 1)..sorted.len() {
314 if used[j] {
315 continue;
316 }
317 if group.len() >= self.max_merge_at_once {
318 break;
319 }
320 let next_docs = sorted[j].num_docs as u64;
321 if total_docs + next_docs > effective_max {
322 break;
323 }
324 if next_docs > total_docs.max(1) * max_ratio {
325 break;
326 }
327 group.push(j);
328 total_docs += next_docs;
329 }
330
331 if group.len() >= self.segments_per_tier
332 && group.len() >= 2
333 && self.passes_min_growth(&group, sorted)
334 {
335 for &i in &group {
336 used[i] = true;
337 }
338 candidates.push(MergeCandidate {
339 segment_ids: group.iter().map(|&i| sorted[i].id.clone()).collect(),
340 });
341 }
342
343 start += 1;
344 }
345
346 candidates
347 }
348
349 fn find_merges_scored(&self, sorted: &[&SegmentInfo]) -> Vec<MergeCandidate> {
352 let max_ratio = self.tier_factor as u64;
353 let effective_max = self.effective_max_docs() as u64;
354
355 let mut scored_groups: Vec<(f64, Vec<usize>)> = Vec::new();
357
358 for start in 0..sorted.len() {
359 let mut group = vec![start];
360 let mut total_docs: u64 = sorted[start].num_docs as u64;
361
362 for j in (start + 1)..sorted.len() {
363 if group.len() >= self.max_merge_at_once {
364 break;
365 }
366 let next_docs = sorted[j].num_docs as u64;
367 if total_docs + next_docs > effective_max {
368 break;
369 }
370 if next_docs > total_docs.max(1) * max_ratio {
371 break;
372 }
373 group.push(j);
374 total_docs += next_docs;
375
376 if group.len() >= self.segments_per_tier
378 && group.len() >= 2
379 && self.passes_min_growth(&group, sorted)
380 {
381 let score = self.score_candidate(&group, sorted);
382 scored_groups.push((score, group.clone()));
383 }
384 }
385 }
386
387 scored_groups.sort_by(|a, b| a.0.total_cmp(&b.0));
389
390 let mut used = vec![false; sorted.len()];
392 let mut candidates = Vec::new();
393
394 for (_score, group) in scored_groups {
395 if group.iter().any(|&i| used[i]) {
396 continue;
397 }
398 for &i in &group {
399 used[i] = true;
400 }
401 candidates.push(MergeCandidate {
402 segment_ids: group.iter().map(|&i| sorted[i].id.clone()).collect(),
403 });
404 }
405
406 candidates
407 }
408}
409
410impl MergePolicy for TieredMergePolicy {
411 fn find_merges(&self, segments: &[SegmentInfo]) -> Vec<MergeCandidate> {
412 if segments.len() < 2 {
413 return Vec::new();
414 }
415
416 let eligible = self.eligible_segments(segments);
418
419 if eligible.len() < 2 {
420 return Vec::new();
421 }
422
423 if self.budget_trigger {
425 let total_docs: u64 = segments.iter().map(|s| s.num_docs as u64).sum();
426 let ideal = self.compute_ideal_segment_count(total_docs);
427 if eligible.len() <= ideal {
428 return Vec::new();
429 }
430 }
431
432 let mut sorted = eligible;
434 sorted.sort_by_key(|s| s.num_docs);
435
436 if self.scored_selection {
438 self.find_merges_scored(&sorted)
439 } else {
440 self.find_merges_greedy(&sorted)
441 }
442 }
443
444 fn has_severe_backlog(&self, segments: &[SegmentInfo]) -> bool {
445 if !self.budget_trigger {
446 return false;
447 }
448 let eligible = self.eligible_segments(segments);
449 if eligible.len() < 2 {
450 return false;
451 }
452 let total_docs: u64 = segments
453 .iter()
454 .map(|segment| u64::from(segment.num_docs))
455 .sum();
456 let ideal = self.compute_ideal_segment_count(total_docs);
457 eligible.len() > ideal.saturating_mul(2)
458 }
459
460 fn clone_box(&self) -> Box<dyn MergePolicy> {
461 Box::new(self.clone())
462 }
463
464 fn max_segment_docs(&self) -> Option<u32> {
465 Some(self.max_segment_docs)
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 fn compute_tier(policy: &TieredMergePolicy, num_docs: u32) -> usize {
475 if num_docs <= policy.tier_floor {
476 return 0;
477 }
478 let ratio = num_docs as f64 / policy.tier_floor as f64;
479 (ratio.log(policy.tier_factor).floor() as usize) + 1
480 }
481
482 #[test]
483 fn test_tiered_policy_compute_tier() {
484 let policy = TieredMergePolicy::default();
485
486 assert_eq!(compute_tier(&policy, 500), 0);
488 assert_eq!(compute_tier(&policy, 1000), 0);
489
490 assert_eq!(compute_tier(&policy, 1001), 1);
492 assert_eq!(compute_tier(&policy, 5000), 1);
493 assert_eq!(compute_tier(&policy, 9999), 1);
494
495 assert_eq!(compute_tier(&policy, 10000), 2);
497 assert_eq!(compute_tier(&policy, 50000), 2);
498
499 assert_eq!(compute_tier(&policy, 100000), 3);
501 }
502
503 #[test]
504 fn test_tiered_policy_no_merge_few_segments() {
505 let policy = TieredMergePolicy::default();
506
507 let segments = vec![
508 SegmentInfo {
509 id: "a".into(),
510 num_docs: 100,
511 },
512 SegmentInfo {
513 id: "b".into(),
514 num_docs: 200,
515 },
516 ];
517
518 assert!(policy.find_merges(&segments).is_empty());
519 }
520
521 #[test]
522 fn test_tiered_policy_merge_same_size() {
523 let policy = TieredMergePolicy {
524 segments_per_tier: 3,
525 ..Default::default()
526 };
527
528 let segments: Vec<_> = (0..5)
530 .map(|i| SegmentInfo {
531 id: format!("seg_{}", i),
532 num_docs: 100 + i * 10,
533 })
534 .collect();
535
536 let candidates = policy.find_merges(&segments);
537 assert_eq!(candidates.len(), 1);
538 assert_eq!(candidates[0].segment_ids.len(), 5);
539 }
540
541 #[test]
542 fn test_tiered_policy_cross_tier_promotion() {
543 let policy = TieredMergePolicy {
544 segments_per_tier: 3,
545 tier_factor: 10.0,
546 tier_floor: 1000,
547 max_merge_at_once: 20,
548 max_merged_docs: 5_000_000,
549 ..Default::default()
550 };
551
552 let mut segments: Vec<_> = (0..4)
555 .map(|i| SegmentInfo {
556 id: format!("small_{}", i),
557 num_docs: 100 + i * 10,
558 })
559 .collect();
560 for i in 0..3 {
561 segments.push(SegmentInfo {
562 id: format!("medium_{}", i),
563 num_docs: 2000 + i * 500,
564 });
565 }
566
567 let candidates = policy.find_merges(&segments);
568 assert_eq!(
569 candidates.len(),
570 1,
571 "should merge all into one cross-tier group"
572 );
573 assert_eq!(
574 candidates[0].segment_ids.len(),
575 7,
576 "all 7 segments should be in the merge"
577 );
578 }
579
580 #[test]
581 fn test_tiered_policy_ratio_guard_separates_groups() {
582 let policy = TieredMergePolicy {
583 segments_per_tier: 3,
584 tier_factor: 10.0,
585 tier_floor: 100,
586 max_merge_at_once: 20,
587 max_merged_docs: 5_000_000,
588 ..Default::default()
589 };
590
591 let mut segments: Vec<_> = (0..4)
597 .map(|i| SegmentInfo {
598 id: format!("tiny_{}", i),
599 num_docs: 10,
600 })
601 .collect();
602 for i in 0..4 {
603 segments.push(SegmentInfo {
604 id: format!("large_{}", i),
605 num_docs: 100_000 + i * 100,
606 });
607 }
608
609 let candidates = policy.find_merges(&segments);
610 assert_eq!(candidates.len(), 2, "should produce two separate groups");
611
612 assert_eq!(candidates[0].segment_ids.len(), 4);
614 assert!(candidates[0].segment_ids[0].starts_with("tiny_"));
615
616 assert_eq!(candidates[1].segment_ids.len(), 4);
618 assert!(candidates[1].segment_ids[0].starts_with("large_"));
619 }
620
621 #[test]
622 fn test_tiered_policy_small_segments_skip_to_large_group() {
623 let policy = TieredMergePolicy {
624 segments_per_tier: 3,
625 tier_factor: 10.0,
626 tier_floor: 1000,
627 max_merge_at_once: 10,
628 max_merged_docs: 5_000_000,
629 ..Default::default()
630 };
631
632 let mut segments = vec![
635 SegmentInfo {
636 id: "tiny_0".into(),
637 num_docs: 10,
638 },
639 SegmentInfo {
640 id: "tiny_1".into(),
641 num_docs: 20,
642 },
643 ];
644 for i in 0..5 {
645 segments.push(SegmentInfo {
646 id: format!("medium_{}", i),
647 num_docs: 5000 + i * 100,
648 });
649 }
650
651 let candidates = policy.find_merges(&segments);
652 assert!(
653 !candidates.is_empty(),
654 "should find a merge even though tiny segments can't form a group"
655 );
656 let total_segs: usize = candidates.iter().map(|c| c.segment_ids.len()).sum();
658 assert!(
659 total_segs >= 5,
660 "should merge at least the 5 medium segments"
661 );
662 }
663
664 #[test]
665 fn test_tiered_policy_respects_max_merged_docs() {
666 let policy = TieredMergePolicy {
667 segments_per_tier: 3,
668 max_merge_at_once: 100,
669 tier_factor: 10.0,
670 tier_floor: 1000,
671 max_merged_docs: 500,
672 ..Default::default()
673 };
674
675 let segments: Vec<_> = (0..10)
677 .map(|i| SegmentInfo {
678 id: format!("seg_{}", i),
679 num_docs: 100,
680 })
681 .collect();
682
683 let candidates = policy.find_merges(&segments);
684 for c in &candidates {
685 let total: u64 = c
686 .segment_ids
687 .iter()
688 .map(|id| segments.iter().find(|s| s.id == *id).unwrap().num_docs as u64)
689 .sum();
690 assert!(
691 total <= 500,
692 "merge total {} exceeds max_merged_docs 500",
693 total
694 );
695 }
696 }
697
698 #[test]
699 fn test_tiered_policy_large_segment_not_remerged_with_small() {
700 let policy = TieredMergePolicy::default(); let mut segments = vec![SegmentInfo {
708 id: "large_merged".into(),
709 num_docs: 50_000,
710 }];
711 for i in 0..5 {
712 segments.push(SegmentInfo {
713 id: format!("new_{}", i),
714 num_docs: 500,
715 });
716 }
717
718 let candidates = policy.find_merges(&segments);
721 assert!(
722 candidates.is_empty(),
723 "should not re-merge large segment with 5 small ones: {:?}",
724 candidates
725 );
726
727 for i in 5..10 {
730 segments.push(SegmentInfo {
731 id: format!("new_{}", i),
732 num_docs: 500,
733 });
734 }
735
736 let candidates = policy.find_merges(&segments);
737 assert_eq!(candidates.len(), 1, "should merge the 10 small segments");
738 assert!(
739 !candidates[0].segment_ids.contains(&"large_merged".into()),
740 "large segment must NOT be in the merge group"
741 );
742 assert_eq!(
743 candidates[0].segment_ids.len(),
744 10,
745 "all 10 small segments should be merged"
746 );
747 }
748
749 #[test]
750 fn test_no_merge_policy() {
751 let policy = NoMergePolicy;
752
753 let segments = vec![
754 SegmentInfo {
755 id: "a".into(),
756 num_docs: 100,
757 },
758 SegmentInfo {
759 id: "b".into(),
760 num_docs: 200,
761 },
762 ];
763
764 assert!(policy.find_merges(&segments).is_empty());
765 }
766
767 #[test]
768 fn test_oversized_exclusion() {
769 let policy = TieredMergePolicy {
771 segments_per_tier: 3,
772 max_merged_docs: 1_000_000,
773 oversized_threshold: 0.5,
774 ..Default::default()
775 };
776
777 let mut segments: Vec<_> = (0..4)
779 .map(|i| SegmentInfo {
780 id: format!("small_{}", i),
781 num_docs: 1000,
782 })
783 .collect();
784 segments.push(SegmentInfo {
785 id: "oversized_0".into(),
786 num_docs: 600_000,
787 });
788 segments.push(SegmentInfo {
789 id: "oversized_1".into(),
790 num_docs: 700_000,
791 });
792
793 let candidates = policy.find_merges(&segments);
794 for c in &candidates {
796 assert!(
797 !c.segment_ids.contains(&"oversized_0".into()),
798 "oversized_0 should be excluded"
799 );
800 assert!(
801 !c.segment_ids.contains(&"oversized_1".into()),
802 "oversized_1 should be excluded"
803 );
804 }
805 }
806
807 #[test]
808 fn test_budget_trigger_prevents_unnecessary_merge() {
809 let policy = TieredMergePolicy {
810 segments_per_tier: 10,
811 tier_factor: 10.0,
812 tier_floor: 1000,
813 floor_segment_docs: 1000,
814 budget_trigger: true,
815 ..Default::default()
816 };
817
818 let segments: Vec<_> = (0..5)
822 .map(|i| SegmentInfo {
823 id: format!("seg_{}", i),
824 num_docs: 10_000,
825 })
826 .collect();
827
828 let candidates = policy.find_merges(&segments);
829 assert!(
830 candidates.is_empty(),
831 "should not merge when under budget: {:?}",
832 candidates
833 );
834 }
835
836 #[test]
837 fn test_budget_trigger_allows_merge_when_over_budget() {
838 let policy = TieredMergePolicy {
839 segments_per_tier: 3,
840 tier_factor: 10.0,
841 tier_floor: 1000,
842 floor_segment_docs: 1000,
843 budget_trigger: true,
844 ..Default::default()
845 };
846
847 let segments: Vec<_> = (0..10)
851 .map(|i| SegmentInfo {
852 id: format!("seg_{}", i),
853 num_docs: 1000,
854 })
855 .collect();
856
857 let candidates = policy.find_merges(&segments);
858 assert!(!candidates.is_empty(), "should merge when over budget");
859 }
860
861 #[test]
862 fn test_severe_backlog_requires_more_than_twice_the_budget() {
863 let policy = TieredMergePolicy {
864 segments_per_tier: 3,
865 tier_factor: 10.0,
866 tier_floor: 1000,
867 floor_segment_docs: 1000,
868 budget_trigger: true,
869 ..Default::default()
870 };
871
872 let six: Vec<_> = (0..6)
875 .map(|i| SegmentInfo {
876 id: format!("seg_{i}"),
877 num_docs: 1000,
878 })
879 .collect();
880 assert!(!policy.has_severe_backlog(&six));
881
882 let mut seven = six;
885 seven.push(SegmentInfo {
886 id: "seg_6".into(),
887 num_docs: 1000,
888 });
889 assert!(policy.has_severe_backlog(&seven));
890 }
891
892 #[test]
893 fn test_min_growth_ratio_rejects_wasteful_merge() {
894 let policy = TieredMergePolicy {
895 segments_per_tier: 3,
896 min_growth_ratio: 0.5,
897 max_merge_at_once: 10,
898 ..Default::default()
899 };
900
901 let mut segments = vec![SegmentInfo {
905 id: "big".into(),
906 num_docs: 100_000,
907 }];
908 for i in 0..3 {
909 segments.push(SegmentInfo {
910 id: format!("tiny_{}", i),
911 num_docs: 10,
912 });
913 }
914
915 let candidates = policy.find_merges(&segments);
916 for c in &candidates {
919 if c.segment_ids.contains(&"big".into()) {
920 let total: u64 = c
921 .segment_ids
922 .iter()
923 .map(|id| segments.iter().find(|s| s.id == *id).unwrap().num_docs as u64)
924 .sum();
925 let largest: u64 = c
926 .segment_ids
927 .iter()
928 .map(|id| segments.iter().find(|s| s.id == *id).unwrap().num_docs as u64)
929 .max()
930 .unwrap();
931 assert!(
932 total as f64 >= 1.5 * largest as f64,
933 "merge with 'big' segment violates min_growth_ratio: total={}, largest={}",
934 total,
935 largest
936 );
937 }
938 }
939 }
940
941 #[test]
942 fn test_scored_selection_prefers_balanced_merge() {
943 let policy = TieredMergePolicy {
944 segments_per_tier: 3,
945 max_merge_at_once: 5,
946 scored_selection: true,
947 ..Default::default()
948 };
949
950 let segments = vec![
953 SegmentInfo {
954 id: "unbal_0".into(),
955 num_docs: 100,
956 },
957 SegmentInfo {
958 id: "unbal_1".into(),
959 num_docs: 100,
960 },
961 SegmentInfo {
962 id: "bal_0".into(),
963 num_docs: 1000,
964 },
965 SegmentInfo {
966 id: "bal_1".into(),
967 num_docs: 1100,
968 },
969 SegmentInfo {
970 id: "bal_2".into(),
971 num_docs: 1200,
972 },
973 SegmentInfo {
974 id: "unbal_2".into(),
975 num_docs: 5000,
976 },
977 ];
978
979 let candidates = policy.find_merges(&segments);
980 assert!(!candidates.is_empty(), "should find at least one merge");
981
982 let first = &candidates[0];
984 let has_balanced = first.segment_ids.iter().any(|id| id.starts_with("bal_"));
985 assert!(
986 has_balanced,
987 "scored selection should prefer balanced group, got: {:?}",
988 first.segment_ids
989 );
990 }
991
992 #[test]
993 fn test_large_scale_preset_values() {
994 let p = TieredMergePolicy::large_scale();
995 assert_eq!(p.tier_floor, 50_000);
996 assert_eq!(p.max_merged_docs, 5_000_000);
1001 assert_eq!(p.max_segment_docs, 5_000_000);
1002 assert_eq!(p.floor_segment_docs, 50_000);
1003 assert!(p.budget_trigger);
1004 assert!(p.scored_selection);
1005 assert_eq!(p.segments_per_tier, 10);
1006 assert!((p.min_growth_ratio - 0.5).abs() < f64::EPSILON);
1007 assert!((p.oversized_threshold - 0.5).abs() < f64::EPSILON);
1008 }
1009
1010 #[test]
1011 fn test_bulk_indexing_preset_values() {
1012 let p = TieredMergePolicy::bulk_indexing();
1013 assert_eq!(p.segments_per_tier, 20);
1014 assert_eq!(p.max_merge_at_once, 20);
1015 assert_eq!(p.tier_floor, 100_000);
1016 assert_eq!(p.max_merged_docs, 50_000_000);
1017 assert_eq!(p.max_segment_docs, 50_000_000);
1018 assert_eq!(p.floor_segment_docs, 100_000);
1019 assert!(p.budget_trigger);
1020 assert!(p.scored_selection);
1021 assert!((p.min_growth_ratio - 0.75).abs() < f64::EPSILON);
1022 }
1023
1024 #[test]
1025 fn test_default_max_segment_docs() {
1026 let p = TieredMergePolicy::default();
1027 assert_eq!(p.max_segment_docs, 10_000_000);
1028 assert_eq!(p.max_segment_docs().unwrap(), 10_000_000);
1029 }
1030
1031 #[test]
1032 fn test_max_segment_docs_caps_merge_output() {
1033 let policy = TieredMergePolicy {
1035 segments_per_tier: 3,
1036 max_merge_at_once: 100,
1037 max_merged_docs: 50_000_000,
1038 max_segment_docs: 5_000_000,
1039 ..Default::default()
1040 };
1041
1042 let segments: Vec<_> = (0..10)
1044 .map(|i| SegmentInfo {
1045 id: format!("seg_{}", i),
1046 num_docs: 1_000_000,
1047 })
1048 .collect();
1049
1050 let candidates = policy.find_merges(&segments);
1051 for c in &candidates {
1052 let total: u64 = c
1053 .segment_ids
1054 .iter()
1055 .map(|id| segments.iter().find(|s| s.id == *id).unwrap().num_docs as u64)
1056 .sum();
1057 assert!(
1058 total <= 5_000_000,
1059 "merge total {} exceeds max_segment_docs 5M",
1060 total
1061 );
1062 }
1063 }
1064}