Skip to content

Commit

Permalink
Set typemap on cursor, only root._id is mapped
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Jan 14, 2025
1 parent 22f40a0 commit bd218ac
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 35 deletions.
70 changes: 43 additions & 27 deletions src/Scout/ScoutEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ final class ScoutEngine extends Engine
],
];

private const TYPEMAP = ['root' => 'object', 'document' => 'bson', 'array' => 'bson'];

public function __construct(
private Database $database,
private bool $softDelete,
Expand Down Expand Up @@ -96,28 +98,42 @@ public function update($models)

$bulk = [];
foreach ($models as $model) {
assert($model instanceof Model && method_exists($model, 'toSearchableArray'), new LogicException(sprintf('Model "%s" must use "%s" trait', $model::class, Searchable::class)));

$searchableData = $model->toSearchableArray();
$searchableData = self::serialize($searchableData);

if ($searchableData) {
unset($searchableData['_id']);

$searchableData = array_merge($searchableData, $model->scoutMetadata());
if (isset($searchableData['__soft_deleted'])) {
$searchableData['__soft_deleted'] = (bool) $searchableData['__soft_deleted'];
}

// Skip/remove the model if it doesn't provide any searchable data
if (! $searchableData) {
$bulk[] = [
'updateOne' => [
'deleteOne' => [
['_id' => $model->getScoutKey()],
[
'$setOnInsert' => ['_id' => $model->getScoutKey()],
'$set' => $searchableData,
],
['upsert' => true],
],
];

continue;
}

unset($searchableData['_id']);

$searchableData = array_merge($searchableData, $model->scoutMetadata());

/** Convert the __soft_deleted set by {@see Searchable::pushSoftDeleteMetadata()}
* into a boolean for efficient storage and indexing. */
if (isset($searchableData['__soft_deleted'])) {
$searchableData['__soft_deleted'] = (bool) $searchableData['__soft_deleted'];
}

$bulk[] = [
'updateOne' => [
['_id' => $model->getScoutKey()],
[
'$setOnInsert' => ['_id' => $model->getScoutKey()],
'$set' => $searchableData,
],
['upsert' => true],
],
];
}

$this->getIndexableCollection($models)->bulkWrite($bulk);
Expand All @@ -133,7 +149,7 @@ public function update($models)
#[Override]
public function delete($models): void
{
assert($models instanceof Collection, new TypeError(sprintf('Argument #1 ($models) must be of type %s, %s given', Collection::class, get_debug_type($models))));
assert($models instanceof EloquentCollection, new TypeError(sprintf('Argument #1 ($models) must be of type %s, %s given', Collection::class, get_debug_type($models))));

if ($models->isEmpty()) {
return;
Expand All @@ -149,7 +165,7 @@ public function delete($models): void
*
* @see Engine::search()
*
* @return mixed
* @return array
*/
#[Override]
public function search(Builder $builder)
Expand All @@ -158,14 +174,14 @@ public function search(Builder $builder)
}

/**
* Perform the given search on the engine.
* Perform the given search on the engine with pagination.
*
* @see Engine::paginate()
*
* @param int $perPage
* @param int $page
*
* @return mixed
* @return array
*/
#[Override]
public function paginate(Builder $builder, $perPage, $page)
Expand All @@ -182,20 +198,21 @@ public function paginate(Builder $builder, $perPage, $page)
/**
* Perform the given search on the engine.
*/
protected function performSearch(Builder $builder, ?int $offset = null): array
private function performSearch(Builder $builder, ?int $offset = null): array
{
$collection = $this->getSearchableCollection($builder->model);

if ($builder->callback) {
$result = call_user_func(
$cursor = call_user_func(
$builder->callback,
$collection,
$builder->query,
$offset,
);
assert($result instanceof CursorInterface, new LogicException(sprintf('The search builder closure must return a MongoDB cursor, %s returned', get_debug_type($result))));
assert($cursor instanceof CursorInterface, new LogicException(sprintf('The search builder closure must return a MongoDB cursor, %s returned', get_debug_type($cursor))));
$cursor->setTypeMap(self::TYPEMAP);

return $result->toArray();
return $cursor->toArray();
}

$compound = [
Expand Down Expand Up @@ -270,11 +287,10 @@ protected function performSearch(Builder $builder, ?int $offset = null): array
$pipeline[] = ['$limit' => $builder->limit];
}

$options = [
'typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array'],
];
$cursor = $collection->aggregate($pipeline);
$cursor->setTypeMap(self::TYPEMAP);

return $collection->aggregate($pipeline, $options)->toArray();
return $cursor->toArray();
}

/**
Expand Down Expand Up @@ -539,6 +555,6 @@ private function wait(Closure $callback): void
sleep(1);
}

throw new MongoDBRuntimeException('Atlas search index operation time out');
throw new MongoDBRuntimeException(sprintf('Atlas search index operation time out after %s seconds', self::WAIT_TIMEOUT_SEC));
}
}
3 changes: 1 addition & 2 deletions tests/AtlasSearchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@

use function array_map;
use function assert;

use function mt_getrandmax;
use function rand;
use function range;
use function srand;
use function sort;
use function srand;
use function usleep;
use function usort;

Expand Down
15 changes: 9 additions & 6 deletions tests/Scout/ScoutEngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
/** Unit tests that do not require an Atlas Search cluster */
class ScoutEngineTest extends TestCase
{
private const EXPECTED_SEARCH_OPTIONS = ['typeMap' => ['root' => 'array', 'document' => 'array', 'array' => 'array']];
private const EXPECTED_TYPEMAP = ['root' => 'object', 'document' => 'bson', 'array' => 'bson'];

/** @param callable(): Builder $builder */
#[DataProvider('provideSearchPipelines')]
Expand All @@ -40,13 +40,16 @@ public function testSearch(Closure $builder, array $expectedPipeline): void
->with('collection_searchable')
->andReturn($collection);
$cursor = m::mock(CursorInterface::class);
$cursor->shouldReceive('setTypeMap')->once()->with(self::EXPECTED_TYPEMAP);
$cursor->shouldReceive('toArray')->once()->with()->andReturn($data);

$collection->shouldReceive('getCollectionName')
->zeroOrMoreTimes()
->andReturn('collection_searchable');
$collection->shouldReceive('aggregate')
->once()
->withArgs(function ($pipeline, $options) use ($expectedPipeline) {
->withArgs(function ($pipeline) use ($expectedPipeline) {
self::assertEquals($expectedPipeline, $pipeline);
self::assertEquals(self::EXPECTED_SEARCH_OPTIONS, $options);

return true;
})
Expand Down Expand Up @@ -295,10 +298,11 @@ function () {
fn () => new Builder(new SearchableModel(), 'query', callback: function (...$args) {
$this->assertCount(3, $args);
$this->assertInstanceOf(Collection::class, $args[0]);
$this->assertSame('collection_searchable', $args[0]->getCollectionName());
$this->assertSame('query', $args[1]);
$this->assertNull($args[2]);

return $args[0]->aggregate(['pipeline'], self::EXPECTED_SEARCH_OPTIONS);
return $args[0]->aggregate(['pipeline']);
}),
['pipeline'],
];
Expand Down Expand Up @@ -383,11 +387,10 @@ public function testPaginate()
],
], $args[0]);

$this->assertSame(self::EXPECTED_SEARCH_OPTIONS, $args[1]);

return true;
})
->andReturn($cursor);
$cursor->shouldReceive('setTypeMap')->once()->with(self::EXPECTED_TYPEMAP);
$cursor->shouldReceive('toArray')
->once()
->with()
Expand Down

0 comments on commit bd218ac

Please sign in to comment.