use std::collections::BTreeSet;
use crate::content::node::{NodeState, PropertyState, PropertyValues};
use crate::content::{PropertyType, PropertyValue};
use crate::index::lucene::analysis::numeric::{
NumericTerm, date_to_long, double_terms, integer_terms, long_terms,
};
use crate::index::lucene::analysis::{Analyzer, AnalyzerSettings, TokenStreamResult, field_names};
use crate::index::lucene::codec::postings::IndexOptions;
use crate::index::lucene::documents::aggregate::{Match, Matcher};
use crate::index::lucene::documents::binaries::BinaryTextPolicy;
use crate::index::lucene::documents::facets::{FacetDimension, FacetValue, facet_field_name};
use crate::index::lucene::documents::rules::{
IndexingRule, IndexingRules, PropertyDefinition, PropertyInclude,
};
use crate::index::lucene::writer::{DocValue, Document, Field, StoredValue, Token};
use crate::index::{IndexError, IndexResult};
mod aggregation;
const PATH_FIELD: &str = ":path";
const FULLTEXT_FIELD: &str = ":fulltext";
const NODE_NAME_FIELD: &str = ":nodeName";
const NULL_PROPERTIES_FIELD: &str = ":nullProps";
const NOT_NULL_PROPERTIES_FIELD: &str = ":notNullProps";
const DEPTH_FIELD: &str = ":depth";
const ANALYZED_PREFIX: &str = "full:";
const RELATIVE_NODE_PREFIX: &str = "fullnode:";
const DOC_VALUE_PREFIX: &str = ":dv";
const STRING_DOC_VALUE_MAXIMUM_LENGTH: usize = 32_766;
fn truncated_doc_value(text: &str) -> Vec<u8> {
let bytes = text.as_bytes();
if bytes.len() <= STRING_DOC_VALUE_MAXIMUM_LENGTH {
return bytes.to_vec();
}
let mut at = STRING_DOC_VALUE_MAXIMUM_LENGTH - 1;
while bytes[at] & 0b1100_0000 == 0b1000_0000 {
let Some(previous) = at.checked_sub(1) else {
return Vec::new();
};
at = previous;
}
if bytes[at] & 0b1100_0000 == 0b1100_0000 {
let Some(previous) = at.checked_sub(1) else {
return Vec::new();
};
at = previous;
}
bytes[..=at].to_vec()
}
#[derive(Clone, Debug)]
pub struct MadeDocument {
pub document: Document,
pub facet_dimensions: Vec<FacetDimension>,
}
pub struct DocumentMaker<'definition> {
definition_path: &'definition str,
rules: &'definition IndexingRules,
analyzer: Analyzer,
binaries: BinaryTextPolicy,
}
impl<'definition> DocumentMaker<'definition> {
#[must_use]
pub fn new(
definition_path: &'definition str,
rules: &'definition IndexingRules,
binaries: BinaryTextPolicy,
) -> Self {
let analyzer = Analyzer::new(AnalyzerSettings {
maximum_field_length: rules.maximum_field_length.map_or_else(
|| Some(crate::index::lucene::analysis::DEFAULT_MAXIMUM_FIELD_LENGTH),
|length| usize::try_from(length).ok(),
),
index_original_term: rules.index_original_term,
evaluate_path_restrictions: rules.evaluate_path_restrictions,
suggest_analyzed: rules.suggest_analyzed,
});
Self {
definition_path,
rules,
analyzer,
binaries,
}
}
pub fn make(
&self,
node: &NodeState<'_>,
path: &str,
rule: &IndexingRule,
) -> IndexResult<Option<MadeDocument>> {
let mut state = DocumentState::new(path);
state.add(string_field(PATH_FIELD, path, true));
self.index_properties(node, path, rule, &mut state)?;
self.index_aggregates(node, rule, &mut state)?;
Self::index_markers(node, rule, &mut state)?;
Self::add_node_name_field(path, rule, &mut state);
if !rule.indexes_all_nodes_of_matching_type && !state.dirty {
return Ok(None);
}
self.index_node_name(path, rule, &mut state);
if self.rules.evaluate_path_restrictions {
self.index_ancestors(path, &mut state);
}
Ok(Some(self.finalize(state)))
}
}
struct DocumentState {
fields: Vec<Field>,
dirty: bool,
facet: bool,
doc_value_names: BTreeSet<String>,
dimensions: Vec<FacetDimension>,
suggest_values: Vec<String>,
path: String,
}
impl DocumentState {
fn new(path: &str) -> Self {
Self {
fields: Vec::new(),
dirty: false,
facet: false,
doc_value_names: BTreeSet::new(),
dimensions: Vec::new(),
suggest_values: Vec::new(),
path: path.to_owned(),
}
}
fn add(&mut self, field: Field) {
self.fields.push(field);
}
fn add_dirty(&mut self, field: Field) {
self.fields.push(field);
self.dirty = true;
}
}
const NOT_TOKENIZED: [&str; 7] = [
"jcr:uuid",
"rep:authorizableId",
"rep:disabled",
"rep:impersonators",
"rep:members",
"rep:password",
"rep:principalName",
];
fn skip_tokenization(property_name: &str, definition: &PropertyDefinition) -> bool {
definition.is_regexp && NOT_TOKENIZED.binary_search(&property_name).is_ok()
}
fn string_field(name: &str, value: &str, stored: bool) -> Field {
let mut field = Field::indexed(
name,
IndexOptions::Documents,
vec![Token {
bytes: value.as_bytes().to_vec(),
position_increment: 1,
start_offset: 0,
end_offset: value.len() as u32,
}],
);
field.omit_norms = true;
field.final_offset = value.len() as u32;
if stored {
field.stored = Some(StoredValue::Text(value.to_owned()));
}
field
}
fn text_field(name: &str, analyzed: &TokenStreamResult, stored: Option<&str>) -> Field {
let mut field = tokenized(
name,
IndexOptions::DocumentsAndFrequenciesAndPositions,
analyzed,
);
field.omit_norms = false;
if let Some(value) = stored {
field.stored = Some(StoredValue::Text(value.to_owned()));
}
field
}
fn oak_text_field(name: &str, analyzed: &TokenStreamResult, stored: Option<&str>) -> Field {
let options = if stored.is_some() {
IndexOptions::DocumentsAndFrequenciesAndPositionsAndOffsets
} else {
IndexOptions::DocumentsAndFrequenciesAndPositions
};
let mut field = tokenized(name, options, analyzed);
field.omit_norms = true;
if let Some(value) = stored {
field.stored = Some(StoredValue::Text(value.to_owned()));
}
field
}
fn tokenized(name: &str, options: IndexOptions, analyzed: &TokenStreamResult) -> Field {
let mut field = Field::indexed(
name,
options,
analyzed
.tokens
.iter()
.map(|token| Token {
bytes: token.term.as_bytes().to_vec(),
position_increment: token.position_increment,
start_offset: token.start_offset,
end_offset: token.end_offset,
})
.collect(),
);
field.final_position_increment = analyzed.final_position_increment;
field.final_offset = analyzed.final_offset;
field
}
fn numeric_field(name: &str, terms: Vec<NumericTerm>) -> Field {
let mut field = Field::indexed(
name,
IndexOptions::Documents,
terms
.into_iter()
.map(|term| Token {
bytes: term.bytes,
position_increment: term.position_increment,
start_offset: 0,
end_offset: 0,
})
.collect(),
);
field.omit_norms = true;
field.final_offset = 0;
field
}
fn doc_value_field(name: &str, value: DocValue) -> Field {
Field {
name: name.to_owned(),
options: IndexOptions::Documents,
indexed: false,
tokens: Vec::new(),
final_position_increment: 0,
final_offset: 0,
stored: None,
doc_value: Some(value),
omit_norms: true,
boost: 1.0,
}
}
impl DocumentMaker<'_> {
fn index_properties(
&self,
node: &NodeState<'_>,
path: &str,
rule: &IndexingRule,
state: &mut DocumentState,
) -> IndexResult<()> {
let has_mime_type = node.property("jcr:mimeType")?.is_some();
let name = path.rsplit('/').next().unwrap_or(path).to_owned();
let node_name_property = PropertyState {
name: NODE_NAME_FIELD.to_owned(),
property_type: PropertyType::String,
values: PropertyValues::Single(PropertyValue::String(name)),
};
let mut properties = node.properties()?;
properties.push(node_name_property);
for property in &properties {
if property.name.starts_with(':') && property.name != NODE_NAME_FIELD {
continue;
}
let Some(definition) = rule.config_of(&property.name) else {
continue;
};
if !definition.index {
continue;
}
if definition.ordered {
self.index_ordered(property, definition, state)?;
}
self.index_property(property, definition, rule, has_mime_type, state)?;
}
Ok(())
}
fn index_ordered(
&self,
property: &PropertyState,
definition: &PropertyDefinition,
state: &mut DocumentState,
) -> IndexResult<()> {
let PropertyValues::Single(value) = &property.values else {
return Ok(());
};
if !self.includes_property_value(property, value, definition) {
return Ok(());
}
let name = format!("{DOC_VALUE_PREFIX}{}", property.name);
if state.doc_value_names.contains(&name) {
return Ok(());
}
let declared = definition
.declared_type
.as_deref()
.and_then(type_of_name)
.unwrap_or(property.property_type);
let doc_value = match declared {
PropertyType::Long => value
.as_text()
.and_then(|text| text.parse().ok())
.map(DocValue::Numeric),
PropertyType::Date => Some(DocValue::Numeric(Self::date_value(
value,
&property.name,
&state.path,
)?)),
PropertyType::Double => value
.as_text()
.and_then(|text| text.parse::<f64>().ok())
.map(|number| DocValue::Numeric(number.to_bits() as i64)),
PropertyType::Boolean => value
.as_text()
.map(|text| DocValue::Sorted(text.into_bytes())),
PropertyType::String => value
.as_text()
.map(|text| DocValue::Sorted(truncated_doc_value(&text))),
_ => None,
};
if let Some(doc_value) = doc_value {
state.doc_value_names.insert(name.clone());
state.add_dirty(doc_value_field(&name, doc_value));
}
Ok(())
}
fn index_property(
&self,
property: &PropertyState,
definition: &PropertyDefinition,
rule: &IndexingRule,
has_mime_type: bool,
state: &mut DocumentState,
) -> IndexResult<()> {
let included_type = includes_property_type(&rule.include_property_types, property);
let included_by_definition =
includes_property_type(&definition.included_property_types, property);
if property.property_type == PropertyType::Binary {
if included_type && definition.fulltext_enabled() {
self.index_binary(property, has_mime_type, &[FULLTEXT_FIELD], state);
}
return Ok(());
}
if definition.property_index && included_by_definition {
self.index_typed(property, definition, state)?;
}
if definition.fulltext_enabled() && included_type {
for value in values_of(property) {
let Some(text) = value.as_text() else {
continue;
};
if !self.includes_value(&text, definition) {
continue;
}
if definition.analyzed && included_by_definition {
self.index_analyzed(&property.name, &text, definition, state);
}
if definition.use_in_suggest {
Self::index_suggest(&text, state);
}
if definition.use_in_spellcheck {
self.index_spellcheck(&text, state);
}
if definition.node_scope_index {
self.index_fulltext(FULLTEXT_FIELD, &text, None, state);
}
state.dirty = true;
}
}
if definition.facet {
Self::index_facet(property, state);
}
Ok(())
}
}
impl DocumentMaker<'_> {
fn index_typed(
&self,
property: &PropertyState,
definition: &PropertyDefinition,
state: &mut DocumentState,
) -> IndexResult<()> {
for value in values_of(property) {
if !self.includes_property_value(property, value, definition) {
continue;
}
let field = match property.property_type {
PropertyType::Long => value
.as_text()
.and_then(|text| text.parse::<i64>().ok())
.map(|number| numeric_field(&property.name, long_terms(number))),
PropertyType::Date => Some(numeric_field(
&property.name,
long_terms(Self::date_value(value, &property.name, &state.path)?),
)),
PropertyType::Double => value
.as_text()
.and_then(|text| text.parse::<f64>().ok())
.map(|number| numeric_field(&property.name, double_terms(number))),
PropertyType::Binary => None,
_ => value
.as_text()
.map(|text| string_field(&property.name, &text, false)),
};
if let Some(field) = field {
state.add_dirty(field);
}
}
Ok(())
}
fn index_analyzed(
&self,
property_name: &str,
value: &str,
definition: &PropertyDefinition,
state: &mut DocumentState,
) {
let name = format!("{ANALYZED_PREFIX}{property_name}");
if skip_tokenization(property_name, definition) {
state.add_dirty(string_field(&name, value, false));
return;
}
let analyzed = self.analyzer.tokens(&name, value);
state.add_dirty(oak_text_field(
&name,
&analyzed,
definition.use_in_excerpt.then_some(value),
));
}
fn index_suggest(value: &str, state: &mut DocumentState) {
state.suggest_values.push(value.to_owned());
state.dirty = true;
}
fn index_spellcheck(&self, value: &str, state: &mut DocumentState) {
let analyzed = self.analyzer.tokens(field_names::SPELLCHECK, value);
state.add_dirty(oak_text_field(field_names::SPELLCHECK, &analyzed, None));
}
fn index_fulltext(
&self,
name: &str,
value: &str,
stored: Option<&str>,
state: &mut DocumentState,
) {
let analyzed = self.analyzer.tokens(name, value);
state.add_dirty(text_field(name, &analyzed, stored));
}
fn index_binary(
&self,
property: &PropertyState,
has_mime_type: bool,
names: &[&str],
state: &mut DocumentState,
) {
for value in values_of(property) {
let PropertyValue::Binary(binary) = value else {
continue;
};
if let Some(text) = self.binaries.text_of(binary, has_mime_type) {
for name in names {
self.index_fulltext(name, &text, Some(&text), state);
}
}
}
state.dirty = true;
}
fn index_facet(property: &PropertyState, state: &mut DocumentState) {
let string_valued = property.property_type == PropertyType::String;
let multi_valued = matches!(property.values, PropertyValues::Multiple(_)) && string_valued;
state.dimensions.push(FacetDimension {
name: property.name.clone(),
multi_valued,
});
if !string_valued {
return;
}
let mut values = Vec::new();
for value in values_of(property) {
let Some(text) = value.as_text() else {
continue;
};
if text.is_empty() {
continue;
}
values.push(FacetValue {
dimension: property.name.clone(),
label: text,
});
}
if values.is_empty() {
return;
}
state.facet = true;
for value in values {
state.add_dirty(pending_facet_field(&property.name, value));
}
}
}
fn pending_facet_field(property_name: &str, value: FacetValue) -> Field {
let mut field = Field::stored(
format!("{FACET_PENDING_PREFIX}{property_name}"),
StoredValue::Text(value.label),
);
field.indexed = false;
field
}
const FACET_PENDING_PREFIX: &str = "\u{0}facet:";
pub(super) fn includes_property_type(names: &[String], property: &PropertyState) -> bool {
names.is_empty()
|| names
.iter()
.any(|name| type_of_name(name) == Some(property.property_type))
}
fn type_of_name(name: &str) -> Option<PropertyType> {
match name {
"String" => Some(PropertyType::String),
"Binary" => Some(PropertyType::Binary),
"Long" => Some(PropertyType::Long),
"Double" => Some(PropertyType::Double),
"Date" => Some(PropertyType::Date),
"Boolean" => Some(PropertyType::Boolean),
"Name" => Some(PropertyType::Name),
"Path" => Some(PropertyType::Path),
"Reference" => Some(PropertyType::Reference),
"WeakReference" => Some(PropertyType::WeakReference),
"URI" => Some(PropertyType::Uri),
"Decimal" => Some(PropertyType::Decimal),
_ => None,
}
}
fn values_of(property: &PropertyState) -> &[PropertyValue] {
match &property.values {
PropertyValues::Single(value) => std::slice::from_ref(value),
PropertyValues::Multiple(values) => values,
}
}
impl DocumentMaker<'_> {
fn index_markers(
node: &NodeState<'_>,
rule: &IndexingRule,
state: &mut DocumentState,
) -> IndexResult<()> {
for definition in rule.definitions() {
if definition.null_check_enabled && property_for(node, definition)?.is_none() {
state.add_dirty(string_field(NULL_PROPERTIES_FIELD, &definition.name, false));
}
}
for definition in rule.definitions() {
if definition.not_null_check_enabled && property_for(node, definition)?.is_some() {
state.add_dirty(string_field(
NOT_NULL_PROPERTIES_FIELD,
&definition.name,
false,
));
}
}
Ok(())
}
fn index_node_name(&self, path: &str, rule: &IndexingRule, state: &mut DocumentState) {
let name = path.rsplit('/').next().unwrap_or(path);
if rule.fulltext_enabled {
self.index_fulltext(FULLTEXT_FIELD, name, None, state);
}
}
fn add_node_name_field(path: &str, rule: &IndexingRule, state: &mut DocumentState) {
if !rule.node_name_indexed {
return;
}
let name = path.rsplit('/').next().unwrap_or(path);
let value = name.split_once(':').map_or(name, |(_, rest)| rest);
state.add_dirty(string_field(NODE_NAME_FIELD, value, false));
}
fn index_ancestors(&self, path: &str, state: &mut DocumentState) {
let parent = parent_path_of(path);
let analyzed = self.analyzer.tokens(field_names::ANCESTORS, &parent);
state.add(text_field(field_names::ANCESTORS, &analyzed, None));
let depth = i32::try_from(path.split('/').filter(|step| !step.is_empty()).count())
.unwrap_or(i32::MAX);
state.add(numeric_field(DEPTH_FIELD, integer_terms(depth)));
}
fn finalize(&self, state: DocumentState) -> MadeDocument {
let DocumentState {
fields,
facet,
dimensions,
suggest_values,
..
} = state;
let mut fields = if facet { build_facets(fields) } else { fields };
if !suggest_values.is_empty() {
let joined = suggest_values.join("\n");
let analyzed = self.analyzer.tokens(field_names::SUGGEST, &joined);
fields.push(oak_text_field(field_names::SUGGEST, &analyzed, None));
}
MadeDocument {
document: Document { fields },
facet_dimensions: dimensions,
}
}
}
fn property_for(
node: &NodeState<'_>,
definition: &PropertyDefinition,
) -> IndexResult<Option<PropertyState>> {
let mut current = *node;
for ancestor in &definition.ancestors {
let Some(child) = current.child_node(ancestor)? else {
return Ok(None);
};
current = child;
}
let name = definition
.name
.rsplit('/')
.next()
.unwrap_or(&definition.name);
Ok(current.property(name)?)
}
fn parent_path_of(path: &str) -> String {
match path.rfind('/') {
Some(0) => "/".to_owned(),
Some(at) => path[..at].to_owned(),
None => String::new(),
}
}
fn build_facets(fields: Vec<Field>) -> Vec<Field> {
let mut facet_fields = Vec::new();
let mut rest = Vec::new();
for field in fields {
let Some(property_name) = field.name.strip_prefix(FACET_PENDING_PREFIX) else {
rest.push(field);
continue;
};
let Some(StoredValue::Text(label)) = field.stored else {
continue;
};
let index_field = facet_field_name(property_name);
let value = FacetValue {
dimension: property_name.to_owned(),
label,
};
let Ok(path) = value.to_path("") else {
continue;
};
facet_fields.push(doc_value_field(
&index_field,
DocValue::SortedSet(vec![path.clone().into_bytes()]),
));
facet_fields.push(string_field(&index_field, &path, false));
facet_fields.push(string_field(&index_field, &value.dimension, false));
}
facet_fields.extend(rest);
facet_fields
}
impl DocumentMaker<'_> {
fn includes_property_value(
&self,
property: &PropertyState,
value: &PropertyValue,
definition: &PropertyDefinition,
) -> bool {
if property.property_type == PropertyType::Binary {
return true;
}
if definition.value_pattern.matches_all() {
return true;
}
value
.as_text()
.is_some_and(|text| self.includes_value(&text, definition))
}
fn includes_value(&self, value: &str, definition: &PropertyDefinition) -> bool {
definition
.value_pattern
.matches(value, self.definition_path)
.unwrap_or(true)
}
fn date_value(value: &PropertyValue, property_name: &str, path: &str) -> IndexResult<i64> {
let text = value.as_text().unwrap_or_default();
date_to_long(&text).map_err(|_| IndexError::UnparseableDate {
value: format!("{path}@{property_name} = {text:?}"),
})
}
}