mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-18 15:33:15 +00:00
Handle inequality comparisons with technical data.
This commit is contained in:
@@ -32,7 +32,7 @@
|
|||||||
%token id id|recordid
|
%token id id|recordid
|
||||||
%token field_prefix field\.
|
%token field_prefix field\.
|
||||||
%token flag_prefix flag\.
|
%token flag_prefix flag\.
|
||||||
%token meta_prefix meta\.
|
%token meta_prefix (?:meta|exif)\.
|
||||||
%token true true|1
|
%token true true|1
|
||||||
%token false false|0
|
%token false false|0
|
||||||
%token word [^\s\(\)\[\]:<>≤≥=]+
|
%token word [^\s\(\)\[\]:<>≤≥=]+
|
||||||
@@ -73,10 +73,10 @@ key_value_pair:
|
|||||||
| ::flag_prefix:: flag() ::colon:: ::space::? boolean() #flag_statement
|
| ::flag_prefix:: flag() ::colon:: ::space::? boolean() #flag_statement
|
||||||
| ::field_prefix:: field() ::colon:: ::space::? term() #field_statement
|
| ::field_prefix:: field() ::colon:: ::space::? term() #field_statement
|
||||||
| field() ::colon:: ::space::? term() #field_statement
|
| field() ::colon:: ::space::? term() #field_statement
|
||||||
| field() ::space::? ::lt:: ::space::? value() #less_than
|
| key() ::space::? ::lt:: ::space::? value() #less_than
|
||||||
| field() ::space::? ::gt:: ::space::? value() #greater_than
|
| key() ::space::? ::gt:: ::space::? value() #greater_than
|
||||||
| field() ::space::? ::lte:: ::space::? value() #less_than_or_equal_to
|
| key() ::space::? ::lte:: ::space::? value() #less_than_or_equal_to
|
||||||
| field() ::space::? ::gte:: ::space::? value() #greater_than_or_equal_to
|
| key() ::space::? ::gte:: ::space::? value() #greater_than_or_equal_to
|
||||||
| field() ::space::? ::equal:: ::space::? value() #equal_to
|
| field() ::space::? ::equal:: ::space::? value() #equal_to
|
||||||
|
|
||||||
#native_key:
|
#native_key:
|
||||||
@@ -85,9 +85,18 @@ key_value_pair:
|
|||||||
| <type>
|
| <type>
|
||||||
| <id>
|
| <id>
|
||||||
|
|
||||||
|
key:
|
||||||
|
::meta_prefix:: meta_key()
|
||||||
|
| ::field_prefix:: field_key()
|
||||||
|
| field_key()
|
||||||
|
|
||||||
#meta_key:
|
#meta_key:
|
||||||
word_or_keyword()+
|
word_or_keyword()+
|
||||||
|
|
||||||
|
#field_key:
|
||||||
|
word_or_keyword()+
|
||||||
|
| quoted_string()
|
||||||
|
|
||||||
#flag:
|
#flag:
|
||||||
word_or_keyword()+
|
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;
|
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||||
|
|
||||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
|
||||||
use Assert\Assertion;
|
use Assert\Assertion;
|
||||||
|
|
||||||
class MetadataKey implements Key
|
class MetadataKey implements Key
|
||||||
|
@@ -2,41 +2,49 @@
|
|||||||
|
|
||||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
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\Exception\QueryException;
|
||||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||||
|
|
||||||
class RangeExpression extends Node
|
class RangeExpression extends Node
|
||||||
{
|
{
|
||||||
private $field;
|
private $key;
|
||||||
|
private $field_cache;
|
||||||
private $lower_bound;
|
private $lower_bound;
|
||||||
private $lower_inclusive;
|
private $lower_inclusive;
|
||||||
private $higher_bound;
|
private $higher_bound;
|
||||||
private $higher_inclusive;
|
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_bound = $lb;
|
||||||
$this->lower_inclusive = $li;
|
$this->lower_inclusive = $li;
|
||||||
$this->higher_bound = $hb;
|
$this->higher_bound = $hb;
|
||||||
@@ -45,14 +53,9 @@ class RangeExpression extends Node
|
|||||||
|
|
||||||
public function buildQuery(QueryContext $context)
|
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();
|
$params = array();
|
||||||
if ($this->lower_bound !== null) {
|
if ($this->lower_bound !== null) {
|
||||||
if (!$structure_field->isValueCompatible($this->lower_bound)) {
|
if (!$this->isValueCompatible($this->lower_bound, $context)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->lower_inclusive) {
|
if ($this->lower_inclusive) {
|
||||||
@@ -62,7 +65,7 @@ class RangeExpression extends Node
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->higher_bound !== null) {
|
if ($this->higher_bound !== null) {
|
||||||
if (!$structure_field->isValueCompatible($this->higher_bound)) {
|
if (!$this->isValueCompatible($this->higher_bound, $context)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->higher_inclusive) {
|
if ($this->higher_inclusive) {
|
||||||
@@ -73,9 +76,48 @@ class RangeExpression extends Node
|
|||||||
}
|
}
|
||||||
|
|
||||||
$query = [];
|
$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()
|
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 EQUAL_EXPR = '#equal_to';
|
||||||
const FIELD_STATEMENT = '#field_statement';
|
const FIELD_STATEMENT = '#field_statement';
|
||||||
const FIELD = '#field';
|
const FIELD = '#field';
|
||||||
|
const FIELD_KEY = '#field_key';
|
||||||
const VALUE = '#value';
|
const VALUE = '#value';
|
||||||
const TERM = '#thesaurus_term';
|
const TERM = '#thesaurus_term';
|
||||||
const TEXT = '#text';
|
const TEXT = '#text';
|
||||||
|
@@ -101,6 +101,9 @@ class QueryVisitor implements Visit
|
|||||||
case NodeTypes::METADATA_KEY:
|
case NodeTypes::METADATA_KEY:
|
||||||
return $this->visitMetadataKeyNode($element);
|
return $this->visitMetadataKeyNode($element);
|
||||||
|
|
||||||
|
case NodeTypes::FIELD_KEY:
|
||||||
|
return new AST\KeyValue\FieldKey($this->visitString($element));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Exception(sprintf('Unknown node type "%s".', $element->getId()));
|
throw new Exception(sprintf('Unknown node type "%s".', $element->getId()));
|
||||||
}
|
}
|
||||||
@@ -151,18 +154,18 @@ class QueryVisitor implements Visit
|
|||||||
if ($node->getChildrenNumber() !== 2) {
|
if ($node->getChildrenNumber() !== 2) {
|
||||||
throw new Exception('Comparison operator can only have 2 childs.');
|
throw new Exception('Comparison operator can only have 2 childs.');
|
||||||
}
|
}
|
||||||
$field = $node->getChild(0)->accept($this);
|
$key = $node->getChild(0)->accept($this);
|
||||||
$expression = $node->getChild(1)->accept($this);
|
$boundary = $node->getChild(1)->accept($this);
|
||||||
|
|
||||||
switch ($node->getId()) {
|
switch ($node->getId()) {
|
||||||
case NodeTypes::LT_EXPR:
|
case NodeTypes::LT_EXPR:
|
||||||
return AST\RangeExpression::lessThan($field, $expression);
|
return AST\RangeExpression::lessThan($key, $boundary);
|
||||||
case NodeTypes::LTE_EXPR:
|
case NodeTypes::LTE_EXPR:
|
||||||
return AST\RangeExpression::lessThanOrEqual($field, $expression);
|
return AST\RangeExpression::lessThanOrEqual($key, $boundary);
|
||||||
case NodeTypes::GT_EXPR:
|
case NodeTypes::GT_EXPR:
|
||||||
return AST\RangeExpression::greaterThan($field, $expression);
|
return AST\RangeExpression::greaterThan($key, $boundary);
|
||||||
case NodeTypes::GTE_EXPR:
|
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">))
|
foo EXCEPT (bar EXCEPT baz)|(<text:"foo"> EXCEPT (<text:"bar"> EXCEPT <text:"baz">))
|
||||||
|
|
||||||
# Comparison operators
|
# Comparison operators
|
||||||
foo < 42|<range:foo lt="42">
|
foo < 42|<range:field.foo lt="42">
|
||||||
foo ≤ 42|<range:foo lte="42">
|
foo ≤ 42|<range:field.foo lte="42">
|
||||||
foo > 42|<range:foo gt="42">
|
foo > 42|<range:field.foo gt="42">
|
||||||
foo ≥ 42|<range:foo gte="42">
|
foo ≥ 42|<range:field.foo gte="42">
|
||||||
foo < 2015/01/01|<range:foo lt="2015/01/01">
|
foo < 2015/01/01|<range:field.foo lt="2015/01/01">
|
||||||
foo ≤ 2015/01/01|<range:foo lte="2015/01/01">
|
foo ≤ 2015/01/01|<range:field.foo lte="2015/01/01">
|
||||||
foo > 2015/01/01|<range:foo gt="2015/01/01">
|
foo > 2015/01/01|<range:field.foo gt="2015/01/01">
|
||||||
foo ≥ 2015/01/01|<range:foo gte="2015/01/01">
|
foo ≥ 2015/01/01|<range:field.foo gte="2015/01/01">
|
||||||
foo < "2015/01/01"|<range:foo lt="2015/01/01">
|
foo < "2015/01/01"|<range:field.foo lt="2015/01/01">
|
||||||
foo ≤ "2015/01/01"|<range:foo lte="2015/01/01">
|
foo ≤ "2015/01/01"|<range:field.foo lte="2015/01/01">
|
||||||
foo > "2015/01/01"|<range:foo gt="2015/01/01">
|
foo > "2015/01/01"|<range:field.foo gt="2015/01/01">
|
||||||
foo ≥ "2015/01/01"|<range:foo gte="2015/01/01">
|
foo ≥ "2015/01/01"|<range:field.foo gte="2015/01/01">
|
||||||
foo = 42|(<field:foo> == <value:"42">)
|
foo = 42|(<field:foo> == <value:"42">)
|
||||||
foo = bar|(<field:foo> == <value:"bar">)
|
foo = bar|(<field:foo> == <value:"bar">)
|
||||||
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
|
# Metadata (EXIF or anything else) matcher
|
||||||
meta.MimeType:image/jpeg|<metadata.MimeType:"image/jpeg">
|
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
|
# Unescaped "." issue on key prefixes
|
||||||
fieldOne:foo|(<field:fieldOne> MATCHES <text:"foo">)
|
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