├── 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('/\A', '', $tag)];
148 | }
149 | }
150 | }
151 |
152 | /**
153 | * Add requested suggestions to the payload
154 | */
155 | private function getSuggest()
156 | {
157 | if ($suggestions = Utils::findKey($this->options, '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 |
--------------------------------------------------------------------------------