Skip to content

Commit 4281011

Browse files
committed
Handle NaN's for tfidf.
1 parent de6dc72 commit 4281011

1 file changed

Lines changed: 19 additions & 8 deletions

File tree

src/model.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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
6161
const K: f32 = 2.0;
6262
const B: f32 = 0.75;
63-
/// avgdl is passed in so it can be computed once per query rather than once per document.
6463
fn 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
8183
fn 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
8790
fn 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

Comments
 (0)