Same grammar path for native keys than « IN » expressions

- Replace Database/Collection/Type/RecordIdentifier specific AST with a generic KeyValueExpression
- « IN » queries in regular fields are still using InExpression right now
- Add some tests for « type:XXXX » queries
This commit is contained in:
Mathieu Darse
2015-10-06 18:41:35 +02:00
parent b55cb86690
commit e4ec73c58b
12 changed files with 210 additions and 145 deletions

View File

@@ -31,10 +31,11 @@
%token collection collection %token collection collection
%token type type %token type type
%token id id|recordid %token id id|recordid
%token field_prefix field.
%token flag_prefix flag. %token flag_prefix flag.
%token true true|1 %token true true|1
%token false false|0 %token false false|0
%token word [^\s()\[\]:<>≤≥=]+ %token word [^\s\(\)\[\]:<>≤≥=]+
// relative order of precedence is NOT > XOR > AND > OR // relative order of precedence is NOT > XOR > AND > OR
@@ -75,7 +76,18 @@ boolean:
// Field narrowing // Field narrowing
quinary: quinary:
senary() ( ::space:: ::in:: ::space:: field() #in )? senary() ( ::space:: ::in:: ::space:: key() #in )?
key:
native_key() #native_key
| ::field_prefix:: field()
| field()
native_key:
<database>
| <collection>
| <type>
| <id>
#field: #field:
word_or_keyword()+ word_or_keyword()+
@@ -151,6 +163,7 @@ keyword:
| <collection> | <collection>
| <type> | <type>
| <id> | <id>
| <field_prefix>
| <flag_prefix> | <flag_prefix>
| <true> | <true>
| <false> | <false>

View File

@@ -4,7 +4,6 @@ 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;
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Concept; use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Concept;
use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\TermInterface; use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\TermInterface;

View File

@@ -1,34 +0,0 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
class CollectionExpression extends Node
{
private $collectionName;
public function __construct($collectionName)
{
$this->collectionName = $collectionName;
}
public function buildQuery(QueryContext $context)
{
return [
'term' => [
'collection_name' => $this->collectionName
]
];
}
public function getTermNodes()
{
return array();
}
public function __toString()
{
return sprintf('<collection:%s>', $this->collectionName);
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
class DatabaseExpression extends Node
{
private $database;
public function __construct($database)
{
$this->database = $database;
}
public function buildQuery(QueryContext $context)
{
return [
'term' => [
'databox_name' => $this->database
]
];
}
public function getTermNodes()
{
return array();
}
public function __toString()
{
return sprintf('<database:%s>', $this->database);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
class Key
{
const TYPE_DATABASE = 'database';
const TYPE_COLLECTION = 'collection';
const TYPE_MEDIA_TYPE = 'media_type';
const TYPE_RECORD_IDENTIFIER = 'record_identifier';
private $type;
private $key;
public static function database()
{
return new self(self::TYPE_DATABASE, 'databox_name');
}
public static function collection()
{
return new self(self::TYPE_COLLECTION, 'collection_name');
}
public static function mediaType()
{
return new self(self::TYPE_MEDIA_TYPE, 'type');
}
public static function recordIdentifier()
{
return new self(self::TYPE_RECORD_IDENTIFIER, 'record_id');
}
private function __construct($type, $key)
{
$this->type = $type;
$this->key = $key;
}
public function getIndexField()
{
return $this->key;
}
public function __toString()
{
return $this->type;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Assert\Assertion;
class KeyValueExpression extends Node
{
protected $key;
protected $value;
public function __construct(Key $key, $value)
{
Assertion::string($value);
$this->key = $key;
$this->value = $value;
}
public function buildQuery(QueryContext $context)
{
return [
'term' => [
$this->key->getIndexField() => $this->value
]
];
}
public function getTermNodes()
{
return [];
}
public function __toString()
{
return sprintf('<%s:%s>', $this->key, $this->value);
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
class RecordIdentifierExpression extends Node
{
private $record_id;
public function __construct($record_id)
{
$this->record_id = $record_id;
}
public function buildQuery(QueryContext $context)
{
return [
'term' => [
'record_id' => $this->record_id
]
];
}
public function getTermNodes()
{
return array();
}
public function __toString()
{
return sprintf('<record_identifier:%s>', $this->record_id);
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
class TypeExpression extends Node
{
private $typeName;
public function __construct($typeName)
{
$this->typeName = $typeName;
}
public function buildQuery(QueryContext $context)
{
return [
'term' => [
'type' => $this->typeName
]
];
}
public function getTermNodes()
{
return array();
}
public function __toString()
{
return sprintf('<type:%s>', $this->typeName);
}
}

View File

@@ -23,6 +23,7 @@ class NodeTypes
const CONTEXT = '#context'; const CONTEXT = '#context';
const FLAG_STATEMENT = '#flag_statement'; const FLAG_STATEMENT = '#flag_statement';
const FLAG = '#flag'; const FLAG = '#flag';
const NATIVE_KEY = '#native_key';
const COLLECTION = '#collection'; const COLLECTION = '#collection';
const TYPE = '#type'; const TYPE = '#type';
const DATABASE = '#database'; const DATABASE = '#database';
@@ -31,6 +32,10 @@ class NodeTypes
const TOKEN_WORD = 'word'; const TOKEN_WORD = 'word';
const TOKEN_QUOTED_STRING = 'quoted'; const TOKEN_QUOTED_STRING = 'quoted';
const TOKEN_RAW_STRING = 'raw_quoted'; const TOKEN_RAW_STRING = 'raw_quoted';
const TOKEN_DATABASE = 'database';
const TOKEN_COLLECTION = 'collection';
const TOKEN_MEDIA_TYPE = 'type';
const TOKEN_RECORD_ID = 'id';
const TOKEN_TRUE = 'true'; const TOKEN_TRUE = 'true';
const TOKEN_FALSE = 'false'; const TOKEN_FALSE = 'false';
} }

View File

@@ -88,6 +88,9 @@ class QueryVisitor implements Visit
case NodeTypes::FLAG: case NodeTypes::FLAG:
return $this->visitString($element); return $this->visitString($element);
case NodeTypes::NATIVE_KEY:
return $this->visitNativeKeyNode($element);
case NodeTypes::DATABASE: case NodeTypes::DATABASE:
return $this->visitDatabaseNode($element); return $this->visitDatabaseNode($element);
@@ -119,9 +122,18 @@ class QueryVisitor implements Visit
if ($element->getChildrenNumber() !== 2) { if ($element->getChildrenNumber() !== 2) {
throw new \Exception('IN expression can only have 2 childs.'); throw new \Exception('IN expression can only have 2 childs.');
} }
$expression = $element->getChild(0)->accept($this); $expression = $element->getChild(0);
$field = $this->visit($element->getChild(1)); $field = $this->visit($element->getChild(1));
return new AST\InExpression($field, $expression); if ($field instanceof AST\Field) {
return new AST\InExpression($field, $this->visit($expression));
} elseif ($field instanceof AST\Key) {
return new AST\KeyValueExpression(
$field,
$this->visitString($expression)
);
} else {
throw new \Exception(sprintf('Unexpected key node type "%s".', is_object($field) ? get_class($field) : gettype($field)));
}
} }
private function visitAndNode(Element $element) private function visitAndNode(Element $element)
@@ -299,6 +311,26 @@ class QueryVisitor implements Visit
} }
} }
private function visitNativeKeyNode(Element $element)
{
if ($element->getChildrenNumber() !== 1) {
throw new \Exception('Native key node can only have a single child.');
}
$type = $element->getChild(0)->getValue()['token'];
switch ($type) {
case NodeTypes::TOKEN_DATABASE:
return AST\Key::database();
case NodeTypes::TOKEN_COLLECTION:
return AST\Key::collection();
case NodeTypes::TOKEN_MEDIA_TYPE:
return AST\Key::mediaType();
case NodeTypes::TOKEN_RECORD_ID:
return AST\Key::recordIdentifier();
default:
throw new InvalidArgumentException(sprintf('Unexpected token type "%s" for native key.', $type));
}
}
private function visitDatabaseNode(Element $element) private function visitDatabaseNode(Element $element)
{ {
if ($element->getChildrenNumber() !== 1) { if ($element->getChildrenNumber() !== 1) {
@@ -306,7 +338,7 @@ class QueryVisitor implements Visit
} }
$baseName = $element->getChild(0)->getValue()['value']; $baseName = $element->getChild(0)->getValue()['value'];
return new AST\DatabaseExpression($baseName); return new AST\KeyValueExpression(AST\Key::database(), $baseName);
} }
private function visitCollectionNode(Element $element) private function visitCollectionNode(Element $element)
@@ -316,7 +348,7 @@ class QueryVisitor implements Visit
} }
$collectionName = $element->getChild(0)->getValue()['value']; $collectionName = $element->getChild(0)->getValue()['value'];
return new AST\CollectionExpression($collectionName); return new AST\KeyValueExpression(AST\Key::collection(), $collectionName);
} }
private function visitTypeNode(Element $element) private function visitTypeNode(Element $element)
@@ -326,7 +358,7 @@ class QueryVisitor implements Visit
} }
$typeName = $element->getChild(0)->getValue()['value']; $typeName = $element->getChild(0)->getValue()['value'];
return new AST\TypeExpression($typeName); return new AST\KeyValueExpression(AST\Key::mediaType(), $typeName);
} }
private function visitIdentifierNode(Element $element) private function visitIdentifierNode(Element $element)
@@ -336,6 +368,6 @@ class QueryVisitor implements Visit
} }
$identifier = $element->getChild(0)->getValue()['value']; $identifier = $element->getChild(0)->getValue()['value'];
return new AST\RecordIdentifierExpression($identifier); return new AST\KeyValueExpression(AST\Key::recordIdentifier(), $identifier);
} }
} }

View File

@@ -0,0 +1,40 @@
<?php
namespace Alchemy\Tests\Phrasea\SearchEngine\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Key;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\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');
$node = new KeyValueExpression(Key::database(), 'bar');
$this->assertEquals('<database:bar>', (string) $node);
}
public function testQueryBuild()
{
$query_context = $this->prophesize(QueryContext::class);
$key = $this->prophesize(Key::class);
$key->getIndexField()->willReturn('foo');
$node = new KeyValueExpression($key->reveal(), 'bar');
$query = $node->buildQuery($query_context->reveal());
$expected = '{
"term": {
"foo": "bar"
}
}';
$this->assertEquals(json_decode($expected, true), $query);
}
}

View File

@@ -66,6 +66,27 @@ foo bar IN baz|(<text:"foo bar"> IN <field:baz>)
foo IN bar baz|<text:"foo IN bar baz"> foo IN bar baz|<text:"foo IN bar baz">
fooINbar|<text:"fooINbar"> fooINbar|<text:"fooINbar">
# Native fields with IN syntax (temporary)
foo IN collection|<collection:foo>
foo IN collection AND bar|(<collection:foo> AND <text:"bar">)
foo IN collection bar|<text:"foo IN collection bar">
foo IN database|<database:foo>
foo IN database AND bar|(<database:foo> AND <text:"bar">)
foo IN database bar|<text:"foo IN database bar">
foo IN type|<media_type:foo>
foo IN type AND bar|(<media_type:foo> AND <text:"bar">)
foo IN type bar|<text:"foo IN type bar">
90 IN id|<record_identifier:90>
90 IN id AND foo|(<record_identifier:90> AND <text:"foo">)
90 IN id foo|<text:"90 IN id foo">
90 IN recordid|<record_identifier:90>
# Regular field with name colliding with a native key
foo IN field.collection|(<text:"foo"> IN <field:collection>)
foo IN field.database|(<text:"foo"> IN <field:database>)
foo IN field.type|(<text:"foo"> IN <field:type>)
foo IN field.id|(<text:"foo"> IN <field:id>)
# Matchers # Matchers
collection:foo|<collection:foo> collection:foo|<collection:foo>
collection:foo AND bar|(<collection:foo> AND <text:"bar">) collection:foo AND bar|(<collection:foo> AND <text:"bar">)
@@ -73,6 +94,9 @@ collection:foo bar|<text:"collection:foo bar">
database:foo|<database:foo> database:foo|<database:foo>
database:foo AND bar|(<database:foo> AND <text:"bar">) database:foo AND bar|(<database:foo> AND <text:"bar">)
database:foo bar|<text:"database:foo bar"> database:foo bar|<text:"database:foo bar">
type:foo|<media_type:foo>
type:foo AND bar|(<media_type:foo> AND <text:"bar">)
type:foo bar|<text:"type:foo bar">
id:90|<record_identifier:90> id:90|<record_identifier:90>
id:90 AND foo|(<record_identifier:90> AND <text:"foo">) id:90 AND foo|(<record_identifier:90> AND <text:"foo">)
id:90 foo|<text:"id:90 foo"> id:90 foo|<text:"id:90 foo">
Can't render this file because it contains an unexpected character in line 1 and column 11.