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:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+278
View File
@@ -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)
}
+205
View File
@@ -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)
}
}
+315
View File
@@ -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
}
+249
View File
@@ -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!(),
}
}
}
+336
View File
@@ -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(())
}
}
+228
View File
@@ -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
}
}
+340
View File
@@ -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",
},
}
}
}
+423
View File
@@ -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<_>>())
}
}
}
+708
View File
@@ -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"),
}
}
}
+451
View File
@@ -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,
},
}
}
}