Skip to content

Commit d6d8004

Browse files
authored
PHPORM-275 PHPORM-276 Add Query\Builder::search() and autocomplete() (#3232)
1 parent 5bed82c commit d6d8004

File tree

3 files changed

+162
-1
lines changed

3 files changed

+162
-1
lines changed

src/Eloquent/Builder.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
namespace MongoDB\Laravel\Eloquent;
66

77
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
8+
use Illuminate\Database\Eloquent\Collection;
9+
use Illuminate\Database\Eloquent\Model;
810
use MongoDB\BSON\Document;
11+
use MongoDB\Builder\Type\SearchOperatorInterface;
912
use MongoDB\Driver\CursorInterface;
1013
use MongoDB\Driver\Exception\WriteException;
1114
use MongoDB\Laravel\Connection;
@@ -21,7 +24,10 @@
2124
use function iterator_to_array;
2225
use function property_exists;
2326

24-
/** @method \MongoDB\Laravel\Query\Builder toBase() */
27+
/**
28+
* @method \MongoDB\Laravel\Query\Builder toBase()
29+
* @template TModel of Model
30+
*/
2531
class Builder extends EloquentBuilder
2632
{
2733
private const DUPLICATE_KEY_ERROR = 11000;
@@ -49,6 +55,7 @@ class Builder extends EloquentBuilder
4955
'insertusing',
5056
'max',
5157
'min',
58+
'autocomplete',
5259
'pluck',
5360
'pull',
5461
'push',
@@ -69,6 +76,31 @@ public function aggregate($function = null, $columns = ['*'])
6976
return $result ?: $this;
7077
}
7178

79+
/**
80+
* Performs a full-text search of the field or fields in an Atlas collection.
81+
*
82+
* @see https://www.mongodb.com/docs/atlas/atlas-search/aggregation-stages/search/
83+
*
84+
* @return Collection<int, TModel>
85+
*/
86+
public function search(
87+
SearchOperatorInterface|array $operator,
88+
?string $index = null,
89+
?array $highlight = null,
90+
?bool $concurrent = null,
91+
?string $count = null,
92+
?string $searchAfter = null,
93+
?string $searchBefore = null,
94+
?bool $scoreDetails = null,
95+
?array $sort = null,
96+
?bool $returnStoredSource = null,
97+
?array $tracking = null,
98+
): Collection {
99+
$results = $this->toBase()->search($operator, $index, $highlight, $concurrent, $count, $searchAfter, $searchBefore, $scoreDetails, $sort, $returnStoredSource, $tracking);
100+
101+
return $this->model->hydrate($results->all());
102+
}
103+
72104
/** @inheritdoc */
73105
public function update(array $values, array $options = [])
74106
{

src/Query/Builder.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
use MongoDB\BSON\ObjectID;
2424
use MongoDB\BSON\Regex;
2525
use MongoDB\BSON\UTCDateTime;
26+
use MongoDB\Builder\Search;
2627
use MongoDB\Builder\Stage\FluentFactoryTrait;
28+
use MongoDB\Builder\Type\SearchOperatorInterface;
2729
use MongoDB\Driver\Cursor;
2830
use Override;
2931
use RuntimeException;
3032
use stdClass;
3133

3234
use function array_fill_keys;
35+
use function array_filter;
3336
use function array_is_list;
3437
use function array_key_exists;
3538
use function array_map;
@@ -1490,6 +1493,68 @@ public function options(array $options)
14901493
return $this;
14911494
}
14921495

1496+
/**
1497+
* Performs a full-text search of the field or fields in an Atlas collection.
1498+
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
1499+
*
1500+
* @see https://www.mongodb.com/docs/atlas/atlas-search/aggregation-stages/search/
1501+
*
1502+
* @return Collection<object|array>
1503+
*/
1504+
public function search(
1505+
SearchOperatorInterface|array $operator,
1506+
?string $index = null,
1507+
?array $highlight = null,
1508+
?bool $concurrent = null,
1509+
?string $count = null,
1510+
?string $searchAfter = null,
1511+
?string $searchBefore = null,
1512+
?bool $scoreDetails = null,
1513+
?array $sort = null,
1514+
?bool $returnStoredSource = null,
1515+
?array $tracking = null,
1516+
): Collection {
1517+
// Forward named arguments to the search stage, skip null values
1518+
$args = array_filter([
1519+
'operator' => $operator,
1520+
'index' => $index,
1521+
'highlight' => $highlight,
1522+
'concurrent' => $concurrent,
1523+
'count' => $count,
1524+
'searchAfter' => $searchAfter,
1525+
'searchBefore' => $searchBefore,
1526+
'scoreDetails' => $scoreDetails,
1527+
'sort' => $sort,
1528+
'returnStoredSource' => $returnStoredSource,
1529+
'tracking' => $tracking,
1530+
], fn ($arg) => $arg !== null);
1531+
1532+
return $this->aggregate()->search(...$args)->get();
1533+
}
1534+
1535+
/**
1536+
* Performs an autocomplete search of the field using an Atlas Search index.
1537+
* NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments.
1538+
* You must create an Atlas Search index with an autocomplete configuration before you can use this stage.
1539+
*
1540+
* @see https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/
1541+
*
1542+
* @return Collection<string>
1543+
*/
1544+
public function autocomplete(string $path, string $query, bool|array $fuzzy = false, string $tokenOrder = 'any'): Collection
1545+
{
1546+
$args = ['path' => $path, 'query' => $query, 'tokenOrder' => $tokenOrder];
1547+
if ($fuzzy === true) {
1548+
$args['fuzzy'] = ['maxEdits' => 2];
1549+
} elseif ($fuzzy !== false) {
1550+
$args['fuzzy'] = $fuzzy;
1551+
}
1552+
1553+
return $this->aggregate()->search(
1554+
Search::autocomplete(...$args),
1555+
)->get()->pluck($path);
1556+
}
1557+
14931558
/**
14941559
* Apply the connection's session to options if it's not already specified.
14951560
*/

tests/AtlasSearchTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace MongoDB\Laravel\Tests;
44

5+
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
6+
use Illuminate\Support\Collection as LaravelCollection;
57
use Illuminate\Support\Facades\Schema;
8+
use MongoDB\Builder\Search;
69
use MongoDB\Collection as MongoDBCollection;
710
use MongoDB\Driver\Exception\ServerException;
811
use MongoDB\Laravel\Schema\Builder;
@@ -43,13 +46,15 @@ public function setUp(): void
4346

4447
$collection = $this->getConnection('mongodb')->getCollection('books');
4548
assert($collection instanceof MongoDBCollection);
49+
4650
try {
4751
$collection->createSearchIndex([
4852
'mappings' => [
4953
'fields' => [
5054
'title' => [
5155
['type' => 'string', 'analyzer' => 'lucene.english'],
5256
['type' => 'autocomplete', 'analyzer' => 'lucene.english'],
57+
['type' => 'token'],
5358
],
5459
],
5560
],
@@ -135,4 +140,63 @@ public function testGetIndexes()
135140

136141
self::assertSame($expected, $indexes);
137142
}
143+
144+
public function testEloquentBuilderSearch()
145+
{
146+
$results = Book::search(
147+
sort: ['title' => 1],
148+
operator: Search::text('title', 'systems'),
149+
);
150+
151+
self::assertInstanceOf(EloquentCollection::class, $results);
152+
self::assertCount(3, $results);
153+
self::assertInstanceOf(Book::class, $results->first());
154+
self::assertSame([
155+
'Database System Concepts',
156+
'Modern Operating Systems',
157+
'Operating System Concepts',
158+
], $results->pluck('title')->all());
159+
}
160+
161+
public function testDatabaseBuilderSearch()
162+
{
163+
$results = $this->getConnection('mongodb')->table('books')
164+
->search(Search::text('title', 'systems'), sort: ['title' => 1]);
165+
166+
self::assertInstanceOf(LaravelCollection::class, $results);
167+
self::assertCount(3, $results);
168+
self::assertIsArray($results->first());
169+
self::assertSame([
170+
'Database System Concepts',
171+
'Modern Operating Systems',
172+
'Operating System Concepts',
173+
], $results->pluck('title')->all());
174+
}
175+
176+
public function testEloquentBuilderAutocomplete()
177+
{
178+
$results = Book::autocomplete('title', 'system');
179+
180+
self::assertInstanceOf(LaravelCollection::class, $results);
181+
self::assertCount(3, $results);
182+
self::assertSame([
183+
'Operating System Concepts',
184+
'Database System Concepts',
185+
'Modern Operating Systems',
186+
], $results->all());
187+
}
188+
189+
public function testDatabaseBuilderAutocomplete()
190+
{
191+
$results = $this->getConnection('mongodb')->table('books')
192+
->autocomplete('title', 'system');
193+
194+
self::assertInstanceOf(LaravelCollection::class, $results);
195+
self::assertCount(3, $results);
196+
self::assertSame([
197+
'Operating System Concepts',
198+
'Database System Concepts',
199+
'Modern Operating Systems',
200+
], $results->all());
201+
}
138202
}

0 commit comments

Comments
 (0)