Skip to content

Commit 7eb6622

Browse files
committed
handle exclusive out of bounds ranges on fastfield range queries
closes quickwit-oss/quickwit#3790
1 parent e4e416a commit 7eb6622

File tree

2 files changed

+27
-6
lines changed

2 files changed

+27
-6
lines changed

src/query/range_query/range_query_u64_fastfield.rs

+26-5
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,14 @@ impl Weight for FastFieldRangeWeight {
7676
else {
7777
return Ok(Box::new(EmptyScorer));
7878
};
79+
#[allow(clippy::reversed_empty_ranges)]
7980
let value_range = bound_to_value_range(
8081
&self.lower_bound,
8182
&self.upper_bound,
8283
column.min_value(),
8384
column.max_value(),
84-
);
85+
)
86+
.unwrap_or(1..=0); // empty range
8587
if value_range.is_empty() {
8688
return Ok(Box::new(EmptyScorer));
8789
}
@@ -102,26 +104,28 @@ impl Weight for FastFieldRangeWeight {
102104
}
103105
}
104106

107+
// Returns None, if the range cannot be converted to a inclusive range (which equals to a empty
108+
// range).
105109
fn bound_to_value_range<T: MonotonicallyMappableToU64>(
106110
lower_bound: &Bound<T>,
107111
upper_bound: &Bound<T>,
108112
min_value: T,
109113
max_value: T,
110-
) -> RangeInclusive<T> {
114+
) -> Option<RangeInclusive<T>> {
111115
let mut start_value = match lower_bound {
112116
Bound::Included(val) => *val,
113-
Bound::Excluded(val) => T::from_u64(val.to_u64() + 1),
117+
Bound::Excluded(val) => T::from_u64(val.to_u64().checked_add(1)?),
114118
Bound::Unbounded => min_value,
115119
};
116120
if start_value.partial_cmp(&min_value) == Some(std::cmp::Ordering::Less) {
117121
start_value = min_value;
118122
}
119123
let end_value = match upper_bound {
120124
Bound::Included(val) => *val,
121-
Bound::Excluded(val) => T::from_u64(val.to_u64() - 1),
125+
Bound::Excluded(val) => T::from_u64(val.to_u64().checked_sub(1)?),
122126
Bound::Unbounded => max_value,
123127
};
124-
start_value..=end_value
128+
Some(start_value..=end_value)
125129
}
126130

127131
#[cfg(test)]
@@ -295,6 +299,9 @@ pub mod tests {
295299
let gen_query_inclusive = |field: &str, range: RangeInclusive<u64>| {
296300
format!("{}:[{} TO {}]", field, range.start(), range.end())
297301
};
302+
let gen_query_exclusive = |field: &str, range: RangeInclusive<u64>| {
303+
format!("{}:{{{} TO {}}}", field, range.start(), range.end())
304+
};
298305

299306
let test_sample = |sample_docs: Vec<Doc>| {
300307
let mut ids: Vec<u64> = sample_docs.iter().map(|doc| doc.id).collect();
@@ -310,6 +317,20 @@ pub mod tests {
310317
let query = gen_query_inclusive("ids", ids[0]..=ids[1]);
311318
assert_eq!(get_num_hits(query_from_text(&query)), expected_num_hits);
312319

320+
// Exclusive range
321+
let expected_num_hits = docs
322+
.iter()
323+
.filter(|doc| {
324+
(ids[0].saturating_add(1)..=ids[1].saturating_sub(1)).contains(&doc.id)
325+
})
326+
.count();
327+
328+
let query = gen_query_exclusive("id", ids[0]..=ids[1]);
329+
assert_eq!(get_num_hits(query_from_text(&query)), expected_num_hits);
330+
331+
let query = gen_query_exclusive("ids", ids[0]..=ids[1]);
332+
assert_eq!(get_num_hits(query_from_text(&query)), expected_num_hits);
333+
313334
// Intersection search
314335
let id_filter = sample_docs[0].id_name.to_string();
315336
let expected_num_hits = docs

src/schema/term.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ where B: AsRef<[u8]>
419419
let pos = bytes.iter().cloned().position(|b| b == JSON_END_OF_PATH)?;
420420
// split at pos + 1, so that json_path_bytes includes the JSON_END_OF_PATH byte.
421421
let (json_path_bytes, term) = bytes.split_at(pos + 1);
422-
Some((json_path_bytes, ValueBytes::wrap(&term)))
422+
Some((json_path_bytes, ValueBytes::wrap(term)))
423423
}
424424

425425
/// Returns the encoded ValueBytes after the json path.

0 commit comments

Comments
 (0)