@@ -60,9 +60,9 @@ fn compute_idf(term: &str, model: &InMemoryModel) -> f32 {
6060// K and B are free parameters, usually chosen, in absence of an advanced optimization, as K = [1.2, 2.0] and B = 0.75
6161const K : f32 = 2.0 ;
6262const B : f32 = 0.75 ;
63- /// avgdl is passed in so it can be computed once per query rather than once per document.
6463fn bm25_score ( query : & Vec < String > , doc : & Doc , model : & InMemoryModel , avgdl : f32 ) -> f32 {
6564 // Ranking documents according to BM25 Algorithm: https://en.wikipedia.org/wiki/Okapi_BM25
65+ if avgdl == 0.0 { return 0.0 ; } // guard: no tokens in corpus means undefined avgdl
6666 let mut score = 0f32 ;
6767 let doc_length = doc. count as f32 ;
6868
@@ -72,21 +72,25 @@ fn bm25_score(query: &Vec<String>, doc: &Doc, model: &InMemoryModel, avgdl: f32)
7272 let idf = compute_idf ( term, model) ;
7373
7474 let denom = tf + K * ( 1f32 - B + B * doc_length / avgdl) ;
75+ // denom can only be zero if tf==0 and avgdl is infinite, guard to be safe
76+ if denom == 0.0 { continue ; }
7577 score += idf * tf * ( K + 1f32 ) / denom;
7678 }
7779 score
7880}
7981
80- // For TF-IDF Ranking
82+ // For TF-IDF Ranking
8183fn tf ( term : & str , doc : & Doc ) -> f32 {
82- let n = doc. ft . get ( term) . cloned ( ) . unwrap_or ( 0 ) as f32 ; // Number of times term occured in document
83- let d = doc. count as f32 ; // Total number of terms present in document
84- n / d
84+ if doc. count == 0 { return 0.0 ; } // guard: doc had no surviving tokens
85+ let n = doc. ft . get ( term) . cloned ( ) . unwrap_or ( 0 ) as f32 ; // occurrences in this doc
86+ let d = doc. count as f32 ; // total tokens in this doc
87+ n / d
8588}
8689// For TF-IDF Ranking
8790fn idf ( term : & str , model : & InMemoryModel ) -> f32 {
88- let n = model. docs . len ( ) as f32 ; // total docs in corpus
89- let d = model. gtf . get ( term) . cloned ( ) . unwrap_or ( 1 ) as f32 ; // number of time the terms has appeared in the entire corpus
91+ let n = model. docs . len ( ) as f32 ; // total docs in corpus
92+ if n == 0.0 { return 0.0 ; } // guard: empty corpus → no meaningful IDF
93+ let d = model. gtf . get ( term) . cloned ( ) . unwrap_or ( 1 ) as f32 ; // docs containing term
9094 f32:: log10 ( n / d)
9195}
9296
@@ -129,7 +133,8 @@ impl Model for InMemoryModel {
129133 }
130134
131135 // Rank the files in desc order
132- results. sort_by ( |( _, ra) , ( _, rb) | rb. partial_cmp ( ra) . expect ( "Compared with NaN values" ) ) ;
136+ // partial_cmp returns None only for NaN; treat NaN as equal rather than panicking.
137+ results. sort_by ( |( _, ra) , ( _, rb) | rb. partial_cmp ( ra) . unwrap_or ( std:: cmp:: Ordering :: Equal ) ) ;
133138 Ok ( results)
134139 }
135140
@@ -147,6 +152,12 @@ impl Model for InMemoryModel {
147152 // Total count of terms in FreqTable
148153 let term_count: usize = ft. values ( ) . sum ( ) ;
149154
155+ // Skip documents with no surviving tokens (e.g. all content was stop words).
156+ // Indexing them would give doc.count=0, causing tf()=0/0=NaN at query time.
157+ if term_count == 0 {
158+ return Ok ( ( ) ) ;
159+ }
160+
150161 // Update global term frequency
151162 for term in ft. keys ( ) {
152163 self . gtf . entry ( term. to_owned ( ) ) . and_modify ( |x| * x += 1 ) . or_insert ( 1 ) ;
0 commit comments