1+ use std:: cell:: RefCell ;
2+ use std:: num:: NonZeroUsize ;
13use std:: sync:: Arc ;
24
5+ use lru:: LruCache ;
6+
37use crate :: fieldnorm:: FieldNormReader ;
48use crate :: query:: Explanation ;
59use crate :: schema:: Field ;
@@ -59,7 +63,9 @@ fn cached_tf_component(fieldnorm: u32, average_fieldnorm: Score) -> Score {
5963 K1 * ( 1.0 - B + B * fieldnorm as Score / average_fieldnorm)
6064}
6165
62- fn compute_tf_cache ( average_fieldnorm : Score ) -> Arc < [ Score ; 256 ] > {
66+ const BM25_TF_CACHE_CAPACITY : usize = 64 ;
67+
68+ fn compute_tf_cache_uncached ( average_fieldnorm : Score ) -> Arc < [ Score ; 256 ] > {
6369 let mut cache: [ Score ; 256 ] = [ 0.0 ; 256 ] ;
6470 for ( fieldnorm_id, cache_mut) in cache. iter_mut ( ) . enumerate ( ) {
6571 let fieldnorm = FieldNormReader :: id_to_fieldnorm ( fieldnorm_id as u8 ) ;
@@ -68,6 +74,36 @@ fn compute_tf_cache(average_fieldnorm: Score) -> Arc<[Score; 256]> {
6874 Arc :: new ( cache)
6975}
7076
77+ thread_local ! {
78+ static TF_CACHES : RefCell <LruCache <u32 , Arc <[ Score ; 256 ] >>> = RefCell :: new( LruCache :: new(
79+ NonZeroUsize :: new( BM25_TF_CACHE_CAPACITY ) . unwrap( ) ,
80+ ) ) ;
81+ }
82+
83+ /// The cache is shared across all [Bm25Weight] with the same average fieldnorm on the same thread.
84+ /// It is stored in a thread local LRU cache.
85+ ///
86+ /// On one query all terms on the same field will share the same average fieldnorm, and thus the same cache.
87+ /// This will lower cache pressure.
88+ ///
89+ /// Even between queries (on the same thread), the cache will be reused, which allows the cache to
90+ /// better learn the memory address of the cache and access patterns.
91+ ///
92+ /// Thread local is used in order to be defensive about potential contention on the cache.
93+ fn compute_tf_cache ( average_fieldnorm : Score ) -> Arc < [ Score ; 256 ] > {
94+ let cache_key = average_fieldnorm. to_bits ( ) ;
95+ TF_CACHES . with ( |cache_by_average_fieldnorm| {
96+ let mut cache_by_average_fieldnorm = cache_by_average_fieldnorm. borrow_mut ( ) ;
97+ if let Some ( cache) = cache_by_average_fieldnorm. get ( & cache_key) {
98+ return cache. clone ( ) ;
99+ }
100+
101+ let cache = compute_tf_cache_uncached ( average_fieldnorm) ;
102+ cache_by_average_fieldnorm. put ( cache_key, cache. clone ( ) ) ;
103+ cache
104+ } )
105+ }
106+
71107/// A struct used for computing BM25 scores.
72108#[ derive( Clone ) ]
73109pub struct Bm25Weight {
@@ -229,12 +265,20 @@ impl Bm25Weight {
229265#[ cfg( test) ]
230266mod tests {
231267
232- use super :: idf;
268+ use super :: { idf, Bm25Weight } ;
233269 use crate :: { assert_nearly_equals, Score } ;
234270
235271 #[ test]
236272 fn test_idf ( ) {
237273 let score: Score = 2.0 ;
238274 assert_nearly_equals ! ( idf( 1 , 2 ) , score. ln( ) ) ;
239275 }
276+
277+ #[ test]
278+ fn test_bm25_tf_cache_is_shared_for_same_average_fieldnorm ( ) {
279+ let weight1 = Bm25Weight :: for_one_term ( 1 , 10 , 3.0 ) ;
280+ let weight2 = Bm25Weight :: for_one_term ( 2 , 10 , 3.0 ) ;
281+
282+ assert ! ( std:: sync:: Arc :: ptr_eq( & weight1. cache, & weight2. cache) ) ;
283+ }
240284}
0 commit comments