Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, Store, U32_LEN, ValueKey,
|
||||
search::*,
|
||||
write::{
|
||||
SEARCH_INDEX_MAX_FIELD_LEN, SearchIndex, SearchIndexClass, SearchIndexField, SearchIndexId,
|
||||
SearchIndexType, ValueClass,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use roaring::RoaringBitmap;
|
||||
use std::{
|
||||
collections::hash_map::Entry,
|
||||
ops::{BitAndAssign, BitOrAssign},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct BitmapCache {
|
||||
cache: AHashMap<(CheekyHash, u8), Option<RoaringBitmap>>,
|
||||
}
|
||||
|
||||
impl BitmapCache {
|
||||
pub async fn merge_bitmaps(
|
||||
&mut self,
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
account_id: u32,
|
||||
hashes: impl Iterator<Item = CheekyHash>,
|
||||
field: u8,
|
||||
is_union: bool,
|
||||
) -> trc::Result<Option<RoaringBitmap>> {
|
||||
let mut result = RoaringBitmap::new();
|
||||
for (idx, hash) in hashes.enumerate() {
|
||||
match self.cache.entry((hash, field)) {
|
||||
Entry::Occupied(entry) => {
|
||||
if let Some(bm) = entry.get() {
|
||||
if is_union {
|
||||
result.bitor_assign(bm);
|
||||
} else if idx == 0 {
|
||||
result = bm.clone();
|
||||
} else {
|
||||
result.bitand_assign(bm);
|
||||
if result.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
} else if !is_union {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let from_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let to_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let key_len = (U32_LEN * 2) + hash.len() + 2;
|
||||
let mut documents = RoaringBitmap::new();
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(from_key, to_key).no_values().ascending(),
|
||||
|key, _| {
|
||||
if key.len() == key_len {
|
||||
documents.insert(key.deserialize_be_u32(key.len() - U32_LEN)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !documents.is_empty() {
|
||||
if is_union {
|
||||
result.bitor_assign(&documents);
|
||||
} else if idx == 0 {
|
||||
result = documents.clone();
|
||||
} else {
|
||||
result.bitand_assign(&documents);
|
||||
if result.is_empty() {
|
||||
entry.insert(Some(documents));
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
entry.insert(Some(documents));
|
||||
} else if !is_union {
|
||||
entry.insert(None);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !result.is_empty() {
|
||||
Ok(Some(result))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn range_to_bitmap(
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
account_id: u32,
|
||||
field_id: u8,
|
||||
match_value: &[u8],
|
||||
op: SearchOperator,
|
||||
) -> trc::Result<Option<RoaringBitmap>> {
|
||||
let ((from_value, from_doc_id, from_field), (end_value, end_doc_id, end_field)) = match op {
|
||||
SearchOperator::LowerThan => ((&[][..], 0, field_id), (match_value, 0, field_id)),
|
||||
SearchOperator::LowerEqualThan => {
|
||||
((&[][..], 0, field_id), (match_value, u32::MAX, field_id))
|
||||
}
|
||||
SearchOperator::GreaterThan => (
|
||||
(match_value, u32::MAX, field_id),
|
||||
(&[][..], u32::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::GreaterEqualThan => (
|
||||
(match_value, 0, field_id),
|
||||
(&[][..], u32::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::Equal | SearchOperator::Contains => (
|
||||
(match_value, 0, field_id),
|
||||
(match_value, u32::MAX, field_id),
|
||||
),
|
||||
};
|
||||
|
||||
let begin = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: from_doc_id,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: from_field,
|
||||
data: from_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let end = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: end_doc_id,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: end_field,
|
||||
data: end_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let mut bm = RoaringBitmap::new();
|
||||
let prefix = KeySerializer::new(U32_LEN + 2)
|
||||
.write(index.as_u8() | 1 << 6)
|
||||
.write(account_id)
|
||||
.write(field_id)
|
||||
.finalize();
|
||||
let prefix_len = prefix.len();
|
||||
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).no_values().ascending(),
|
||||
|key, _| {
|
||||
if !key.starts_with(&prefix) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let id_pos = key.len() - U32_LEN;
|
||||
let value = key
|
||||
.get(prefix_len..id_pos)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
|
||||
let matches = match op {
|
||||
SearchOperator::LowerThan => value < match_value,
|
||||
SearchOperator::LowerEqualThan => value <= match_value,
|
||||
SearchOperator::GreaterThan => value > match_value,
|
||||
SearchOperator::GreaterEqualThan => value >= match_value,
|
||||
SearchOperator::Equal | SearchOperator::Contains => value == match_value,
|
||||
};
|
||||
|
||||
if matches {
|
||||
bm.insert(key.deserialize_be_u32(id_pos)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !bm.is_empty() {
|
||||
Ok(Some(bm))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn sort_order(
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
account_id: u32,
|
||||
field_id: u8,
|
||||
) -> trc::Result<AHashMap<u32, u32>> {
|
||||
let begin = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id,
|
||||
data: vec![0u8],
|
||||
},
|
||||
},
|
||||
}));
|
||||
let end = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id,
|
||||
data: vec![u8::MAX; SEARCH_INDEX_MAX_FIELD_LEN],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let mut last_value = Vec::new();
|
||||
let mut results = AHashMap::new();
|
||||
let mut pos = 0;
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).no_values().ascending(),
|
||||
|key, _| {
|
||||
let value = key
|
||||
.get(U32_LEN + 2..key.len() - U32_LEN)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
if value != last_value {
|
||||
pos += 1;
|
||||
last_value = value.to_vec();
|
||||
}
|
||||
|
||||
results.insert(key.deserialize_be_u32(key.len() - U32_LEN)?, pos);
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
IterateParams, Store, U64_LEN, ValueKey,
|
||||
search::*,
|
||||
write::{
|
||||
SearchIndex, SearchIndexClass, SearchIndexField, SearchIndexId, SearchIndexType,
|
||||
ValueClass,
|
||||
key::{DeserializeBigEndian, KeySerializer},
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use roaring::RoaringTreemap;
|
||||
use std::{
|
||||
collections::hash_map::Entry,
|
||||
ops::{BitAndAssign, BitOrAssign},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct TreemapCache {
|
||||
cache: AHashMap<(CheekyHash, u8), Option<RoaringTreemap>>,
|
||||
}
|
||||
|
||||
impl TreemapCache {
|
||||
pub async fn merge_treemaps(
|
||||
&mut self,
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
hashes: impl Iterator<Item = CheekyHash>,
|
||||
field: u8,
|
||||
is_union: bool,
|
||||
) -> trc::Result<Option<RoaringTreemap>> {
|
||||
let mut result = RoaringTreemap::new();
|
||||
for (idx, hash) in hashes.enumerate() {
|
||||
match self.cache.entry((hash, field)) {
|
||||
Entry::Occupied(entry) => {
|
||||
if let Some(bm) = entry.get() {
|
||||
if is_union {
|
||||
result.bitor_assign(bm);
|
||||
} else if idx == 0 {
|
||||
result = bm.clone();
|
||||
} else {
|
||||
result.bitand_assign(bm);
|
||||
if result.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
} else if !is_union {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let from_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: 0 },
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let to_key = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: u64::MAX },
|
||||
typ: SearchIndexType::Term { hash, field },
|
||||
}));
|
||||
let key_len = U64_LEN + hash.len() + 2;
|
||||
let mut documents = RoaringTreemap::new();
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(from_key, to_key).no_values().ascending(),
|
||||
|key, _| {
|
||||
if key.len() == key_len {
|
||||
documents.insert(key.deserialize_be_u64(key.len() - U64_LEN)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !documents.is_empty() {
|
||||
if is_union {
|
||||
result.bitor_assign(&documents);
|
||||
} else if idx == 0 {
|
||||
result = documents.clone();
|
||||
} else {
|
||||
result.bitand_assign(&documents);
|
||||
if result.is_empty() {
|
||||
entry.insert(Some(documents));
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
entry.insert(Some(documents));
|
||||
} else if !is_union {
|
||||
entry.insert(None);
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !result.is_empty() {
|
||||
Ok(Some(result))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn range_to_treemap(
|
||||
store: &Store,
|
||||
index: SearchIndex,
|
||||
field_id: u8,
|
||||
match_value: &[u8],
|
||||
op: SearchOperator,
|
||||
) -> trc::Result<Option<RoaringTreemap>> {
|
||||
let ((from_value, from_id, from_field), (end_value, end_id, end_field)) = match op {
|
||||
SearchOperator::LowerThan => ((&[][..], 0, field_id), (match_value, 0, field_id)),
|
||||
SearchOperator::LowerEqualThan => {
|
||||
((&[][..], 0, field_id), (match_value, u64::MAX, field_id))
|
||||
}
|
||||
SearchOperator::GreaterThan => (
|
||||
(match_value, u64::MAX, field_id),
|
||||
(&[][..], u64::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::GreaterEqualThan => (
|
||||
(match_value, 0, field_id),
|
||||
(&[][..], u64::MAX, field_id + 1),
|
||||
),
|
||||
SearchOperator::Equal | SearchOperator::Contains => (
|
||||
(match_value, 0, field_id),
|
||||
(match_value, u64::MAX, field_id),
|
||||
),
|
||||
};
|
||||
|
||||
let begin = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: from_id },
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: from_field,
|
||||
data: from_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let end = ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: end_id },
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: end_field,
|
||||
data: end_value.to_vec(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let mut bm = RoaringTreemap::new();
|
||||
let prefix = KeySerializer::new(U64_LEN + 2)
|
||||
.write(index.as_u8() | 1 << 6)
|
||||
.write(field_id)
|
||||
.finalize();
|
||||
let prefix_len = prefix.len();
|
||||
|
||||
store
|
||||
.iterate(
|
||||
IterateParams::new(begin, end).no_values().ascending(),
|
||||
|key, _| {
|
||||
if !key.starts_with(&prefix) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let id_pos = key.len() - U64_LEN;
|
||||
let value = key
|
||||
.get(prefix_len..id_pos)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
|
||||
let matches = match op {
|
||||
SearchOperator::LowerThan => value < match_value,
|
||||
SearchOperator::LowerEqualThan => value <= match_value,
|
||||
SearchOperator::GreaterThan => value > match_value,
|
||||
SearchOperator::GreaterEqualThan => value >= match_value,
|
||||
SearchOperator::Equal | SearchOperator::Contains => value == match_value,
|
||||
};
|
||||
|
||||
if matches {
|
||||
bm.insert(key.deserialize_be_u64(id_pos)?);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if !bm.is_empty() {
|
||||
Ok(Some(bm))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
|
||||
impl IndexDocument {
|
||||
pub fn new(index: SearchIndex) -> Self {
|
||||
Self {
|
||||
fields: Default::default(),
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_account_id(mut self, account_id: u32) -> Self {
|
||||
self.fields
|
||||
.insert(SearchField::AccountId, SearchValue::Uint(account_id as u64));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_document_id(mut self, document_id: u32) -> Self {
|
||||
self.fields.insert(
|
||||
SearchField::DocumentId,
|
||||
SearchValue::Uint(document_id as u64),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: u64) -> Self {
|
||||
self.fields.insert(SearchField::Id, SearchValue::Uint(id));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn index_text(&mut self, field: impl Into<SearchField>, value: &str, language: Language) {
|
||||
match self.fields.entry(field.into()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let SearchValue::Text {
|
||||
value: existing_value,
|
||||
..
|
||||
} = entry.get_mut()
|
||||
{
|
||||
existing_value.push(' ');
|
||||
sanitize_text_to_buf(existing_value, value);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(SearchValue::Text {
|
||||
value: sanitize_text(value),
|
||||
language,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index_bool(&mut self, field: impl Into<SearchField>, value: bool) {
|
||||
self.fields
|
||||
.insert(field.into(), SearchValue::Boolean(value));
|
||||
}
|
||||
|
||||
pub fn index_integer<N: Into<i64>>(&mut self, field: impl Into<SearchField>, value: N) {
|
||||
self.fields
|
||||
.insert(field.into(), SearchValue::Int(value.into()));
|
||||
}
|
||||
|
||||
pub fn index_unsigned<N: Into<u64>>(&mut self, field: impl Into<SearchField>, value: N) {
|
||||
self.fields
|
||||
.insert(field.into(), SearchValue::Uint(value.into()));
|
||||
}
|
||||
|
||||
pub fn index_keyword(&mut self, field: impl Into<SearchField>, value: impl AsRef<str>) {
|
||||
self.fields.insert(
|
||||
field.into(),
|
||||
SearchValue::Text {
|
||||
value: sanitize_text(value.as_ref()),
|
||||
language: Language::None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn insert_key_value(
|
||||
&mut self,
|
||||
field: impl Into<SearchField>,
|
||||
key: impl AsRef<str>,
|
||||
value: impl AsRef<str>,
|
||||
) {
|
||||
let search_field = field.into();
|
||||
let key = key
|
||||
.as_ref()
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_control())
|
||||
.map(|ch| ch.to_ascii_lowercase())
|
||||
.collect::<String>();
|
||||
let value = value.as_ref();
|
||||
|
||||
match self.fields.entry(search_field) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let SearchValue::KeyValues(existing_key_values) = entry.get_mut() {
|
||||
if let Some(existing_value) = existing_key_values.get_mut(&key) {
|
||||
existing_value.push(' ');
|
||||
sanitize_text_to_buf(existing_value, value);
|
||||
} else {
|
||||
existing_key_values.append(key, sanitize_text(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
let mut new_key_values = VecMap::new();
|
||||
new_key_values.append(key, sanitize_text(value));
|
||||
entry.insert(SearchValue::KeyValues(new_key_values));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.fields.is_empty()
|
||||
}
|
||||
|
||||
pub fn has_field(&self, field: &SearchField) -> bool {
|
||||
self.fields.contains_key(field)
|
||||
}
|
||||
|
||||
pub fn fields(&self) -> impl Iterator<Item = (&SearchField, &SearchValue)> {
|
||||
self.fields.iter()
|
||||
}
|
||||
|
||||
pub fn set_unknown_language(&mut self, lang: Language) {
|
||||
for value in self.fields.values_mut() {
|
||||
if let SearchValue::Text { language, .. } = value
|
||||
&& language.is_unknown()
|
||||
{
|
||||
*language = lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchFilter {
|
||||
pub fn cond(
|
||||
field: impl Into<SearchField>,
|
||||
op: SearchOperator,
|
||||
value: impl Into<SearchValue>,
|
||||
) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eq(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::Equal,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lt(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::LowerThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn le(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::LowerEqualThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gt(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::GreaterThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ge(field: impl Into<SearchField>, value: impl Into<SearchValue>) -> Self {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::GreaterEqualThan,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_text_detect(
|
||||
field: impl Into<SearchField>,
|
||||
text: impl Into<String>,
|
||||
default_language: Language,
|
||||
) -> Self {
|
||||
let (text, language) = Language::detect(text.into(), default_language);
|
||||
Self::has_text(field, text, language)
|
||||
}
|
||||
|
||||
pub fn has_text(
|
||||
field: impl Into<SearchField>,
|
||||
text: impl Into<String>,
|
||||
language: Language,
|
||||
) -> Self {
|
||||
let text = text.into();
|
||||
let (is_exact, text) = if let Some(text) = text
|
||||
.strip_prefix('"')
|
||||
.and_then(|t| t.strip_suffix('"'))
|
||||
.or_else(|| text.strip_prefix('\'').and_then(|t| t.strip_suffix('\'')))
|
||||
{
|
||||
(true, text.to_string())
|
||||
} else {
|
||||
(false, text)
|
||||
};
|
||||
|
||||
if !matches!(language, Language::None) && is_exact {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Text {
|
||||
value: text,
|
||||
language,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
SearchFilter::Operator {
|
||||
field: field.into(),
|
||||
op: SearchOperator::Contains,
|
||||
value: SearchValue::Text {
|
||||
value: text,
|
||||
language,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn has_english_text(field: impl Into<SearchField>, text: impl Into<String>) -> Self {
|
||||
Self::has_text(field, text, Language::English)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn has_keyword(field: impl Into<SearchField>, text: impl Into<String>) -> Self {
|
||||
Self::has_text(field, text, Language::None)
|
||||
}
|
||||
|
||||
pub fn is_in_set(set: RoaringBitmap) -> Self {
|
||||
SearchFilter::DocumentSet(set)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchComparator {
|
||||
pub fn field(field: impl Into<SearchField>, ascending: bool) -> Self {
|
||||
Self::Field {
|
||||
field: field.into(),
|
||||
ascending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(set: RoaringBitmap, ascending: bool) -> Self {
|
||||
Self::DocumentSet { set, ascending }
|
||||
}
|
||||
|
||||
pub fn sorted_set(set: AHashMap<u32, u32>, ascending: bool) -> Self {
|
||||
Self::SortedSet { set, ascending }
|
||||
}
|
||||
|
||||
pub fn ascending(field: impl Into<SearchField>) -> Self {
|
||||
Self::Field {
|
||||
field: field.into(),
|
||||
ascending: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn descending(field: impl Into<SearchField>) -> Self {
|
||||
Self::Field {
|
||||
field: field.into(),
|
||||
ascending: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn write_sanitized(out: &mut String, text: &str) {
|
||||
let mut last_is_space = true;
|
||||
for ch in text.chars() {
|
||||
match ch {
|
||||
' ' | '\x09'..='\x0d' => {
|
||||
if !last_is_space {
|
||||
out.push(' ');
|
||||
last_is_space = true;
|
||||
}
|
||||
}
|
||||
'\0'..='\x1f' | '\x7f'..='\u{9f}' => {}
|
||||
ch => {
|
||||
out.push(ch);
|
||||
last_is_space = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sanitize_text_to_buf(out: &mut String, text: &str) {
|
||||
out.reserve_exact(text.len());
|
||||
write_sanitized(out, text);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sanitize_text(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
write_sanitized(&mut out, text);
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
|
||||
impl SearchableField for EmailSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Email
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Email(EmailSearchField::From),
|
||||
SearchField::Email(EmailSearchField::To),
|
||||
SearchField::Email(EmailSearchField::Cc),
|
||||
SearchField::Email(EmailSearchField::Bcc),
|
||||
SearchField::Email(EmailSearchField::Subject),
|
||||
SearchField::Email(EmailSearchField::Body),
|
||||
SearchField::Email(EmailSearchField::Attachment),
|
||||
SearchField::Email(EmailSearchField::ReceivedAt),
|
||||
SearchField::Email(EmailSearchField::SentAt),
|
||||
SearchField::Email(EmailSearchField::Size),
|
||||
SearchField::Email(EmailSearchField::HasAttachment),
|
||||
SearchField::Email(EmailSearchField::Headers),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
#[cfg(not(feature = "test_mode"))]
|
||||
{
|
||||
matches!(
|
||||
self,
|
||||
EmailSearchField::From
|
||||
| EmailSearchField::To
|
||||
| EmailSearchField::Subject
|
||||
| EmailSearchField::ReceivedAt
|
||||
| EmailSearchField::SentAt
|
||||
| EmailSearchField::Size
|
||||
| EmailSearchField::HasAttachment,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "test_mode")]
|
||||
{
|
||||
matches!(
|
||||
self,
|
||||
EmailSearchField::From
|
||||
| EmailSearchField::To
|
||||
| EmailSearchField::Subject
|
||||
| EmailSearchField::ReceivedAt
|
||||
| EmailSearchField::SentAt
|
||||
| EmailSearchField::Size
|
||||
| EmailSearchField::HasAttachment
|
||||
| EmailSearchField::Bcc
|
||||
| EmailSearchField::Cc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EmailSearchField::From
|
||||
| EmailSearchField::To
|
||||
| EmailSearchField::Cc
|
||||
| EmailSearchField::Bcc
|
||||
| EmailSearchField::Subject
|
||||
| EmailSearchField::Body
|
||||
| EmailSearchField::Attachment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for CalendarSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Calendar
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Calendar(CalendarSearchField::Title),
|
||||
SearchField::Calendar(CalendarSearchField::Description),
|
||||
SearchField::Calendar(CalendarSearchField::Location),
|
||||
SearchField::Calendar(CalendarSearchField::Owner),
|
||||
SearchField::Calendar(CalendarSearchField::Attendee),
|
||||
SearchField::Calendar(CalendarSearchField::Start),
|
||||
SearchField::Calendar(CalendarSearchField::Uid),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
matches!(self, CalendarSearchField::Start | CalendarSearchField::Uid)
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
!self.is_indexed()
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for ContactSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Contacts
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Contact(ContactSearchField::Member),
|
||||
SearchField::Contact(ContactSearchField::Kind),
|
||||
SearchField::Contact(ContactSearchField::Name),
|
||||
SearchField::Contact(ContactSearchField::Nickname),
|
||||
SearchField::Contact(ContactSearchField::Organization),
|
||||
SearchField::Contact(ContactSearchField::Email),
|
||||
SearchField::Contact(ContactSearchField::Phone),
|
||||
SearchField::Contact(ContactSearchField::OnlineService),
|
||||
SearchField::Contact(ContactSearchField::Address),
|
||||
SearchField::Contact(ContactSearchField::Note),
|
||||
SearchField::Contact(ContactSearchField::Uid),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
matches!(self, ContactSearchField::Uid | ContactSearchField::Kind)
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
!self.is_indexed()
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for FileSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::File
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::AccountId, SearchField::DocumentId]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::File(FileSearchField::Name),
|
||||
SearchField::File(FileSearchField::Content),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableField for TracingSearchField {
|
||||
fn index() -> SearchIndex {
|
||||
SearchIndex::Tracing
|
||||
}
|
||||
|
||||
fn primary_keys() -> &'static [SearchField] {
|
||||
&[SearchField::Id]
|
||||
}
|
||||
|
||||
fn all_fields() -> &'static [SearchField] {
|
||||
&[
|
||||
SearchField::Tracing(TracingSearchField::EventType),
|
||||
SearchField::Tracing(TracingSearchField::QueueId),
|
||||
SearchField::Tracing(TracingSearchField::Keywords),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_indexed(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TracingSearchField::QueueId | TracingSearchField::EventType
|
||||
)
|
||||
}
|
||||
|
||||
fn is_text(&self) -> bool {
|
||||
matches!(self, TracingSearchField::Keywords)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchField {
|
||||
pub(crate) fn is_indexed(&self) -> bool {
|
||||
match self {
|
||||
SearchField::Email(field) => field.is_indexed(),
|
||||
SearchField::Calendar(field) => field.is_indexed(),
|
||||
SearchField::Contact(field) => field.is_indexed(),
|
||||
SearchField::File(field) => field.is_indexed(),
|
||||
SearchField::Tracing(field) => field.is_indexed(),
|
||||
SearchField::AccountId | SearchField::DocumentId | SearchField::Id => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_text(&self) -> bool {
|
||||
match self {
|
||||
SearchField::Email(field) => field.is_text(),
|
||||
SearchField::Calendar(field) => field.is_text(),
|
||||
SearchField::Contact(field) => field.is_text(),
|
||||
SearchField::File(field) => field.is_text(),
|
||||
SearchField::Tracing(field) => field.is_text(),
|
||||
SearchField::AccountId | SearchField::DocumentId | SearchField::Id => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_json(&self) -> bool {
|
||||
matches!(self, SearchField::Email(EmailSearchField::Headers))
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
pub fn all_fields(&self) -> &[SearchField] {
|
||||
match self {
|
||||
SearchIndex::Email => EmailSearchField::all_fields(),
|
||||
SearchIndex::Calendar => CalendarSearchField::all_fields(),
|
||||
SearchIndex::Contacts => ContactSearchField::all_fields(),
|
||||
SearchIndex::File => FileSearchField::all_fields(),
|
||||
SearchIndex::Tracing => TracingSearchField::all_fields(),
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_keys(&self) -> &'static [SearchField] {
|
||||
match self {
|
||||
SearchIndex::Email => EmailSearchField::primary_keys(),
|
||||
SearchIndex::Calendar => CalendarSearchField::primary_keys(),
|
||||
SearchIndex::Contacts => ContactSearchField::primary_keys(),
|
||||
SearchIndex::File => FileSearchField::primary_keys(),
|
||||
SearchIndex::Tracing => TracingSearchField::primary_keys(),
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Deserialize, IterateParams, Store, U64_LEN, ValueKey,
|
||||
search::{
|
||||
IndexDocument, SearchField, SearchFilter, SearchOperator, SearchQuery, SearchValue,
|
||||
term::{TermIndex, TermIndexBuilder},
|
||||
},
|
||||
write::{
|
||||
AlignedBytes, Archive, BatchBuilder, SEARCH_INDEX_MAX_FIELD_LEN, SearchIndex,
|
||||
SearchIndexClass, SearchIndexField, SearchIndexId, SearchIndexType, ValueClass,
|
||||
key::DeserializeBigEndian,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use trc::AddContext;
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
impl Store {
|
||||
pub(crate) async fn index(&self, documents: Vec<IndexDocument>) -> trc::Result<()> {
|
||||
let truncate_at = if self.is_foundationdb() { 1_048_576 } else { 0 };
|
||||
|
||||
for document in documents {
|
||||
let mut batch = BatchBuilder::new();
|
||||
let index = document.index;
|
||||
let mut old_term_index = None;
|
||||
|
||||
if matches!(index, SearchIndex::Calendar | SearchIndex::Contacts) {
|
||||
let mut account_id = None;
|
||||
let mut document_id = None;
|
||||
for (field, value) in &document.fields {
|
||||
if let SearchValue::Uint(id) = value {
|
||||
match field {
|
||||
SearchField::AccountId => {
|
||||
account_id = Some(*id as u32);
|
||||
}
|
||||
SearchField::DocumentId => {
|
||||
document_id = Some(*id as u32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(account_id), Some(document_id)) = (account_id, document_id)
|
||||
&& let Some(archive) = self
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::from(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
old_term_index = Some(archive);
|
||||
}
|
||||
}
|
||||
|
||||
let term_index_builder = TermIndexBuilder::build(document, truncate_at);
|
||||
if let Some(old_term_index) = old_term_index {
|
||||
let old_term_index = old_term_index
|
||||
.unarchive::<TermIndex>()
|
||||
.caused_by(trc::location!())?;
|
||||
term_index_builder
|
||||
.index
|
||||
.merge_index(&mut batch, index, term_index_builder.id, old_term_index)
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
term_index_builder
|
||||
.index
|
||||
.write_index(&mut batch, index, term_index_builder.id)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
let mut commit_points = batch.commit_points();
|
||||
for commit_point in commit_points.iter() {
|
||||
let batch = batch.build_one(commit_point);
|
||||
self.write(batch).await.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn unindex(&self, query: SearchQuery) -> trc::Result<()> {
|
||||
let index = query.index;
|
||||
let mut account_documents: AHashMap<u32, Vec<u32>> = AHashMap::new();
|
||||
let mut ids = vec![];
|
||||
let mut to_id = None;
|
||||
let mut last_account_id = None;
|
||||
|
||||
for filter in query.filters {
|
||||
match filter {
|
||||
SearchFilter::Operator { field, op, value } => match (field, value) {
|
||||
(SearchField::AccountId, SearchValue::Uint(id))
|
||||
if op == SearchOperator::Equal =>
|
||||
{
|
||||
last_account_id = Some(id as u32);
|
||||
account_documents.entry(id as u32).or_default();
|
||||
}
|
||||
(SearchField::DocumentId, SearchValue::Uint(id))
|
||||
if op == SearchOperator::Equal && last_account_id.is_some() =>
|
||||
{
|
||||
account_documents
|
||||
.get_mut(&last_account_id.unwrap())
|
||||
.unwrap()
|
||||
.push(id as u32);
|
||||
}
|
||||
(SearchField::Id, SearchValue::Uint(id)) => match op {
|
||||
SearchOperator::LowerThan => {
|
||||
to_id = Some(id.saturating_sub(1));
|
||||
}
|
||||
SearchOperator::LowerEqualThan => {
|
||||
to_id = Some(id);
|
||||
}
|
||||
SearchOperator::Equal => {
|
||||
ids.push(id);
|
||||
}
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.reason("Unsupported operator for Id field"));
|
||||
}
|
||||
},
|
||||
filter => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Unsupported unindex filter {filter:?}")));
|
||||
}
|
||||
},
|
||||
SearchFilter::And | SearchFilter::Or | SearchFilter::End => {}
|
||||
SearchFilter::Not | SearchFilter::DocumentSet(_) => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Unsupported unindex filter {filter:?}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete by account and document ids
|
||||
for (account_id, document_ids) in account_documents {
|
||||
if !document_ids.is_empty() {
|
||||
for document_id in document_ids {
|
||||
let Some(archive) = self
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::from(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let term_index = archive
|
||||
.unarchive::<TermIndex>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
term_index.delete_index(
|
||||
&mut batch,
|
||||
index,
|
||||
SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id,
|
||||
},
|
||||
);
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
} else {
|
||||
// Delete all documents for the account
|
||||
self.delete_range(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.delete_range(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: 0,
|
||||
data: vec![0u8],
|
||||
},
|
||||
},
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: u8::MAX,
|
||||
data: vec![u8::MAX; SEARCH_INDEX_MAX_FIELD_LEN],
|
||||
},
|
||||
},
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
self.delete_range(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: 0,
|
||||
},
|
||||
typ: SearchIndexType::Term {
|
||||
hash: CheekyHash::NULL,
|
||||
field: 0,
|
||||
},
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Account {
|
||||
account_id,
|
||||
document_id: u32::MAX,
|
||||
},
|
||||
typ: SearchIndexType::Term {
|
||||
hash: CheekyHash::FULL,
|
||||
field: u8::MAX,
|
||||
},
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete by global ids
|
||||
for id in ids {
|
||||
let Some(archive) = self
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::from(ValueClass::SearchIndex(
|
||||
SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id },
|
||||
typ: SearchIndexType::Document,
|
||||
},
|
||||
)))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let term_index = archive
|
||||
.unarchive::<TermIndex>()
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
term_index.delete_index(&mut batch, index, SearchIndexId::Global { id });
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
// Delete ranges
|
||||
if let Some(to_id) = to_id {
|
||||
let mut batches = Vec::new();
|
||||
self.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: 0 },
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
ValueKey::from(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id: SearchIndexId::Global { id: to_id },
|
||||
typ: SearchIndexType::Document,
|
||||
})),
|
||||
),
|
||||
|key, value| {
|
||||
let archive = <Archive<AlignedBytes> as Deserialize>::deserialize(value)?;
|
||||
let term_index = archive.unarchive::<TermIndex>()?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
term_index.delete_index(
|
||||
&mut batch,
|
||||
index,
|
||||
SearchIndexId::Global {
|
||||
id: key.deserialize_be_u64(key.len() - U64_LEN)?,
|
||||
},
|
||||
);
|
||||
batches.push(batch);
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for mut batch in batches {
|
||||
self.write(batch.build_all())
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
struct State {
|
||||
pub op: SearchFilter,
|
||||
pub bm: Option<RoaringBitmap>,
|
||||
}
|
||||
|
||||
impl SearchQuery {
|
||||
pub fn new(index: SearchIndex) -> Self {
|
||||
Self {
|
||||
index,
|
||||
filters: Vec::new(),
|
||||
comparators: Vec::new(),
|
||||
mask: RoaringBitmap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_filters(mut self, filters: Vec<SearchFilter>) -> Self {
|
||||
if self.filters.is_empty() {
|
||||
self.filters = filters;
|
||||
} else {
|
||||
self.filters.extend(filters);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_comparators(mut self, comparators: Vec<SearchComparator>) -> Self {
|
||||
if self.comparators.is_empty() {
|
||||
self.comparators = comparators;
|
||||
} else {
|
||||
self.comparators.extend(comparators);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_filter(mut self, filter: SearchFilter) -> Self {
|
||||
self.filters.push(filter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_filter(&mut self, filter: SearchFilter) -> &mut Self {
|
||||
self.filters.push(filter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_comparator(mut self, comparator: SearchComparator) -> Self {
|
||||
self.comparators.push(comparator);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mask(mut self, mask: RoaringBitmap) -> Self {
|
||||
self.mask = mask;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_account_id(mut self, account_id: u32) -> Self {
|
||||
self.filters.push(SearchFilter::cond(
|
||||
SearchField::AccountId,
|
||||
SearchOperator::Equal,
|
||||
SearchValue::Uint(account_id as u64),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn filter(self) -> QueryResults {
|
||||
if self.filters.is_empty() {
|
||||
return QueryResults {
|
||||
results: self.mask,
|
||||
comparators: self.comparators,
|
||||
};
|
||||
}
|
||||
let mut state: State = State {
|
||||
op: SearchFilter::And,
|
||||
bm: None,
|
||||
};
|
||||
let mut stack = Vec::new();
|
||||
let mut filters = self.filters.into_iter().peekable();
|
||||
let mask = self.mask;
|
||||
|
||||
while let Some(filter) = filters.next() {
|
||||
let mut result = match filter {
|
||||
SearchFilter::DocumentSet(set) => Some(set),
|
||||
op @ (SearchFilter::And | SearchFilter::Or | SearchFilter::Not) => {
|
||||
stack.push(state);
|
||||
state = State { op, bm: None };
|
||||
continue;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(prev_state) = stack.pop() {
|
||||
let bm = state.bm;
|
||||
state = prev_state;
|
||||
bm
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
SearchFilter::Operator { .. } => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Apply logical operation
|
||||
if let Some(dest) = &mut state.bm {
|
||||
match state.op {
|
||||
SearchFilter::And => {
|
||||
if let Some(result) = result {
|
||||
dest.bitand_assign(result);
|
||||
} else {
|
||||
dest.clear();
|
||||
}
|
||||
}
|
||||
SearchFilter::Or => {
|
||||
if let Some(result) = result {
|
||||
dest.bitor_assign(result);
|
||||
}
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
if let Some(mut result) = result {
|
||||
result.bitxor_assign(&mask);
|
||||
dest.bitand_assign(result);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if let Some(ref mut result_) = result {
|
||||
if let SearchFilter::Not = state.op {
|
||||
result_.bitxor_assign(&mask);
|
||||
}
|
||||
state.bm = result;
|
||||
} else if let SearchFilter::Not = state.op {
|
||||
state.bm = Some(mask.clone());
|
||||
} else {
|
||||
state.bm = Some(RoaringBitmap::new());
|
||||
}
|
||||
|
||||
// And short-circuit
|
||||
if matches!(state.op, SearchFilter::And) && state.bm.as_ref().unwrap().is_empty() {
|
||||
while let Some(filter) = filters.peek() {
|
||||
if matches!(filter, SearchFilter::End) {
|
||||
break;
|
||||
} else {
|
||||
filters.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AND with mask
|
||||
let mut results = state.bm.unwrap_or_default();
|
||||
results.bitand_assign(&mask);
|
||||
QueryResults {
|
||||
results,
|
||||
comparators: self.comparators,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryResults {
|
||||
pub fn new(results: RoaringBitmap, comparators: Vec<SearchComparator>) -> Self {
|
||||
Self {
|
||||
results,
|
||||
comparators,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_comparators(mut self, comparators: Vec<SearchComparator>) -> Self {
|
||||
if self.comparators.is_empty() {
|
||||
self.comparators = comparators;
|
||||
} else {
|
||||
self.comparators.extend(comparators);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn results(&self) -> &RoaringBitmap {
|
||||
&self.results
|
||||
}
|
||||
|
||||
pub fn update_results(&mut self, results: RoaringBitmap) {
|
||||
self.results = results;
|
||||
}
|
||||
|
||||
pub fn into_bitmap(self) -> RoaringBitmap {
|
||||
self.results
|
||||
}
|
||||
|
||||
pub fn into_sorted(self) -> Vec<u32> {
|
||||
let comparators = self.comparators;
|
||||
let mut results = self.results.into_iter().collect::<Vec<u32>>();
|
||||
|
||||
if !results.is_empty() && !comparators.is_empty() {
|
||||
results.sort_by(|a, b| {
|
||||
for comparator in &comparators {
|
||||
let (a, b, is_ascending) = match comparator {
|
||||
SearchComparator::DocumentSet { set, ascending } => {
|
||||
(set.contains(*a) as u32, set.contains(*b) as u32, *ascending)
|
||||
}
|
||||
SearchComparator::SortedSet { set, ascending } => {
|
||||
let missing = if *ascending { u32::MAX } else { 0 };
|
||||
(
|
||||
*set.get(a).unwrap_or(&missing),
|
||||
*set.get(b).unwrap_or(&missing),
|
||||
*ascending,
|
||||
)
|
||||
}
|
||||
SearchComparator::Field { .. } => continue,
|
||||
};
|
||||
|
||||
let ordering = if is_ascending { a.cmp(&b) } else { b.cmp(&a) };
|
||||
|
||||
if ordering != Ordering::Equal {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
});
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod bm_u32;
|
||||
pub mod bm_u64;
|
||||
pub mod document;
|
||||
pub mod fields;
|
||||
pub mod index;
|
||||
pub mod local;
|
||||
pub mod query;
|
||||
pub mod split;
|
||||
pub mod term;
|
||||
|
||||
use crate::write::SearchIndex;
|
||||
use ahash::AHashMap;
|
||||
use nlp::language::Language;
|
||||
use roaring::RoaringBitmap;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fmt::Display;
|
||||
use std::ops::{BitAndAssign, BitOrAssign, BitXorAssign};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SearchOperator {
|
||||
LowerThan,
|
||||
LowerEqualThan,
|
||||
GreaterThan,
|
||||
GreaterEqualThan,
|
||||
Equal,
|
||||
Contains,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SearchField {
|
||||
AccountId,
|
||||
DocumentId,
|
||||
Id,
|
||||
Email(EmailSearchField),
|
||||
Calendar(CalendarSearchField),
|
||||
Contact(ContactSearchField),
|
||||
File(FileSearchField),
|
||||
Tracing(TracingSearchField),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum EmailSearchField {
|
||||
From,
|
||||
To,
|
||||
Cc,
|
||||
Bcc,
|
||||
Subject,
|
||||
Body,
|
||||
Attachment,
|
||||
ReceivedAt,
|
||||
SentAt,
|
||||
Size,
|
||||
HasAttachment,
|
||||
Headers,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum CalendarSearchField {
|
||||
Title,
|
||||
Description,
|
||||
Location,
|
||||
Owner,
|
||||
Attendee,
|
||||
Start,
|
||||
Uid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ContactSearchField {
|
||||
Member,
|
||||
Kind,
|
||||
Name,
|
||||
Nickname,
|
||||
Organization,
|
||||
Email,
|
||||
Phone,
|
||||
OnlineService,
|
||||
Address,
|
||||
Note,
|
||||
Uid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum FileSearchField {
|
||||
Name,
|
||||
Content,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TracingSearchField {
|
||||
EventType,
|
||||
QueueId,
|
||||
Keywords,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SearchValue {
|
||||
Text { value: String, language: Language },
|
||||
KeyValues(VecMap<String, String>),
|
||||
Int(i64),
|
||||
Uint(u64),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
pub trait SearchDocumentId: Sized + Copy + Display {
|
||||
fn from_u64(id: u64) -> Self;
|
||||
fn field() -> SearchField;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchQuery {
|
||||
pub(crate) index: SearchIndex,
|
||||
pub(crate) filters: Vec<SearchFilter>,
|
||||
pub(crate) comparators: Vec<SearchComparator>,
|
||||
pub(crate) mask: RoaringBitmap,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub enum SearchFilter {
|
||||
Operator {
|
||||
field: SearchField,
|
||||
op: SearchOperator,
|
||||
value: SearchValue,
|
||||
},
|
||||
DocumentSet(RoaringBitmap),
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
#[default]
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SearchComparator {
|
||||
Field {
|
||||
field: SearchField,
|
||||
ascending: bool,
|
||||
},
|
||||
DocumentSet {
|
||||
set: RoaringBitmap,
|
||||
ascending: bool,
|
||||
},
|
||||
SortedSet {
|
||||
set: AHashMap<u32, u32>,
|
||||
ascending: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IndexDocument {
|
||||
pub(crate) index: SearchIndex,
|
||||
pub(crate) fields: AHashMap<SearchField, SearchValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QueryResults {
|
||||
results: RoaringBitmap,
|
||||
comparators: Vec<SearchComparator>,
|
||||
}
|
||||
|
||||
impl From<EmailSearchField> for SearchField {
|
||||
fn from(field: EmailSearchField) -> Self {
|
||||
SearchField::Email(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CalendarSearchField> for SearchField {
|
||||
fn from(field: CalendarSearchField) -> Self {
|
||||
SearchField::Calendar(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContactSearchField> for SearchField {
|
||||
fn from(field: ContactSearchField) -> Self {
|
||||
SearchField::Contact(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileSearchField> for SearchField {
|
||||
fn from(field: FileSearchField) -> Self {
|
||||
SearchField::File(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TracingSearchField> for SearchField {
|
||||
fn from(field: TracingSearchField) -> Self {
|
||||
SearchField::Tracing(field)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for SearchValue {
|
||||
fn from(value: u64) -> Self {
|
||||
SearchValue::Uint(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for SearchValue {
|
||||
fn from(value: i64) -> Self {
|
||||
SearchValue::Int(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for SearchValue {
|
||||
fn from(value: u32) -> Self {
|
||||
SearchValue::Uint(value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for SearchValue {
|
||||
fn from(value: i32) -> Self {
|
||||
SearchValue::Int(value as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for SearchValue {
|
||||
fn from(value: usize) -> Self {
|
||||
SearchValue::Uint(value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for SearchValue {
|
||||
fn from(value: bool) -> Self {
|
||||
SearchValue::Boolean(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SearchValue {
|
||||
fn from(value: String) -> Self {
|
||||
SearchValue::Text {
|
||||
value,
|
||||
language: Language::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchDocumentId for u32 {
|
||||
fn from_u64(id: u64) -> Self {
|
||||
id as u32
|
||||
}
|
||||
|
||||
fn field() -> SearchField {
|
||||
SearchField::DocumentId
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchDocumentId for u64 {
|
||||
fn from_u64(id: u64) -> Self {
|
||||
id
|
||||
}
|
||||
|
||||
fn field() -> SearchField {
|
||||
SearchField::Id
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SearchableField: Sized {
|
||||
fn index() -> SearchIndex;
|
||||
fn primary_keys() -> &'static [SearchField];
|
||||
fn all_fields() -> &'static [SearchField];
|
||||
fn is_indexed(&self) -> bool;
|
||||
fn is_text(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Eq for SearchFilter {}
|
||||
|
||||
impl SearchIndex {
|
||||
pub fn index_name(&self) -> &'static str {
|
||||
match self {
|
||||
SearchIndex::Email => "st_email",
|
||||
SearchIndex::Calendar => "st_calendar",
|
||||
SearchIndex::Contacts => "st_contact",
|
||||
SearchIndex::File => "st_file",
|
||||
SearchIndex::Tracing => "st_tracing",
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchField {
|
||||
pub fn field_name(&self) -> &'static str {
|
||||
match self {
|
||||
SearchField::AccountId => "acc_id",
|
||||
SearchField::DocumentId => "doc_id",
|
||||
SearchField::Id => "id",
|
||||
SearchField::Email(field) => match field {
|
||||
EmailSearchField::From => "from",
|
||||
EmailSearchField::To => "to",
|
||||
EmailSearchField::Cc => "cc",
|
||||
EmailSearchField::Bcc => "bcc",
|
||||
EmailSearchField::Subject => "subj",
|
||||
EmailSearchField::Body => "body",
|
||||
EmailSearchField::Attachment => "attach",
|
||||
EmailSearchField::ReceivedAt => "rcvd",
|
||||
EmailSearchField::SentAt => "sent",
|
||||
EmailSearchField::Size => "size",
|
||||
EmailSearchField::HasAttachment => "has_att",
|
||||
EmailSearchField::Headers => "headers",
|
||||
},
|
||||
SearchField::Calendar(field) => match field {
|
||||
CalendarSearchField::Title => "title",
|
||||
CalendarSearchField::Description => "desc",
|
||||
CalendarSearchField::Location => "loc",
|
||||
CalendarSearchField::Owner => "owner",
|
||||
CalendarSearchField::Attendee => "attendee",
|
||||
CalendarSearchField::Start => "start",
|
||||
CalendarSearchField::Uid => "uid",
|
||||
},
|
||||
SearchField::Contact(field) => match field {
|
||||
ContactSearchField::Member => "member",
|
||||
ContactSearchField::Kind => "kind",
|
||||
ContactSearchField::Name => "name",
|
||||
ContactSearchField::Nickname => "nick",
|
||||
ContactSearchField::Organization => "org",
|
||||
ContactSearchField::Email => "email",
|
||||
ContactSearchField::Phone => "phone",
|
||||
ContactSearchField::OnlineService => "online",
|
||||
ContactSearchField::Address => "addr",
|
||||
ContactSearchField::Note => "note",
|
||||
ContactSearchField::Uid => "uid",
|
||||
},
|
||||
SearchField::File(field) => match field {
|
||||
FileSearchField::Name => "name",
|
||||
FileSearchField::Content => "content",
|
||||
},
|
||||
SearchField::Tracing(field) => match field {
|
||||
TracingSearchField::EventType => "ev_type",
|
||||
TracingSearchField::QueueId => "queue_id",
|
||||
TracingSearchField::Keywords => "keywords",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Store,
|
||||
backend::MAX_TOKEN_LENGTH,
|
||||
search::{
|
||||
QueryResults, SearchComparator, SearchField, SearchFilter, SearchOperator, SearchQuery,
|
||||
SearchValue,
|
||||
bm_u32::{BitmapCache, range_to_bitmap, sort_order},
|
||||
bm_u64::{TreemapCache, range_to_treemap},
|
||||
},
|
||||
write::SEARCH_INDEX_MAX_FIELD_LEN,
|
||||
};
|
||||
use nlp::{language::stemmer::Stemmer, tokenizers::space::SpaceTokenizer};
|
||||
use roaring::{RoaringBitmap, RoaringTreemap};
|
||||
use std::ops::{BitAndAssign, BitOrAssign, BitXorAssign};
|
||||
use utils::cheeky_hash::CheekyHash;
|
||||
|
||||
impl Store {
|
||||
pub(crate) async fn query_account(&self, query: SearchQuery) -> trc::Result<Vec<u32>> {
|
||||
struct State {
|
||||
pub op: SearchFilter,
|
||||
pub bm: Option<RoaringBitmap>,
|
||||
}
|
||||
let mut state: State = State {
|
||||
op: SearchFilter::And,
|
||||
bm: None,
|
||||
};
|
||||
let mut stack = Vec::new();
|
||||
let mask = query.mask;
|
||||
let mut bitmaps = BitmapCache::default();
|
||||
let mut account_id = u32::MAX;
|
||||
|
||||
for filter in &query.filters {
|
||||
if let SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
value: SearchValue::Uint(id),
|
||||
..
|
||||
} = filter
|
||||
{
|
||||
account_id = *id as u32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if account_id == u32::MAX {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Account ID must be specified before other filters"));
|
||||
}
|
||||
|
||||
let mut results;
|
||||
if query.filters.len() > 1 {
|
||||
let mut filters = query.filters.into_iter().peekable();
|
||||
while let Some(filter) = filters.next() {
|
||||
let mut result = match filter {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if matches!(field, SearchField::AccountId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if field.is_text()
|
||||
&& matches!(op, SearchOperator::Contains | SearchOperator::Equal)
|
||||
{
|
||||
let (value, language) = match value {
|
||||
SearchValue::Text { value, language } => (value, language),
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected text value for text field"));
|
||||
}
|
||||
};
|
||||
|
||||
if op == SearchOperator::Equal {
|
||||
bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
language
|
||||
.tokenize_text(&value, MAX_TOKEN_LENGTH)
|
||||
.map(|token| CheekyHash::new(token.word.as_bytes())),
|
||||
field.u8_id(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
let mut result = RoaringBitmap::new();
|
||||
for token in Stemmer::new(&value, language, MAX_TOKEN_LENGTH) {
|
||||
let mut tokens = Vec::with_capacity(3);
|
||||
tokens.push(CheekyHash::new(token.word.as_bytes()));
|
||||
tokens.push(CheekyHash::new(
|
||||
format!("{}*", token.word).as_bytes(),
|
||||
));
|
||||
if let Some(stemmed_word) = token.stemmed_word {
|
||||
tokens.push(CheekyHash::new(
|
||||
format!("{stemmed_word}*").as_bytes(),
|
||||
));
|
||||
}
|
||||
let union = bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
tokens.into_iter(),
|
||||
field.u8_id(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
if let Some(union) = union {
|
||||
if result.is_empty() {
|
||||
result = union;
|
||||
} else {
|
||||
result.bitand_assign(&union);
|
||||
if result.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !result.is_empty() {
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if field.is_json() {
|
||||
let (key, value) = match value {
|
||||
SearchValue::KeyValues(kv) => kv.into_iter().next().unwrap(),
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected text value for text field"));
|
||||
}
|
||||
};
|
||||
|
||||
if !value.is_empty() {
|
||||
bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
SpaceTokenizer::new(value.as_str(), MAX_TOKEN_LENGTH).map(
|
||||
|value| {
|
||||
CheekyHash::new(format!("{key} {value}").as_bytes())
|
||||
},
|
||||
),
|
||||
field.u8_id(),
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
bitmaps
|
||||
.merge_bitmaps(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
[CheekyHash::new(key.as_bytes())].into_iter(),
|
||||
field.u8_id(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
} else if field.is_indexed() {
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => {
|
||||
let mut value = value.into_bytes();
|
||||
value.truncate(SEARCH_INDEX_MAX_FIELD_LEN);
|
||||
value
|
||||
}
|
||||
SearchValue::Int(v) => (v as u64).to_be_bytes().to_vec(),
|
||||
SearchValue::Uint(v) => v.to_be_bytes().to_vec(),
|
||||
SearchValue::Boolean(v) => vec![v as u8],
|
||||
SearchValue::KeyValues(_) => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected non key-value for non-text field"));
|
||||
}
|
||||
};
|
||||
|
||||
range_to_bitmap(
|
||||
self,
|
||||
query.index,
|
||||
account_id,
|
||||
field.u8_id(),
|
||||
&value,
|
||||
op,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Field {field:?} is not indexed")));
|
||||
}
|
||||
}
|
||||
SearchFilter::DocumentSet(bitmap) => Some(bitmap),
|
||||
op @ (SearchFilter::And | SearchFilter::Or | SearchFilter::Not) => {
|
||||
stack.push(state);
|
||||
state = State { op, bm: None };
|
||||
continue;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(prev_state) = stack.pop() {
|
||||
let bm = state.bm;
|
||||
state = prev_state;
|
||||
bm
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply logical operation
|
||||
if let Some(dest) = &mut state.bm {
|
||||
match state.op {
|
||||
SearchFilter::And => {
|
||||
if let Some(result) = result {
|
||||
dest.bitand_assign(result);
|
||||
} else {
|
||||
dest.clear();
|
||||
}
|
||||
}
|
||||
SearchFilter::Or => {
|
||||
if let Some(result) = result {
|
||||
dest.bitor_assign(result);
|
||||
}
|
||||
}
|
||||
SearchFilter::Not => {
|
||||
if let Some(mut result) = result {
|
||||
result.bitxor_assign(&mask);
|
||||
dest.bitand_assign(result);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if let Some(result_) = &mut result {
|
||||
if let SearchFilter::Not = state.op {
|
||||
result_.bitxor_assign(&mask);
|
||||
}
|
||||
state.bm = result;
|
||||
} else if let SearchFilter::Not = state.op {
|
||||
state.bm = Some(mask.clone());
|
||||
} else {
|
||||
state.bm = Some(RoaringBitmap::new());
|
||||
}
|
||||
|
||||
// And short circuit
|
||||
if matches!(state.op, SearchFilter::And) && state.bm.as_ref().unwrap().is_empty() {
|
||||
while let Some(filter) = filters.peek() {
|
||||
if matches!(filter, SearchFilter::End) {
|
||||
break;
|
||||
} else {
|
||||
filters.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = state.bm.unwrap_or_default();
|
||||
results.bitand_assign(&mask);
|
||||
} else {
|
||||
results = mask;
|
||||
}
|
||||
|
||||
if results.len() > 1 && !query.comparators.is_empty() {
|
||||
let mut comparators = Vec::with_capacity(query.comparators.len());
|
||||
for comparator in query.comparators {
|
||||
let comparator = match comparator {
|
||||
SearchComparator::Field { field, ascending } => SearchComparator::SortedSet {
|
||||
set: sort_order(self, query.index, account_id, field.u8_id()).await?,
|
||||
ascending,
|
||||
},
|
||||
_ => comparator,
|
||||
};
|
||||
|
||||
comparators.push(comparator);
|
||||
}
|
||||
|
||||
Ok(QueryResults::new(results, comparators).into_sorted())
|
||||
} else {
|
||||
Ok(results.into_iter().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn query_global(&self, query: SearchQuery) -> trc::Result<Vec<u64>> {
|
||||
struct State {
|
||||
pub op: SearchFilter,
|
||||
pub bm: Option<RoaringTreemap>,
|
||||
}
|
||||
let mut state: State = State {
|
||||
op: SearchFilter::And,
|
||||
bm: None,
|
||||
};
|
||||
let mut stack = Vec::new();
|
||||
let mut filters = query.filters.into_iter().peekable();
|
||||
let mut bitmaps = TreemapCache::default();
|
||||
|
||||
while let Some(filter) = filters.next() {
|
||||
let result = match filter {
|
||||
SearchFilter::Operator { field, op, value } => {
|
||||
if field.is_text() {
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => value,
|
||||
_ => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected text value for text field"));
|
||||
}
|
||||
};
|
||||
|
||||
bitmaps
|
||||
.merge_treemaps(
|
||||
self,
|
||||
query.index,
|
||||
SpaceTokenizer::new(value.as_str(), MAX_TOKEN_LENGTH)
|
||||
.map(|word| CheekyHash::new(word.as_bytes())),
|
||||
field.u8_id(),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
} else if field.is_indexed() || matches!(field, SearchField::Id) {
|
||||
let value = match value {
|
||||
SearchValue::Text { value, .. } => value.into_bytes(),
|
||||
SearchValue::Int(v) => (v as u64).to_be_bytes().to_vec(),
|
||||
SearchValue::Uint(v) => v.to_be_bytes().to_vec(),
|
||||
SearchValue::Boolean(v) => vec![v as u8],
|
||||
SearchValue::KeyValues(_) => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Expected non key-value for non-text field"));
|
||||
}
|
||||
};
|
||||
|
||||
range_to_treemap(self, query.index, field.u8_id(), &value, op).await?
|
||||
} else {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details(format!("Field {field:?} is not indexed")));
|
||||
}
|
||||
}
|
||||
SearchFilter::DocumentSet(_) | SearchFilter::Not => {
|
||||
return Err(trc::StoreEvent::UnexpectedError
|
||||
.into_err()
|
||||
.details("Unsupported filter in global search"));
|
||||
}
|
||||
op @ (SearchFilter::And | SearchFilter::Or) => {
|
||||
stack.push(state);
|
||||
state = State { op, bm: None };
|
||||
continue;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(prev_state) = stack.pop() {
|
||||
let bm = state.bm;
|
||||
state = prev_state;
|
||||
bm
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply logical operation
|
||||
if let Some(dest) = &mut state.bm {
|
||||
match state.op {
|
||||
SearchFilter::And => {
|
||||
if let Some(result) = result {
|
||||
dest.bitand_assign(result);
|
||||
} else {
|
||||
dest.clear();
|
||||
}
|
||||
}
|
||||
SearchFilter::Or => {
|
||||
if let Some(result) = result {
|
||||
dest.bitor_assign(result);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else if result.is_some() {
|
||||
state.bm = result;
|
||||
} else {
|
||||
state.bm = Some(RoaringTreemap::new());
|
||||
}
|
||||
|
||||
// And short circuit
|
||||
if matches!(state.op, SearchFilter::And) && state.bm.as_ref().unwrap().is_empty() {
|
||||
while let Some(filter) = filters.peek() {
|
||||
if matches!(filter, SearchFilter::End) {
|
||||
break;
|
||||
} else {
|
||||
filters.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if query.comparators.iter().all(|c| {
|
||||
matches!(
|
||||
c,
|
||||
SearchComparator::Field {
|
||||
field: SearchField::Id,
|
||||
ascending: false
|
||||
}
|
||||
)
|
||||
}) {
|
||||
Ok(state
|
||||
.bm
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect::<Vec<_>>())
|
||||
} else {
|
||||
Ok(state.bm.unwrap_or_default().into_iter().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::search::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SplitFilter {
|
||||
Internal(SearchFilter),
|
||||
External(Vec<SearchFilter>),
|
||||
}
|
||||
|
||||
pub(crate) fn split_filters(filters_in: Vec<SearchFilter>) -> Option<Vec<SplitFilter>> {
|
||||
let mut account_id = u64::MAX;
|
||||
let mut filters: Vec<SearchFilter> = Vec::with_capacity(filters_in.len());
|
||||
let mut op_stack = Vec::new();
|
||||
let mut document_sets: AHashMap<usize, RoaringBitmap> = AHashMap::new();
|
||||
let mut operators: AHashMap<usize, Vec<SearchFilter>> = AHashMap::new();
|
||||
|
||||
for filter in filters_in {
|
||||
match filter {
|
||||
op @ (SearchFilter::And | SearchFilter::Or | SearchFilter::Not) => {
|
||||
op_stack.push(op.clone());
|
||||
filters.push(op);
|
||||
}
|
||||
SearchFilter::End => {
|
||||
if let Some(ops) = operators.remove(&op_stack.len()) {
|
||||
filters.extend(ops);
|
||||
}
|
||||
if let Some(docs) = document_sets.remove(&op_stack.len()) {
|
||||
filters.push(SearchFilter::DocumentSet(docs));
|
||||
}
|
||||
filters.push(SearchFilter::End);
|
||||
op_stack.pop()?;
|
||||
}
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
value: SearchValue::Uint(id),
|
||||
..
|
||||
} => {
|
||||
account_id = id;
|
||||
}
|
||||
SearchFilter::Operator { .. } => {
|
||||
operators.entry(op_stack.len()).or_default().push(filter);
|
||||
}
|
||||
SearchFilter::DocumentSet(docs) => match document_sets.entry(op_stack.len()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if matches!(op_stack.last(), Some(SearchFilter::Or)) {
|
||||
entry.get_mut().bitor_assign(&docs);
|
||||
} else {
|
||||
entry.get_mut().bitand_assign(&docs);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(docs);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ops) = operators.remove(&0) {
|
||||
filters.extend(ops);
|
||||
}
|
||||
if let Some(docs) = document_sets.remove(&0) {
|
||||
filters.push(SearchFilter::DocumentSet(docs));
|
||||
}
|
||||
|
||||
if account_id == u64::MAX {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut split: Vec<SplitFilter> = Vec::new();
|
||||
let mut i = 0;
|
||||
|
||||
'outer: while i < filters.len() {
|
||||
let mut j = i;
|
||||
let mut depth = 0;
|
||||
|
||||
while j < filters.len() {
|
||||
match &filters[j] {
|
||||
SearchFilter::And | SearchFilter::Or | SearchFilter::Not => {
|
||||
depth += 1;
|
||||
}
|
||||
SearchFilter::End => {
|
||||
depth -= 1;
|
||||
if depth < 0 {
|
||||
if j > i {
|
||||
break;
|
||||
} else {
|
||||
split.push(SplitFilter::Internal(SearchFilter::End));
|
||||
i += 1;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchFilter::Operator { .. } => {}
|
||||
SearchFilter::DocumentSet(_) => {
|
||||
if depth == 0 && j > i {
|
||||
break;
|
||||
} else {
|
||||
split.push(SplitFilter::Internal(std::mem::take(&mut filters[i])));
|
||||
i += 1;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
|
||||
let mut external_filters = vec![SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Uint(account_id),
|
||||
}];
|
||||
let add_or =
|
||||
matches!(split.last(), Some(SplitFilter::Internal(SearchFilter::Or))) && j > i + 1;
|
||||
if add_or {
|
||||
external_filters.push(SearchFilter::Or);
|
||||
}
|
||||
external_filters.extend(&mut filters[i..j].iter_mut().map(std::mem::take));
|
||||
if add_or {
|
||||
external_filters.push(SearchFilter::End);
|
||||
}
|
||||
split.push(SplitFilter::External(external_filters));
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
Some(split)
|
||||
}
|
||||
|
||||
// Test cases
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_filters_exhaustive() {
|
||||
let test_cases: Vec<(&str, Vec<SearchFilter>, Vec<SplitFilter>)> = vec![
|
||||
// Test 1: Operator followed by document set at depth 0
|
||||
(
|
||||
"Operator then document set at depth 0",
|
||||
vec![account_id(42), other_op("test"), doc_set(&[1, 2, 3])],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("test")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 2: Document set followed by operator at depth 0
|
||||
(
|
||||
"Document set then operator at depth 0",
|
||||
vec![account_id(42), doc_set(&[1, 2, 3]), other_op("test")],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("test")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 3: Multiple document sets with operator in between
|
||||
(
|
||||
"Multiple document sets at depth 0 with operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
other_op("middle"),
|
||||
doc_set(&[2, 4]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("middle")]),
|
||||
SplitFilter::Internal(doc_set(&[2])),
|
||||
],
|
||||
),
|
||||
// Test 4: Document set at depth 0, then AND group
|
||||
(
|
||||
"Document set then AND group",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 5: AND group followed by document set at depth 0
|
||||
(
|
||||
"AND group then document set",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 6: Operator at depth 0, then OR group, then document set
|
||||
(
|
||||
"Operator, OR group, then document set",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("pre"),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 3]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("pre"),
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 7: Document set, OR group, operator
|
||||
(
|
||||
"Document set, OR group, operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 8: Multiple OR branches with document sets between
|
||||
(
|
||||
"Multiple OR branches with document sets between",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 5, 6]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 9: Document sets at different depths - depth 0 and inside AND
|
||||
(
|
||||
"Document sets at different depths in AND",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[2, 3]),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[2, 3])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 10: Operator, AND group with doc set inside, operator
|
||||
(
|
||||
"Operator, AND(operator, doc_set), operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("pre"),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[1, 2, 3]),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("pre"), other_op("post")]),
|
||||
],
|
||||
),
|
||||
// Test 11: Document set, nested groups, document set
|
||||
(
|
||||
"Doc set, AND(OR(a,b)), doc set",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
doc_set(&[1, 2]),
|
||||
other_op("c"),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[3, 4]),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3, 4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
],
|
||||
),
|
||||
// Test 12: OR with nested AND containing document sets, followed by operator
|
||||
(
|
||||
"OR(AND(doc_set, doc_set), operator) followed by operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
doc_set(&[1, 2]),
|
||||
doc_set(&[2, 3]),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::Internal(doc_set(&[2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("b")]),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("post")]),
|
||||
],
|
||||
),
|
||||
// Test 13: Complex: doc set, AND group, doc set, OR group, doc set
|
||||
(
|
||||
"Complex: doc, AND, doc, OR, doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2, 3]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 3, 5]),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2, 3, 6]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2, 3])),
|
||||
],
|
||||
),
|
||||
// Test 14: Operator, NOT group, document set
|
||||
(
|
||||
"Operator, NOT(operator), document set",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("pre"),
|
||||
SearchFilter::Not,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[1, 2]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Not,
|
||||
other_op("a"),
|
||||
SearchFilter::End,
|
||||
other_op("pre"),
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 15: Document set, NOT group, operator
|
||||
(
|
||||
"Document set, NOT(operator), operator",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::Not,
|
||||
other_op("a"),
|
||||
doc_set(&[3, 4]),
|
||||
SearchFilter::End,
|
||||
other_op("post"),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Not),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[3, 4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("post")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
],
|
||||
),
|
||||
// Test 16: Alternating doc sets and operators
|
||||
(
|
||||
"Alternating: doc, op, doc, op, doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1]),
|
||||
other_op("a"),
|
||||
doc_set(&[1, 2]),
|
||||
other_op("b"),
|
||||
doc_set(&[1, 3]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![account_id(42), other_op("a"), other_op("b")]),
|
||||
SplitFilter::Internal(doc_set(&[1])),
|
||||
],
|
||||
),
|
||||
// Test 17: Multiple operators, then OR group with doc set inside, then doc set
|
||||
(
|
||||
"Multiple ops, OR(op, doc_set), doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
other_op("a"),
|
||||
SearchFilter::Or,
|
||||
other_op("c"),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
doc_set(&[3, 4]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::External(vec![account_id(42), other_op("c")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a"), other_op("b")]),
|
||||
SplitFilter::Internal(doc_set(&[3, 4])),
|
||||
],
|
||||
),
|
||||
// Test 18: Doc set before and after nested OR(AND(op))
|
||||
(
|
||||
"Doc, OR(AND(op)), doc",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1]),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
doc_set(&[2]),
|
||||
],
|
||||
vec![
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
other_op("b"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[])),
|
||||
],
|
||||
),
|
||||
// Test 19: AND group with doc set, operator between, OR group with doc set
|
||||
(
|
||||
"AND(op, doc), op, OR(op, doc)",
|
||||
vec![
|
||||
account_id(42),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[1, 2]),
|
||||
SearchFilter::End,
|
||||
other_op("middle"),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
other_op("c"),
|
||||
doc_set(&[3, 4]),
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[1, 2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::External(vec![
|
||||
account_id(42),
|
||||
SearchFilter::Or,
|
||||
other_op("b"),
|
||||
other_op("c"),
|
||||
SearchFilter::End,
|
||||
]),
|
||||
SplitFilter::Internal(doc_set(&[3, 4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::External(vec![account_id(42), other_op("middle")]),
|
||||
],
|
||||
),
|
||||
// Test 20: Deep nesting with document sets at multiple levels
|
||||
(
|
||||
"Deep nesting: doc, AND(doc, OR(doc, AND(op, doc)))",
|
||||
vec![
|
||||
account_id(42),
|
||||
doc_set(&[1]),
|
||||
SearchFilter::And,
|
||||
doc_set(&[2]),
|
||||
SearchFilter::Or,
|
||||
doc_set(&[3]),
|
||||
SearchFilter::And,
|
||||
other_op("a"),
|
||||
doc_set(&[4]),
|
||||
SearchFilter::End,
|
||||
SearchFilter::End,
|
||||
SearchFilter::End,
|
||||
],
|
||||
vec![
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::Internal(SearchFilter::Or),
|
||||
SplitFilter::Internal(SearchFilter::And),
|
||||
SplitFilter::External(vec![account_id(42), other_op("a")]),
|
||||
SplitFilter::Internal(doc_set(&[4])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[3])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[2])),
|
||||
SplitFilter::Internal(SearchFilter::End),
|
||||
SplitFilter::Internal(doc_set(&[1])),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
for (description, input, expected) in test_cases {
|
||||
println!("------ Running test: {} ------", description);
|
||||
let result = split_filters(input.clone());
|
||||
assert!(result.is_some(), "Test '{}' returned None", description);
|
||||
|
||||
let result = result.unwrap();
|
||||
if result != expected {
|
||||
print_split_filter_code(&result);
|
||||
}
|
||||
assert_eq!(result, expected, "Test '{description}' failed",);
|
||||
}
|
||||
}
|
||||
|
||||
fn account_id(id: u64) -> SearchFilter {
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::AccountId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Uint(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn other_op(value: &str) -> SearchFilter {
|
||||
SearchFilter::Operator {
|
||||
field: SearchField::DocumentId,
|
||||
op: SearchOperator::Equal,
|
||||
value: SearchValue::Text {
|
||||
value: value.to_string(),
|
||||
language: Language::None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn doc_set(ids: &[u32]) -> SearchFilter {
|
||||
let mut bitmap = RoaringBitmap::new();
|
||||
for id in ids {
|
||||
bitmap.insert(*id);
|
||||
}
|
||||
SearchFilter::DocumentSet(bitmap)
|
||||
}
|
||||
|
||||
fn print_split_filter_code(splits: &[SplitFilter]) {
|
||||
println!("vec![");
|
||||
for split in splits {
|
||||
match split {
|
||||
SplitFilter::Internal(filter) => {
|
||||
print!(" SplitFilter::Internal(");
|
||||
print_search_filter_code(filter, 0);
|
||||
println!("),");
|
||||
}
|
||||
SplitFilter::External(filters) => {
|
||||
println!(" SplitFilter::External(vec![");
|
||||
for filter in filters {
|
||||
print!(" ");
|
||||
print_search_filter_code(filter, 2);
|
||||
println!(",");
|
||||
}
|
||||
println!(" ]),");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("]");
|
||||
}
|
||||
|
||||
fn print_search_filter_code(filter: &SearchFilter, indent_level: usize) {
|
||||
let indent = " ".repeat(indent_level);
|
||||
match filter {
|
||||
SearchFilter::Operator { field, op, value } => match (field, op, value) {
|
||||
(SearchField::AccountId, SearchOperator::Equal, SearchValue::Uint(id)) => {
|
||||
print!("account_id({})", id);
|
||||
}
|
||||
(
|
||||
SearchField::DocumentId,
|
||||
SearchOperator::Equal,
|
||||
SearchValue::Text { value, .. },
|
||||
) => {
|
||||
print!("other_op(\"{}\")", value);
|
||||
}
|
||||
_ => {
|
||||
println!("SearchFilter::Operator {{");
|
||||
println!("{} field: {:?},", indent, field);
|
||||
println!("{} op: {:?},", indent, op);
|
||||
println!("{} value: {:?},", indent, value);
|
||||
print!("{}}}", indent);
|
||||
}
|
||||
},
|
||||
SearchFilter::DocumentSet(bitmap) => {
|
||||
let ids: Vec<u32> = bitmap.iter().collect();
|
||||
if ids.is_empty() {
|
||||
print!("doc_set(&[])");
|
||||
} else if ids.len() <= 5 {
|
||||
print!("doc_set(&[");
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
if i > 0 {
|
||||
print!(", ");
|
||||
}
|
||||
print!("{}", id);
|
||||
}
|
||||
print!("])");
|
||||
} else {
|
||||
// For large bitmaps, create inline
|
||||
println!("{{");
|
||||
println!("{} let mut bitmap = RoaringBitmap::new();", indent);
|
||||
for id in ids {
|
||||
println!("{} bitmap.insert({});", indent, id);
|
||||
}
|
||||
print!("{} doc_set_bitmap(bitmap)", indent);
|
||||
println!();
|
||||
print!("{}}}", indent);
|
||||
}
|
||||
}
|
||||
SearchFilter::And => print!("SearchFilter::And"),
|
||||
SearchFilter::Or => print!("SearchFilter::Or"),
|
||||
SearchFilter::Not => print!("SearchFilter::Not"),
|
||||
SearchFilter::End => print!("SearchFilter::End"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
Serialize,
|
||||
backend::MAX_TOKEN_LENGTH,
|
||||
search::*,
|
||||
write::{
|
||||
Archiver, BatchBuilder, SEARCH_INDEX_MAX_FIELD_LEN, SearchIndexClass, SearchIndexField,
|
||||
SearchIndexId, SearchIndexType, ValueClass,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use nlp::{
|
||||
language::stemmer::Stemmer,
|
||||
tokenizers::{space::SpaceTokenizer, word::WordTokenizer},
|
||||
};
|
||||
use utils::{
|
||||
cheeky_hash::{CheekyBTreeMap, CheekyHash},
|
||||
map::bitmap::BitPop,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
|
||||
pub(crate) struct TermIndex {
|
||||
terms: Vec<Term>,
|
||||
fields: Vec<SearchIndexField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)]
|
||||
pub(crate) struct Term {
|
||||
hash: CheekyHash,
|
||||
fields: u32,
|
||||
}
|
||||
|
||||
pub(crate) struct TermIndexBuilder {
|
||||
pub(crate) index: TermIndex,
|
||||
pub(crate) id: SearchIndexId,
|
||||
}
|
||||
|
||||
impl TermIndexBuilder {
|
||||
pub fn build(document: IndexDocument, truncate_at: usize) -> Self {
|
||||
let mut terms: CheekyBTreeMap<u32> = CheekyBTreeMap::new();
|
||||
let mut fields: Vec<SearchIndexField> = Vec::new();
|
||||
let mut account_id = None;
|
||||
let mut document_id = None;
|
||||
let mut id = None;
|
||||
|
||||
for (field, value) in document.fields {
|
||||
match field {
|
||||
SearchField::Id => {
|
||||
if let SearchValue::Uint(v) = value {
|
||||
fields.push(SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: v.to_be_bytes().to_vec(),
|
||||
});
|
||||
id = Some(v);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
SearchField::AccountId => {
|
||||
if let SearchValue::Uint(v) = value {
|
||||
account_id = Some(v);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
SearchField::DocumentId => {
|
||||
if let SearchValue::Uint(v) = value {
|
||||
document_id = Some(v);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let field = match value {
|
||||
SearchValue::Text { value, language } => {
|
||||
if field.is_text() {
|
||||
let value = if truncate_at > 0 && value.len() > truncate_at {
|
||||
let pos = value.floor_char_boundary(truncate_at);
|
||||
&value[..pos]
|
||||
} else {
|
||||
&value
|
||||
};
|
||||
|
||||
match language {
|
||||
Language::Unknown => {
|
||||
for token in WordTokenizer::new(value, MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(token.word.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
Language::None => {
|
||||
for token in SpaceTokenizer::new(value, MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(token.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for token in Stemmer::new(value, language, MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(token.word.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
|
||||
if let Some(stemmed_word) = token.stemmed_word {
|
||||
terms
|
||||
.entry(CheekyHash::new(
|
||||
format!("{}*", stemmed_word).as_bytes(),
|
||||
))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if field.is_indexed() {
|
||||
let mut data = value.into_bytes();
|
||||
data.truncate(SEARCH_INDEX_MAX_FIELD_LEN);
|
||||
|
||||
SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data,
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
SearchValue::KeyValues(map) => {
|
||||
for (key, value) in map {
|
||||
terms
|
||||
.entry(CheekyHash::new(key.as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
for token in SpaceTokenizer::new(value.as_str(), MAX_TOKEN_LENGTH) {
|
||||
terms
|
||||
.entry(CheekyHash::new(format!("{key} {token}").as_bytes()))
|
||||
.or_default()
|
||||
.bit_push(field.u8_id());
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
SearchValue::Int(v) => SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: (v as u64).to_be_bytes().to_vec(),
|
||||
},
|
||||
SearchValue::Uint(v) => SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: v.to_be_bytes().to_vec(),
|
||||
},
|
||||
SearchValue::Boolean(v) => SearchIndexField {
|
||||
field_id: field.u8_id(),
|
||||
data: vec![v as u8],
|
||||
},
|
||||
};
|
||||
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
TermIndexBuilder {
|
||||
index: TermIndex {
|
||||
terms: terms
|
||||
.into_iter()
|
||||
.map(|(k, v)| Term { hash: k, fields: v })
|
||||
.collect(),
|
||||
fields,
|
||||
},
|
||||
id: match (account_id, document_id, id) {
|
||||
(Some(account_id), Some(document_id), None) => SearchIndexId::Account {
|
||||
account_id: account_id as u32,
|
||||
document_id: document_id as u32,
|
||||
},
|
||||
(None, None, Some(id)) => SearchIndexId::Global { id },
|
||||
_ => {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Invalid combination of AccountId {account_id:?}, DocumentId {document_id:?} and Id {id:?} fields"
|
||||
);
|
||||
SearchIndexId::Global { id: 0 }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TermIndex {
|
||||
pub fn write_index(
|
||||
self,
|
||||
batch: &mut BatchBuilder,
|
||||
index: SearchIndex,
|
||||
id: SearchIndexId,
|
||||
) -> trc::Result<()> {
|
||||
let archive = Archiver::new(self);
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
archive.serialize()?,
|
||||
)
|
||||
.commit_point();
|
||||
|
||||
for term in archive.inner.terms {
|
||||
let mut fields = term.fields;
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Term {
|
||||
hash: term.hash,
|
||||
field,
|
||||
},
|
||||
}),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
for field in archive.inner.fields {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index { field },
|
||||
}),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn merge_index(
|
||||
self,
|
||||
batch: &mut BatchBuilder,
|
||||
index: SearchIndex,
|
||||
id: SearchIndexId,
|
||||
old_term: &ArchivedTermIndex,
|
||||
) -> trc::Result<()> {
|
||||
let archive = Archiver::new(self);
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Document,
|
||||
}),
|
||||
archive.serialize()?,
|
||||
)
|
||||
.commit_point();
|
||||
|
||||
let mut old_terms = AHashSet::with_capacity(old_term.terms.len());
|
||||
let mut old_fields = AHashSet::with_capacity(old_term.fields.len());
|
||||
for term in old_term.terms.iter() {
|
||||
let mut fields = term.fields.to_native();
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
old_terms.insert(SearchIndexType::Term {
|
||||
hash: term.hash.to_native(),
|
||||
field,
|
||||
});
|
||||
}
|
||||
}
|
||||
for field in old_term.fields.iter() {
|
||||
old_fields.insert(SearchIndexField {
|
||||
field_id: field.field_id,
|
||||
data: field.data.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
for term in archive.inner.terms {
|
||||
let mut fields = term.fields;
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
let typ = SearchIndexType::Term {
|
||||
hash: term.hash,
|
||||
field,
|
||||
};
|
||||
|
||||
if !old_terms.remove(&typ) {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass { index, id, typ }),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for field in archive.inner.fields {
|
||||
if !old_fields.remove(&field) {
|
||||
batch
|
||||
.set(
|
||||
ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index { field },
|
||||
}),
|
||||
vec![],
|
||||
)
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
for typ in old_terms {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass { index, id, typ }))
|
||||
.commit_point();
|
||||
}
|
||||
|
||||
for field in old_fields {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index { field },
|
||||
}))
|
||||
.commit_point();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchivedTermIndex {
|
||||
pub fn delete_index(&self, batch: &mut BatchBuilder, index: SearchIndex, id: SearchIndexId) {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Document,
|
||||
}))
|
||||
.commit_point();
|
||||
|
||||
for term in self.terms.iter() {
|
||||
let mut fields = term.fields.to_native();
|
||||
while let Some(field) = fields.bit_pop() {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Term {
|
||||
hash: term.hash.to_native(),
|
||||
field,
|
||||
},
|
||||
}))
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
for field in self.fields.iter() {
|
||||
batch
|
||||
.clear(ValueClass::SearchIndex(SearchIndexClass {
|
||||
index,
|
||||
id,
|
||||
typ: SearchIndexType::Index {
|
||||
field: SearchIndexField {
|
||||
field_id: field.field_id,
|
||||
data: field.data.to_vec(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchIndex {
|
||||
pub(crate) fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
SearchIndex::Email => 0,
|
||||
SearchIndex::Calendar => 1,
|
||||
SearchIndex::Contacts => 2,
|
||||
SearchIndex::File => 3,
|
||||
SearchIndex::Tracing => 4,
|
||||
SearchIndex::InMemory => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchField {
|
||||
pub(crate) fn u8_id(&self) -> u8 {
|
||||
match self {
|
||||
SearchField::AccountId => 0,
|
||||
SearchField::DocumentId => 1,
|
||||
SearchField::Id => 2,
|
||||
SearchField::Email(field) => match field {
|
||||
EmailSearchField::From => 3,
|
||||
EmailSearchField::To => 4,
|
||||
EmailSearchField::Cc => 5,
|
||||
EmailSearchField::Bcc => 6,
|
||||
EmailSearchField::Subject => 7,
|
||||
EmailSearchField::Body => 8,
|
||||
EmailSearchField::Attachment => 9,
|
||||
EmailSearchField::ReceivedAt => 10,
|
||||
EmailSearchField::SentAt => 11,
|
||||
EmailSearchField::Size => 12,
|
||||
EmailSearchField::HasAttachment => 13,
|
||||
EmailSearchField::Headers => 14,
|
||||
},
|
||||
SearchField::Calendar(field) => match field {
|
||||
CalendarSearchField::Title => 3,
|
||||
CalendarSearchField::Description => 4,
|
||||
CalendarSearchField::Location => 5,
|
||||
CalendarSearchField::Owner => 6,
|
||||
CalendarSearchField::Attendee => 7,
|
||||
CalendarSearchField::Start => 8,
|
||||
CalendarSearchField::Uid => 9,
|
||||
},
|
||||
SearchField::Contact(field) => match field {
|
||||
ContactSearchField::Member => 3,
|
||||
ContactSearchField::Kind => 4,
|
||||
ContactSearchField::Name => 5,
|
||||
ContactSearchField::Nickname => 6,
|
||||
ContactSearchField::Organization => 7,
|
||||
ContactSearchField::Email => 8,
|
||||
ContactSearchField::Phone => 9,
|
||||
ContactSearchField::OnlineService => 10,
|
||||
ContactSearchField::Address => 11,
|
||||
ContactSearchField::Note => 12,
|
||||
ContactSearchField::Uid => 13,
|
||||
},
|
||||
SearchField::File(field) => match field {
|
||||
FileSearchField::Name => 3,
|
||||
FileSearchField::Content => 4,
|
||||
},
|
||||
SearchField::Tracing(field) => match field {
|
||||
TracingSearchField::EventType => 3,
|
||||
TracingSearchField::QueueId => 4,
|
||||
TracingSearchField::Keywords => 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user