├── tests ├── Support │ ├── Stubs │ │ ├── NamespaceDummy.php │ │ ├── Foo │ │ │ └── Bar.php │ │ ├── Toy.php │ │ ├── Wife.php │ │ ├── Child.php │ │ └── Husband.php │ └── Dummy.php ├── database │ └── testing.sqlite ├── bootstrap.php └── Iverberk │ └── Larasearch │ ├── Traits │ ├── CallableTraitTest.php │ ├── TransformableTraitTest.php │ └── SearchableTraitTest.php │ ├── Jobs │ ├── DeleteJobTest.php │ └── ReindexJobTest.php │ ├── Response │ ├── RecordsTest.php │ ├── ResultsTest.php │ └── ResultTest.php │ ├── UtilsTest.php │ ├── ObserverTest.php │ ├── Commands │ ├── ReindexCommandTest.php │ └── PathsCommandTest.php │ ├── ResponseTest.php │ ├── LarasearchServiceProviderTest.php │ ├── ProxyTest.php │ └── QueryTest.php ├── .gitignore ├── .travis.yml ├── src ├── Iverberk │ └── Larasearch │ │ ├── Exceptions │ │ └── ImportException.php │ │ ├── Traits │ │ ├── TransformableTrait.php │ │ ├── CallableTrait.php │ │ └── SearchableTrait.php │ │ ├── Response │ │ ├── Records.php │ │ ├── Results.php │ │ └── Result.php │ │ ├── Jobs │ │ ├── DeleteJob.php │ │ └── ReindexJob.php │ │ ├── Utils.php │ │ ├── Commands │ │ ├── ReindexCommand.php │ │ └── PathsCommand.php │ │ ├── Response.php │ │ ├── Observer.php │ │ ├── LarasearchServiceProvider.php │ │ ├── Proxy.php │ │ ├── Query.php │ │ └── Index.php └── config │ └── larasearch.php ├── phpunit.xml ├── composer.json ├── LICENSE └── README.md /tests/Support/Stubs/NamespaceDummy.php: -------------------------------------------------------------------------------- 1 | belongsToMany('Child'); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Exceptions/ImportException.php: -------------------------------------------------------------------------------- 1 | load($relations)->toArray(); 18 | 19 | return $doc; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Traits/CallableTrait.php: -------------------------------------------------------------------------------- 1 | belongsTo('Husband'); 18 | } 19 | 20 | /** 21 | * @return \Illuminate\Database\Eloquent\Relations 22 | */ 23 | public function children() 24 | { 25 | return $this->hasMany('Child', 'mother_id'); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | init([ 14 | 'debug' => true, 15 | 'cacheDir' => '/tmp/larasearch', 16 | 'includePaths' => [$src, $eloquent] 17 | ]); 18 | 19 | // Boot the Eloquent component 20 | 21 | $capsule = new \Illuminate\Database\Capsule\Manager(); 22 | 23 | $capsule->addConnection(array( 24 | 'driver' => 'sqlite', 25 | 'database' => __DIR__ . '/database/testing.sqlite', 26 | 'prefix' => '', 27 | )); 28 | 29 | $capsule->bootEloquent(); 30 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | ./src/Iverberk/Larasearch 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Traits/CallableTraitTest.php: -------------------------------------------------------------------------------- 1 | null]); 20 | 21 | \Husband::bootCallableTrait(); 22 | 23 | // $husband->verifyInvoked('observe'); 24 | } 25 | 26 | /** 27 | * @test 28 | * @expectedException \Exception 29 | */ 30 | public function it_should_boot_callback_trait_and_throw_exception() 31 | { 32 | \Dummy::bootCallableTrait(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/Support/Stubs/Child.php: -------------------------------------------------------------------------------- 1 | belongsTo('Wife'); 17 | } 18 | 19 | /** 20 | * @follow UNLESS Husband 21 | * @follow UNLESS Wife 22 | * 23 | * @return \Illuminate\Database\Eloquent\Relations 24 | */ 25 | public function father() 26 | { 27 | return $this->belongsTo('Husband'); 28 | } 29 | 30 | /** 31 | * @return \Illuminate\Database\Eloquent\Relations 32 | */ 33 | public function toys() 34 | { 35 | return $this->belongsToMany('Toy'); 36 | } 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Response/Records.php: -------------------------------------------------------------------------------- 1 | response = $response; 24 | 25 | $ids = array_map(function ($hit) 26 | { 27 | return $hit['_id']; 28 | }, $this->response->getHits()); 29 | 30 | $model = $response->getModel(); 31 | 32 | parent::__construct($model::whereIn('id', $ids)->get()->toArray()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Response/Results.php: -------------------------------------------------------------------------------- 1 | response = $response; 24 | 25 | parent::__construct( 26 | array_map( 27 | function ($hit) 28 | { 29 | return new Result($hit); 30 | }, 31 | $this->response->getHits() 32 | ) 33 | ); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iverberk/larasearch", 3 | "description": "Elasticsearch enabled Eloquent models", 4 | "keywords": ["search", "elasticsearch", "eloquent"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Ivo Verberk" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.4.0", 13 | "illuminate/support": "~5.0", 14 | "illuminate/database": "~5.0", 15 | "illuminate/console": "~5.0", 16 | "illuminate/config": "~5.0", 17 | "doctrine/dbal": "2.5.1", 18 | "elasticsearch/elasticsearch": "~1.0", 19 | "nikic/php-parser": "*" 20 | }, 21 | "autoload": { 22 | "psr-0": { 23 | "Iverberk\\Larasearch": "src/" 24 | }, 25 | "classmap": [ 26 | "tests/Support/Stubs", 27 | "tests/Support" 28 | ] 29 | }, 30 | "require-dev": { 31 | "mockery/mockery": "dev-master", 32 | "phpunit/phpunit": "4.4.*", 33 | "codeception/aspect-mock": "*" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Traits/TransformableTraitTest.php: -------------------------------------------------------------------------------- 1 | makePartial(); 25 | 26 | /** 27 | * 28 | * Expectation 29 | * 30 | */ 31 | $husband->shouldReceive('load->toArray')->once()->andReturn('mock'); 32 | 33 | Config::shouldReceive('get')->with('/paths\..*/')->once(); 34 | /** 35 | * 36 | * Assertion 37 | * 38 | */ 39 | $transformed = $husband->transform(true); 40 | 41 | $this->assertEquals('mock', $transformed); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ivo Verberk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Support/Stubs/Husband.php: -------------------------------------------------------------------------------- 1 | ['name', 'wife.name'], 11 | 12 | 'suggest' => ['name'], 13 | 14 | 'text_start' => ['name', 'wife.children.name'], 15 | 'text_middle' => ['name', 'wife.children.name'], 16 | 'text_end' => ['name', 'wife.children.name'], 17 | 18 | 'word_start' => ['name', 'wife.children.name'], 19 | 'word_middle' => ['name', 'wife.children.name'], 20 | 'word_end' => ['name', 'wife.children.name'] 21 | ]; 22 | 23 | /** 24 | * @follow UNLESS Toy 25 | * @follow UNLESS Child 26 | * 27 | * @return \Illuminate\Database\Eloquent\Relations 28 | */ 29 | public function wife() 30 | { 31 | return $this->hasOne('Wife'); 32 | } 33 | 34 | /** 35 | * @follow NEVER 36 | * 37 | * @return \Illuminate\Database\Eloquent\Relations 38 | */ 39 | public function children() 40 | { 41 | return $this->hasMany('Child', 'father_id'); 42 | } 43 | 44 | public function getEsId() 45 | { 46 | return 'dummy_id'; 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Jobs/DeleteJob.php: -------------------------------------------------------------------------------- 1 | app = $app; 31 | $this->config = $config; 32 | } 33 | 34 | /** 35 | * @param Job $job 36 | * @param mixed $models 37 | */ 38 | public function fire(Job $job, $models) 39 | { 40 | $loggerContainerBinding = $this->config->get('logger', 'iverberk.larasearch.logger'); 41 | $logger = $this->app->make($loggerContainerBinding); 42 | 43 | foreach ($models as $model) 44 | { 45 | list($class, $id) = explode(':', $model); 46 | 47 | $logger->info('Deleting ' . $class . ' with ID: ' . $id . ' from Elasticsearch'); 48 | 49 | $model = new $class; 50 | 51 | $model->deleteDoc($id); 52 | } 53 | 54 | $job->delete(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Jobs/DeleteJobTest.php: -------------------------------------------------------------------------------- 1 | true]); 28 | 29 | $job = m::mock('Illuminate\Queue\Jobs\Job'); 30 | $models = [ 31 | 'Husband:999' 32 | ]; 33 | 34 | /** 35 | * 36 | * Expectation 37 | * 38 | */ 39 | $logger->shouldReceive('info')->with('Deleting Husband with ID: 999 from Elasticsearch'); 40 | $config->shouldReceive('get')->with('logger', 'iverberk.larasearch.logger')->andReturn('iverberk.larasearch.logger'); 41 | $app->shouldReceive('make')->with('iverberk.larasearch.logger')->andReturn($logger); 42 | $job->shouldReceive('delete')->once(); 43 | 44 | /** 45 | * 46 | * Assertion 47 | * 48 | */ 49 | with(new DeleteJob($app, $config))->fire($job, $models); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Jobs/ReindexJob.php: -------------------------------------------------------------------------------- 1 | app = $app; 32 | $this->config = $config; 33 | } 34 | 35 | public function fire(Job $job, $models) 36 | { 37 | $loggerContainerBinding = $this->config->get('larasearch.logger'); 38 | $logger = $this->app->make($loggerContainerBinding); 39 | 40 | foreach ($models as $model) 41 | { 42 | list($class, $id) = explode(':', $model); 43 | 44 | $logger->info('Indexing ' . $class . ' with ID: ' . $id); 45 | 46 | try 47 | { 48 | $model = $class::findOrFail($id); 49 | $model->refreshDoc($model); 50 | } catch (Exception $e) 51 | { 52 | $logger->error('Indexing ' . $class . ' with ID: ' . $id . ' failed: ' . $e->getMessage()); 53 | 54 | $job->release(60); 55 | } 56 | } 57 | 58 | $job->delete(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Response/RecordsTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('whereIn') 32 | ->andReturnUsing(function ($attribute, $items) use ($test, $husbandMock) 33 | { 34 | $test->assertEquals('id', $attribute); 35 | $test->assertEquals([1, 2], $items); 36 | return $husbandMock; 37 | }); 38 | $husbandMock->shouldReceive('get->toArray') 39 | ->andReturn(['item1', 'item2']); 40 | 41 | $response->shouldReceive('getHits')->andReturn([['_id' => 1], ['_id' => 2]]); 42 | $response->shouldReceive('getModel')->andReturn($husbandMock); 43 | 44 | /** 45 | * 46 | * Assertion 47 | * 48 | */ 49 | $records = new Records($response); 50 | 51 | $this->assertInstanceOf('Illuminate\Support\Collection', $records); 52 | $this->assertEquals('item1', $records->first()); 53 | $this->assertEquals('item2', $records[1]); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Response/ResultsTest.php: -------------------------------------------------------------------------------- 1 | 1, 19 | '_type' => 2, 20 | '_index' => 3, 21 | '_score' => 4, 22 | '_source' => [ 23 | 'id' => 5, 24 | 'foo' => 'bar' 25 | ], 26 | 'fields' => [ 27 | 'field1' => 'value1', 28 | 'field2' => 'value2', 29 | 'nested' => [ 30 | 'nested_field' => 'nested_value' 31 | ] 32 | ], 33 | 'highlight' => [ 34 | 'field3' => 'value3', 35 | 'field4' => 'value4' 36 | ] 37 | ]; 38 | 39 | /** 40 | * 41 | * Set 42 | * 43 | */ 44 | $response = m::mock('Iverberk\Larasearch\Response'); 45 | 46 | /** 47 | * 48 | * Expectation 49 | * 50 | */ 51 | $response->shouldReceive('getHits')->andReturn([$hit, $hit]); 52 | 53 | /** 54 | * 55 | * Assertion 56 | * 57 | */ 58 | $results = new Results($response); 59 | 60 | $this->assertInstanceOf('Illuminate\Support\Collection', $results); 61 | $this->assertInstanceOf('Iverberk\Larasearch\Response\Result', $results->first()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ------------ 3 | 4 | Larasearch is a Laravel package that aims to seamlessly integrate Elasticsearch functionality with the Eloquent ORM. 5 | 6 | Features 7 | -------- 8 | 9 | - Plug 'n Play searching functionality for Eloquent models 10 | - Automatic creation/indexing based on Eloquent model properties and relations 11 | - Aggregations, Suggestions, Autocomplete, Highlighting, etc. It's all there! 12 | - Load Eloquent models based on Elasticsearch queries 13 | - Automatic reindexing on updates of (related) Eloquent models 14 | 15 | Installation 16 | ------------ 17 | 18 | *Laravel 5* 19 | 20 | NB: This is preliminary support. When L5 compatibility is stable I will tag it with a version. 21 | 22 | Add Larasearch to your composer.json file: 23 | 24 | ```"iverberk/larasearch": "dev-L5"``` 25 | 26 | Add the service provider to your Laravel application config: 27 | 28 | ```PHP 29 | 'Iverberk\Larasearch\LarasearchServiceProvider' 30 | ``` 31 | 32 | *Laravel 4* 33 | 34 | Add Larasearch to your composer.json file: 35 | 36 | ```"iverberk/larasearch": "0.8.0"``` 37 | 38 | Add the service provider to your Laravel application config: 39 | 40 | ```PHP 41 | 'Iverberk\Larasearch\LarasearchServiceProvider' 42 | ``` 43 | 44 | 45 | 46 | Wiki 47 | ---- 48 | Please see the Github [wiki](https://github.com/iverberk/larasearch/wiki/Introduction) for the most up-to-date documentation. 49 | 50 | Changelog 51 | --------- 52 | All releases are tracked and documented in the [changelog](https://github.com/iverberk/larasearch/wiki/Changelog). 53 | 54 | Credits 55 | ------- 56 | This package is very much inspired by these excellent packages that already exist for the Ruby/Rails ecosystem. 57 | 58 | * [Searchkick](https://github.com/ankane/searchkick) 59 | * [Elasticsearch Rails](https://github.com/elasticsearch/elasticsearch-rails) 60 | 61 | A lot of their ideas have been reused to work within a PHP/Laravel environment. 62 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/UtilsTest.php: -------------------------------------------------------------------------------- 1 | 'test_value' 10 | ]; 11 | 12 | $this->assertEquals('test_value', Utils::findKey($params, 'test_key')); 13 | $this->assertEquals('test_value', Utils::findKey((object)$params, 'test_key')); 14 | 15 | $this->assertEquals('default_value', Utils::findKey($params, 'bad_key', 'default_value')); 16 | $this->assertEquals('default_value', Utils::findKey((object)$params, 'bad_key', 'default_value')); 17 | 18 | $this->assertEquals(null, Utils::findKey($params, 'bad_key')); 19 | $this->assertEquals(null, Utils::findKey((object)$params, 'bad_key')); 20 | } 21 | 22 | public function testThatArraysAreMergedRecursivelyByOverwritingCommonKeys() 23 | { 24 | $arr1 = [ 25 | 'key1' => 'value1', 26 | 'key2' => 'value2', 27 | 'key3' => [ 28 | 'key4' => 'value4', 29 | 'key5' => 'value5', 30 | 'key6' => [ 31 | 'key7' => 'value7' 32 | ] 33 | ] 34 | ]; 35 | 36 | $arr2 = [ 37 | 'key1' => 'value_override_1', 38 | 'key8' => 'value8', 39 | 'key3' => [ 40 | 'key4' => 'value_override_4', 41 | 'key9' => 'value9', 42 | 'key10' => [ 43 | 'key11' => 'value12' 44 | ] 45 | ] 46 | ]; 47 | 48 | $arr = Utils::array_merge_recursive_distinct($arr1, $arr2); 49 | 50 | $this->assertArrayHasKey('key1', $arr); 51 | $this->assertArrayHasKey('key2', $arr); 52 | $this->assertArrayHasKey('key8', $arr); 53 | 54 | $this->assertEquals('value_override_1', $arr['key1']); 55 | $this->assertEquals('value_override_4', $arr['key3']['key4']); 56 | } 57 | 58 | public function testThatSearchableModelsAreFoundInDirectories() 59 | { 60 | $models = Utils::findSearchableModels(array(__DIR__ . '/../../Support/Stubs')); 61 | 62 | $this->assertContains('Husband', $models); 63 | $this->assertContains('Wife', $models); 64 | $this->assertContains('Toy', $models); 65 | $this->assertContains('Child', $models); 66 | $this->assertContains('House\\Item', $models); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Traits/SearchableTrait.php: -------------------------------------------------------------------------------- 1 | getKey(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Jobs/ReindexJobTest.php: -------------------------------------------------------------------------------- 1 | with('iverberk.larasearch.proxy', Mockery::any()) 28 | ->once() 29 | ->andReturn('mock'); 30 | 31 | $app = m::mock('Illuminate\Foundation\Application'); 32 | $config = m::mock('Illuminate\Config\Repository'); 33 | $logger = m::mock('Monolog\Logger'); 34 | $job = m::mock('Illuminate\Queue\Jobs\Job'); 35 | $models = [ 36 | 'Husband:99999' 37 | ]; 38 | 39 | /** 40 | * 41 | * Expectation 42 | * 43 | */ 44 | $logger->shouldReceive('info')->with('Indexing Husband with ID: 99999'); 45 | $logger->shouldReceive('error')->with('Indexing Husband with ID: 99999 failed: No query results for model [Husband].'); 46 | $config->shouldReceive('get')->with('larasearch.logger')->andReturn('iverberk.larasearch.logger'); 47 | $app->shouldReceive('make')->with('iverberk.larasearch.logger')->andReturn($logger); 48 | $job->shouldReceive('delete')->once(); 49 | $job->shouldReceive('release')->with(60)->once(); 50 | 51 | /** 52 | * 53 | * Assertion 54 | * 55 | */ 56 | with(new ReindexJob($app, $config))->fire($job, $models); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function it_should_fire_job_with_resolvable_models() 63 | { 64 | /** 65 | * 66 | * Set 67 | * 68 | */ 69 | $app = m::mock('Illuminate\Foundation\Application'); 70 | $config = m::mock('Illuminate\Config\Repository'); 71 | $logger = m::mock('Monolog\Logger'); 72 | $model = m::mock('Husband'); 73 | $model->shouldReceive('refreshDoc')->with($model)->once(); 74 | $husband = am::double('Husband', ['findOrFail' => $model]); 75 | $models = [ 76 | 'Husband:999' 77 | ]; 78 | 79 | /** 80 | * 81 | * Expectation 82 | * 83 | */ 84 | $logger->shouldReceive('info')->with('Indexing Husband with ID: 999'); 85 | $config->shouldReceive('get')->with('larasearch.logger')->andReturn('iverberk.larasearch.logger'); 86 | $app->shouldReceive('make')->with('iverberk.larasearch.logger')->andReturn($logger); 87 | $job = m::mock('Illuminate\Queue\Jobs\Job'); 88 | $job->shouldReceive('delete')->once(); 89 | 90 | /** 91 | * 92 | * Assertion 93 | * 94 | */ 95 | with(new ReindexJob($app, $config))->fire($job, $models); 96 | 97 | // $husband->verifyInvoked('findOrFail'); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Utils.php: -------------------------------------------------------------------------------- 1 | &$value) 54 | { 55 | if (is_array($value) && isset ($merged[$key]) && is_array($merged[$key])) 56 | { 57 | $merged[$key] = self::array_merge_recursive_distinct($merged[$key], $value); 58 | } else 59 | { 60 | $merged[$key] = $value; 61 | } 62 | } 63 | 64 | return $merged; 65 | } 66 | 67 | public static function findSearchableModels($directories) 68 | { 69 | $models = []; 70 | $parser = new PHPParser_Parser(new PHPParser_Lexer); 71 | 72 | // Iterate over each directory and inspect files for models 73 | foreach ($directories as $directory) 74 | { 75 | // iterate over all .php files in the directory 76 | $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); 77 | $files = new RegexIterator($files, '/\.php$/'); 78 | 79 | foreach ($files as $file) 80 | { 81 | // read the file that should be converted 82 | $code = file_get_contents($file); 83 | 84 | // parse 85 | $stmts = $parser->parse($code); 86 | 87 | $walk = function ($stmt, $key, $ns) use (&$models, &$walk) 88 | { 89 | if ($stmt instanceof PHPParser_Node_Stmt_Namespace) 90 | { 91 | $new_ns = implode('\\', $stmt->name->parts); 92 | if ($ns && strpos($new_ns, $ns) !== 0) $new_ns = $ns . $new_ns; 93 | array_walk($stmt->stmts, $walk, $new_ns); 94 | } else if ($stmt instanceof PHPParser_Node_Stmt_Class) 95 | { 96 | $class = $stmt->name; 97 | if ($ns) $class = $ns . '\\' . $class; 98 | if (in_array('Iverberk\\Larasearch\\Traits\\SearchableTrait', class_uses($class))) 99 | { 100 | $models[] = $class; 101 | } 102 | } 103 | }; 104 | 105 | array_walk($stmts, $walk, ''); 106 | } 107 | } 108 | 109 | return $models; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Commands/ReindexCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 34 | 35 | foreach ($models as $model) 36 | { 37 | $instance = $this->getModelInstance($model); 38 | $this->reindexModel($instance); 39 | } 40 | 41 | if ($directories = $this->option('dir')) 42 | { 43 | $directoryModels = array_diff(Utils::findSearchableModels($directories), $models); 44 | 45 | foreach ($directoryModels as $model) 46 | { 47 | $instance = $this->getModelInstance($model); 48 | $this->reindexModel($instance); 49 | } 50 | } 51 | 52 | if (empty($models) && empty($directoryModels)) 53 | { 54 | $this->info('No models found.'); 55 | } 56 | } 57 | 58 | /** 59 | * Get the console command arguments. 60 | * 61 | * @return array 62 | */ 63 | protected function getArguments() 64 | { 65 | return array( 66 | array('model', InputOption::VALUE_OPTIONAL, 'Eloquent model to reindex', null) 67 | ); 68 | } 69 | 70 | /** 71 | * Get the console command options. 72 | * 73 | * @return array 74 | */ 75 | protected function getOptions() 76 | { 77 | return array( 78 | array('relations', null, InputOption::VALUE_NONE, 'Reindex related Eloquent models', null), 79 | array('mapping', null, InputOption::VALUE_REQUIRED, 'A file containing custom mappings', null), 80 | array('dir', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Directory to scan for searchable models', null), 81 | array('batch', null, InputOption::VALUE_OPTIONAL, 'The number of records to index in a single batch', 750), 82 | array('force', null, InputOption::VALUE_NONE, 'Overwrite existing indices and documents', null), 83 | ); 84 | } 85 | 86 | /** 87 | * Reindex a model to Elasticsearch 88 | * 89 | * @param Model $model 90 | */ 91 | protected function reindexModel(Model $model) 92 | { 93 | $mapping = $this->option('mapping') ? json_decode(File::get($this->option('mapping')), true) : null; 94 | 95 | $this->info('---> Reindexing ' . get_class($model)); 96 | 97 | $model->reindex( 98 | $this->option('relations'), 99 | $this->option('batch'), 100 | $mapping, 101 | function ($batch) 102 | { 103 | $this->info("* Batch ${batch}"); 104 | } 105 | ); 106 | } 107 | 108 | /** 109 | * Simple method to create instances of classes on the fly 110 | * It's primarily here to enable unit-testing 111 | * 112 | * @param string $model 113 | */ 114 | protected function getModelInstance($model) 115 | { 116 | return new $model; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Response.php: -------------------------------------------------------------------------------- 1 | model = $model; 27 | $this->response = $response; 28 | } 29 | 30 | /** 31 | * @return Model 32 | */ 33 | public function getModel() 34 | { 35 | return $this->model; 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function getResponse() 42 | { 43 | return $this->response; 44 | } 45 | 46 | /** 47 | * @return Results 48 | */ 49 | public function getResults() 50 | { 51 | return new Results($this); 52 | } 53 | 54 | /** 55 | * @return \Illuminate\Database\Eloquent\Collection|static[] 56 | */ 57 | public function getRecords() 58 | { 59 | if (count($this->getHits()) > 0) 60 | { 61 | $ids = array_map(function ($hit) 62 | { 63 | return $hit['_id']; 64 | }, $this->getHits()); 65 | 66 | return call_user_func_array(array($this->model, 'whereIn'), array('id', $ids))->get(); 67 | } else 68 | { 69 | return call_user_func(array($this->model, 'newCollection')); 70 | } 71 | } 72 | 73 | /** 74 | * @return mixed 75 | */ 76 | public function getTook() 77 | { 78 | return $this->response['took']; 79 | } 80 | 81 | /** 82 | * @return mixed 83 | */ 84 | public function getHits() 85 | { 86 | return $this->response['hits']['hits']; 87 | } 88 | 89 | /** 90 | * @return mixed 91 | */ 92 | public function getTimedOut() 93 | { 94 | return $this->response['timed_out']; 95 | } 96 | 97 | /** 98 | * @return mixed 99 | */ 100 | public function getShards() 101 | { 102 | return $this->response['_shards']; 103 | } 104 | 105 | /** 106 | * @return mixed 107 | */ 108 | public function getMaxScore() 109 | { 110 | return $this->response['hits']['max_score']; 111 | } 112 | 113 | /** 114 | * @return mixed 115 | */ 116 | public function getTotal() 117 | { 118 | return $this->response['hits']['total']; 119 | } 120 | 121 | /** 122 | * @param array $fields 123 | * @return mixed 124 | */ 125 | public function getSuggestions($fields = []) 126 | { 127 | if (!empty($fields)) 128 | { 129 | $results = []; 130 | foreach ($fields as $field) 131 | { 132 | foreach ($this->response['suggest'] as $key => $value) 133 | { 134 | if (preg_match("/^${field}.*/", $key) !== false) 135 | { 136 | $results[$field] = $value; 137 | } 138 | } 139 | } 140 | 141 | return $results; 142 | } else 143 | { 144 | return $this->response['suggest']; 145 | } 146 | } 147 | 148 | /** 149 | * @param string $name 150 | * @return array 151 | */ 152 | public function getAggregations($name = '') 153 | { 154 | return empty($name) ? $this->response['aggregations'] : $this->response['aggregations'][$name]; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Traits/SearchableTraitTest.php: -------------------------------------------------------------------------------- 1 | with('iverberk.larasearch.proxy', m::type('Illuminate\Database\Eloquent\Model')) 40 | ->andReturn($proxy); 41 | 42 | /** 43 | * 44 | * 45 | * Assertion 46 | * 47 | */ 48 | $proxy1 = \Husband::getProxy(); 49 | 50 | $this->assertSame($proxy, $proxy1); 51 | 52 | $proxy2 = \Husband::getProxy(); 53 | 54 | $this->assertSame($proxy, $proxy2); 55 | } 56 | 57 | /** 58 | * @test 59 | * @expectedException \Exception 60 | */ 61 | public function it_should_throw_an_exception_if_included_in_a_non_eloquent_model() 62 | { 63 | /** 64 | * 65 | * Set 66 | * 67 | */ 68 | \Dummy::getProxy(); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function it_should_call_methods_on_the_proxy() 75 | { 76 | /** 77 | * 78 | * Set 79 | * 80 | */ 81 | $proxy = m::mock('Iverberk\Larasearch\proxy'); 82 | $husband = am::double('Husband', ['getProxy' => $proxy]); 83 | 84 | /** 85 | * 86 | * Expectation 87 | * 88 | */ 89 | $proxy->shouldReceive('search') 90 | ->with('*') 91 | ->once() 92 | ->andReturn('result_static'); 93 | 94 | $proxy->shouldReceive('search') 95 | ->with('**') 96 | ->once() 97 | ->andReturn('result'); 98 | 99 | /** 100 | * 101 | * Assertion 102 | * 103 | */ 104 | $result = \Husband::search('*'); 105 | 106 | $this->assertEquals('result_static', $result); 107 | 108 | $result = \Husband::search('**'); 109 | 110 | $this->assertEquals('result', $result); 111 | 112 | $husband->verifyInvoked('getProxy'); 113 | } 114 | 115 | /** 116 | * @test 117 | * @expectedException \BadMethodCallException 118 | */ 119 | public function it_should_not_call_methods_on_the_proxy() 120 | { 121 | /** 122 | * 123 | * Expectation 124 | * 125 | */ 126 | App::clearResolvedInstance('app'); 127 | App::shouldReceive('make')->with('Elasticsearch')->andReturn(true); 128 | App::shouldReceive('make')->with('iverberk.larasearch.index', m::type('array'))->andReturn(true); 129 | 130 | /** 131 | * 132 | * Assertion 133 | * 134 | */ 135 | // Overrule the proxy defined in previous tests 136 | am::double('Husband', ['getProxy' => new Proxy(new Husband)]); 137 | 138 | // Call a non existing method 139 | \Husband::bogus('*'); 140 | } 141 | 142 | /** 143 | * @test 144 | */ 145 | public function it_should_get_elasticsearch_id() 146 | { 147 | /** 148 | * Assertions 149 | */ 150 | $husband = new Husband(); 151 | 152 | $this->assertEquals('dummy_id', $husband->getEsId()); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Observer.php: -------------------------------------------------------------------------------- 1 | getKey()]); 19 | 20 | // Update all related model documents to reflect that $model has been removed 21 | Queue::push('Iverberk\Larasearch\Jobs\ReindexJob', $this->findAffectedModels($model, true)); 22 | } 23 | 24 | /** 25 | * Model save event handler 26 | * 27 | * @param Model $model 28 | */ 29 | public function saved(Model $model) 30 | { 31 | if ($model::$__es_enable && $model->shouldIndex()) 32 | { 33 | Queue::push('Iverberk\Larasearch\Jobs\ReindexJob', $this->findAffectedModels($model)); 34 | } 35 | } 36 | 37 | /** 38 | * Find all searchable models that are affected by the model change 39 | * 40 | * @param Model $model 41 | * @return array 42 | */ 43 | private function findAffectedModels(Model $model, $excludeCurrent = false) 44 | { 45 | // Temporary array to store affected models 46 | $affectedModels = []; 47 | 48 | $paths = Config::get('larasearch.reversedPaths.' . get_class($model), []); 49 | 50 | foreach ((array)$paths as $path) 51 | { 52 | if (!empty($path)) 53 | { 54 | $model = $model->load($path); 55 | 56 | // Explode the path into an array 57 | $path = explode('.', $path); 58 | 59 | // Define a little recursive function to walk the relations of the model based on the path 60 | // Eventually it will queue all affected searchable models for reindexing 61 | $walk = function ($relation, array $path) use (&$walk, &$affectedModels) 62 | { 63 | $segment = array_shift($path); 64 | 65 | $relation = $relation instanceof Collection ? $relation : new Collection([$relation]); 66 | 67 | foreach ($relation as $record) 68 | { 69 | if ($record instanceof Model) 70 | { 71 | if (!empty($segment)) 72 | { 73 | if (array_key_exists($segment, $record->getRelations())) 74 | { 75 | $walk($record->getRelation($segment), $path); 76 | } else 77 | { 78 | // Apparently the relation doesn't exist on this model, so skip the rest of the path as well 79 | return; 80 | } 81 | } else 82 | { 83 | if (in_array('Iverberk\Larasearch\Traits\SearchableTrait', class_uses($record))) 84 | { 85 | $affectedModels[] = get_class($record) . ':' . $record->getKey(); 86 | } 87 | } 88 | } 89 | } 90 | }; 91 | 92 | $walk($model->getRelation(array_shift($path)), $path); 93 | } else if (!$excludeCurrent) 94 | { 95 | if (in_array('Iverberk\Larasearch\Traits\SearchableTrait', class_uses($model))) 96 | { 97 | $affectedModels[] = get_class($model) . ':' . $model->getKey(); 98 | } 99 | } 100 | } 101 | 102 | return array_unique($affectedModels); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Response/ResultTest.php: -------------------------------------------------------------------------------- 1 | hit = [ 19 | '_id' => 1, 20 | '_type' => 2, 21 | '_index' => 3, 22 | '_score' => 4, 23 | '_source' => [ 24 | 'id' => 5, 25 | 'foo' => 'bar' 26 | ], 27 | 'fields' => [ 28 | 'field1' => 'value1', 29 | 'field2' => 'value2', 30 | 'nested' => [ 31 | 'nested_field' => 'nested_value' 32 | ] 33 | ], 34 | 'highlight' => [ 35 | 'field3' => 'value3', 36 | 'field4' => 'value4' 37 | ] 38 | ]; 39 | 40 | $result = new Result($this->hit); 41 | 42 | $this->result = m::mock($result); 43 | } 44 | 45 | protected function tearDown() 46 | { 47 | m::close(); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function it_should_get_id() 54 | { 55 | $this->assertEquals(1, $this->result->getId()); 56 | } 57 | 58 | /** 59 | * @test 60 | */ 61 | public function it_should_get_type() 62 | { 63 | $this->assertEquals(2, $this->result->getType()); 64 | } 65 | 66 | /** 67 | * @test 68 | */ 69 | public function it_should_get_index() 70 | { 71 | $this->assertEquals(3, $this->result->getIndex()); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function it_should_get_score() 78 | { 79 | $this->assertEquals(4, $this->result->getScore()); 80 | } 81 | 82 | /** 83 | * @test 84 | */ 85 | public function it_should_get_source() 86 | { 87 | $this->assertEquals(['id' => 5, 'foo' => 'bar'], $this->result->getSource()); 88 | } 89 | 90 | /** 91 | * @test 92 | */ 93 | public function it_should_get_fields() 94 | { 95 | $this->assertEquals(['field1' => 'value1', 'field2' => 'value2', 'nested' => ['nested_field' => 'nested_value']], $this->result->getFields()); 96 | $this->assertEquals(['field1' => 'value1'], $this->result->getFields(['field1'])); 97 | $this->assertEquals(['field1' => 'value1', 'field2' => 'value2'], $this->result->getFields(['field1', 'field2'])); 98 | } 99 | 100 | /** 101 | * @test 102 | */ 103 | public function it_should_get_hit() 104 | { 105 | $this->assertEquals($this->hit, $this->result->getHit()); 106 | } 107 | 108 | /** 109 | * @test 110 | */ 111 | public function it_should_get_highlights() 112 | { 113 | $this->assertEquals($this->hit['highlight'], $this->result->getHighLights()); 114 | $this->assertEquals(['field3' => 'value3'], $this->result->getHighLights(['field3'])); 115 | $this->assertEquals(['field3' => 'value3', 'field4' => 'value4'], $this->result->getHighLights(['field3', 'field4'])); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function it_should_get_attributes_from_hit() 122 | { 123 | $result = new Result($this->hit); 124 | 125 | $this->assertEquals('bar', $result->foo); 126 | $this->assertEquals('nested_value', $result['fields.nested.nested_field']); 127 | } 128 | 129 | /** 130 | * @test 131 | */ 132 | public function it_should_convert_to_array() 133 | { 134 | $this->assertEquals([ 135 | 'field1' => 'value1', 136 | 'field2' => 'value2', 137 | 'nested' => [ 138 | 'nested_field' => 'nested_value' 139 | ] 140 | ], 141 | $this->result->toArray()); 142 | } 143 | 144 | /** 145 | * The set and unset function will never be implemented 146 | * but who doesn't like 100% test coverage ;-) 147 | * 148 | * @test 149 | */ 150 | public function it_should_cover_hundred_procent() 151 | { 152 | $this->result['foo'] = 'bar'; 153 | unset($this->result['foo']); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/ObserverTest.php: -------------------------------------------------------------------------------- 1 | null]); 30 | 31 | $husband = m::mock('Husband'); 32 | $husband->shouldReceive('shouldIndex')->andReturn(false); 33 | 34 | /** 35 | * 36 | * 37 | * Assertion 38 | * 39 | */ 40 | with(new Observer)->saved($husband); 41 | 42 | $queue->verifyNeverInvoked('push'); 43 | } 44 | 45 | /** 46 | * @test 47 | */ 48 | public function it_should_reindex_on_model_save() 49 | { 50 | /** 51 | * 52 | * Expectation 53 | * 54 | */ 55 | Facade::clearResolvedInstances(); 56 | 57 | $proxy = m::mock('Iverberk\Larasearch\Proxy'); 58 | $proxy->shouldReceive('shouldIndex')->andReturn(true); 59 | 60 | App::shouldReceive('make') 61 | ->with('iverberk.larasearch.proxy', m::type('Illuminate\Database\Eloquent\Model')) 62 | ->andReturn($proxy); 63 | 64 | Config::shouldReceive('get') 65 | ->with('larasearch.reversedPaths.Husband', []) 66 | ->once() 67 | ->andReturn(['', 'wife', 'children', 'children.toys']); 68 | 69 | Queue::shouldReceive('push') 70 | ->with('Iverberk\Larasearch\Jobs\ReindexJob', [ 71 | 'Husband:2', 72 | 'Wife:2', 73 | 'Child:2', 74 | 'Toy:2' 75 | ])->once(); 76 | 77 | /** 78 | * 79 | * 80 | * Assertion 81 | * 82 | */ 83 | $husband = \Husband::find(2); 84 | $husband->clearProxy(); 85 | 86 | with(new Observer)->saved($husband); 87 | 88 | /** 89 | * 90 | * Expectation 91 | * 92 | */ 93 | Facade::clearResolvedInstances(); 94 | 95 | $proxy = m::mock('Iverberk\Larasearch\Proxy'); 96 | $proxy->shouldReceive('shouldIndex')->andReturn(true); 97 | 98 | App::shouldReceive('make') 99 | ->with('iverberk.larasearch.proxy', m::type('Illuminate\Database\Eloquent\Model')) 100 | ->andReturn($proxy); 101 | 102 | Config::shouldReceive('get') 103 | ->with('larasearch.reversedPaths.Toy', []) 104 | ->once() 105 | ->andReturn(['', 'children', 'children.mother.husband', 'children.mother']); 106 | 107 | Queue::shouldReceive('push') 108 | ->with('Iverberk\Larasearch\Jobs\ReindexJob', [ 109 | 'Toy:2', 110 | 'Child:8', 111 | 'Child:2', 112 | 'Husband:8', 113 | 'Husband:2', 114 | 'Wife:8', 115 | 'Wife:2' 116 | ])->once(); 117 | 118 | /** 119 | * 120 | * 121 | * Assertion 122 | * 123 | */ 124 | $toy = \Toy::find(2); 125 | 126 | with(new Observer)->saved($toy); 127 | } 128 | 129 | /** 130 | * @test 131 | */ 132 | public function it_should_reindex_on_model_delete() 133 | { 134 | /** 135 | * 136 | * Expectation 137 | * 138 | */ 139 | Facade::clearResolvedInstances(); 140 | 141 | Queue::shouldReceive('push') 142 | ->with('Iverberk\Larasearch\Jobs\DeleteJob', ['Husband:2']) 143 | ->once(); 144 | 145 | Queue::shouldReceive('push') 146 | ->with('Iverberk\Larasearch\Jobs\ReindexJob', ['Wife:2', 'Child:2', 'Toy:2']) 147 | ->once(); 148 | 149 | Config::shouldReceive('get') 150 | ->with('/^larasearch.reversedPaths\..*$/', []) 151 | ->once() 152 | ->andReturn(['', 'wife', 'children', 'children.toys']); 153 | 154 | $husband = \Husband::find(2); 155 | 156 | with(new Observer)->deleted($husband); 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/LarasearchServiceProvider.php: -------------------------------------------------------------------------------- 1 | bootContainerBindings(); 24 | 25 | $this->publishes([ 26 | __DIR__ . '/../../config/larasearch.php' => base_path('config/larasearch.php'), 27 | ], 'config'); 28 | } 29 | 30 | /** 31 | * Register the service provider. 32 | * 33 | * @return void 34 | */ 35 | public function register() 36 | { 37 | $this->registerCommands(); 38 | 39 | if (file_exists(base_path('config/larasearch.php'))) 40 | { 41 | $this->mergeConfigFrom(base_path('config/larasearch.php'), 'larasearch'); 42 | } 43 | else 44 | { 45 | $this->mergeConfigFrom(__DIR__ . '/../../config/larasearch.php', 'larasearch'); 46 | } 47 | } 48 | 49 | /** 50 | * Boot the container bindings. 51 | * 52 | * @return void 53 | */ 54 | public function bootContainerBindings() 55 | { 56 | $this->bindElasticsearch(); 57 | $this->bindLogger(); 58 | $this->bindIndex(); 59 | $this->bindQuery(); 60 | $this->bindProxy(); 61 | $this->bindResult(); 62 | } 63 | 64 | /** 65 | * Bind a Larasearch log handler to the container 66 | */ 67 | protected function bindLogger() 68 | { 69 | $this->app->singleton('iverberk.larasearch.logger', function ($app) 70 | { 71 | return new Logger('larasearch', [new NullHandler()]); 72 | }); 73 | } 74 | 75 | /** 76 | * Bind the Elasticsearch client to the container 77 | */ 78 | protected function bindElasticsearch() 79 | { 80 | $this->app->singleton('Elasticsearch', function ($app) 81 | { 82 | return new Client(\Illuminate\Support\Facades\Config::get('larasearch.elasticsearch.params')); 83 | }); 84 | } 85 | 86 | /** 87 | * Bind the Larasearch index to the container 88 | */ 89 | protected function bindIndex() 90 | { 91 | $this->app->bind('iverberk.larasearch.index', function ($app, $params) 92 | { 93 | $name = isset($params['name']) ? $params['name'] : ''; 94 | 95 | return new Index($params['proxy'], $name); 96 | }); 97 | } 98 | 99 | /** 100 | * Bind the Larasearch Query to the container 101 | */ 102 | protected function bindQuery() 103 | { 104 | $this->app->bind('iverberk.larasearch.query', function ($app, $params) 105 | { 106 | return new Query($params['proxy'], $params['term'], $params['options']); 107 | }); 108 | } 109 | 110 | /** 111 | * Bind the Larasearch proxy to the container 112 | */ 113 | protected function bindProxy() 114 | { 115 | $this->app->bind('iverberk.larasearch.proxy', function ($app, $model) 116 | { 117 | return new Proxy($model); 118 | }); 119 | } 120 | 121 | /** 122 | * Bind the Larasearch result to the container 123 | */ 124 | protected function bindResult() 125 | { 126 | $this->app->bind('iverberk.larasearch.response.result', function ($app, array $hit) 127 | { 128 | return new Result($hit); 129 | }); 130 | } 131 | 132 | /** 133 | * Register the commands. 134 | * 135 | * @return void 136 | */ 137 | protected function registerCommands() 138 | { 139 | $this->app['iverberk.larasearch.commands.reindex'] = $this->app->share(function ($app) 140 | { 141 | return new ReindexCommand(); 142 | }); 143 | 144 | $this->app['iverberk.larasearch.commands.paths'] = $this->app->share(function ($app) 145 | { 146 | return new PathsCommand(); 147 | }); 148 | 149 | $this->commands('iverberk.larasearch.commands.reindex'); 150 | $this->commands('iverberk.larasearch.commands.paths'); 151 | } 152 | 153 | /** 154 | * Get the services provided by the provider. 155 | * 156 | * @return array 157 | */ 158 | public function provides() 159 | { 160 | return array(); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Commands/ReindexCommandTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($options, $command->getOptions()); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function it_should_get_arguments() 47 | { 48 | /** 49 | * 50 | * Set 51 | * 52 | **/ 53 | $command = m::mock('Iverberk\Larasearch\Commands\ReindexCommand'); 54 | $arguments = array( 55 | array('model', InputOption::VALUE_OPTIONAL, 'Eloquent model to reindex', null) 56 | ); 57 | 58 | /** 59 | * 60 | * Assertion 61 | * 62 | **/ 63 | $this->assertEquals($arguments, $command->getArguments()); 64 | } 65 | 66 | /** 67 | * @test 68 | */ 69 | public function it_should_fire_without_models() 70 | { 71 | /** 72 | * 73 | * Set 74 | * 75 | **/ 76 | /* @var \Mockery\Mock $command */ 77 | $command = m::mock('Iverberk\Larasearch\Commands\ReindexCommand')->makePartial(); 78 | 79 | /** 80 | * 81 | * Expectation 82 | * 83 | */ 84 | $command->shouldReceive('argument') 85 | ->with('model') 86 | ->once() 87 | ->andReturn([]); 88 | 89 | $command->shouldReceive('option') 90 | ->with('dir') 91 | ->once() 92 | ->andReturn([]); 93 | 94 | $command->shouldReceive('info') 95 | ->once() 96 | ->andReturn(true); 97 | 98 | /** 99 | * 100 | * Assertion 101 | * 102 | */ 103 | $command->fire(); 104 | } 105 | 106 | /** 107 | * @test 108 | */ 109 | public function it_should_fire_with_models() 110 | { 111 | /** 112 | * 113 | * Set 114 | * 115 | */ 116 | /* @var \Mockery\Mock $command */ 117 | $command = m::mock('Iverberk\Larasearch\Commands\ReindexCommand')->makePartial(); 118 | $command->shouldAllowMockingProtectedMethods(); 119 | 120 | $model = m::mock('Husband'); 121 | 122 | /** 123 | * 124 | * Expectation 125 | * 126 | */ 127 | $model->shouldReceive('reindex') 128 | ->with(true, 750, null, \Mockery::type('closure')) 129 | ->times(5) 130 | ->andReturnUsing(function ($relations, $batch, $mapping, $callback) 131 | { 132 | $callback(1); 133 | }); 134 | 135 | $command->shouldReceive('argument') 136 | ->with('model') 137 | ->once() 138 | ->andReturn(['Husband']); 139 | 140 | $command->shouldReceive('option') 141 | ->with('dir') 142 | ->once() 143 | ->andReturn([__DIR__ . '/../../../Support/Stubs']); 144 | 145 | $command->shouldReceive('option') 146 | ->with('mapping') 147 | ->times(5) 148 | ->andReturn(false); 149 | 150 | $command->shouldReceive('option') 151 | ->with('relations') 152 | ->times(5) 153 | ->andReturn(true); 154 | 155 | $command->shouldReceive('option') 156 | ->with('batch') 157 | ->times(5) 158 | ->andReturn(750); 159 | 160 | $command->shouldReceive('info')->andReturn(true); 161 | 162 | $command->shouldReceive('getModelInstance')->times(5)->andReturn($model); 163 | 164 | /** 165 | * 166 | * Assertion 167 | * 168 | */ 169 | $command->fire(); 170 | } 171 | 172 | /** 173 | * @test 174 | */ 175 | public function it_should_get_a_model_instance() 176 | { 177 | /** 178 | * 179 | * Set 180 | * 181 | */ 182 | $command = m::mock('Iverberk\Larasearch\Commands\ReindexCommand')->makePartial(); 183 | $command->shouldAllowMockingProtectedMethods(); 184 | 185 | /** 186 | * 187 | * Assertion 188 | * 189 | */ 190 | $model = $command->getModelInstance('Husband'); 191 | 192 | $this->assertInstanceOf('Husband', $model); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Response/Result.php: -------------------------------------------------------------------------------- 1 | hit = $hit; 24 | } 25 | 26 | /** 27 | * Return the hit id 28 | * 29 | * @acccess public 30 | * @return integer 31 | */ 32 | public function getId() 33 | { 34 | return (int)$this->hit['_id']; 35 | } 36 | 37 | /** 38 | * Return the hit document type 39 | * 40 | * @access public 41 | * @return string 42 | */ 43 | public function getType() 44 | { 45 | return $this->hit['_type']; 46 | } 47 | 48 | /** 49 | * Return the hit index 50 | * 51 | * @access public 52 | * @return string 53 | */ 54 | public function getIndex() 55 | { 56 | return $this->hit['_index']; 57 | } 58 | 59 | /** 60 | * Return the hit score 61 | * 62 | * @access public 63 | * @return float 64 | */ 65 | public function getScore() 66 | { 67 | return (float)$this->hit['_score']; 68 | } 69 | 70 | /** 71 | * Return the _source object 72 | * 73 | * @access public 74 | * @return array 75 | */ 76 | public function getSource() 77 | { 78 | return $this->hit['_source']; 79 | } 80 | 81 | /** 82 | * @param array $fields 83 | * @return array 84 | */ 85 | public function getFields($fields = []) 86 | { 87 | $results = []; 88 | foreach ($fields as $field) 89 | { 90 | $results[$field] = $this->hit['fields'][$field]; 91 | } 92 | 93 | return empty($fields) ? $this->hit['fields'] : $results; 94 | } 95 | 96 | /** 97 | * Return the hit object 98 | * 99 | * @access public 100 | * @return array 101 | */ 102 | public function getHit() 103 | { 104 | return $this->hit; 105 | } 106 | 107 | /** 108 | * @param array $fields 109 | * @return array 110 | */ 111 | public function getHighlights($fields = []) 112 | { 113 | if (!empty($fields)) 114 | { 115 | $results = []; 116 | foreach ($fields as $field) 117 | { 118 | foreach ($this->hit['highlight'] as $key => $value) 119 | { 120 | if (preg_match("/^${field}.*/", $key) === 1) 121 | { 122 | $results[$field] = $value; 123 | } 124 | } 125 | } 126 | 127 | return $results; 128 | } else 129 | { 130 | return $this->hit['highlight']; 131 | } 132 | } 133 | 134 | /** 135 | * Get data by key 136 | * 137 | * @param string The key data to retrieve 138 | * @return mixed 139 | * @access public 140 | */ 141 | public function __get($key) 142 | { 143 | $item = array_get($this->hit, $this->getPath($key)); 144 | 145 | return $item; 146 | } 147 | 148 | /** 149 | * Whether or not an offset exists 150 | * 151 | * @param mixed $offset 152 | * @access public 153 | * @return boolean 154 | * @abstracting ArrayAccess 155 | */ 156 | public function offsetExists($offset) 157 | { 158 | return (array_get($this->hit, $this->getPath($offset)) !== null); 159 | } 160 | 161 | /** 162 | * Returns the value at specified offset 163 | * 164 | * @param mixed $offset 165 | * @access public 166 | * @return mixed 167 | * @abstracting ArrayAccess 168 | */ 169 | public function offsetGet($offset) 170 | { 171 | return $this->offsetExists($offset) ? array_get($this->hit, $this->getPath($offset)) : null; 172 | } 173 | 174 | /** 175 | * Assigns a value to the specified offset 176 | * 177 | * @param mixed $offset 178 | * @param mixed $value 179 | * @access public 180 | * @abstracting ArrayAccess 181 | */ 182 | public function offsetSet($offset, $value) 183 | { 184 | // Not allowed for Elasticsearch responses, update the Eloquent model instead. 185 | } 186 | 187 | /** 188 | * Unsets an offset 189 | * 190 | * @param mixed $offset 191 | * @access public 192 | * @abstracting ArrayAccess 193 | */ 194 | public function offsetUnset($offset) 195 | { 196 | // Not allowed for Elasticsearch responses, update the Eloquent model instead. 197 | } 198 | 199 | /** 200 | * Check if the $offset parameter contains a dot and return the appropriate path 201 | * in the array 202 | * 203 | * @access private 204 | * @param $offset 205 | * @return string 206 | */ 207 | private function getPath($offset) 208 | { 209 | return (strpos($offset, '.') !== false) ? $path = $offset : "_source.${offset}"; 210 | } 211 | 212 | /** 213 | * Get the instance as an array. 214 | * 215 | * @return array 216 | */ 217 | public function toArray() 218 | { 219 | return isset($this->hit['fields']) ? $this->hit['fields'] : $this->hit['_source']; 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Proxy.php: -------------------------------------------------------------------------------- 1 | config = property_exists($class, '__es_config') ? $class::$__es_config : []; 24 | 25 | $this->config['model'] = $model; 26 | $this->config['type'] = str_singular($model->getTable()); 27 | 28 | $this->config['client'] = App::make('Elasticsearch'); 29 | $this->config['index'] = App::make('iverberk.larasearch.index', array('proxy' => $this)); 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function getConfig() 36 | { 37 | return $this->config; 38 | } 39 | 40 | /** 41 | * @return Model 42 | */ 43 | public function getModel() 44 | { 45 | return $this->config['model']; 46 | } 47 | 48 | /** 49 | * @return Index 50 | */ 51 | public function getIndex() 52 | { 53 | return $this->config['index']; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function getType() 60 | { 61 | return $this->config['type']; 62 | } 63 | 64 | /** 65 | * @return \Elasticsearch\Client 66 | */ 67 | public function getClient() 68 | { 69 | return $this->config['client']; 70 | } 71 | 72 | /** 73 | * @param $term 74 | * @param array $options 75 | * @return \Iverberk\Larasearch\Response 76 | */ 77 | public function search($term, $options = []) 78 | { 79 | return App::make('iverberk.larasearch.query', ['proxy' => $this, 'term' => $term, 'options' => $options])->execute(); 80 | } 81 | 82 | /** 83 | * Performs a search based on a custom Elasticsearch query 84 | * 85 | * @param array $query 86 | * @param array $options 87 | * @return \Iverberk\Larasearch\Response 88 | */ 89 | public function searchByQuery($query, $options = []) 90 | { 91 | $options = array_merge(['query' => $query], $options); 92 | 93 | return App::make('iverberk.larasearch.query', ['proxy' => $this, 'term' => null, 'options' => $options])->execute(); 94 | } 95 | 96 | /** 97 | * Retrieves a single document by identifier 98 | * 99 | * @param $id 100 | * @return Result 101 | */ 102 | public function searchById($id) 103 | { 104 | return App::make('iverberk.larasearch.response.result', $this->config['client']->get( 105 | [ 106 | 'index' => $this->getIndex()->getName(), 107 | 'type' => $this->getType(), 108 | 'id' => $id 109 | ] 110 | ) 111 | ); 112 | } 113 | 114 | /** 115 | * @param bool $relations 116 | * @param int $batchSize 117 | * @param array $mapping 118 | * @param callable $callback 119 | * @internal param bool $force 120 | * @internal param array $params 121 | */ 122 | public function reindex($relations = false, $batchSize = 750, $mapping = [], Callable $callback = null) 123 | { 124 | $model = $this->config['model']; 125 | $name = $this->config['index']->getName(); 126 | 127 | $newName = $name . '_' . date("YmdHis"); 128 | $relations = $relations ? Config::get('larasearch.paths.' . get_class($model)) : []; 129 | 130 | Index::clean($name); 131 | 132 | $index = App::make('iverberk.larasearch.index', array('name' => $newName, 'proxy' => $this)); 133 | $index->create($mapping); 134 | 135 | if ($index->aliasExists($name)) 136 | { 137 | $index->import($model, $relations, $batchSize, $callback); 138 | $remove = []; 139 | 140 | foreach (Index::getAlias($name) as $index => $aliases) 141 | { 142 | $remove = [ 143 | 'remove' => [ 144 | 'index' => $index, 145 | 'alias' => $name 146 | ] 147 | ]; 148 | } 149 | 150 | $add = [ 151 | 'add' => [ 152 | 'index' => $newName, 153 | 'alias' => $name 154 | ] 155 | ]; 156 | 157 | $actions[] = array_merge($remove, $add); 158 | 159 | Index::updateAliases(['actions' => $actions]); 160 | Index::clean($name); 161 | } else 162 | { 163 | if ($this->config['index']->exists()) $this->config['index']->delete(); 164 | 165 | $actions[] = 166 | [ 167 | 'add' => [ 168 | 'index' => $newName, 169 | 'alias' => $name 170 | ] 171 | ]; 172 | 173 | Index::updateAliases([ 174 | 'actions' => $actions 175 | ]); 176 | 177 | $index->import($model, $relations, $batchSize, $callback); 178 | } 179 | 180 | Index::refresh($name); 181 | } 182 | 183 | /** 184 | * Determine if the model requires a (re)index. Defaults to 'true' but can 185 | * be overridden by user-defined logic. 186 | * 187 | * @return bool 188 | */ 189 | public function shouldIndex() 190 | { 191 | return true; 192 | } 193 | 194 | /** 195 | * Reindex a specific database record to Elasticsearch 196 | */ 197 | public function refreshDoc($model) 198 | { 199 | $this->config['client']->index( 200 | [ 201 | 'id' => $model->getEsId(), 202 | 'index' => $this->getIndex()->getName(), 203 | 'type' => $this->getType(), 204 | 'body' => $model->transform(true) 205 | ] 206 | ); 207 | } 208 | 209 | /** 210 | * Delete a specific database record within Elasticsearch 211 | * 212 | * @param $id Eloquent id of model object 213 | */ 214 | public function deleteDoc($id) 215 | { 216 | $this->config['client']->delete( 217 | [ 218 | 'id' => $id, 219 | 'index' => $this->getIndex()->getName(), 220 | 'type' => $this->getType() 221 | ] 222 | ); 223 | } 224 | 225 | /** 226 | * Globally enable (re)indexing for this model 227 | */ 228 | public function enableIndexing() 229 | { 230 | $class = get_class($this->config['model']); 231 | 232 | $class::$__es_enable = true; 233 | } 234 | 235 | /** 236 | * Globally disable (re)indexing for this model 237 | */ 238 | public function disableIndexing() 239 | { 240 | $class = get_class($this->config['model']); 241 | 242 | $class::$__es_enable = false; 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/ResponseTest.php: -------------------------------------------------------------------------------- 1 | 'took', 9 | 'timed_out' => 'timed_out', 10 | '_shards' => 'shards', 11 | 'suggest' => [ 12 | 'key_dummy' => 'value_dummy' 13 | ], 14 | 'aggregations' => [ 15 | 'named_aggregation' => 'named_aggregation' 16 | ], 17 | 'hits' => [ 18 | 'total' => 'total', 19 | 'max_score' => 'max_score', 20 | 'hits' => [ 21 | ['_id' => '1'] 22 | ] 23 | ] 24 | 25 | ]; 26 | 27 | protected function tearDown() 28 | { 29 | m::close(); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function it_should_get_model() 36 | { 37 | /** 38 | * 39 | * Set 40 | * 41 | */ 42 | list($response, $model) = $this->getMocks(); 43 | 44 | /** 45 | * 46 | * Assertion 47 | * 48 | */ 49 | $this->assertEquals($model, $response->getModel()); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function it_should_get_response() 56 | { 57 | /** 58 | * 59 | * Set 60 | * 61 | */ 62 | list($response, $model) = $this->getMocks(); 63 | 64 | /** 65 | * 66 | * Assertion 67 | * 68 | */ 69 | $this->assertEquals( 70 | $this->responseFixture, 71 | $response->getResponse()); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function it_should_get_results() 78 | { 79 | /** 80 | * 81 | * Set 82 | * 83 | */ 84 | list($response, $model) = $this->getMocks(); 85 | 86 | /** 87 | * 88 | * Assertion 89 | * 90 | */ 91 | $results = $response->getResults(); 92 | 93 | $this->assertInstanceOf('Iverberk\Larasearch\Response\Results', $results); 94 | $this->assertEquals(1, $results->first()->getId()); 95 | } 96 | 97 | /** 98 | * @test 99 | */ 100 | public function it_should_get_records() 101 | { 102 | /** 103 | * 104 | * Set 105 | * 106 | * @var \Mockery\Mock $model 107 | */ 108 | list($response, $model) = $this->getMocks(); 109 | 110 | /** 111 | * 112 | * Expectation 113 | * 114 | */ 115 | $model->shouldReceive('get')->andReturn('succes'); 116 | $model->shouldReceive('whereIn') 117 | ->with('id', [1]) 118 | ->andReturn($model); 119 | 120 | /** 121 | * 122 | * Assertion 123 | * 124 | */ 125 | $records = $response->getRecords(); 126 | 127 | $this->assertEquals('succes', $records); 128 | } 129 | 130 | /** 131 | * @test 132 | */ 133 | public function it_should_get_took() 134 | { 135 | /** 136 | * 137 | * Set 138 | * 139 | */ 140 | list($response, $model) = $this->getMocks(); 141 | 142 | /** 143 | * 144 | * Assertion 145 | * 146 | */ 147 | $this->assertEquals('took', $response->getTook()); 148 | } 149 | 150 | /** 151 | * @test 152 | */ 153 | public function it_should_get_hits() 154 | { 155 | /** 156 | * 157 | * Set 158 | * 159 | */ 160 | list($response, $model) = $this->getMocks(); 161 | 162 | /** 163 | * 164 | * Assertion 165 | * 166 | */ 167 | $this->assertEquals([['_id' => 1]], $response->getHits()); 168 | } 169 | 170 | /** 171 | * @test 172 | */ 173 | public function it_should_get_timed_out() 174 | { 175 | /** 176 | * 177 | * Set 178 | * 179 | */ 180 | list($response, $model) = $this->getMocks(); 181 | 182 | /** 183 | * 184 | * Assertion 185 | * 186 | */ 187 | $this->assertEquals('timed_out', $response->getTimedOut()); 188 | } 189 | 190 | /** 191 | * @test 192 | */ 193 | public function it_should_get_shards() 194 | { 195 | /** 196 | * 197 | * Set 198 | * 199 | */ 200 | list($response, $model) = $this->getMocks(); 201 | 202 | /** 203 | * 204 | * Assertion 205 | * 206 | */ 207 | $this->assertEquals('shards', $response->getShards()); 208 | } 209 | 210 | /** 211 | * @test 212 | */ 213 | public function it_should_get_max_score() 214 | { 215 | /** 216 | * 217 | * Set 218 | * 219 | */ 220 | list($response, $model) = $this->getMocks(); 221 | 222 | /** 223 | * 224 | * Assertion 225 | * 226 | */ 227 | $this->assertEquals('max_score', $response->getMaxScore()); 228 | } 229 | 230 | /** 231 | * @test 232 | */ 233 | public function it_should_get_total() 234 | { 235 | /** 236 | * 237 | * Set 238 | * 239 | */ 240 | list($response, $model) = $this->getMocks(); 241 | 242 | /** 243 | * 244 | * Assertion 245 | * 246 | */ 247 | $this->assertEquals('total', $response->getTotal()); 248 | } 249 | 250 | /** 251 | * @test 252 | */ 253 | public function it_should_get_suggestions_with_fields() 254 | { 255 | /** 256 | * 257 | * Set 258 | * 259 | */ 260 | list($response, $model) = $this->getMocks(); 261 | 262 | /** 263 | * 264 | * Assertion 265 | * 266 | */ 267 | $suggestions = $response->getSuggestions(['key']); 268 | 269 | $this->assertEquals(['key' => 'value_dummy'], $suggestions); 270 | } 271 | 272 | /** 273 | * @test 274 | */ 275 | public function it_should_get_suggestions_without_fields() 276 | { 277 | /** 278 | * 279 | * Set 280 | * 281 | */ 282 | list($response, $model) = $this->getMocks(); 283 | 284 | /** 285 | * 286 | * Assertion 287 | * 288 | */ 289 | $suggestions = $response->getSuggestions(); 290 | 291 | $this->assertEquals(['key_dummy' => 'value_dummy'], $suggestions); 292 | } 293 | 294 | /** 295 | * @test 296 | */ 297 | public function it_should_get_aggregations_with_name() 298 | { 299 | /** 300 | * 301 | * Set 302 | * 303 | */ 304 | list($response, $model) = $this->getMocks(); 305 | 306 | /** 307 | * 308 | * Assertion 309 | * 310 | */ 311 | $aggregations = $response->getAggregations('named_aggregation'); 312 | 313 | $this->assertEquals('named_aggregation', $aggregations); 314 | } 315 | 316 | /** 317 | * @test 318 | */ 319 | public function it_should_get_aggregations_without_name() 320 | { 321 | /** 322 | * 323 | * Set 324 | * 325 | */ 326 | list($response, $model) = $this->getMocks(); 327 | 328 | /** 329 | * 330 | * Assertion 331 | * 332 | */ 333 | $aggregations = $response->getAggregations(); 334 | 335 | $this->assertEquals(['named_aggregation' => 'named_aggregation'], $aggregations); 336 | } 337 | 338 | 339 | /** 340 | * Construct a Response mock 341 | * 342 | * @return array 343 | */ 344 | private function getMocks() 345 | { 346 | $model = m::mock('Husband'); 347 | 348 | $response = m::mock('Iverberk\Larasearch\Response', array($model, $this->responseFixture))->makePartial(); 349 | 350 | return [$response, $model]; 351 | } 352 | 353 | 354 | } 355 | -------------------------------------------------------------------------------- /src/config/larasearch.php: -------------------------------------------------------------------------------- 1 | [ 13 | 14 | /** 15 | * Configuration array for the low-level Elasticsearch client. See 16 | * http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html 17 | * for additional options. 18 | */ 19 | 20 | 'params' => [ 21 | 'hosts' => [ 'localhost:9200' ], 22 | 'connectionClass' => '\Elasticsearch\Connections\GuzzleConnection', 23 | 'connectionFactoryClass'=> '\Elasticsearch\Connections\ConnectionFactory', 24 | 'connectionPoolClass' => '\Elasticsearch\ConnectionPool\StaticNoPingConnectionPool', 25 | 'selectorClass' => '\Elasticsearch\ConnectionPool\Selectors\RoundRobinSelector', 26 | 'serializerClass' => '\Elasticsearch\Serializers\SmartSerializer', 27 | 'sniffOnStart' => false, 28 | 'connectionParams' => [], 29 | 'logging' => false, 30 | 'logObject' => null, 31 | 'logPath' => 'elasticsearch.log', 32 | 'logLevel' => LogLevel::WARNING, 33 | 'traceObject' => null, 34 | 'tracePath' => 'elasticsearch.log', 35 | 'traceLevel' => LogLevel::WARNING, 36 | 'guzzleOptions' => [], 37 | 'connectionPoolParams' => ['randomizeHosts' => true], 38 | 'retries' => null, 39 | ], 40 | 41 | 'analyzers' => [ 42 | 'autocomplete', 43 | 'suggest', 44 | 'text_start', 45 | 'text_middle', 46 | 'text_end', 47 | 'word_start', 48 | 'word_middle', 49 | 'word_end' 50 | ], 51 | 52 | /** 53 | * Default configuration array for Elasticsearch indices based on Eloquent models 54 | * CREDIT: Analyzers, Tokenizers and Filters are copied and renamed from the Searchkick 55 | * project to get started quickly. 56 | */ 57 | 58 | 'defaults' => [ 59 | 'index' => [ 60 | 'settings' => [ 61 | 'number_of_shards' => 1, 62 | 'number_of_replicas' => 0, 63 | 'analysis' => [ 64 | 'analyzer' => [ 65 | 'larasearch_keyword' => [ 66 | 'type' => "custom", 67 | 'tokenizer' => "keyword", 68 | 'filter' => ["lowercase", "larasearch_stemmer"] 69 | ], 70 | 'default_index' => [ 71 | 'type' => "custom", 72 | 'tokenizer' => "standard", 73 | 'filter' => ["standard", "lowercase", "asciifolding", "larasearch_index_shingle", "larasearch_stemmer"] 74 | ], 75 | 'larasearch_search' => [ 76 | 'type' => "custom", 77 | 'tokenizer' => "standard", 78 | 'filter' => ["standard", "lowercase", "asciifolding", "larasearch_search_shingle", "larasearch_stemmer"] 79 | ], 80 | 'larasearch_search2' => [ 81 | 'type' => "custom", 82 | 'tokenizer' => "standard", 83 | 'filter' => ["standard", "lowercase", "asciifolding", "larasearch_stemmer"] 84 | ], 85 | 'larasearch_autocomplete_index' => [ 86 | 'type' => "custom", 87 | 'tokenizer' => "larasearch_autocomplete_ngram", 88 | 'filter' => ["lowercase", "asciifolding"] 89 | ], 90 | 'larasearch_autocomplete_search' => [ 91 | 'type' => "custom", 92 | 'tokenizer' => "keyword", 93 | 'filter' => ["lowercase", "asciifolding"] 94 | ], 95 | 'larasearch_word_search' => [ 96 | 'type' => "custom", 97 | 'tokenizer' => "standard", 98 | 'filter' => ["lowercase", "asciifolding"] 99 | ], 100 | 'larasearch_suggest_index' => [ 101 | 'type' => "custom", 102 | 'tokenizer' => "standard", 103 | 'filter' => ["lowercase", "asciifolding", "larasearch_suggest_shingle"] 104 | ], 105 | 'larasearch_text_start_index' => [ 106 | 'type' => "custom", 107 | 'tokenizer' => "keyword", 108 | 'filter' => ["lowercase", "asciifolding", "larasearch_edge_ngram"] 109 | ], 110 | 'larasearch_text_middle_index' => [ 111 | 'type' => "custom", 112 | 'tokenizer' => "keyword", 113 | 'filter' => ["lowercase", "asciifolding", "larasearch_ngram"] 114 | ], 115 | 'larasearch_text_end_index' => [ 116 | 'type' => "custom", 117 | 'tokenizer' => "keyword", 118 | 'filter' => ["lowercase", "asciifolding", "reverse", "larasearch_edge_ngram", "reverse"] 119 | ], 120 | 'larasearch_word_start_index' => [ 121 | 'type' => "custom", 122 | 'tokenizer' => "standard", 123 | 'filter' => ["lowercase", "asciifolding", "larasearch_edge_ngram"] 124 | ], 125 | 'larasearch_word_middle_index' => [ 126 | 'type' => "custom", 127 | 'tokenizer' => "standard", 128 | 'filter' => ["lowercase", "asciifolding", "larasearch_ngram"] 129 | ], 130 | 'larasearch_word_end_index' => [ 131 | 'type' => "custom", 132 | 'tokenizer' => "standard", 133 | 'filter' => ["lowercase", "asciifolding", "reverse", "larasearch_edge_ngram", "reverse"] 134 | ] 135 | ], 136 | 'filter' => [ 137 | 'larasearch_index_shingle' => [ 138 | 'type' => "shingle", 139 | 'token_separator' => "" 140 | ], 141 | 'larasearch_search_shingle' => [ 142 | 'type' => "shingle", 143 | 'token_separator' => "", 144 | 'output_unigrams' => false, 145 | 'output_unigrams_if_no_shingles' => true 146 | ], 147 | 'larasearch_suggest_shingle' => [ 148 | 'type' => "shingle", 149 | 'max_shingle_size' => 5 150 | ], 151 | 'larasearch_edge_ngram' => [ 152 | 'type' => "edgeNGram", 153 | 'min_gram' => 1, 154 | 'max_gram' => 50 155 | ], 156 | 'larasearch_ngram' => [ 157 | 'type' => "nGram", 158 | 'min_gram' => 1, 159 | 'max_gram' => 50 160 | ], 161 | 'larasearch_stemmer' => [ 162 | 'type' => "snowball", 163 | 'language' => "English" 164 | ] 165 | ], 166 | 'tokenizer' => [ 167 | 'larasearch_autocomplete_ngram' => [ 168 | 'type' => "edgeNGram", 169 | 'min_gram' => 1, 170 | 'max_gram' => 50 171 | ] 172 | ] 173 | ] 174 | ], 175 | 'mappings' => [ 176 | '_default_' => [ 177 | # https://gist.github.com/kimchy/2898285 178 | 'dynamic_templates' => [ 179 | [ 180 | 'string_template' => [ 181 | 'match' => '*', 182 | 'match_mapping_type' => 'string', 183 | 'mapping' => [ 184 | # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/ 185 | 'type' => 'multi_field', 186 | 'fields' => [ 187 | # analyzed field must be the default field for include_in_all 188 | # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/ 189 | # however, we can include the not_analyzed field in _all 190 | # and the _all index analyzer will take care of it 191 | '{name}' => ['type' => 'string', 'index' => 'not_analyzed'], 192 | 'analyzed' => ['type' => 'string', 'index' => 'analyzed'] 193 | ] 194 | ] 195 | ] 196 | ] 197 | ] 198 | ] 199 | ] 200 | ] 201 | ], 202 | 203 | 'index_prefix' => '' 204 | ], 205 | 206 | 'logger' => 'iverberk.larasearch.logger' 207 | 208 | )); 209 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Query.php: -------------------------------------------------------------------------------- 1 | proxy = $proxy; 45 | $this->term = $term; 46 | $this->options = $options; 47 | } 48 | 49 | /** 50 | * 51 | */ 52 | private function getAggregations() 53 | { 54 | if ($aggregations = Utils::findKey($this->options, 'aggs', false)) 55 | { 56 | foreach ($aggregations as $name => $aggregation) 57 | { 58 | switch ($aggregation['type']) 59 | { 60 | case 'terms': 61 | $this->payload['aggs'][$name]['terms'] = ['field' => $aggregation['field'], 'size' => 0]; 62 | break; 63 | } 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * 70 | */ 71 | private function getFields() 72 | { 73 | if (array_key_exists('fields', $this->options)) 74 | { 75 | if (array_key_exists('autocomplete', $this->options)) 76 | { 77 | $this->fields = array_map(function ($field) 78 | { 79 | return "${field}.autocomplete"; 80 | }, 81 | $this->options['fields'] 82 | ); 83 | } else 84 | { 85 | foreach ($this->options['fields'] as $key => $value) 86 | { 87 | if (is_string($key)) 88 | { 89 | $k = $key; 90 | $v = $value; 91 | } else 92 | { 93 | // $key is the numerical index, so use $value as key 94 | $k = $value; 95 | $v = 'word'; 96 | } 97 | $this->fields[] = "$k." . (($v == 'word') ? 'analyzed' : $v); 98 | } 99 | 100 | } 101 | } else 102 | { 103 | if (array_key_exists('autocomplete', $this->options)) 104 | { 105 | $this->fields = array_map(function ($field) 106 | { 107 | return "${field}.autocomplete"; 108 | }, 109 | Utils::findKey($this->proxy->getConfig(), 'autocomplete', [])); 110 | } else 111 | { 112 | $this->fields = ['_all']; 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * Add requested pagination parameters to the payload 119 | */ 120 | private function getPagination() 121 | { 122 | # pagination 123 | $this->pagination['page'] = 1; 124 | $this->pagination['per_page'] = Utils::findKey($this->options, 'limit', 50); 125 | $this->pagination['padding'] = Utils::findKey($this->options, 'padding', 0); 126 | $this->pagination['offset'] = Utils::findKey($this->options, 'offset', 0); 127 | 128 | $this->payload['size'] = $this->pagination['per_page']; 129 | $this->payload['from'] = $this->pagination['offset']; 130 | } 131 | 132 | /** 133 | * Add requested highlights to the payload 134 | */ 135 | private function getHighlight() 136 | { 137 | if (Utils::findKey($this->options, 'highlight', false)) 138 | { 139 | foreach ($this->fields as $field) 140 | { 141 | $this->payload['highlight']['fields'][$field] = new StdClass(); 142 | } 143 | 144 | if ($tag = Utils::findKey($this->options['highlight'], 'tag', false)) 145 | { 146 | $this->payload['highlight']['pre_tags'] = [$tag]; 147 | $this->payload['highlight']['post_tags'] = [preg_replace('/\Aoptions, 'suggest', false)) 158 | { 159 | $suggest_fields = Utils::findKey($this->proxy->getConfig(), 'suggest', []); 160 | 161 | if ($fields = Utils::findKey($this->options, 'fields', false)) 162 | { 163 | $suggest_fields = array_intersect($suggest_fields, $fields); 164 | } 165 | 166 | if (!empty($suggest_fields)) 167 | { 168 | $this->payload['suggest'] = ['text' => $this->term]; 169 | foreach ($suggest_fields as $field) 170 | { 171 | $this->payload['suggest'][$field] = ['phrase' => ['field' => "${field}.suggest"]]; 172 | } 173 | } 174 | } 175 | } 176 | 177 | 178 | /** 179 | * Allow sorting of results 180 | */ 181 | private function getSort() 182 | { 183 | if ($sort = Utils::findKey($this->options, 'sort', false)) 184 | { 185 | $this->payload['sort'] = $sort; 186 | } 187 | } 188 | 189 | /** 190 | * Construct the payload from the options 191 | */ 192 | private function getPayload() 193 | { 194 | $payloads = [ 195 | 'json' => Utils::findKey($this->options, 'json', false), 196 | 'query' => Utils::findKey($this->options, 'query', false), 197 | 'similar' => [ 198 | 'query' => [ 199 | 'more_like_this' => [ 200 | 'fields' => $this->fields, 201 | 'like_text' => $this->term, 202 | 'min_doc_freq' => 1, 203 | 'min_term_freq' => 1, 204 | 'analyzer' => "larasearch_search2" 205 | ] 206 | ] 207 | ], 208 | 'autocomplete' => [ 209 | 'query' => [ 210 | 'multi_match' => [ 211 | 'fields' => $this->fields, 212 | 'query' => $this->term, 213 | 'analyzer' => "larasearch_autocomplete_search" 214 | ] 215 | ] 216 | ] 217 | ]; 218 | 219 | // Find the correct payload based on the options 220 | $payload_key = array_intersect_key($this->options, $payloads); 221 | 222 | $operator = Utils::findKey($this->options, 'operator', 'and'); 223 | 224 | if (count($payload_key) == 1) 225 | { 226 | $this->payload = array_merge($this->payload, $payloads[key($payload_key)]); 227 | } elseif (count($payload_key) == 0) 228 | { 229 | if ($this->term == '*') 230 | { 231 | $payload = ['match_all' => []]; 232 | } else 233 | { 234 | $queries = []; 235 | 236 | foreach ($this->fields as $field) 237 | { 238 | $qs = []; 239 | 240 | $shared_options = [ 241 | 'query' => $this->term, 242 | 'operator' => $operator, 243 | 'boost' => 1 244 | ]; 245 | 246 | if ($field == '_all' || substr_compare($field, '.analyzed', -9, 9) === 0) 247 | { 248 | $qs = array_merge($qs, [ 249 | array_merge($shared_options, ['boost' => 10, 'analyzer' => "larasearch_search"]), 250 | array_merge($shared_options, ['boost' => 10, 'analyzer' => "larasearch_search2"]) 251 | ] 252 | ); 253 | if ($misspellings = Utils::findKey($this->options, 'misspellings', false)) 254 | { 255 | $distance = 1; 256 | $qs = array_merge($qs, [ 257 | array_merge($shared_options, ['fuzziness' => $distance, 'max_expansions' => 3, 'analyzer' => "larasearch_search"]), 258 | array_merge($shared_options, ['fuzziness' => $distance, 'max_expansions' => 3, 'analyzer' => "larasearch_search2"]) 259 | ] 260 | ); 261 | 262 | } 263 | } elseif (substr_compare($field, '.exact', -6, 6) === 0) 264 | { 265 | $f = substr($field, 0, -6); 266 | $queries[] = [ 267 | 'match' => [ 268 | $f => array_merge($shared_options, ['analyzer' => 'keyword']) 269 | ] 270 | ]; 271 | } else 272 | { 273 | $analyzer = preg_match('/\.word_(start|middle|end)\z/', $field) ? "larasearch_word_search" : "larasearch_autocomplete_search"; 274 | $qs[] = array_merge($shared_options, ['analyzer' => $analyzer]); 275 | } 276 | 277 | $queries = array_merge($queries, array_map(function ($q) use ($field) 278 | { 279 | return ['match' => [$field => $q]]; 280 | }, $qs)); 281 | } 282 | 283 | $payload = ['dis_max' => ['queries' => $queries]]; 284 | } 285 | 286 | $this->payload['query'] = $payload; 287 | } else 288 | { 289 | // We have multiple query definitions, so abort 290 | throw new \InvalidArgumentException('Cannot use multiple query definitions.'); 291 | } 292 | 293 | if ($load = Utils::findKey($this->options, 'load', false)) 294 | { 295 | $this->payload['fields'] = is_array($load) ? $load : []; 296 | } elseif ($select = Utils::findKey($this->options, 'select', false)) 297 | { 298 | $this->payload['fields'] = $select; 299 | } 300 | } 301 | 302 | /** 303 | * Execute the query and return the response in a rich wrapper class 304 | * 305 | * @return Response 306 | */ 307 | public function execute() 308 | { 309 | $this->getFields(); 310 | $this->getPagination(); 311 | $this->getHighlight(); 312 | $this->getSuggest(); 313 | $this->getAggregations(); 314 | $this->getSort(); 315 | $this->getPayload(); 316 | 317 | $params = [ 318 | 'index' => Utils::findKey($this->options, 'index', false) ?: $this->proxy->getIndex()->getName(), 319 | 'type' => Utils::findKey($this->options, 'type', false) ?: $this->proxy->getType(), 320 | 'body' => $this->payload 321 | ]; 322 | 323 | return new Response($this->proxy->getModel(), $this->proxy->getClient()->search($params)); 324 | } 325 | 326 | } 327 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/LarasearchServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | base_path($path); 10 | } 11 | 12 | /** 13 | * Class LarasearchServiceProviderTest 14 | */ 15 | class LarasearchServiceProviderTest extends \PHPUnit_Framework_TestCase { 16 | 17 | public static $functions; 18 | protected static $providers_real_path; 19 | 20 | protected function setup() 21 | { 22 | self::$functions = m::mock(); 23 | self::$functions->shouldReceive('base_path')->andReturn(''); 24 | self::$providers_real_path = realpath(__DIR__ . '/../../../src/Iverberk/Larasearch'); 25 | } 26 | 27 | protected function tearDown() 28 | { 29 | m::close(); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function it_should_boot() 36 | { 37 | /** 38 | * Set 39 | */ 40 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[bootContainerBindings, publishes]', ['something']); 41 | $sp->shouldAllowMockingProtectedMethods(); 42 | 43 | /** 44 | * Expectation 45 | */ 46 | $sp->shouldReceive('publishes') 47 | ->with([ 48 | self::$providers_real_path . '/../../config/larasearch.php' => base_path('config/larasearch.php'), 49 | ], 'config') 50 | ->once(); 51 | 52 | $sp->shouldReceive('bootContainerBindings') 53 | ->once(); 54 | 55 | /** 56 | * Assertion 57 | */ 58 | $sp->boot(); 59 | } 60 | 61 | /** 62 | * @test 63 | */ 64 | public function it_should_boot_container_bindings() 65 | { 66 | /** 67 | * Set 68 | */ 69 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[' . 70 | 'bindProxy, bindIndex, bindLogger, bindElasticsearch, bindQuery, bindResult]', ['something']); 71 | $sp->shouldAllowMockingProtectedMethods(); 72 | 73 | /** 74 | * Expectation 75 | */ 76 | $sp->shouldReceive('bindElasticsearch')->once()->andReturn(true); 77 | $sp->shouldReceive('bindLogger')->once()->andReturn(true); 78 | $sp->shouldReceive('bindProxy')->once()->andReturn(true); 79 | $sp->shouldReceive('bindIndex')->once()->andReturn(true); 80 | $sp->shouldReceive('bindQuery')->once()->andReturn(true); 81 | $sp->shouldReceive('bindResult')->once()->andReturn(true); 82 | 83 | /** 84 | * Assertions 85 | */ 86 | $sp->bootContainerBindings(); 87 | } 88 | 89 | /** 90 | * @test 91 | */ 92 | public function it_should_bind_elasticsearch() 93 | { 94 | /** 95 | * Set 96 | */ 97 | $app = m::mock('LaravelApp'); 98 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[bindElasticsearch]', [$app]); 99 | 100 | /** 101 | * Expectation 102 | */ 103 | Config::shouldReceive('get') 104 | ->with('larasearch.elasticsearch.params') 105 | ->once() 106 | ->andReturn([]); 107 | 108 | $app->shouldReceive('singleton') 109 | ->once() 110 | ->andReturnUsing( 111 | function ($name, $closure) use ($app) 112 | { 113 | $this->assertEquals('Elasticsearch', $name); 114 | $this->assertInstanceOf('Elasticsearch\Client', $closure($app)); 115 | } 116 | ); 117 | 118 | $sp->bindElasticsearch(); 119 | } 120 | 121 | /** 122 | * @test 123 | */ 124 | public function it_should_bind_logger() 125 | { 126 | /** 127 | * Set 128 | */ 129 | $app = m::mock('LaravelApp'); 130 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[bindLogger]', [$app]); 131 | 132 | /** 133 | * Expectation 134 | */ 135 | $app->shouldReceive('singleton') 136 | ->once() 137 | ->andReturnUsing( 138 | function ($name, $closure) use ($app) 139 | { 140 | $this->assertEquals('iverberk.larasearch.logger', $name); 141 | $this->assertInstanceOf('Monolog\Logger', $closure($app)); 142 | } 143 | ); 144 | 145 | $sp->bindLogger(); 146 | } 147 | 148 | /** 149 | * @test 150 | */ 151 | public function it_should_bind_index() 152 | { 153 | /** 154 | * Set 155 | */ 156 | App::clearResolvedInstances(); 157 | Config::clearResolvedInstances(); 158 | 159 | App::shouldReceive('make') 160 | ->with('iverberk.larasearch.index', m::any()) 161 | ->once() 162 | ->andReturn('mock'); 163 | 164 | App::shouldReceive('make') 165 | ->with('Elasticsearch') 166 | ->twice() 167 | ->andReturn('mock'); 168 | 169 | Config::shouldReceive('get') 170 | ->with('larasearch.elasticsearch.index_prefix', '') 171 | ->andReturn(''); 172 | 173 | $model = m::mock('Illuminate\Database\Eloquent\Model'); 174 | $model->shouldReceive('getTable') 175 | ->once() 176 | ->andReturn('mockType'); 177 | $app = m::mock('LaravelApp'); 178 | $proxy = m::mock('Iverberk\Larasearch\Proxy', [$model]); 179 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[bindIndex]', [$app]); 180 | 181 | /** 182 | * Expectation 183 | */ 184 | $app->shouldReceive('bind') 185 | ->once() 186 | ->andReturnUsing( 187 | function ($name, $closure) use ($app, $proxy) 188 | { 189 | $this->assertEquals('iverberk.larasearch.index', $name); 190 | $this->assertInstanceOf('Iverberk\Larasearch\Index', 191 | $closure($app, ['proxy' => $proxy, 'name' => 'name'])); 192 | } 193 | ); 194 | 195 | 196 | /** 197 | * Assertion 198 | */ 199 | $sp->bindIndex(); 200 | } 201 | 202 | /** 203 | * @test 204 | */ 205 | public function it_should_bind_query() 206 | { 207 | /** 208 | * Set 209 | */ 210 | $model = m::mock('Illuminate\Database\Eloquent\Model'); 211 | $model->shouldReceive('getTable') 212 | ->once() 213 | ->andReturn('mockType'); 214 | $app = m::mock('LaravelApp'); 215 | $proxy = m::mock('Iverberk\Larasearch\Proxy', [$model]); 216 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[bindQuery]', [$app]); 217 | 218 | /** 219 | * Expectation 220 | */ 221 | $app->shouldReceive('bind') 222 | ->once() 223 | ->andReturnUsing( 224 | function ($name, $closure) use ($app, $proxy) 225 | { 226 | $this->assertEquals('iverberk.larasearch.query', $name); 227 | $this->assertInstanceOf('Iverberk\Larasearch\Query', 228 | $closure($app, ['proxy' => $proxy, 'term' => 'term', 'options' => []])); 229 | } 230 | ); 231 | 232 | /** 233 | * Assertion 234 | */ 235 | $sp->bindQuery(); 236 | } 237 | 238 | /** 239 | * @test 240 | */ 241 | public function it_should_bind_proxy() 242 | { 243 | /** 244 | * Set 245 | */ 246 | $model = m::mock('Illuminate\Database\Eloquent\Model'); 247 | $model->shouldReceive('getTable') 248 | ->once() 249 | ->andReturn('mockType'); 250 | $app = m::mock('LaravelApp'); 251 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[bindProxy]', [$app]); 252 | 253 | /** 254 | * Expectation 255 | */ 256 | $app->shouldReceive('bind') 257 | ->once() 258 | ->andReturnUsing( 259 | function ($name, $closure) use ($app, $model) 260 | { 261 | $this->assertEquals('iverberk.larasearch.proxy', $name); 262 | $this->assertInstanceOf('Iverberk\Larasearch\Proxy', 263 | $closure($app, $model)); 264 | } 265 | ); 266 | 267 | 268 | /** 269 | * Assertion 270 | */ 271 | $sp->bindProxy(); 272 | } 273 | 274 | /** 275 | * @test 276 | */ 277 | public function it_should_bind_result() 278 | { 279 | /** 280 | * Set 281 | */ 282 | $app = m::mock('LaravelApp'); 283 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[bindResult]', [$app]); 284 | 285 | /** 286 | * Expectation 287 | */ 288 | $app->shouldReceive('bind') 289 | ->once() 290 | ->andReturnUsing( 291 | function ($name, $closure) use ($app) 292 | { 293 | $this->assertEquals('iverberk.larasearch.response.result', $name); 294 | $this->assertInstanceOf('Iverberk\Larasearch\Response\Result', 295 | $closure($app, [])); 296 | } 297 | ); 298 | 299 | /** 300 | * Assertion 301 | */ 302 | $sp->bindResult(); 303 | } 304 | 305 | /** 306 | * @test 307 | */ 308 | public function it_should_register_commands() 309 | { 310 | /** 311 | * Set 312 | */ 313 | $app = m::mock('Illuminate\Container\Container'); 314 | $sp = m::mock('Iverberk\Larasearch\LarasearchServiceProvider[commands, mergeConfigFrom]', [$app]); 315 | $sp->shouldAllowMockingProtectedMethods(); 316 | 317 | /** 318 | * Expectation 319 | */ 320 | $app->shouldReceive('offsetSet')->andReturn(true); 321 | $app->shouldReceive('offsetGet')->andReturn(true); 322 | 323 | $app->shouldReceive('share') 324 | ->once() 325 | ->andReturnUsing(function ($closure) use ($app) 326 | { 327 | $this->assertInstanceOf('Iverberk\Larasearch\Commands\ReindexCommand', $closure($app)); 328 | }); 329 | 330 | $app->shouldReceive('share') 331 | ->once() 332 | ->andReturnUsing(function ($closure) use ($app) 333 | { 334 | $this->assertInstanceOf('Iverberk\Larasearch\Commands\PathsCommand', $closure($app)); 335 | }); 336 | 337 | $sp->shouldReceive('commands') 338 | ->with('iverberk.larasearch.commands.reindex') 339 | ->once() 340 | ->andReturn(true); 341 | 342 | $sp->shouldReceive('commands') 343 | ->with('iverberk.larasearch.commands.paths') 344 | ->once() 345 | ->andReturn(true); 346 | 347 | $sp->shouldReceive('mergeConfigFrom') 348 | ->with(self::$providers_real_path . '/../../config/larasearch.php', 'larasearch') 349 | ->once(); 350 | 351 | /** 352 | * Assertion 353 | */ 354 | $sp->register(); 355 | } 356 | 357 | /** 358 | * @test 359 | */ 360 | public function it_should_provide_services() 361 | { 362 | /** 363 | * Set 364 | */ 365 | $app = m::mock('LaravelApp'); 366 | 367 | /** 368 | * Assertion 369 | */ 370 | $sp = new LarasearchServiceProvider($app); 371 | $this->assertEquals([], $sp->provides()); 372 | } 373 | 374 | } 375 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/Commands/PathsCommandTest.php: -------------------------------------------------------------------------------- 1 | app_path(); 14 | } 15 | 16 | function constant($const) 17 | { 18 | return PathsCommandTest::$functions->constant($const); 19 | } 20 | 21 | function base_path() 22 | { 23 | return PathsCommandTest::$functions->base_path(); 24 | } 25 | 26 | /** 27 | * Class PathsCommandTest 28 | * @package Iverberk\Larasearch\Commands 29 | * @preserveGlobalState disabled 30 | */ 31 | class PathsCommandTest extends \PHPUnit_Framework_TestCase { 32 | 33 | /** 34 | * @var \Mockery\Mock $functions 35 | */ 36 | public static $functions; 37 | 38 | /** 39 | * 40 | */ 41 | public function setUp() 42 | { 43 | parent::setUp(); 44 | 45 | self::$functions = m::mock(); 46 | } 47 | 48 | /** 49 | * 50 | */ 51 | protected function tearDown() 52 | { 53 | m::close(); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function it_should_get_options() 60 | { 61 | /** 62 | * 63 | * Set 64 | * 65 | **/ 66 | $command = m::mock('Iverberk\Larasearch\Commands\PathsCommand'); 67 | $options = array( 68 | array('dir', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Directory to scan for searchable models', null, ''), 69 | array('relations', null, InputOption::VALUE_NONE, 'Include related Eloquent models', null), 70 | array('write-config', null, InputOption::VALUE_NONE, 'Include the compiled paths in the package configuration', null), 71 | ); 72 | 73 | /** 74 | * 75 | * Assertion 76 | * 77 | **/ 78 | $this->assertEquals($options, $command->getOptions()); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function it_should_get_arguments() 85 | { 86 | /** 87 | * 88 | * Set 89 | * 90 | **/ 91 | $command = m::mock('Iverberk\Larasearch\Commands\PathsCommand'); 92 | $arguments = array( 93 | array('model', InputOption::VALUE_OPTIONAL, 'Eloquent model to find paths for', null) 94 | ); 95 | 96 | /** 97 | * 98 | * Assertion 99 | * 100 | **/ 101 | $this->assertEquals($arguments, $command->getArguments()); 102 | } 103 | 104 | /** 105 | * @test 106 | */ 107 | public function it_should_fire_with_models_and_config() 108 | { 109 | /** 110 | * 111 | * Set 112 | * 113 | * @var \Mockery\Mock $command 114 | */ 115 | $command = m::mock('Iverberk\Larasearch\Commands\PathsCommand')->makePartial(); 116 | $command->shouldAllowMockingProtectedMethods(); 117 | 118 | File::clearResolvedInstance('files'); 119 | File::shouldReceive('put')->once()->andReturn(true); 120 | File::shouldReceive('exists')->once()->andReturn(true); 121 | 122 | App::shouldReceive('make')->andReturn(true); 123 | 124 | self::$functions->shouldReceive('base_path')->once()->andReturn(''); 125 | 126 | /** 127 | * 128 | * Expectation 129 | * 130 | */ 131 | $command->shouldReceive('argument') 132 | ->with('model') 133 | ->once() 134 | ->andReturn(['Husband']); 135 | 136 | $command->shouldReceive('option') 137 | ->with('dir') 138 | ->once() 139 | ->andReturn([__DIR__ . '/../../../Support/Stubs']); 140 | 141 | $command->shouldReceive('option') 142 | ->with('write-config') 143 | ->once() 144 | ->andReturn(true); 145 | 146 | $command->shouldReceive('option') 147 | ->with('relations') 148 | ->times(17) 149 | ->andReturn(true); 150 | 151 | $command->shouldReceive('error', 'confirm', 'call', 'info') 152 | ->andReturn(true); 153 | 154 | /** 155 | * 156 | * Assertion 157 | * 158 | */ 159 | $command->fire(); 160 | 161 | 162 | $expected = [ 163 | 'Husband' => ['wife.children.toys'], 164 | 'Child' => ['mother', 'father', 'toys'], 165 | 'Toy' => ['children.mother', 'children.father'], 166 | 'Wife' => ['husband', 'children.toys'], 167 | 'House\\Item' => [] 168 | ]; 169 | $actual = $command->getPaths(); 170 | 171 | foreach ($expected as $model => $paths) 172 | { 173 | $this->assertArrayHasKey($model, $actual); 174 | 175 | foreach ($paths as $path) 176 | { 177 | $this->assertContains($path, $actual[$model]); 178 | } 179 | } 180 | 181 | $expected = [ 182 | 'Husband' => ['', 'wife', 'children', 'children.toys'], 183 | 'Child' => ['mother.husband', 'mother', '', 'toys'], 184 | 'Toy' => ['children.mother.husband', 'children.mother', 'children', ''], 185 | 'Wife' => ['husband', '', 'children', 'children.toys'], 186 | 'House\\Item' => [''] 187 | ]; 188 | $actual = $command->getReversedPaths(); 189 | 190 | foreach ($expected as $model => $paths) 191 | { 192 | $this->assertArrayHasKey($model, $actual); 193 | 194 | foreach ($paths as $path) 195 | { 196 | $this->assertContains($path, $actual[$model]); 197 | } 198 | } 199 | } 200 | 201 | /** 202 | * @test 203 | */ 204 | public function it_should_fire_without_models() 205 | { 206 | /** 207 | * Set 208 | * 209 | * @var \Mockery\Mock $command 210 | */ 211 | $command = m::mock('Iverberk\Larasearch\Commands\PathsCommand')->makePartial(); 212 | $command->shouldAllowMockingProtectedMethods(); 213 | 214 | App::shouldReceive('make')->andReturn(true); 215 | 216 | /** 217 | * 218 | * Expectation 219 | * 220 | */ 221 | $command->shouldReceive('argument') 222 | ->with('model') 223 | ->once() 224 | ->andReturn([]); 225 | 226 | 227 | $command->shouldReceive('option') 228 | ->with('dir') 229 | ->once() 230 | ->andReturn([]); 231 | 232 | 233 | $command->shouldReceive('compilePaths', 'error', 'confirm', 'call', 'info') 234 | ->andReturn(true); 235 | 236 | /** 237 | * 238 | * Assertion 239 | * 240 | */ 241 | $command->fire(); 242 | 243 | $this->assertEquals( 244 | [], 245 | $command->getPaths() 246 | ); 247 | 248 | $this->assertEquals( 249 | [], 250 | $command->getReversedPaths() 251 | ); 252 | } 253 | 254 | /** 255 | * @test 256 | */ 257 | public function it_should_fire_without_config() 258 | { 259 | /** 260 | * Set 261 | * 262 | * @var \Mockery\Mock $command 263 | */ 264 | $command = m::mock('Iverberk\Larasearch\Commands\PathsCommand')->makePartial(); 265 | $command->shouldAllowMockingProtectedMethods(); 266 | 267 | App::shouldReceive('make')->andReturn(true); 268 | 269 | /** 270 | * 271 | * Expectation 272 | * 273 | */ 274 | $command->shouldReceive('argument') 275 | ->with('model') 276 | ->once() 277 | ->andReturn(['Husband']); 278 | 279 | $command->shouldReceive('option') 280 | ->with('dir') 281 | ->once() 282 | ->andReturn([__DIR__ . '/../../../Support/Stubs']); 283 | 284 | $command->shouldReceive('option') 285 | ->with('write-config') 286 | ->once() 287 | ->andReturn(false); 288 | 289 | $command->shouldReceive('compilePaths', 'error', 'confirm', 'call', 'info') 290 | ->andReturn(true); 291 | 292 | /** 293 | * 294 | * Assertion 295 | * 296 | */ 297 | $command->fire(); 298 | } 299 | 300 | /** 301 | * @test 302 | */ 303 | public function it_should_fire_with_laravel_and_config_confirmed() 304 | { 305 | /** 306 | * 307 | * Set 308 | * 309 | * @var \Mockery\Mock $command 310 | */ 311 | $command = m::mock('Iverberk\Larasearch\Commands\PathsCommand')->makePartial(); 312 | $command->shouldAllowMockingProtectedMethods(); 313 | 314 | File::clearResolvedInstance('files'); 315 | File::shouldReceive('put')->once()->andReturn(true); 316 | File::shouldReceive('exists')->once()->andReturn(false); 317 | 318 | App::shouldReceive('make')->andReturn(true); 319 | 320 | self::$functions->shouldReceive('base_path')->once()->andReturn(''); 321 | 322 | /** 323 | * 324 | * Expectation 325 | * 326 | */ 327 | $command->shouldReceive('getLaravel') 328 | ->once() 329 | ->andReturn(true); 330 | 331 | $command->shouldReceive('argument') 332 | ->with('model') 333 | ->once() 334 | ->andReturn(['Husband']); 335 | 336 | $command->shouldReceive('option') 337 | ->with('dir') 338 | ->once() 339 | ->andReturn([__DIR__ . '/../../../Support/Stubs']); 340 | 341 | $command->shouldReceive('option') 342 | ->with('write-config') 343 | ->once() 344 | ->andReturn(true); 345 | 346 | $command->shouldReceive('confirm') 347 | ->once() 348 | ->andReturn(true); 349 | 350 | $command->shouldReceive('compilePaths', 'error', 'call', 'info') 351 | ->andReturn(true); 352 | 353 | /** 354 | * 355 | * Assertion 356 | * 357 | */ 358 | $command->fire(); 359 | } 360 | 361 | /** 362 | * @test 363 | */ 364 | public function it_should_fire_with_config_not_confirmed() 365 | { 366 | /** 367 | * Set 368 | * 369 | * @var \Mockery\Mock $command 370 | */ 371 | $command = m::mock('Iverberk\Larasearch\Commands\PathsCommand')->makePartial(); 372 | $command->shouldAllowMockingProtectedMethods(); 373 | 374 | File::clearResolvedInstance('files'); 375 | File::shouldReceive('exists')->once()->andReturn(false); 376 | 377 | App::shouldReceive('make')->andReturn(true); 378 | 379 | self::$functions->shouldReceive('base_path')->once()->andReturn(''); 380 | 381 | /** 382 | * Expectation 383 | */ 384 | $command->shouldReceive('getLaravel') 385 | ->once() 386 | ->andReturn(true); 387 | 388 | $command->shouldReceive('argument') 389 | ->with('model') 390 | ->once() 391 | ->andReturn(['Husband']); 392 | 393 | $command->shouldReceive('option') 394 | ->with('dir') 395 | ->once() 396 | ->andReturn([__DIR__ . '/../../../Support/Stubs']); 397 | 398 | $command->shouldReceive('option') 399 | ->with('write-config') 400 | ->once() 401 | ->andReturn(true); 402 | 403 | $command->shouldReceive('confirm') 404 | ->once() 405 | ->andReturn(false); 406 | 407 | $command->shouldReceive('compilePaths', 'error', 'call', 'info') 408 | ->andReturn(true); 409 | 410 | /* 411 | |------------------------------------------------------------ 412 | | Assertion 413 | |------------------------------------------------------------ 414 | */ 415 | $command->fire(); 416 | } 417 | 418 | } 419 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/ProxyTest.php: -------------------------------------------------------------------------------- 1 | date(); 11 | } 12 | 13 | class ProxyTest extends \PHPUnit_Framework_TestCase { 14 | 15 | /** 16 | * @var \Mockery\Mock 17 | */ 18 | private $proxy; 19 | 20 | /** 21 | * @var \Mockery\Mock 22 | */ 23 | private $model; 24 | 25 | /** 26 | * @var \Mockery\Mock 27 | */ 28 | private $index; 29 | 30 | /** 31 | * @var \Mockery\Mock 32 | */ 33 | private $client; 34 | 35 | /** 36 | * @var \Mockery\Mock 37 | */ 38 | public static $functions; 39 | 40 | protected function setUp() 41 | { 42 | parent::setUp(); 43 | 44 | self::$functions = m::mock(); 45 | 46 | $this->index = m::mock('Iverberk\Larasearch\Index'); 47 | $this->model = m::mock('Husband')->makePartial(); 48 | $this->client = m::mock('Elasticsearch\Client'); 49 | 50 | $this->model->shouldReceive('getTable') 51 | ->once() 52 | ->andReturn('Husbands'); 53 | 54 | Facade::clearResolvedInstances(); 55 | App::shouldReceive('make') 56 | ->with('Elasticsearch') 57 | ->once() 58 | ->andReturn($this->client); 59 | 60 | App::shouldReceive('make') 61 | ->with('iverberk.larasearch.index', m::type('array')) 62 | ->once() 63 | ->andReturn($this->index); 64 | 65 | $this->proxy = new Proxy($this->model); 66 | 67 | self::$functions->shouldReceive('date')->andReturn('9999'); 68 | } 69 | 70 | protected function tearDown() 71 | { 72 | m::close(); 73 | am::clean(); 74 | } 75 | 76 | /** 77 | * @test 78 | */ 79 | public function it_can_get_config() 80 | { 81 | $this->assertEquals([ 82 | 'model' => $this->model, 83 | 'type' => 'Husband', 84 | 'client' => $this->client, 85 | 'index' => $this->index, 86 | 'autocomplete' => ['name', 'wife.name'], 87 | 'suggest' => ['name'], 88 | 'text_start' => ['name', 'wife.children.name'], 89 | 'text_middle' => ['name', 'wife.children.name'], 90 | 'text_end' => ['name', 'wife.children.name'], 91 | 'word_start' => ['name', 'wife.children.name'], 92 | 'word_middle' => ['name', 'wife.children.name'], 93 | 'word_end' => ['name', 'wife.children.name'] 94 | ], 95 | $this->proxy->getConfig()); 96 | } 97 | 98 | /** 99 | * @test 100 | */ 101 | public function it_can_get_model() 102 | { 103 | $this->assertEquals($this->model, $this->proxy->getModel()); 104 | } 105 | 106 | /** 107 | * @test 108 | */ 109 | public function it_can_get_index() 110 | { 111 | $this->assertEquals($this->index, $this->proxy->getIndex()); 112 | } 113 | 114 | /** 115 | * @test 116 | */ 117 | public function it_can_get_type() 118 | { 119 | $this->assertEquals('Husband', $this->proxy->getType()); 120 | } 121 | 122 | /** 123 | * @test 124 | */ 125 | public function it_can_get_client() 126 | { 127 | $this->assertEquals($this->client, $this->proxy->getClient()); 128 | } 129 | 130 | /** 131 | * @test 132 | */ 133 | public function it_can_search() 134 | { 135 | /** 136 | * 137 | * Set 138 | * 139 | */ 140 | $query = m::mock('Iverberk\Larasearch\Query'); 141 | 142 | /** 143 | * 144 | * Expectation 145 | * 146 | */ 147 | $query->shouldReceive('execute')->andReturn('result'); 148 | 149 | App::shouldReceive('make') 150 | ->with('iverberk.larasearch.query', ['proxy' => $this->proxy, 'term' => '*', 'options' => ['option']]) 151 | ->once() 152 | ->andReturn($query); 153 | 154 | /** 155 | * 156 | * Assertion 157 | * 158 | */ 159 | $result = $this->proxy->search('*', ['option']); 160 | 161 | $this->assertEquals('result', $result); 162 | } 163 | 164 | /** 165 | * @test 166 | */ 167 | public function it_can_search_with_a_query() 168 | { 169 | /** 170 | * 171 | * Set 172 | * 173 | */ 174 | $queryMock = m::mock('Iverberk\Larasearch\Query'); 175 | 176 | $query['index'] = 'my_index'; 177 | $query['type'] = 'my_type'; 178 | $query['body']['query']['match']['testField'] = 'abc'; 179 | 180 | /** 181 | * 182 | * Expectation 183 | * 184 | */ 185 | $queryMock->shouldReceive('execute')->andReturn('result'); 186 | 187 | App::shouldReceive('make') 188 | ->with('iverberk.larasearch.query', [ 189 | 'proxy' => $this->proxy, 190 | 'term' => null, 191 | 'options' => array_merge(['query' => $query], ['option'])]) 192 | ->once() 193 | ->andReturn($queryMock); 194 | 195 | /** 196 | * 197 | * Assertion 198 | * 199 | */ 200 | $result = $this->proxy->searchByQuery($query, ['option']); 201 | 202 | $this->assertEquals('result', $result); 203 | } 204 | 205 | /** 206 | * @test 207 | */ 208 | public function it_can_search_for_a_single_document() 209 | { 210 | /** 211 | * 212 | * Set 213 | * 214 | */ 215 | $query['index'] = 'my_index'; 216 | $query['type'] = 'my_type'; 217 | $query['id'] = 'abc'; 218 | 219 | /** 220 | * 221 | * Expectation 222 | * 223 | */ 224 | $this->index->shouldReceive('getName')->andReturn('index'); 225 | $this->client->shouldReceive('get')->andReturn(['hit']); 226 | 227 | App::shouldReceive('make') 228 | ->with('iverberk.larasearch.response.result', ['hit']) 229 | ->once() 230 | ->andReturn('result'); 231 | 232 | /** 233 | * 234 | * Assertion 235 | * 236 | */ 237 | $result = $this->proxy->searchById('abc'); 238 | 239 | $this->assertEquals('result', $result); 240 | } 241 | 242 | /** 243 | * @test 244 | */ 245 | public function it_can_reindex_when_alias_does_not_exist() 246 | { 247 | /** 248 | * 249 | * Set 250 | * 251 | */ 252 | $indexDouble = am::double('Iverberk\Larasearch\Index', ['refresh' => null, 'clean' => null, 'updateAliases' => null]); 253 | $indexMock = m::mock('Iverberk\Larasearch\Index'); 254 | 255 | /** 256 | * 257 | * Expectation 258 | * 259 | */ 260 | $this->index->shouldReceive('getName')->andReturn('Husband'); 261 | $this->index->shouldReceive('exists')->once()->andReturn(true); 262 | $this->index->shouldReceive('delete')->once()->andReturn(); 263 | 264 | App::shouldReceive('make') 265 | ->with('iverberk.larasearch.index', ['name' => 'Husband_9999', 'proxy' => $this->proxy]) 266 | ->andReturn($indexMock); 267 | 268 | $indexMock->shouldReceive('create')->once()->andReturn(); 269 | $indexMock->shouldReceive('aliasExists')->once()->andReturn(false); 270 | $indexMock->shouldReceive('import')->andReturn(); 271 | 272 | /** 273 | * 274 | * Assertion 275 | * 276 | */ 277 | $this->proxy->reindex(); 278 | 279 | $indexDouble->verifyInvoked('refresh', 'Husband'); 280 | $indexDouble->verifyInvoked('clean', 'Husband'); 281 | } 282 | 283 | /** 284 | * @test 285 | */ 286 | public function it_can_reindex_when_alias_exists() 287 | { 288 | /** 289 | * 290 | * Set 291 | * 292 | */ 293 | $indexDouble = am::double('Iverberk\Larasearch\Index', [ 294 | 'refresh' => null, 295 | 'clean' => null, 296 | 'updateAliases' => null, 297 | 'getAlias' => ['mockIndex' => 'aliases'] 298 | ]); 299 | $indexMock = m::mock('Iverberk\Larasearch\Index'); 300 | 301 | $operations[] = [ 302 | 'add' => [ 303 | 'alias' => 'Husband', 304 | 'index' => 'Husband_9999' 305 | ], 306 | 'remove' => [ 307 | 'alias' => 'Husband', 308 | 'index' => 'mockIndex' 309 | ] 310 | ]; 311 | 312 | $actions[] = ['actions' => $operations]; 313 | $test = $this; 314 | 315 | /** 316 | * 317 | * Expectation 318 | * 319 | */ 320 | $this->index->shouldReceive('getName')->andReturn('Husband'); 321 | 322 | $indexMock->shouldReceive('create')->once()->andReturn(); 323 | $indexMock->shouldReceive('aliasExists')->once()->andReturn(true); 324 | $indexMock->shouldReceive('import')->andReturn(); 325 | 326 | App::shouldReceive('make') 327 | ->with('iverberk.larasearch.index', ['name' => 'Husband_9999', 'proxy' => $this->proxy]) 328 | ->andReturn($indexMock); 329 | 330 | /** 331 | * 332 | * Assertion 333 | * 334 | */ 335 | $this->proxy->reindex(); 336 | 337 | $indexDouble->verifyInvoked('refresh', 'Husband'); 338 | $indexDouble->verifyInvoked('clean', 'Husband'); 339 | $indexDouble->verifyInvoked('updateAliases', function ($calls) use ($test, $actions) 340 | { 341 | $test->assertEquals($actions, $calls[0]); 342 | }); 343 | } 344 | 345 | /** 346 | * @test 347 | */ 348 | public function it_should_index() 349 | { 350 | $this->assertEquals(true, $this->proxy->shouldIndex()); 351 | } 352 | 353 | /** 354 | * @test 355 | */ 356 | public function it_should_refresh_docs() 357 | { 358 | /** 359 | * 360 | * Expectation 361 | * 362 | */ 363 | $this->model->shouldReceive('transform') 364 | ->with(true) 365 | ->andReturn('body'); 366 | 367 | $this->model->shouldReceive('getEsId') 368 | ->andReturn(1); 369 | 370 | $this->client->shouldReceive('index') 371 | ->with([ 372 | 'id' => '1', 373 | 'index' => 'Husband', 374 | 'type' => 'Husband', 375 | 'body' => 'body' 376 | ]) 377 | ->andReturn(); 378 | 379 | $this->index->shouldReceive('getName') 380 | ->andReturn('Husband'); 381 | 382 | /** 383 | * 384 | * Assertion 385 | * 386 | */ 387 | $this->proxy->refreshDoc($this->model); 388 | } 389 | 390 | /** 391 | * @test 392 | */ 393 | public function it_should_delete_docs() 394 | { 395 | /** 396 | * 397 | * Expectation 398 | * 399 | */ 400 | $this->client->shouldReceive('delete') 401 | ->with([ 402 | 'id' => 1, 403 | 'index' => 'Husband', 404 | 'type' => 'Husband' 405 | ]) 406 | ->andReturn(); 407 | 408 | $this->index->shouldReceive('getName') 409 | ->andReturn('Husband'); 410 | 411 | /** 412 | * 413 | * Assertion 414 | * 415 | */ 416 | $this->proxy->deleteDoc(1); 417 | } 418 | 419 | /** 420 | * @test 421 | */ 422 | public function it_should_enable_indexing_globally() 423 | { 424 | /** 425 | * Assertions 426 | */ 427 | $this->proxy->enableIndexing(); 428 | 429 | $this->assertEquals(\Husband::$__es_enable, true); 430 | } 431 | 432 | /** 433 | * @test 434 | */ 435 | public function it_should_disable_indexing_globally() 436 | { 437 | /** 438 | * Assertions 439 | */ 440 | $this->proxy->disableIndexing(); 441 | 442 | $this->assertEquals(\Husband::$__es_enable, false); 443 | } 444 | 445 | } 446 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Commands/PathsCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 56 | 57 | foreach ($models as $model) 58 | { 59 | $this->compilePaths(new $model); 60 | } 61 | 62 | if ($directories = $this->option('dir')) 63 | { 64 | $directoryModels = array_diff(Utils::findSearchableModels($directories), $models); 65 | 66 | foreach ($directoryModels as $model) 67 | { 68 | // Find paths for related models 69 | $this->compilePaths(new $model); 70 | } 71 | } 72 | 73 | if (!empty($models) || !empty($directoryModels)) 74 | { 75 | $this->writeConfig(); 76 | } else 77 | { 78 | $this->info('No models found.'); 79 | } 80 | } 81 | 82 | /** 83 | * @return array 84 | */ 85 | public function getPaths() 86 | { 87 | return $this->paths; 88 | } 89 | 90 | /** 91 | * @return array 92 | */ 93 | public function getReversedPaths() 94 | { 95 | return $this->reversedPaths; 96 | } 97 | 98 | /** 99 | * Get the console command arguments. 100 | * 101 | * @return array 102 | */ 103 | protected function getArguments() 104 | { 105 | return array( 106 | array('model', InputOption::VALUE_OPTIONAL, 'Eloquent model to find paths for', null) 107 | ); 108 | } 109 | 110 | /** 111 | * Get the console command options. 112 | * 113 | * @return array 114 | */ 115 | protected function getOptions() 116 | { 117 | return array( 118 | array('dir', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Directory to scan for searchable models', null, ''), 119 | array('relations', null, InputOption::VALUE_NONE, 'Include related Eloquent models', null), 120 | array('write-config', null, InputOption::VALUE_NONE, 'Include the compiled paths in the package configuration', null), 121 | ); 122 | } 123 | 124 | /** 125 | * Inspect all relations and build a (reverse) path for every relation. 126 | * This information is used to quickly determine which relations need to 127 | * be eager loaded on a model when (re)indexing. It also defines the document 128 | * structure for Elasticsearch. 129 | * 130 | * @param \Illuminate\Database\Eloquent\Model 131 | * @param string $ancestor 132 | * @param array $path 133 | * @param array $reversedPath 134 | * @param null $start 135 | */ 136 | protected function compilePaths(Model $model, $ancestor = null, $path = [], $reversedPath = [], $start = null) 137 | { 138 | // Initialize some variables if this is the first call 139 | if ($ancestor == null) $ancestor = $model; 140 | if ($start == null) $start = $model; 141 | 142 | $modelClass = get_class($model); 143 | 144 | // Initialize the found relations to an empty array 145 | $relations = []; 146 | 147 | if ($this->option('relations')) 148 | { 149 | // Find all related models 150 | $relatedModels = $this->getRelatedModels($model); 151 | 152 | foreach ($relatedModels as $related) 153 | { 154 | $newPath = $path; 155 | $newPath[] = $related['method']->name; 156 | 157 | // Check if we need to recurse for this related model 158 | if (!$related['model'] instanceof $ancestor && 159 | !$related['model'] instanceof $start && 160 | $this->checkDocHints($related['method']->getDocComment(), $start) 161 | ) 162 | { 163 | // Get the relations of the related model here, so 164 | // that we can build a reversed path for this relation 165 | $this->getRelatedModels($related['model']); 166 | 167 | $newReversedPath = $reversedPath; 168 | 169 | // Check if a reciprocal relation is found back to the original model 170 | if (!isset($this->relationClassMethods[get_class($related['model'])][$modelClass])) 171 | { 172 | // Check if we are possibly dealing with a polymorphic relation (reference to itself) 173 | if (array_key_exists(get_class($related['model']), $this->relationClassMethods[get_class($related['model'])])) 174 | { 175 | $model = get_class($related['model']); 176 | $newReversedPath[] = $this->relationClassMethods[$model][$model]; 177 | } else 178 | { 179 | throw new \RuntimeException("Reciprocal relation not found for model '" . get_class($related['model']) . "' from within '$modelClass' model"); 180 | } 181 | } else 182 | { 183 | $newReversedPath[] = $this->relationClassMethods[get_class($related['model'])][$modelClass]; 184 | } 185 | 186 | // Add this relation 187 | $relations[] = $related; 188 | 189 | $this->reversedPaths[$modelClass][] = implode('.', array_reverse($reversedPath)); 190 | $this->reversedPaths[$modelClass] = array_values(array_unique($this->reversedPaths[$modelClass])); 191 | 192 | $this->compilePaths($related['model'], $model, $newPath, $newReversedPath, $start); 193 | } 194 | } 195 | } 196 | 197 | // Found no more relations for this model so build the final path 198 | // and add the last inverse path segment 199 | if (empty($relations)) 200 | { 201 | 202 | if (!empty($path)) 203 | { 204 | $this->paths[get_class($start)][] = implode('.', $path); 205 | } else 206 | { 207 | $this->paths[get_class($start)] = []; 208 | } 209 | 210 | $this->reversedPaths[$modelClass][] = implode('.', array_reverse($reversedPath)); 211 | $this->reversedPaths[$modelClass] = array_values(array_unique($this->reversedPaths[$modelClass])); 212 | } 213 | } 214 | 215 | /** 216 | * Inspect the relation method annotations to see if we need to follow the relation 217 | * 218 | * @param string $docComment 219 | * @param $model 220 | * @return bool 221 | */ 222 | protected function checkDocHints($docComment, $model) 223 | { 224 | // Check if we never follow this relation 225 | if (preg_match('/@follow\s+NEVER/', $docComment)) return false; 226 | 227 | // Check if we follow the relation from the 'base' model 228 | if (preg_match('/@follow\s+UNLESS\s+' . str_replace('\\', '\\\\', get_class($model)) . '\b/', $docComment)) 229 | { 230 | return false; 231 | } 232 | 233 | if (preg_match('/@follow\s+FROM\b/', $docComment) && !preg_match('/@follow\s+FROM\s+' . str_replace('\\', '\\\\', get_class($model)) . '\b/', $docComment)) 234 | { 235 | return false; 236 | } 237 | 238 | // We follow the relation 239 | return true; 240 | } 241 | 242 | /** 243 | * Find related models from a base model 244 | * 245 | * @param $model 246 | * @return array 247 | */ 248 | protected function getRelatedModels(Model $model) 249 | { 250 | // Store the class name 251 | $modelClass = get_class($model); 252 | 253 | // Check if we already know the related models for this model 254 | if (!isset($this->relatedModels[$modelClass])) 255 | { 256 | $relatedModels = []; 257 | 258 | $methods = with(new \ReflectionClass($model))->getMethods(); 259 | 260 | // Iterate all class methods 261 | foreach ($methods as $method) 262 | { 263 | // Check if this method returns an Eloquent relation 264 | if ($method->class == $modelClass && 265 | preg_match('/@return\s+\\\\Illuminate\\\\Database\\\\Eloquent\\\\Relations/', $method->getDocComment()) 266 | ) 267 | { 268 | // Get the method name, so that we can call it on the model 269 | $relationMethod = $method->name; 270 | 271 | // Find the relation 272 | $relation = $model->$relationMethod(); 273 | 274 | if ($relation instanceof Relation) 275 | { 276 | // Find the related model 277 | $related = $relation->getRelated(); 278 | 279 | // Store the method to help build the inverse path 280 | $this->relationClassMethods[$modelClass][get_class($related)] = $relationMethod; 281 | 282 | $relatedModels[] = ['model' => $related, 'method' => $method]; 283 | } 284 | } 285 | } 286 | 287 | // Cache related models for this model 288 | $this->relatedModels[$modelClass] = $relatedModels; 289 | 290 | // Return the related models 291 | return $relatedModels; 292 | } else 293 | { 294 | // Return from cache 295 | return $this->relatedModels[$modelClass]; 296 | } 297 | } 298 | 299 | /** 300 | * Write the paths config to a file or to standard output 301 | * 302 | * @return void 303 | */ 304 | private function writeConfig() 305 | { 306 | if ($this->option('write-config')) 307 | { 308 | $configFile = base_path() . '/config/larasearch.php'; 309 | 310 | if ($this->getLaravel()) 311 | { 312 | if ( ! File::exists($configFile)) 313 | { 314 | if ($this->confirm('It appears that you have not yet published the larasearch config. Would you like to do this now?', false)) 315 | { 316 | $this->call('vendor:publish', ['--provider' => 'Iverberk\\Larasearch\LarasearchServiceProvider', '--tag' => 'config']); 317 | } 318 | else 319 | { 320 | return; 321 | } 322 | } 323 | } 324 | else 325 | { 326 | if ( ! File::exists($configFile)) 327 | { 328 | $this->info('Lumen application detected. Please copy the config manually to config/larasearch.php.'); 329 | } 330 | } 331 | 332 | File::put(dirname($configFile) . "/paths.json", json_encode(['paths' => $this->paths, 'reversedPaths' => $this->reversedPaths], JSON_PRETTY_PRINT)); 333 | 334 | $this->info('Paths file written to local package configuration'); 335 | } 336 | else 337 | { 338 | $this->info(json_encode(['paths' => $this->paths, 'reversedPaths' => $this->reversedPaths], JSON_PRETTY_PRINT)); 339 | } 340 | } 341 | 342 | } 343 | -------------------------------------------------------------------------------- /src/Iverberk/Larasearch/Index.php: -------------------------------------------------------------------------------- 1 | setProxy($proxy); 59 | $this->setName($name ?: $proxy->getModel()->getTable()); 60 | } 61 | 62 | /** 63 | * Import an Eloquent 64 | * 65 | * @param Model $model 66 | * @param array $relations 67 | * @param int $batchSize 68 | * @param callable $callback 69 | * @internal param $type 70 | */ 71 | public function import(Model $model, $relations = [], $batchSize = 750, Callable $callback = null) 72 | { 73 | $batch = 0; 74 | 75 | while (true) 76 | { 77 | // Increase the batch number 78 | $batch += 1; 79 | 80 | // Load records from the database 81 | $records = $model 82 | ->with($relations) 83 | ->skip($batchSize * ($batch - 1)) 84 | ->take($batchSize) 85 | ->get(); 86 | 87 | // Break out of the loop if we are out of records 88 | if (count($records) == 0) break; 89 | 90 | // Call the callback function to provide feedback on the import process 91 | if ($callback) 92 | { 93 | $callback($batch); 94 | } 95 | 96 | // Transform each record before sending it to Elasticsearch 97 | $data = []; 98 | 99 | foreach ($records as $record) 100 | { 101 | $data[] = [ 102 | 'index' => [ 103 | '_id' => $record->getEsId() 104 | ] 105 | ]; 106 | 107 | $data[] = $record->transform(!empty($relations)); 108 | } 109 | 110 | // Bulk import the data to Elasticsearch 111 | $this->bulk($data); 112 | } 113 | } 114 | 115 | /** 116 | * Set index name 117 | * 118 | * @param string 119 | * @return Index 120 | */ 121 | public function setName($name) 122 | { 123 | $index_prefix = Config::get('larasearch.elasticsearch.index_prefix', ''); 124 | if ($index_prefix && !Str::startsWith($name, $index_prefix)) $name = $index_prefix . $name; 125 | 126 | $this->name = $name; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Get index name 133 | * 134 | * @return string 135 | */ 136 | public function getName() 137 | { 138 | return strtolower($this->name); 139 | } 140 | 141 | /** 142 | * Set ElasticSearch Proxy for the index 143 | * 144 | * @param Proxy $proxy 145 | * @return \Iverberk\Larasearch\Proxy 146 | * @author Chris Nagle 147 | */ 148 | public function setProxy(Proxy $proxy) 149 | { 150 | $this->proxy = $proxy; 151 | 152 | return $proxy; 153 | } 154 | 155 | /** 156 | * Get ElasticSearch Proxy for the index 157 | * 158 | * @return \Iverberk\Larasearch\Proxy 159 | */ 160 | public function getProxy() 161 | { 162 | return $this->proxy; 163 | } 164 | 165 | /** 166 | * Create a new index 167 | * 168 | * @param array $options 169 | */ 170 | public function create($options = []) 171 | { 172 | $body = empty($options) ? $this->getDefaultIndexParams() : $options; 173 | 174 | self::getClient()->indices()->create(['index' => $this->getName(), 'body' => $body]); 175 | } 176 | 177 | /** 178 | * Delete an index 179 | */ 180 | public function delete() 181 | { 182 | self::getClient()->indices()->delete(['index' => $this->getName()]); 183 | } 184 | 185 | /** 186 | * Check if an index exists 187 | * 188 | * @return bool 189 | */ 190 | public function exists() 191 | { 192 | return self::getClient()->indices()->exists(['index' => $this->getName()]); 193 | } 194 | 195 | /** 196 | * Check if an alias exists 197 | * 198 | * @param $alias 199 | * @return bool 200 | */ 201 | public function aliasExists($alias) 202 | { 203 | $index_prefix = Config::get('larasearch.elasticsearch.index_prefix', ''); 204 | if ($index_prefix && !Str::startsWith($alias, $index_prefix)) $alias = $index_prefix . $alias; 205 | 206 | return self::getClient()->indices()->existsAlias(['name' => $alias]); 207 | } 208 | 209 | /** 210 | * Store a record in the index 211 | * 212 | * @param $record 213 | */ 214 | public function store($record) 215 | { 216 | $params['index'] = $this->getName(); 217 | $params['type'] = $record['type']; 218 | $params['id'] = $record['id']; 219 | $params['body'] = $record['data']; 220 | 221 | self::getClient()->index($params); 222 | } 223 | 224 | /** 225 | * Retrieve a record from the index 226 | * 227 | * @param $record 228 | */ 229 | public function retrieve($record) 230 | { 231 | $params['index'] = $this->getName(); 232 | $params['type'] = $record['type']; 233 | $params['id'] = $record['id']; 234 | 235 | self::getClient()->get($params); 236 | } 237 | 238 | /** 239 | * Remove a record from the index 240 | * 241 | * @param $record 242 | */ 243 | public function remove($record) 244 | { 245 | $params['index'] = $this->getName(); 246 | $params['type'] = $record['type']; 247 | $params['id'] = $record['id']; 248 | 249 | self::getClient()->delete($params); 250 | } 251 | 252 | /** 253 | * Inspect tokens returned from the analyzer 254 | * 255 | * @param string $text 256 | * @param array $options 257 | */ 258 | public function tokens($text, $options = []) 259 | { 260 | self::getClient()->indices()->analyze(array_merge(['index' => $this->getName(), 'text' => $text], $options)); 261 | } 262 | 263 | /** 264 | * @return array 265 | */ 266 | public function getParams() 267 | { 268 | return $this->params; 269 | } 270 | 271 | /** 272 | * @param $params 273 | */ 274 | public function setParams($params) 275 | { 276 | $this->params = $params; 277 | } 278 | 279 | /** 280 | * @param $records 281 | * @throws ImportException 282 | */ 283 | public function bulk($records) 284 | { 285 | $params['index'] = $this->getName(); 286 | $params['type'] = $this->getProxy()->getType(); 287 | $params['body'] = $records; 288 | 289 | $results = self::getClient()->bulk($params); 290 | 291 | if ($results['errors']) 292 | { 293 | $errorItems = []; 294 | 295 | foreach ($results['items'] as $item) 296 | { 297 | if (array_key_exists('error', $item['index'])) 298 | { 299 | $errorItems[] = $item; 300 | } 301 | } 302 | 303 | throw new ImportException('Bulk import with errors', 1, $errorItems); 304 | } 305 | } 306 | 307 | /** 308 | * Clean old indices that start with $name 309 | * 310 | * @param $name 311 | */ 312 | public static function clean($name) 313 | { 314 | $index_prefix = Config::get('larasearch.elasticsearch.index_prefix', ''); 315 | if ($index_prefix && !Str::startsWith($name, $index_prefix)) $name = $index_prefix . $name; 316 | 317 | $indices = self::getClient()->indices()->getAliases(); 318 | foreach ($indices as $index => $value) 319 | { 320 | if (empty($value['aliases']) && preg_match("/^${name}_\\d{14,17}$/", $index)) 321 | { 322 | self::getClient()->indices()->delete(['index' => $index]); 323 | } 324 | } 325 | } 326 | 327 | /** 328 | * Retrieve aliases 329 | * 330 | * @param $name 331 | * @return array 332 | */ 333 | public static function getAlias($name) 334 | { 335 | $index_prefix = Config::get('larasearch.elasticsearch.index_prefix', ''); 336 | if ($index_prefix && !Str::startsWith($name, $index_prefix)) $name = $index_prefix . $name; 337 | 338 | return self::getClient()->indices()->getAlias(['name' => $name]); 339 | } 340 | 341 | /** 342 | * @param array $actions 343 | * @return array 344 | */ 345 | public static function updateAliases(array $actions) 346 | { 347 | if (isset($actions['actions']) && ($index_prefix = Config::get('larasearch.elasticsearch.index_prefix', ''))) 348 | { 349 | foreach ($actions['actions'] as &$action) 350 | { 351 | list($verb, $data) = each($action); 352 | if (!Str::startsWith($data['index'], $index_prefix)) $action[$verb]['index'] = $index_prefix . $data['index']; 353 | if (!Str::startsWith($data['alias'], $index_prefix)) $action[$verb]['alias'] = $index_prefix . $data['alias']; 354 | } 355 | } 356 | 357 | return self::getClient()->indices()->updateAliases(['body' => $actions]); 358 | } 359 | 360 | /** 361 | * Refresh an index 362 | * 363 | * @param $index 364 | * @return array 365 | */ 366 | public static function refresh($index) 367 | { 368 | $index_prefix = Config::get('larasearch.elasticsearch.index_prefix', ''); 369 | if ($index_prefix && !Str::startsWith($index, $index_prefix)) $index = $index_prefix . $index; 370 | 371 | return self::getClient()->indices()->refresh(['index' => $index]); 372 | } 373 | 374 | /** 375 | * Initialize the default index settings and mappings 376 | * 377 | * @return array 378 | */ 379 | private function getDefaultIndexParams() 380 | { 381 | $analyzers = Config::get('larasearch.elasticsearch.analyzers'); 382 | $params = Config::get('larasearch.elasticsearch.defaults.index'); 383 | $mapping = []; 384 | 385 | $mapping_options = array_combine( 386 | $analyzers, 387 | array_map(function ($type) 388 | { 389 | $config = $this->getProxy()->getConfig(); 390 | 391 | // Maintain backwards compatibility by allowing a plain array of analyzer => fields 392 | $field_mappings = Utils::findKey($config, $type, false) ?: []; 393 | 394 | // Also read from a dedicated array key called 'analyzers' 395 | if (isset($config['analyzers'])) 396 | { 397 | $field_mappings = array_merge($field_mappings, Utils::findKey($config['analyzers'], $type, false) ?: []); 398 | } 399 | 400 | return $field_mappings; 401 | }, 402 | $analyzers 403 | ) 404 | ); 405 | 406 | foreach (array_unique(array_flatten(array_values($mapping_options))) as $field) 407 | { 408 | // Extract path segments from dot separated field 409 | $pathSegments = explode('.', $field); 410 | 411 | // Last element is the field name 412 | $fieldName = array_pop($pathSegments); 413 | 414 | // Apply default field mapping 415 | $fieldMapping = [ 416 | 'type' => "multi_field", 417 | 'fields' => [ 418 | $fieldName => [ 419 | 'type' => 'string', 420 | 'index' => 'not_analyzed' 421 | ], 422 | 'analyzed' => [ 423 | 'type' => 'string', 424 | 'index' => 'analyzed' 425 | ] 426 | ] 427 | ]; 428 | 429 | // Check if we need to add additional mappings 430 | foreach ($mapping_options as $type => $fields) 431 | { 432 | if (in_array($field, $fields)) 433 | { 434 | $fieldMapping['fields'][$type] = [ 435 | 'type' => 'string', 436 | 'index' => 'analyzed', 437 | 'analyzer' => "larasearch_${type}_index" 438 | ]; 439 | } 440 | } 441 | 442 | if (!empty($pathSegments)) 443 | { 444 | $mapping = Utils::array_merge_recursive_distinct( 445 | $mapping, 446 | $this->getNestedFieldMapping($fieldName, $fieldMapping, $pathSegments) 447 | ); 448 | } else 449 | { 450 | $mapping[$fieldName] = $fieldMapping; 451 | } 452 | } 453 | 454 | if (!empty($mapping)) $params['mappings']['_default_']['properties'] = $mapping; 455 | 456 | $params['index'] = $this->getName(); 457 | $params['type'] = $this->getProxy()->getType(); 458 | 459 | return $params; 460 | } 461 | 462 | /** 463 | * @param $fieldName 464 | * @param $fieldMapping 465 | * @param $pathSegments 466 | * @return array 467 | */ 468 | private function getNestedFieldMapping($fieldName, $fieldMapping, $pathSegments) 469 | { 470 | $nested = []; 471 | $current = array_pop($pathSegments); 472 | 473 | // Create the first level 474 | $nested[$current] = [ 475 | 'type' => 'object', 476 | 'properties' => [ 477 | $fieldName => $fieldMapping 478 | ] 479 | ]; 480 | 481 | // Add any additional levels 482 | foreach (array_reverse($pathSegments) as $pathSegment) 483 | { 484 | $nested[$pathSegment] = [ 485 | 'type' => 'object', 486 | 'properties' => $nested 487 | ]; 488 | 489 | unset($nested[$current]); 490 | $current = $pathSegment; 491 | } 492 | 493 | return $nested; 494 | } 495 | 496 | } 497 | -------------------------------------------------------------------------------- /tests/Iverberk/Larasearch/QueryTest.php: -------------------------------------------------------------------------------- 1 | getMocks(); 26 | $test = $this; 27 | 28 | $query = m::mock('Iverberk\Larasearch\Query', [$proxy, 'term'])->makePartial(); 29 | 30 | /** 31 | * 32 | * Expectation 33 | * 34 | */ 35 | $client->shouldReceive('search') 36 | ->andReturnUsing(function ($params) use ($test) 37 | { 38 | $test->assertEquals(json_decode( 39 | '{ 40 | "index": "Husband", 41 | "type": "Husband", 42 | "body": { 43 | "size": 50, 44 | "from": 0, 45 | "query": { 46 | "dis_max": { 47 | "queries": [ 48 | { 49 | "match": { 50 | "_all": { 51 | "query": "term", 52 | "operator": "and", 53 | "boost": 10, 54 | "analyzer": "larasearch_search" 55 | } 56 | } 57 | }, 58 | { 59 | "match": { 60 | "_all": { 61 | "query": "term", 62 | "operator": "and", 63 | "boost": 10, 64 | "analyzer": "larasearch_search2" 65 | } 66 | } 67 | } 68 | ] 69 | } 70 | } 71 | } 72 | }', true) 73 | , 74 | $params); 75 | 76 | return []; 77 | }); 78 | 79 | /** 80 | * 81 | * Assertion 82 | * 83 | */ 84 | $response = $query->execute(); 85 | 86 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 87 | } 88 | 89 | /** 90 | * @test 91 | */ 92 | public function it_should_search_on_term_with_exact_field() 93 | { 94 | /** 95 | * 96 | * Set 97 | * 98 | * @var \Mockery\Mock $client 99 | */ 100 | list($proxy, $client, $model) = $this->getMocks(); 101 | $test = $this; 102 | 103 | $query = m::mock('Iverberk\Larasearch\Query', [ 104 | $proxy, 105 | 'term', 106 | [ 107 | 'fields' => ['name' => 'exact'] 108 | ] 109 | ])->makePartial(); 110 | 111 | /** 112 | * 113 | * Expectation 114 | * 115 | */ 116 | $client->shouldReceive('search') 117 | ->andReturnUsing(function ($params) use ($test) 118 | { 119 | $test->assertEquals(json_decode( 120 | '{ 121 | "index": "Husband", 122 | "type": "Husband", 123 | "body": { 124 | "size": 50, 125 | "from": 0, 126 | "query": { 127 | "dis_max": { 128 | "queries": [ 129 | { 130 | "match": { 131 | "name": { 132 | "query": "term", 133 | "operator": "and", 134 | "boost": 1, 135 | "analyzer": "keyword" 136 | } 137 | } 138 | } 139 | ] 140 | } 141 | } 142 | } 143 | }', true) 144 | , 145 | $params); 146 | 147 | return []; 148 | }); 149 | 150 | /** 151 | * 152 | * Assertion 153 | * 154 | */ 155 | $response = $query->execute(); 156 | 157 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 158 | } 159 | 160 | /** 161 | * @test 162 | */ 163 | public function it_should_search_with_term_and_misspellings() 164 | { 165 | /** 166 | * 167 | * Set 168 | * 169 | * @var \Mockery\Mock $client 170 | */ 171 | list($proxy, $client, $model) = $this->getMocks(); 172 | $test = $this; 173 | 174 | $query = m::mock('Iverberk\Larasearch\Query', [ 175 | $proxy, 176 | 'term', 177 | [ 178 | 'misspellings' => true 179 | ] 180 | ])->makePartial(); 181 | 182 | /** 183 | * 184 | * Expectation 185 | * 186 | */ 187 | $client->shouldReceive('search') 188 | ->andReturnUsing(function ($params) use ($test) 189 | { 190 | $test->assertEquals(json_decode( 191 | '{ 192 | "index": "Husband", 193 | "type": "Husband", 194 | "body": { 195 | "size": 50, 196 | "from": 0, 197 | "query": { 198 | "dis_max": { 199 | "queries": [ 200 | { 201 | "match": { 202 | "_all": { 203 | "query": "term", 204 | "operator": "and", 205 | "boost": 10, 206 | "analyzer": "larasearch_search" 207 | } 208 | } 209 | }, 210 | { 211 | "match": { 212 | "_all": { 213 | "query": "term", 214 | "operator": "and", 215 | "boost": 10, 216 | "analyzer": "larasearch_search2" 217 | } 218 | } 219 | }, 220 | { 221 | "match": { 222 | "_all": { 223 | "query": "term", 224 | "operator": "and", 225 | "boost": 1, 226 | "fuzziness": 1, 227 | "max_expansions": 3, 228 | "analyzer": "larasearch_search" 229 | } 230 | } 231 | }, 232 | { 233 | "match": { 234 | "_all": { 235 | "query": "term", 236 | "operator": "and", 237 | "boost": 1, 238 | "fuzziness": 1, 239 | "max_expansions": 3, 240 | "analyzer": "larasearch_search2" 241 | } 242 | } 243 | } 244 | ] 245 | } 246 | } 247 | } 248 | }', true), 249 | $params); 250 | 251 | return []; 252 | }); 253 | 254 | /** 255 | * 256 | * Assertion 257 | * 258 | */ 259 | $response = $query->execute(); 260 | 261 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 262 | } 263 | 264 | /** 265 | * @test 266 | */ 267 | public function it_should_search_on_fields_with_term() 268 | { 269 | /** 270 | * 271 | * Set 272 | * 273 | * @var \Mockery\Mock $client 274 | */ 275 | list($proxy, $client, $model) = $this->getMocks(); 276 | $test = $this; 277 | 278 | $query = m::mock('Iverberk\Larasearch\Query', [ 279 | $proxy, 280 | 'term', 281 | [ 282 | 'fields' => ['name' => 'word_start', 'wife.name'] 283 | ] 284 | ])->makePartial(); 285 | 286 | /** 287 | * 288 | * Expectation 289 | * 290 | */ 291 | $client->shouldReceive('search') 292 | ->andReturnUsing(function ($params) use ($test) 293 | { 294 | $test->assertEquals(json_decode( 295 | '{ 296 | "index": "Husband", 297 | "type": "Husband", 298 | "body": { 299 | "size": 50, 300 | "from": 0, 301 | "query": { 302 | "dis_max": { 303 | "queries": [ 304 | { 305 | "match": { 306 | "name.word_start": { 307 | "query": "term", 308 | "operator": "and", 309 | "boost": 1, 310 | "analyzer": "larasearch_word_search" 311 | } 312 | } 313 | }, 314 | { 315 | "match": { 316 | "wife.name.analyzed": { 317 | "query": "term", 318 | "operator": "and", 319 | "boost": 10, 320 | "analyzer": "larasearch_search" 321 | } 322 | } 323 | }, 324 | { 325 | "match": { 326 | "wife.name.analyzed": { 327 | "query": "term", 328 | "operator": "and", 329 | "boost": 10, 330 | "analyzer": "larasearch_search2" 331 | } 332 | } 333 | } 334 | ] 335 | } 336 | } 337 | } 338 | }', true) 339 | , 340 | $params); 341 | 342 | return []; 343 | }); 344 | 345 | /** 346 | * 347 | * Assertion 348 | * 349 | */ 350 | $response = $query->execute(); 351 | 352 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 353 | } 354 | 355 | /** 356 | * @test 357 | */ 358 | public function it_should_search_on_term_with_autocomplete() 359 | { 360 | /** 361 | * 362 | * Set 363 | * 364 | * @var \Mockery\Mock $client 365 | */ 366 | list($proxy, $client, $model) = $this->getMocks(); 367 | $test = $this; 368 | 369 | $query = m::mock('Iverberk\Larasearch\Query', [ 370 | $proxy, 371 | 'term', 372 | [ 373 | 'autocomplete' => true 374 | ] 375 | ])->makePartial(); 376 | 377 | /** 378 | * 379 | * Expectation 380 | * 381 | */ 382 | $client->shouldReceive('search') 383 | ->andReturnUsing(function ($params) use ($test) 384 | { 385 | $test->assertEquals(json_decode( 386 | '{ 387 | "index": "Husband", 388 | "type": "Husband", 389 | "body": { 390 | "size": 50, 391 | "from": 0, 392 | "query": { 393 | "multi_match": { 394 | "fields": [ 395 | "name.autocomplete", 396 | "wife.name.autocomplete" 397 | ], 398 | "query": "term", 399 | "analyzer": "larasearch_autocomplete_search" 400 | } 401 | } 402 | } 403 | }', true) 404 | , 405 | $params); 406 | 407 | return []; 408 | }); 409 | 410 | /** 411 | * 412 | * Assertion 413 | * 414 | */ 415 | $response = $query->execute(); 416 | 417 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 418 | } 419 | 420 | /** 421 | * @test 422 | */ 423 | public function it_should_search_on_fields_with_term_and_autocomplete() 424 | { 425 | /** 426 | * 427 | * Set 428 | * 429 | * @var \Mockery\Mock $client 430 | */ 431 | list($proxy, $client, $model) = $this->getMocks(); 432 | $test = $this; 433 | 434 | $query = m::mock('Iverberk\Larasearch\Query', [ 435 | $proxy, 436 | 'term', 437 | [ 438 | 'fields' => ['wife.name'], 439 | 'autocomplete' => true 440 | ] 441 | ])->makePartial(); 442 | 443 | /** 444 | * 445 | * Expectation 446 | * 447 | */ 448 | $client->shouldReceive('search') 449 | ->andReturnUsing(function ($params) use ($test) 450 | { 451 | $test->assertEquals(json_decode( 452 | '{ 453 | "index": "Husband", 454 | "type": "Husband", 455 | "body": { 456 | "size": 50, 457 | "from": 0, 458 | "query": { 459 | "multi_match": { 460 | "fields": [ 461 | "wife.name.autocomplete" 462 | ], 463 | "query": "term", 464 | "analyzer": "larasearch_autocomplete_search" 465 | } 466 | } 467 | } 468 | }', true) 469 | , 470 | $params); 471 | 472 | return []; 473 | }); 474 | 475 | /** 476 | * 477 | * Assertion 478 | * 479 | */ 480 | $response = $query->execute(); 481 | 482 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 483 | } 484 | 485 | /** 486 | * @test 487 | */ 488 | public function it_should_search_on_fields_with_term_and_select() 489 | { 490 | /** 491 | * 492 | * Set 493 | * 494 | * @var \Mockery\Mock $client 495 | */ 496 | list($proxy, $client, $model) = $this->getMocks(); 497 | $test = $this; 498 | 499 | $query = m::mock('Iverberk\Larasearch\Query', [ 500 | $proxy, 501 | 'term', 502 | [ 503 | 'fields' => ['wife.name'], 504 | 'select' => ['name'] 505 | ] 506 | ])->makePartial(); 507 | 508 | /** 509 | * 510 | * Expectation 511 | * 512 | */ 513 | $client->shouldReceive('search') 514 | ->andReturnUsing(function ($params) use ($test) 515 | { 516 | $test->assertEquals(json_decode( 517 | '{ 518 | "index": "Husband", 519 | "type": "Husband", 520 | "body": { 521 | "size": 50, 522 | "from": 0, 523 | "query": { 524 | "dis_max": { 525 | "queries": [ 526 | { 527 | "match": { 528 | "wife.name.analyzed": { 529 | "query": "term", 530 | "operator": "and", 531 | "boost": 10, 532 | "analyzer": "larasearch_search" 533 | } 534 | } 535 | }, 536 | { 537 | "match": { 538 | "wife.name.analyzed": { 539 | "query": "term", 540 | "operator": "and", 541 | "boost": 10, 542 | "analyzer": "larasearch_search2" 543 | } 544 | } 545 | } 546 | ] 547 | } 548 | }, 549 | "fields": [ 550 | "name" 551 | ] 552 | } 553 | }', true) 554 | , 555 | $params); 556 | 557 | return []; 558 | }); 559 | 560 | /** 561 | * 562 | * Assertion 563 | * 564 | */ 565 | $response = $query->execute(); 566 | 567 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 568 | } 569 | 570 | /** 571 | * @test 572 | */ 573 | public function it_should_search_on_fields_with_term_and_load() 574 | { 575 | /** 576 | * 577 | * Set 578 | * 579 | * @var \Mockery\Mock $client 580 | */ 581 | list($proxy, $client, $model) = $this->getMocks(); 582 | $test = $this; 583 | 584 | $query = m::mock('Iverberk\Larasearch\Query', [ 585 | $proxy, 586 | 'term', 587 | [ 588 | 'fields' => ['wife.name'], 589 | 'load' => ['name'] 590 | ] 591 | ])->makePartial(); 592 | 593 | /** 594 | * 595 | * Expectation 596 | * 597 | */ 598 | $client->shouldReceive('search') 599 | ->andReturnUsing(function ($params) use ($test) 600 | { 601 | $test->assertEquals(json_decode( 602 | '{ 603 | "index": "Husband", 604 | "type": "Husband", 605 | "body": { 606 | "size": 50, 607 | "from": 0, 608 | "query": { 609 | "dis_max": { 610 | "queries": [ 611 | { 612 | "match": { 613 | "wife.name.analyzed": { 614 | "query": "term", 615 | "operator": "and", 616 | "boost": 10, 617 | "analyzer": "larasearch_search" 618 | } 619 | } 620 | }, 621 | { 622 | "match": { 623 | "wife.name.analyzed": { 624 | "query": "term", 625 | "operator": "and", 626 | "boost": 10, 627 | "analyzer": "larasearch_search2" 628 | } 629 | } 630 | } 631 | ] 632 | } 633 | }, 634 | "fields": [ 635 | "name" 636 | ] 637 | } 638 | }', true) 639 | , 640 | $params); 641 | 642 | return []; 643 | }); 644 | 645 | /** 646 | * 647 | * Assertion 648 | * 649 | */ 650 | $response = $query->execute(); 651 | 652 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 653 | } 654 | 655 | /** 656 | * @test 657 | */ 658 | public function it_should_search_on_fields_and_highlight() 659 | { 660 | /** 661 | * 662 | * Set 663 | * 664 | * @var \Mockery\Mock $client 665 | */ 666 | list($proxy, $client, $model) = $this->getMocks(); 667 | $test = $this; 668 | 669 | $query = m::mock('Iverberk\Larasearch\Query', [ 670 | $proxy, 671 | 'term', 672 | [ 673 | 'fields' => ['name'], 674 | 'highlight' => [ 675 | 'tag' => '' 676 | ] 677 | ] 678 | ])->makePartial(); 679 | 680 | /** 681 | * 682 | * Expectation 683 | * 684 | */ 685 | $client->shouldReceive('search') 686 | ->andReturnUsing(function ($params) use ($test) 687 | { 688 | $query = json_decode( 689 | '{ 690 | "index": "Husband", 691 | "type": "Husband", 692 | "body": { 693 | "size": 50, 694 | "from": 0, 695 | "highlight": { 696 | "fields": { 697 | "name.analyzed": {} 698 | }, 699 | "pre_tags": [ 700 | "" 701 | ], 702 | "post_tags": [ 703 | "<\/b>" 704 | ] 705 | }, 706 | "query": { 707 | "dis_max": { 708 | "queries": [ 709 | { 710 | "match": { 711 | "name.analyzed": { 712 | "query": "term", 713 | "operator": "and", 714 | "boost": 10, 715 | "analyzer": "larasearch_search" 716 | } 717 | } 718 | }, 719 | { 720 | "match": { 721 | "name.analyzed": { 722 | "query": "term", 723 | "operator": "and", 724 | "boost": 10, 725 | "analyzer": "larasearch_search2" 726 | } 727 | } 728 | } 729 | ] 730 | } 731 | } 732 | } 733 | }', true); 734 | 735 | // Mark explicit stdclass 736 | $query['body']['highlight']['fields']['name.analyzed'] = new \StdClass; 737 | $test->assertEquals( 738 | $query, 739 | $params); 740 | 741 | return []; 742 | }); 743 | 744 | /** 745 | * 746 | * Assertion 747 | * 748 | */ 749 | $response = $query->execute(); 750 | 751 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 752 | } 753 | 754 | /** 755 | * @test 756 | */ 757 | public function it_should_search_on_fields_with_suggestions() 758 | { 759 | /** 760 | * 761 | * Set 762 | * 763 | * @var \Mockery\Mock $client 764 | */ 765 | list($proxy, $client, $model) = $this->getMocks(); 766 | $test = $this; 767 | 768 | $query = m::mock('Iverberk\Larasearch\Query', [ 769 | $proxy, 770 | 'term', 771 | [ 772 | 'fields' => ['name'], 773 | 'suggest' => true 774 | ] 775 | ])->makePartial(); 776 | 777 | /** 778 | * 779 | * Expectation 780 | * 781 | */ 782 | $client->shouldReceive('search') 783 | ->andReturnUsing(function ($params) use ($test) 784 | { 785 | $test->assertEquals(json_decode( 786 | '{ 787 | "index": "Husband", 788 | "type": "Husband", 789 | "body": { 790 | "size": 50, 791 | "from": 0, 792 | "suggest": { 793 | "text": "term", 794 | "name": { 795 | "phrase": { 796 | "field": "name.suggest" 797 | } 798 | } 799 | }, 800 | "query": { 801 | "dis_max": { 802 | "queries": [ 803 | { 804 | "match": { 805 | "name.analyzed": { 806 | "query": "term", 807 | "operator": "and", 808 | "boost": 10, 809 | "analyzer": "larasearch_search" 810 | } 811 | } 812 | }, 813 | { 814 | "match": { 815 | "name.analyzed": { 816 | "query": "term", 817 | "operator": "and", 818 | "boost": 10, 819 | "analyzer": "larasearch_search2" 820 | } 821 | } 822 | } 823 | ] 824 | } 825 | } 826 | } 827 | }', true), 828 | $params); 829 | 830 | return []; 831 | }); 832 | 833 | /** 834 | * 835 | * Assertion 836 | * 837 | */ 838 | $response = $query->execute(); 839 | 840 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 841 | } 842 | 843 | /** 844 | * @test 845 | */ 846 | public function it_should_search_with_aggregations() 847 | { 848 | /** 849 | * 850 | * Set 851 | * 852 | * @var \Mockery\Mock $client 853 | */ 854 | list($proxy, $client, $model) = $this->getMocks(); 855 | $test = $this; 856 | 857 | $query = m::mock('Iverberk\Larasearch\Query', [ 858 | $proxy, 859 | '*', 860 | [ 861 | 'aggs' => [ 862 | 'agg_name' => [ 863 | 'type' => 'terms', 864 | 'field' => 'name' 865 | ] 866 | ] 867 | ] 868 | ])->makePartial(); 869 | 870 | /** 871 | * 872 | * Expectation 873 | * 874 | */ 875 | $client->shouldReceive('search') 876 | ->andReturnUsing(function ($params) use ($test) 877 | { 878 | $test->assertEquals(json_decode( 879 | '{ 880 | "index": "Husband", 881 | "type": "Husband", 882 | "body": { 883 | "size": 50, 884 | "from": 0, 885 | "aggs": { 886 | "agg_name": { 887 | "terms": { 888 | "field": "name", 889 | "size": 0 890 | } 891 | } 892 | }, 893 | "query": { 894 | "match_all": [ 895 | 896 | ] 897 | } 898 | } 899 | }', true), 900 | $params); 901 | 902 | return []; 903 | }); 904 | 905 | /** 906 | * 907 | * Assertion 908 | * 909 | */ 910 | $response = $query->execute(); 911 | 912 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 913 | } 914 | 915 | /** 916 | * @test 917 | */ 918 | public function it_should_search_with_sort() 919 | { 920 | /** 921 | * Set 922 | * @var \Mockery\Mock $client 923 | */ 924 | list($proxy, $client, $model) = $this->getMocks(); 925 | $test = $this; 926 | 927 | $query = m::mock('Iverberk\Larasearch\Query', [$proxy, 'term', ['sort' => 'name']])->makePartial(); 928 | 929 | /** 930 | * Expectation 931 | */ 932 | 933 | $client->shouldReceive('search') 934 | ->andReturnUsing(function ($params) use ($test) 935 | { 936 | $test->assertEquals(json_decode( 937 | '{ 938 | "index": "Husband", 939 | "type": "Husband", 940 | "body": { 941 | "size": 50, 942 | "from": 0, 943 | "sort": "name", 944 | "query": { 945 | "dis_max": { 946 | "queries": [ 947 | { 948 | "match": { 949 | "_all": { 950 | "query": "term", 951 | "operator": "and", 952 | "boost": 10, 953 | "analyzer": "larasearch_search" 954 | } 955 | } 956 | }, 957 | { 958 | "match": { 959 | "_all": { 960 | "query": "term", 961 | "operator": "and", 962 | "boost": 10, 963 | "analyzer": "larasearch_search2" 964 | } 965 | } 966 | } 967 | ] 968 | } 969 | } 970 | } 971 | }', true), $params); 972 | return []; 973 | }); 974 | 975 | /** 976 | * Assertion 977 | */ 978 | $response = $query->execute(); 979 | 980 | $this->assertInstanceOf('Iverberk\Larasearch\Response', $response); 981 | } 982 | 983 | 984 | /** 985 | * @test 986 | * @expectedException \InvalidArgumentException 987 | */ 988 | public function it_should_throw_an_exception_when_multiple_queries_are_provided() 989 | { 990 | /** 991 | * 992 | * Set 993 | * 994 | * @var \Mockery\Mock $client 995 | */ 996 | list($proxy, $client, $model) = $this->getMocks(); 997 | $test = $this; 998 | 999 | $query = m::mock('Iverberk\Larasearch\Query', [ 1000 | $proxy, 1001 | '*', 1002 | [ 1003 | 'json' => '{}', 1004 | 'query' => [] 1005 | ] 1006 | ])->makePartial(); 1007 | 1008 | /** 1009 | * 1010 | * Assertion 1011 | * 1012 | */ 1013 | $query->execute(); 1014 | } 1015 | 1016 | /** 1017 | * Construct an helper mocks 1018 | * 1019 | * @return array 1020 | */ 1021 | private function getMocks() 1022 | { 1023 | $client = m::mock('Elasticsearch\Client'); 1024 | $model = m::mock('Illuminate\Database\Eloquent\Model'); 1025 | 1026 | $proxy = m::mock('Iverberk\Larasearch\Proxy'); 1027 | $proxy->shouldReceive('getClient') 1028 | ->andReturn($client); 1029 | $proxy->shouldReceive('getModel') 1030 | ->andReturn($model); 1031 | $proxy->shouldReceive('getIndex->getName') 1032 | ->andReturn('Husband'); 1033 | $proxy->shouldReceive('getType') 1034 | ->andReturn('Husband'); 1035 | $proxy->shouldReceive('getConfig') 1036 | ->andReturn([ 1037 | 'autocomplete' => ['name', 'wife.name'], 1038 | 1039 | 'suggest' => ['name'], 1040 | 1041 | 'text_start' => ['name', 'wife.children.name'], 1042 | 'text_middle' => ['name', 'wife.children.name'], 1043 | 'text_end' => ['name', 'wife.children.name'], 1044 | 1045 | 'word_start' => ['name', 'wife.children.name'], 1046 | 'word_middle' => ['name', 'wife.children.name'], 1047 | 'word_end' => ['name', 'wife.children.name'] 1048 | ]); 1049 | 1050 | return [$proxy, $client, $model]; 1051 | } 1052 | 1053 | } 1054 | --------------------------------------------------------------------------------