Handle inequality comparisons with technical data.

This commit is contained in:
Mathieu Darse
2015-11-05 19:43:03 +01:00
parent 713442c7b4
commit 467e530a93
8 changed files with 228 additions and 45 deletions

View File

@@ -32,7 +32,7 @@
%token id id|recordid
%token field_prefix field\.
%token flag_prefix flag\.
%token meta_prefix meta\.
%token meta_prefix (?:meta|exif)\.
%token true true|1
%token false false|0
%token word [^\s\(\)\[\]:<>≤≥=]+
@@ -73,10 +73,10 @@ key_value_pair:
| ::flag_prefix:: flag() ::colon:: ::space::? boolean() #flag_statement
| ::field_prefix:: field() ::colon:: ::space::? term() #field_statement
| field() ::colon:: ::space::? term() #field_statement
| field() ::space::? ::lt:: ::space::? value() #less_than
| field() ::space::? ::gt:: ::space::? value() #greater_than
| field() ::space::? ::lte:: ::space::? value() #less_than_or_equal_to
| field() ::space::? ::gte:: ::space::? value() #greater_than_or_equal_to
| key() ::space::? ::lt:: ::space::? value() #less_than
| key() ::space::? ::gt:: ::space::? value() #greater_than
| key() ::space::? ::lte:: ::space::? value() #less_than_or_equal_to
| key() ::space::? ::gte:: ::space::? value() #greater_than_or_equal_to
| field() ::space::? ::equal:: ::space::? value() #equal_to
#native_key:
@@ -85,9 +85,18 @@ key_value_pair:
| <type>
| <id>
key:
::meta_prefix:: meta_key()
| ::field_prefix:: field_key()
| field_key()
#meta_key:
word_or_keyword()+
#field_key:
word_or_keyword()+
| quoted_string()
#flag:
word_or_keyword()+

View File

@@ -0,0 +1,31 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
use Assert\Assertion;
class FieldKey implements Key
{
private $name;
public function __construct($name)
{
Assertion::string($name);
$this->name = $name;
}
public function getIndexField()
{
return 'yolo';
}
public function getValue()
{
return $this->name;
}
public function __toString()
{
return sprintf('field.%s', $this->name);
}
}

View File

@@ -2,7 +2,6 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Assert\Assertion;
class MetadataKey implements Key

View File

@@ -2,41 +2,49 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Assert\Assertion;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\FieldKey;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
class RangeExpression extends Node
{
private $field;
private $key;
private $field_cache;
private $lower_bound;
private $lower_inclusive;
private $higher_bound;
private $higher_inclusive;
public static function lessThan(Field $field, $bound)
public static function lessThan(Key $key, $bound)
{
return new self($field, $bound, false);
return new self($key, $bound, false);
}
public static function lessThanOrEqual(Field $field, $bound)
public static function lessThanOrEqual(Key $key, $bound)
{
return new self($field, $bound, true);
return new self($key, $bound, true);
}
public static function greaterThan(Field $field, $bound)
public static function greaterThan(Key $key, $bound)
{
return new self($field, null, null, $bound, false);
return new self($key, null, false, $bound, false);
}
public static function greaterThanOrEqual(Field $field, $bound)
public static function greaterThanOrEqual(Key $key, $bound)
{
return new self($field, null, null, $bound, true);
return new self($key, null, false, $bound, true);
}
public function __construct(Field $field, $lb, $li = false, $hb = null, $hi = false)
public function __construct(Key $key, $lb, $li = false, $hb = null, $hi = false)
{
$this->field = $field;
Assertion::nullOrScalar($lb);
Assertion::boolean($li);
Assertion::nullOrScalar($hb);
Assertion::boolean($hi);
$this->key = $key;
$this->lower_bound = $lb;
$this->lower_inclusive = $li;
$this->higher_bound = $hb;
@@ -45,14 +53,9 @@ class RangeExpression extends Node
public function buildQuery(QueryContext $context)
{
$structure_field = $context->get($this->field);
if (!$structure_field) {
throw new QueryException(sprintf('Field "%s" does not exist', $this->field->getValue()));
}
$params = array();
if ($this->lower_bound !== null) {
if (!$structure_field->isValueCompatible($this->lower_bound)) {
if (!$this->isValueCompatible($this->lower_bound, $context)) {
return;
}
if ($this->lower_inclusive) {
@@ -62,7 +65,7 @@ class RangeExpression extends Node
}
}
if ($this->higher_bound !== null) {
if (!$structure_field->isValueCompatible($this->higher_bound)) {
if (!$this->isValueCompatible($this->higher_bound, $context)) {
return;
}
if ($this->higher_inclusive) {
@@ -73,9 +76,48 @@ class RangeExpression extends Node
}
$query = [];
$query['range'][$structure_field->getIndexField()] = $params;
$query['range'][$this->getIndexField($context)] = $params;
return QueryHelper::wrapPrivateFieldQuery($structure_field, $query);
return $this->postProcessQuery($query, $context);
}
private function isValueCompatible($value, QueryContext $context)
{
if ($this->key instanceof FieldKey) {
return $this->getField($context)->isValueCompatible($value);
} else {
return true;
}
}
private function getIndexField(QueryContext $context)
{
if ($this->key instanceof FieldKey) {
return $this->getField($context)->getIndexField();
} else {
return $this->key->getIndexField();
}
}
private function postProcessQuery($query, QueryContext $context)
{
if ($this->key instanceof FieldKey) {
$field = $this->getField($context);
return QueryHelper::wrapPrivateFieldQuery($field, $query);
} else {
return $query;
}
}
private function getField(QueryContext $context)
{
if ($this->field_cache === null) {
$this->field_cache = $context->get($this->key->getValue());
}
if ($this->field_cache === null) {
throw new QueryException(sprintf('Field "%s" does not exist', $this->key->getValue()));
}
return $this->field_cache;
}
public function getTermNodes()
@@ -101,6 +143,6 @@ class RangeExpression extends Node
}
}
return sprintf('<range:%s%s>', $this->field->getValue(), $string);
return sprintf('<range:%s%s>', $this->key, $string);
}
}

View File

@@ -17,6 +17,7 @@ class NodeTypes
const EQUAL_EXPR = '#equal_to';
const FIELD_STATEMENT = '#field_statement';
const FIELD = '#field';
const FIELD_KEY = '#field_key';
const VALUE = '#value';
const TERM = '#thesaurus_term';
const TEXT = '#text';

View File

@@ -101,6 +101,9 @@ class QueryVisitor implements Visit
case NodeTypes::METADATA_KEY:
return $this->visitMetadataKeyNode($element);
case NodeTypes::FIELD_KEY:
return new AST\KeyValue\FieldKey($this->visitString($element));
default:
throw new Exception(sprintf('Unknown node type "%s".', $element->getId()));
}
@@ -151,18 +154,18 @@ class QueryVisitor implements Visit
if ($node->getChildrenNumber() !== 2) {
throw new Exception('Comparison operator can only have 2 childs.');
}
$field = $node->getChild(0)->accept($this);
$expression = $node->getChild(1)->accept($this);
$key = $node->getChild(0)->accept($this);
$boundary = $node->getChild(1)->accept($this);
switch ($node->getId()) {
case NodeTypes::LT_EXPR:
return AST\RangeExpression::lessThan($field, $expression);
return AST\RangeExpression::lessThan($key, $boundary);
case NodeTypes::LTE_EXPR:
return AST\RangeExpression::lessThanOrEqual($field, $expression);
return AST\RangeExpression::lessThanOrEqual($key, $boundary);
case NodeTypes::GT_EXPR:
return AST\RangeExpression::greaterThan($field, $expression);
return AST\RangeExpression::greaterThan($key, $boundary);
case NodeTypes::GTE_EXPR:
return AST\RangeExpression::greaterThanOrEqual($field, $expression);
return AST\RangeExpression::greaterThanOrEqual($key, $boundary);
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Alchemy\Tests\Phrasea\SearchEngine\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\FieldKey;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\MetadataKey;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\NativeKey;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\RangeExpression;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field;
/**
* @group unit
* @group searchengine
* @group ast
*/
class RangeExpressionTest extends \PHPUnit_Framework_TestCase
{
public function testSerializability()
{
$this->assertTrue(method_exists(RangeExpression::class, '__toString'), 'Class does not have method __toString');
}
/**
* @dataProvider serializationProvider
*/
public function testSerialization($expression, $serialization)
{
$this->assertEquals($serialization, (string) $expression);
}
public function serializationProvider()
{
$key_prophecy = $this->prophesize(Key::class);
$key_prophecy->__toString()->willReturn('foo');
$key = $key_prophecy->reveal();
return [
[RangeExpression::lessThan($key, 42), '<range:foo lt="42">' ],
[RangeExpression::lessThanOrEqual($key, 42), '<range:foo lte="42">'],
[RangeExpression::greaterThan($key, 42), '<range:foo gt="42">' ],
[RangeExpression::greaterThanOrEqual($key, 42), '<range:foo gte="42">'],
];
}
/**
* @dataProvider queryProvider
*/
public function testQueryBuild($factory, $key, $value, $result)
{
$query_context = $this->prophesize(QueryContext::class);
$node = RangeExpression::$factory($key, $value);
$query = $node->buildQuery($query_context->reveal());
$this->assertEquals(json_decode($result, true), $query);
}
public function queryProvider()
{
$key_prophecy = $this->prophesize(Key::class);
$key_prophecy->getIndexField()->willReturn('foo');
$key = $key_prophecy->reveal();
return [
['lessThan', $key, 'bar', '{"range":{"foo": {"lt":"bar"}}}'],
['lessThanOrEqual', $key, 'baz', '{"range":{"foo": {"lte":"baz"}}}'],
['greaterThan', $key, 'qux', '{"range":{"foo": {"gt":"qux"}}}'],
['greaterThanOrEqual', $key, 'bla', '{"range":{"foo": {"gte":"bla"}}}'],
];
}
public function testQueryBuildWithFieldKey()
{
$key = $this->prophesize(FieldKey::class);
$key->getValue()->willReturn('foo');
$node = RangeExpression::lessThan($key->reveal(), 'bar');
$structure_field = $this->prophesize(Field::class);
$structure_field->isPrivate()->willReturn(false);
$structure_field->isValueCompatible('bar')->willReturn(true);
$structure_field->getIndexField()->willReturn('baz');
$query_context = $this->prophesize(QueryContext::class);
$query_context->get('foo')->willReturn($structure_field->reveal());
$query = $node->buildQuery($query_context->reveal());
$expected = '{
"range": {
"baz": {
"lt": "bar"
}
}
}';
$this->assertEquals(json_decode($expected, true), $query);
}
}

View File

@@ -49,18 +49,18 @@ foo EXCEPT (bar OR baz)|(<text:"foo"> EXCEPT (<text:"bar"> OR <text:"baz">))
foo EXCEPT (bar EXCEPT baz)|(<text:"foo"> EXCEPT (<text:"bar"> EXCEPT <text:"baz">))
# Comparison operators
foo < 42|<range:foo lt="42">
foo ≤ 42|<range:foo lte="42">
foo > 42|<range:foo gt="42">
foo ≥ 42|<range:foo gte="42">
foo < 2015/01/01|<range:foo lt="2015/01/01">
foo ≤ 2015/01/01|<range:foo lte="2015/01/01">
foo > 2015/01/01|<range:foo gt="2015/01/01">
foo ≥ 2015/01/01|<range:foo gte="2015/01/01">
foo < "2015/01/01"|<range:foo lt="2015/01/01">
foo ≤ "2015/01/01"|<range:foo lte="2015/01/01">
foo > "2015/01/01"|<range:foo gt="2015/01/01">
foo ≥ "2015/01/01"|<range:foo gte="2015/01/01">
foo < 42|<range:field.foo lt="42">
foo ≤ 42|<range:field.foo lte="42">
foo > 42|<range:field.foo gt="42">
foo ≥ 42|<range:field.foo gte="42">
foo < 2015/01/01|<range:field.foo lt="2015/01/01">
foo ≤ 2015/01/01|<range:field.foo lte="2015/01/01">
foo > 2015/01/01|<range:field.foo gt="2015/01/01">
foo ≥ 2015/01/01|<range:field.foo gte="2015/01/01">
foo < "2015/01/01"|<range:field.foo lt="2015/01/01">
foo ≤ "2015/01/01"|<range:field.foo lte="2015/01/01">
foo > "2015/01/01"|<range:field.foo gt="2015/01/01">
foo ≥ "2015/01/01"|<range:field.foo gte="2015/01/01">
foo = 42|(<field:foo> == <value:"42">)
foo = bar|(<field:foo> == <value:"bar">)
foo = "bar"|(<field:foo> == <value:"bar">)
@@ -104,6 +104,11 @@ true|<text:"true">
# Metadata (EXIF or anything else) matcher
meta.MimeType:image/jpeg|<metadata.MimeType:"image/jpeg">
exif.MimeType:image/jpeg|<metadata.MimeType:"image/jpeg">
meta.Duration < 300|<range:metadata.Duration lt="300">
meta.Duration ≤ 300|<range:metadata.Duration lte="300">
meta.Duration > 300|<range:metadata.Duration gt="300">
meta.Duration ≥ 300|<range:metadata.Duration gte="300">
# Unescaped "." issue on key prefixes
fieldOne:foo|(<field:fieldOne> MATCHES <text:"foo">)
Can't render this file because it contains an unexpected character in line 1 and column 11.