mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-14 21:43:18 +00:00
Merge pull request #1564 from mdarse/query-on-exif
Query support for metadata tags
This commit is contained in:
@@ -30,8 +30,9 @@
|
||||
%token collection collection
|
||||
%token type type
|
||||
%token id id|recordid
|
||||
%token field_prefix field.
|
||||
%token flag_prefix flag.
|
||||
%token field_prefix field\.
|
||||
%token flag_prefix flag\.
|
||||
%token meta_prefix (?:meta|exif)\.
|
||||
%token true true|1
|
||||
%token false false|0
|
||||
%token word [^\s\(\)\[\]:<>≤≥=]+
|
||||
@@ -67,18 +68,19 @@ quaternary:
|
||||
// Key value pairs & field level matchers (restricted to a single field)
|
||||
|
||||
key_value_pair:
|
||||
native_key() ::colon:: ::space::? value() #native_key_value
|
||||
match_key() ::colon:: ::space::? value() #match_expression
|
||||
| ::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
|
||||
| field() ::space::? ::equal:: ::space::? value() #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
|
||||
| key() ::space::? ::equal:: ::space::? value() #equal_to
|
||||
|
||||
#flag:
|
||||
word_or_keyword()+
|
||||
match_key:
|
||||
native_key()
|
||||
| ::meta_prefix:: meta_key()
|
||||
|
||||
#native_key:
|
||||
<database>
|
||||
@@ -86,6 +88,21 @@ 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()+
|
||||
|
||||
#field:
|
||||
word_or_keyword()+
|
||||
| quoted_string()
|
||||
@@ -149,6 +166,7 @@ keyword:
|
||||
| <id>
|
||||
| <field_prefix>
|
||||
| <flag_prefix>
|
||||
| <meta_prefix>
|
||||
| <true>
|
||||
| <false>
|
||||
|
||||
|
@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\Boolean;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
class AndOperator extends BinaryOperator
|
||||
class AndExpression extends BinaryExpression
|
||||
{
|
||||
protected $operator = 'AND';
|
||||
|
@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\Boolean;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Node;
|
||||
|
||||
abstract class BinaryOperator extends Node
|
||||
abstract class BinaryExpression extends Node
|
||||
{
|
||||
protected $left;
|
||||
protected $right;
|
@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\Boolean;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
class ExceptOperator extends BinaryOperator
|
||||
class ExceptExpression extends BinaryExpression
|
||||
{
|
||||
protected $operator = 'EXCEPT';
|
||||
|
@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\Boolean;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
class OrOperator extends BinaryOperator
|
||||
class OrExpression extends BinaryExpression
|
||||
{
|
||||
protected $operator = 'OR';
|
||||
|
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||
|
||||
class FieldEqualsExpression extends Node
|
||||
{
|
||||
private $field;
|
||||
private $value;
|
||||
|
||||
public function __construct(Field $field, $value)
|
||||
{
|
||||
$this->field = $field;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
if (!$structure_field->isValueCompatible($this->value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query = [
|
||||
'term' => [
|
||||
$structure_field->getIndexField(true) => $this->value
|
||||
]
|
||||
];
|
||||
|
||||
return QueryHelper::wrapPrivateFieldQuery($structure_field, $query);
|
||||
}
|
||||
|
||||
public function getTermNodes()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('(%s == <value:"%s">)', $this->field, $this->value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\FieldKey;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Node;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryPostProcessor;
|
||||
|
||||
class EqualExpression extends Node
|
||||
{
|
||||
private $key;
|
||||
private $value;
|
||||
|
||||
public function __construct(Key $key, $value)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function buildQuery(QueryContext $context)
|
||||
{
|
||||
if (!$this->key->isValueCompatible($this->value, $context)) {
|
||||
throw new QueryException(sprintf('Value "%s" for metadata tag "%s" is not valid.', $this->value, $this->key));
|
||||
}
|
||||
|
||||
$query = [
|
||||
'term' => [
|
||||
$this->key->getIndexField($context, true) => $this->value
|
||||
]
|
||||
];
|
||||
|
||||
if ($this->key instanceof QueryPostProcessor) {
|
||||
return $this->key->postProcessQuery($query, $context);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function getTermNodes()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('(<%s> == <value:"%s">)', $this->key, $this->value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryPostProcessor;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
|
||||
use Assert\Assertion;
|
||||
|
||||
class FieldKey implements Key, QueryPostProcessor
|
||||
{
|
||||
private $name;
|
||||
private $field_cache = [];
|
||||
|
||||
public function __construct($name)
|
||||
{
|
||||
Assertion::string($name);
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getIndexField(QueryContext $context, $raw = false)
|
||||
{
|
||||
return $this->getField($context)->getIndexField($raw);
|
||||
}
|
||||
|
||||
public function isValueCompatible($value, QueryContext $context)
|
||||
{
|
||||
return ValueChecker::isValueCompatible($this->getField($context), $value);
|
||||
}
|
||||
|
||||
public function postProcessQuery($query, QueryContext $context)
|
||||
{
|
||||
$field = $this->getField($context);
|
||||
return QueryHelper::wrapPrivateFieldQuery($field, $query);
|
||||
}
|
||||
|
||||
private function getField(QueryContext $context)
|
||||
{
|
||||
$hash = spl_object_hash($context);
|
||||
if (!isset($this->field_cache[$hash])) {
|
||||
$this->field_cache[$hash] = $context->get($this->name);
|
||||
}
|
||||
$field = $this->field_cache[$hash];
|
||||
if ($field === null) {
|
||||
throw new QueryException(sprintf('Field "%s" does not exist', $this->name));
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
|
||||
public function clearCache()
|
||||
{
|
||||
$this->field_cache = [];
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('field.%s', $this->name);
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
interface Key
|
||||
{
|
||||
public function buildQueryForValue($value, QueryContext $context);
|
||||
public function getIndexField(QueryContext $context, $raw = false);
|
||||
public function isValueCompatible($value, QueryContext $context);
|
||||
public function __toString();
|
||||
}
|
||||
|
@@ -3,13 +3,14 @@
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Node;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Assert\Assertion;
|
||||
|
||||
class Expression extends Node
|
||||
class MatchExpression extends Node
|
||||
{
|
||||
protected $key;
|
||||
protected $value;
|
||||
private $key;
|
||||
private $value;
|
||||
|
||||
public function __construct(Key $key, $value)
|
||||
{
|
||||
@@ -20,7 +21,15 @@ class Expression extends Node
|
||||
|
||||
public function buildQuery(QueryContext $context)
|
||||
{
|
||||
return $this->key->buildQueryForValue($this->value, $context);
|
||||
if (!$this->key->isValueCompatible($this->value, $context)) {
|
||||
throw new QueryException(sprintf('Value "%s" for metadata tag "%s" is not valid.', $this->value, $this->key));
|
||||
}
|
||||
|
||||
return [
|
||||
'match' => [
|
||||
$this->key->getIndexField($context) => $this->value
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getTermNodes()
|
||||
@@ -30,6 +39,6 @@ class Expression extends Node
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('<%s:%s>', $this->key, $this->value);
|
||||
return sprintf('<%s:"%s">', $this->key, $this->value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
|
||||
use Assert\Assertion;
|
||||
|
||||
class MetadataKey implements Key
|
||||
{
|
||||
private $name;
|
||||
private $tag_cache = [];
|
||||
|
||||
public function __construct($name)
|
||||
{
|
||||
Assertion::string($name);
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getIndexField(QueryContext $context, $raw = false)
|
||||
{
|
||||
return $this->getTag($context)->getIndexField($raw);
|
||||
}
|
||||
|
||||
public function isValueCompatible($value, QueryContext $context)
|
||||
{
|
||||
return ValueChecker::isValueCompatible($this->getTag($context), $value);
|
||||
}
|
||||
|
||||
private function getTag(QueryContext $context)
|
||||
{
|
||||
$hash = spl_object_hash($context);
|
||||
if (!isset($this->tag_cache[$hash])) {
|
||||
$this->tag_cache[$hash] = $context->getMetadataTag($this->name);
|
||||
}
|
||||
$tag = $this->tag_cache[$hash];
|
||||
if ($tag === null) {
|
||||
throw new QueryException(sprintf('Metadata tag "%s" does not exist', $this->name));
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public function clearCache()
|
||||
{
|
||||
$this->tag_cache = [];
|
||||
}
|
||||
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('metadata.%s', $this->name);
|
||||
}
|
||||
}
|
@@ -3,7 +3,6 @@
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Assert\Assertion;
|
||||
|
||||
class NativeKey implements Key
|
||||
{
|
||||
@@ -41,14 +40,14 @@ class NativeKey implements Key
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
public function buildQueryForValue($value, QueryContext $context)
|
||||
public function getIndexField(QueryContext $context, $raw = false)
|
||||
{
|
||||
Assertion::string($value);
|
||||
return [
|
||||
'term' => [
|
||||
$this->key => $value
|
||||
]
|
||||
];
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function isValueCompatible($value, QueryContext $context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
|
@@ -1,42 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
|
||||
|
||||
use Assert\Assertion;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\FieldKey;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Node;
|
||||
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 $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,16 +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)) {
|
||||
return;
|
||||
}
|
||||
$this->assertValueCompatible($this->lower_bound, $context);
|
||||
if ($this->lower_inclusive) {
|
||||
$params['lte'] = $this->lower_bound;
|
||||
} else {
|
||||
@@ -62,9 +63,7 @@ class RangeExpression extends Node
|
||||
}
|
||||
}
|
||||
if ($this->higher_bound !== null) {
|
||||
if (!$structure_field->isValueCompatible($this->higher_bound)) {
|
||||
return;
|
||||
}
|
||||
$this->assertValueCompatible($this->higher_bound, $context);
|
||||
if ($this->higher_inclusive) {
|
||||
$params['gte'] = $this->higher_bound;
|
||||
} else {
|
||||
@@ -73,9 +72,20 @@ class RangeExpression extends Node
|
||||
}
|
||||
|
||||
$query = [];
|
||||
$query['range'][$structure_field->getIndexField()] = $params;
|
||||
$query['range'][$this->key->getIndexField($context)] = $params;
|
||||
|
||||
return QueryHelper::wrapPrivateFieldQuery($structure_field, $query);
|
||||
if ($this->key instanceof QueryPostProcessor) {
|
||||
return $this->key->postProcessQuery($query, $context);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function assertValueCompatible($value, QueryContext $context)
|
||||
{
|
||||
if (!$this->key->isValueCompatible($value, $context)) {
|
||||
throw new QueryException(sprintf('Value "%s" for metadata tag "%s" is not valid.', $value, $this->key));
|
||||
}
|
||||
}
|
||||
|
||||
public function getTermNodes()
|
||||
@@ -101,6 +111,6 @@ class RangeExpression extends Node
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('<range:%s%s>', $this->field->getValue(), $string);
|
||||
return sprintf('<range:%s%s>', $this->key, $string);
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
|
||||
|
||||
class QuotedTextNode extends Node
|
||||
{
|
||||
@@ -38,11 +38,11 @@ class QuotedTextNode extends Node
|
||||
};
|
||||
|
||||
$unrestricted_fields = $context->getUnrestrictedFields();
|
||||
$unrestricted_fields = StructureField::filterByValueCompatibility($unrestricted_fields, $this->text);
|
||||
$unrestricted_fields = ValueChecker::filterByValueCompatibility($unrestricted_fields, $this->text);
|
||||
$query = $query_builder($unrestricted_fields);
|
||||
|
||||
$private_fields = $context->getPrivateFields();
|
||||
$private_fields = StructureField::filterByValueCompatibility($private_fields, $this->text);
|
||||
$private_fields = ValueChecker::filterByValueCompatibility($private_fields, $this->text);
|
||||
foreach (QueryHelper::wrapPrivateFieldQueries($private_fields, $query_builder) as $private_field_query) {
|
||||
$query = QueryHelper::applyBooleanClause($query, 'should', $private_field_query);
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
|
||||
|
||||
class RawNode extends Node
|
||||
{
|
||||
@@ -56,11 +56,11 @@ class RawNode extends Node
|
||||
};
|
||||
|
||||
$unrestricted_fields = $context->getUnrestrictedFields();
|
||||
$unrestricted_fields = StructureField::filterByValueCompatibility($unrestricted_fields, $this->text);
|
||||
$unrestricted_fields = ValueChecker::filterByValueCompatibility($unrestricted_fields, $this->text);
|
||||
$query = $query_builder($unrestricted_fields);
|
||||
|
||||
$private_fields = $context->getPrivateFields();
|
||||
$private_fields = StructureField::filterByValueCompatibility($private_fields, $this->text);
|
||||
$private_fields = ValueChecker::filterByValueCompatibility($private_fields, $this->text);
|
||||
foreach (QueryHelper::wrapPrivateFieldQueries($private_fields, $query_builder) as $private_field_query) {
|
||||
$query = QueryHelper::applyBooleanClause($query, 'should', $private_field_query);
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Term;
|
||||
|
||||
class TextNode extends AbstractTermNode implements ContextAbleInterface
|
||||
@@ -41,7 +41,7 @@ class TextNode extends AbstractTermNode implements ContextAbleInterface
|
||||
$query_builder = function (array $fields) use ($context) {
|
||||
// Full text
|
||||
$index_fields = [];
|
||||
foreach (StructureField::filterByValueCompatibility($fields, $this->text) as $field) {
|
||||
foreach (ValueChecker::filterByValueCompatibility($fields, $this->text) as $field) {
|
||||
foreach ($context->localizeField($field) as $f) {
|
||||
$index_fields[] = $f;
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ SQL;
|
||||
$value = $metadata['value'];
|
||||
|
||||
// Do not keep empty values
|
||||
if (empty($key) || empty($value)) {
|
||||
if ($key === '' || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -76,23 +76,7 @@ SQL;
|
||||
case 'caption':
|
||||
// Sanitize fields
|
||||
$value = StringHelper::crlfNormalize($value);
|
||||
switch ($this->structure->typeOf($key)) {
|
||||
case Mapping::TYPE_DATE:
|
||||
$value = $this->helper->sanitizeDate($value);
|
||||
break;
|
||||
|
||||
case Mapping::TYPE_FLOAT:
|
||||
case Mapping::TYPE_DOUBLE:
|
||||
$value = (float) $value;
|
||||
break;
|
||||
|
||||
case Mapping::TYPE_INTEGER:
|
||||
case Mapping::TYPE_LONG:
|
||||
case Mapping::TYPE_SHORT:
|
||||
case Mapping::TYPE_BYTE:
|
||||
$value = (int) $value;
|
||||
break;
|
||||
}
|
||||
$value = $this->sanitizeValue($value, $this->structure->typeOf($key));
|
||||
// Private caption fields are kept apart
|
||||
$type = $metadata['private'] ? 'private_caption' : 'caption';
|
||||
// Caption are multi-valued
|
||||
@@ -110,6 +94,10 @@ SQL;
|
||||
|
||||
case 'exif':
|
||||
// EXIF data is single-valued
|
||||
$tag = $this->structure->getMetadataTagByName($key);
|
||||
if ($tag) {
|
||||
$value = $this->sanitizeValue($value, $tag->getType());
|
||||
}
|
||||
$record['exif'][$key] = $value;
|
||||
break;
|
||||
|
||||
@@ -119,4 +107,28 @@ SQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitizeValue($value, $type)
|
||||
{
|
||||
switch ($type) {
|
||||
case Mapping::TYPE_DATE:
|
||||
return $this->helper->sanitizeDate($value);
|
||||
|
||||
case Mapping::TYPE_FLOAT:
|
||||
case Mapping::TYPE_DOUBLE:
|
||||
return (float) $value;
|
||||
|
||||
case Mapping::TYPE_INTEGER:
|
||||
case Mapping::TYPE_LONG:
|
||||
case Mapping::TYPE_SHORT:
|
||||
case Mapping::TYPE_BYTE:
|
||||
return (int) $value;
|
||||
|
||||
case Mapping::TYPE_BOOLEAN:
|
||||
return (bool) $value;
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -36,7 +36,6 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\CandidateTerms;
|
||||
use databox;
|
||||
use Iterator;
|
||||
use media_subdef;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class RecordIndexer
|
||||
@@ -308,7 +307,7 @@ class RecordIndexer
|
||||
// Thesaurus
|
||||
->add('concept_path', $this->getThesaurusPathMapping())
|
||||
// EXIF
|
||||
->add('exif', $this->getExifMapping())
|
||||
->add('exif', $this->getMetadataTagMapping())
|
||||
// Status
|
||||
->add('flags', $this->getFlagsMapping())
|
||||
->add('flags_bitfield', 'integer')->notIndexed()
|
||||
@@ -375,36 +374,20 @@ class RecordIndexer
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
// @todo Add call to addAnalyzedVersion ?
|
||||
private function getExifMapping()
|
||||
private function getMetadataTagMapping()
|
||||
{
|
||||
$mapping = new Mapping();
|
||||
$mapping
|
||||
->add(media_subdef::TC_DATA_WIDTH, 'integer')
|
||||
->add(media_subdef::TC_DATA_HEIGHT, 'integer')
|
||||
->add(media_subdef::TC_DATA_COLORSPACE, 'string')->notAnalyzed()
|
||||
->add(media_subdef::TC_DATA_CHANNELS, 'integer')
|
||||
->add(media_subdef::TC_DATA_ORIENTATION, 'integer')
|
||||
->add(media_subdef::TC_DATA_COLORDEPTH, 'integer')
|
||||
->add(media_subdef::TC_DATA_DURATION, 'float')
|
||||
->add(media_subdef::TC_DATA_AUDIOCODEC, 'string')->notAnalyzed()
|
||||
->add(media_subdef::TC_DATA_AUDIOSAMPLERATE, 'float')
|
||||
->add(media_subdef::TC_DATA_VIDEOCODEC, 'string')->notAnalyzed()
|
||||
->add(media_subdef::TC_DATA_FRAMERATE, 'float')
|
||||
->add(media_subdef::TC_DATA_MIMETYPE, 'string')->notAnalyzed()
|
||||
->add(media_subdef::TC_DATA_FILESIZE, 'long')
|
||||
// TODO use geo point type for lat/long
|
||||
->add(media_subdef::TC_DATA_LONGITUDE, 'float')
|
||||
->add(media_subdef::TC_DATA_LATITUDE, 'float')
|
||||
->add(media_subdef::TC_DATA_FOCALLENGTH, 'float')
|
||||
->add(media_subdef::TC_DATA_CAMERAMODEL, 'string')
|
||||
->add(media_subdef::TC_DATA_FLASHFIRED, 'boolean')
|
||||
->add(media_subdef::TC_DATA_APERTURE, 'float')
|
||||
->add(media_subdef::TC_DATA_SHUTTERSPEED, 'float')
|
||||
->add(media_subdef::TC_DATA_HYPERFOCALDISTANCE, 'float')
|
||||
->add(media_subdef::TC_DATA_ISO, 'integer')
|
||||
->add(media_subdef::TC_DATA_LIGHTVALUE, 'float')
|
||||
;
|
||||
foreach ($this->structure->getMetadataTags() as $tag) {
|
||||
$type = $tag->getType();
|
||||
$mapping->add($tag->getName(), $type);
|
||||
if ($type === Mapping::TYPE_STRING) {
|
||||
if ($tag->isAnalyzable()) {
|
||||
$mapping->addRawVersion();
|
||||
} else {
|
||||
$mapping->notAnalyzed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
}
|
||||
|
@@ -15,15 +15,17 @@ class NodeTypes
|
||||
const LTE_EXPR = '#less_than_or_equal_to';
|
||||
const GTE_EXPR = '#greater_than_or_equal_to';
|
||||
const EQUAL_EXPR = '#equal_to';
|
||||
const MATCH_EXPR = '#match_expression';
|
||||
const FIELD_STATEMENT = '#field_statement';
|
||||
const FIELD = '#field';
|
||||
const FIELD_KEY = '#field_key';
|
||||
const VALUE = '#value';
|
||||
const TERM = '#thesaurus_term';
|
||||
const TEXT = '#text';
|
||||
const CONTEXT = '#context';
|
||||
const METADATA_KEY = '#meta_key';
|
||||
const FLAG_STATEMENT = '#flag_statement';
|
||||
const FLAG = '#flag';
|
||||
const NATIVE_KEY_VALUE = '#native_key_value';
|
||||
const NATIVE_KEY = '#native_key';
|
||||
// Token types for leaf nodes
|
||||
const TOKEN_WORD = 'word';
|
||||
|
@@ -85,6 +85,11 @@ class QueryContext
|
||||
return $this->structure->getFlagByName($name);
|
||||
}
|
||||
|
||||
public function getMetadataTag($name)
|
||||
{
|
||||
return $this->structure->getMetadataTagByName($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Maybe we should put this logic in Field class?
|
||||
*/
|
||||
|
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\Search;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
interface QueryPostProcessor
|
||||
{
|
||||
public function postProcessQuery($query, QueryContext $context);
|
||||
}
|
@@ -89,12 +89,18 @@ class QueryVisitor implements Visit
|
||||
case NodeTypes::FLAG:
|
||||
return new AST\Flag($this->visitString($element));
|
||||
|
||||
case NodeTypes::NATIVE_KEY_VALUE:
|
||||
return $this->visitNativeKeyValueNode($element);
|
||||
case NodeTypes::MATCH_EXPR:
|
||||
return $this->visitMatchExpressionNode($element);
|
||||
|
||||
case NodeTypes::NATIVE_KEY:
|
||||
return $this->visitNativeKeyNode($element);
|
||||
|
||||
case NodeTypes::METADATA_KEY:
|
||||
return new AST\KeyValue\MetadataKey($this->visitString($element));
|
||||
|
||||
case NodeTypes::FIELD_KEY:
|
||||
return new AST\KeyValue\FieldKey($this->visitString($element));
|
||||
|
||||
default:
|
||||
throw new Exception(sprintf('Unknown node type "%s".', $element->getId()));
|
||||
}
|
||||
@@ -111,60 +117,53 @@ class QueryVisitor implements Visit
|
||||
|
||||
private function visitFieldStatementNode(TreeNode $node)
|
||||
{
|
||||
if ($node->getChildrenNumber() !== 2) {
|
||||
throw new Exception('Field statement must have 2 childs.');
|
||||
}
|
||||
$field = $this->visit($node->getChild(0));
|
||||
$value = $this->visit($node->getChild(1));
|
||||
return new AST\FieldMatchExpression($field, $value);
|
||||
return $this->handleBinaryExpression($node, function($left, $right) {
|
||||
return new AST\FieldMatchExpression($left, $right);
|
||||
});
|
||||
}
|
||||
|
||||
private function visitAndNode(Element $element)
|
||||
{
|
||||
return $this->handleBinaryOperator($element, function($left, $right) {
|
||||
return new AST\Boolean\AndOperator($left, $right);
|
||||
return $this->handleBinaryExpression($element, function($left, $right) {
|
||||
return new AST\Boolean\AndExpression($left, $right);
|
||||
});
|
||||
}
|
||||
|
||||
private function visitOrNode(Element $element)
|
||||
{
|
||||
return $this->handleBinaryOperator($element, function($left, $right) {
|
||||
return new AST\Boolean\OrOperator($left, $right);
|
||||
return $this->handleBinaryExpression($element, function($left, $right) {
|
||||
return new AST\Boolean\OrExpression($left, $right);
|
||||
});
|
||||
}
|
||||
|
||||
private function visitExceptNode(Element $element)
|
||||
{
|
||||
return $this->handleBinaryOperator($element, function($left, $right) {
|
||||
return new AST\Boolean\ExceptOperator($left, $right);
|
||||
return $this->handleBinaryExpression($element, function($left, $right) {
|
||||
return new AST\Boolean\ExceptExpression($left, $right);
|
||||
});
|
||||
}
|
||||
|
||||
private function visitRangeNode(TreeNode $node)
|
||||
{
|
||||
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);
|
||||
$this->assertChildrenCount($node, 2);
|
||||
$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\KeyValue\RangeExpression::lessThan($key, $boundary);
|
||||
case NodeTypes::LTE_EXPR:
|
||||
return AST\RangeExpression::lessThanOrEqual($field, $expression);
|
||||
return AST\KeyValue\RangeExpression::lessThanOrEqual($key, $boundary);
|
||||
case NodeTypes::GT_EXPR:
|
||||
return AST\RangeExpression::greaterThan($field, $expression);
|
||||
return AST\KeyValue\RangeExpression::greaterThan($key, $boundary);
|
||||
case NodeTypes::GTE_EXPR:
|
||||
return AST\RangeExpression::greaterThanOrEqual($field, $expression);
|
||||
return AST\KeyValue\RangeExpression::greaterThanOrEqual($key, $boundary);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleBinaryOperator(Element $element, \Closure $factory)
|
||||
private function handleBinaryExpression(Element $element, \Closure $factory)
|
||||
{
|
||||
if ($element->getChildrenNumber() !== 2) {
|
||||
throw new Exception('Binary expression can only have 2 childs.');
|
||||
}
|
||||
$this->assertChildrenCount($element, 2);
|
||||
$left = $element->getChild(0)->accept($this);
|
||||
$right = $element->getChild(1)->accept($this);
|
||||
|
||||
@@ -173,14 +172,9 @@ class QueryVisitor implements Visit
|
||||
|
||||
private function visitEqualNode(TreeNode $node)
|
||||
{
|
||||
if ($node->getChildrenNumber() !== 2) {
|
||||
throw new Exception('Equality operator can only have 2 childs.');
|
||||
}
|
||||
|
||||
return new AST\FieldEqualsExpression(
|
||||
$node->getChild(0)->accept($this),
|
||||
$node->getChild(1)->accept($this)
|
||||
);
|
||||
return $this->handleBinaryExpression($node, function($left, $right) {
|
||||
return new AST\KeyValue\EqualExpression($left, $right);
|
||||
});
|
||||
}
|
||||
|
||||
private function visitTerm(Element $element)
|
||||
@@ -242,7 +236,7 @@ class QueryVisitor implements Visit
|
||||
throw new Exception('Unexpected context after non-contextualizable node');
|
||||
}
|
||||
} elseif ($node instanceof AST\Node) {
|
||||
$root = new AST\Boolean\AndOperator($root, $node);
|
||||
$root = new AST\Boolean\AndExpression($root, $node);
|
||||
} else {
|
||||
throw new Exception('Unexpected node type inside text node.');
|
||||
}
|
||||
@@ -267,9 +261,7 @@ class QueryVisitor implements Visit
|
||||
|
||||
private function visitFlagStatementNode(TreeNode $node)
|
||||
{
|
||||
if ($node->getChildrenNumber() !== 2) {
|
||||
throw new Exception('Flag statement can only have 2 childs.');
|
||||
}
|
||||
$this->assertChildrenCount($node, 2);
|
||||
$flag = $node->getChild(0)->accept($this);
|
||||
if (!$flag instanceof AST\Flag) {
|
||||
throw new \Exception('Flag statement key must be a flag node.');
|
||||
@@ -298,21 +290,16 @@ class QueryVisitor implements Visit
|
||||
}
|
||||
}
|
||||
|
||||
private function visitNativeKeyValueNode(TreeNode $node)
|
||||
private function visitMatchExpressionNode(TreeNode $node)
|
||||
{
|
||||
if ($node->getChildrenNumber() !== 2) {
|
||||
throw new Exception('Key value expression can only have 2 childs.');
|
||||
}
|
||||
$key = $this->visit($node->getChild(0));
|
||||
$value = $this->visit($node->getChild(1));
|
||||
return new AST\KeyValue\Expression($key, $value);
|
||||
return $this->handleBinaryExpression($node, function($left, $right) {
|
||||
return new AST\KeyValue\MatchExpression($left, $right);
|
||||
});
|
||||
}
|
||||
|
||||
private function visitNativeKeyNode(Element $element)
|
||||
{
|
||||
if ($element->getChildrenNumber() !== 1) {
|
||||
throw new Exception('Native key node can only have a single child.');
|
||||
}
|
||||
$this->assertChildrenCount($element, 1);
|
||||
$type = $element->getChild(0)->getValue()['token'];
|
||||
switch ($type) {
|
||||
case NodeTypes::TOKEN_DATABASE:
|
||||
@@ -327,4 +314,11 @@ class QueryVisitor implements Visit
|
||||
throw new InvalidArgumentException(sprintf('Unexpected token type "%s" for native key.', $type));
|
||||
}
|
||||
}
|
||||
|
||||
private function assertChildrenCount(TreeNode $node, $count)
|
||||
{
|
||||
if ($node->getChildrenNumber() !== $count) {
|
||||
throw new Exception(sprintf('Node was expected to have only %s children.', $count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\MergeException;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Concept;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Helper as ThesaurusHelper;
|
||||
use Assert\Assertion;
|
||||
@@ -13,7 +12,7 @@ use databox_field;
|
||||
/**
|
||||
* @todo Field labels
|
||||
*/
|
||||
class Field
|
||||
class Field implements Typed
|
||||
{
|
||||
private $name;
|
||||
private $type;
|
||||
@@ -119,41 +118,6 @@ class Field
|
||||
);
|
||||
}
|
||||
|
||||
public function isValueCompatible($value)
|
||||
{
|
||||
return count(self::filterByValueCompatibility([$this], $value)) > 0;
|
||||
}
|
||||
|
||||
public static function filterByValueCompatibility(array $fields, $value)
|
||||
{
|
||||
$is_numeric = is_numeric($value);
|
||||
$is_valid_date = RecordHelper::validateDate($value);
|
||||
$filtered = [];
|
||||
foreach ($fields as $field) {
|
||||
switch ($field->type) {
|
||||
case Mapping::TYPE_FLOAT:
|
||||
case Mapping::TYPE_DOUBLE:
|
||||
case Mapping::TYPE_INTEGER:
|
||||
case Mapping::TYPE_LONG:
|
||||
case Mapping::TYPE_SHORT:
|
||||
case Mapping::TYPE_BYTE:
|
||||
if ($is_numeric) {
|
||||
$filtered[] = $field;
|
||||
}
|
||||
break;
|
||||
case Mapping::TYPE_DATE:
|
||||
if ($is_valid_date) {
|
||||
$filtered[] = $field;
|
||||
}
|
||||
break;
|
||||
case Mapping::TYPE_STRING:
|
||||
default:
|
||||
$filtered[] = $field;
|
||||
}
|
||||
}
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
public function getConceptPathIndexField()
|
||||
{
|
||||
return sprintf('concept_path.%s', $this->name);
|
||||
|
@@ -19,6 +19,7 @@ final class GlobalStructure implements Structure
|
||||
/** @var Field[] */
|
||||
private $facets = array();
|
||||
private $flags = array();
|
||||
private $metadata_tags = array();
|
||||
|
||||
/**
|
||||
* @param \databox[] $databoxes
|
||||
@@ -36,19 +37,23 @@ final class GlobalStructure implements Structure
|
||||
$flags[] = Flag::createFromLegacyStatus($status);
|
||||
}
|
||||
}
|
||||
return new self($fields, $flags);
|
||||
return new self($fields, $flags, MetadataHelper::createTags());
|
||||
}
|
||||
|
||||
public function __construct(array $fields = [], array $flags = [])
|
||||
public function __construct(array $fields = [], array $flags = [], array $metadata_tags = [])
|
||||
{
|
||||
Assertion::allIsInstanceOf($fields, Field::class);
|
||||
Assertion::allIsInstanceOf($flags, Flag::class);
|
||||
Assertion::allIsInstanceOf($metadata_tags, Tag::class);
|
||||
foreach ($fields as $field) {
|
||||
$this->add($field);
|
||||
}
|
||||
foreach ($flags as $flag) {
|
||||
$this->flags[$flag->getName()] = $flag;
|
||||
}
|
||||
foreach ($metadata_tags as $tag) {
|
||||
$this->metadata_tags[$tag->getName()] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
public function add(Field $field)
|
||||
@@ -148,6 +153,17 @@ final class GlobalStructure implements Structure
|
||||
$this->flags[$name] : null;
|
||||
}
|
||||
|
||||
public function getMetadataTags()
|
||||
{
|
||||
return $this->metadata_tags;
|
||||
}
|
||||
|
||||
public function getMetadataTagByName($name)
|
||||
{
|
||||
return isset($this->metadata_tags[$name]) ?
|
||||
$this->metadata_tags[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of collections indexed by field name.
|
||||
*
|
||||
|
@@ -85,6 +85,16 @@ final class LimitedStructure implements Structure
|
||||
return $this->structure->getFlagByName($name);
|
||||
}
|
||||
|
||||
public function getMetadataTags()
|
||||
{
|
||||
return $this->structure->getMetadataTags();
|
||||
}
|
||||
|
||||
public function getMetadataTagByName($name)
|
||||
{
|
||||
return $this->structure->getMetadataTagByName($name);
|
||||
}
|
||||
|
||||
private function limit(array $fields)
|
||||
{
|
||||
$allowed_collections = $this->allowedCollections();
|
||||
|
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
|
||||
|
||||
use Assert\Assertion;
|
||||
use InvalidArgumentException;
|
||||
use media_subdef;
|
||||
|
||||
class MetadataHelper
|
||||
{
|
||||
private function __construct() {}
|
||||
|
||||
public static function createTags()
|
||||
{
|
||||
static $tag_descriptors = [
|
||||
[media_subdef::TC_DATA_WIDTH , 'integer', false],
|
||||
[media_subdef::TC_DATA_HEIGHT , 'integer', false],
|
||||
[media_subdef::TC_DATA_COLORSPACE , 'string' , false],
|
||||
[media_subdef::TC_DATA_CHANNELS , 'integer', false],
|
||||
[media_subdef::TC_DATA_ORIENTATION , 'integer', false],
|
||||
[media_subdef::TC_DATA_COLORDEPTH , 'integer', false],
|
||||
[media_subdef::TC_DATA_DURATION , 'float' , false],
|
||||
[media_subdef::TC_DATA_AUDIOCODEC , 'string' , false],
|
||||
[media_subdef::TC_DATA_AUDIOSAMPLERATE , 'float' , false],
|
||||
[media_subdef::TC_DATA_VIDEOCODEC , 'string' , false],
|
||||
[media_subdef::TC_DATA_FRAMERATE , 'float' , false],
|
||||
[media_subdef::TC_DATA_MIMETYPE , 'string' , false],
|
||||
[media_subdef::TC_DATA_FILESIZE , 'long' , false],
|
||||
// TODO use geo point type for lat/long
|
||||
[media_subdef::TC_DATA_LONGITUDE , 'float' , false],
|
||||
[media_subdef::TC_DATA_LATITUDE , 'float' , false],
|
||||
[media_subdef::TC_DATA_FOCALLENGTH , 'float' , false],
|
||||
[media_subdef::TC_DATA_CAMERAMODEL , 'string' , true ],
|
||||
[media_subdef::TC_DATA_FLASHFIRED , 'boolean', false],
|
||||
[media_subdef::TC_DATA_APERTURE , 'float' , false],
|
||||
[media_subdef::TC_DATA_SHUTTERSPEED , 'float' , false],
|
||||
[media_subdef::TC_DATA_HYPERFOCALDISTANCE, 'float' , false],
|
||||
[media_subdef::TC_DATA_ISO , 'integer', false],
|
||||
[media_subdef::TC_DATA_LIGHTVALUE , 'float' , false]
|
||||
];
|
||||
|
||||
$tags = [];
|
||||
foreach ($tag_descriptors as $descriptor) {
|
||||
$tags[] = new Tag($descriptor[0], $descriptor[1], $descriptor[2]);
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
}
|
@@ -27,4 +27,7 @@ interface Structure
|
||||
|
||||
public function getAllFlags();
|
||||
public function getFlagByName($name);
|
||||
|
||||
public function getMetadataTags();
|
||||
public function getMetadataTagByName($name);
|
||||
}
|
||||
|
47
lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Tag.php
Normal file
47
lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Tag.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
|
||||
use Assert\Assertion;
|
||||
|
||||
class Tag implements Typed
|
||||
{
|
||||
private $name;
|
||||
private $type;
|
||||
private $analyzable;
|
||||
|
||||
public function __construct($name, $type, $analyzable = false)
|
||||
{
|
||||
Assertion::string($name);
|
||||
Assertion::string($type);
|
||||
Assertion::boolean($analyzable);
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->analyzable = $analyzable;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isAnalyzable()
|
||||
{
|
||||
return $this->analyzable;
|
||||
}
|
||||
|
||||
public function getIndexField($raw = false)
|
||||
{
|
||||
return sprintf(
|
||||
'exif.%s%s',
|
||||
$this->name,
|
||||
$raw && $this->type === Mapping::TYPE_STRING ? '.raw' : ''
|
||||
);
|
||||
}
|
||||
}
|
13
lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Typed.php
Normal file
13
lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Typed.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
|
||||
|
||||
interface Typed
|
||||
{
|
||||
/**
|
||||
* Get the type of the object
|
||||
*
|
||||
* @return string One of Mapping::TYPE_* constants
|
||||
*/
|
||||
public function getType();
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
|
||||
use Assert\Assertion;
|
||||
|
||||
class ValueChecker
|
||||
{
|
||||
private function __construct() {}
|
||||
|
||||
public static function isValueCompatible(Typed $typed, $value)
|
||||
{
|
||||
return count(self::filterByValueCompatibility([$typed], $value)) > 0;
|
||||
}
|
||||
|
||||
public static function filterByValueCompatibility(array $list, $value)
|
||||
{
|
||||
Assertion::allIsInstanceOf($list, Typed::class);
|
||||
$is_numeric = is_numeric($value);
|
||||
$is_valid_date = RecordHelper::validateDate($value);
|
||||
$filtered = [];
|
||||
foreach ($list as $item) {
|
||||
switch ($item->getType()) {
|
||||
case Mapping::TYPE_FLOAT:
|
||||
case Mapping::TYPE_DOUBLE:
|
||||
case Mapping::TYPE_INTEGER:
|
||||
case Mapping::TYPE_LONG:
|
||||
case Mapping::TYPE_SHORT:
|
||||
case Mapping::TYPE_BYTE:
|
||||
if ($is_numeric) {
|
||||
$filtered[] = $item;
|
||||
}
|
||||
break;
|
||||
case Mapping::TYPE_DATE:
|
||||
if ($is_valid_date) {
|
||||
$filtered[] = $item;
|
||||
}
|
||||
break;
|
||||
case Mapping::TYPE_STRING:
|
||||
default:
|
||||
$filtered[] = $item;
|
||||
}
|
||||
}
|
||||
return $filtered;
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Tests\Phrasea\SearchEngine\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\EqualExpression;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
|
||||
|
||||
/**
|
||||
* @group unit
|
||||
* @group searchengine
|
||||
* @group ast
|
||||
*/
|
||||
class EqualExpressionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testSerialization()
|
||||
{
|
||||
$this->assertTrue(method_exists(EqualExpression::class, '__toString'), 'Class does not have method __toString');
|
||||
$key = $this->prophesize(Key::class);
|
||||
$key->__toString()->willReturn('foo');
|
||||
$node = new EqualExpression($key->reveal(), 'bar');
|
||||
$this->assertEquals('(<foo> == <value:"bar">)', (string) $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider queryProvider
|
||||
*/
|
||||
public function testQueryBuild($index_field, $value, $compatible_value, $private, $expected_json)
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class)->reveal();
|
||||
|
||||
$key = $this->prophesize(Key::class);
|
||||
$key->isValueCompatible($value, $query_context)->willReturn($compatible_value);
|
||||
$key->getIndexField($query_context, true)->willReturn($index_field);
|
||||
$key->__toString()->willReturn('foo');
|
||||
// TODO Test keys implementing QueryPostProcessor
|
||||
|
||||
$node = new EqualExpression($key->reveal(), 'bar');
|
||||
$query = $node->buildQuery($query_context);
|
||||
|
||||
$this->assertEquals(json_decode($expected_json, true), $query);
|
||||
}
|
||||
|
||||
public function queryProvider()
|
||||
{
|
||||
return [
|
||||
// TODO Put this case in another test case
|
||||
// ['foo.raw', 'bar', true, true, '{
|
||||
// "filtered": {
|
||||
// "filter": {
|
||||
// "terms": {
|
||||
// "base_id": ["baz","qux"] } },
|
||||
// "query": {
|
||||
// "term": {
|
||||
// "foo.raw": "bar" } } } }'],
|
||||
['foo.raw', 'bar', true, false, '{
|
||||
"term": {
|
||||
"foo.raw": "bar" } }'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException
|
||||
* @expectedExceptionMessageRegExp #"foo"#u
|
||||
*/
|
||||
public function testQueryBuildWithIncompatibleValue()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class)->reveal();
|
||||
$key = $this->prophesize(Key::class);
|
||||
$key->isValueCompatible('bar', $query_context)->willReturn(false);
|
||||
$key->getIndexField($query_context, true)->willReturn('foo.raw');
|
||||
$key->__toString()->willReturn('foo');
|
||||
|
||||
$node = new EqualExpression($key->reveal(), 'bar');
|
||||
$node->buildQuery($query_context);
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Tests\Phrasea\SearchEngine\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Field as ASTField;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\FieldEqualsExpression;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
|
||||
|
||||
/**
|
||||
* @group unit
|
||||
* @group searchengine
|
||||
* @group ast
|
||||
*/
|
||||
class FieldEqualsExpressionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testSerialization()
|
||||
{
|
||||
$this->assertTrue(method_exists(FieldEqualsExpression::class, '__toString'), 'Class does not have method __toString');
|
||||
$field = $this->prophesize(ASTField::class);
|
||||
$field->__toString()->willReturn('foo');
|
||||
$node = new FieldEqualsExpression($field->reveal(), 'bar');
|
||||
$this->assertEquals('(foo == <value:"bar">)', (string) $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider queryProvider
|
||||
*/
|
||||
public function testQueryBuild($index_field, $value, $compatible_value, $private, $expected_json)
|
||||
{
|
||||
$structure_field = $this->prophesize(StructureField::class);
|
||||
$structure_field->isValueCompatible($value)->willReturn($compatible_value);
|
||||
$structure_field->getIndexField(true)->willReturn($index_field);
|
||||
$structure_field->isPrivate()->willReturn($private);
|
||||
if ($private) {
|
||||
$structure_field->getDependantCollections()->willReturn(['baz', 'qux']);
|
||||
}
|
||||
|
||||
$ast_field = $this->prophesize(ASTField::class);
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
$query_context->get($ast_field->reveal())->willReturn($structure_field);
|
||||
|
||||
$node = new FieldEqualsExpression($ast_field->reveal(), 'bar');
|
||||
$query = $node->buildQuery($query_context->reveal());
|
||||
|
||||
$this->assertEquals(json_decode($expected_json, true), $query);
|
||||
}
|
||||
|
||||
public function queryProvider()
|
||||
{
|
||||
return [
|
||||
['foo.raw', 'bar', true, true, '{
|
||||
"filtered": {
|
||||
"filter": {
|
||||
"terms": {
|
||||
"base_id": ["baz","qux"] } },
|
||||
"query": {
|
||||
"term": {
|
||||
"foo.raw": "bar" } } } }'],
|
||||
['foo.raw', 'bar', true, false, '{
|
||||
"term": {
|
||||
"foo.raw": "bar" } }'],
|
||||
['foo.raw', 'bar', false, true, 'null'],
|
||||
['foo.raw', 'bar', false, false, 'null'],
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Tests\Phrasea\SearchEngine\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Expression as KeyValueExpression;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
/**
|
||||
* @group unit
|
||||
* @group searchengine
|
||||
* @group ast
|
||||
*/
|
||||
class KeyValueExpressionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testSerialization()
|
||||
{
|
||||
$this->assertTrue(method_exists(KeyValueExpression::class, '__toString'), 'Class does not have method __toString');
|
||||
$key = $this->prophesize(Key::class);
|
||||
$key->__toString()->willReturn('foo');
|
||||
$node = new KeyValueExpression($key->reveal(), 'bar');
|
||||
$this->assertEquals('<foo:bar>', (string) $node);
|
||||
}
|
||||
|
||||
public function testQueryBuild()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
$key = $this->prophesize(Key::class);
|
||||
$key->buildQueryForValue('bar', $query_context->reveal())->willReturn('baz');
|
||||
|
||||
$node = new KeyValueExpression($key->reveal(), 'bar');
|
||||
$query = $node->buildQuery($query_context->reveal());
|
||||
$this->assertEquals('baz', $query);
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Tests\Phrasea\SearchEngine\AST;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\MatchExpression;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\MetadataKey;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\NativeKey;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
|
||||
/**
|
||||
* @group unit
|
||||
* @group searchengine
|
||||
* @group ast
|
||||
*/
|
||||
class MatchExpressionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testSerialization()
|
||||
{
|
||||
$this->assertTrue(method_exists(MatchExpression::class, '__toString'), 'Class does not have method __toString');
|
||||
$key = $this->prophesize(Key::class);
|
||||
$key->__toString()->willReturn('foo');
|
||||
$node = new MatchExpression($key->reveal(), 'bar');
|
||||
$this->assertEquals('<foo:"bar">', (string) $node);
|
||||
}
|
||||
|
||||
public function testQueryBuild()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class)->reveal();
|
||||
$key = $this->prophesize(Key::class);
|
||||
$key->isValueCompatible('bar', $query_context)->willReturn(true);
|
||||
$key->getIndexField($query_context)->willReturn('foo');
|
||||
$node = new MatchExpression($key->reveal(), 'bar');
|
||||
$query = $node->buildQuery($query_context);
|
||||
|
||||
$result = '{"match":{"foo": "bar"}}';
|
||||
$this->assertEquals(json_decode($result, true), $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider keyProvider
|
||||
*/
|
||||
public function testNativeQueryBuild($key, $value, $result)
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
$node = new MatchExpression($key, $value);
|
||||
$query = $node->buildQuery($query_context->reveal());
|
||||
$this->assertEquals(json_decode($result, true), $query);
|
||||
}
|
||||
|
||||
public function keyProvider()
|
||||
{
|
||||
return [
|
||||
[NativeKey::database(), 'foo', '{"match":{"databox_name": "foo"}}'],
|
||||
[NativeKey::collection(), 'bar', '{"match":{"collection_name": "bar"}}'],
|
||||
[NativeKey::mediaType(), 'baz', '{"match":{"type": "baz"}}'],
|
||||
[NativeKey::recordIdentifier(), 'qux', '{"match":{"record_id": "qux"}}'],
|
||||
];
|
||||
}
|
||||
}
|
@@ -21,63 +21,22 @@ class NativeKeyTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals('record_identifier', (string) NativeKey::recordIdentifier());
|
||||
}
|
||||
|
||||
public function testDatabaseQuery()
|
||||
/**
|
||||
* @dataProvider keyProvider
|
||||
*/
|
||||
public function testGetIndexField($key, $field)
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
$key = NativeKey::database();
|
||||
$query = $key->buildQueryForValue('bar', $query_context->reveal());
|
||||
|
||||
$expected = '{
|
||||
"term": {
|
||||
"databox_name": "bar"
|
||||
}
|
||||
}';
|
||||
|
||||
$this->assertEquals(json_decode($expected, true), $query);
|
||||
$this->assertEquals($key->getIndexField($query_context->reveal()), $field);
|
||||
}
|
||||
|
||||
public function testCollectionQuery()
|
||||
public function keyProvider()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
$key = NativeKey::collection();
|
||||
$query = $key->buildQueryForValue('bar', $query_context->reveal());
|
||||
|
||||
$expected = '{
|
||||
"term": {
|
||||
"collection_name": "bar"
|
||||
}
|
||||
}';
|
||||
|
||||
$this->assertEquals(json_decode($expected, true), $query);
|
||||
}
|
||||
|
||||
public function testMediaTypeQuery()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
$key = NativeKey::mediaType();
|
||||
$query = $key->buildQueryForValue('bar', $query_context->reveal());
|
||||
|
||||
$expected = '{
|
||||
"term": {
|
||||
"type": "bar"
|
||||
}
|
||||
}';
|
||||
|
||||
$this->assertEquals(json_decode($expected, true), $query);
|
||||
}
|
||||
|
||||
public function testRecordIdentifierQuery()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
$key = NativeKey::recordIdentifier();
|
||||
$query = $key->buildQueryForValue('bar', $query_context->reveal());
|
||||
|
||||
$expected = '{
|
||||
"term": {
|
||||
"record_id": "bar"
|
||||
}
|
||||
}';
|
||||
|
||||
$this->assertEquals(json_decode($expected, true), $query);
|
||||
return [
|
||||
[NativeKey::database(), 'databox_name'],
|
||||
[NativeKey::collection(), 'collection_name'],
|
||||
[NativeKey::mediaType(), 'type'],
|
||||
[NativeKey::recordIdentifier(), 'record_id']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -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\KeyValue\RangeExpression;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @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, $query_context, $key, $value, $result)
|
||||
{
|
||||
$node = RangeExpression::$factory($key, $value);
|
||||
$query = $node->buildQuery($query_context);
|
||||
$this->assertEquals(json_decode($result, true), $query);
|
||||
}
|
||||
|
||||
public function queryProvider()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class)->reveal();
|
||||
$key_prophecy = $this->prophesize(Key::class);
|
||||
$key_prophecy->getIndexField($query_context)->willReturn('foo');
|
||||
$key_prophecy->isValueCompatible('bar', $query_context)->willReturn(true);
|
||||
$key = $key_prophecy->reveal();
|
||||
return [
|
||||
['lessThan', $query_context, $key, 'bar', '{"range":{"foo": {"lt":"bar"}}}'],
|
||||
['lessThanOrEqual', $query_context, $key, 'bar', '{"range":{"foo": {"lte":"bar"}}}'],
|
||||
['greaterThan', $query_context, $key, 'bar', '{"range":{"foo": {"gt":"bar"}}}'],
|
||||
['greaterThanOrEqual', $query_context, $key, 'bar', '{"range":{"foo": {"gte":"bar"}}}'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testQueryBuildWithFieldKey()
|
||||
{
|
||||
$query_context = $this->prophesize(QueryContext::class)->reveal();
|
||||
$key = $this->prophesize(FieldKey::class);
|
||||
$key->getIndexField($query_context)->willReturn('baz');
|
||||
$key->isValueCompatible('bar', $query_context)->willReturn(true);
|
||||
$key->postProcessQuery(Argument::any(), $query_context)->willReturnArgument(0);
|
||||
|
||||
$node = RangeExpression::lessThan($key->reveal(), 'bar');
|
||||
$query = $node->buildQuery($query_context);
|
||||
|
||||
$expected = '{
|
||||
"range": {
|
||||
"baz": {
|
||||
"lt": "bar"
|
||||
}
|
||||
}
|
||||
}';
|
||||
|
||||
$this->assertEquals(json_decode($expected, true), $query);
|
||||
}
|
||||
}
|
@@ -24,6 +24,7 @@ class RawNodeTest extends \PHPUnit_Framework_TestCase
|
||||
public function testQueryBuildOnSingleField()
|
||||
{
|
||||
$field = $this->prophesize(Field::class);
|
||||
$field->getType()->willReturn(Mapping::TYPE_STRING);
|
||||
$field->getIndexField(true)->willReturn('foo.raw');
|
||||
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
@@ -45,8 +46,10 @@ class RawNodeTest extends \PHPUnit_Framework_TestCase
|
||||
public function testQueryBuildOnMultipleFields()
|
||||
{
|
||||
$field_a = $this->prophesize(Field::class);
|
||||
$field_a->getType()->willReturn(Mapping::TYPE_STRING);
|
||||
$field_a->getIndexField(true)->willReturn('foo.raw');
|
||||
$field_b = $this->prophesize(Field::class);
|
||||
$field_b->getType()->willReturn(Mapping::TYPE_STRING);
|
||||
$field_b->getIndexField(true)->willReturn('bar.raw');
|
||||
|
||||
$query_context = $this->prophesize(QueryContext::class);
|
||||
|
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Tests\Phrasea\SearchEngine\Structure;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Typed;
|
||||
|
||||
/**
|
||||
* @group unit
|
||||
* @group structure
|
||||
*/
|
||||
class ValueCheckerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider escapeRawProvider
|
||||
*/
|
||||
public function testValueCompatibility($subject, $value, $compatible)
|
||||
{
|
||||
$this->assertEquals($compatible, ValueChecker::isValueCompatible($subject, $value));
|
||||
}
|
||||
|
||||
public function escapeRawProvider()
|
||||
{
|
||||
$values = [
|
||||
[Mapping::TYPE_FLOAT , 42 , true ],
|
||||
[Mapping::TYPE_FLOAT , '42' , true ],
|
||||
[Mapping::TYPE_FLOAT , '42foo' , false],
|
||||
[Mapping::TYPE_FLOAT , 'foo' , false],
|
||||
[Mapping::TYPE_DOUBLE , 42 , true ],
|
||||
[Mapping::TYPE_DOUBLE , '42' , true ],
|
||||
[Mapping::TYPE_DOUBLE , '42foo' , false],
|
||||
[Mapping::TYPE_DOUBLE , 'foo' , false],
|
||||
[Mapping::TYPE_INTEGER, 42 , true ],
|
||||
[Mapping::TYPE_INTEGER, '42' , true ],
|
||||
[Mapping::TYPE_INTEGER, '42foo' , false],
|
||||
[Mapping::TYPE_INTEGER, 'foo' , false],
|
||||
[Mapping::TYPE_LONG , 42 , true ],
|
||||
[Mapping::TYPE_LONG , '42' , true ],
|
||||
[Mapping::TYPE_LONG , '42foo' , false],
|
||||
[Mapping::TYPE_LONG , 'foo' , false],
|
||||
[Mapping::TYPE_SHORT , 42 , true ],
|
||||
[Mapping::TYPE_SHORT , '42' , true ],
|
||||
[Mapping::TYPE_SHORT , '42foo' , false],
|
||||
[Mapping::TYPE_SHORT , 'foo' , false],
|
||||
[Mapping::TYPE_BYTE , 42 , true ],
|
||||
[Mapping::TYPE_BYTE , '42' , true ],
|
||||
[Mapping::TYPE_BYTE , '42foo' , false],
|
||||
[Mapping::TYPE_BYTE , 'foo' , false],
|
||||
|
||||
[Mapping::TYPE_STRING , 'foo' , true ],
|
||||
[Mapping::TYPE_STRING , '42' , true ],
|
||||
[Mapping::TYPE_STRING , 42 , true ],
|
||||
|
||||
[Mapping::TYPE_BOOLEAN, true , true ],
|
||||
[Mapping::TYPE_BOOLEAN, false , true ],
|
||||
[Mapping::TYPE_BOOLEAN, 'yes' , true ],
|
||||
[Mapping::TYPE_BOOLEAN, 'no' , true ],
|
||||
[Mapping::TYPE_BOOLEAN, 'foo' , true ],
|
||||
[Mapping::TYPE_BOOLEAN, 42 , true ],
|
||||
|
||||
[Mapping::TYPE_DATE , '2015/01/01' , true ],
|
||||
[Mapping::TYPE_DATE , '2015/01/01 00:00:00', false],
|
||||
[Mapping::TYPE_DATE , 'foo' , false],
|
||||
];
|
||||
|
||||
foreach ($values as &$value) {
|
||||
$value[0] = $this->createTypedMock($value[0]);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
private function createTypedMock($type)
|
||||
{
|
||||
$typed = $this->prophesize(Typed::class);
|
||||
$typed->getType()->willReturn($type)->shouldBeCalled();
|
||||
return $typed->reveal();
|
||||
}
|
||||
}
|
@@ -49,21 +49,21 @@ 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|(<field:foo> == <value:"42">)
|
||||
foo = bar|(<field:foo> == <value:"bar">)
|
||||
foo = "bar"|(<field:foo> == <value:"bar">)
|
||||
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">)
|
||||
|
||||
# Field narrowing
|
||||
foo:bar|(<field:foo> MATCHES <text:"bar">)
|
||||
@@ -79,19 +79,19 @@ field.type:foo|(<field:type> MATCHES <text:"foo">)
|
||||
field.id:foo|(<field:id> MATCHES <text:"foo">)
|
||||
|
||||
# Matchers
|
||||
collection:foo|<collection:foo>
|
||||
collection:foo AND bar|(<collection:foo> AND <text:"bar">)
|
||||
collection:foo bar|(<collection:foo> AND <text:"bar">)
|
||||
database:foo|<database:foo>
|
||||
database:foo AND bar|(<database:foo> AND <text:"bar">)
|
||||
database:foo bar|(<database:foo> AND <text:"bar">)
|
||||
type:foo|<media_type:foo>
|
||||
type:foo AND bar|(<media_type:foo> AND <text:"bar">)
|
||||
type:foo bar|(<media_type:foo> AND <text:"bar">)
|
||||
id:90|<record_identifier:90>
|
||||
id:90 AND foo|(<record_identifier:90> AND <text:"foo">)
|
||||
id:90 foo|(<record_identifier:90> AND <text:"foo">)
|
||||
recordid:90|<record_identifier:90>
|
||||
collection:foo|<collection:"foo">
|
||||
collection:foo AND bar|(<collection:"foo"> AND <text:"bar">)
|
||||
collection:foo bar|(<collection:"foo"> AND <text:"bar">)
|
||||
database:foo|<database:"foo">
|
||||
database:foo AND bar|(<database:"foo"> AND <text:"bar">)
|
||||
database:foo bar|(<database:"foo"> AND <text:"bar">)
|
||||
type:foo|<media_type:"foo">
|
||||
type:foo AND bar|(<media_type:"foo"> AND <text:"bar">)
|
||||
type:foo bar|(<media_type:"foo"> AND <text:"bar">)
|
||||
id:90|<record_identifier:"90">
|
||||
id:90 AND foo|(<record_identifier:"90"> AND <text:"foo">)
|
||||
id:90 foo|(<record_identifier:"90"> AND <text:"foo">)
|
||||
recordid:90|<record_identifier:"90">
|
||||
|
||||
# Flag matcher
|
||||
flag.foo:true|<flag:foo set>
|
||||
@@ -102,6 +102,20 @@ flag.true:true|<flag:true set>
|
||||
flag.foo bar:true|(<text:"flag.foo"> AND (<field:bar> MATCHES <text:"true">))
|
||||
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">
|
||||
meta.Duration = 300|(<metadata.Duration> == <value:"300">)
|
||||
|
||||
# Unescaped "." issue on key prefixes
|
||||
fieldOne:foo|(<field:fieldOne> MATCHES <text:"foo">)
|
||||
flagged:true|(<field:flagged> MATCHES <text:"true">)
|
||||
metadata:foo|(<field:metadata> MATCHES <text:"foo">)
|
||||
|
||||
# Matcher on unknown name --> fulltext
|
||||
foo:bar|(<field:foo> MATCHES <text:"bar">)
|
||||
foo:bar AND baz|((<field:foo> MATCHES <text:"bar">) AND <text:"baz">)
|
||||
|
Can't render this file because it contains an unexpected character in line 1 and column 11.
|
Reference in New Issue
Block a user