@@ -3,6 +3,12 @@ use std::io;
3
3
use common:: json_path_writer:: JSON_END_OF_PATH ;
4
4
use common:: BinarySerializable ;
5
5
use fnv:: FnvHashSet ;
6
+ #[ cfg( feature = "quickwit" ) ]
7
+ use futures_util:: { FutureExt , StreamExt , TryStreamExt } ;
8
+ #[ cfg( feature = "quickwit" ) ]
9
+ use itertools:: Itertools ;
10
+ #[ cfg( feature = "quickwit" ) ]
11
+ use tantivy_fst:: automaton:: { AlwaysMatch , Automaton } ;
6
12
7
13
use crate :: directory:: FileSlice ;
8
14
use crate :: positions:: PositionReader ;
@@ -219,13 +225,18 @@ impl InvertedIndexReader {
219
225
self . termdict . get_async ( term. serialized_value_bytes ( ) ) . await
220
226
}
221
227
222
- async fn get_term_range_async (
223
- & self ,
228
+ async fn get_term_range_async < ' a , A : Automaton + ' a > (
229
+ & ' a self ,
224
230
terms : impl std:: ops:: RangeBounds < Term > ,
231
+ automaton : A ,
225
232
limit : Option < u64 > ,
226
- ) -> io:: Result < impl Iterator < Item = TermInfo > + ' _ > {
233
+ merge_holes_under_bytes : usize ,
234
+ ) -> io:: Result < impl Iterator < Item = TermInfo > + ' a >
235
+ where
236
+ A :: State : Clone ,
237
+ {
227
238
use std:: ops:: Bound ;
228
- let range_builder = self . termdict . range ( ) ;
239
+ let range_builder = self . termdict . search ( automaton ) ;
229
240
let range_builder = match terms. start_bound ( ) {
230
241
Bound :: Included ( bound) => range_builder. ge ( bound. serialized_value_bytes ( ) ) ,
231
242
Bound :: Excluded ( bound) => range_builder. gt ( bound. serialized_value_bytes ( ) ) ,
@@ -242,7 +253,9 @@ impl InvertedIndexReader {
242
253
range_builder
243
254
} ;
244
255
245
- let mut stream = range_builder. into_stream_async ( ) . await ?;
256
+ let mut stream = range_builder
257
+ . into_stream_async_merging_holes ( merge_holes_under_bytes)
258
+ . await ?;
246
259
247
260
let iter = std:: iter:: from_fn ( move || stream. next ( ) . map ( |( _k, v) | v. clone ( ) ) ) ;
248
261
@@ -288,7 +301,9 @@ impl InvertedIndexReader {
288
301
limit : Option < u64 > ,
289
302
with_positions : bool ,
290
303
) -> io:: Result < bool > {
291
- let mut term_info = self . get_term_range_async ( terms, limit) . await ?;
304
+ let mut term_info = self
305
+ . get_term_range_async ( terms, AlwaysMatch , limit, 0 )
306
+ . await ?;
292
307
293
308
let Some ( first_terminfo) = term_info. next ( ) else {
294
309
// no key matches, nothing more to load
@@ -315,6 +330,84 @@ impl InvertedIndexReader {
315
330
Ok ( true )
316
331
}
317
332
333
+ /// Warmup a block postings given a range of `Term`s.
334
+ /// This method is for an advanced usage only.
335
+ ///
336
+ /// returns a boolean, whether a term matching the range was found in the dictionary
337
+ pub async fn warm_postings_automaton <
338
+ A : Automaton + Clone + Send + ' static ,
339
+ E : FnOnce ( Box < dyn FnOnce ( ) -> io:: Result < ( ) > + Send > ) -> F ,
340
+ F : std:: future:: Future < Output = io:: Result < ( ) > > ,
341
+ > (
342
+ & self ,
343
+ automaton : A ,
344
+ // with_positions: bool, at the moment we have no use for it, and supporting it would add
345
+ // complexity to the coalesce
346
+ executor : E ,
347
+ ) -> io:: Result < bool >
348
+ where
349
+ A :: State : Clone ,
350
+ {
351
+ // merge holes under 4MiB, that's how many bytes we can hope to receive during a TTFB from
352
+ // S3 (~80MiB/s, and 50ms latency)
353
+ const MERGE_HOLES_UNDER_BYTES : usize = ( 80 * 1024 * 1024 * 50 ) / 1000 ;
354
+ // we build a first iterator to download everything. Simply calling the function already
355
+ // download everything we need from the sstable, but doesn't start iterating over it.
356
+ let _term_info_iter = self
357
+ . get_term_range_async ( .., automaton. clone ( ) , None , MERGE_HOLES_UNDER_BYTES )
358
+ . await ?;
359
+
360
+ let ( sender, posting_ranges_to_load_stream) = futures_channel:: mpsc:: unbounded ( ) ;
361
+ let termdict = self . termdict . clone ( ) ;
362
+ let cpu_bound_task = move || {
363
+ // then we build a 2nd iterator, this one with no holes, so we don't go through blocks
364
+ // we can't match.
365
+ // This makes the assumption there is a caching layer below us, which gives sync read
366
+ // for free after the initial async access. This might not always be true, but is in
367
+ // Quickwit.
368
+ // We build things from this closure otherwise we get into lifetime issues that can only
369
+ // be solved with self referential strucs. Returning an io::Result from here is a bit
370
+ // more leaky abstraction-wise, but a lot better than the alternative
371
+ let mut stream = termdict. search ( automaton) . into_stream ( ) ?;
372
+
373
+ // we could do without an iterator, but this allows us access to coalesce which simplify
374
+ // things
375
+ let posting_ranges_iter =
376
+ std:: iter:: from_fn ( move || stream. next ( ) . map ( |( _k, v) | v. postings_range . clone ( ) ) ) ;
377
+
378
+ let merged_posting_ranges_iter = posting_ranges_iter. coalesce ( |range1, range2| {
379
+ if range1. end + MERGE_HOLES_UNDER_BYTES >= range2. start {
380
+ Ok ( range1. start ..range2. end )
381
+ } else {
382
+ Err ( ( range1, range2) )
383
+ }
384
+ } ) ;
385
+
386
+ for posting_range in merged_posting_ranges_iter {
387
+ if let Err ( _) = sender. unbounded_send ( posting_range) {
388
+ // this should happen only when search is cancelled
389
+ return Err ( io:: Error :: other ( "failed to send posting range back" ) ) ;
390
+ }
391
+ }
392
+ Ok ( ( ) )
393
+ } ;
394
+ let task_handle = executor ( Box :: new ( cpu_bound_task) ) ;
395
+
396
+ let posting_downloader = posting_ranges_to_load_stream
397
+ . map ( |posting_slice| {
398
+ self . postings_file_slice
399
+ . read_bytes_slice_async ( posting_slice)
400
+ . map ( |result| result. map ( |_slice| ( ) ) )
401
+ } )
402
+ . buffer_unordered ( 5 )
403
+ . try_collect :: < Vec < ( ) > > ( ) ;
404
+
405
+ let ( _, slices_downloaded) =
406
+ futures_util:: future:: try_join ( task_handle, posting_downloader) . await ?;
407
+
408
+ Ok ( !slices_downloaded. is_empty ( ) )
409
+ }
410
+
318
411
/// Warmup the block postings for all terms.
319
412
/// This method is for an advanced usage only.
320
413
///
0 commit comments