search fix to new architecture
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
use common::search::{
|
||||
json_path_term, normalize_column_name, normalize_exact, tokenize_ngram, tokenize_word,
|
||||
SchemaFields,
|
||||
ARCHIVED_ROW_KEY_PREFIX, SchemaFields, json_path_term, normalize_column_name,
|
||||
normalize_exact, tokenize_ngram, tokenize_word,
|
||||
};
|
||||
use common::proto::komp_ac::search::SearchVersionScope;
|
||||
use tantivy::query::{
|
||||
BooleanQuery, BoostQuery, EmptyQuery, FuzzyTermQuery, Occur, PhraseQuery, Query, QueryParser,
|
||||
TermQuery,
|
||||
RegexQuery, TermQuery,
|
||||
};
|
||||
use tantivy::schema::{IndexRecordOption, Term};
|
||||
use tantivy::Index;
|
||||
@@ -19,7 +20,6 @@ pub enum ConstraintMode {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SearchConstraint {
|
||||
pub targets: Vec<SearchConstraintTarget>,
|
||||
pub query: String,
|
||||
pub mode: ConstraintMode,
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ pub struct SearchConstraint {
|
||||
pub struct SearchConstraintTarget {
|
||||
pub table_name: Option<String>,
|
||||
pub column: String,
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
pub fn build_master_query(
|
||||
@@ -35,6 +36,7 @@ pub fn build_master_query(
|
||||
free_query: &str,
|
||||
must: &[SearchConstraint],
|
||||
table_filter: Option<&str>,
|
||||
version_scope: SearchVersionScope,
|
||||
) -> Result<Box<dyn Query>, Status> {
|
||||
let mut clauses: Vec<(Occur, Box<dyn Query>)> = Vec::new();
|
||||
let mut has_search_clause = false;
|
||||
@@ -60,6 +62,17 @@ pub fn build_master_query(
|
||||
));
|
||||
}
|
||||
|
||||
let archived_rows = RegexQuery::from_pattern(
|
||||
&format!("{ARCHIVED_ROW_KEY_PREFIX}.*"),
|
||||
fields.row_key,
|
||||
)
|
||||
.map_err(|error| Status::internal(format!("Archived-row query build failed: {error}")))?;
|
||||
match version_scope {
|
||||
SearchVersionScope::Current => clauses.push((Occur::MustNot, Box::new(archived_rows))),
|
||||
SearchVersionScope::Archived => clauses.push((Occur::Must, Box::new(archived_rows))),
|
||||
SearchVersionScope::All => {}
|
||||
}
|
||||
|
||||
if !has_search_clause {
|
||||
return Ok(Box::new(EmptyQuery));
|
||||
}
|
||||
@@ -75,9 +88,9 @@ fn constraint_predicate(
|
||||
|
||||
for target in &constraint.targets {
|
||||
let column_predicate = match constraint.mode {
|
||||
ConstraintMode::Exact => exact_predicate(fields, &target.column, &constraint.query)?,
|
||||
ConstraintMode::Exact => exact_predicate(fields, &target.column, &target.query)?,
|
||||
ConstraintMode::Fuzzy => {
|
||||
fuzzy_predicate_scoped(fields, &target.column, &constraint.query)?
|
||||
fuzzy_predicate_scoped(fields, &target.column, &target.query)?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -289,3 +302,260 @@ fn fuzzy_distance(word_len: usize) -> Option<u8> {
|
||||
_ => Some(2),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use common::search::{archived_search_row_key, create_search_schema, register_tokenizers};
|
||||
use tantivy::collector::Count;
|
||||
use tantivy::schema::OwnedValue;
|
||||
use tantivy::TantivyDocument;
|
||||
|
||||
struct TestDocument<'a> {
|
||||
row_key: String,
|
||||
row_id: u64,
|
||||
table_name: &'a str,
|
||||
values: &'a [(&'a str, &'a str)],
|
||||
}
|
||||
|
||||
fn index_documents(documents: &[TestDocument<'_>]) -> (Index, SchemaFields) {
|
||||
let schema = create_search_schema();
|
||||
let index = Index::create_in_ram(schema.clone());
|
||||
register_tokenizers(&index).expect("tokenizers should register");
|
||||
let fields = SchemaFields::from(&schema).expect("schema should match");
|
||||
let mut writer = index.writer(50_000_000).expect("writer should open");
|
||||
|
||||
for source in documents {
|
||||
let mut document = TantivyDocument::default();
|
||||
document.add_u64(fields.pg_id, source.row_id);
|
||||
document.add_text(fields.table_name, source.table_name);
|
||||
document.add_text(fields.row_key, &source.row_key);
|
||||
let mut object = std::collections::BTreeMap::new();
|
||||
for (column, value) in source.values {
|
||||
document.add_text(fields.all_text, value);
|
||||
object.insert((*column).to_string(), OwnedValue::from(*value));
|
||||
}
|
||||
document.add_object(fields.data_word, object.clone());
|
||||
document.add_object(fields.data_ngram, object.clone());
|
||||
document.add_object(fields.data_exact, object);
|
||||
writer.add_document(document).expect("document should index");
|
||||
}
|
||||
writer.commit().expect("documents should commit");
|
||||
(index, fields)
|
||||
}
|
||||
|
||||
fn index_versions() -> (Index, SchemaFields) {
|
||||
index_documents(&[
|
||||
TestDocument {
|
||||
row_key: "customers:7".to_string(),
|
||||
row_id: 7,
|
||||
table_name: "customers",
|
||||
values: &[("1", "new")],
|
||||
},
|
||||
TestDocument {
|
||||
row_key: archived_search_row_key(40, 7, 1),
|
||||
row_id: 7,
|
||||
table_name: "customers",
|
||||
values: &[("1", "old")],
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
fn count(index: &Index, query: &dyn Query) -> usize {
|
||||
index
|
||||
.reader()
|
||||
.expect("reader should open")
|
||||
.searcher()
|
||||
.search(query, &Count)
|
||||
.expect("query should run")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_scope_separates_current_and_archived_documents() {
|
||||
let (index, fields) = index_versions();
|
||||
let constraint = SearchConstraint {
|
||||
targets: vec![SearchConstraintTarget {
|
||||
table_name: None,
|
||||
column: "1".to_string(),
|
||||
query: "old".to_string(),
|
||||
}],
|
||||
mode: ConstraintMode::Exact,
|
||||
};
|
||||
|
||||
let current = build_master_query(
|
||||
&index,
|
||||
&fields,
|
||||
"",
|
||||
std::slice::from_ref(&constraint),
|
||||
Some("customers"),
|
||||
SearchVersionScope::Current,
|
||||
)
|
||||
.expect("current query should build");
|
||||
let archived = build_master_query(
|
||||
&index,
|
||||
&fields,
|
||||
"",
|
||||
&[constraint],
|
||||
Some("customers"),
|
||||
SearchVersionScope::Archived,
|
||||
)
|
||||
.expect("archive query should build");
|
||||
|
||||
assert_eq!(count(&index, &*current), 0);
|
||||
assert_eq!(count(&index, &*archived), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn physical_constraint_survives_any_display_alias_rename() {
|
||||
let (index, fields) = index_versions();
|
||||
for public_alias in ["adresar", "customer", "renamed_again"] {
|
||||
let resolved_physical_column = if !public_alias.is_empty() { "1" } else { unreachable!() };
|
||||
let query = build_master_query(
|
||||
&index,
|
||||
&fields,
|
||||
"",
|
||||
&[SearchConstraint {
|
||||
targets: vec![SearchConstraintTarget {
|
||||
table_name: None,
|
||||
column: resolved_physical_column.to_string(),
|
||||
query: "new".to_string(),
|
||||
}],
|
||||
mode: ConstraintMode::Exact,
|
||||
}],
|
||||
Some("customers"),
|
||||
SearchVersionScope::Current,
|
||||
)
|
||||
.expect("renamed alias should resolve to the same physical query");
|
||||
assert_eq!(count(&index, &*query), 1, "alias {public_alias}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_constraints_are_anded_and_table_scoped() {
|
||||
let (index, fields) = index_documents(&[
|
||||
TestDocument {
|
||||
row_key: "customers:1".to_string(),
|
||||
row_id: 1,
|
||||
table_name: "customers",
|
||||
values: &[("1", "Alice Example"), ("2", "Bratislava")],
|
||||
},
|
||||
TestDocument {
|
||||
row_key: "customers:2".to_string(),
|
||||
row_id: 2,
|
||||
table_name: "customers",
|
||||
values: &[("1", "Alice Example"), ("2", "Kosice")],
|
||||
},
|
||||
TestDocument {
|
||||
row_key: "suppliers:3".to_string(),
|
||||
row_id: 3,
|
||||
table_name: "suppliers",
|
||||
values: &[("1", "Alice Example"), ("2", "Bratislava")],
|
||||
},
|
||||
]);
|
||||
let constraints = [
|
||||
SearchConstraint {
|
||||
targets: vec![SearchConstraintTarget {
|
||||
table_name: None,
|
||||
column: "1".to_string(),
|
||||
query: "Alice Example".to_string(),
|
||||
}],
|
||||
mode: ConstraintMode::Exact,
|
||||
},
|
||||
SearchConstraint {
|
||||
targets: vec![SearchConstraintTarget {
|
||||
table_name: None,
|
||||
column: "2".to_string(),
|
||||
query: "Bratislava".to_string(),
|
||||
}],
|
||||
mode: ConstraintMode::Exact,
|
||||
},
|
||||
];
|
||||
let query = build_master_query(
|
||||
&index,
|
||||
&fields,
|
||||
"",
|
||||
&constraints,
|
||||
Some("customers"),
|
||||
SearchVersionScope::Current,
|
||||
)
|
||||
.expect("exact query should build");
|
||||
|
||||
assert_eq!(count(&index, &*query), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzzy_constraints_stay_within_the_resolved_physical_column() {
|
||||
let (index, fields) = index_documents(&[
|
||||
TestDocument {
|
||||
row_key: "customers:1".to_string(),
|
||||
row_id: 1,
|
||||
table_name: "customers",
|
||||
values: &[("1", "Alice"), ("2", "Bratislava")],
|
||||
},
|
||||
TestDocument {
|
||||
row_key: "customers:2".to_string(),
|
||||
row_id: 2,
|
||||
table_name: "customers",
|
||||
values: &[("1", "Bratislava"), ("2", "Kosice")],
|
||||
},
|
||||
]);
|
||||
let constraint = |column: &str| SearchConstraint {
|
||||
targets: vec![SearchConstraintTarget {
|
||||
table_name: None,
|
||||
column: column.to_string(),
|
||||
query: "Bratislva".to_string(),
|
||||
}],
|
||||
mode: ConstraintMode::Fuzzy,
|
||||
};
|
||||
let address_query = build_master_query(
|
||||
&index,
|
||||
&fields,
|
||||
"",
|
||||
&[constraint("2")],
|
||||
Some("customers"),
|
||||
SearchVersionScope::Current,
|
||||
)
|
||||
.expect("fuzzy address query should build");
|
||||
let name_query = build_master_query(
|
||||
&index,
|
||||
&fields,
|
||||
"",
|
||||
&[constraint("1")],
|
||||
Some("customers"),
|
||||
SearchVersionScope::Current,
|
||||
)
|
||||
.expect("fuzzy name query should build");
|
||||
|
||||
assert_eq!(count(&index, &*address_query), 1);
|
||||
assert_eq!(count(&index, &*name_query), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_text_searches_all_public_values_but_respects_table_filter() {
|
||||
let (index, fields) = index_documents(&[
|
||||
TestDocument {
|
||||
row_key: "customers:1".to_string(),
|
||||
row_id: 1,
|
||||
table_name: "customers",
|
||||
values: &[("1", "Alice Example")],
|
||||
},
|
||||
TestDocument {
|
||||
row_key: "suppliers:2".to_string(),
|
||||
row_id: 2,
|
||||
table_name: "suppliers",
|
||||
values: &[("8", "Alice Example")],
|
||||
},
|
||||
]);
|
||||
let query = build_master_query(
|
||||
&index,
|
||||
&fields,
|
||||
"Alice Example",
|
||||
&[],
|
||||
Some("customers"),
|
||||
SearchVersionScope::Current,
|
||||
)
|
||||
.expect("free-text query should build");
|
||||
|
||||
assert_eq!(count(&index, &*query), 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user