Skip to content

Commit

Permalink
Merge pull request #17 from matchish/mixed
Browse files Browse the repository at this point in the history
Search among multiple models
  • Loading branch information
matchish authored May 11, 2019
2 parents 8d84935 + c564b2a commit 064df21
Show file tree
Hide file tree
Showing 21 changed files with 288 additions and 58 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/)

## [Unreleased]

## [2.0.2] - 2019-05-10
### Added
- Search amongst multiple models

## [2.0.1] - 2019-05-06
### Added
- Progress report for console commands
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ test: up ## Execute application tests
test-coverage: up ## Execute application tests and generate report
$(docker_compose_bin) exec "$(APP_CONTAINER_NAME)" ./vendor/bin/phpstan analyze
$(docker_compose_bin) exec "$(APP_CONTAINER_NAME)" ./vendor/bin/phpunit --coverage-html build/coverage-report

test-filter:
$(docker_compose_bin) exec "$(APP_CONTAINER_NAME)" ./vendor/bin/phpunit --filter=$(filter) --testdox
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ If you need any help, [stack overflow](https://stackoverflow.com/questions/tagge

## :two_hearts: Features

- [Search amongst multiple models](#search-amongst-multiple-models)
- [**Zero downtime** reimport](#zero-downtime-reimport) - it’s a breeze to import data in production.
- [Elasticsearch **7.0** ready](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html) - We don't use mapping types.
- Import all searchable models at once.
- A fully configurable mapping for each model.
- Full power of ElasticSearch in your queries
Expand Down Expand Up @@ -135,6 +137,19 @@ $results = Product::search('zonga', function($client, $body) {
`$client` is `\ElasticSearch\Client` object from [elasticsearch/elasticsearch](https://packagist.org/packages/elasticsearch/elasticsearch) package
And `$body` is `ONGR\ElasticsearchDSL\Search` from [ongr/elasticsearch-dsl](https://packagist.org/packages/ongr/elasticsearch-dsl) package

### Search amongst multiple models
You can do it with `Mixed` class, just pass indices names separated by commas to `within` method.
```php
Mixed::search('title:Barselona or to:Barselona')
within(implode(',', [
(new Ticket())->serchableAs(),
(new Book())->serchableAs(),
]))
->get();
```
In this example you will get collection of `Ticket` and `Book` models where tisket arrival city or
book title is `Barselona`

>Don't forget to :star: the package if you like it. :pray:
## :free: License
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "matchish/laravel-scout-elasticsearch",
"description": "This package extends Laravel Scout adding full power of ElasticSearch",
"version": "2.0.1",
"version": "2.0.2",
"keywords": [
"laravel",
"scout",
Expand Down
Binary file modified docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 27 additions & 24 deletions src/ElasticSearch/EloquentHitsIteratorAggregate.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use Traversable;
use Laravel\Scout\Builder;
use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;

/**
* @internal
Expand All @@ -21,25 +20,19 @@ final class EloquentHitsIteratorAggregate implements \IteratorAggregate
/**
* @var array
*/
private $keys;
/**
* @var Model
*/
private $model;
private $results;
/**
* @var callable|null
*/
private $callback;

/**
* @param array $keys
* @param Model $model
* @param array $results
* @param callable|null $callback
*/
public function __construct(array $keys, Model $model, callable $callback = null)
public function __construct(array $results, callable $callback = null)
{
$this->keys = $keys;
$this->model = $model;
$this->results = $results;
$this->callback = $callback;
}

Expand All @@ -52,20 +45,30 @@ public function __construct(array $keys, Model $model, callable $callback = null
*/
public function getIterator()
{
/** @var Searchable $model */
$model = $this->model;
$builder = new Builder($this->model, '');
if (! empty($this->callback)) {
$builder->query($this->callback);
$hits = collect();
if ($this->results['hits']['total']) {
$hits = $this->results['hits']['hits'];
$models = collect($hits)->groupBy('_source.__class_name')
->map(function ($results, $class) {
$model = new $class;
$builder = new Builder($model, '');
if (! empty($this->callback)) {
$builder->query($this->callback);
}
/* @var Searchable $model */
return $models = $model->getScoutModelsByIds(
$builder, $results->pluck('_id')->all()
);
})
->flatten()->keyBy(function ($model) {
return get_class($model).'::'.$model->getScoutKey();
});
$hits = collect($hits)->map(function ($hit) use ($models) {
$key = $hit['_source']['__class_name'].'::'.$hit['_id'];

return isset($models[$key]) ? $models[$key] : null;
})->filter()->all();
}
$models = $model->getScoutModelsByIds(
$builder, $this->keys
)->keyBy(function ($model) {
return $model->getScoutKey();
});
$hits = collect($this->keys)->map(function ($key) use ($models) {
return isset($models[$key]) ? $models[$key] : null;
})->filter()->all();

return new \ArrayIterator($hits);
}
Expand Down
5 changes: 4 additions & 1 deletion src/ElasticSearch/Params/Bulk.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ function ($payload, $model) {

$payload['body'][] = array_merge(
$model->toSearchableArray(),
$model->scoutMetadata()
$model->scoutMetadata(),
[
'__class_name' => get_class($model),
]
);

return $payload;
Expand Down
6 changes: 3 additions & 3 deletions src/Engines/ElasticSearchEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Laravel\Scout\Engines\Engine;
use ONGR\ElasticsearchDSL\Search;
use Laravel\Scout\Builder as BaseBuilder;
use Illuminate\Database\Eloquent\Collection;
use ONGR\ElasticsearchDSL\Query\MatchAllQuery;
use Matchish\ScoutElasticSearch\ElasticSearch\Index;
use Matchish\ScoutElasticSearch\ElasticSearch\Params\Bulk;
Expand Down Expand Up @@ -102,10 +103,9 @@ public function mapIds($results)
*/
public function map(BaseBuilder $builder, $results, $model)
{
$ids = $this->mapIds($results)->all();
$hits = new EloquentHitsIteratorAggregate($ids, $model, $builder->queryCallback);
$hits = new EloquentHitsIteratorAggregate($results, $builder->queryCallback);

return new \Illuminate\Database\Eloquent\Collection($hits);
return new Collection($hits);
}

/**
Expand Down
20 changes: 8 additions & 12 deletions src/Jobs/ImportStages.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,13 @@ class ImportStages extends Collection
public static function fromSearchable(Model $searchable)
{
$index = Index::fromSearchable($searchable);
$main = PullFromSource::chunked($searchable);
if ($main->count()) {
return (new static([
new CleanUp($searchable),
new CreateWriteIndex($searchable, $index),
PullFromSource::chunked($searchable),
new RefreshIndex($index),
new SwitchToNewAndRemoveOldIndex($searchable, $index),
]))->flatten();
} else {
return new static;
}

return (new static([
new CleanUp($searchable),
new CreateWriteIndex($searchable, $index),
PullFromSource::chunked($searchable),
new RefreshIndex($index),
new SwitchToNewAndRemoveOldIndex($searchable, $index),
]))->flatten()->filter();
}
}
24 changes: 24 additions & 0 deletions src/Mixed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Matchish\ScoutElasticSearch;

use Laravel\Scout\Builder;
use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;

final class Mixed
{
/**
* Perform a search against the model's indexed data.
*
* @param string $query
* @param \Closure $callback
* @return \Laravel\Scout\Builder
*/
public static function search($query = '', $callback = null)
{
return new Builder(new class extends Model {
use Searchable;
}, $query, $callback);
}
}
6 changes: 3 additions & 3 deletions tests/Feature/ImportCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ public function test_progress_report()
trim($output[0]));
$this->assertEquals(
'[OK] '.trans('scout::import.done', ['searchable' => Product::class]),
trim($output[2]));
trim($output[14]));
$this->assertEquals(
trans('scout::import.start', ['searchable' => Book::class]),
trim($output[4]));
trim($output[16]));
$this->assertEquals(
'[OK] '.trans('scout::import.done', ['searchable' => Book::class]),
trim($output[6]));
trim($output[30]));
}

public function test_progress_report_in_queue()
Expand Down
45 changes: 45 additions & 0 deletions tests/Feature/SearchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

namespace Tests\Feature;

use App\Book;
use App\Ticket;
use App\Product;
use Tests\IntegrationTestCase;
use Illuminate\Pagination\Paginator;
use Matchish\ScoutElasticSearch\Mixed;
use Illuminate\Support\Facades\Artisan;

final class SearchTest extends IntegrationTestCase
Expand Down Expand Up @@ -88,4 +91,46 @@ public function test_within()
->get();
$this->assertEquals($kindleAmount, $kindle->count());
}

public function test_mixed()
{
$newyorkAmount = rand(1, 5);
$barselonaAmount = rand(1, 5);

$dispatcher = Ticket::getEventDispatcher();
Ticket::unsetEventDispatcher();

factory(Ticket::class, $newyorkAmount)->state('new-york')->create();
factory(Ticket::class, $barselonaAmount)->state('barselona')->create();

Ticket::setEventDispatcher($dispatcher);

$dispatcher = Book::getEventDispatcher();
Book::unsetEventDispatcher();

factory(Book::class, $newyorkAmount)->state('new-york')->create();
factory(Book::class, $barselonaAmount)->state('barselona')->create();

Book::setEventDispatcher($dispatcher);

Artisan::call('scout:import');

$mixed = Mixed::search('new-york')->within(
implode(',', [(new Book)->searchableAs(),
(new Ticket())->searchableAs(),
]))->get();
$this->assertEquals($newyorkAmount * 2, $mixed->count());
$this->assertEquals(['tickets' => $newyorkAmount, 'books' => $newyorkAmount], $mixed->map->getTable()->countBy()->all());
}

public function test_mixed_no_results()
{
Artisan::call('scout:import');

$mixed = Mixed::search('lisbon')->within(
implode(',', [(new Book)->searchableAs(),
(new Ticket())->searchableAs(),
]))->get();
$this->assertEquals(0, $mixed->count());
}
}
13 changes: 8 additions & 5 deletions tests/Integration/Engines/ElasticSearchEngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Integration\Engines;

use stdClass;
use App\Product;
use Laravel\Scout\Builder;
use Tests\IntegrationTestCase;
Expand Down Expand Up @@ -43,7 +44,7 @@ public function test_update()
'index' => 'products',
'body' => [
'query' => [
'match_all' => new \stdClass(),
'match_all' => new stdClass(),
],
],
];
Expand All @@ -66,7 +67,7 @@ public function test_delete()
'index' => 'products',
'body' => [
'query' => [
'match_all' => new \stdClass(),
'match_all' => new stdClass(),
],
],
];
Expand All @@ -88,7 +89,7 @@ public function test_flush()
'index' => 'products',
'body' => [
'query' => [
'match_all' => new \stdClass(),
'match_all' => new stdClass(),
],
],
];
Expand All @@ -101,9 +102,11 @@ public function test_map_with_custom_key_name()
$this->app['config']['scout.key'] = 'custom_key';
$models = Product::all();
$keys = $models->map(function ($product) {
return ['_id' => $product->getScoutKey()];
return ['_id' => $product->getScoutKey(), '_source' => [
'__class_name' => Product::class,
]];
})->all();
$results = ['hits' => ['hits' => $keys]];
$results = ['hits' => ['hits' => $keys, 'total' => $models->count()]];
$mappedModels = $this->engine->map(new Builder(new Product(), 'zonga'), $results, new Product());
$this->assertEquals($models->map->id->all(), $mappedModels->map->id->all());
}
Expand Down
6 changes: 3 additions & 3 deletions tests/Unit/ElasticSearch/Params/BulkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function test_index()
$this->assertEquals([
'body' => [
['index' => ['_index' => 'products', '_type' => '_doc', '_id' => 2]],
['title' => 'Scout', 'id' => 2],
['title' => 'Scout', 'id' => 2, '__class_name' => 'App\Product'],
],
], $params);
}
Expand All @@ -63,7 +63,7 @@ public function test_index_with_custom_key_name()
$this->assertEquals([
'body' => [
['index' => ['_index' => 'products', '_type' => '_doc', '_id' => 'Scout']],
['title' => 'Scout', 'id' => 2],
['title' => 'Scout', 'id' => 2, '__class_name' => 'App\Product'],
],
], $params);
}
Expand All @@ -79,7 +79,7 @@ public function test_push_soft_delete_meta_data()
$this->assertEquals([
'body' => [
['index' => ['_index' => 'products', '_type' => '_doc', '_id' => 2]],
['title' => 'Scout', '__soft_deleted' => 0, 'id' => 2],
['title' => 'Scout', '__soft_deleted' => 0, 'id' => 2, '__class_name' => 'App\Product'],
],
], $params);
}
Expand Down
17 changes: 14 additions & 3 deletions tests/Unit/Engines/ElasticSearchEngineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,20 @@ public function test_pass_query_to_callback_before_executing()
return $query;
});
$engine = new ElasticSearchEngine(app(Client::class));
$engine->map($builder, ['hits' => ['hits' => ['_id' => 1],
['_id' => 2],
]], new Product());
$engine->map($builder, [
'hits' => [
'hits' => [
[
'_id' => 1, '_source' => [
'__class_name' => Product::class,
], ],
[
'_id' => 2, '_source' => [
'__class_name' => Product::class,
], ],
],
'total' => 2,
], ], new Product());
$this->assertTrue($spy->executed);
}
}
Loading

0 comments on commit 064df21

Please sign in to comment.