mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-18 07:23:13 +00:00
Handle inequality comparisons with technical data.
This commit is contained in:
@@ -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()+
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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.
|
Reference in New Issue
Block a user