@@ -11,27 +11,33 @@ use text_splitter::{ChunkConfig, TextSplitter};
1111// use text_splitter::{ChunkConfig, TextSplitter};
1212use tokenizers:: Tokenizer ;
1313
14- fn median < T > ( data : & [ T ] ) -> T
14+ fn median < T > ( data : & [ T ] ) -> Option < T >
1515where
1616 T : Copy + PartialOrd + std:: ops:: Add < Output = T > + std:: ops:: Div < Output = T > + From < u8 > ,
1717{
18- assert ! ( !data. is_empty( ) , "median requires at least one data point" ) ;
18+ if data. is_empty ( ) {
19+ return None ;
20+ }
1921 let mut sorted = data. to_vec ( ) ;
20- sorted. sort_by ( |a, b| a. partial_cmp ( b) . unwrap ( ) ) ;
22+ // Use `unwrap_or` to handle NaN values (treat them as equal) instead of panicking.
23+ sorted. sort_by ( |a, b| a. partial_cmp ( b) . unwrap_or ( std:: cmp:: Ordering :: Equal ) ) ;
2124 let mid = sorted. len ( ) / 2 ;
22- if sorted. len ( ) % 2 == 0 {
25+ let result = if sorted. len ( ) % 2 == 0 {
2326 ( sorted[ mid - 1 ] + sorted[ mid] ) / T :: from ( 2u8 )
2427 } else {
2528 sorted[ mid]
26- }
29+ } ;
30+ Some ( result)
2731}
2832
29- fn std_dev ( data : & [ f32 ] ) -> f32 {
30- assert ! ( data. len( ) > 1 , "standard deviation requires at least two data points" ) ;
33+ fn std_dev ( data : & [ f32 ] ) -> Option < f32 > {
34+ if data. len ( ) < 2 {
35+ return None ;
36+ }
3137 let n = data. len ( ) as f32 ;
3238 let mean = data. iter ( ) . sum :: < f32 > ( ) / n;
3339 let variance = data. iter ( ) . map ( |x| ( x - mean) . powi ( 2 ) ) . sum :: < f32 > ( ) / n;
34- variance. sqrt ( )
40+ Some ( variance. sqrt ( ) )
3541}
3642
3743pub struct StatisticalChunker {
@@ -255,7 +261,14 @@ impl StatisticalChunker {
255261 raw_similarities
256262 }
257263
258- fn _find_optimal_threshold ( & self , batch_splits : & [ & str ] , similarities : & Vec < f32 > ) -> f32 {
264+ fn _find_optimal_threshold ( & self , batch_splits : & [ & str ] , similarities : & [ f32 ] ) -> f32 {
265+ // Guard: we need at least 2 similarity scores to compute median + std_dev.
266+ // With 0 scores there are no chunk boundaries to find; return a neutral threshold.
267+ // With 1 score there is no variance to measure; use that single score directly.
268+ if similarities. len ( ) < 2 {
269+ return similarities. first ( ) . copied ( ) . unwrap_or ( 0.5 ) ;
270+ }
271+
259272 let tokens = self
260273 . tokenizer
261274 . encode_batch ( batch_splits. to_vec ( ) , true )
@@ -274,8 +287,10 @@ impl StatisticalChunker {
274287 . collect :: < Vec < _ > > ( ) ;
275288
276289 // analyze the distribution of similarity scores to set initial bounds
277- let median_score = median ( similarities) ;
278- let std_dev = std_dev ( similarities) ;
290+ // Both median() and std_dev() return Option; the len() >= 2 guard above
291+ // ensures they always return Some(_) here.
292+ let median_score = median ( similarities) . unwrap_or ( 0.5 ) ;
293+ let std_dev = std_dev ( similarities) . unwrap_or ( 0.0 ) ;
279294
280295 // set initial bounds based on median and standard deviation
281296 let mut low = f32:: max ( 0.0 , median_score - std_dev) ;
@@ -300,7 +315,7 @@ impl StatisticalChunker {
300315 . map ( |( start, end) | cumulative_token_counts[ * end] - cumulative_token_counts[ * start] )
301316 . collect ( ) ;
302317
303- median_tokens = median ( & split_token_counts) ;
318+ median_tokens = median ( & split_token_counts) . unwrap_or ( 0 ) ;
304319
305320 if self . min_split_tokens - self . split_token_tolerance <= median_tokens
306321 && median_tokens <= self . max_split_tokens + self . split_token_tolerance
@@ -315,7 +330,7 @@ impl StatisticalChunker {
315330 }
316331 calculated_threshold
317332 }
318- fn _find_split_indices ( & self , similarities : & Vec < f32 > , threshold : f32 ) -> Vec < usize > {
333+ fn _find_split_indices ( & self , similarities : & [ f32 ] , threshold : f32 ) -> Vec < usize > {
319334 let mut split_indices = Vec :: new ( ) ;
320335 for ( idx, score) in enumerate ( similarities) {
321336 if * score < threshold {
0 commit comments