PHRAS-2879_facet-order_4.1

save all facet settings in conf, order is preserved in admin/search-engine-settings.
todo : ux to re-order facets
todo : return facets into query results in this setting order.
todo : migration from dbox settings (field struct) to conf ; remove field setting from admin
This commit is contained in:
Jean-Yves Gaulier
2020-01-22 12:26:34 +01:00
parent 579ba471c4
commit 2ccdcfed27
3 changed files with 135 additions and 85 deletions

View File

@@ -13,6 +13,7 @@ namespace Alchemy\Phrasea\Controller\Admin;
use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchSettingsFormType; use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchSettingsFormType;
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchOptions; use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchOptions;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\GlobalStructure;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -85,7 +86,10 @@ class SearchEngineController extends Controller
*/ */
private function getConfigurationForm(ElasticsearchOptions $options) private function getConfigurationForm(ElasticsearchOptions $options)
{ {
return $this->app->form(new ElasticsearchSettingsFormType(), $options, [ /** @var GlobalStructure $g */
$g = $this->app['search_engine.structure'];
return $this->app->form(new ElasticsearchSettingsFormType($g, $options), $options, [
'action' => $this->app->url('admin_searchengine_form'), 'action' => $this->app->url('admin_searchengine_form'),
]); ]);
} }

View File

@@ -9,6 +9,9 @@
*/ */
namespace Alchemy\Phrasea\SearchEngine\Elastic; namespace Alchemy\Phrasea\SearchEngine\Elastic;
use igorw;
class ElasticsearchOptions class ElasticsearchOptions
{ {
const POPULATE_ORDER_RID = "RECORD_ID"; const POPULATE_ORDER_RID = "RECORD_ID";
@@ -58,13 +61,8 @@ class ElasticsearchOptions
'populate_direction' => self::POPULATE_DIRECTION_DESC, 'populate_direction' => self::POPULATE_DIRECTION_DESC,
'activeTab' => null, 'activeTab' => null,
]; ];
foreach(self::getAggregableTechnicalFields() as $k => $f) {
$defaultOptions[$k.'_limit'] = 0;
}
$options = array_replace($defaultOptions, $options); $options = array_replace($defaultOptions, $options);
$self = new self(); $self = new self();
$self->setHost($options['host']); $self->setHost($options['host']);
$self->setPort($options['port']); $self->setPort($options['port']);
@@ -76,11 +74,10 @@ class ElasticsearchOptions
$self->setPopulateOrder($options['populate_order']); $self->setPopulateOrder($options['populate_order']);
$self->setPopulateDirection($options['populate_direction']); $self->setPopulateDirection($options['populate_direction']);
$self->setActiveTab($options['activeTab']); $self->setActiveTab($options['activeTab']);
foreach(self::getAggregableTechnicalFields() as $k => $f) { foreach($options['aggregates'] as $fieldname=>$attributes) {
$self->setAggregableFieldLimit($k, $options[$k.'_limit']); $self->setAggregableField($fieldname, $attributes);
} }
return $self; return $self;
} }
@@ -99,10 +96,11 @@ class ElasticsearchOptions
'highlight' => $this->highlight, 'highlight' => $this->highlight,
'populate_order' => $this->populateOrder, 'populate_order' => $this->populateOrder,
'populate_direction' => $this->populateDirection, 'populate_direction' => $this->populateDirection,
'activeTab' => $this->activeTab 'activeTab' => $this->activeTab,
'aggregates' => []
]; ];
foreach(self::getAggregableTechnicalFields() as $k => $f) { foreach($this->_customValues['aggregates'] as $fieldname=>$attributes) {
$ret[$k.'_limit'] = $this->getAggregableFieldLimit($k); $ret['aggregates'][$fieldname] = $attributes;
} }
return $ret; return $ret;
@@ -220,14 +218,19 @@ class ElasticsearchOptions
$this->highlight = $highlight; $this->highlight = $highlight;
} }
public function setAggregableFieldLimit($key, $value) public function setAggregableField($key, $attributes)
{ {
$this->_customValues[$key.'_limit'] = $value; $this->_customValues['aggregates'][$key] = $attributes;
} }
public function getAggregableFieldLimit($key) public function getAggregableField($key)
{ {
return $this->_customValues[$key.'_limit']; return $this->_customValues['aggregates'][$key];
}
public function getAggregableFields()
{
return $this->_customValues['aggregates'];
} }
public function getActiveTab() public function getActiveTab()
@@ -241,56 +244,56 @@ class ElasticsearchOptions
public function __get($key) public function __get($key)
{ {
if(!array_key_exists($key, $this->_customValues)) { $keys = explode(':', $key);
$this->_customValues[$key] = 0;
} return igorw\get_in($this->_customValues, $keys);
return $this->_customValues[$key];
} }
public function __set($key, $value) public function __set($key, $value)
{ {
$this->_customValues[$key] = $value; $keys = explode(':', $key);
$this->_customValues = igorw\assoc_in($this->_customValues, $keys, $value);
} }
public static function getAggregableTechnicalFields() public static function getAggregableTechnicalFields()
{ {
return [ return [
'base_aggregate' => [ 'base' => [
'type' => 'string', 'type' => 'string',
'label' => 'prod::facet:base_label', 'label' => 'prod::facet:base_label',
'field' => "database", 'field' => "database",
'esfield' => 'databox_name', 'esfield' => 'databox_name',
'query' => 'database:%s', 'query' => 'database:%s',
], ],
'collection_aggregate' => [ 'collection' => [
'type' => 'string', 'type' => 'string',
'label' => 'prod::facet:collection_label', 'label' => 'prod::facet:collection_label',
'field' => "collection", 'field' => "collection",
'esfield' => 'collection_name', 'esfield' => 'collection_name',
'query' => 'collection:%s', 'query' => 'collection:%s',
], ],
'doctype_aggregate' => [ 'doctype' => [
'type' => 'string', 'type' => 'string',
'label' => 'prod::facet:doctype_label', 'label' => 'prod::facet:doctype_label',
'field' => "type", 'field' => "type",
'esfield' => 'type', 'esfield' => 'type',
'query' => 'type:%s', 'query' => 'type:%s',
], ],
'camera_model_aggregate' => [ 'camera_model' => [
'type' => 'string', 'type' => 'string',
'label' => 'Camera Model', 'label' => 'Camera Model',
'field' => "meta.CameraModel", 'field' => "meta.CameraModel",
'esfield' => 'metadata_tags.CameraModel', 'esfield' => 'metadata_tags.CameraModel',
'query' => 'meta.CameraModel:%s', 'query' => 'meta.CameraModel:%s',
], ],
'iso_aggregate' => [ 'iso' => [
'type' => 'number', 'type' => 'number',
'label' => 'ISO', 'label' => 'ISO',
'field' => "meta.ISO", 'field' => "meta.ISO",
'esfield' => 'metadata_tags.ISO', 'esfield' => 'metadata_tags.ISO',
'query' => 'meta.ISO=%s', 'query' => 'meta.ISO=%s',
], ],
'aperture_aggregate' => [ 'aperture' => [
'type' => 'number', 'type' => 'number',
'label' => 'Aperture', 'label' => 'Aperture',
'field' => "meta.Aperture", 'field' => "meta.Aperture",
@@ -300,7 +303,7 @@ class ElasticsearchOptions
return round($value, 1); return round($value, 1);
}, },
], ],
'shutterspeed_aggregate' => [ 'shutterspeed' => [
'type' => 'number', 'type' => 'number',
'label' => 'Shutter speed', 'label' => 'Shutter speed',
'field' => "meta.ShutterSpeed", 'field' => "meta.ShutterSpeed",
@@ -313,7 +316,7 @@ class ElasticsearchOptions
return $value . ' s.'; return $value . ' s.';
}, },
], ],
'flashfired_aggregate' => [ 'flashfired' => [
'type' => 'boolean', 'type' => 'boolean',
'label' => 'FlashFired', 'label' => 'FlashFired',
'field' => "meta.FlashFired", 'field' => "meta.FlashFired",
@@ -327,49 +330,49 @@ class ElasticsearchOptions
return array_key_exists($value, $map) ? $map[$value] : $value; return array_key_exists($value, $map) ? $map[$value] : $value;
}, },
], ],
'framerate_aggregate' => [ 'framerate' => [
'type' => 'number', 'type' => 'number',
'label' => 'FrameRate', 'label' => 'FrameRate',
'field' => "meta.FrameRate", 'field' => "meta.FrameRate",
'esfield' => 'metadata_tags.FrameRate', 'esfield' => 'metadata_tags.FrameRate',
'query' => 'meta.FrameRate=%s', 'query' => 'meta.FrameRate=%s',
], ],
'audiosamplerate_aggregate' => [ 'audiosamplerate' => [
'type' => 'number', 'type' => 'number',
'label' => 'Audio Samplerate', 'label' => 'Audio Samplerate',
'field' => "meta.AudioSamplerate", 'field' => "meta.AudioSamplerate",
'esfield' => 'metadata_tags.AudioSamplerate', 'esfield' => 'metadata_tags.AudioSamplerate',
'query' => 'meta.AudioSamplerate=%s', 'query' => 'meta.AudioSamplerate=%s',
], ],
'videocodec_aggregate' => [ 'videocodec' => [
'type' => 'string', 'type' => 'string',
'label' => 'Video codec', 'label' => 'Video codec',
'field' => "meta.VideoCodec", 'field' => "meta.VideoCodec",
'esfield' => 'metadata_tags.VideoCodec', 'esfield' => 'metadata_tags.VideoCodec',
'query' => 'meta.VideoCodec:%s', 'query' => 'meta.VideoCodec:%s',
], ],
'audiocodec_aggregate' => [ 'audiocodec' => [
'type' => 'string', 'type' => 'string',
'label' => 'Audio codec', 'label' => 'Audio codec',
'field' => "meta.AudioCodec", 'field' => "meta.AudioCodec",
'esfield' => 'metadata_tags.AudioCodec', 'esfield' => 'metadata_tags.AudioCodec',
'query' => 'meta.AudioCodec:%s', 'query' => 'meta.AudioCodec:%s',
], ],
'orientation_aggregate' => [ 'orientation' => [
'type' => 'string', 'type' => 'string',
'label' => 'Orientation', 'label' => 'Orientation',
'field' => "meta.Orientation", 'field' => "meta.Orientation",
'esfield' => 'metadata_tags.Orientation', 'esfield' => 'metadata_tags.Orientation',
'query' => 'meta.Orientation=%s', 'query' => 'meta.Orientation=%s',
], ],
'colorspace_aggregate' => [ 'colorspace' => [
'type' => 'string', 'type' => 'string',
'label' => 'Colorspace', 'label' => 'Colorspace',
'field' => "meta.ColorSpace", 'field' => "meta.ColorSpace",
'esfield' => 'metadata_tags.ColorSpace', 'esfield' => 'metadata_tags.ColorSpace',
'query' => 'meta.ColorSpace:%s', 'query' => 'meta.ColorSpace:%s',
], ],
'mimetype_aggregate' => [ 'mimetype' => [
'type' => 'string', 'type' => 'string',
'label' => 'MimeType', 'label' => 'MimeType',
'field' => "meta.MimeType", 'field' => "meta.MimeType",

View File

@@ -9,6 +9,7 @@
*/ */
namespace Alchemy\Phrasea\SearchEngine\Elastic; namespace Alchemy\Phrasea\SearchEngine\Elastic;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\GlobalStructure;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
@@ -17,6 +18,18 @@ use Symfony\Component\Validator\Constraints\Range;
class ElasticsearchSettingsFormType extends AbstractType class ElasticsearchSettingsFormType extends AbstractType
{ {
/** @var GlobalStructure */
private $globalStructure;
/** @var ElasticsearchOptions */
private $esSettings;
public function __construct(GlobalStructure $g, ElasticsearchOptions $settings)
{
$this->globalStructure = $g;
$this->esSettings = $settings;
}
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$builder $builder
@@ -56,43 +69,14 @@ class ElasticsearchSettingsFormType extends AbstractType
->add('minScore', 'integer', [ ->add('minScore', 'integer', [
'label' => 'Thesaurus Min score', 'label' => 'Thesaurus Min score',
'constraints' => new Range(['min' => 0]), 'constraints' => new Range(['min' => 0]),
]); ])
foreach(ElasticsearchOptions::getAggregableTechnicalFields() as $k => $f) {
if(array_key_exists('choices', $f)) {
// choices[] : choice_key => choice_value
$choices = $f['choices'];
}
else {
$choices = [
"10 values" => 10,
"20 values" => 20,
"50 values" => 50,
"100 values" => 100,
"all values" => -1
];
}
// array_unshift($choices, "not aggregated"); // always as first choice
$choices = array_merge(["not aggregated" => 0], $choices);
$builder
->add($k.'_limit', ChoiceType::class, [
// 'label' => $f['label'],// . ' ' . 'aggregate limit',
'choices_as_values' => true,
'choices' => $choices,
'attr' => [
'class' => 'aggregate'
]
]);
}
$builder
->add('highlight', 'checkbox', [ ->add('highlight', 'checkbox', [
'label' => 'Activate highlight', 'label' => 'Activate highlight',
'required' => false 'required' => false
]) ])
// ->add('save', 'submit', [ // ->add('save', 'submit', [
// 'attr' => ['class' => 'btn btn-primary'] // 'attr' => ['class' => 'btn btn-primary']
// ]) // ])
->add('esSettingFromIndex', 'button', [ ->add('esSettingFromIndex', 'button', [
'label' => 'Get setting form index', 'label' => 'Get setting form index',
'attr' => [ 'attr' => [
@@ -108,7 +92,66 @@ class ElasticsearchSettingsFormType extends AbstractType
]) ])
->add('activeTab', 'hidden'); ->add('activeTab', 'hidden');
; // keep aggregates in configuration order with this intermediate array
$aggs = [];
// helper fct to add aggregate to a tmp list
$addAgg = function($k, $label, $help, $disabled=false, $choices=null) use (&$aggs) {
if(!$choices) {
$choices = [
"10 values" => 10,
"50 values" => 50,
"100 values" => 100,
"all values" => -1
];
}
$choices = array_merge(["not aggregated" => 0], $choices); // add this option always as first choice
$aggs[$k] = [ // default value will be replaced by hardcoded tech fields & all databoxes fields
'label' => $label,
'choices_as_values' => true,
'choices' => $choices,
'attr' => [
'class' => 'aggregate'
],
'disabled' => $disabled,
'help_message' => $help // todo : not displayed ?
];
};
// all fields fron conf
foreach($this->esSettings->getAggregableFields() as $k=>$f) {
// default value will be replaced by hardcoded tech fields & all databoxes fields
$addAgg($k, "/?\\ " . $k, "This field does not exists in current databoxes.", true);
}
// add or replace hardcoded tech fields
foreach(ElasticsearchOptions::getAggregableTechnicalFields() as $k => $f) {
$choices = array_key_exists('choices', $f) ? $f['choices'] : null; // a tech-field can publish it's own choices
$help = null;
$label = '#' . $k;
if(!array_key_exists('_'.$k, $aggs)) {
$label = "/!\\ " . $label;
$help = "New field, please confirm setting.";
}
$addAgg('_'.$k, $label, $help, false, $choices);
}
// add or replace all databoxes fields (nb: new db field - unknown in conf - will be a the end)
foreach($this->globalStructure->getAllFields() as $field) {
$k = $label = $field->getName();
$help = null;
if(!array_key_exists($field->getName(), $aggs)) {
$label = "/!\\ " . $label;
$help = "New field, please confirm setting.";
}
$addAgg($k, $label, $help); // default choices
}
// populate aggs to form
foreach($aggs as $k=>$agg) {
$builder->add('aggregates:' . $k . ':limit', ChoiceType::class, $agg);
}
} }
public function getName() public function getName()