@@ -5,10 +5,11 @@ use std::{
55use std:: collections:: HashMap ;
66use serde:: { Deserialize , Serialize } ;
77use std:: default:: Default ;
8+
89use super :: lexer:: * ;
910
1011pub trait Model {
11- fn search_query ( & self , query : & [ char ] ) -> Result < Vec < ( PathBuf , f32 ) > , ( ) > ;
12+ fn search_query ( & self , query : & [ char ] , model : & InMemoryModel ) -> Result < Vec < ( PathBuf , f32 ) > , ( ) > ;
1213 fn add_document ( & mut self , path : PathBuf , content : & [ char ] , last_modified : SystemTime ) -> Result < ( ) , ( ) > ;
1314 fn requires_reindexing ( & mut self , path : & Path , last_modified : SystemTime ) -> Result < bool , ( ) > ;
1415}
@@ -19,7 +20,7 @@ pub trait Model {
1920pub type FreqTable = HashMap :: < String , usize > ;
2021
2122/* Answers how frequently a term occurs in all documents.
22- Map of term with frequency of occurence in all corpus of documents.*/
23+ Map of term with frequency of occurence in all corpus of documents. */
2324pub type GlobalTermFreq = HashMap :: < String , usize > ;
2425
2526#[ derive( Serialize , Deserialize ) ]
@@ -37,16 +38,51 @@ pub struct InMemoryModel {
3738 pub docs : Docs ,
3839}
3940
40- fn compute_tf ( term : & str , doc : & Doc ) -> f32 {
41- let n = doc. ft . get ( term) . cloned ( ) . unwrap_or ( 0 ) as f32 ; // Number of times term occured in document
42- let d = doc. count as f32 ; // Total number of terms present in document
43- n / d
41+ fn 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
4444}
4545
4646fn compute_idf ( term : & str , model : & InMemoryModel ) -> f32 {
47- let n = model. docs . len ( ) as f32 ; // Number of documents in corpus
48- // NOTE: Can lead to division by 0 if term is not in Document Corpus, Set Denominator to 1 if turns to 0
49- let d = model. gtf . get ( term) . cloned ( ) . unwrap_or ( 1 ) as f32 ; // Number of times term occured in document corpus
47+ // Number of documents in corpus
48+ let total_docs = model. docs . len ( ) as f32 ;
49+
50+ // Number of documents containing the unique 'term'
51+ let doc_freq = model. docs . values ( ) . filter ( |doc| doc. ft . contains_key ( term) ) . count ( ) as f32 ;
52+
53+ f32:: ln ( ( ( total_docs - doc_freq + 0.5 ) + 1f32 ) / ( doc_freq + 0.5 ) )
54+ }
55+
56+ // K and B are free parameters, usually chosen, in absence of an advanced optimization, as K = [1.2, 2.0] and B = 0.75
57+ const K : f32 = 2.0 ;
58+ const B : f32 = 0.75 ;
59+ fn bm25_score ( query : & Vec < String > , doc : & Doc , model : & InMemoryModel ) -> f32 {
60+ // Ranking documents according to BM25 Algorithm: https://en.wikipedia.org/wiki/Okapi_BM25
61+ let mut score= 0f32 ;
62+ let avgdl = compute_avgdl ( model) ;
63+ let doc_length = doc. count as f32 ;
64+
65+ for term in query {
66+ // Number of times a term occurs in the document
67+ let tf = doc. ft . get ( term) . copied ( ) . unwrap_or ( 0 ) as f32 ;
68+ let idf = compute_idf ( & term, model) ;
69+
70+ let denom = tf + K * ( 1f32 - B + B * doc_length / avgdl) ;
71+ score += idf * tf * ( K + 1f32 ) / denom;
72+ }
73+ score
74+ }
75+
76+ // For TF-IDF Ranking
77+ fn tf ( term : & str , doc : & Doc ) -> f32 {
78+ let n = doc. ft . get ( term) . cloned ( ) . unwrap_or ( 0 ) as f32 ; // Number of times term occured in document
79+ let d = doc. count as f32 ; // Total number of terms present in document
80+ n / d
81+ }
82+ // For TF-IDF Ranking
83+ fn idf ( term : & str , model : & InMemoryModel ) -> f32 {
84+ let n = model. docs . len ( ) as f32 ;
85+ let d = model. gtf . get ( term) . cloned ( ) . unwrap_or ( 1 ) as f32 ;
5086 f32:: log10 ( n / d)
5187}
5288
@@ -57,30 +93,35 @@ impl InMemoryModel {
5793 for term in doc. ft . keys ( ) {
5894 // Update the GlobalTermFrequency table
5995 if let Some ( freq) = self . gtf . get_mut ( term) {
60- * freq -= 1 ;
96+ * freq -= freq . saturating_sub ( 1 ) ;
6197 }
6298 }
6399 }
64100 }
65101}
66102
103+ static USE_BM25 : bool = true ;
67104impl Model for InMemoryModel {
68- fn search_query ( & self , query : & [ char ] ) -> Result < Vec < ( PathBuf , f32 ) > , ( ) > {
69- let mut results = Vec :: new ( ) ;
70- // Cache all the tokens and don't retokenize on each query
105+ fn search_query ( & self , query : & [ char ] , model : & InMemoryModel ) -> Result < Vec < ( PathBuf , f32 ) > , ( ) > {
71106 let tokens = Lexer :: new ( & query) . collect :: < Vec < _ > > ( ) ;
107+
108+ let mut results = Vec :: with_capacity ( self . docs . len ( ) ) ;
72109 for ( path, doc) in & self . docs {
73- let mut rank = 0f32 ;
74- for token in & tokens {
75- // Rank is value of tf-idf => tf * idf
76- rank += compute_tf ( & token, & doc) * compute_idf ( & token, & self ) ;
77- }
110+ let rank = if USE_BM25 {
111+ // BM-25 Ranking
112+ bm25_score ( & tokens, doc, model)
113+ } else {
114+ // TF-IDF Ranking
115+ tokens. iter ( )
116+ . map ( |token| tf ( token, doc) * idf ( token, model) )
117+ . sum ( )
118+ } ;
119+
78120 results. push ( ( path. to_owned ( ) , rank) ) ;
79121 }
80122
81123 // Rank the files in desc order
82- results. sort_by ( |( _, ra) , ( _, rb) | ra. partial_cmp ( rb) . expect ( "Compared with NaN values" ) ) ;
83- results. reverse ( ) ;
124+ results. sort_by ( |( _, ra) , ( _, rb) | rb. partial_cmp ( ra) . expect ( "Compared with NaN values" ) ) ;
84125 Ok ( results)
85126 }
86127
@@ -92,7 +133,7 @@ impl Model for InMemoryModel {
92133 let tokens = Lexer :: new ( & content) . collect :: < Vec < _ > > ( ) ;
93134 let mut ft = FreqTable :: new ( ) ;
94135 for token in & tokens {
95- ft. entry ( token. to_owned ( ) ) . and_modify ( |x| * x += 1 ) . or_insert ( 1 ) ;
136+ ft. entry ( token. clone ( ) ) . and_modify ( |x| * x += 1 ) . or_insert ( 1 ) ;
96137 }
97138
98139 // Total count of terms in FreqTable
@@ -116,4 +157,5 @@ impl Model for InMemoryModel {
116157 }
117158}
118159
160+ // TODO: BM25 is very slow, optimize it
119161// TODO: Implement a efficient sqlite Model with parellel processing support
0 commit comments