Skip to content

Commit 1d09811

Browse files
eeeebbbbrrrrstuhood
authored andcommitted
Merge changes (#47, #48, #71, #74)
* perf: remove general overhead during segment merging (#47) * changes to make merging work (#48) * fix: Use smaller buffers during merging (#71) * chore: Use smaller merge buffers. (#74)
1 parent cf79ba0 commit 1d09811

11 files changed

Lines changed: 244 additions & 37 deletions

File tree

common/src/buffered_file_slice.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::cell::RefCell;
2+
use std::cmp::min;
3+
use std::io;
4+
use std::ops::Range;
5+
6+
use super::file_slice::FileSlice;
7+
use super::{HasLen, OwnedBytes};
8+
9+
const DEFAULT_BUFFER_MAX_SIZE: usize = 512 * 1024; // 512K
10+
11+
/// A buffered reader for a FileSlice.
12+
///
13+
/// Reads the underlying `FileSlice` in large, sequential chunks to amortize
14+
/// the cost of `read_bytes` calls, while keeping peak memory usage under control.
15+
///
16+
/// TODO: Rather than wrapping a `FileSlice` in buffering, it will usually be better to adjust a
17+
/// `FileHandle` to directly handle buffering itself.
18+
/// TODO: See: https://github.com/paradedb/paradedb/issues/3374
19+
pub struct BufferedFileSlice {
20+
file_slice: FileSlice,
21+
buffer: RefCell<OwnedBytes>,
22+
buffer_range: RefCell<Range<u64>>,
23+
buffer_max_size: usize,
24+
}
25+
26+
impl BufferedFileSlice {
27+
/// Creates a new `BufferedFileSlice`.
28+
///
29+
/// The `buffer_max_size` is the amount of data that will be read from the
30+
/// `FileSlice` on a buffer miss.
31+
pub fn new(file_slice: FileSlice, buffer_max_size: usize) -> Self {
32+
Self {
33+
file_slice,
34+
buffer: RefCell::new(OwnedBytes::empty()),
35+
buffer_range: RefCell::new(0..0),
36+
buffer_max_size,
37+
}
38+
}
39+
40+
/// Creates a new `BufferedFileSlice` with a default buffer max size.
41+
pub fn new_with_default_buffer_size(file_slice: FileSlice) -> Self {
42+
Self::new(file_slice, DEFAULT_BUFFER_MAX_SIZE)
43+
}
44+
45+
/// Creates an empty `BufferedFileSlice`.
46+
pub fn empty() -> Self {
47+
Self::new(FileSlice::empty(), 0)
48+
}
49+
50+
/// Returns an `OwnedBytes` corresponding to the given `required_range`.
51+
///
52+
/// If the requested range is not in the buffer, this will trigger a read
53+
/// from the underlying `FileSlice`.
54+
///
55+
/// If the requested range is larger than the buffer_max_size, it will be read directly from the
56+
/// source without buffering.
57+
///
58+
/// # Errors
59+
///
60+
/// Returns an `io::Error` if the underlying read fails or the range is
61+
/// out of bounds.
62+
pub fn get_bytes(&self, required_range: Range<u64>) -> io::Result<OwnedBytes> {
63+
let buffer_range = self.buffer_range.borrow();
64+
65+
// Cache miss condition: the required range is not fully contained in the current buffer.
66+
if required_range.start < buffer_range.start || required_range.end > buffer_range.end {
67+
drop(buffer_range); // release borrow before mutating
68+
69+
if required_range.end > self.file_slice.len() as u64 {
70+
return Err(io::Error::new(
71+
io::ErrorKind::UnexpectedEof,
72+
"Requested range extends beyond the end of the file slice.",
73+
));
74+
}
75+
76+
if (required_range.end - required_range.start) as usize > self.buffer_max_size {
77+
// This read is larger than our buffer max size.
78+
// Read it directly and bypass the buffer to avoid churning.
79+
return self
80+
.file_slice
81+
.read_bytes_slice(required_range.start as usize..required_range.end as usize);
82+
}
83+
84+
let new_buffer_start = required_range.start;
85+
let new_buffer_end = min(
86+
new_buffer_start + self.buffer_max_size as u64,
87+
self.file_slice.len() as u64,
88+
);
89+
let read_range = new_buffer_start..new_buffer_end;
90+
91+
let new_buffer = self
92+
.file_slice
93+
.read_bytes_slice(read_range.start as usize..read_range.end as usize)?;
94+
95+
self.buffer.replace(new_buffer);
96+
self.buffer_range.replace(read_range);
97+
}
98+
99+
// Now the data is guaranteed to be in the buffer.
100+
let buffer = self.buffer.borrow();
101+
let buffer_range = self.buffer_range.borrow();
102+
let local_start = (required_range.start - buffer_range.start) as usize;
103+
let local_end = (required_range.end - buffer_range.start) as usize;
104+
Ok(buffer.slice(local_start..local_end))
105+
}
106+
}

common/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub use byteorder::LittleEndian as Endianness;
66

77
mod bitset;
88
pub mod bounds;
9+
pub mod buffered_file_slice;
910
mod byte_count;
1011
mod datetime;
1112
pub mod file_slice;

runtests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#! /bin/bash
22

3-
cargo +stable nextest run --features mmap,stopwords,lz4-compression,zstd-compression,failpoints --verbose --workspace
3+
cargo +stable nextest run --features quickwit,mmap,stopwords,lz4-compression,zstd-compression,failpoints --verbose --workspace

src/directory/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod composite_file;
1919
use std::io::BufWriter;
2020
use std::path::PathBuf;
2121

22+
pub use common::buffered_file_slice::BufferedFileSlice;
2223
pub use common::file_slice::{FileHandle, FileSlice};
2324
pub use common::{AntiCallToken, OwnedBytes, TerminatingWrite};
2425

src/index/merge_optimized_inverted_index_reader.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use std::io;
22

3-
use common::OwnedBytes;
4-
5-
use crate::directory::FileSlice;
3+
use crate::directory::{BufferedFileSlice, FileSlice};
64
use crate::positions::PositionReader;
75
use crate::postings::{BlockSegmentPostings, SegmentPostings, TermInfo};
86
use crate::schema::IndexRecordOption;
@@ -11,16 +9,17 @@ use crate::termdict::TermDictionary;
119
/// The inverted index reader is in charge of accessing
1210
/// the inverted index associated with a specific field.
1311
///
14-
/// This is optimized for merging in that it full reads
15-
/// the postings and positions files into memory when opened.
16-
/// This eliminates all disk I/O to these files during merging.
12+
/// This is optimized for merging in that it uses a buffered reader
13+
/// for the postings and positions files.
14+
/// This eliminates most disk I/O to these files during merging, without
15+
/// reading the entire file into memory at once.
1716
///
1817
/// NB: This is a copy/paste from [`InvertedIndexReader`] and trimmed
1918
/// down to only include the methods required by the merge process.
2019
pub(crate) struct MergeOptimizedInvertedIndexReader {
2120
termdict: TermDictionary,
22-
postings_bytes: OwnedBytes,
23-
positions_bytes: OwnedBytes,
21+
postings_reader: BufferedFileSlice,
22+
positions_reader: BufferedFileSlice,
2423
record_option: IndexRecordOption,
2524
}
2625

@@ -34,8 +33,8 @@ impl MergeOptimizedInvertedIndexReader {
3433
let (_, postings_body) = postings_file_slice.split(8);
3534
Ok(MergeOptimizedInvertedIndexReader {
3635
termdict,
37-
postings_bytes: postings_body.read_bytes()?,
38-
positions_bytes: positions_file_slice.read_bytes()?,
36+
postings_reader: BufferedFileSlice::new_with_default_buffer_size(postings_body),
37+
positions_reader: BufferedFileSlice::new_with_default_buffer_size(positions_file_slice),
3938
record_option,
4039
})
4140
}
@@ -45,8 +44,8 @@ impl MergeOptimizedInvertedIndexReader {
4544
pub fn empty(record_option: IndexRecordOption) -> MergeOptimizedInvertedIndexReader {
4645
MergeOptimizedInvertedIndexReader {
4746
termdict: TermDictionary::empty(),
48-
postings_bytes: FileSlice::empty().read_bytes().unwrap(),
49-
positions_bytes: FileSlice::empty().read_bytes().unwrap(),
47+
postings_reader: BufferedFileSlice::empty(),
48+
positions_reader: BufferedFileSlice::empty(),
5049
record_option,
5150
}
5251
}
@@ -65,7 +64,9 @@ impl MergeOptimizedInvertedIndexReader {
6564
term_info: &TermInfo,
6665
requested_option: IndexRecordOption,
6766
) -> io::Result<BlockSegmentPostings> {
68-
let postings_data = self.postings_bytes.slice(term_info.postings_range.clone());
67+
let postings_data = self.postings_reader.get_bytes(
68+
term_info.postings_range.start as u64..term_info.postings_range.end as u64,
69+
)?;
6970
BlockSegmentPostings::open(
7071
term_info.doc_freq,
7172
postings_data,
@@ -88,9 +89,9 @@ impl MergeOptimizedInvertedIndexReader {
8889
let block_postings = self.read_block_postings_from_terminfo(term_info, option)?;
8990
let position_reader = {
9091
if option.has_positions() {
91-
let positions_data = self
92-
.positions_bytes
93-
.slice(term_info.positions_range.clone());
92+
let positions_data = self.positions_reader.get_bytes(
93+
term_info.positions_range.start as u64..term_info.positions_range.end as u64,
94+
)?;
9495
let position_reader = PositionReader::open(positions_data)?;
9596
Some(position_reader)
9697
} else {

src/indexer/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ mod log_merge_policy;
1818
mod merge_index_test;
1919
mod merge_operation;
2020
pub(crate) mod merge_policy;
21-
pub(crate) mod merger;
21+
pub mod merger;
2222
pub(crate) mod operation;
2323
pub(crate) mod prepared_commit;
2424
mod segment_entry;
2525
mod segment_manager;
2626
mod segment_register;
27-
pub(crate) mod segment_serializer;
27+
pub mod segment_serializer;
2828
pub(crate) mod segment_updater;
2929
pub(crate) mod segment_writer;
3030
pub(crate) mod single_segment_index_writer;

src/positions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
//! * *VIntPosDeltas* := *VIntPosDelta*^(*P* % 128).
2929
//!
3030
//! The skip widths encoded separately makes it easy and fast to rapidly skip over n positions.
31+
3132
mod reader;
3233
mod serializer;
3334

src/positions/serializer.rs

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,109 @@ impl<W: io::Write> PositionSerializer<W> {
4545

4646
/// Writes all of the given positions delta.
4747
pub fn write_positions_delta(&mut self, mut positions_delta: &[u32]) {
48-
while !positions_delta.is_empty() {
49-
let remaining_block_len = self.remaining_block_len();
50-
let num_to_write = remaining_block_len.min(positions_delta.len());
51-
self.block.extend(&positions_delta[..num_to_write]);
52-
positions_delta = &positions_delta[num_to_write..];
53-
if self.remaining_block_len() == 0 {
54-
self.flush_block();
48+
match positions_delta.len() {
49+
0 => {}
50+
1 => {
51+
if self.remaining_block_len() == 0 {
52+
self.flush_block();
53+
}
54+
self.block.push(positions_delta[0]);
55+
}
56+
2 => {
57+
let rem = self.remaining_block_len();
58+
if rem < 2 {
59+
if rem == 1 {
60+
self.block.push(positions_delta[0]);
61+
self.flush_block();
62+
self.block.push(positions_delta[1]);
63+
} else {
64+
self.flush_block();
65+
self.block.push(positions_delta[0]);
66+
self.block.push(positions_delta[1]);
67+
}
68+
} else {
69+
self.block.push(positions_delta[0]);
70+
self.block.push(positions_delta[1]);
71+
}
72+
}
73+
3 => {
74+
let rem = self.remaining_block_len();
75+
match rem {
76+
3.. => {
77+
self.block.push(positions_delta[0]);
78+
self.block.push(positions_delta[1]);
79+
self.block.push(positions_delta[2]);
80+
}
81+
2 => {
82+
self.block.push(positions_delta[0]);
83+
self.block.push(positions_delta[1]);
84+
self.flush_block();
85+
self.block.push(positions_delta[2]);
86+
}
87+
1 => {
88+
self.block.push(positions_delta[0]);
89+
self.flush_block();
90+
self.block.push(positions_delta[1]);
91+
self.block.push(positions_delta[2]);
92+
}
93+
0 => {
94+
self.flush_block();
95+
self.block.push(positions_delta[0]);
96+
self.block.push(positions_delta[1]);
97+
self.block.push(positions_delta[2]);
98+
}
99+
}
100+
}
101+
4 => {
102+
let rem = self.remaining_block_len();
103+
match rem {
104+
4.. => {
105+
self.block.push(positions_delta[0]);
106+
self.block.push(positions_delta[1]);
107+
self.block.push(positions_delta[2]);
108+
self.block.push(positions_delta[3]);
109+
}
110+
3 => {
111+
self.block.push(positions_delta[0]);
112+
self.block.push(positions_delta[1]);
113+
self.block.push(positions_delta[2]);
114+
self.flush_block();
115+
self.block.push(positions_delta[3]);
116+
}
117+
2 => {
118+
self.block.push(positions_delta[0]);
119+
self.block.push(positions_delta[1]);
120+
self.flush_block();
121+
self.block.push(positions_delta[2]);
122+
self.block.push(positions_delta[3]);
123+
}
124+
1 => {
125+
self.block.push(positions_delta[0]);
126+
self.flush_block();
127+
self.block.push(positions_delta[1]);
128+
self.block.push(positions_delta[2]);
129+
self.block.push(positions_delta[3]);
130+
}
131+
0 => {
132+
self.flush_block();
133+
self.block.push(positions_delta[0]);
134+
self.block.push(positions_delta[1]);
135+
self.block.push(positions_delta[2]);
136+
self.block.push(positions_delta[3]);
137+
}
138+
}
139+
}
140+
_ => {
141+
while !positions_delta.is_empty() {
142+
let remaining_block_len = self.remaining_block_len();
143+
let num_to_write = remaining_block_len.min(positions_delta.len());
144+
self.block
145+
.extend_from_slice(&positions_delta[..num_to_write]);
146+
positions_delta = &positions_delta[num_to_write..];
147+
if self.remaining_block_len() == 0 {
148+
self.flush_block();
149+
}
150+
}
55151
}
56152
}
57153
}

src/postings/block_segment_postings.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::query::Bm25Weight;
1010
use crate::schema::IndexRecordOption;
1111
use crate::{DocId, Score, TERMINATED};
1212

13-
fn max_score<I: Iterator<Item = Score>>(mut it: I) -> Option<Score> {
13+
pub(crate) fn max_score<I: Iterator<Item = Score>>(mut it: I) -> Option<Score> {
1414
it.next().map(|first| it.fold(first, Score::max))
1515
}
1616

@@ -33,7 +33,7 @@ pub struct BlockSegmentPostings {
3333
skip_reader: SkipReader,
3434
}
3535

36-
fn decode_bitpacked_block(
36+
pub(crate) fn decode_bitpacked_block(
3737
doc_decoder: &mut BlockDecoder,
3838
freq_decoder_opt: Option<&mut BlockDecoder>,
3939
data: &[u8],
@@ -53,7 +53,7 @@ fn decode_bitpacked_block(
5353
}
5454
}
5555

56-
fn decode_vint_block(
56+
pub(crate) fn decode_vint_block(
5757
doc_decoder: &mut BlockDecoder,
5858
freq_decoder_opt: Option<&mut BlockDecoder>,
5959
data: &[u8],

src/postings/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ mod block_search;
44

55
pub(crate) use self::block_search::branchless_binary_search;
66

7-
mod block_segment_postings;
7+
pub(crate) mod block_segment_postings;
8+
89
pub(crate) mod compression;
910
mod indexing_context;
1011
mod json_postings_writer;
@@ -15,7 +16,7 @@ mod postings_writer;
1516
mod recorder;
1617
mod segment_postings;
1718
mod serializer;
18-
mod skip;
19+
pub(crate) mod skip;
1920
mod term_info;
2021

2122
pub(crate) use loaded_postings::LoadedPostings;

0 commit comments

Comments
 (0)