Skip to content

Commit

Permalink
Array indexing support for search
Browse files Browse the repository at this point in the history
resolves #18
  • Loading branch information
ackwell committed Aug 10, 2024
1 parent b6b7e92 commit 51ed033
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 23 deletions.
28 changes: 26 additions & 2 deletions src/http/api1/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ use super::error;
/// inside structs and relations (i.e. `Foo.Bar=1)`, as well as language tags to
/// target fields in particular languages (i.e. `Foo@ja=1`).
///
/// Arrays must be selected explicitly (i.e. `Foo[]=1`), resulting in a match
/// for any value within the array. An index can be used to reduce the search
/// space (i.e. `Foo[1]=1`).
///
/// By default, results will match at least one clause, with higher relevance
/// scores for those that match more. To modify this behavior, clauses can
/// decorated. `+clause` specifies that the clause _must_ be matched for any
Expand Down Expand Up @@ -149,7 +153,10 @@ fn language(input: &str) -> ParseResult<excel::Language> {
}

fn array_specifier(input: &str) -> ParseResult<query::FieldSpecifier> {
nom_value(query::FieldSpecifier::Array, tag("[]"))(input)
map(
delimited(char('['), opt(map_res(digit1, str::parse)), char(']')),
|index: Option<u32>| query::FieldSpecifier::Array(index),
)(input)
}

fn operation(input: &str) -> ParseResult<query::Operation> {
Expand Down Expand Up @@ -308,7 +315,7 @@ mod test {
leaf(
field_struct("A"),
operation_relation(leaf(
query::FieldSpecifier::Array,
query::FieldSpecifier::Array(None),
query::Operation::Eq(u64(1)),
)),
),
Expand All @@ -318,6 +325,23 @@ mod test {
assert_eq!(got, expected);
}

#[test]
fn parse_arrays_indexing() {
let expected = group(vec![(
query::Occur::Should,
leaf(
field_struct("A"),
operation_relation(leaf(
query::FieldSpecifier::Array(Some(1)),
query::Operation::Eq(u64(1)),
)),
),
)]);

let got = test_parse("A[1]=1");
assert_eq!(got, expected);
}

#[test]
fn parse_multiple() {
let expected = group(vec![
Expand Down
74 changes: 54 additions & 20 deletions src/search/query/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,16 @@ impl<'a> Normalizer<'a> {
context,
),

(pre::FieldSpecifier::Array, schema::Node::Array { count, node }) => {
self.normalize_leaf_bound_array(operation, node, *count, context)
(pre::FieldSpecifier::Array(index), schema::Node::Array { count, node }) => {
self.normalize_leaf_bound_array(operation, node, *count, *index, context)
}

// Anything other than a like-for-like match is, well, a mismatch.
(specifier, node) => Err(Error::QuerySchemaMismatch(context.mismatch(format!(
"cannot use {} query specifier for {} schema structures",
match specifier {
pre::FieldSpecifier::Struct(..) => "struct",
pre::FieldSpecifier::Array => "array",
pre::FieldSpecifier::Array(..) => "array",
},
match node {
schema::Node::Array { .. } => "array",
Expand Down Expand Up @@ -243,33 +243,41 @@ impl<'a> Normalizer<'a> {
operation: &pre::Operation,
node: &schema::Node,
count: u32,
index: Option<u32>,
context: Context,
) -> Result<post::Node> {
let path_entry = match index {
None => std::borrow::Cow::Borrowed("[]"),
Some(value) => std::borrow::Cow::Owned(format!("[{value}]")),
};

let context = Context {
path: &([context.path, &["[]"]].concat()),
path: &([context.path, &[path_entry.as_ref()]].concat()),
..context
};

let size = usize::try_from(node.size()).unwrap();
let clauses = (0..usize::try_from(count).unwrap())
.map(|index| -> Result<_> {
let start = index * size;
let end = start + size;

// TODO: This is duped, helper?
let narrowed_columns = context.columns.get(start..end).ok_or_else(|| {
Error::SchemaGameMismatch(
context.mismatch("game data does not contain enough columns"),
)
})?;
// If there's an index, shortcut with a leaf node.
if let Some(index) = index {
let index_usize = usize::try_from(index).unwrap();
return self.normalise_leaf_bound_array_index(
operation,
node,
index_usize,
size,
context,
);
}

let query = self.normalize_operation(
let clauses = (0..usize::try_from(count).unwrap())
.map(|index| -> Result<_> {
let query = self.normalise_leaf_bound_array_index(
operation,
Context {
schema: node,
columns: narrowed_columns,
..context
},
node,
index,
size,
context.clone(),
)?;

Ok((post::Occur::Should, query))
Expand All @@ -279,6 +287,32 @@ impl<'a> Normalizer<'a> {
Ok(post::Node::Group(post::Group { clauses }))
}

fn normalise_leaf_bound_array_index(
&self,
operation: &pre::Operation,
node: &schema::Node,
index: usize,
size: usize,
context: Context,
) -> Result<post::Node> {
let start = index * size;
let end = start + size;

// TODO: This is duped, helper?
let narrowed_columns = context.columns.get(start..end).ok_or_else(|| {
Error::SchemaGameMismatch(context.mismatch("game data does not contain enough columns"))
})?;

self.normalize_operation(
operation,
Context {
schema: node,
columns: narrowed_columns,
..context
},
)
}

fn normalize_leaf_unbound(
&self,
_operation: &pre::Operation,
Expand Down
2 changes: 1 addition & 1 deletion src/search/query/pre.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ pub type RelationTarget = ();
#[derive(Debug, Clone, PartialEq)]
pub enum FieldSpecifier {
Struct(String, Option<excel::Language>),
Array,
Array(Option<u32>),
}

0 comments on commit 51ed033

Please sign in to comment.