Test value compatibility before creating ES query

This commit is contained in:
Mathieu Darse
2015-11-10 16:26:34 +01:00
parent c89d48c6f7
commit 8e5d09bd76
15 changed files with 183 additions and 57 deletions

View File

@@ -24,7 +24,7 @@ class EqualExpression extends Node
public function buildQuery(QueryContext $context) public function buildQuery(QueryContext $context)
{ {
if (!$this->key->isValueCompatible($this->value, $context)) { if (!$this->key->isValueCompatible($this->value, $context)) {
return null; throw new QueryException(sprintf('Value "%s" for metadata tag "%s" is not valid.', $this->value, $this->key));
} }
$query = [ $query = [

View File

@@ -6,6 +6,7 @@ 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;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryPostProcessor; use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryPostProcessor;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
use Assert\Assertion; use Assert\Assertion;
class FieldKey implements Key, QueryPostProcessor class FieldKey implements Key, QueryPostProcessor
@@ -26,7 +27,7 @@ class FieldKey implements Key, QueryPostProcessor
public function isValueCompatible($value, QueryContext $context) public function isValueCompatible($value, QueryContext $context)
{ {
return $this->getField($context)->isValueCompatible($value); return ValueChecker::isValueCompatible($this->getField($context), $value);
} }
public function postProcessQuery($query, QueryContext $context) public function postProcessQuery($query, QueryContext $context)

View File

@@ -3,6 +3,7 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue; namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Node; 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\QueryContext;
use Assert\Assertion; use Assert\Assertion;
@@ -20,6 +21,10 @@ class MatchExpression extends Node
public function buildQuery(QueryContext $context) 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));
}
return [ return [
'match' => [ 'match' => [
$this->key->getIndexField($context) => $this->value $this->key->getIndexField($context) => $this->value

View File

@@ -4,6 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
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\Structure\ValueChecker;
use Assert\Assertion; use Assert\Assertion;
class MetadataKey implements Key class MetadataKey implements Key
@@ -24,7 +25,7 @@ class MetadataKey implements Key
public function isValueCompatible($value, QueryContext $context) public function isValueCompatible($value, QueryContext $context)
{ {
return $this->getTag($context)->isValueCompatible($value); return ValueChecker::isValueCompatible($this->getTag($context), $value);
} }
private function getTag(QueryContext $context) private function getTag(QueryContext $context)

View File

@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
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;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField; use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
class QuotedTextNode extends Node class QuotedTextNode extends Node
{ {
@@ -38,11 +38,11 @@ class QuotedTextNode extends Node
}; };
$unrestricted_fields = $context->getUnrestrictedFields(); $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); $query = $query_builder($unrestricted_fields);
$private_fields = $context->getPrivateFields(); $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) { foreach (QueryHelper::wrapPrivateFieldQueries($private_fields, $query_builder) as $private_field_query) {
$query = QueryHelper::applyBooleanClause($query, 'should', $private_field_query); $query = QueryHelper::applyBooleanClause($query, 'should', $private_field_query);
} }

View File

@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
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;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField; use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
class RawNode extends Node class RawNode extends Node
{ {
@@ -56,11 +56,11 @@ class RawNode extends Node
}; };
$unrestricted_fields = $context->getUnrestrictedFields(); $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); $query = $query_builder($unrestricted_fields);
$private_fields = $context->getPrivateFields(); $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) { foreach (QueryHelper::wrapPrivateFieldQueries($private_fields, $query_builder) as $private_field_query) {
$query = QueryHelper::applyBooleanClause($query, 'should', $private_field_query); $query = QueryHelper::applyBooleanClause($query, 'should', $private_field_query);
} }

View File

@@ -4,7 +4,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
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;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField; use Alchemy\Phrasea\SearchEngine\Elastic\Structure\ValueChecker;
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Term; use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Term;
class TextNode extends AbstractTermNode implements ContextAbleInterface class TextNode extends AbstractTermNode implements ContextAbleInterface
@@ -41,7 +41,7 @@ class TextNode extends AbstractTermNode implements ContextAbleInterface
$query_builder = function (array $fields) use ($context) { $query_builder = function (array $fields) use ($context) {
// Full text // Full text
$index_fields = []; $index_fields = [];
foreach (StructureField::filterByValueCompatibility($fields, $this->text) as $field) { foreach (ValueChecker::filterByValueCompatibility($fields, $this->text) as $field) {
foreach ($context->localizeField($field) as $f) { foreach ($context->localizeField($field) as $f) {
$index_fields[] = $f; $index_fields[] = $f;
} }

View File

@@ -4,7 +4,6 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\MergeException; use Alchemy\Phrasea\SearchEngine\Elastic\Exception\MergeException;
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping; 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\Concept;
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Helper as ThesaurusHelper; use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Helper as ThesaurusHelper;
use Assert\Assertion; use Assert\Assertion;
@@ -13,7 +12,7 @@ use databox_field;
/** /**
* @todo Field labels * @todo Field labels
*/ */
class Field class Field implements Typed
{ {
private $name; private $name;
private $type; 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() public function getConceptPathIndexField()
{ {
return sprintf('concept_path.%s', $this->name); return sprintf('concept_path.%s', $this->name);

View File

@@ -5,7 +5,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping; use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
use Assert\Assertion; use Assert\Assertion;
class Tag class Tag implements Typed
{ {
private $name; private $name;
private $type; private $type;
@@ -36,11 +36,6 @@ class Tag
return $this->analyzable; return $this->analyzable;
} }
public function isValueCompatible()
{
return true;
}
public function getIndexField($raw = false) public function getIndexField($raw = false)
{ {
return sprintf( return sprintf(

View 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();
}

View File

@@ -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;
}
}

View File

@@ -12,7 +12,7 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
* @group searchengine * @group searchengine
* @group ast * @group ast
*/ */
class FieldEqualsExpressionTest extends \PHPUnit_Framework_TestCase class EqualExpressionTest extends \PHPUnit_Framework_TestCase
{ {
public function testSerialization() public function testSerialization()
{ {
@@ -33,6 +33,7 @@ class FieldEqualsExpressionTest extends \PHPUnit_Framework_TestCase
$key = $this->prophesize(Key::class); $key = $this->prophesize(Key::class);
$key->isValueCompatible($value, $query_context)->willReturn($compatible_value); $key->isValueCompatible($value, $query_context)->willReturn($compatible_value);
$key->getIndexField($query_context, true)->willReturn($index_field); $key->getIndexField($query_context, true)->willReturn($index_field);
$key->__toString()->willReturn('foo');
// TODO Test keys implementing QueryPostProcessor // TODO Test keys implementing QueryPostProcessor
$node = new EqualExpression($key->reveal(), 'bar'); $node = new EqualExpression($key->reveal(), 'bar');
@@ -56,8 +57,22 @@ class FieldEqualsExpressionTest extends \PHPUnit_Framework_TestCase
['foo.raw', 'bar', true, false, '{ ['foo.raw', 'bar', true, false, '{
"term": { "term": {
"foo.raw": "bar" } }'], "foo.raw": "bar" } }'],
['foo.raw', 'bar', false, true, 'null'],
['foo.raw', 'bar', false, false, 'null'],
]; ];
} }
/**
* @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);
}
} }

View File

@@ -28,6 +28,7 @@ class MatchExpressionTest extends \PHPUnit_Framework_TestCase
{ {
$query_context = $this->prophesize(QueryContext::class)->reveal(); $query_context = $this->prophesize(QueryContext::class)->reveal();
$key = $this->prophesize(Key::class); $key = $this->prophesize(Key::class);
$key->isValueCompatible('bar', $query_context)->willReturn(true);
$key->getIndexField($query_context)->willReturn('foo'); $key->getIndexField($query_context)->willReturn('foo');
$node = new MatchExpression($key->reveal(), 'bar'); $node = new MatchExpression($key->reveal(), 'bar');
$query = $node->buildQuery($query_context); $query = $node->buildQuery($query_context);

View File

@@ -24,6 +24,7 @@ class RawNodeTest extends \PHPUnit_Framework_TestCase
public function testQueryBuildOnSingleField() public function testQueryBuildOnSingleField()
{ {
$field = $this->prophesize(Field::class); $field = $this->prophesize(Field::class);
$field->getType()->willReturn(Mapping::TYPE_STRING);
$field->getIndexField(true)->willReturn('foo.raw'); $field->getIndexField(true)->willReturn('foo.raw');
$query_context = $this->prophesize(QueryContext::class); $query_context = $this->prophesize(QueryContext::class);
@@ -45,8 +46,10 @@ class RawNodeTest extends \PHPUnit_Framework_TestCase
public function testQueryBuildOnMultipleFields() public function testQueryBuildOnMultipleFields()
{ {
$field_a = $this->prophesize(Field::class); $field_a = $this->prophesize(Field::class);
$field_a->getType()->willReturn(Mapping::TYPE_STRING);
$field_a->getIndexField(true)->willReturn('foo.raw'); $field_a->getIndexField(true)->willReturn('foo.raw');
$field_b = $this->prophesize(Field::class); $field_b = $this->prophesize(Field::class);
$field_b->getType()->willReturn(Mapping::TYPE_STRING);
$field_b->getIndexField(true)->willReturn('bar.raw'); $field_b->getIndexField(true)->willReturn('bar.raw');
$query_context = $this->prophesize(QueryContext::class); $query_context = $this->prophesize(QueryContext::class);

View File

@@ -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();
}
}