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

Collection Engine #488

Merged
merged 6 commits into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
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: 12 additions & 1 deletion src/EngineManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Exception;
use Illuminate\Support\Manager;
use Laravel\Scout\Engines\AlgoliaEngine;
use Laravel\Scout\Engines\CollectionEngine;
use Laravel\Scout\Engines\MeiliSearchEngine;
use Laravel\Scout\Engines\NullEngine;
use MeiliSearch\Client as MeiliSearch;
Expand Down Expand Up @@ -124,7 +125,17 @@ protected function ensureMeiliSearchClientIsInstalled()
}

/**
* Create a Null engine instance.
* Create a collection engine instance.
*
* @return \Laravel\Scout\Engines\CollectionEngine
*/
public function createCollectionDriver()
{
return new CollectionEngine;
}

/**
* Create a null engine instance.
*
* @return \Laravel\Scout\Engines\NullEngine
*/
Expand Down
239 changes: 239 additions & 0 deletions src/Engines/CollectionEngine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
<?php

namespace Laravel\Scout\Engines;

use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Laravel\Scout\Builder;

class CollectionEngine extends Engine
taylorotwell marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* Create a new engine instance.
*
* @return void
*/
public function __construct()
{
//
}

/**
* Update the given model in the index.
*
* @param \Illuminate\Database\Eloquent\Collection $models
* @return void
*/
public function update($models)
{
//
}

/**
* Remove the given model from the index.
*
* @param \Illuminate\Database\Eloquent\Collection $models
* @return void
*/
public function delete($models)
{
//
}

/**
* Perform the given search on the engine.
*
* @param \Laravel\Scout\Builder $builder
* @return mixed
*/
public function search(Builder $builder)
{
$models = $this->searchModels($builder);

return [
'results' => $models->all(),
'total' => count($models),
];
}

/**
* Perform the given search on the engine.
*
* @param \Laravel\Scout\Builder $builder
* @param int $perPage
* @param int $page
* @return mixed
*/
public function paginate(Builder $builder, $perPage, $page)
{
$models = $this->searchModels($builder);

return [
'results' => $models->forPage($page - 1, $perPage)->all(),
'total' => count($models),
];
}

/**
* Get the Eloquent models for the given builder.
*
* @param \Laravel\Scout\Builder $builder
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function searchModels(Builder $builder)
{
$models = $builder->model->query()
->when(count($builder->wheres) > 0, function ($query) use ($builder) {
foreach ($builder->wheres as $key => $value) {
$query->where($key, $value);
}
})
->orderBy($builder->model->getKeyName(), 'desc')
->cursor();

$models = $models->values();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ->values() is necessary?


if (count($models) === 0) {
return $models;
}

$columns = array_keys($models->first()->toSearchableArray());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it's relevant, but toSearchableArray might be dynamic and depend on the model record. It might be better to execute it per record, and not only based on the first record.


return $models->filter(function ($model) use ($builder, $columns) {
foreach ($columns as $column) {
$attribute = $model->{$column};

if (Str::contains($attribute, $builder->query)) {
return true;
}
}

return false;
})->values();
}

/**
* Pluck and return the primary keys of the given results.
*
* @param mixed $results
* @return \Illuminate\Support\Collection
*/
public function mapIds($results)
{
$results = $results['results'];

return count($results) > 0
? collect($results)->pluck($results[0]->getKeyName())->values()
: collect();
}

/**
* Map the given results to instances of the given model.
*
* @param \Laravel\Scout\Builder $builder
* @param mixed $results
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Collection
*/
public function map(Builder $builder, $results, $model)
{
$results = $results['results'];

if (count($results) === 0) {
return $model->newCollection();
}

$objectIds = collect($results)
->pluck($model->getKeyName())
->values()
->all();

$objectIdPositions = array_flip($objectIds);

return $model->getScoutModelsByIds(
$builder, $objectIds
)->filter(function ($model) use ($objectIds) {
return in_array($model->getScoutKey(), $objectIds);
})->sortBy(function ($model) use ($objectIdPositions) {
return $objectIdPositions[$model->getScoutKey()];
})->values();
}

/**
* Map the given results to instances of the given model via a lazy collection.
*
* @param \Laravel\Scout\Builder $builder
* @param mixed $results
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\LazyCollection
*/
public function lazyMap(Builder $builder, $results, $model)
{
$results = $results['results'];

if (count($results) === 0) {
return LazyCollection::make($model->newCollection());
taylorotwell marked this conversation as resolved.
Show resolved Hide resolved
}

$objectIds = collect($results)
->pluck($model->getKeyName())
->values()->all();

$objectIdPositions = array_flip($objectIds);

return $model->queryScoutModelsByIds(
$builder, $objectIds
)->cursor()->filter(function ($model) use ($objectIds) {
return in_array($model->getScoutKey(), $objectIds);
})->sortBy(function ($model) use ($objectIdPositions) {
taylorotwell marked this conversation as resolved.
Show resolved Hide resolved
return $objectIdPositions[$model->getScoutKey()];
})->values();
}

/**
* Get the total count from a raw result returned by the engine.
*
* @param mixed $results
* @return int
*/
public function getTotalCount($results)
{
return $results['total'];
}

/**
* Flush all of the model's records from the engine.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function flush($model)
{
//
}

/**
* Create a search index.
*
* @param string $name
* @param array $options
* @return mixed
*
* @throws \Exception
*/
public function createIndex($name, array $options = [])
{
//
}

/**
* Delete a search index.
*
* @param string $name
* @return mixed
*/
public function deleteIndex($name)
{
//
}
}
81 changes: 81 additions & 0 deletions tests/Feature/CollectionEngineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Laravel\Scout\Tests\Feature;

use Illuminate\Foundation\Testing\WithFaker;
use Laravel\Scout\ScoutServiceProvider;
use Laravel\Scout\Tests\Fixtures\SearchableUserModel;
use Orchestra\Testbench\Factories\UserFactory;
use Orchestra\Testbench\TestCase;

class CollectionEngineTest extends TestCase
{
use WithFaker;

protected function getPackageProviders($app)
{
return [ScoutServiceProvider::class];
}

protected function defineEnvironment($app)
{
$app->make('config')->set('scout.driver', 'collection');
}

protected function defineDatabaseMigrations()
{
$this->setUpFaker();
$this->loadLaravelMigrations();

UserFactory::new()->create([
'name' => 'Taylor Otwell',
'email' => 'taylor@laravel.com',
]);

UserFactory::new()->create([
'name' => 'Abigail Otwell',
'email' => 'abigail@laravel.com',
]);
}

public function test_it_can_retrieve_results()
{
$models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->get();
$this->assertCount(1, $models);
$this->assertEquals(1, $models[0]->id);

$models = SearchableUserModel::search('Abigail')->where('email', 'abigail@laravel.com')->get();
$this->assertCount(1, $models);
$this->assertEquals(2, $models[0]->id);

$models = SearchableUserModel::search('Taylor')->where('email', 'abigail@laravel.com')->get();
$this->assertCount(0, $models);

$models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->get();
$this->assertCount(1, $models);

$models = SearchableUserModel::search('laravel')->get();
$this->assertCount(2, $models);

$models = SearchableUserModel::search('foo')->get();
$this->assertCount(0, $models);

$models = SearchableUserModel::search('Abigail')->where('email', 'taylor@laravel.com')->get();
$this->assertCount(0, $models);
}

public function test_it_can_paginate_results()
{
$models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate();
$this->assertCount(1, $models);

$models = SearchableUserModel::search('Taylor')->where('email', 'abigail@laravel.com')->paginate();
$this->assertCount(0, $models);

$models = SearchableUserModel::search('Taylor')->where('email', 'taylor@laravel.com')->paginate();
$this->assertCount(1, $models);

$models = SearchableUserModel::search('laravel')->paginate();
$this->assertCount(2, $models);
}
}