mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-23 18:03:17 +00:00
Merge remote-tracking branch 'origin/master' into elastic-indexer
This commit is contained in:
4
build_query_parser.sh
Normal file
4
build_query_parser.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
cd grammar
|
||||
node jison/ports/php/php.js query.jison
|
||||
mv QueryParser.php ../lib/Alchemy/Phrasea/SearchEngine/Elastic/QueryParser.php
|
149
grammar/query.jison
Normal file
149
grammar/query.jison
Normal file
@@ -0,0 +1,149 @@
|
||||
/* description: Parses Phraseanet search queries. */
|
||||
|
||||
/* lexical grammar */
|
||||
%lex
|
||||
|
||||
/* lexical states */
|
||||
%x literal
|
||||
|
||||
/* begin lexing */
|
||||
%%
|
||||
|
||||
\s+ /* skip whitespace */
|
||||
"AND" return 'AND'
|
||||
"and" return 'AND'
|
||||
"et" return 'AND'
|
||||
"OR" return 'OR'
|
||||
"or" return 'OR'
|
||||
"ou" return 'OR'
|
||||
"IN" return 'IN'
|
||||
"in" return 'IN'
|
||||
"dans" return 'IN'
|
||||
"(" return '('
|
||||
")" return ')'
|
||||
"*" return '*'
|
||||
'"' {
|
||||
//js
|
||||
this.begin('literal');
|
||||
//php $this->begin('literal');
|
||||
}
|
||||
<literal>'"' {
|
||||
//js
|
||||
this.popState();
|
||||
//php $this->popState();
|
||||
}
|
||||
<literal>([^"])* return 'LITERAL'
|
||||
\w+ return 'WORD'
|
||||
<<EOF>> return 'EOF'
|
||||
|
||||
/lex
|
||||
|
||||
|
||||
/* operator associations and precedence */
|
||||
|
||||
%left 'WORD'
|
||||
%left 'AND' 'OR'
|
||||
%left 'IN'
|
||||
|
||||
%start query
|
||||
|
||||
|
||||
%% /* language grammar */
|
||||
|
||||
|
||||
query
|
||||
: expressions EOF {
|
||||
//js
|
||||
console.log('[QUERY]', $$);
|
||||
return $$;
|
||||
/*php
|
||||
return $$;
|
||||
*/
|
||||
}
|
||||
;
|
||||
|
||||
expressions
|
||||
: expression expressions {
|
||||
//js
|
||||
$$ = '('+$1+' DEF_OP '+$2+')';
|
||||
console.log('[DEF_OP]', $$);
|
||||
// $$ = sprintf('(%s DEF_OP %s)', $1->text, $2->text);
|
||||
/*php
|
||||
$$ = new AST\AndExpression($1->text, $2->text);
|
||||
*/
|
||||
}
|
||||
| expression
|
||||
;
|
||||
|
||||
expression
|
||||
: expression AND expression {
|
||||
//js
|
||||
$$ = '('+$1+' AND '+$3+')';
|
||||
console.log('[AND]', $$);
|
||||
/*php
|
||||
$$ = new AST\AndExpression($1->text, $3->text);
|
||||
*/
|
||||
}
|
||||
| expression OR expression {
|
||||
//js
|
||||
$$ = '('+$1+' OR '+$3+')';
|
||||
console.log('[OR]', $$);
|
||||
/*php
|
||||
$$ = new AST\OrExpression($1->text, $3->text);
|
||||
*/
|
||||
}
|
||||
| expression IN keyword {
|
||||
//js
|
||||
$$ = '('+$1+' IN '+$3+')';
|
||||
console.log('[IN]', $$);
|
||||
/*php
|
||||
$$ = new AST\InExpression($3->text, $1->text);
|
||||
*/
|
||||
}
|
||||
| '(' expression ')' {
|
||||
//js
|
||||
$$ = $2;
|
||||
//php $$ = $2;
|
||||
}
|
||||
| prefix
|
||||
| text
|
||||
;
|
||||
|
||||
keyword
|
||||
: WORD {
|
||||
//js
|
||||
$$ = '<'+$1+'>';
|
||||
console.log('[FIELD]', $$);
|
||||
//php $$ = new AST\KeywordNode($1->text);
|
||||
}
|
||||
;
|
||||
|
||||
prefix
|
||||
: WORD '*' {
|
||||
//js
|
||||
$$ = $1+'*';
|
||||
console.log('[PREFIX]', $$);
|
||||
//php $$ = new AST\PrefixNode($1->text);
|
||||
}
|
||||
;
|
||||
|
||||
text
|
||||
: WORD {
|
||||
//js
|
||||
$$ = '"'+$1+'"';
|
||||
console.log('[WORD]', $$);
|
||||
//php $$ = new AST\TextNode($1->text);
|
||||
}
|
||||
| LITERAL {
|
||||
//js
|
||||
$$ = '"'+$1+'"';
|
||||
console.log('[LITERAL]', $$);
|
||||
//php $$ = new AST\QuotedTextNode($1->text);
|
||||
}
|
||||
;
|
||||
|
||||
|
||||
//option namespace:Alchemy\Phrasea\SearchEngine\Elastic
|
||||
//option class:QueryParser
|
||||
//option use:AST\Node, AST\TextNode, AST\QuotedTextNode, AST\PrefixNode, AST\KeywordNode, AST\AndExpression, AST\OrExpression, AST\InExpression;
|
||||
//option fileName:QueryParser.php
|
1
grammar/test_query
Normal file
1
grammar/test_query
Normal file
@@ -0,0 +1 @@
|
||||
"chien blanc" ou chat and costume in title
|
@@ -191,6 +191,7 @@ class Query implements ControllerProviderInterface
|
||||
);
|
||||
|
||||
$json['query'] = $query;
|
||||
$json['parsed_query'] = $result->getQuery();
|
||||
$json['phrasea_props'] = $proposals;
|
||||
$json['total_answers'] = (int) $result->getAvailable();
|
||||
$json['next_page'] = ($page < $npages && $result->getAvailable() > 0) ? ($page + 1) : false;
|
||||
|
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
class AndExpression extends Node
|
||||
{
|
||||
protected $members = array();
|
||||
|
||||
public function __construct($left, $right)
|
||||
{
|
||||
$this->members[] = $left;
|
||||
$this->members[] = $right;
|
||||
}
|
||||
|
||||
public function getMembers()
|
||||
{
|
||||
return $this->members;
|
||||
}
|
||||
|
||||
public function getQuery($field = '_all')
|
||||
{
|
||||
$rules = array();
|
||||
foreach ($this->members as $member) {
|
||||
$rules[] = $member->getQuery($field);
|
||||
}
|
||||
|
||||
return array(
|
||||
'bool' => array(
|
||||
'must' => count($rules) > 1 ? $rules : $rules[0]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('(%s)', implode(' AND ', $this->members));
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
class InExpression extends Node
|
||||
{
|
||||
protected $keyword;
|
||||
protected $expression;
|
||||
|
||||
public function __construct(KeywordNode $keyword, $expression)
|
||||
{
|
||||
$this->keyword = $keyword;
|
||||
$this->expression = $expression;
|
||||
}
|
||||
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->expression->getQuery($this->keyword->getValue());
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('(%s IN %s)', $this->expression, $this->keyword);
|
||||
}
|
||||
}
|
28
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeywordNode.php
Normal file
28
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/KeywordNode.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
class KeywordNode extends Node
|
||||
{
|
||||
protected $keyword;
|
||||
|
||||
public function __construct($keyword)
|
||||
{
|
||||
$this->keyword = $keyword;
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return $this->keyword;
|
||||
}
|
||||
|
||||
public function getQuery()
|
||||
{
|
||||
throw new LogicException("A keyword can't be converted to a query.");
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('<%s>', $this->keyword);
|
||||
}
|
||||
}
|
8
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Node.php
Normal file
8
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Node.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
abstract class Node
|
||||
{
|
||||
abstract public function getQuery();
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
class OrExpression extends Node
|
||||
{
|
||||
protected $members = array();
|
||||
|
||||
public function __construct($left, $right)
|
||||
{
|
||||
$this->members[] = $left;
|
||||
$this->members[] = $right;
|
||||
}
|
||||
|
||||
public function getMembers()
|
||||
{
|
||||
return $this->members;
|
||||
}
|
||||
|
||||
public function getQuery($field = '_all')
|
||||
{
|
||||
$rules = array();
|
||||
foreach ($this->members as $member) {
|
||||
$rules[] = $member->getQuery($field);
|
||||
}
|
||||
|
||||
return array(
|
||||
'bool' => array(
|
||||
'should' => count($rules) > 1 ? $rules : $rules[0]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('(%s)', implode(' OR ', $this->members));
|
||||
}
|
||||
}
|
27
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/PrefixNode.php
Normal file
27
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/PrefixNode.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
class PrefixNode extends Node
|
||||
{
|
||||
protected $prefix;
|
||||
|
||||
public function __construct($prefix)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
public function getQuery($field = '_all')
|
||||
{
|
||||
return array(
|
||||
'prefix' => array(
|
||||
$field => $this->prefix
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('prefix("%s")', $this->prefix);
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
class QuotedTextNode extends TextNode
|
||||
{
|
||||
public function getQuery($field = '_all')
|
||||
{
|
||||
return array(
|
||||
'match' => array(
|
||||
$field => array(
|
||||
'query' => $this->text,
|
||||
'operator' => 'and'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
27
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/TextNode.php
Normal file
27
lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/TextNode.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
|
||||
|
||||
class TextNode extends Node
|
||||
{
|
||||
protected $text;
|
||||
|
||||
public function __construct($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
public function getQuery($field = '_all')
|
||||
{
|
||||
return array(
|
||||
'match' => array(
|
||||
$field => $this->text
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return sprintf('"%s"', $this->text);
|
||||
}
|
||||
}
|
@@ -274,26 +274,32 @@ class ElasticSearchEngine implements SearchEngineInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query($query, $offset, $perPage, SearchEngineOptions $options = null)
|
||||
public function query($string, $offset, $perPage, SearchEngineOptions $options = null)
|
||||
{
|
||||
$query = 'all' !== strtolower($query) ? $query : '';
|
||||
$params = $this->createQueryParams($query, $options ?: new SearchEngineOptions());
|
||||
$params['from'] = $offset;
|
||||
$params['size'] = $perPage;
|
||||
$parser = new QueryParser();
|
||||
$ast = $parser->parse($string);
|
||||
$query = $ast->getQuery();
|
||||
|
||||
$res = $this->doExecute('search', $params);
|
||||
// $query = 'all' !== strtolower($query) ? $query : '';
|
||||
// $params = $this->createQueryParams($query, $options ?: new SearchEngineOptions());
|
||||
// $params['from'] = $offset;
|
||||
// $params['size'] = $perPage;
|
||||
|
||||
// $res = $this->doExecute('search', $params);
|
||||
|
||||
$results = new ArrayCollection();
|
||||
$suggestions = new ArrayCollection();
|
||||
$n = 0;
|
||||
// $n = 0;
|
||||
|
||||
foreach ($res['hits']['hits'] as $hit) {
|
||||
$databoxId = is_array($hit['fields']['databox_id']) ? array_pop($hit['fields']['databox_id']) : $hit['fields']['databox_id'];
|
||||
$recordId = is_array($hit['fields']['record_id']) ? array_pop($hit['fields']['record_id']) : $hit['fields']['record_id'];
|
||||
$results[] = new \record_adapter($this->app, $databoxId, $recordId, $n++);
|
||||
}
|
||||
// foreach ($res['hits']['hits'] as $hit) {
|
||||
// $databoxId = is_array($hit['fields']['databox_id']) ? array_pop($hit['fields']['databox_id']) : $hit['fields']['databox_id'];
|
||||
// $recordId = is_array($hit['fields']['record_id']) ? array_pop($hit['fields']['record_id']) : $hit['fields']['record_id'];
|
||||
// $results[] = new \record_adapter($this->app, $databoxId, $recordId, $n++);
|
||||
// }
|
||||
|
||||
return new SearchEngineResult($results, $query, $res['took'], $offset, $res['hits']['total'], $res['hits']['total'], null, null, $suggestions, [], $this->indexName);
|
||||
$query['_ast'] = (string) $ast;
|
||||
|
||||
return new SearchEngineResult($results, json_encode($query), null, null, null, null, null, null, $suggestions, [], $this->indexName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,17 +393,17 @@ class ElasticSearchEngine implements SearchEngineInterface
|
||||
|
||||
private function createESQuery($query, SearchEngineOptions $options)
|
||||
{
|
||||
$preg = preg_match('/\s?(recordid|storyid)\s?=\s?([0-9]+)/i', $query, $matches, 0, 0);
|
||||
// $preg = preg_match('/\s?(recordid|storyid)\s?=\s?([0-9]+)/i', $query, $matches, 0, 0);
|
||||
|
||||
$search = [];
|
||||
if ($preg > 0) {
|
||||
$search['bool']['must'][] = [
|
||||
'term' => [
|
||||
'record_id' => $matches[2],
|
||||
],
|
||||
];
|
||||
$query = '';
|
||||
}
|
||||
// $search = [];
|
||||
// if ($preg > 0) {
|
||||
// $search['bool']['must'][] = [
|
||||
// 'term' => [
|
||||
// 'record_id' => $matches[2],
|
||||
// ],
|
||||
// ];
|
||||
// $query = '';
|
||||
// }
|
||||
|
||||
if ('' !== $query) {
|
||||
if (0 < count($options->getBusinessFieldsOn())) {
|
||||
|
1123
lib/Alchemy/Phrasea/SearchEngine/Elastic/QueryParser.php
Normal file
1123
lib/Alchemy/Phrasea/SearchEngine/Elastic/QueryParser.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -466,6 +466,14 @@ function initAnswerForm() {
|
||||
},
|
||||
success: function (datas) {
|
||||
|
||||
// DEBUG QUERY PARSER
|
||||
var query = datas.parsed_query;
|
||||
try {
|
||||
query = JSON.parse(query);
|
||||
}
|
||||
catch (e) {}
|
||||
console.log('Parsed Query:', query);
|
||||
|
||||
|
||||
$('#answers').empty().append(datas.results).removeClass('loading');
|
||||
|
||||
|
Reference in New Issue
Block a user