Merge remote-tracking branch 'origin/master' into elastic-indexer

This commit is contained in:
Damien Alexandre
2014-09-22 11:59:11 +02:00
15 changed files with 1524 additions and 23 deletions

4
build_query_parser.sh Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
"chien blanc" ou chat and costume in title

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
abstract class Node
{
abstract public function getQuery();
}

View File

@@ -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));
}
}

View 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);
}
}

View File

@@ -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'
)
)
);
}
}

View 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);
}
}

View File

@@ -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())) {

File diff suppressed because it is too large Load Diff

View File

@@ -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');