From e4ec73c58b7965b5048bf2293cfbefcc742224f6 Mon Sep 17 00:00:00 2001 From: Mathieu Darse Date: Tue, 6 Oct 2015 18:41:35 +0200 Subject: [PATCH] =?UTF-8?q?Same=20grammar=20path=20for=20native=20keys=20t?= =?UTF-8?q?han=20=C2=AB=C2=A0IN=C2=A0=C2=BB=20expressions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- grammar/query.pp | 17 ++++++- .../Elastic/AST/AbstractTermNode.php | 1 - .../Elastic/AST/CollectionExpression.php | 34 ------------- .../Elastic/AST/DatabaseExpression.php | 34 ------------- .../Phrasea/SearchEngine/Elastic/AST/Key.php | 50 +++++++++++++++++++ .../Elastic/AST/KeyValueExpression.php | 38 ++++++++++++++ .../AST/RecordIdentifierExpression.php | 34 ------------- .../Elastic/AST/TypeExpression.php | 34 ------------- .../SearchEngine/Elastic/Search/NodeTypes.php | 5 ++ .../Elastic/Search/QueryVisitor.php | 44 +++++++++++++--- .../AST/KeyValueExpressionTest.php | 40 +++++++++++++++ .../SearchEngine/resources/queries.csv | 24 +++++++++ 12 files changed, 210 insertions(+), 145 deletions(-) delete mode 100644 lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/CollectionExpression.php delete mode 100644 lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/DatabaseExpression.php create mode 100644 lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Key.php create mode 100644 lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeyValueExpression.php delete mode 100644 lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/RecordIdentifierExpression.php delete mode 100644 lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/TypeExpression.php create mode 100644 tests/Alchemy/Tests/Phrasea/SearchEngine/AST/KeyValueExpressionTest.php diff --git a/grammar/query.pp b/grammar/query.pp index c68267c356..c5124adfc4 100644 --- a/grammar/query.pp +++ b/grammar/query.pp @@ -31,10 +31,11 @@ %token collection collection %token type type %token id id|recordid +%token field_prefix field. %token flag_prefix flag. %token true true|1 %token false false|0 -%token word [^\s()\[\]:<>≤≥=]+ +%token word [^\s\(\)\[\]:<>≤≥=]+ // relative order of precedence is NOT > XOR > AND > OR @@ -75,7 +76,18 @@ boolean: // Field narrowing quinary: - senary() ( ::space:: ::in:: ::space:: field() #in )? + senary() ( ::space:: ::in:: ::space:: key() #in )? + +key: + native_key() #native_key + | ::field_prefix:: field() + | field() + +native_key: + + | + | + | #field: word_or_keyword()+ @@ -151,6 +163,7 @@ keyword: | | | + | | | | diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/AbstractTermNode.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/AbstractTermNode.php index 1586c78cdd..a822123c97 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/AbstractTermNode.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/AbstractTermNode.php @@ -4,7 +4,6 @@ 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; use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\Concept; use Alchemy\Phrasea\SearchEngine\Elastic\Thesaurus\TermInterface; diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/CollectionExpression.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/CollectionExpression.php deleted file mode 100644 index 5551343b89..0000000000 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/CollectionExpression.php +++ /dev/null @@ -1,34 +0,0 @@ -collectionName = $collectionName; - } - - public function buildQuery(QueryContext $context) - { - return [ - 'term' => [ - 'collection_name' => $this->collectionName - ] - ]; - } - - public function getTermNodes() - { - return array(); - } - - public function __toString() - { - return sprintf('', $this->collectionName); - } -} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/DatabaseExpression.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/DatabaseExpression.php deleted file mode 100644 index d9139a99b6..0000000000 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/DatabaseExpression.php +++ /dev/null @@ -1,34 +0,0 @@ -database = $database; - } - - public function buildQuery(QueryContext $context) - { - return [ - 'term' => [ - 'databox_name' => $this->database - ] - ]; - } - - public function getTermNodes() - { - return array(); - } - - public function __toString() - { - return sprintf('', $this->database); - } -} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Key.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Key.php new file mode 100644 index 0000000000..abc6bf87b3 --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Key.php @@ -0,0 +1,50 @@ +type = $type; + $this->key = $key; + } + + public function getIndexField() + { + return $this->key; + } + + public function __toString() + { + return $this->type; + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeyValueExpression.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeyValueExpression.php new file mode 100644 index 0000000000..9dd90b706a --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeyValueExpression.php @@ -0,0 +1,38 @@ +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); + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/RecordIdentifierExpression.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/RecordIdentifierExpression.php deleted file mode 100644 index ee72df5cc2..0000000000 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/RecordIdentifierExpression.php +++ /dev/null @@ -1,34 +0,0 @@ -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('', $this->record_id); - } -} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/TypeExpression.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/TypeExpression.php deleted file mode 100644 index 94608afc83..0000000000 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/TypeExpression.php +++ /dev/null @@ -1,34 +0,0 @@ -typeName = $typeName; - } - - public function buildQuery(QueryContext $context) - { - return [ - 'term' => [ - 'type' => $this->typeName - ] - ]; - } - - public function getTermNodes() - { - return array(); - } - - public function __toString() - { - return sprintf('', $this->typeName); - } -} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php index 2967bafc70..a44c346cce 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php @@ -23,6 +23,7 @@ class NodeTypes const CONTEXT = '#context'; const FLAG_STATEMENT = '#flag_statement'; const FLAG = '#flag'; + const NATIVE_KEY = '#native_key'; const COLLECTION = '#collection'; const TYPE = '#type'; const DATABASE = '#database'; @@ -31,6 +32,10 @@ class NodeTypes const TOKEN_WORD = 'word'; const TOKEN_QUOTED_STRING = '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_FALSE = 'false'; } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php index c377b83ba0..a4d7c234d9 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php @@ -88,6 +88,9 @@ class QueryVisitor implements Visit case NodeTypes::FLAG: return $this->visitString($element); + case NodeTypes::NATIVE_KEY: + return $this->visitNativeKeyNode($element); + case NodeTypes::DATABASE: return $this->visitDatabaseNode($element); @@ -119,9 +122,18 @@ class QueryVisitor implements Visit if ($element->getChildrenNumber() !== 2) { 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)); - 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) @@ -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) { if ($element->getChildrenNumber() !== 1) { @@ -306,7 +338,7 @@ class QueryVisitor implements Visit } $baseName = $element->getChild(0)->getValue()['value']; - return new AST\DatabaseExpression($baseName); + return new AST\KeyValueExpression(AST\Key::database(), $baseName); } private function visitCollectionNode(Element $element) @@ -316,7 +348,7 @@ class QueryVisitor implements Visit } $collectionName = $element->getChild(0)->getValue()['value']; - return new AST\CollectionExpression($collectionName); + return new AST\KeyValueExpression(AST\Key::collection(), $collectionName); } private function visitTypeNode(Element $element) @@ -326,7 +358,7 @@ class QueryVisitor implements Visit } $typeName = $element->getChild(0)->getValue()['value']; - return new AST\TypeExpression($typeName); + return new AST\KeyValueExpression(AST\Key::mediaType(), $typeName); } private function visitIdentifierNode(Element $element) @@ -336,6 +368,6 @@ class QueryVisitor implements Visit } $identifier = $element->getChild(0)->getValue()['value']; - return new AST\RecordIdentifierExpression($identifier); + return new AST\KeyValueExpression(AST\Key::recordIdentifier(), $identifier); } } diff --git a/tests/Alchemy/Tests/Phrasea/SearchEngine/AST/KeyValueExpressionTest.php b/tests/Alchemy/Tests/Phrasea/SearchEngine/AST/KeyValueExpressionTest.php new file mode 100644 index 0000000000..a60c6e5428 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/SearchEngine/AST/KeyValueExpressionTest.php @@ -0,0 +1,40 @@ +assertTrue(method_exists(KeyValueExpression::class, '__toString'), 'Class does not have method __toString'); + $node = new KeyValueExpression(Key::database(), 'bar'); + $this->assertEquals('', (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); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv b/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv index d1d237df3f..a1205ea877 100644 --- a/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv +++ b/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv @@ -66,6 +66,27 @@ foo bar IN baz|( IN ) foo IN bar baz| fooINbar| +# Native fields with IN syntax (temporary) +foo IN collection| +foo IN collection AND bar|( AND ) +foo IN collection bar| +foo IN database| +foo IN database AND bar|( AND ) +foo IN database bar| +foo IN type| +foo IN type AND bar|( AND ) +foo IN type bar| +90 IN id| +90 IN id AND foo|( AND ) +90 IN id foo| +90 IN recordid| + +# Regular field with name colliding with a native key +foo IN field.collection|( IN ) +foo IN field.database|( IN ) +foo IN field.type|( IN ) +foo IN field.id|( IN ) + # Matchers collection:foo| collection:foo AND bar|( AND ) @@ -73,6 +94,9 @@ collection:foo bar| database:foo| database:foo AND bar|( AND ) database:foo bar| +type:foo| +type:foo AND bar|( AND ) +type:foo bar| id:90| id:90 AND foo|( AND ) id:90 foo|