Skip to content

Commit

Permalink
feature #256 [Elasticsearch] order-by support (dkarlovi)
Browse files Browse the repository at this point in the history
This PR was merged into the 2.0-dev branch.

Discussion
----------

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #241 
| License       | MIT
| Doc PR        | N/A

Based on #255, that must be merged first.

TODO:

- [x] tests for ordering by `has_child` queries
- [x] functionality and tests for API Platform integration (`options`, order from Doctrine must be identical to what Elasticsearch returns)

Commits
-------

2e2b5e7 [Elasticsearch] order-by support
1c567c4 [API Platform] order-by integration for Elasticsearch
  • Loading branch information
sstok authored Jan 11, 2019
2 parents 8fa701e + 1c567c4 commit ef11d4e
Show file tree
Hide file tree
Showing 10 changed files with 585 additions and 81 deletions.
33 changes: 30 additions & 3 deletions lib/ApiPlatform/Elasticsearch/Extension/SearchExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,19 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
foreach ($configuration['mappings'] as $fieldName => $mapping) {
$conditions = [];
if (\is_array($mapping)) {
ArrayKeysValidator::assertOnlyKeys($mapping, ['property', 'conditions'], $configPath.'['.$fieldName.']');
ArrayKeysValidator::assertOnlyKeys($mapping, ['property', 'conditions', 'options'], $configPath.'['.$fieldName.']');
ArrayKeysValidator::assertKeysExists($mapping, ['property'], $configPath.'['.$fieldName.']');

foreach ($mapping['conditions'] as $idx => $conditionMapping) {
$conditionMappings = $mapping['conditions'] ?? [];
foreach ($conditionMappings as $idx => $conditionMapping) {
ArrayKeysValidator::assertOnlyKeys($conditionMapping, ['property', 'value'], $configPath.'['.$fieldName.'][conditions]['.$idx.']');

$conditions[$conditionMapping['property']] = $conditionMapping['value'];
}
$mapping = $mapping['property'];
}

$conditionGenerator->registerField($fieldName, $mapping, $conditions);
$conditionGenerator->registerField($fieldName, $mapping, $conditions, $mapping['options'] ?? []);
}

$normalizer = null;
Expand Down Expand Up @@ -143,6 +145,8 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
->in($rootAlias.'.'.$identifier, ':ids')
)
->setParameter('ids', $ids);

$this->generateOrderByClause($queryBuilder, $rootAlias.'.'.$identifier, $ids);
}

private function getIdentifierNames(string $class): array
Expand All @@ -156,4 +160,27 @@ private function getIdentifierNames(string $class): array

return $this->identifierNames[$class];
}

private function generateOrderByClause(QueryBuilder $queryBuilder, string $identifier, array $ids): void
{
if ([] === $ids) {
return;
}

$clause = ['CASE'];
$last = 0;
foreach ($ids as $idx => $id) {
$alias = sprintf('id%1$s', $idx);
$queryBuilder->setParameter($alias, $id);
$clause[] = sprintf('WHEN %1$s = :%2$s THEN %3$d', $identifier, $alias, $idx);
++$last;
}
$clause[] = sprintf('ELSE %1$d', $last);
$clause[] = 'END';
$clause[] = 'AS HIDDEN order_by';

$queryBuilder
->addSelect(implode(' ', $clause))
->orderBy('order_by', 'ASC');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public function testApplyToCollectionWithValidCondition()
$queryBuilderProphecy->getFirstResult()->shouldBeCalled();
$queryBuilderProphecy->getMaxResults()->shouldBeCalled();
$queryBuilderProphecy->getRootAliases()->willReturn(['o']);
$queryBuilderProphecy->setParameter('id0', 3)->shouldBeCalled();
$queryBuilderProphecy->setParameter('id1', 1)->shouldBeCalled();
$queryBuilderProphecy->setParameter('id2', 5)->shouldBeCalled();
$queryBuilderProphecy->addSelect('CASE WHEN o.id = :id0 THEN 0 WHEN o.id = :id1 THEN 1 WHEN o.id = :id2 THEN 2 ELSE 3 END AS HIDDEN order_by')->willReturn($queryBuilderProphecy);
$queryBuilderProphecy->orderBy('order_by', 'ASC')->shouldBeCalled();
$queryBuilder = $queryBuilderProphecy->reveal();

$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
Expand All @@ -81,8 +86,8 @@ public function testApplyToCollectionWithValidCondition()
$conditionGenerator = $conditionGeneratorProphecy->reveal();

$cachedConditionGeneratorProphecy = $this->prophesize(CachedConditionGenerator::class);
$cachedConditionGeneratorProphecy->registerField('dummy-id', 'id', [])->shouldBeCalled();
$cachedConditionGeneratorProphecy->registerField('dummy-name', 'name', [])->shouldBeCalled();
$cachedConditionGeneratorProphecy->registerField('dummy-id', 'id', [], [])->shouldBeCalled();
$cachedConditionGeneratorProphecy->registerField('dummy-name', 'name', [], [])->shouldBeCalled();
$cachedConditionGeneratorProphecy->getQuery()->willReturn($query);
$cachedConditionGeneratorProphecy->getMappings()->shouldBeCalled();
$cachedConditionGenerator = $cachedConditionGeneratorProphecy->reveal();
Expand Down
4 changes: 2 additions & 2 deletions lib/Elasticsearch/CachedConditionGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ public function __construct(ConditionGenerator $conditionGenerator, Cache $cache
$this->cacheTtl = $ttl;
}

public function registerField(string $fieldName, string $mapping, array $conditions = [])
public function registerField(string $fieldName, string $mapping, array $conditions = [], array $options = [])
{
$this->conditionGenerator->registerField($fieldName, $mapping, $conditions);
$this->conditionGenerator->registerField($fieldName, $mapping, $conditions, $options);

return $this;
}
Expand Down
3 changes: 2 additions & 1 deletion lib/Elasticsearch/ConditionGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ interface ConditionGenerator
* @param string $fieldName Field set name
* @param string $mapping Elasticsearch property mapping
* @param array $conditions Additional conditions to apply if this mapping is used
* @param array $options Additional options to send directly to Elasticsearch
*
* @return static
*/
public function registerField(string $fieldName, string $mapping, array $conditions = []);
public function registerField(string $fieldName, string $mapping, array $conditions = [], array $options = []);

/**
* Return a valid Elastica\Query search query. Query can be sent to a _search endpoint as is.
Expand Down
35 changes: 32 additions & 3 deletions lib/Elasticsearch/FieldMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Rollerworks\Component\Search\Elasticsearch;

use Rollerworks\Component\Search\Field\FieldConfig;
use Rollerworks\Component\Search\Field\OrderField;

/** @internal */
final class FieldMapping implements \Serializable
Expand All @@ -23,10 +24,10 @@ final class FieldMapping implements \Serializable
public $typeName;
public $propertyName;
public $propertyValue;
public $propertyQuery;
public $nested = false;
public $join = false;
public $boost;
public $options; // special options (reserved)

/**
* @var ValueConversion
Expand All @@ -43,10 +44,14 @@ final class FieldMapping implements \Serializable
*/
public $conditions;

public function __construct(string $fieldName, string $property, FieldConfig $fieldConfig, array $conditions = [])
/**
* @var array
*/
public $options;

public function __construct(string $fieldName, string $property, FieldConfig $fieldConfig, array $conditions = [], array $options = [])
{
$this->fieldName = $fieldName;
$this->conditions = $conditions;

$mapping = $this->parseProperty($property);
$this->indexName = $mapping['indexName'];
Expand All @@ -55,6 +60,9 @@ public function __construct(string $fieldName, string $property, FieldConfig $fi
$this->nested = $mapping['nested'];
$this->join = $mapping['join'];

$this->conditions = $this->expandConditions($conditions, $fieldConfig);
$this->options = $options;

$converter = $fieldConfig->getOption('elasticsearch_conversion');

if ($converter instanceof ValueConversion) {
Expand Down Expand Up @@ -125,6 +133,27 @@ private function parseProperty(string $property): array
return compact('indexName', 'typeName', 'propertyName', 'nested', 'join');
}

private function expandConditions(array $conditions, FieldConfig $fieldConfig): array
{
if (OrderField::isOrder($this->fieldName) && $this->join) {
// sorting by has_child query is special
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html#_sorting
$property = $this->indexName.($this->typeName ? '/'.$this->typeName : '').'#'.$this->join['type'].'>';

$scoreQuery = new self('_', $property, $fieldConfig, [], ['score_mode' => 'max']);
$scoreQuery->propertyQuery = [
QueryConditionGenerator::QUERY_FUNCTION_SCORE => [
QueryConditionGenerator::QUERY_SCRIPT_SCORE => [
QueryConditionGenerator::QUERY_SCRIPT => sprintf('%1$s * doc["%2$s"].value', QueryConditionGenerator::SORT_SCORE, $this->propertyName),
],
],
];
$conditions[] = $scoreQuery;
}

return $conditions;
}

public function serialize()
{
return serialize(
Expand Down
Loading

0 comments on commit ef11d4e

Please sign in to comment.