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['query'] = $query;
|
||||||
|
$json['parsed_query'] = $result->getQuery();
|
||||||
$json['phrasea_props'] = $proposals;
|
$json['phrasea_props'] = $proposals;
|
||||||
$json['total_answers'] = (int) $result->getAvailable();
|
$json['total_answers'] = (int) $result->getAvailable();
|
||||||
$json['next_page'] = ($page < $npages && $result->getAvailable() > 0) ? ($page + 1) : false;
|
$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}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function query($query, $offset, $perPage, SearchEngineOptions $options = null)
|
public function query($string, $offset, $perPage, SearchEngineOptions $options = null)
|
||||||
{
|
{
|
||||||
$query = 'all' !== strtolower($query) ? $query : '';
|
$parser = new QueryParser();
|
||||||
$params = $this->createQueryParams($query, $options ?: new SearchEngineOptions());
|
$ast = $parser->parse($string);
|
||||||
$params['from'] = $offset;
|
$query = $ast->getQuery();
|
||||||
$params['size'] = $perPage;
|
|
||||||
|
|
||||||
$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();
|
$results = new ArrayCollection();
|
||||||
$suggestions = new ArrayCollection();
|
$suggestions = new ArrayCollection();
|
||||||
$n = 0;
|
// $n = 0;
|
||||||
|
|
||||||
foreach ($res['hits']['hits'] as $hit) {
|
// foreach ($res['hits']['hits'] as $hit) {
|
||||||
$databoxId = is_array($hit['fields']['databox_id']) ? array_pop($hit['fields']['databox_id']) : $hit['fields']['databox_id'];
|
// $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'];
|
// $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++);
|
// $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)
|
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 = [];
|
// $search = [];
|
||||||
if ($preg > 0) {
|
// if ($preg > 0) {
|
||||||
$search['bool']['must'][] = [
|
// $search['bool']['must'][] = [
|
||||||
'term' => [
|
// 'term' => [
|
||||||
'record_id' => $matches[2],
|
// 'record_id' => $matches[2],
|
||||||
],
|
// ],
|
||||||
];
|
// ];
|
||||||
$query = '';
|
// $query = '';
|
||||||
}
|
// }
|
||||||
|
|
||||||
if ('' !== $query) {
|
if ('' !== $query) {
|
||||||
if (0 < count($options->getBusinessFieldsOn())) {
|
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) {
|
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');
|
$('#answers').empty().append(datas.results).removeClass('loading');
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user