mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-24 10:23:17 +00:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Key.php
Normal file
50
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Key.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
Reference in New Issue
Block a user