diff --git a/grammar/query.pp b/grammar/query.pp index 9c71566e7d..e8ffeda1c5 100644 --- a/grammar/query.pp +++ b/grammar/query.pp @@ -30,6 +30,8 @@ %token collection collection %token type type %token id id|recordid +%token created_on created_(on|at) +%token updated_on updated_(on|at) %token field_prefix field\. %token flag_prefix flag\. %token meta_prefix (?:meta|exif)\. @@ -89,10 +91,15 @@ match_key: | key: - ::meta_prefix:: meta_key() + timestamp_key() + | ::meta_prefix:: meta_key() | ::field_prefix:: field_key() | field_key() +#timestamp_key: + + | + #meta_key: word_or_keyword()+ @@ -164,6 +171,8 @@ keyword: | | | + | + | | | | diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeyValue/TimestampKey.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeyValue/TimestampKey.php new file mode 100644 index 0000000000..834113c40b --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeyValue/TimestampKey.php @@ -0,0 +1,42 @@ +type = $type; + $this->index_field = $index_field; + } + + public function getIndexField(QueryContext $context, $raw = false) + { + return $this->index_field; + } + + public function isValueCompatible($value, QueryContext $context) + { + return true; + } + + public function __toString() + { + return $this->type; + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php index 601dd1cf10..8656543652 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php @@ -302,8 +302,8 @@ class RecordIndexer ->add('type', 'string')->notAnalyzed() ->add('record_type', 'string')->notAnalyzed() // record or story // Dates - ->add('created_on', 'date')->format(Mapping::DATE_FORMAT_MYSQL) - ->add('updated_on', 'date')->format(Mapping::DATE_FORMAT_MYSQL) + ->add('created_on', 'date')->format(Mapping::DATE_FORMAT_MYSQL_OR_CAPTION) + ->add('updated_on', 'date')->format(Mapping::DATE_FORMAT_MYSQL_OR_CAPTION) // Thesaurus ->add('concept_path', $this->getThesaurusPathMapping()) // EXIF diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Mapping.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Mapping.php index 5d0f8443b0..2f44fcc7bd 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Mapping.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Mapping.php @@ -22,6 +22,7 @@ class Mapping const DATE_FORMAT_MYSQL = 'yyyy-MM-dd HH:mm:ss'; const DATE_FORMAT_CAPTION = 'yyyy/MM/dd'; // ES format + const DATE_FORMAT_MYSQL_OR_CAPTION = 'yyyy-MM-dd HH:mm:ss||yyyy/MM/dd'; const DATE_FORMAT_CAPTION_PHP = 'Y/m/d'; // PHP format // Core types diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php index 637d5db5b5..6239f32f83 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/NodeTypes.php @@ -27,6 +27,7 @@ class NodeTypes const FLAG_STATEMENT = '#flag_statement'; const FLAG = '#flag'; const NATIVE_KEY = '#native_key'; + const TIMESTAMP_KEY = '#timestamp_key'; // Token types for leaf nodes const TOKEN_WORD = 'word'; const TOKEN_QUOTED_STRING = 'quoted'; @@ -35,6 +36,8 @@ class NodeTypes const TOKEN_COLLECTION = 'collection'; const TOKEN_MEDIA_TYPE = 'type'; const TOKEN_RECORD_ID = 'id'; + const TOKEN_CREATED_ON = 'created_on'; + const TOKEN_UPDATED_ON = 'updated_on'; 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 2e4df86afc..17955e1589 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php @@ -95,6 +95,9 @@ class QueryVisitor implements Visit case NodeTypes::NATIVE_KEY: return $this->visitNativeKeyNode($element); + case NodeTypes::TIMESTAMP_KEY: + return $this->visitTimestampKeyNode($element); + case NodeTypes::METADATA_KEY: return new AST\KeyValue\MetadataKey($this->visitString($element)); @@ -297,10 +300,10 @@ class QueryVisitor implements Visit }); } - private function visitNativeKeyNode(Element $element) + private function visitNativeKeyNode(TreeNode $node) { - $this->assertChildrenCount($element, 1); - $type = $element->getChild(0)->getValue()['token']; + $this->assertChildrenCount($node, 1); + $type = $node->getChild(0)->getValue()['token']; switch ($type) { case NodeTypes::TOKEN_DATABASE: return AST\KeyValue\NativeKey::database(); @@ -315,6 +318,20 @@ class QueryVisitor implements Visit } } + private function visitTimestampKeyNode(TreeNode $node) + { + $this->assertChildrenCount($node, 1); + $type = $node->getChild(0)->getValue()['token']; + switch ($type) { + case NodeTypes::TOKEN_CREATED_ON: + return AST\KeyValue\TimestampKey::createdOn(); + case NodeTypes::TOKEN_UPDATED_ON: + return AST\KeyValue\TimestampKey::updatedOn(); + default: + throw new InvalidArgumentException(sprintf('Unexpected token type "%s" for timestamp key.', $type)); + } + } + private function assertChildrenCount(TreeNode $node, $count) { if ($node->getChildrenNumber() !== $count) { diff --git a/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv b/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv index 53e39de0d9..44f5ff3b02 100644 --- a/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv +++ b/tests/Alchemy/Tests/Phrasea/SearchEngine/resources/queries.csv @@ -93,6 +93,20 @@ id:90 AND foo|( AND ) id:90 foo|( AND ) recordid:90| +# Timestamps +created_on < "2015/01/01"| +created_on ≤ "2015/01/01"| +created_on = "2015/01/01"|( == ) +created_on ≥ "2015/01/01"| +created_on > "2015/01/01"| +updated_on < "2015/01/01"| +updated_on ≤ "2015/01/01"| +updated_on = "2015/01/01"|( == ) +updated_on ≥ "2015/01/01"| +updated_on > "2015/01/01"| +created_at > "2015/01/01"| +updated_at > "2015/01/01"| + # Flag matcher flag.foo:true| flag.foo:1|