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 $search stage to aggregation pipeline builder #2516

Merged
merged 5 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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{
jmikola marked this conversation as resolved.
Show resolved Hide resolved
* '$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();
jmikola marked this conversation as resolved.
Show resolved Hide resolved
}

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> */
public function getExpression(): array
jmikola marked this conversation as resolved.
Show resolved Hide resolved
{
return [$this->getOperatorName() => $this->getOperatorParams()];
}

protected function getSearchStage(): Search
{
return $this->search;
}
}
91 changes: 91 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,91 @@
<?php

declare(strict_types=1);

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

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

use function array_values;

/** @internal */
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,
jmikola marked this conversation as resolved.
Show resolved Hide resolved
'path' => $this->path,
];

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

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

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