This commit is contained in:
Mauro D
2023-03-22 17:41:58 +00:00
parent ca889365c5
commit 8cbe5bebc3
30 changed files with 5407 additions and 0 deletions

299
src/query/filter.rs Normal file
View File

@@ -0,0 +1,299 @@
use std::{
borrow::Cow,
ops::{BitAndAssign, BitOrAssign, BitXorAssign},
};
use ahash::AHashSet;
use roaring::RoaringBitmap;
use crate::{
fts::{
index::MAX_TOKEN_LENGTH, stemmer::Stemmer, term_index::TermIndex, tokenizers::Tokenizer,
},
write::Tokenize,
BitmapKey, Error, IndexKey, Store, ValueKey, BM_TERM, TERM_EXACT, TERM_STEMMED,
};
use super::{Filter, ResultSet};
struct State {
op: Filter,
bm: Option<RoaringBitmap>,
}
impl Store {
pub fn filter(
&self,
account_id: u32,
collection: u8,
filters: Vec<Filter>,
) -> crate::Result<ResultSet> {
let document_ids = self
.get_document_ids(account_id, collection)?
.unwrap_or_else(RoaringBitmap::new);
let mut state: State = Filter::And.into();
let mut stack = Vec::new();
let mut filters = filters.into_iter().peekable();
while let Some(filter) = filters.next() {
match filter {
Filter::HasKeyword { field, value } => {
state.op.apply(
&mut state.bm,
self.get_bitmap(BitmapKey {
account_id,
collection,
family: BM_TERM | TERM_EXACT,
field,
key: value.as_bytes(),
})?,
&document_ids,
);
}
Filter::HasKeywords { field, value } => {
let tokens = value.tokenize();
state.op.apply(
&mut state.bm,
self.get_bitmaps_intersection(
tokens
.iter()
.map(|key| BitmapKey {
account_id,
collection,
family: BM_TERM | TERM_EXACT,
field,
key,
})
.collect(),
)?,
&document_ids,
);
}
Filter::MatchValue { field, op, value } => {
state.op.apply(
&mut state.bm,
self.range_to_bitmap(
IndexKey {
account_id,
collection,
field,
key: &value,
},
op,
)?,
&document_ids,
);
}
Filter::HasText {
field,
text,
language,
match_phrase,
} => {
if match_phrase {
let phrase = Tokenizer::new(&text, language, MAX_TOKEN_LENGTH)
.map(|token| token.word)
.collect::<Vec<_>>();
let mut keys = Vec::with_capacity(phrase.len());
for word in &phrase {
let key = BitmapKey {
account_id,
collection,
family: BM_TERM | TERM_EXACT,
field,
key: word.as_bytes(),
};
if !keys.contains(&key) {
keys.push(key);
}
}
// Retrieve the Term Index for each candidate and match the exact phrase
if let Some(candidates) = self.get_bitmaps_intersection(keys)? {
let mut results = RoaringBitmap::new();
for document_id in candidates.iter() {
if let Some(term_index) = self.get_value::<TermIndex>(ValueKey {
account_id,
collection,
document_id,
field: u8::MAX,
})? {
if term_index
.match_terms(
&phrase
.iter()
.map(|w| term_index.get_match_term(w, None))
.collect::<Vec<_>>(),
None,
true,
false,
false,
)
.map_err(|e| {
Error::InternalError(format!(
"Corrupted TermIndex for {}: {:?}",
document_id, e
))
})?
.is_some()
{
results.insert(document_id);
}
}
}
state.op.apply(&mut state.bm, results.into(), &document_ids);
} else {
state.op.apply(&mut state.bm, None, &document_ids);
}
} else {
let words = Stemmer::new(&text, language, MAX_TOKEN_LENGTH)
.map(|token| (token.word, token.stemmed_word.unwrap_or(Cow::from(""))))
.collect::<AHashSet<_>>();
let mut requested_keys = AHashSet::default();
let mut text_bitmap = None;
for (word, stemmed_word) in &words {
let mut keys = Vec::new();
for (word, family) in [
(word, BM_TERM | TERM_EXACT),
(word, BM_TERM | TERM_STEMMED),
(stemmed_word, BM_TERM | TERM_EXACT),
(stemmed_word, BM_TERM | TERM_STEMMED),
] {
if !word.is_empty() {
let key = BitmapKey {
account_id,
collection,
family,
field,
key: word.as_bytes(),
};
if !requested_keys.contains(&key) {
requested_keys.insert(key);
keys.push(key);
}
}
}
// Term already matched on a previous iteration
if keys.is_empty() {
continue;
}
Filter::And.apply(
&mut text_bitmap,
self.get_bitmaps_union(keys)?,
&document_ids,
);
if text_bitmap.as_ref().unwrap().is_empty() {
break;
}
}
state.op.apply(&mut state.bm, text_bitmap, &document_ids);
}
}
Filter::InBitmap { family, field, key } => {
state.op.apply(
&mut state.bm,
self.get_bitmap(BitmapKey {
account_id,
collection,
family,
field,
key: &key,
})?,
&document_ids,
);
}
Filter::DocumentSet(set) => {
state.op.apply(&mut state.bm, Some(set), &document_ids);
}
op @ (Filter::And | Filter::Or | Filter::Not) => {
stack.push(state);
state = op.into();
continue;
}
Filter::End => {
if let Some(mut prev_state) = stack.pop() {
prev_state
.op
.apply(&mut prev_state.bm, state.bm, &document_ids);
state = prev_state;
} else {
break;
}
}
}
if matches!(state.op, Filter::And) && state.bm.as_ref().unwrap().is_empty() {
while let Some(filter) = filters.peek() {
if matches!(filter, Filter::End) {
break;
} else {
filters.next();
}
}
}
}
Ok(ResultSet {
results: state.bm.unwrap_or_else(RoaringBitmap::new),
document_ids,
})
}
}
impl Filter {
#[inline(always)]
pub fn apply(
&self,
dest: &mut Option<RoaringBitmap>,
mut src: Option<RoaringBitmap>,
not_mask: &RoaringBitmap,
) {
if let Some(dest) = dest {
match self {
Filter::And => {
if let Some(src) = src {
dest.bitand_assign(src);
} else {
dest.clear();
}
}
Filter::Or => {
if let Some(src) = src {
dest.bitor_assign(src);
}
}
Filter::Not => {
if let Some(mut src) = src {
src.bitxor_assign(not_mask);
dest.bitand_assign(src);
}
}
_ => unreachable!(),
}
} else if let Some(ref mut src_) = src {
if let Filter::Not = self {
src_.bitxor_assign(not_mask);
}
*dest = src;
} else if let Filter::Not = self {
*dest = Some(not_mask.clone());
} else {
*dest = Some(RoaringBitmap::new());
}
}
}
impl From<Filter> for State {
fn from(value: Filter) -> Self {
Self {
op: value,
bm: None,
}
}
}

160
src/query/mod.rs Normal file
View File

@@ -0,0 +1,160 @@
pub mod filter;
pub mod sort;
use roaring::RoaringBitmap;
use crate::{
fts::{lang::LanguageDetector, Language},
Serialize,
};
#[derive(Debug, Clone, Copy)]
pub enum Operator {
LowerThan,
LowerEqualThan,
GreaterThan,
GreaterEqualThan,
Equal,
}
#[derive(Debug)]
pub enum Filter {
HasKeyword {
field: u8,
value: String,
},
HasKeywords {
field: u8,
value: String,
},
MatchValue {
field: u8,
op: Operator,
value: Vec<u8>,
},
HasText {
field: u8,
text: String,
language: Language,
match_phrase: bool,
},
InBitmap {
family: u8,
field: u8,
key: Vec<u8>,
},
DocumentSet(RoaringBitmap),
And,
Or,
Not,
End,
}
#[derive(Debug)]
pub enum Comparator {
Field { field: u8, ascending: bool },
DocumentSet { set: RoaringBitmap, ascending: bool },
}
pub struct ResultSet {
results: RoaringBitmap,
document_ids: RoaringBitmap,
}
pub struct SortedResultRet {
pub position: i32,
pub ids: Vec<u32>,
pub found_anchor: bool,
}
impl Filter {
pub fn new_condition(field: impl Into<u8>, op: Operator, value: impl Serialize) -> Self {
Filter::MatchValue {
field: field.into(),
op,
value: value.serialize(),
}
}
pub fn eq(field: impl Into<u8>, value: impl Serialize) -> Self {
Filter::MatchValue {
field: field.into(),
op: Operator::Equal,
value: value.serialize(),
}
}
pub fn lt(field: impl Into<u8>, value: impl Serialize) -> Self {
Filter::MatchValue {
field: field.into(),
op: Operator::LowerThan,
value: value.serialize(),
}
}
pub fn le(field: impl Into<u8>, value: impl Serialize) -> Self {
Filter::MatchValue {
field: field.into(),
op: Operator::LowerEqualThan,
value: value.serialize(),
}
}
pub fn gt(field: impl Into<u8>, value: impl Serialize) -> Self {
Filter::MatchValue {
field: field.into(),
op: Operator::GreaterThan,
value: value.serialize(),
}
}
pub fn ge(field: impl Into<u8>, value: impl Serialize) -> Self {
Filter::MatchValue {
field: field.into(),
op: Operator::GreaterEqualThan,
value: value.serialize(),
}
}
pub fn match_text(field: impl Into<u8>, mut text: String, mut language: Language) -> Self {
let match_phrase = (text.starts_with('"') && text.ends_with('"'))
|| (text.starts_with('\'') && text.ends_with('\''));
if !match_phrase && language == Language::Unknown {
language = if let Some((l, t)) = text
.split_once(':')
.and_then(|(l, t)| (Language::from_iso_639(l)?, t.to_string()).into())
{
text = t;
l
} else {
LanguageDetector::detect_single(&text)
.and_then(|(l, c)| if c > 0.3 { Some(l) } else { None })
.unwrap_or(Language::Unknown)
};
}
Filter::HasText {
field: field.into(),
text,
language,
match_phrase,
}
}
}
impl Comparator {
pub fn ascending(field: impl Into<u8>) -> Self {
Self::Field {
field: field.into(),
ascending: true,
}
}
pub fn descending(field: impl Into<u8>) -> Self {
Self::Field {
field: field.into(),
ascending: false,
}
}
}

424
src/query/sort.rs Normal file
View File

@@ -0,0 +1,424 @@
use std::ops::{BitAndAssign, BitXorAssign};
use roaring::RoaringBitmap;
use rocksdb::{
DBIteratorWithThreadMode, Direction, IteratorMode, MultiThreaded, OptimisticTransactionDB,
};
use crate::{
backend::rocksdb::{ACCOUNT_KEY_LEN, CF_INDEXES},
write::key::KeySerializer,
Error, Store,
};
use super::{Comparator, ResultSet, SortedResultRet};
enum IndexType<'x> {
DocumentSet {
set: RoaringBitmap,
it: Option<roaring::bitmap::IntoIter>,
},
DB {
it: Option<DBIteratorWithThreadMode<'x, OptimisticTransactionDB<MultiThreaded>>>,
prefix: Vec<u8>,
start_key: Vec<u8>,
ascending: bool,
prev_item: Option<u32>,
prev_key: Option<Box<[u8]>>,
},
}
struct IndexIterator<'x> {
index: IndexType<'x>,
remaining: RoaringBitmap,
eof: bool,
}
impl Store {
#[allow(clippy::too_many_arguments)]
pub fn sort(
&self,
account_id: u32,
collection: u8,
mut result_set: ResultSet,
comparators: Vec<Comparator>,
limit: usize,
mut position: i32,
anchor: Option<u32>,
mut anchor_offset: i32,
) -> crate::Result<SortedResultRet> {
let has_anchor = anchor.is_some();
let mut anchor_found = false;
let requested_position = position;
let mut result = SortedResultRet {
position,
ids: Vec::with_capacity(std::cmp::min(limit, result_set.results.len() as usize)),
found_anchor: true,
};
let mut iterators = comparators
.into_iter()
.map(|comp| IndexIterator {
index: match comp {
Comparator::Field { field, ascending } => {
let prefix = KeySerializer::new(ACCOUNT_KEY_LEN)
.write(account_id)
.write(collection)
.write(field)
.finalize();
IndexType::DB {
it: None,
start_key: if !ascending {
let (key_account_id, key_collection, key_field) = if field < u8::MAX
{
(account_id, collection, field + 1)
} else if (collection) < u8::MAX {
(account_id, (collection) + 1, field)
} else {
(account_id + 1, collection, field)
};
KeySerializer::new(ACCOUNT_KEY_LEN)
.write(key_account_id)
.write(key_collection)
.write(key_field)
.finalize()
} else {
prefix.clone()
},
prefix,
ascending,
prev_item: None,
prev_key: None,
}
}
Comparator::DocumentSet { mut set, ascending } => IndexType::DocumentSet {
set: if !ascending {
if !set.is_empty() {
set.bitxor_assign(&result_set.document_ids);
set
} else {
result_set.document_ids.clone()
}
} else {
set
},
it: None,
},
},
remaining: std::mem::replace(&mut result_set.results, RoaringBitmap::new()),
eof: false,
})
.collect::<Vec<_>>();
let mut current = 0;
'outer: loop {
let mut doc_id;
'inner: loop {
let (it_opts, mut next_it_opts) = if current < iterators.len() - 1 {
let (iterators_first, iterators_last) = iterators.split_at_mut(current + 1);
(
iterators_first.last_mut().unwrap(),
iterators_last.first_mut(),
)
} else {
(&mut iterators[current], None)
};
if !matches!(it_opts.index, IndexType::DB { prev_item,.. } if prev_item.is_some())
{
if it_opts.remaining.is_empty() {
if current > 0 {
current -= 1;
continue 'inner;
} else {
break 'outer;
}
} else if it_opts.remaining.len() == 1 || it_opts.eof {
doc_id = it_opts.remaining.min().unwrap();
it_opts.remaining.remove(doc_id);
break 'inner;
}
}
match &mut it_opts.index {
IndexType::DB {
it,
prefix,
start_key,
ascending,
prev_item,
prev_key,
} => {
let it = if let Some(it) = it {
it
} else {
*it = Some(self.db.iterator_cf(
&self.db.cf_handle(CF_INDEXES).unwrap(),
IteratorMode::From(
start_key,
if *ascending {
Direction::Forward
} else {
Direction::Reverse
},
),
));
it.as_mut().unwrap()
};
let mut prev_key_prefix = prev_key
.as_ref()
.and_then(|k| k.get(..k.len() - std::mem::size_of::<u32>()))
.unwrap_or_default();
if let Some(prev_item) = prev_item.take() {
if let Some(next_it_opts) = &mut next_it_opts {
next_it_opts.remaining.insert(prev_item);
} else {
doc_id = prev_item;
break 'inner;
}
}
let mut is_eof = false;
loop {
if let Some(result) = it.next() {
let (key, _) = result.map_err(|e| {
Error::InternalError(format!("Iterator error: {}", e))
})?;
if !key.starts_with(prefix) {
*prev_key = None;
is_eof = true;
break;
}
doc_id = u32::from_be_bytes(
key.get(key.len() - std::mem::size_of::<u32>()..)
.ok_or_else(|| {
Error::InternalError("Invalid index entry".to_string())
})?
.try_into()
.unwrap(),
);
if it_opts.remaining.contains(doc_id) {
it_opts.remaining.remove(doc_id);
if let Some(next_it_opts) = &mut next_it_opts {
if let Some(prev_key_) = &*prev_key {
if key.len() != prev_key_.len()
|| !key.starts_with(prev_key_prefix)
{
*prev_item = Some(doc_id);
*prev_key = Some(key);
break;
}
} else {
*prev_key = Some(key);
prev_key_prefix = prev_key
.as_ref()
.and_then(|key| {
key.get(
..key.len() - std::mem::size_of::<u32>(),
)
})
.ok_or_else(|| {
Error::InternalError(
"Invalid index entry".to_string(),
)
})?;
}
next_it_opts.remaining.insert(doc_id);
} else {
// doc id found
break 'inner;
}
}
} else {
is_eof = true;
break;
}
}
if is_eof {
if let Some(next_it_opts) = &mut next_it_opts {
if !it_opts.remaining.is_empty() {
next_it_opts.remaining |= &it_opts.remaining;
it_opts.remaining.clear();
}
*prev_key = None;
it_opts.eof = true;
}
}
}
IndexType::DocumentSet { set, it } => {
if let Some(it) = it {
if let Some(_doc_id) = it.next() {
doc_id = _doc_id;
break 'inner;
}
} else {
let mut set = set.clone();
set.bitand_assign(&it_opts.remaining);
let set_len = set.len();
if set_len > 0 {
it_opts.remaining.bitxor_assign(&set);
match &mut next_it_opts {
Some(next_it_opts) if set_len > 1 => {
next_it_opts.remaining = set;
}
_ if set_len == 1 => {
doc_id = set.min().unwrap();
break 'inner;
}
_ => {
let mut it_ = set.into_iter();
let result = it_.next();
*it = Some(it_);
if let Some(result) = result {
doc_id = result;
break 'inner;
} else {
break 'outer;
}
}
}
} else if !it_opts.remaining.is_empty() {
if let Some(ref mut next_it_opts) = next_it_opts {
next_it_opts.remaining = std::mem::take(&mut it_opts.remaining);
}
}
};
}
};
if let Some(next_it_opts) = next_it_opts {
if !next_it_opts.remaining.is_empty() {
if next_it_opts.remaining.len() == 1 {
doc_id = next_it_opts.remaining.min().unwrap();
next_it_opts.remaining.remove(doc_id);
break 'inner;
} else {
match &mut next_it_opts.index {
IndexType::DB {
it,
start_key,
ascending,
prev_item,
prev_key,
..
} => {
if let Some(it) = it {
*it = self.db.iterator_cf(
&self.db.cf_handle(CF_INDEXES).unwrap(),
IteratorMode::From(
start_key,
if *ascending {
Direction::Forward
} else {
Direction::Reverse
},
),
);
}
*prev_item = None;
*prev_key = None;
}
IndexType::DocumentSet { it, .. } => {
*it = None;
}
}
current += 1;
next_it_opts.eof = false;
continue 'inner;
}
}
}
it_opts.eof = true;
if it_opts.remaining.is_empty() {
if current > 0 {
current -= 1;
} else {
break 'outer;
}
}
}
// Pagination
if !has_anchor {
if position >= 0 {
if position > 0 {
position -= 1;
} else {
result.ids.push(doc_id);
if limit > 0 && result.ids.len() == limit {
break 'outer;
}
}
} else {
result.ids.push(doc_id);
}
} else if anchor_offset >= 0 {
if !anchor_found {
if &doc_id != anchor.as_ref().unwrap() {
continue 'outer;
}
anchor_found = true;
}
if anchor_offset > 0 {
anchor_offset -= 1;
} else {
result.ids.push(doc_id);
if limit > 0 && result.ids.len() == limit {
break 'outer;
}
}
} else {
anchor_found = &doc_id == anchor.as_ref().unwrap();
result.ids.push(doc_id);
if !anchor_found {
continue 'outer;
}
position = anchor_offset;
break 'outer;
}
}
if !has_anchor || anchor_found {
if !has_anchor && requested_position >= 0 {
result.position = if position == 0 { requested_position } else { 0 };
} else if position >= 0 {
result.position = position;
} else {
let position = position.unsigned_abs() as usize;
let start_offset = if position < result.ids.len() {
result.ids.len() - position
} else {
0
};
result.position = start_offset as i32;
let end_offset = if limit > 0 {
std::cmp::min(start_offset + limit, result.ids.len())
} else {
result.ids.len()
};
result.ids = result.ids[start_offset..end_offset].to_vec()
}
} else {
result.found_anchor = false;
}
Ok(result)
}
}