Skip to content

Commit 74bf406

Browse files
authored
Merge pull request #17 from vectorlessflow/dev
Dev
2 parents d223792 + d084994 commit 74bf406

10 files changed

Lines changed: 1508 additions & 3 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "vectorless"
3-
version = "0.1.16"
3+
version = "0.1.17"
44
edition = "2024"
55
authors = ["zTgx <beautifularea@gmail.com>"]
66
description = "Hierarchical, reasoning-native document intelligence engine"

src/config/types/storage.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,163 @@ pub struct StrategyConfig {
337337
/// Low similarity threshold for "explore" decision.
338338
#[serde(default = "default_low_similarity_threshold")]
339339
pub low_similarity_threshold: f32,
340+
341+
/// Hybrid strategy configuration (BM25 + LLM refinement).
342+
#[serde(default)]
343+
pub hybrid: HybridStrategyConfig,
344+
345+
/// Cross-document strategy configuration.
346+
#[serde(default)]
347+
pub cross_document: CrossDocumentStrategyConfig,
348+
349+
/// Page-range strategy configuration.
350+
#[serde(default)]
351+
pub page_range: PageRangeStrategyConfig,
352+
}
353+
354+
/// Hybrid strategy configuration (BM25 pre-filter + LLM refinement).
355+
#[derive(Debug, Clone, Serialize, Deserialize)]
356+
pub struct HybridStrategyConfig {
357+
/// Enable hybrid strategy.
358+
#[serde(default = "default_true")]
359+
pub enabled: bool,
360+
361+
/// BM25 pre-filter: keep top N% of candidates.
362+
#[serde(default = "default_pre_filter_ratio")]
363+
pub pre_filter_ratio: f32,
364+
365+
/// Minimum candidates to pass to LLM.
366+
#[serde(default = "default_min_candidates")]
367+
pub min_candidates: usize,
368+
369+
/// Maximum candidates for LLM refinement.
370+
#[serde(default = "default_max_candidates")]
371+
pub max_candidates: usize,
372+
373+
/// BM25 score for auto-accept (skip LLM).
374+
#[serde(default = "default_auto_accept_threshold")]
375+
pub auto_accept_threshold: f32,
376+
377+
/// BM25 score for auto-reject (skip LLM).
378+
#[serde(default = "default_auto_reject_threshold")]
379+
pub auto_reject_threshold: f32,
380+
381+
/// Weight for BM25 score in final scoring.
382+
#[serde(default = "default_bm25_weight")]
383+
pub bm25_weight: f32,
384+
385+
/// Weight for LLM score in final scoring.
386+
#[serde(default = "default_llm_weight")]
387+
pub llm_weight: f32,
388+
}
389+
390+
fn default_true() -> bool { true }
391+
fn default_pre_filter_ratio() -> f32 { 0.3 }
392+
fn default_min_candidates() -> usize { 2 }
393+
fn default_max_candidates() -> usize { 5 }
394+
fn default_auto_accept_threshold() -> f32 { 0.85 }
395+
fn default_auto_reject_threshold() -> f32 { 0.15 }
396+
fn default_bm25_weight() -> f32 { 0.4 }
397+
fn default_llm_weight() -> f32 { 0.6 }
398+
399+
impl Default for HybridStrategyConfig {
400+
fn default() -> Self {
401+
Self {
402+
enabled: true,
403+
pre_filter_ratio: default_pre_filter_ratio(),
404+
min_candidates: default_min_candidates(),
405+
max_candidates: default_max_candidates(),
406+
auto_accept_threshold: default_auto_accept_threshold(),
407+
auto_reject_threshold: default_auto_reject_threshold(),
408+
bm25_weight: default_bm25_weight(),
409+
llm_weight: default_llm_weight(),
410+
}
411+
}
412+
}
413+
414+
/// Cross-document strategy configuration.
415+
#[derive(Debug, Clone, Serialize, Deserialize)]
416+
pub struct CrossDocumentStrategyConfig {
417+
/// Enable cross-document strategy.
418+
#[serde(default = "default_true")]
419+
pub enabled: bool,
420+
421+
/// Maximum documents to search.
422+
#[serde(default = "default_max_documents")]
423+
pub max_documents: usize,
424+
425+
/// Maximum results per document.
426+
#[serde(default = "default_max_results_per_doc")]
427+
pub max_results_per_doc: usize,
428+
429+
/// Maximum total results.
430+
#[serde(default = "default_max_total_results")]
431+
pub max_total_results: usize,
432+
433+
/// Minimum score threshold.
434+
#[serde(default = "default_min_score")]
435+
pub min_score: f32,
436+
437+
/// Merge strategy: TopK, BestPerDocument, WeightedByRelevance.
438+
#[serde(default = "default_merge_strategy")]
439+
pub merge_strategy: String,
440+
441+
/// Search documents in parallel.
442+
#[serde(default = "default_true")]
443+
pub parallel_search: bool,
444+
}
445+
446+
fn default_max_documents() -> usize { 10 }
447+
fn default_max_results_per_doc() -> usize { 3 }
448+
fn default_max_total_results() -> usize { 10 }
449+
fn default_min_score() -> f32 { 0.3 }
450+
fn default_merge_strategy() -> String { "TopK".to_string() }
451+
452+
impl Default for CrossDocumentStrategyConfig {
453+
fn default() -> Self {
454+
Self {
455+
enabled: true,
456+
max_documents: default_max_documents(),
457+
max_results_per_doc: default_max_results_per_doc(),
458+
max_total_results: default_max_total_results(),
459+
min_score: default_min_score(),
460+
merge_strategy: default_merge_strategy(),
461+
parallel_search: true,
462+
}
463+
}
464+
}
465+
466+
/// Page-range strategy configuration.
467+
#[derive(Debug, Clone, Serialize, Deserialize)]
468+
pub struct PageRangeStrategyConfig {
469+
/// Enable page-range strategy.
470+
#[serde(default = "default_true")]
471+
pub enabled: bool,
472+
473+
/// Include nodes that span across the boundary.
474+
#[serde(default = "default_true")]
475+
pub include_boundary_nodes: bool,
476+
477+
/// Expand range by N pages for context.
478+
#[serde(default)]
479+
pub expand_context_pages: usize,
480+
481+
/// Minimum overlap ratio for node inclusion.
482+
#[serde(default = "default_min_overlap_ratio")]
483+
pub min_overlap_ratio: f32,
484+
}
485+
486+
fn default_min_overlap_ratio() -> f32 { 0.1 }
487+
488+
impl Default for PageRangeStrategyConfig {
489+
fn default() -> Self {
490+
Self {
491+
enabled: true,
492+
include_boundary_nodes: true,
493+
expand_context_pages: 0,
494+
min_overlap_ratio: default_min_overlap_ratio(),
495+
}
496+
}
340497
}
341498

342499
fn default_exploration_weight() -> f32 {
@@ -362,6 +519,9 @@ impl Default for StrategyConfig {
362519
similarity_threshold: default_similarity_threshold(),
363520
high_similarity_threshold: default_high_similarity_threshold(),
364521
low_similarity_threshold: default_low_similarity_threshold(),
522+
hybrid: HybridStrategyConfig::default(),
523+
cross_document: CrossDocumentStrategyConfig::default(),
524+
page_range: PageRangeStrategyConfig::default(),
365525
}
366526
}
367527
}
@@ -453,5 +613,37 @@ mod tests {
453613
let config = StrategyConfig::default();
454614
assert!((config.exploration_weight - 1.414).abs() < 0.001);
455615
assert_eq!(config.similarity_threshold, 0.5);
616+
assert!(config.hybrid.enabled);
617+
assert!(config.cross_document.enabled);
618+
assert!(config.page_range.enabled);
619+
}
620+
621+
#[test]
622+
fn test_hybrid_strategy_config_defaults() {
623+
let config = HybridStrategyConfig::default();
624+
assert!(config.enabled);
625+
assert!((config.pre_filter_ratio - 0.3).abs() < f32::EPSILON);
626+
assert_eq!(config.min_candidates, 2);
627+
assert_eq!(config.max_candidates, 5);
628+
assert!((config.auto_accept_threshold - 0.85).abs() < f32::EPSILON);
629+
}
630+
631+
#[test]
632+
fn test_cross_document_strategy_config_defaults() {
633+
let config = CrossDocumentStrategyConfig::default();
634+
assert!(config.enabled);
635+
assert_eq!(config.max_documents, 10);
636+
assert_eq!(config.max_results_per_doc, 3);
637+
assert_eq!(config.merge_strategy, "TopK");
638+
assert!(config.parallel_search);
639+
}
640+
641+
#[test]
642+
fn test_page_range_strategy_config_defaults() {
643+
let config = PageRangeStrategyConfig::default();
644+
assert!(config.enabled);
645+
assert!(config.include_boundary_nodes);
646+
assert_eq!(config.expand_context_pages, 0);
647+
assert!((config.min_overlap_ratio - 0.1).abs() < f32::EPSILON);
456648
}
457649
}

src/retrieval/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ pub use stages::{AnalyzeStage, EvaluateStage, PlanStage, SearchStage};
8989

9090
// Strategy exports
9191
pub use strategy::{
92-
KeywordStrategy, LlmStrategy, RetrievalStrategy, SemanticStrategy, StrategyCapabilities,
92+
CrossDocumentConfig, CrossDocumentStrategy, DocumentEntry, DocumentId, DocumentResult,
93+
HybridConfig, HybridStrategy, KeywordStrategy, LlmStrategy, MergeStrategy,
94+
PageRange, PageRangeConfig, PageRangeStrategy, RetrievalStrategy, SemanticStrategy,
95+
StrategyCapabilities, StrategyCost,
9396
};
9497

9598
// Search exports

src/retrieval/stages/search.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ use crate::retrieval::pipeline::{
2121
use crate::retrieval::search::{
2222
BeamSearch, GreedySearch, SearchConfig as SearchAlgConfig, SearchTree,
2323
};
24-
use crate::retrieval::strategy::{KeywordStrategy, LlmStrategy, RetrievalStrategy};
24+
use crate::retrieval::strategy::{
25+
HybridConfig, HybridStrategy, KeywordStrategy, LlmStrategy, RetrievalStrategy,
26+
};
2527
use crate::retrieval::types::StrategyPreference;
2628

2729
/// Search Stage - executes tree search with optional Pilot guidance.
@@ -52,6 +54,7 @@ pub struct SearchStage {
5254
keyword_strategy: KeywordStrategy,
5355
llm_strategy: Option<Arc<LlmStrategy>>,
5456
semantic_strategy: Option<Arc<dyn RetrievalStrategy>>,
57+
hybrid_strategy: Option<Arc<dyn RetrievalStrategy>>,
5558
/// Pilot for navigation guidance (optional).
5659
pilot: Option<Arc<dyn Pilot>>,
5760
}
@@ -69,6 +72,7 @@ impl SearchStage {
6972
keyword_strategy: KeywordStrategy::new(),
7073
llm_strategy: None,
7174
semantic_strategy: None,
75+
hybrid_strategy: None,
7276
pilot: None,
7377
}
7478
}
@@ -95,6 +99,26 @@ impl SearchStage {
9599
self
96100
}
97101

102+
/// Add hybrid strategy (BM25 + LLM refinement).
103+
///
104+
/// If no LLM strategy is set, creates one from the provided LLM strategy.
105+
pub fn with_hybrid_strategy(mut self, strategy: Arc<dyn RetrievalStrategy>) -> Self {
106+
self.hybrid_strategy = Some(strategy);
107+
self
108+
}
109+
110+
/// Configure hybrid strategy with custom config using the LLM strategy.
111+
pub fn with_hybrid_config(mut self, config: HybridConfig) -> Self {
112+
if let Some(ref llm) = self.llm_strategy {
113+
// Clone the LlmStrategy and box it
114+
let llm_boxed: Box<dyn RetrievalStrategy> = Box::new((**llm).clone());
115+
self.hybrid_strategy = Some(Arc::new(
116+
HybridStrategy::new(llm_boxed).with_config(config)
117+
));
118+
}
119+
self
120+
}
121+
98122
/// Check if Pilot is available and active.
99123
pub fn has_pilot(&self) -> bool {
100124
self.pilot.as_ref().map(|p| p.is_active()).unwrap_or(false)
@@ -127,6 +151,29 @@ impl SearchStage {
127151
Arc::new(self.keyword_strategy.clone())
128152
}
129153
}
154+
StrategyPreference::ForceHybrid => {
155+
if let Some(ref strategy) = self.hybrid_strategy {
156+
info!("Using Hybrid strategy");
157+
strategy.clone()
158+
} else if let Some(ref llm) = self.llm_strategy {
159+
info!("Using Hybrid strategy (auto-created from LLM)");
160+
let llm_boxed: Box<dyn RetrievalStrategy> = Box::new((**llm).clone());
161+
Arc::new(HybridStrategy::new(llm_boxed))
162+
} else {
163+
warn!("Hybrid strategy requested but no LLM available, falling back to Keyword");
164+
Arc::new(self.keyword_strategy.clone())
165+
}
166+
}
167+
StrategyPreference::ForceCrossDocument | StrategyPreference::ForcePageRange => {
168+
// These require special setup, fall back to hybrid or keyword
169+
if let Some(ref strategy) = self.hybrid_strategy {
170+
info!("Using Hybrid strategy as fallback for {:?})", preference);
171+
strategy.clone()
172+
} else {
173+
warn!("{:?} requires special configuration, falling back to Keyword", preference);
174+
Arc::new(self.keyword_strategy.clone())
175+
}
176+
}
130177
StrategyPreference::Auto => {
131178
// Default to keyword, let plan stage decide
132179
Arc::new(self.keyword_strategy.clone())

0 commit comments

Comments
 (0)