Skip to content

Commit 3091159

Browse files
authored
Add $search stage to aggregation pipeline builder (#2516)
* 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
1 parent 21a66c2 commit 3091159

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3829
-0
lines changed

lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php

+13
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,19 @@ public function sample(int $size): Stage\Sample
543543
return $this->addStage($stage);
544544
}
545545

546+
/**
547+
* The $search stage performs a full-text search on the specified field or
548+
* fields which must be covered by an Atlas Search index.
549+
*
550+
* @see https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#mongodb-pipeline-pipe.-search
551+
*/
552+
public function search(): Stage\Search
553+
{
554+
$stage = new Stage\Search($this);
555+
556+
return $this->addStage($stage);
557+
}
558+
546559
/**
547560
* Adds new fields to documents. $set outputs documents that contain all
548561
* existing fields from the input documents and newly added fields.

lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php

+11
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,17 @@ public function sample(int $size): Stage\Sample
365365
return $this->builder->sample($size);
366366
}
367367

368+
/**
369+
* The $search stage performs a full-text search on the specified field or
370+
* fields which must be covered by an Atlas Search index.
371+
*
372+
* @see https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#mongodb-pipeline-pipe.-search
373+
*/
374+
public function search(): Stage\Search
375+
{
376+
return $this->builder->search();
377+
}
378+
368379
/**
369380
* Adds new fields to documents. $set outputs documents that contain all
370381
* existing fields from the input documents and newly added fields.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Aggregation\Stage;
6+
7+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
8+
use Doctrine\ODM\MongoDB\Aggregation\Stage;
9+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SearchOperator;
10+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperators;
11+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search\SupportsAllSearchOperatorsTrait;
12+
13+
/**
14+
* @psalm-type CountType = 'lowerBound'|'total'
15+
* @psalm-type SearchStageExpression = array{
16+
* '$search': object{
17+
* index?: string,
18+
* count?: object{
19+
* type: CountType,
20+
* threshold?: int,
21+
* },
22+
* highlight?: object{
23+
* path: string,
24+
* maxCharsToExamine?: int,
25+
* maxNumPassages?: int,
26+
* },
27+
* returnStoredSource?: bool,
28+
* autocomplete?: object,
29+
* compound?: object,
30+
* embeddedDocument?: object,
31+
* equals?: object,
32+
* exists?: object,
33+
* geoShape?: object,
34+
* geoWithin?: object,
35+
* moreLikeThis?: object,
36+
* near?: object,
37+
* phrase?: object,
38+
* queryString?: object,
39+
* range?: object,
40+
* regex?: object,
41+
* text?: object,
42+
* wildcard?: object,
43+
* }
44+
* }
45+
*/
46+
class Search extends Stage implements SupportsAllSearchOperators
47+
{
48+
use SupportsAllSearchOperatorsTrait;
49+
50+
private string $indexName = '';
51+
private ?object $count = null;
52+
private ?object $highlight = null;
53+
private ?bool $returnStoredSource = null;
54+
private ?SearchOperator $operator = null;
55+
56+
public function __construct(Builder $builder)
57+
{
58+
parent::__construct($builder);
59+
}
60+
61+
/** @psalm-return SearchStageExpression */
62+
public function getExpression(): array
63+
{
64+
$params = (object) [];
65+
66+
if ($this->indexName) {
67+
$params->index = $this->indexName;
68+
}
69+
70+
if ($this->count) {
71+
$params->count = $this->count;
72+
}
73+
74+
if ($this->highlight) {
75+
$params->highlight = $this->highlight;
76+
}
77+
78+
if ($this->returnStoredSource !== null) {
79+
$params->returnStoredSource = $this->returnStoredSource;
80+
}
81+
82+
if ($this->operator !== null) {
83+
$operatorName = $this->operator->getOperatorName();
84+
$params->$operatorName = $this->operator->getOperatorParams();
85+
}
86+
87+
return ['$search' => $params];
88+
}
89+
90+
public function index(string $name): static
91+
{
92+
$this->indexName = $name;
93+
94+
return $this;
95+
}
96+
97+
/** @psalm-param CountType $type */
98+
public function countDocuments(string $type, ?int $threshold = null): static
99+
{
100+
$this->count = (object) ['type' => $type];
101+
102+
if ($threshold !== null) {
103+
$this->count->threshold = $threshold;
104+
}
105+
106+
return $this;
107+
}
108+
109+
public function highlight(string $path, ?int $maxCharsToExamine = null, ?int $maxNumPassages = null): static
110+
{
111+
$this->highlight = (object) ['path' => $path];
112+
113+
if ($maxCharsToExamine !== null) {
114+
$this->highlight->maxCharsToExamine = $maxCharsToExamine;
115+
}
116+
117+
if ($maxNumPassages !== null) {
118+
$this->highlight->maxNumPassages = $maxNumPassages;
119+
}
120+
121+
return $this;
122+
}
123+
124+
public function returnStoredSource(bool $returnStoredSource = true): static
125+
{
126+
$this->returnStoredSource = $returnStoredSource;
127+
128+
return $this;
129+
}
130+
131+
/**
132+
* @param T $operator
133+
*
134+
* @return T
135+
*
136+
* @template T of SearchOperator
137+
*/
138+
protected function addOperator(SearchOperator $operator): SearchOperator
139+
{
140+
return $this->operator = $operator;
141+
}
142+
143+
protected function getSearchStage(): static
144+
{
145+
return $this;
146+
}
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search;
6+
7+
use Doctrine\ODM\MongoDB\Aggregation\Stage;
8+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search;
9+
10+
/** @internal */
11+
abstract class AbstractSearchOperator extends Stage implements SearchOperator
12+
{
13+
public function __construct(private Search $search)
14+
{
15+
parent::__construct($search->builder);
16+
}
17+
18+
public function index(string $name): Search
19+
{
20+
return $this->search->index($name);
21+
}
22+
23+
public function countDocuments(string $type, ?int $threshold = null): Search
24+
{
25+
return $this->search->countDocuments($type, $threshold);
26+
}
27+
28+
public function highlight(string $path, ?int $maxCharsToExamine = null, ?int $maxNumPassages = null): Search
29+
{
30+
return $this->search->highlight($path, $maxCharsToExamine, $maxNumPassages);
31+
}
32+
33+
public function returnStoredSource(bool $returnStoredSource): Search
34+
{
35+
return $this->search->returnStoredSource($returnStoredSource);
36+
}
37+
38+
/** @return array<string, object> */
39+
final public function getExpression(): array
40+
{
41+
return [$this->getOperatorName() => $this->getOperatorParams()];
42+
}
43+
44+
protected function getSearchStage(): Search
45+
{
46+
return $this->search;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Aggregation\Stage\Search;
6+
7+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Search;
8+
9+
use function array_values;
10+
11+
/**
12+
* @internal
13+
*
14+
* @see https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/
15+
*/
16+
class Autocomplete extends AbstractSearchOperator implements ScoredSearchOperator
17+
{
18+
use ScoredSearchOperatorTrait;
19+
20+
/** @var list<string> */
21+
private array $query;
22+
private string $path;
23+
private string $tokenOrder = '';
24+
private ?object $fuzzy = null;
25+
26+
public function __construct(Search $search, string $path, string ...$query)
27+
{
28+
parent::__construct($search);
29+
30+
$this->query(...$query);
31+
$this->path($path);
32+
}
33+
34+
public function query(string ...$query): static
35+
{
36+
$this->query = array_values($query);
37+
38+
return $this;
39+
}
40+
41+
public function path(string $path): static
42+
{
43+
$this->path = $path;
44+
45+
return $this;
46+
}
47+
48+
public function tokenOrder(string $order): static
49+
{
50+
$this->tokenOrder = $order;
51+
52+
return $this;
53+
}
54+
55+
public function fuzzy(?int $maxEdits = null, ?int $prefixLength = null, ?int $maxExpansions = null): static
56+
{
57+
$this->fuzzy = (object) [];
58+
if ($maxEdits !== null) {
59+
$this->fuzzy->maxEdits = $maxEdits;
60+
}
61+
62+
if ($prefixLength !== null) {
63+
$this->fuzzy->prefixLength = $prefixLength;
64+
}
65+
66+
if ($maxExpansions !== null) {
67+
$this->fuzzy->maxExpansions = $maxExpansions;
68+
}
69+
70+
return $this;
71+
}
72+
73+
public function getOperatorName(): string
74+
{
75+
return 'autocomplete';
76+
}
77+
78+
public function getOperatorParams(): object
79+
{
80+
$params = (object) [
81+
'query' => $this->query,
82+
'path' => $this->path,
83+
];
84+
85+
if ($this->tokenOrder) {
86+
$params->tokenOrder = $this->tokenOrder;
87+
}
88+
89+
if ($this->fuzzy) {
90+
$params->fuzzy = $this->fuzzy;
91+
}
92+
93+
return $this->appendScore($params);
94+
}
95+
}

0 commit comments

Comments
 (0)