mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-24 10:23:17 +00:00
Refactor facets handling
This commit is contained in:
@@ -183,9 +183,9 @@ class Query implements ControllerProviderInterface
|
|||||||
|
|
||||||
/** Debug */
|
/** Debug */
|
||||||
$json['parsed_query'] = $result->getQuery();
|
$json['parsed_query'] = $result->getQuery();
|
||||||
$json['aggregations'] = $result->getAggregations();
|
|
||||||
/** End debug */
|
/** End debug */
|
||||||
|
|
||||||
|
$json['facets'] = $result->getFacets();
|
||||||
$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;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic;
|
|||||||
|
|
||||||
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\RecordIndexer;
|
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\RecordIndexer;
|
||||||
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\TermIndexer;
|
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\Elastic\Search\QueryContext;
|
||||||
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
|
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
|
||||||
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
|
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
|
||||||
@@ -291,6 +292,8 @@ class ElasticSearchEngine implements SearchEngineInterface
|
|||||||
$results[] = ElasticsearchRecordHydrator::hydrate($hit['_source'], $n++);
|
$results[] = ElasticsearchRecordHydrator::hydrate($hit['_source'], $n++);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$facets = new FacetsResponse($res);
|
||||||
|
|
||||||
$query['ast'] = $this->app['query_parser']->parse($string)->dump();
|
$query['ast'] = $this->app['query_parser']->parse($string)->dump();
|
||||||
$query['query_main'] = $recordQuery;
|
$query['query_main'] = $recordQuery;
|
||||||
$query['query'] = $params['body'];
|
$query['query'] = $params['body'];
|
||||||
@@ -298,7 +301,7 @@ class ElasticSearchEngine implements SearchEngineInterface
|
|||||||
|
|
||||||
return new SearchEngineResult($results, json_encode($query), $res['took'], $offset,
|
return new SearchEngineResult($results, json_encode($query), $res['took'], $offset,
|
||||||
$res['hits']['total'], $res['hits']['total'], null, null, $suggestions, [],
|
$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
|
// filter aggregation to allowed databoxes
|
||||||
// declare aggregation on current field
|
// declare aggregation on current field
|
||||||
$agg = array();
|
$agg = array();
|
||||||
// array_values is needed to ensure array serialization
|
// TODO (mdarse) Remove databox filtering. It's already done by the
|
||||||
$agg['filter']['terms']['databox_id'] = array_values($databoxes);
|
// ACL filter in the query scope, so no document that shouldn't be
|
||||||
$agg['aggs']['distinct_occurrence']['terms']['field'] =
|
// 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);
|
sprintf('%s.%s.raw', $prefix, $field_name);
|
||||||
|
|
||||||
$aggs[$field_name] = $agg;
|
$aggs[$field_name] = $agg;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Alchemy\Phrasea\SearchEngine;
|
namespace Alchemy\Phrasea\SearchEngine;
|
||||||
|
|
||||||
|
use Alchemy\Phrasea\SearchEngine\Elastic\Search\FacetsResponse;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
||||||
class SearchEngineResult
|
class SearchEngineResult
|
||||||
@@ -26,10 +27,10 @@ class SearchEngineResult
|
|||||||
protected $suggestions;
|
protected $suggestions;
|
||||||
protected $propositions;
|
protected $propositions;
|
||||||
protected $indexes;
|
protected $indexes;
|
||||||
protected $aggregations;
|
protected $facets;
|
||||||
|
|
||||||
public function __construct(ArrayCollection $results, $query, $duration, $offsetStart, $available, $total, $error,
|
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->results = $results;
|
||||||
$this->query = $query;
|
$this->query = $query;
|
||||||
@@ -42,7 +43,7 @@ class SearchEngineResult
|
|||||||
$this->suggestions = $suggestions;
|
$this->suggestions = $suggestions;
|
||||||
$this->propositions = $propositions;
|
$this->propositions = $propositions;
|
||||||
$this->indexes = $indexes;
|
$this->indexes = $indexes;
|
||||||
$this->aggregations = $aggregations;
|
$this->facets = $facets;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -179,8 +180,8 @@ class SearchEngineResult
|
|||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getAggregations()
|
public function getFacets()
|
||||||
{
|
{
|
||||||
return $this->aggregations;
|
return $this->facets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -464,100 +464,12 @@ function initAnswerForm() {
|
|||||||
success: function (datas) {
|
success: function (datas) {
|
||||||
|
|
||||||
// DEBUG QUERY PARSER
|
// DEBUG QUERY PARSER
|
||||||
var query = datas.parsed_query;
|
console.info(JSON.parse(datas.parsed_query));
|
||||||
try {
|
|
||||||
query = JSON.parse(query);
|
|
||||||
}
|
|
||||||
catch (e) {}
|
|
||||||
console.info(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');
|
$('#answers').empty().append(datas.results).removeClass('loading');
|
||||||
|
|
||||||
var $tree = $("#proposals");
|
loadFacets(datas.facets);
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#answers img.lazyload").lazyload({
|
$("#answers img.lazyload").lazyload({
|
||||||
container: $('#answers')
|
container: $('#answers')
|
||||||
@@ -600,6 +512,63 @@ function initAnswerForm() {
|
|||||||
searchForm.removeClass('triggerAfterInit').trigger('submit');
|
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() {
|
function answerSizer() {
|
||||||
var el = $('#idFrameC').outerWidth();
|
var el = $('#idFrameC').outerWidth();
|
||||||
if (!$.support.cssFloat) {
|
if (!$.support.cssFloat) {
|
||||||
|
|||||||
Reference in New Issue
Block a user