Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sort operator to $search stage #2554

Merged
merged 5 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,10 @@ public function sortByCount(string $expression): Stage\SortByCount
*
* @param array<string, int|string>|string $fieldName Field name or array of field/order pairs
* @param int|string $order Field order (if one field is specified)
*
* @return Stage\Sort
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with native return type?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sort method for a Search stage does not return a sort stage, but handles this internally. We've had this with other stages (e.g. GeoNear::limit), but I believe I can still use a native type of Stage and refine it using the @return annotation.

*/
public function sort($fieldName, $order = null): Stage\Sort
public function sort($fieldName, $order = null)
{
return $this->builder->sort($fieldName, $order);
}
Expand Down
36 changes: 36 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperators;
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperatorsTrait;

use function in_array;
use function is_array;
use function is_string;
use function strtolower;

/**
* @psalm-type CountType = 'lowerBound'|'total'
* @psalm-type SortMetaKeywords = 'searchScore'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks odd, name suggests there can be multiple keywords yet this is a string. Is this coming from MongoDB itself?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the same strategy we use in the Sort stage itself, where there are multiple meta sort options. In the $search aggregation pipeline stage, the only allowed meta sort is on the search score, hence the single keyword here. I can also change the naming to SortMetaKeywordType or something similar.

* @psalm-type SortMeta = array{'$meta': SortMetaKeywords}
* @psalm-type SearchStageExpression = array{
* '$search': object{
* index?: string,
Expand All @@ -25,6 +32,7 @@
* maxNumPassages?: int,
* },
* returnStoredSource?: bool,
* sort?: object,
* autocomplete?: object,
* compound?: object,
* embeddedDocument?: object,
Expand Down Expand Up @@ -53,6 +61,9 @@ class Search extends Stage implements SupportsAllSearchOperators
private ?bool $returnStoredSource = null;
private ?SearchOperator $operator = null;

/** @var array<string, -1|1|SortMeta> */
private array $sort = [];

public function __construct(Builder $builder)
{
parent::__construct($builder);
Expand All @@ -79,6 +90,10 @@ public function getExpression(): array
$params->returnStoredSource = $this->returnStoredSource;
}

if ($this->sort) {
$params->sort = (object) $this->sort;
}

if ($this->operator !== null) {
$operatorName = $this->operator->getOperatorName();
$params->$operatorName = $this->operator->getOperatorParams();
Expand Down Expand Up @@ -128,6 +143,27 @@ public function returnStoredSource(bool $returnStoredSource = true): static
return $this;
}

public function sort($fieldName, $order = null): static
Copy link
Member

@malarzm malarzm Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add PHPDoc, especially given multiple meanings of $fieldName. Or maybe there's a way to get rid of this overloading? Maybe we should encourage multiple calls to sort instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all grew out of the query builder, which allows for precisely that:

$builder
    ->sort('foo', 1)
    ->sort('bar', -1);

// Equivalent to the above
$builder
    ->sort(['foo' => 1, 'bar' => -1]);

I've kept this strategy when creating the egg builder, and now it continues in the $search stage.

In the new aggregation pipeline builder we're creating we're instead leveraging named arguments for this:

Stage::sort(foo: 1, bar: -1)

Long story short, this is going to change once the MongoDB-supported builder is stable enough for us to use it underneath and deprecate ODM's builder :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stage::sort(foo: 1, bar: -1)

this looks nice :) looking forward to it!

{
$allowedMetaSort = ['searchScore'];

$fields = is_array($fieldName) ? $fieldName : [$fieldName => $order];

foreach ($fields as $fieldName => $order) {
if (is_string($order)) {
if (in_array($order, $allowedMetaSort, true)) {
$order = ['$meta' => $order];
} else {
$order = strtolower($order) === 'asc' ? 1 : -1;
}
}

$this->sort[$fieldName] = $order;
}

return $this;
}

/**
* @param T $operator
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public function returnStoredSource(bool $returnStoredSource): Search
return $this->search->returnStoredSource($returnStoredSource);
}

public function sort($fieldName, $order = null): Search
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHPDoc is missing here too

{
return $this->search->sort($fieldName, $order);
}

/**
* @return array<string, object>
* @psalm-return non-empty-array<non-empty-string, object>
Expand Down
54 changes: 12 additions & 42 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,6 @@ parameters:
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Bucket/AbstractOutput.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Densify\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php

-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Densify.php

-
message: "#^Circular definition detected in type alias FacetStageExpression\\.$#"
count: 1
Expand Down Expand Up @@ -96,17 +86,17 @@ parameters:
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Lookup.php

-
message: "#^Circular definition detected in type alias WhenMatchedParamType\\.$#"
message: "#^Circular definition detected in type alias MergeStageExpression\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php

-
message: "#^Circular definition detected in type alias WhenMatchedType\\.$#"
message: "#^Circular definition detected in type alias WhenMatchedParamType\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Merge\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
message: "#^Circular definition detected in type alias WhenMatchedType\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php

Expand All @@ -115,11 +105,6 @@ parameters:
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php

-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Merge.php

-
message: "#^Property Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Merge\\:\\:\\$whenMatched type has no value type specified in iterable type array\\.$#"
count: 1
Expand All @@ -141,19 +126,19 @@ parameters:
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\:\\:near\\(\\) has parameter \\$origin with no value type specified in iterable type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\:\\:near\\(\\) has parameter \\$origin with no value type specified in iterable type array\\.$#"
message: "#^Return type \\(static\\(Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\)\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\:\\:sort\\(\\) should be compatible with return type \\(Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Sort\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\:\\:sort\\(\\)$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php

-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
message: "#^Return type \\(Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\\\AbstractSearchOperator\\:\\:sort\\(\\) should be compatible with return type \\(Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Sort\\) of method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\:\\:sort\\(\\)$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/AbstractSearchOperator.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\Search\\\\Compound\\:\\:geoShape\\(\\) has parameter \\$geometry with no value type specified in iterable type array\\.$#"
Expand Down Expand Up @@ -470,31 +455,11 @@ parameters:
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/SupportsNearOperator.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\SetWindowFields\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php

-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/SetWindowFields.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\UnionWith\\:\\:getExpression\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\UnionWith\\:\\:pipeline\\(\\) has parameter \\$pipeline with no value type specified in iterable type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php

-
message: "#^PHPDoc tag @return with type mixed is not subtype of native type array\\.$#"
count: 1
path: lib/Doctrine/ODM/MongoDB/Aggregation/Stage/UnionWith.php

-
message: "#^Property Doctrine\\\\ODM\\\\MongoDB\\\\Aggregation\\\\Stage\\\\UnionWith\\:\\:\\$pipeline type has no value type specified in iterable type array\\.$#"
count: 1
Expand Down Expand Up @@ -710,6 +675,11 @@ parameters:
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Tests\\\\Aggregation\\\\Stage\\\\SearchTest\\:\\:testSearchOperatorsWithSort\\(\\) has parameter \\$expectedOperator with no value type specified in iterable type array\\.$#"
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php

-
message: "#^Method Doctrine\\\\ODM\\\\MongoDB\\\\Tests\\\\Aggregation\\\\Stage\\\\SetWindowFieldsTest\\:\\:testOperators\\(\\) has parameter \\$args with no type specified\\.$#"
count: 1
Expand Down
47 changes: 47 additions & 0 deletions tests/Doctrine/ODM/MongoDB/Tests/Aggregation/Stage/SearchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,53 @@ public function testSearchOperators(array $expectedOperator, Closure $createOper
);
}

#[DataProvider('provideAutocompleteBuilders')]
#[DataProvider('provideCompoundBuilders')]
#[DataProvider('provideEmbeddedDocumentBuilders')]
#[DataProvider('provideEqualsBuilders')]
#[DataProvider('provideExistsBuilders')]
#[DataProvider('provideGeoShapeBuilders')]
#[DataProvider('provideGeoWithinBuilders')]
#[DataProvider('provideMoreLikeThisBuilders')]
#[DataProvider('provideNearBuilders')]
#[DataProvider('providePhraseBuilders')]
#[DataProvider('provideQueryStringBuilders')]
#[DataProvider('provideRangeBuilders')]
#[DataProvider('provideRegexBuilders')]
#[DataProvider('provideTextBuilders')]
#[DataProvider('provideWildcardBuilders')]
public function testSearchOperatorsWithSort(array $expectedOperator, Closure $createOperator): void
{
$baseExpected = [
'index' => 'my_search_index',
'sort' => (object) [
'unused' => ['$meta' => 'searchScore'],
'date' => -1,
'bar' => 1,
],
];

$searchStage = new Search($this->getTestAggregationBuilder());
$searchStage
->index('my_search_index');

$result = $createOperator($searchStage);

self::logicalOr(
new IsInstanceOf(AbstractSearchOperator::class),
new IsInstanceOf(Search::class),
);

$result
->sort(['unused' => 'searchScore', 'date' => -1])
->sort(['bar' => 1]);

self::assertEquals(
['$search' => (object) array_merge($baseExpected, $expectedOperator)],
$searchStage->getExpression(),
);
}

#[DataProvider('provideAutocompleteBuilders')]
#[DataProvider('provideEmbeddedDocumentBuilders')]
#[DataProvider('provideEqualsBuilders')]
Expand Down