Skip to content
This repository has been archived by the owner on Sep 12, 2018. It is now read-only.

Fulltext search #477

Closed
wants to merge 14 commits into from
39 changes: 31 additions & 8 deletions query-algebrizer/src/clauses/fulltext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use mentat_core::{
ValueType,
};

use mentat_core::util::Either;

use mentat_query::{
Binding,
FnArg,
Expand Down Expand Up @@ -153,29 +155,50 @@ impl ConjoiningClauses {
// - It's already bound, either by input or by a previous pattern like `ground`.
// - It's not already bound, but it's a defined input of type Text. Not yet implemented: TODO.
// - It's not bound. The query cannot be algebrized.
let search: TypedValue = match args.next().unwrap() {
let search: Either<TypedValue, QualifiedAlias> = match args.next().unwrap() {
FnArg::Constant(NonIntegerConstant::Text(s)) => {
TypedValue::String(s)
Either::Left(TypedValue::String(s))
},
FnArg::Variable(in_var) => {
match self.bound_value(&in_var) {
Some(t @ TypedValue::String(_)) => t,
Some(t @ TypedValue::String(_)) => Either::Left(t),
Some(_) => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "string".into(), 2)),
None => {
if self.input_variables.contains(&in_var) &&
self.known_type(&in_var) == Some(ValueType::String) {
// Sorry, we haven't implemented late binding.
// Regardless of whether we'll be providing a string later, or the value
// comes from a column, it must be a string.
if self.known_type(&in_var) != Some(ValueType::String) {
bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "string".into(), 2));
}

if self.input_variables.contains(&in_var) {
// Sorry, we haven't implemented late binding.
// TODO: implement this.
bail!(ErrorKind::UnboundVariable((*in_var.0).clone()));
} else {
// It must be bound earlier in the query. We already established that
// it must be a string column.
if let Some(binding) = self.column_bindings
.get(&in_var)
.and_then(|bindings| bindings.get(0).cloned()) {
Either::Right(binding)
} else {
bail!(ErrorKind::UnboundVariable((*in_var.0).clone()));
}
}
bail!(ErrorKind::UnboundVariable((*in_var.0).clone()));
},
}
},
_ => bail!(ErrorKind::InvalidArgument(where_fn.operator.clone(), "string".into(), 2)),
};

let qv = match search {
Either::Left(tv) => QueryValue::TypedValue(tv),
Either::Right(qa) => QueryValue::Column(qa),
};

let constraint = ColumnConstraint::Matches(QualifiedAlias(fulltext_values_alias.clone(),
Column::Fulltext(FulltextColumn::Text)),
QueryValue::TypedValue(search));
qv);
self.wheres.add_intersection(constraint);

if let VariableOrPlaceholder::Variable(ref var) = b_entity {
Expand Down
65 changes: 65 additions & 0 deletions tests/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ extern crate time;
extern crate mentat;
extern crate mentat_core;
extern crate mentat_db;
extern crate mentat_query_algebrizer; // For errors.

use std::str::FromStr;

Expand All @@ -28,6 +29,7 @@ use mentat_core::{

use mentat::{
NamespacedKeyword,
PlainSymbol,
QueryInputs,
QueryResults,
Variable,
Expand Down Expand Up @@ -259,12 +261,19 @@ fn test_instants_and_uuids() {
fn test_fulltext() {
let mut c = new_connection("").expect("Couldn't open conn.");
let mut conn = Conn::connect(&mut c).expect("Couldn't open DB.");

conn.transact(&mut c, r#"[
[:db/add "a" :db/ident :foo/term]
[:db/add "a" :db/valueType :db.type/string]
[:db/add "a" :db/fulltext false]
[:db/add "a" :db/cardinality :db.cardinality/many]

[:db/add "s" :db/ident :foo/fts]
[:db/add "s" :db/valueType :db.type/string]
[:db/add "s" :db/fulltext true]
[:db/add "s" :db/cardinality :db.cardinality/many]
]"#).unwrap();

let v = conn.transact(&mut c, r#"[
[:db/add "v" :foo/fts "hello darkness my old friend"]
[:db/add "v" :foo/fts "I've come to talk with you again"]
Expand All @@ -290,4 +299,60 @@ fn test_fulltext() {
},
_ => panic!("Expected query to work."),
}

let a = conn.transact(&mut c, r#"[[:db/add "a" :foo/term "talk"]]"#)
.unwrap()
.tempids
.get("a").cloned()
.expect("a was mapped");

// If you use a non-constant search term, it must be bound earlier in the query.
let query = r#"[:find ?x ?val
:where
[(fulltext $ :foo/fts ?term) [[?x ?val]]]
[?a :foo/term ?term]
]"#;
let r = conn.q_once(&mut c, query, None);
match r {
Err(Error(ErrorKind::QueryError(mentat_query_algebrizer::ErrorKind::InvalidArgument(PlainSymbol(s), ty, i)), _)) => {
assert_eq!(s, "fulltext");
assert_eq!(ty, "string");
assert_eq!(i, 2);
},
_ => panic!("Expected query to fail."),
}

// Bound to the wrong type? Error.
let query = r#"[:find ?x ?val
:where
[?a :foo/term ?term]
[(fulltext $ :foo/fts ?a) [[?x ?val]]]]"#;
let r = conn.q_once(&mut c, query, None);
match r {
Err(Error(ErrorKind::QueryError(mentat_query_algebrizer::ErrorKind::InvalidArgument(PlainSymbol(s), ty, i)), _)) => {
assert_eq!(s, "fulltext");
assert_eq!(ty, "string");
assert_eq!(i, 2);
},
_ => panic!("Expected query to fail."),
}

// If it's bound, and the right type, it'll work!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nifty!

let query = r#"[:find ?x ?val
:in ?a
:where
[?a :foo/term ?term]
[(fulltext $ :foo/fts ?term) [[?x ?val]]]]"#;
let inputs = QueryInputs::with_value_sequence(vec![(Variable::from_valid_name("?a"), TypedValue::Ref(a))]);
let r = conn.q_once(&mut c, query, inputs);
match r {
Result::Ok(QueryResults::Rel(rels)) => {
assert_eq!(rels, vec![
vec![TypedValue::Ref(v),
TypedValue::String("I've come to talk with you again".to_string().into()),
]
]);
},
_ => panic!("Expected query to work."),
}
}