@@ -34,13 +34,17 @@ pub type Docs = HashMap::<PathBuf, Doc>;
3434
3535#[ derive( Default , Deserialize , Serialize ) ]
3636pub struct InMemoryModel {
37- pub gtf : GlobalTermFreq ,
38- pub docs : Docs ,
37+ pub gtf : GlobalTermFreq ,
38+ pub docs : Docs ,
39+ /// Cached sum of all doc.count values. Kept in sync by add_document /
40+ /// remove_document so that avgdl can be computed in O(1) at query time.
41+ pub total_tokens : usize ,
3942}
4043
44+ /// O(1) — uses the cached total_tokens field instead of iterating all docs.
4145fn compute_avgdl ( model : & InMemoryModel ) -> f32 {
42- let total : usize = model. docs . values ( ) . map ( |doc| doc . count ) . sum ( ) ;
43- total as f32 / model. docs . len ( ) as f32
46+ if model. docs . is_empty ( ) { return 0.0 ; }
47+ model . total_tokens as f32 / model. docs . len ( ) as f32
4448}
4549
4650fn compute_idf ( term : & str , model : & InMemoryModel ) -> f32 {
@@ -56,16 +60,16 @@ fn compute_idf(term: &str, model: &InMemoryModel) -> f32 {
5660// K and B are free parameters, usually chosen, in absence of an advanced optimization, as K = [1.2, 2.0] and B = 0.75
5761const K : f32 = 2.0 ;
5862const B : f32 = 0.75 ;
59- fn bm25_score ( query : & Vec < String > , doc : & Doc , model : & InMemoryModel ) -> f32 {
63+ /// avgdl is passed in so it can be computed once per query rather than once per document.
64+ fn bm25_score ( query : & Vec < String > , doc : & Doc , model : & InMemoryModel , avgdl : f32 ) -> f32 {
6065 // Ranking documents according to BM25 Algorithm: https://en.wikipedia.org/wiki/Okapi_BM25
61- let mut score= 0f32 ;
62- let avgdl = compute_avgdl ( model) ;
66+ let mut score = 0f32 ;
6367 let doc_length = doc. count as f32 ;
6468
6569 for term in query {
6670 // Number of times a term occurs in the document
6771 let tf = doc. ft . get ( term) . copied ( ) . unwrap_or ( 0 ) as f32 ;
68- let idf = compute_idf ( & term, model) ;
72+ let idf = compute_idf ( term, model) ;
6973
7074 let denom = tf + K * ( 1f32 - B + B * doc_length / avgdl) ;
7175 score += idf * tf * ( K + 1f32 ) / denom;
@@ -88,8 +92,9 @@ fn idf(term: &str, model: &InMemoryModel) -> f32 {
8892
8993impl InMemoryModel {
9094 fn remove_document ( & mut self , file_path : & Path ) {
91- // Remove the doc from docs
9295 if let Some ( doc) = self . docs . remove ( file_path) {
96+ // Keep the cached total in sync
97+ self . total_tokens = self . total_tokens . saturating_sub ( doc. count ) ;
9398 for term in doc. ft . keys ( ) {
9499 // Update the GlobalTermFrequency table
95100 if let Some ( freq) = self . gtf . get_mut ( term) {
@@ -103,13 +108,16 @@ impl InMemoryModel {
103108use crate :: RankMethod ;
104109impl Model for InMemoryModel {
105110 fn search_query ( & self , query : & [ char ] , model : & InMemoryModel , rank_method : RankMethod ) -> Result < Vec < ( PathBuf , f32 ) > , ( ) > {
106- let tokens = Lexer :: new ( & query) . collect :: < Vec < _ > > ( ) ;
107-
111+ let tokens = Lexer :: new ( query) . collect :: < Vec < _ > > ( ) ;
112+
113+ // Compute avgdl once per query (O(1) with cached total_tokens).
114+ let avgdl = compute_avgdl ( self ) ;
115+
108116 let mut results = Vec :: with_capacity ( self . docs . len ( ) ) ;
109117 for ( path, doc) in & self . docs {
110118 let rank = if rank_method == RankMethod :: Bm25 {
111- // BM-25 Ranking
112- bm25_score ( & tokens, doc, model)
119+ // BM-25 Ranking — avgdl already computed above, not per-doc
120+ bm25_score ( & tokens, doc, model, avgdl )
113121 } else {
114122 // TF-IDF Ranking
115123 tokens. iter ( )
@@ -136,16 +144,19 @@ impl Model for InMemoryModel {
136144 ft. entry ( token. clone ( ) ) . and_modify ( |x| * x += 1 ) . or_insert ( 1 ) ;
137145 }
138146
139- // Total count of terms in FreqTable
140- let term_count = ft. iter ( ) . map ( | ( _ , c ) | * c ) . sum ( ) ;
147+ // Total count of terms in FreqTable
148+ let term_count: usize = ft. values ( ) . sum ( ) ;
141149
142150 // Update global term frequency
143151 for term in ft. keys ( ) {
144152 self . gtf . entry ( term. to_owned ( ) ) . and_modify ( |x| * x += 1 ) . or_insert ( 1 ) ;
145153 }
146154
155+ // Keep the cached total in sync
156+ self . total_tokens += term_count;
157+
147158 // Update the Docs table
148- self . docs . insert ( file_path, Doc { count : term_count, ft : ft , last_modified : last_modified } ) ;
159+ self . docs . insert ( file_path, Doc { count : term_count, ft, last_modified } ) ;
149160 Ok ( ( ) )
150161 }
151162
@@ -157,5 +168,4 @@ impl Model for InMemoryModel {
157168 }
158169}
159170
160- // TODO: BM25 is very slow, optimize it
161- // TODO: Implement a efficient sqlite Model with parellel processing support
171+ // TODO: Implement an efficient sqlite Model with parallel processing support
0 commit comments