Skip to content

Commit

Permalink
Add $search stage to aggregation pipeline builder (#2516)
Browse files Browse the repository at this point in the history
* Add $search stage to aggregation pipeline builder

* Make AbstractSearchOperator::getExpression final

* Make appendScore method private for scored operators

* Add documentation links to search operator classes

* Separate wildcard and regex search operators
  • Loading branch information
alcaeus committed Mar 29, 2023
1 parent 21a66c2 commit 3091159
Show file tree
Hide file tree
Showing 60 changed files with 3,829 additions and 0 deletions.
13 changes: 13 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,19 @@ public function sample(int $size): Stage\Sample
return $this->addStage($stage);
}

/**
* The $search stage performs a full-text search on the specified field or
* fields which must be covered by an Atlas Search index.
*
* @see https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#mongodb-pipeline-pipe.-search
*/
public function search(): Stage\Search
{
$stage = new Stage\Search($this);

return $this->addStage($stage);
}

/**
* Adds new fields to documents. $set outputs documents that contain all
* existing fields from the input documents and newly added fields.
Expand Down
11 changes: 11 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,17 @@ public function sample(int $size): Stage\Sample
return $this->builder->sample($size);
}

/**
* The $search stage performs a full-text search on the specified field or
* fields which must be covered by an Atlas Search index.
*
* @see https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#mongodb-pipeline-pipe.-search
*/
public function search(): Stage\Search
{
return $this->builder->search();
}

/**
* Adds new fields to documents. $set outputs documents that contain all
* existing fields from the input documents and newly added fields.
Expand Down
147 changes: 147 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Aggregation\Stage;

use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Doctrine\ODM\MongoDB\Aggregation\Stage;
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SearchOperator;
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperators;
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperatorsTrait;

/**
* @psalm-type CountType = 'lowerBound'|'total'
* @psalm-type SearchStageExpression = array{
* '$search': object{
* index?: string,
* count?: object{
* type: CountType,
* threshold?: int,
* },
* highlight?: object{
* path: string,
* maxCharsToExamine?: int,
* maxNumPassages?: int,
* },
* returnStoredSource?: bool,
* autocomplete?: object,
* compound?: object,
* embeddedDocument?: object,
* equals?: object,
* exists?: object,
* geoShape?: object,
* geoWithin?: object,
* moreLikeThis?: object,
* near?: object,
* phrase?: object,
* queryString?: object,
* range?: object,
* regex?: object,
* text?: object,
* wildcard?: object,
* }
* }
*/
class Search extends Stage implements SupportsAllSearchOperators
{
use SupportsAllSearchOperatorsTrait;

private string $indexName = '';
private ?object $count = null;
private ?object $highlight = null;
private ?bool $returnStoredSource = null;
private ?SearchOperator $operator = null;

public function __construct(Builder $builder)
{
parent::__construct($builder);
}

/** @psalm-return SearchStageExpression */
public function getExpression(): array
{
$params = (object) [];

if ($this->indexName) {
$params->index = $this->indexName;
}

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

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

if ($this->returnStoredSource !== null) {
$params->returnStoredSource = $this->returnStoredSource;
}

if ($this->operator !== null) {
$operatorName = $this->operator->getOperatorName();
$params->$operatorName = $this->operator->getOperatorParams();
}

return ['$search' => $params];
}

public function index(string $name): static
{
$this->indexName = $name;

return $this;
}

/** @psalm-param CountType $type */
public function countDocuments(string $type, ?int $threshold = null): static
{
$this->count = (object) ['type' => $type];

if ($threshold !== null) {
$this->count->threshold = $threshold;
}

return $this;
}

public function highlight(string $path, ?int $maxCharsToExamine = null, ?int $maxNumPassages = null): static
{
$this->highlight = (object) ['path' => $path];

if ($maxCharsToExamine !== null) {
$this->highlight->maxCharsToExamine = $maxCharsToExamine;
}

if ($maxNumPassages !== null) {
$this->highlight->maxNumPassages = $maxNumPassages;
}

return $this;
}

public function returnStoredSource(bool $returnStoredSource = true): static
{
$this->returnStoredSource = $returnStoredSource;

return $this;
}

/**
* @param T $operator
*
* @return T
*
* @template T of SearchOperator
*/
protected function addOperator(SearchOperator $operator): SearchOperator
{
return $this->operator = $operator;
}

protected function getSearchStage(): static
{
return $this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search;

use Doctrine\ODM\MongoDB\Aggregation\Stage;
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search;

/** @internal */
abstract class AbstractSearchOperator extends Stage implements SearchOperator
{
public function __construct(private Search $search)
{
parent::__construct($search->builder);
}

public function index(string $name): Search
{
return $this->search->index($name);
}

public function countDocuments(string $type, ?int $threshold = null): Search
{
return $this->search->countDocuments($type, $threshold);
}

public function highlight(string $path, ?int $maxCharsToExamine = null, ?int $maxNumPassages = null): Search
{
return $this->search->highlight($path, $maxCharsToExamine, $maxNumPassages);
}

public function returnStoredSource(bool $returnStoredSource): Search
{
return $this->search->returnStoredSource($returnStoredSource);
}

/** @return array<string, object> */
final public function getExpression(): array
{
return [$this->getOperatorName() => $this->getOperatorParams()];
}

protected function getSearchStage(): Search
{
return $this->search;
}
}
95 changes: 95 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Aggregation/Stage/Search/Autocomplete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search;

use Doctrine\ODM\MongoDB\Aggregation\Stage\Search;

use function array_values;

/**
* @internal
*
* @see https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/
*/
class Autocomplete extends AbstractSearchOperator implements ScoredSearchOperator
{
use ScoredSearchOperatorTrait;

/** @var list<string> */
private array $query;
private string $path;
private string $tokenOrder = '';
private ?object $fuzzy = null;

public function __construct(Search $search, string $path, string ...$query)
{
parent::__construct($search);

$this->query(...$query);
$this->path($path);
}

public function query(string ...$query): static
{
$this->query = array_values($query);

return $this;
}

public function path(string $path): static
{
$this->path = $path;

return $this;
}

public function tokenOrder(string $order): static
{
$this->tokenOrder = $order;

return $this;
}

public function fuzzy(?int $maxEdits = null, ?int $prefixLength = null, ?int $maxExpansions = null): static
{
$this->fuzzy = (object) [];
if ($maxEdits !== null) {
$this->fuzzy->maxEdits = $maxEdits;
}

if ($prefixLength !== null) {
$this->fuzzy->prefixLength = $prefixLength;
}

if ($maxExpansions !== null) {
$this->fuzzy->maxExpansions = $maxExpansions;
}

return $this;
}

public function getOperatorName(): string
{
return 'autocomplete';
}

public function getOperatorParams(): object
{
$params = (object) [
'query' => $this->query,
'path' => $this->path,
];

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

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

return $this->appendScore($params);
}
}
Loading

0 comments on commit 3091159

Please sign in to comment.