Put flags in ES structure

This commit is contained in:
Mathieu Darse
2015-09-28 20:27:29 +02:00
parent 8b87e94ff2
commit 4424d43b18
14 changed files with 232 additions and 52 deletions

View File

@@ -0,0 +1,21 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Assert\Assertion;
class Flag
{
private $name;
public function __construct($name)
{
Assertion::string($name);
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}

View File

@@ -2,9 +2,11 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Assert\Assertion;
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Flag as FlagStructure;
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
use Assert\Assertion;
class FlagStatement extends Node
{
@@ -21,12 +23,14 @@ class FlagStatement extends Node
public function buildQuery(QueryContext $context)
{
// TODO Ensure flag exists
$key = RecordHelper::normalizeFlagKey($this->name);
$field = sprintf('flags.%s', $key);
$name = FlagStructure::normalizeName($this->name);
$flag = $context->getFlag($name);
if (!$flag) {
throw new QueryException(sprintf('Flag "%s" does not exist', $this->name));
}
return [
'term' => [
$field => $this->set
$flag->getIndexField() => $this->set
]
];
}

View File

@@ -19,6 +19,7 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Search\AggregationHelper;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\FacetsResponse;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryCompiler;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Flag;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\LimitedStructure;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Structure;
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
@@ -566,7 +567,7 @@ class ElasticSearchEngine implements SearchEngineInterface
$databoxId = $databox->get_sbas_id();
$statusStructure = $databox->getStatusStructure();
foreach($statusStructure as $bit => $status) {
$flags[$databoxId][$bit] = RecordHelper::normalizeFlagKey($status['labelon']);
$flags[$databoxId][$bit] = Flag::normalizeName($status['labelon']);
}
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\SearchEngine\Elastic\Exception;
class StructureException extends Exception
{
}

View File

@@ -39,6 +39,7 @@ class CoreHydrator implements HydratorInterface
// Some casting
$record['record_id'] = (int) $record['record_id'];
$record['collection_id'] = (int) $record['collection_id'];
$record['flags_bitfield'] = (int) $record['flags_bitfield'];
// Some identifiers
$record['id'] = $this->helper->getUniqueRecordId($this->databox_id, $record['record_id']);
$record['base_id'] = $this->helper->getUniqueCollectionId($this->databox_id, $record['collection_id']);

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2014 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator;
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\StructureException;
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Flag;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Structure;
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
use databox;
class FlagHydrator implements HydratorInterface
{
public function __construct(Structure $structure, databox $databox)
{
$this->field_names_map = self::buildFieldNamesMap($structure, $databox);
}
private static function buildFieldNamesMap(Structure $structure, databox $databox)
{
$names_map = [];
foreach ($structure->getAllFlags() as $name => $flag) {
$bit = $flag->getBitPositionInDatabox($databox);
if ($bit === null) {
continue;
}
if (isset($names_map[$bit])) {
throw new StructureException(sprintf('Duplicated flag for bit %d', $bit));
}
$names_map[$bit] = $name;
}
return $names_map;
}
public function hydrateRecords(array &$records)
{
foreach ($records as &$record) {
if (isset($record['flags_bitfield'])) {
$record['flags'] = $this->bitfieldToFlagsMap($record['flags_bitfield']);
var_dump($record['flags_bitfield'], $record['flags']);
}
}
}
private function bitfieldToFlagsMap($bitfield)
{
$flags = [];
foreach ($this->field_names_map as $position => $name) {
$flags[$name] = \databox_status::bitIsSet($bitfield, $position);
}
return $flags;
}
}

View File

@@ -20,6 +20,7 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Delegate\RecordListFetch
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Delegate\ScheduledFetcherDelegate;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Fetcher;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator\CoreHydrator;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator\FlagHydrator;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator\MetadataHydrator;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator\SubDefinitionHydrator;
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator\ThesaurusHydrator;
@@ -143,6 +144,7 @@ class RecordIndexer
new CoreHydrator($databox->get_sbas_id(), $databox->get_viewname(), $this->helper),
new TitleHydrator($connection),
new MetadataHydrator($connection, $this->structure, $this->helper),
new FlagHydrator($this->structure, $databox),
new ThesaurusHydrator($this->structure, $this->thesaurus, $candidateTerms),
new SubDefinitionHydrator($connection)
), $delegate);
@@ -174,7 +176,7 @@ class RecordIndexer
$params['id'] = $record['id'];
unset($record['id']);
$params['type'] = self::TYPE_NAME;
$params['body'] = $this->transform($record);
$params['body'] = $record;
$bulk->index($params);
}
}
@@ -307,37 +309,10 @@ class RecordIndexer
private function getFlagsMapping()
{
$mapping = new Mapping();
foreach ($this->appbox->get_databoxes() as $databox) {
foreach ($databox->getStatusStructure() as $bit => $status) {
$key = RecordHelper::normalizeFlagKey($status['labelon']);
// We only add to mapping new statuses
if (!$mapping->has($key)) {
$mapping->add($key, 'boolean');
}
}
foreach ($this->structure->getAllFlags() as $name => $_) {
$mapping->add($name, 'boolean');
}
return $mapping;
}
/**
* Inspired by ESRecordSerializer
*
* @todo complete, with all the other transformations
* @todo convert this function in a HydratorInterface and inject into fetcher
* @param $record
*/
private function transform($record)
{
$databox = $this->appbox->get_databox($record['databox_id']);
foreach ($databox->getStatusStructure() as $bit => $status) {
$key = RecordHelper::normalizeFlagKey($status['labelon']);
$record['flags'][$key] = \databox_status::bitIsSet($record['flags_bitfield'], $bit);
}
return $record;
}
}

View File

@@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic;
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\MergeException;
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Flag;
use appbox;
use DateTime;
use igorw;
@@ -70,11 +71,6 @@ class RecordHelper
return $this->collectionMap;
}
public static function normalizeFlagKey($key)
{
return StringUtils::slugify($key, '_');
}
public static function validateDate($date)
{
$d = DateTime::createFromFormat(Mapping::DATE_FORMAT_CAPTION_PHP, $date);

View File

@@ -6,6 +6,7 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Field as ASTField;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Flag;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Structure;
/**
@@ -73,11 +74,15 @@ class QueryContext
if ($name instanceof ASTField) {
$name = $name->getValue();
}
$field = $this->structure->get($name);
if (!$field) {
return null;
return $this->structure->get($name);
}
public function getFlag($name)
{
if ($name instanceof Flag) {
$name = $name->getName();
}
return $field;
return $this->structure->getFlagByName($name);
}
/**

View File

@@ -86,7 +86,7 @@ class QueryVisitor implements Visit
return $this->visitFlagStatementNode($element);
case NodeTypes::FLAG:
return $this->visitString($element);
return new AST\Flag($this->visitString($element));
case NodeTypes::DATABASE:
return $this->visitDatabaseNode($element);
@@ -275,9 +275,13 @@ class QueryVisitor implements Visit
if ($node->getChildrenNumber() !== 2) {
throw new \Exception('Flag statement can only have 2 childs.');
}
$flag = $node->getChild(0)->accept($this);
if (!$flag instanceof AST\Flag) {
throw new \Exception('Flag statement key must be a flag node.');
}
return new AST\FlagStatement(
$node->getChild(0)->accept($this),
$flag->getName(),
$this->visitBoolean($node->getChild(1))
);
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
use Alchemy\Phrasea\SearchEngine\Elastic\StringUtils;
use Assert\Assertion;
use InvalidArgumentException;
class Flag
{
private $name;
public static function createFromLegacyStatus(array $status)
{
if (!isset($status['labelon'])) {
throw new InvalidArgumentException('Status array must contain the "labelon" key.');
}
return new self(self::normalizeName($status['labelon']));
}
public function __construct($name)
{
Assertion::string($name);
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function getIndexField()
{
return sprintf('flags.%s', $this->name);
}
public static function normalizeName($key)
{
return StringUtils::slugify($key, '_');
}
/*
* TODO: Rewrite to have all data injected at construct time in createFromLegacyStatus()
*/
public function getBitPositionInDatabox(\databox $databox)
{
foreach ($databox->getStatusStructure() as $bit => $status) {
$candidate_name = self::normalizeName($status['labelon']);
if ($candidate_name === $this->name) {
return (int) $status['bit'];
}
}
}
}

View File

@@ -3,6 +3,7 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
use Assert\Assertion;
use DomainException;
final class GlobalStructure implements Structure
@@ -17,6 +18,7 @@ final class GlobalStructure implements Structure
private $private = array();
/** @var Field[] */
private $facets = array();
private $flags = array();
/**
* @param \databox[] $databoxes
@@ -24,17 +26,32 @@ final class GlobalStructure implements Structure
*/
public static function createFromDataboxes(array $databoxes)
{
$structure = new self();
$fields = [];
$flags = [];
foreach ($databoxes as $databox) {
foreach ($databox->get_meta_structure() as $fieldStructure) {
$field = Field::createFromLegacyField($fieldStructure);
$structure->add($field);
$fields[] = Field::createFromLegacyField($fieldStructure);
}
foreach ($databox->getStatusStructure() as $status) {
$flags[] = Flag::createFromLegacyStatus($status);
}
}
return $structure;
return new self($fields, $flags);
}
public function add(Field $field)
public function __construct(array $fields, array $flags)
{
Assertion::allIsInstanceOf($fields, Field::class);
Assertion::allIsInstanceOf($flags, Flag::class);
foreach ($fields as $field) {
$this->add($field);
}
foreach ($flags as $flag) {
$this->flags[$flag->getName()] = $flag;
}
}
private function add(Field $field)
{
$name = $field->getName();
if (isset($this->fields[$name])) {
@@ -120,6 +137,17 @@ final class GlobalStructure implements Structure
throw new DomainException(sprintf('Unknown field "%s".', $name));
}
public function getAllFlags()
{
return $this->flags;
}
public function getFlagByName($name)
{
return isset($this->flags[$name]) ?
$this->flags[$name] : null;
}
/**
* Returns an array of collections indexed by field name.
*

View File

@@ -75,6 +75,16 @@ final class LimitedStructure implements Structure
return $this->structure->isPrivate($name);
}
public function getAllFlags()
{
return $this->structure->getAllFlags();
}
public function getFlagByName($name)
{
return $this->structure->getFlagByName($name);
}
private function limit(array $fields)
{
$allowed_collections = $this->allowedCollections();

View File

@@ -24,4 +24,7 @@ interface Structure
* @throws \DomainException
*/
public function isPrivate($name);
public function getAllFlags();
public function getFlagByName($name);
}