diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Flag.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Flag.php new file mode 100644 index 0000000000..42862eab62 --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/Flag.php @@ -0,0 +1,21 @@ +name = $name; + } + + public function getName() + { + return $this->name; + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/FlagStatement.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/FlagStatement.php index fd8857632f..8b1866862a 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/FlagStatement.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/AST/FlagStatement.php @@ -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 ] ]; } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php index e427f11e05..e5a0a02008 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php @@ -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']); } } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Exception/StructureException.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Exception/StructureException.php new file mode 100644 index 0000000000..7d713605f9 --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Exception/StructureException.php @@ -0,0 +1,16 @@ +helper->getUniqueRecordId($this->databox_id, $record['record_id']); $record['base_id'] = $this->helper->getUniqueCollectionId($this->databox_id, $record['collection_id']); diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/FlagHydrator.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/FlagHydrator.php new file mode 100644 index 0000000000..135282edbb --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/FlagHydrator.php @@ -0,0 +1,62 @@ +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; + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php index 6650a68a53..f7bbf8e5aa 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php @@ -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; - } } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/RecordHelper.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/RecordHelper.php index 73090c4c11..91dc676b10 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/RecordHelper.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/RecordHelper.php @@ -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); diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryContext.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryContext.php index 5aff671a91..a9362624db 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryContext.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryContext.php @@ -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); } /** diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php index c377b83ba0..8649d2e5b7 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Search/QueryVisitor.php @@ -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)) ); } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Flag.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Flag.php new file mode 100644 index 0000000000..c1eeac0577 --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Flag.php @@ -0,0 +1,54 @@ +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']; + } + } + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php index 4bfc5d97fd..57f9fba751 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/GlobalStructure.php @@ -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. * diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php index db0256e105..aa187102d5 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/LimitedStructure.php @@ -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(); diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php index 7b4acf585f..f4d50be9ce 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Structure.php @@ -24,4 +24,7 @@ interface Structure * @throws \DomainException */ public function isPrivate($name); + + public function getAllFlags(); + public function getFlagByName($name); }