Refactor facets handling

This commit is contained in:
Mathieu Darse
2015-03-17 18:24:18 +01:00
parent e4ee7fc7d7
commit 4419b1b3a7
5 changed files with 144 additions and 100 deletions

View File

@@ -183,9 +183,9 @@ class Query implements ControllerProviderInterface
/** Debug */
$json['parsed_query'] = $result->getQuery();
$json['aggregations'] = $result->getAggregations();
/** End debug */
$json['facets'] = $result->getFacets();
$json['phrasea_props'] = $proposals;
$json['total_answers'] = (int) $result->getAvailable();
$json['next_page'] = ($page < $npages && $result->getAvailable() > 0) ? ($page + 1) : false;

View File

@@ -13,6 +13,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\RecordIndexer;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\TermIndexer;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\FacetsResponse;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
@@ -291,6 +292,8 @@ class ElasticSearchEngine implements SearchEngineInterface
$results[] = ElasticsearchRecordHydrator::hydrate($hit['_source'], $n++);
}
$facets = new FacetsResponse($res);
$query['ast'] = $this->app['query_parser']->parse($string)->dump();
$query['query_main'] = $recordQuery;
$query['query'] = $params['body'];
@@ -298,7 +301,7 @@ class ElasticSearchEngine implements SearchEngineInterface
return new SearchEngineResult($results, json_encode($query), $res['took'], $offset,
$res['hits']['total'], $res['hits']['total'], null, null, $suggestions, [],
$this->indexName, isset($res['aggregations']) ? $res['aggregations'] : []);
$this->indexName, $facets);
}
/**
@@ -419,9 +422,13 @@ class ElasticSearchEngine implements SearchEngineInterface
// filter aggregation to allowed databoxes
// declare aggregation on current field
$agg = array();
// array_values is needed to ensure array serialization
$agg['filter']['terms']['databox_id'] = array_values($databoxes);
$agg['aggs']['distinct_occurrence']['terms']['field'] =
// TODO (mdarse) Remove databox filtering. It's already done by the
// ACL filter in the query scope, so no document that shouldn't be
// displayed can go this far.
// // array_values is needed to ensure array serialization
// $agg['filter']['terms']['databox_id'] = array_values($databoxes);
// $agg['aggs']['distinct_occurrence']['terms']['field'] =
$agg['terms']['field'] =
sprintf('%s.%s.raw', $prefix, $field_name);
$aggs[$field_name] = $agg;

View File

@@ -0,0 +1,67 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\Search;
use Alchemy\Phrasea\Exception\RuntimeException;
use JsonSerializable;
class FacetsResponse implements JsonSerializable
{
private $facets = array();
public function __construct(array $response)
{
if (!isset($response['aggregations'])) {
return;
}
foreach ($response['aggregations'] as $name => $aggregation) {
if (!isset($aggregation['buckets'])) {
$this->throwAggregationResponseError();
}
$values = $this->buildBucketsValues($name, $aggregation['buckets']);
if ($values) {
$this->facets[] = array(
'name' => $name,
'values' => $values,
);
}
}
}
private function buildBucketsValues($name, $buckets)
{
$values = array();
foreach ($buckets as $bucket) {
if (!isset($bucket['key']) || !isset($bucket['doc_count'])) {
$this->throwAggregationResponseError();
}
$values[] = array(
'value' => $bucket['key'],
'count' => $bucket['doc_count'],
'query' => $this->buildQuery($name, $bucket['key']),
);
}
return $values;
}
private function buildQuery($name, $value)
{
// Strip double quotes from values to prevent broken queries
$value = str_replace('/"/u', ' ', $value);
// TODO escape value when escaping is supported in query parser
return ($name === 'Collection') ?
sprintf('collection:"%s"', $value) :
sprintf('"%s" IN %s', $value, $name);
}
private function throwAggregationResponseError()
{
throw new RuntimeException('Invalid aggregation response');
}
public function jsonSerialize()
{
return $this->facets;
}
}

View File

@@ -11,6 +11,7 @@
namespace Alchemy\Phrasea\SearchEngine;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\FacetsResponse;
use Doctrine\Common\Collections\ArrayCollection;
class SearchEngineResult
@@ -26,10 +27,10 @@ class SearchEngineResult
protected $suggestions;
protected $propositions;
protected $indexes;
protected $aggregations;
protected $facets;
public function __construct(ArrayCollection $results, $query, $duration, $offsetStart, $available, $total, $error,
$warning, ArrayCollection $suggestions, $propositions, $indexes, $aggregations = array())
$warning, ArrayCollection $suggestions, $propositions, $indexes, FacetsResponse $facets = null)
{
$this->results = $results;
$this->query = $query;
@@ -42,7 +43,7 @@ class SearchEngineResult
$this->suggestions = $suggestions;
$this->propositions = $propositions;
$this->indexes = $indexes;
$this->aggregations = $aggregations;
$this->facets = $facets;
return $this;
}
@@ -179,8 +180,8 @@ class SearchEngineResult
/**
* @return array
*/
public function getAggregations()
public function getFacets()
{
return $this->aggregations;
return $this->facets;
}
}

View File

@@ -464,100 +464,12 @@ function initAnswerForm() {
success: function (datas) {
// DEBUG QUERY PARSER
var query = datas.parsed_query;
try {
query = JSON.parse(query);
}
catch (e) {}
console.info(query);
console.info(JSON.parse(datas.parsed_query));
var aggs = datas.aggregations;
try {
aggs = JSON.parse(aggs);
}
catch (e) {}
console.debug('Aggregations:');
var toDisplay = [];
_.each(aggs, function(field_aggs, key) {
_.each(field_aggs, function(value) {
_.each(value.buckets, function(bucket, keyBis) {
if (!toDisplay[keyBis]) { toDisplay[keyBis] = {}; }
toDisplay[keyBis][key] = bucket.key + ' ('+ bucket.doc_count + ')';
});
});
});
console.table(toDisplay);
var treeData = [];
_.each(aggs, function(field_aggs, key) {
var entry = {
"title" : key,
"key": key,
"folder": true,
"children" : []
};
if(field_aggs.hasOwnProperty('buckets')){
_.each(field_aggs.buckets, function(bucket) {
entry.children.push({
"title": bucket.key + ' ('+ bucket.doc_count + ')',
"key": bucket.key,
"query": '"'+ bucket.key + '" IN ' + key
});
});
} else {
_.each(field_aggs, function (agg) {
_.each(agg.buckets, function(bucket) {
entry.children.push({
"title": bucket.key + ' ('+ bucket.doc_count + ')',
"key": bucket.key,
"query": '"'+ bucket.key + '" IN ' + key
});
});
});
}
treeData.push(entry);
});
$('#answers').empty().append(datas.results).removeClass('loading');
var $tree = $("#proposals");
if ($tree.data("ui-fancytree")) {
$tree.fancytree("destroy");
}
if (treeData.length > 0) {
$tree.fancytree({
source: treeData,
activate: function(event, data){
var node = data.node;
if (typeof node.data.query === "undefined") {
return;
}
var $input = $('form[name="phrasea_query"] input[name="qry"]');
var current_query = $input.val();
var query = node.data.query;
if (current_query != '') {
query = '('+current_query+') AND ('+query+')';
}
$input.val(query);
checkFilters();
newSearch();
$('searchForm').trigger('submit');
}
});
$tree.fancytree("getRootNode").visit(function(node){
node.setExpanded(true);
});
}
loadFacets(datas.facets);
$("#answers img.lazyload").lazyload({
container: $('#answers')
@@ -600,6 +512,63 @@ function initAnswerForm() {
searchForm.removeClass('triggerAfterInit').trigger('submit');
}
}
function loadFacets(facets) {
// Convert facets data to fancytree source format
var treeSource = _.map(facets, function(facet) {
// Values
var values = _.map(facet.values, function(value) {
return {
title: value.value + ' (' + value.count + ')',
query: value.query
}
});
// Facet
return {
title: facet.name,
folder: true,
children: values,
expanded: true
};
});
return getFacetsTree().reload(treeSource);
}
function getFacetsTree() {
var $facetsTree = $('#proposals');
if (!$facetsTree.data('ui-fancytree')) {
$facetsTree.fancytree({
source: [],
activate: function(event, data){
var query = data.node.data.query;
if (!query) return;
facetCombinedSearch(query);
}
});
}
return $facetsTree.fancytree('getTree');
}
var $searchForm;
var $searchInput;
var $facetsBackButton;
function facetSearch(query) {
var currentQuery = $searchInput.val();
if (currentQuery) {
query = '(' + currentQuery + ') AND (' + query + ')';
}
checkFilters();
newSearch();
$searchInput.val(query);
$searchForm.trigger('submit');
}
$(document).ready(function() {
$searchForm = $('#searchForm');
$searchInput = $searchForm.find('input[name="qry"]');
});
function answerSizer() {
var el = $('#idFrameC').outerWidth();
if (!$.support.cssFloat) {