`
99 |
100 | Flush all of the model documents from the index.
101 |
102 | Arguments:
103 |
104 | ```
105 | model Name or comma separated names of the model(s) to index
106 | ```
107 |
108 | #### `search:queue`
109 |
110 | Reduces the number of calls made to the CloudSearch server by queueing the updates and deletes.
111 |
112 | ## Indexing
113 |
114 | Once you have added the `LaravelCloudSearch\Eloquent\Searchable` trait to a model, all you need to do is save a model instance and it will automatically be added to your index when the `search:queue` command is ran.
115 |
116 | ```php
117 | $post = new App\Post;
118 |
119 | // ...
120 |
121 | $post->save();
122 | ```
123 |
124 | > **Note**: if the model document has already been indexed, then it will simply be updated. If it does not exist, it will be added.
125 |
126 | ## Updating Documents
127 |
128 | To update an index model, you only need to update the model instance's properties and `save`` the model to your database. The package will automatically persist the changes to your search index:
129 |
130 | ```php
131 | $post = App\Post::find(1);
132 |
133 | // Update the post...
134 |
135 | $post->save();
136 | ```
137 |
138 | ## Removing Documents
139 |
140 | To remove a document from your index, simply `delete` the model from the database. This form of removal is even compatible with **soft deleted** models:
141 |
142 | ```php
143 | $post = App\Post::find(1);
144 |
145 | $post->delete();
146 | ```
147 |
148 | ## Searching
149 |
150 | You may begin searching a model using the `search` method. The search method accepts a single string that will be used to search your models. You should then chain the `get` method onto the search query to retrieve the Eloquent models that match the given search query:
151 |
152 | ```php
153 | $posts = App\Post::search('Kitten fluff')->get();
154 | ```
155 |
156 | Since package searches return a collection of Eloquent models, you may even return the results directly from a route or controller and they will automatically be converted to JSON:
157 |
158 | ```php
159 | use Illuminate\Http\Request;
160 |
161 | Route::get('/search', function (Request $request) {
162 | return App\Post::search($request->search)->get();
163 | });
164 | ```
165 |
166 | ## Pagination
167 |
168 | In addition to retrieving a collection of models, you may paginate your search results using the `paginate` method. This method will return a `Paginator` instance just as if you had paginated a traditional Eloquent query:
169 |
170 | ```php
171 | $posts = App\Post::search('Kitten fluff')->paginate();
172 | ```
173 | You may specify how many models to retrieve per page by passing the amount as the first argument to the `paginate` method:
174 |
175 | ```php
176 | $posts = App\Post::search('Kitten fluff')->paginate(15);
177 | ```
178 | Once you have retrieved the results, you may display the results and render the page links using Blade just as if you had paginated a traditional Eloquent query:
179 |
180 | ```blade
181 |
182 | @foreach ($posts as $post)
183 | {{ $post->title }}
184 | @endforeach
185 |
186 |
187 | {{ $posts->links() }}
188 | ```
189 |
190 | ## Basic Builder Usage
191 |
192 | Initialize a builder instance:
193 |
194 | ```php
195 | $query = app(\LaravelCloudSearch\CloudSearcher::class)->newQuery();
196 | ```
197 |
198 | You can chain query methods like so:
199 |
200 | ```php
201 | $query->phrase('ford')
202 | ->term('National Equipment', 'seller')
203 | ->range('year', '2010');
204 | ```
205 |
206 | use the `get()` or `paginate()` methods to submit query and retrieve results from AWS.
207 |
208 | ```php
209 | $results = $query->get();
210 | ```
211 |
212 | In the example above we did not set the search type, so this means the results that are returned will match any document on CloudSearch domain. To refine you search to certain model, either use the model like shown in the example previously or use the `searchableType()` method to set the class name of the model (this is done automatically in the model instance call):
213 |
214 | ```php
215 | $query = app(\LaravelCloudSearch\CloudSearcher::class)->newQuery();
216 |
217 | $results = $query->searchableType(\App\LawnMower::class)
218 | ->term('honda', 'name')
219 | ->get();
220 | ```
221 |
222 | ### Search Query Operators and Nested Queries
223 |
224 | You can use the `and`, `or`, and `not` operators to build compound and nested queries. The corresponding `and()`, `or()`, and `not()` methods expect a closure as their argument. You can chain all available methods as well nest more sub-queries inside of closures.
225 |
226 | ```php
227 | $query->or(function($builder) {
228 | $builder->phrase('ford')
229 | ->phrase('truck');
230 | });
231 | ```
232 |
233 | ## Queue
234 |
235 | The help reduce the number of bulk requests made to the CloudSearch endpoint (because they cost) a queue system is used. This can be set in Laravel [Task Scheduling](https://laravel.com/docs/5.4/scheduling). You can decide how often it is ran using the scheduled task frequency options. Please note this uses the DB to function.
236 |
237 | Example of the task added to `/app/Console/Kernel.php`:
238 |
239 | ```php
240 | /**
241 | * Define the application's command schedule.
242 | *
243 | * @param \Illuminate\Console\Scheduling\Schedule $schedule
244 | *
245 | * @return void
246 | */
247 | protected function schedule(Schedule $schedule)
248 | {
249 | $schedule->command('search:queue')->everyTenMinutes();
250 | }
251 | ```
252 |
253 | ## Multilingual
254 |
255 | > This feature is experimental
256 |
257 | Laravel CloudSearch can support multiple languages by appending the language code to the index type, so when the system performs a search it will only look for data that is on in the current system locale suffixed index type. For this to work the model needs to use the `LaravelCloudSearch\Eloquent\Localized` trait or something similar to it.
258 |
--------------------------------------------------------------------------------
/src/CloudSearcher.php:
--------------------------------------------------------------------------------
1 | config = $config;
50 | }
51 |
52 | /**
53 | * Queue the given model action.
54 | *
55 | * @param string $action
56 | * @param Model $model
57 | *
58 | * @return bool
59 | */
60 | public function queue($action, Model $model)
61 | {
62 | switch($action) {
63 | case 'update':
64 | return $this->searchQueue()->push('update', $model->getKey(), get_class($model));
65 | break;
66 | case 'delete':
67 | return $this->searchQueue()->push('delete', $this->getSearchDocumentId($model), get_class($model));
68 | break;
69 | }
70 |
71 | return false;
72 | }
73 |
74 | /**
75 | * Add/Update the given models in the index.
76 | *
77 | * @param Collection $models
78 | *
79 | * @return array
80 | */
81 | public function update(Collection $models)
82 | {
83 | $payload = new Collection();
84 |
85 | $models->each(function ($model) use ($payload) {
86 | if ($fields = $this->getSearchDocument($model)) {
87 | $payload->push([
88 | 'type' => 'add',
89 | 'id' => $this->getSearchDocumentId($model),
90 | 'fields' => array_map(function ($value) {
91 | return is_null($value) ? '' : $value;
92 | }, $fields),
93 | ]);
94 | }
95 | });
96 |
97 | return $this->domainClient()->uploadDocuments([
98 | 'documents' => json_encode($payload->all()),
99 | 'contentType' => 'application/json',
100 | ]);
101 | }
102 |
103 | /**
104 | * Remove from search index
105 | *
106 | * @param Collection $entries
107 | *
108 | * @return array
109 | */
110 | public function delete(Collection $entries)
111 | {
112 | $payload = new Collection();
113 |
114 | // Add to the payload
115 | $entries->each(function ($model) use ($payload) {
116 | $search_document_id = $model instanceof Model
117 | ? $this->getSearchDocumentId($model)
118 | : $model;
119 |
120 | $payload->push([
121 | 'type' => 'delete',
122 | 'id' => $search_document_id,
123 | ]);
124 | });
125 |
126 | return $this->domainClient()->uploadDocuments([
127 | 'documents' => json_encode($payload->all()),
128 | 'contentType' => 'application/json',
129 | ]);
130 | }
131 |
132 | /**
133 | * Quick and simple search used for autocompletion.
134 | *
135 | * @param string $term
136 | * @param int $perPage
137 | *
138 | * @return LengthAwarePaginator|array
139 | */
140 | public function searchAll($term, $perPage = 15)
141 | {
142 | return $this->newQuery()
143 | ->term($term)
144 | ->take($perPage)
145 | ->get();
146 | }
147 |
148 | /**
149 | * Get search results.
150 | *
151 | * @param StructuredQueryBuilder $builder
152 | *
153 | * @return Collection
154 | */
155 | public function get(StructuredQueryBuilder $builder)
156 | {
157 | return $this->hydrateResults(Arr::get($this->execute($builder), 'hits.hit', []));
158 | }
159 |
160 | /**
161 | * Paginate the given search results.
162 | *
163 | * @param StructuredQueryBuilder $builder
164 | * @param int $perPage
165 | *
166 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
167 | */
168 | public function paginate(StructuredQueryBuilder $builder, $perPage = 15)
169 | {
170 | // Get current page
171 | $page = LengthAwarePaginator::resolveCurrentPage();
172 |
173 | // Set pagination params
174 | $builder->size($perPage)
175 | ->start((($page * $perPage) - $perPage));
176 |
177 | // Make request
178 | return $this->paginateResults($this->execute($builder), $page, $perPage);
179 | }
180 |
181 | /**
182 | * Perform the given search.
183 | *
184 | * @param StructuredQueryBuilder $builder
185 | *
186 | * @return mixed
187 | */
188 | public function execute(StructuredQueryBuilder $builder)
189 | {
190 | try {
191 | return $this->domainClient()->search($builder->buildStructuredQuery());
192 | }
193 | catch (CloudSearchDomainException $e) {
194 | dd($e->getAwsErrorMessage() ?: $e->getMessage());
195 |
196 | return $e->getAwsErrorMessage() ?: $e->getMessage();
197 | }
198 | }
199 |
200 | /**
201 | * Create collection from results.
202 | *
203 | * @param array $items
204 | *
205 | * @return Collection
206 | */
207 | protected function hydrateResults(array $items)
208 | {
209 | $items = array_map(function ($item) {
210 | return $this->newFromHitBuilder($item);
211 | }, $items);
212 |
213 | return Collection::make($items);
214 | }
215 |
216 | /**
217 | * Paginate the given query into a simple paginator.
218 | *
219 | * @param Result $result
220 | * @param int $page
221 | * @param int $perPage
222 | * @param array $append
223 | *
224 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
225 | */
226 | protected function paginateResults(Result $result, $page, $perPage, array $append = [])
227 | {
228 | // Get total number of pages
229 | $total = Arr::get($result, 'hits.found', 0);
230 |
231 | // Create pagination instance
232 | $paginator = (new LengthAwarePaginator($this->hydrateResults(Arr::get($result, 'hits.hit', [])), $total, $perPage, $page, [
233 | 'path' => Paginator::resolveCurrentPath(),
234 | ]));
235 |
236 | return $paginator->appends($append);
237 | }
238 |
239 | /**
240 | * New from hit builder.
241 | *
242 | * @param array $hit
243 | *
244 | * @return Model
245 | */
246 | protected function newFromHitBuilder($hit = [])
247 | {
248 | // Reconstitute the attributes from the field values
249 | $attributes = array_map(function ($field) {
250 | return $field[0];
251 | }, Arr::get($hit, 'fields', []));
252 |
253 | // Get model name from source
254 | if (!($model = Arr::pull($attributes, 'searchable_type'))) return null;
255 |
256 | // Set type
257 | $attributes['result_type'] = $this->getClassBasename($model);
258 |
259 | // Create model instance from type
260 | return $this->newFromBuilderRecursive(new $model, $attributes);
261 | }
262 |
263 | /**
264 | * Create a new model instance that is existing recursive.
265 | *
266 | * @param Model $model
267 | * @param array $attributes
268 | * @param Relation $parentRelation
269 | *
270 | * @return Model
271 | */
272 | protected function newFromBuilderRecursive(Model $model, array $attributes = [], Relation $parentRelation = null)
273 | {
274 | // Create a new instance of the given model
275 | $instance = $model->newInstance([], $exists = true);
276 |
277 | // Set the array of model attributes
278 | $instance->setRawAttributes((array)$attributes, $sync = true);
279 |
280 | // Load relations recursive
281 | $this->loadRelationsAttributesRecursive($instance);
282 |
283 | // Load pivot
284 | $this->loadPivotAttribute($instance, $parentRelation);
285 |
286 | return $instance;
287 | }
288 |
289 | /**
290 | * Get the relations attributes from a model.
291 | *
292 | * @param Model $model
293 | */
294 | protected function loadRelationsAttributesRecursive(Model $model)
295 | {
296 | $attributes = $model->getAttributes();
297 |
298 | foreach ($attributes as $key => $value) {
299 | if (method_exists($model, $key)) {
300 | $reflection_method = new ReflectionMethod($model, $key);
301 |
302 | if ($reflection_method->class != 'Illuminate\Database\Eloquent\Model') {
303 | $relation = $model->$key();
304 |
305 | if ($relation instanceof Relation) {
306 | // Check if the relation field is single model or collections
307 | if (is_null($value) === true || !$this->isMultiLevelArray($value)) {
308 | $value = [$value];
309 | }
310 |
311 | $models = $this->hydrateRecursive($relation->getModel(), $value, $relation);
312 |
313 | // Unset attribute before match relation
314 | unset($model[$key]);
315 | $relation->match([$model], $models, $key);
316 | }
317 | }
318 | }
319 | }
320 | }
321 |
322 | /**
323 | * Get the pivot attribute from a model.
324 | *
325 | * @param Model $model
326 | * @param Relation $parentRelation
327 | */
328 | protected function loadPivotAttribute(Model $model, Relation $parentRelation = null)
329 | {
330 | foreach ($model->getAttributes() as $key => $value) {
331 | if ($key === 'pivot') {
332 | unset($model[$key]);
333 |
334 | $pivot = $parentRelation->newExistingPivot($value);
335 | $model->setRelation($key, $pivot);
336 | }
337 | }
338 | }
339 |
340 | /**
341 | * Check if an array is multi-level array like [[id], [id], [id]].
342 | *
343 | * For detect if a relation field is single model or collections.
344 | *
345 | * @param array $array
346 | *
347 | * @return boolean
348 | */
349 | protected function isMultiLevelArray(array $array)
350 | {
351 | foreach ($array as $key => $value) {
352 | if (!is_array($value)) {
353 | return false;
354 | }
355 | }
356 |
357 | return true;
358 | }
359 |
360 | /**
361 | * Create a collection of models from plain arrays recursive.
362 | *
363 | * @param Model $model
364 | * @param Relation $parentRelation
365 | * @param array $items
366 | *
367 | * @return Collection
368 | */
369 | protected function hydrateRecursive(Model $model, array $items, Relation $parentRelation = null)
370 | {
371 | $items = array_map(function ($item) use ($model, $parentRelation) {
372 | return $this->newFromBuilderRecursive($model, ($item ?: []), $parentRelation);
373 | }, $items);
374 |
375 | return $model->newCollection($items);
376 | }
377 |
378 | /**
379 | * Get index document data for Laravel CloudSearch.
380 | *
381 | * @param Model $model
382 | *
383 | * @return array|null
384 | */
385 | protected function getSearchDocument(Model $model)
386 | {
387 | if ($data = $model->getSearchDocument()) {
388 | $data['searchable_type'] = get_class($model);
389 | }
390 |
391 | return $data;
392 | }
393 |
394 | /**
395 | * Create a document ID for Laravel CloudSearch using the searchable ID and
396 | * the class name of the model.
397 | *
398 | * @param Model $model
399 | *
400 | * @return string
401 | */
402 | protected function getSearchDocumentId(Model $model)
403 | {
404 | return $this->getClassBasename($model) . '-' . $model->getSearchableId();
405 | }
406 |
407 | /**
408 | * Get configuration value.
409 | *
410 | * @param string $key
411 | * @param mixed $default
412 | *
413 | * @return mixed
414 | */
415 | public function config($key, $default = null)
416 | {
417 | return Arr::get($this->config, $key, $default);
418 | }
419 |
420 | /**
421 | * Return the CloudSearch domain client instance.
422 | *
423 | * @return CloudSearchDomainClient
424 | */
425 | public function domainClient()
426 | {
427 | if (is_null($this->domainClient)) {
428 | $this->domainClient = new CloudSearchDomainClient($this->config('config'));
429 | }
430 |
431 | return $this->domainClient;
432 | }
433 |
434 | /**
435 | * Return the CloudSearch client instance.
436 | *
437 | * @return CloudSearchClient
438 | */
439 | public function searchClient()
440 | {
441 | if (is_null($this->searchClient)) {
442 | $this->searchClient = new CloudSearchClient([
443 | 'region' => $this->config('config.region'),
444 | 'credentials' => $this->config('config.credentials'),
445 | 'version' => $this->config('config.version'),
446 | ]);
447 | }
448 |
449 | return $this->searchClient;
450 | }
451 |
452 | /**
453 | * Get the queue instance.
454 | *
455 | * @return Queue
456 | */
457 | public function searchQueue()
458 | {
459 | return app(Queue::class);
460 | }
461 |
462 | /**
463 | * Get the class "basename" of the given object / class.
464 | *
465 | * @param string|object $class
466 | *
467 | * @return string
468 | */
469 | protected function getClassBasename($class)
470 | {
471 | $class = is_object($class) ? get_class($class) : $class;
472 |
473 | return basename(str_replace('\\', '/', strtolower($class)));
474 | }
475 |
476 | /**
477 | * Create a new query builder instance.
478 | *
479 | * @return Builder
480 | */
481 | public function newQuery()
482 | {
483 | return new Builder($this);
484 | }
485 | }
486 |
--------------------------------------------------------------------------------
/src/Console/AbstractCommand.php:
--------------------------------------------------------------------------------
1 | cloudSearcher = $cloudSearcher;
39 | $this->batching_size = $cloudSearcher->config('batching_size', 100);
40 |
41 | $this->models = config('cloud-search.model_namespace', '\\App');
42 | }
43 |
44 | /**
45 | * Perform action model mapping.
46 | *
47 | * @param string $action
48 | */
49 | protected function processModels($action)
50 | {
51 | // Check for multilingual support
52 | $locales = $this->getLocales();
53 |
54 | // Process all provided models
55 | foreach ($this->getModelArgument() as $model) {
56 | if ($model = $this->validateModel("{$this->models}\\{$model}")) {
57 |
58 | // Get model instance
59 | $instance = new $model();
60 |
61 | // Perform action
62 | if (empty($locales) === false && method_exists($instance, 'getLocalizedSearchableId')) {
63 |
64 | // Process each locale using the by locale macro
65 | foreach ($locales as $locale) {
66 |
67 | $this->line("\nIndexing locale: {$locale}");
68 |
69 | $this->$action(
70 | $instance->byLocale($locale),
71 | $model
72 | );
73 | }
74 | }
75 | else {
76 | $this->$action($instance, $model);
77 | }
78 | }
79 | }
80 | }
81 |
82 | /**
83 | * Get action argument.
84 | *
85 | * @param array $validActions
86 | *
87 | * @return array
88 | */
89 | protected function getActionArgument($validActions = [])
90 | {
91 | $action = strtolower($this->argument('action'));
92 |
93 | if (in_array($action, $validActions) === false) {
94 | throw new \RuntimeException("The [{$action}] option does not exist.");
95 | }
96 |
97 | return $action;
98 | }
99 |
100 | /**
101 | * Get model argument.
102 | *
103 | * @return array
104 | */
105 | protected function getModelArgument()
106 | {
107 | $models = explode(',', preg_replace('/\s+/', '', $this->argument('model')));
108 |
109 | return array_map(function ($model) {
110 | $model = array_map(function ($m) {
111 | return Str::studly($m);
112 | }, explode('\\', $model));
113 |
114 | return implode('\\', $model);
115 | }, $models);
116 | }
117 |
118 | /**
119 | * Get an array of supported locales.
120 | *
121 | * @return array|null
122 | */
123 | protected function getLocales()
124 | {
125 | // Get user specified locales
126 | if ($locales = $this->option('locales')) {
127 | return array_filter(explode(',', preg_replace('/\s+/', '', $locales)));
128 | }
129 |
130 | // Check for package
131 | if (class_exists('\\Torann\\Localization\\LocaleManager')) {
132 | return app(LocaleManager::class)->getSupportedLanguagesKeys();
133 | }
134 |
135 | return config('cloud-search.support_locales');
136 | }
137 |
138 | /**
139 | * Validate model.
140 | *
141 | * @param string $model
142 | *
143 | * @return bool
144 | */
145 | protected function validateModel($model)
146 | {
147 | // Verify model existence
148 | if (class_exists($model) === false) {
149 | $this->error("Model [{$model}] not found");
150 |
151 | return false;
152 | }
153 |
154 | // Verify model is Elasticsearch ready
155 | if (method_exists($model, 'getSearchDocument') === false) {
156 | $this->error("Model [{$model}] does not support searching.");
157 |
158 | return false;
159 | }
160 |
161 | return $model;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/Console/FieldsCommand.php:
--------------------------------------------------------------------------------
1 | error("No fields defined in the config.");
45 | return null;
46 | }
47 |
48 | // Ensure the system specific value is set
49 | $fields['searchable_type'] = 'literal';
50 |
51 | // Set CloudSearch domain for later
52 | $this->domain = $this->cloudSearcher->config('domain_name');
53 |
54 | $this->getOutput()->write('Syncing fields');
55 |
56 | // Process everything
57 | $this->syncCurrentFields($fields);
58 | $this->syncNewFields($fields);
59 |
60 | $this->getOutput()->writeln('success');
61 | $this->line('');
62 |
63 | // Check for changes
64 | if ($this->changes > 0) {
65 | $this->comment("{$this->changes} field change(s) were made to the \"{$this->domain}\" domain, these changes will not be reflected in search results until the index is rebuilt.");
66 |
67 | if ($this->confirm("Would you like to rebuild the index?")) {
68 | $this->runIndexing();
69 | }
70 | }
71 | else {
72 | $this->comment('Fields are up to date.');
73 | }
74 | }
75 |
76 | /**
77 | * Update or remove any current fields.
78 | *
79 | * @param array $fields
80 | */
81 | protected function syncCurrentFields(&$fields)
82 | {
83 | foreach($this->getFields() as $name=>$type) {
84 |
85 | // Was the field removed
86 | if (($current_type = Arr::get($fields, $name)) === null) {
87 | $this->deleteField($name);
88 | }
89 |
90 | // Was the field changed
91 | else if ($current_type !== $type) {
92 | $this->defineField($name, $type);
93 | }
94 |
95 | unset($fields[$name]);
96 |
97 | $this->getOutput()->write('.');
98 | }
99 | }
100 |
101 | /**
102 | * Sync new fields.
103 | *
104 | * @param array $fields
105 | */
106 | protected function syncNewFields($fields)
107 | {
108 | foreach($fields as $name=>$type) {
109 | $this->defineField($name, $type);
110 | $this->getOutput()->write('.');
111 | }
112 | }
113 |
114 | /**
115 | * Get all fields for the domain.
116 | *
117 | * @param array $fields
118 | *
119 | * @return array
120 | */
121 | protected function getFields(array $fields = [])
122 | {
123 | $response = $this->cloudSearcher->searchClient()->describeIndexFields([
124 | 'DomainName' => $this->domain,
125 | ]);
126 |
127 | foreach(Arr::get($response, 'IndexFields', []) as $value) {
128 | $fields[Arr::get($value, 'Options.IndexFieldName')] = Arr::get($value, 'Options.IndexFieldType');
129 | }
130 |
131 | return $fields;
132 | }
133 |
134 | /**
135 | * Create or updates a field in the domain.
136 | *
137 | * @param string $field
138 | * @param string $type
139 | *
140 | * @return bool
141 | */
142 | protected function defineField($field, $type)
143 | {
144 | $response = $this->cloudSearcher->searchClient()->defineIndexField([
145 | 'DomainName' => $this->domain,
146 | 'IndexField' => [
147 | 'IndexFieldName' => $field,
148 | 'IndexFieldType' => $type,
149 | ],
150 | ]);
151 |
152 | // Check for success
153 | if ($result = Arr::get($response, '@metadata.statusCode') == 200) {
154 | $this->changes++;
155 | }
156 |
157 | return $result;
158 | }
159 |
160 | /**
161 | * Delete the given field from the domain.
162 | *
163 | * @param string $field
164 | *
165 | * @return bool
166 | */
167 | protected function deleteField($field)
168 | {
169 | $response = $this->cloudSearcher->searchClient()->deleteIndexField([
170 | 'DomainName' => $this->domain,
171 | 'IndexFieldName' => $field,
172 | ]);
173 |
174 | // Check for success
175 | if ($result = Arr::get($response, '@metadata.statusCode') == 200) {
176 | $this->changes++;
177 | }
178 |
179 | return $result;
180 | }
181 |
182 | /**
183 | * Tells the search domain to start indexing its documents using the latest indexing options.
184 | */
185 | protected function runIndexing()
186 | {
187 | $response = $this->cloudSearcher->searchClient()->indexDocuments([
188 | 'DomainName' => $this->domain,
189 | ]);
190 |
191 | if (Arr::get($response, '@metadata.statusCode') == 200) {
192 | $this->line('CloudSearch is currently rebuilding your index.');
193 | }
194 | else {
195 | $this->error('Something prevented the rebuild process. Log into your AWS console to find out more.');
196 | }
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/Console/FlushCommand.php:
--------------------------------------------------------------------------------
1 | processModels('flush');
33 | }
34 |
35 | /**
36 | * Index all model entries to ElasticSearch.
37 | *
38 | * @param Model $instance
39 | * @param string $name
40 | *
41 | * @return bool
42 | */
43 | protected function flush(Model $instance, $name)
44 | {
45 | $this->getOutput()->write("Flushing [{$name}]");
46 |
47 | $instance->chunk($this->batching_size, function ($models) {
48 | $this->cloudSearcher->delete($models);
49 | $this->getOutput()->write(str_repeat('.', $models->count()));
50 | });
51 |
52 | $this->getOutput()->writeln('done');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Console/IndexCommand.php:
--------------------------------------------------------------------------------
1 | processModels('index');
33 | }
34 |
35 | /**
36 | * Index all model entries to ElasticSearch.
37 | *
38 | * @param Model $instance
39 | * @param string $name
40 | *
41 | * @return bool
42 | */
43 | protected function index(Model $instance, $name)
44 | {
45 | $this->getOutput()->write("Indexing [{$name}]");
46 |
47 | $instance->chunk($this->batching_size, function ($models) use (&$total) {
48 | $this->cloudSearcher->update($models);
49 | $this->getOutput()->write(str_repeat('.', $models->count()));
50 | });
51 |
52 | $this->getOutput()->writeln('done');
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Console/QueueCommand.php:
--------------------------------------------------------------------------------
1 | cloudSearcher = $cloudSearcher;
51 | $this->batching_size = $cloudSearcher->config('batching_size', 100);
52 | $this->queue = $queue;
53 | }
54 |
55 | /**
56 | * Execute the console command.
57 | */
58 | public function handle()
59 | {
60 | $this->line('Processing search queue');
61 |
62 | $this->queue->getBatch()->each(function ($collection, $action) {
63 | $collection->groupBy('entry_type')->each(function ($items, $model) use ($action) {
64 | $this->{$action}($items, $model);
65 | });
66 | });
67 |
68 | $this->queue->flushBatch();
69 | }
70 |
71 | /**
72 | * Add or update given models in the search index.
73 | *
74 | * @param \Illuminate\Support\Collection $items
75 | * @param string $model
76 | */
77 | protected function update($items, $model)
78 | {
79 | // Get the model's primary key
80 | $instance = new $model;
81 |
82 | // Create a full column name
83 | $key = $instance->getTable() . '.' . $instance->getKeyName();
84 |
85 | // Process all models
86 | $model::whereIn($key, $items->pluck('entry_id'))->chunk($this->batching_size, function($models) {
87 | $this->cloudSearcher->update($models);
88 | });
89 | }
90 |
91 | /**
92 | * Delete given models from search index.
93 | *
94 | * @param \Illuminate\Support\Collection $items
95 | * @param string $model
96 | */
97 | protected function delete($items, $model)
98 | {
99 | foreach($items->chunk($this->batching_size) as $models) {
100 | $this->cloudSearcher->delete($models->pluck('entry_id'));
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/Eloquent/Localized.php:
--------------------------------------------------------------------------------
1 | locale . '-' . $this->getKey();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Eloquent/LocalizedScope.php:
--------------------------------------------------------------------------------
1 | where($model->getTable() . '.locale', app()->getLocale());
20 | }
21 |
22 | /**
23 | * Extend the query builder with the needed functions.
24 | *
25 | * @param \Illuminate\Database\Eloquent\Builder $builder
26 | */
27 | public function extend(Builder $builder)
28 | {
29 | $this->addByLocale($builder);
30 | $this->addWithoutLocalization($builder);
31 | }
32 |
33 | /**
34 | * Add the by locale extension to the builder.
35 | *
36 | * @param \Illuminate\Database\Eloquent\Builder $builder
37 | * @return void
38 | */
39 | public function addByLocale(Builder $builder)
40 | {
41 | $builder->macro('byLocale', function (Builder $builder, $locale) {
42 |
43 | $builder->withoutGlobalScope($this);
44 |
45 | return $builder->where($builder->getModel()->getTable() . '.locale', $locale);
46 | });
47 | }
48 |
49 | /**
50 | * Add the without-moderated extension to the builder.
51 | *
52 | * @param \Illuminate\Database\Eloquent\Builder $builder
53 | */
54 | protected function addWithoutLocalization(Builder $builder)
55 | {
56 | $builder->macro('withoutLocalization', function (Builder $builder) {
57 | return $builder->withoutGlobalScope($this);
58 | });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Eloquent/Observer.php:
--------------------------------------------------------------------------------
1 | addToCloudSearch();
15 | }
16 |
17 | /**
18 | * Handle the deleted event for the model.
19 | *
20 | * @param \Illuminate\Database\Eloquent\Model $model
21 | */
22 | public function deleted($model)
23 | {
24 | $model->deleteFromCloudSearch();
25 | }
26 |
27 | /**
28 | * Handle the restored event for the model.
29 | *
30 | * @param \Illuminate\Database\Eloquent\Model $model
31 | */
32 | public function restored($model)
33 | {
34 | $this->saved($model);
35 | }
36 | }
--------------------------------------------------------------------------------
/src/Eloquent/Searchable.php:
--------------------------------------------------------------------------------
1 | getCloudSearch()->queue('update', $this);
37 |
38 | }
39 |
40 | /**
41 | * Dispatch the job to make the model unsearchable.
42 | *
43 | * @return bool
44 | */
45 | public function deleteFromCloudSearch()
46 | {
47 | return $this->getCloudSearch()->queue('delete', $this);
48 | }
49 |
50 | /**
51 | * Perform a search against the model's indexed data.
52 | *
53 | * @param string $query
54 | *
55 | * @return \LaravelCloudSearch\Query\Builder
56 | */
57 | public static function search($query)
58 | {
59 | return self::searchBuilder()->term($query);
60 | }
61 |
62 | /**
63 | * Get the search builder instance.
64 | *
65 | * @return \LaravelCloudSearch\Query\Builder
66 | */
67 | public static function searchBuilder()
68 | {
69 | $instance = new static();
70 |
71 | $builder = new Builder($instance->getCloudSearch());
72 |
73 | return $builder->searchableType($instance);
74 | }
75 |
76 | /**
77 | * Get search document ID for the model.
78 | *
79 | * @return string|int
80 | */
81 | public function getSearchableId()
82 | {
83 | if (method_exists($this, 'getLocalizedSearchableId')) {
84 | return $this->getLocalizedSearchableId();
85 | }
86 |
87 | return $this->getKey();
88 | }
89 |
90 | /**
91 | * Get search document data for the model.
92 | *
93 | * @return array
94 | */
95 | abstract public function getSearchDocument();
96 |
97 | /**
98 | * Get a CloudSearch for the model.
99 | *
100 | * @return CloudSearcher
101 | */
102 | public function getCloudSearch()
103 | {
104 | return app(CloudSearcher::class);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/LaravelCloudSearchServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->singleton(CloudSearcher::class, function ($app) {
17 | return new CloudSearcher($app->config->get('cloud-search'));
18 | });
19 |
20 | $this->app->singleton(Queue::class, function ($app) {
21 | return new Queue($app['db']);
22 | });
23 |
24 | if ($this->app->runningInConsole()) {
25 | $this->registerResources();
26 | $this->registerCommands();
27 | }
28 | }
29 |
30 | /**
31 | * Register the resources.
32 | *
33 | * @return bool
34 | */
35 | protected function registerResources()
36 | {
37 | if ($this->isLumen() === false) {
38 | $this->publishes([
39 | __DIR__.'/../config/cloud-search.php' => config_path('cloud-search.php'),
40 | ], 'config');
41 |
42 | $this->mergeConfigFrom(
43 | __DIR__.'/../config/cloud-search.php', 'cloud-search'
44 | );
45 | }
46 |
47 | $this->publishes([
48 | __DIR__ . '/../database/migrations' => base_path('/database/migrations'),
49 | ], 'migrations');
50 | }
51 |
52 | /**
53 | * Register all commands.
54 | *
55 | * @return void
56 | */
57 | public function registerCommands()
58 | {
59 | $this->commands([
60 | Console\FieldsCommand::class,
61 | Console\FlushCommand::class,
62 | Console\IndexCommand::class,
63 | Console\QueueCommand::class,
64 | ]);
65 | }
66 |
67 | /**
68 | * Check if package is running under a Lumen app.
69 | *
70 | * @return bool
71 | */
72 | protected function isLumen()
73 | {
74 | return str_contains($this->app->version(), 'Lumen') === true;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Query/Builder.php:
--------------------------------------------------------------------------------
1 | cloudSearcher = $cloudSearcher;
25 | $this->builder = new StructuredQueryBuilder();
26 | }
27 |
28 | /**
29 | * Set the searchable type.
30 | *
31 | * @param mixed $type
32 | *
33 | * @return self
34 | */
35 | public function searchableType($type)
36 | {
37 | if (is_object($type)) {
38 | $type = get_class($type);
39 | }
40 |
41 | // Set the search type
42 | $this->phrase($type, 'searchable_type');
43 |
44 | // Set locale
45 | if (method_exists($type, 'getLocalizedSearchableId')) {
46 | $this->byLocale();
47 | }
48 |
49 | return $this;
50 | }
51 |
52 | /**
53 | * Set the locale to use for searching.
54 | *
55 | * @param string $locale
56 | *
57 | * @return self
58 | */
59 | public function byLocale($locale = null)
60 | {
61 | // Use the current system locale if one is not set
62 | $this->phrase($locale ?: app()->getLocale(), 'locale');
63 |
64 | return $this;
65 | }
66 |
67 | /**
68 | * Cursor method
69 | *
70 | * @param string $cursor
71 | *
72 | * @return self
73 | */
74 | public function cursor($cursor = 'initial')
75 | {
76 | $this->builder->cursor($cursor);
77 |
78 | return $this;
79 | }
80 |
81 | /**
82 | * Set builder expression
83 | *
84 | * @param array $filters
85 | *
86 | * @return self
87 | */
88 | public function filter(array $filters)
89 | {
90 | foreach($filters as $field=>$value) {
91 | if (is_array($value)) {
92 | foreach(array_flatten(array_filter($value)) as $v) {
93 | $this->phrase($v, $field);
94 | }
95 | }
96 | else {
97 | $this->phrase($value, $field);
98 | }
99 | }
100 |
101 | return $this;
102 | }
103 |
104 | /**
105 | * Set builder expression
106 | *
107 | * @param string $accessor
108 | * @param string $expression
109 | *
110 | * @return self
111 | */
112 | public function expr($accessor, $expression)
113 | {
114 | $this->builder->expr($accessor, $expression);
115 |
116 | return $this;
117 | }
118 |
119 | /**
120 | * Build return facets array
121 | *
122 | * @param string $field
123 | * @param string $sort
124 | * @param integer $size
125 | *
126 | * @return self
127 | */
128 | public function facet($field, $sort = "bucket", $size = 10)
129 | {
130 | $this->builder->facet($field, $sort, $size);
131 |
132 | return $this;
133 | }
134 |
135 | /**
136 | * Build return facets with explicit buckets
137 | *
138 | * @param string $field
139 | * @param array $buckets
140 | * @param string $method
141 | *
142 | * @return self
143 | */
144 | public function facetBuckets($field, $buckets, $method = "filter")
145 | {
146 | $this->builder->facetBuckets($field, $buckets, $method);
147 |
148 | return $this;
149 | }
150 |
151 | /**
152 | * Create an 'and' wrapped query block
153 | *
154 | * @param Closure|string $block
155 | *
156 | * @return self
157 | */
158 | public function qAnd($block)
159 | {
160 | $this->builder->q->qAnd($block);
161 |
162 | return $this;
163 | }
164 |
165 | /**
166 | * Create an 'and' wrapped filter query block
167 | *
168 | * @param Closure|string $block
169 | *
170 | * @return self
171 | */
172 | public function filterAnd($block)
173 | {
174 | $this->builder->fq->qAnd($block);
175 |
176 | return $this;
177 | }
178 |
179 | /**
180 | * Build match all query
181 | *
182 | * @return self
183 | */
184 | public function matchall()
185 | {
186 | $this->builder->q->matchall();
187 |
188 | return $this;
189 | }
190 |
191 | /**
192 | * Create a near (sloppy) query
193 | *
194 | * @param string $value
195 | * @param string $field
196 | * @param int $distance
197 | * @param int $boost
198 | *
199 | * @return self
200 | */
201 | public function near($value, $field = null, $distance = 3, $boost = null)
202 | {
203 | $this->builder->q->near($value, $field, $distance, $boost);
204 |
205 | return $this;
206 | }
207 |
208 | /**
209 | * Create a near (sloppy) query
210 | *
211 | * @param string $value
212 | * @param string $field
213 | * @param int $distance
214 | * @param int $boost
215 | *
216 | * @return self
217 | */
218 | public function filterNear($value, $field, $distance = 3, $boost = null)
219 | {
220 | $this->builder->fq->near($value, $field, $distance, $boost);
221 |
222 | return $this;
223 | }
224 |
225 | /**
226 | * Create a 'not' wrapped query block
227 | *
228 | * @param Closure|string $block
229 | *
230 | * @return self
231 | */
232 | public function qNot($block)
233 | {
234 | $this->builder->q->qNot($block);
235 |
236 | return $this;
237 | }
238 |
239 | /**
240 | * Create a 'not' wrapped query block
241 | *
242 | * @param Closure|string $block
243 | *
244 | * @return self
245 | */
246 | public function filterNot($block)
247 | {
248 | $this->builder->fq->qNot($block);
249 |
250 | return $this;
251 | }
252 |
253 | /**
254 | * Create an 'or' wrapped query block
255 | *
256 | * @param Closure|string $block
257 | *
258 | * @return self
259 | */
260 | public function qOr($block)
261 | {
262 | $this->builder->q->qOr($block);
263 |
264 | return $this;
265 | }
266 |
267 | /**
268 | * Create an 'or' wrapped query block
269 | *
270 | * @param Closure|string $block
271 | *
272 | * @return self
273 | */
274 | public function filterOr($block)
275 | {
276 | $this->builder->fq->qOr($block);
277 |
278 | return $this;
279 | }
280 |
281 | /**
282 | * Create a phrase query
283 | *
284 | * @param string $value
285 | * @param string $field
286 | * @param int $boost
287 | *
288 | * @return self
289 | */
290 | public function phrase($value, $field = null, $boost = null)
291 | {
292 | $this->builder->q->phrase($value, $field, $boost);
293 |
294 | return $this;
295 | }
296 |
297 | /**
298 | * Create a phrase query
299 | *
300 | * @param string $value
301 | * @param string $field
302 | * @param int $boost
303 | *
304 | * @return self
305 | */
306 | public function filterPhrase($value, $field, $boost = null)
307 | {
308 | $this->builder->fq->phrase($value, $field, $boost);
309 |
310 | return $this;
311 | }
312 |
313 | /**
314 | * Create a prefix query
315 | *
316 | * @param string $value
317 | * @param string $field
318 | * @param int $boost
319 | *
320 | * @return self
321 | */
322 | public function prefix($value, $field = null, $boost = null)
323 | {
324 | $this->builder->q->prefix($value, $field, $boost);
325 |
326 | return $this;
327 | }
328 |
329 | /**
330 | * Create a prefix query
331 | *
332 | * @param string $value
333 | * @param string $field
334 | * @param int $boost
335 | *
336 | * @return self
337 | */
338 | public function filterPrefix($value, $field, $boost = null)
339 | {
340 | $this->builder->fq->prefix($value, $field, $boost);
341 |
342 | return $this;
343 | }
344 |
345 | /**
346 | * Create a range query
347 | *
348 | * @param string $field
349 | * @param string|int $min
350 | * @param string|int $max
351 | *
352 | * @return self
353 | */
354 | public function range($field, $min, $max = null)
355 | {
356 | $this->builder->q->range($field, $min, $max);
357 |
358 | return $this;
359 | }
360 |
361 | /**
362 | * Create a range query
363 | *
364 | * @param string $field
365 | * @param string|int $min
366 | * @param string|int $max
367 | *
368 | * @return self
369 | */
370 | public function filterRange($field, $min, $max = null)
371 | {
372 | $this->builder->fq->range($field, $min, $max);
373 |
374 | return $this;
375 | }
376 |
377 | /**
378 | * Create a term query
379 | *
380 | * @param string $value
381 | * @param string $field
382 | * @param int $boost
383 | *
384 | * @return self
385 | */
386 | public function term($value, $field = null, $boost = null)
387 | {
388 | $this->builder->q->term($value, $field, $boost);
389 |
390 | return $this;
391 | }
392 |
393 | /**
394 | * Create a term query
395 | *
396 | * @param string $value
397 | * @param string $field
398 | * @param int $boost
399 | *
400 | * @return self
401 | */
402 | public function filterTerm($value, $field, $boost = null)
403 | {
404 | $this->builder->fq->term($value, $field, $boost);
405 |
406 | return $this;
407 | }
408 |
409 | /**
410 | * Set return fields property of query
411 | *
412 | * @param string $value
413 | *
414 | * @return self
415 | */
416 | public function returnFields($value)
417 | {
418 | $this->builder->returnFields($value);
419 |
420 | return $this;
421 | }
422 |
423 | /**
424 | * Set options property of query
425 | *
426 | * @param string $key
427 | * @param string $value
428 | *
429 | * @return self
430 | */
431 | public function options($key, $value)
432 | {
433 | $this->builder->options($key, $value);
434 |
435 | return $this;
436 | }
437 |
438 | /**
439 | * Set the "limit" for the query.
440 | *
441 | * @param int $value
442 | *
443 | * @return self
444 | */
445 | public function take($value)
446 | {
447 | $this->builder->size($value);
448 |
449 | return $this;
450 | }
451 |
452 | /**
453 | * Sort query
454 | *
455 | * @param string $field
456 | * @param string $direction
457 | *
458 | * @return self
459 | */
460 | public function sort($field, $direction = 'asc')
461 | {
462 | $this->builder->sort($field, $direction);
463 |
464 | return $this;
465 | }
466 |
467 | /**
468 | * Set start property of query
469 | *
470 | * @param int $value
471 | *
472 | * @return self
473 | */
474 | public function start($value)
475 | {
476 | $this->builder->start($value);
477 |
478 | return $this;
479 | }
480 |
481 | /**
482 | * Build field statistics
483 | *
484 | * @param string $field
485 | *
486 | * @return self
487 | */
488 | public function stats($field)
489 | {
490 | $this->builder->stats($field);
491 |
492 | return $this;
493 | }
494 |
495 | /**
496 | * Build a location range filter
497 | *
498 | * @param string $field
499 | * @param string $lat
500 | * @param string $lon
501 | * @param integer $radius
502 | * @param bool $addExpr
503 | *
504 | * @return self
505 | */
506 | public function latlon($field, $lat, $lon, $radius = 50, $addExpr = false)
507 | {
508 | $this->builder->latlon($field, $lat, $lon, $radius, $addExpr);
509 |
510 | return $this;
511 | }
512 |
513 | /**
514 | * Build distance expression
515 | *
516 | * @param string $field
517 | * @param string $lat
518 | * @param string $lon
519 | *
520 | * @return self
521 | */
522 | public function addDistanceExpr($field, $lat, $lon)
523 | {
524 | $this->builder->addDistanceExpr($field, $lat, $lon);
525 |
526 | return $this;
527 | }
528 |
529 | /**
530 | * Get the first result from the search.
531 | *
532 | * @return \Illuminate\Database\Eloquent\Model
533 | */
534 | public function first()
535 | {
536 | $this->take(1);
537 |
538 | return $this->get()->first();
539 | }
540 |
541 | /**
542 | * Method to trigger request-response
543 | *
544 | * @return \Illuminate\Support\Collection
545 | */
546 | public function get()
547 | {
548 | return $this->cloudSearch()->get($this->builder);
549 | }
550 |
551 | /**
552 | * Paginate the given query into a simple paginator.
553 | *
554 | * @param int $perPage
555 | *
556 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
557 | */
558 | public function paginate($perPage = 15)
559 | {
560 | return $this->cloudSearch()->paginate($this->builder, $perPage);
561 | }
562 |
563 | /**
564 | * Get the CloudSearch to handle the query.
565 | *
566 | * @return \LaravelCloudSearch\CloudSearcher
567 | */
568 | protected function cloudSearch()
569 | {
570 | return $this->cloudSearcher;
571 | }
572 | }
--------------------------------------------------------------------------------
/src/Query/StructuredQueryBuilder.php:
--------------------------------------------------------------------------------
1 | q = new StructuredSearch;
102 | $this->fq = new StructuredSearch;
103 | }
104 |
105 | /**
106 | * Alias to get structured search query
107 | *
108 | * @return array
109 | */
110 | public function getQuery()
111 | {
112 | return $this->q->getQuery();
113 | }
114 |
115 | /**
116 | * Alias to get structured filter query
117 | *
118 | * @return array
119 | */
120 | public function getFilterQuery()
121 | {
122 | return $this->fq->getQuery();
123 | }
124 |
125 | /**
126 | * CURSOR
127 | * Retrieves a cursor value you can use to page through large result sets.
128 | * Use the size parameter to control the number of hits you want to include
129 | * in each response. You can specify either the cursor or start parameter in
130 | * a request, they are mutually exclusive.
131 | *
132 | * To get the first cursor, specify cursor=initial in your initial request.
133 | * In subsequent requests, specify the cursor value returned in the hits
134 | * section of the response.
135 | *
136 | * @param string $cursor
137 | *
138 | * @return self
139 | */
140 | public function cursor($cursor = 'initial')
141 | {
142 | $this->cursor = $cursor == 0 ? 'initial' : $cursor;
143 |
144 | return $this;
145 | }
146 |
147 | /**
148 | * EXPRESSION
149 | * Defines an expression that can be used to sort results. You can also
150 | * specify an expression as a return field.
151 | *
152 | * @param string $accessor
153 | * @param string $expression
154 | *
155 | * @return self
156 | */
157 | public function expr($accessor, $expression)
158 | {
159 | $this->expressions[$accessor] = $expression;
160 | }
161 |
162 | /**
163 | * FACET (sorted)
164 | * Specifies a field that you want to get facet information for—FIELD is the
165 | * name of the field. The specified field must be facet enabled in the
166 | * domain configuration. Facet options are specified as a JSON object. If
167 | * the JSON object is empty, facet.FIELD={}, facet counts are computed for
168 | * all field values, the facets are sorted by facet count, and the top 10
169 | * facets are returned in the results.
170 | *
171 | * sort specifies how you want to sort the facets in the results: bucket or
172 | * count. Specify bucket to sort alphabetically or numerically by facet
173 | * value (in ascending order). Specify count to sort by the facet counts
174 | * computed for each facet value (in descending order).
175 | *
176 | * size specifies the maximum number of facets to include in the results.
177 | * By default, Amazon CloudSearch returns counts for the top 10.
178 | *
179 | * @param string $field
180 | * @param string $sort
181 | * @param integer $size
182 | *
183 | * @return self
184 | */
185 | public function facet($field, $sort = "bucket", $size = 10)
186 | {
187 | $this->facets[$field] = [
188 | 'sort' => $sort,
189 | 'size' => $size,
190 | ];
191 | }
192 |
193 | /**
194 | * FACET (BUCKETS)
195 | * specifies an array of the facet values or ranges you want to count.
196 | * Buckets are returned in the order they are specified in the request. To
197 | * specify a range of values, use a comma (,) to separate the upper and
198 | * lower bounds and enclose the range using brackets or braces. A square
199 | * bracket, [ or ], indicates that the bound is included in the range, a
200 | * curly brace, { or }, excludes the bound. You can omit the upper or lower
201 | * bound to specify an open-ended range. When omitting a bound, you must
202 | * use a curly brace.
203 | *
204 | * @param string $field
205 | * @@param array $buckets
206 | * @param string $method
207 | *
208 | * @return self
209 | */
210 | public function facetBuckets($field, $buckets, $method = "filter")
211 | {
212 | $this->facets[$field] = [
213 | 'buckets' => $buckets,
214 | 'method' => $method,
215 | ];
216 | }
217 |
218 | /**
219 | * QUERY PARSER OPTIONS
220 | * Configure options for the query parser specified in the q.parser
221 | * parameter. The options are specified as a JSON object, for example:
222 | * q.options={defaultOperator: 'or', fields: ['title^5','description']}
223 | *
224 | * defaultOperator-The default operator used to combine individual terms
225 | * in the search string. (and|or)
226 | * defaultOperator: 'or'
227 | *
228 | * fields—An array of the fields to search when no fields are specified in
229 | * a search. You can specify a weight for each field to control the relative
230 | * importance of each field when Amazon CloudSearch calculates relevance scores.
231 | * fields: ['title^5','description']
232 | *
233 | * @param string $key
234 | * @param mixed $value
235 | *
236 | * @return self
237 | */
238 | public function options($key, $value)
239 | {
240 | $this->options[$key] = $value;
241 |
242 | return $this;
243 | }
244 |
245 | /**
246 | * The field and expression values to include in the response, specified as
247 | * a comma-separated list. By default, a search response includes all return
248 | * enabled fields (return=_all_fields). To return only the document IDs for
249 | * the matching documents, specify return=_no_fields. To retrieve the
250 | * relevance score calculated for each document, specify return=_score. You
251 | * specify multiple return fields as a comma separated list. For example,
252 | * return=title,_score returns just the title and relevance score of each
253 | * matching document.
254 | *
255 | * @param string $returnFields
256 | *
257 | * @return self
258 | */
259 | public function returnFields($returnFields)
260 | {
261 | $this->returnFields = $returnFields;
262 |
263 | return $this;
264 | }
265 |
266 | /**
267 | * The maximum number of search hits to return.
268 | *
269 | * @param integer $size
270 | *
271 | * @return self
272 | */
273 | public function size($size)
274 | {
275 | $this->size = $size;
276 |
277 | return $this;
278 | }
279 |
280 | /**
281 | * A comma-separated list of fields or custom expressions to use to sort the
282 | * search results. You must specify the sort direction (asc or desc) for
283 | * each field. For example, sort=year desc,title asc. You can specify a
284 | * maximum of 10 fields and expressions. To use a field to sort results, it
285 | * must be sort enabled in the domain configuration. Array type fields
286 | * cannot be used for sorting. If no sort parameter is specified, results
287 | * are sorted by their default relevance scores in descending order:
288 | * sort=_score desc. You can also sort by document ID (sort=_id) and
289 | * version (sort=_version).
290 | *
291 | * @param string $field
292 | * @param string $direction
293 | *
294 | * @return self
295 | */
296 | public function sort($field, $direction = 'asc')
297 | {
298 | $this->sort = "{$field} {$direction}";
299 |
300 | return $this;
301 | }
302 |
303 | /**
304 | * The offset of the first search hit you want to return. You can specify
305 | * either the start or cursor parameter in a request, they are mutually
306 | * exclusive.
307 | *
308 | * @param integer $start
309 | *
310 | * @return self
311 | */
312 | public function start($start)
313 | {
314 | $this->start = $start;
315 |
316 | return $this;
317 | }
318 |
319 | /**
320 | * To get statistics for a field you use the stats.FIELD parameter. FIELD
321 | * is the name of a facet-enabled numeric field. You specify an empty JSON
322 | * object, stats.FIELD={}, to get all of the available statistics for the
323 | * specified field. (The stats.FIELD parameter does not support any options;
324 | * you must pass an empty JSON object.) You can request statistics for
325 | * multiple fields in the same request.
326 | *
327 | * You can get statistics only for facet-enabled numeric fields: date,
328 | * date-array, double, double-array, int, or int-array. Note that only the
329 | * count, max, min, and missing statistics are returned for date and
330 | * date-array fields.
331 | *
332 | * @param string $field
333 | *
334 | * @return self
335 | */
336 | public function stats($field)
337 | {
338 | $this->stats[] = $field;
339 | }
340 |
341 |
342 | /**
343 | * Special function to filter by distance (lat/lon)
344 | *
345 | * @param string $field
346 | * @param float $lat
347 | * @param float $lon
348 | * @param integer $radius
349 | * @param boolean $addExpr
350 | *
351 | * @return self
352 | */
353 | public function latlon($field, $lat, $lon, $radius = 50, $addExpr = false)
354 | {
355 | // upper left bound
356 | $lat1 = $lat + ($radius / 69);
357 | $lon1 = $lon - $radius / abs(cos(deg2rad($lat)) * 69);
358 |
359 | // lower right bound
360 | $lat2 = $lat - ($radius / 69);
361 | $lon2 = $lon + $radius / abs(cos(deg2rad($lat)) * 69);
362 |
363 | $min = "'{$lat1},{$lon1}'";
364 | $max = "'{$lat2},{$lon2}'";
365 | $this->fq->range($field, $min, $max);
366 |
367 | if ($addExpr) {
368 | $this->addDistanceExpr($field, $lat, $lon);
369 | }
370 |
371 | return $this;
372 | }
373 |
374 | /**
375 | * Special function to add 'distance' expression
376 | *
377 | * @param string $field
378 | * @param string $lat
379 | * @param string $lon
380 | *
381 | * @return self
382 | */
383 | public function addDistanceExpr($field, $lat, $lon)
384 | {
385 | $expression = "haversin(" .
386 | "{$lat}," .
387 | "{$lon}," .
388 | "{$field}.latitude," .
389 | "{$field}.longitude)";
390 | $this->expr("distance", $expression);
391 |
392 | return $this;
393 | }
394 |
395 | /**
396 | * Build the structured query array to send to AWS search
397 | *
398 | * @return array
399 | */
400 | public function buildStructuredQuery()
401 | {
402 | $structuredQuery = [];
403 |
404 | // cursor
405 | if ($this->cursor) {
406 | $structuredQuery['cursor'] = $this->cursor;
407 | }
408 |
409 | // expressions
410 | if ($this->expressions) {
411 | $structuredQuery['expr'] = json_encode($this->expressions);
412 | }
413 |
414 | // facets
415 | if ($this->facets) {
416 | $structuredQuery['facet'] = json_encode($this->facets);
417 | }
418 |
419 | // filter query
420 | if ($this->fq->query) {
421 | $structuredQuery['filterQuery'] = (string)$this->fq;
422 | }
423 |
424 | // query
425 | if ($this->q->query) {
426 | $structuredQuery['query'] = (string)$this->q;
427 | }
428 |
429 | // options
430 | if ($this->options) {
431 | $structuredQuery['queryOptions'] = json_encode($this->options);
432 | }
433 |
434 | // highlights
435 | // partial
436 | // parser
437 | $structuredQuery['queryParser'] = 'structured';
438 |
439 | // return
440 | if ($this->returnFields) {
441 | $structuredQuery['return'] = $this->returnFields;
442 | }
443 |
444 | // size
445 | $structuredQuery['size'] = $this->size;
446 |
447 | // sort
448 | if ($this->sort) {
449 | $structuredQuery['sort'] = $this->sort;
450 | }
451 |
452 | if (!$this->cursor) {
453 | $structuredQuery['start'] = $this->start;
454 | }
455 |
456 | // stats
457 | if ($this->stats) {
458 |
459 | // Parse fields
460 | $stats = array_map(function($field) {
461 | return "\"{$field}\":{}";
462 | }, $this->stats);
463 |
464 | $structuredQuery['stats'] = "{" . implode(',', $stats) . "}";
465 | }
466 |
467 | return $structuredQuery;
468 | }
469 | }
--------------------------------------------------------------------------------
/src/Query/StructuredSearch.php:
--------------------------------------------------------------------------------
1 | query;
25 | }
26 |
27 | /**
28 | * Includes a document only if it matches all of the specified expressions.
29 | * (Boolean AND operator.) The expressions can contain any of the structured
30 | * query operators, or a simple search string.
31 | *
32 | * @param Closure|string $block
33 | *
34 | * @return StructuredSearch
35 | */
36 | public function qAnd($block)
37 | {
38 | if ($block instanceof Closure) {
39 |
40 | $block($builder = new self);
41 |
42 | $this->query[] = "(and " . implode('', $builder->getQuery()) . ")";
43 | }
44 | else if (gettype($block) == "string") {
45 | $this->query[] = "(and '{$block}')";
46 | }
47 |
48 | return $this;
49 | }
50 |
51 | /**
52 | * Matches every document in the domain.
53 | *
54 | * @return StructuredSearch
55 | */
56 | public function matchall()
57 | {
58 | $this->query[] = "(matchall)";
59 | }
60 |
61 | /**
62 | * Searches a text or text-array field for the specified multi-term string and matches documents that contain the
63 | * terms within the specified distance of one another. (This is sometimes called a sloppy phrase search.) If you
64 | * omit the field option, Amazon CloudSearch searches all statically configured text and text-array fields by
65 | * default. Dynamic fields and literal fields are not searched by default. You can specify which fields you want to
66 | * search by default by specifying the q.options fields option.
67 | *
68 | * @param string $value
69 | * @param string $field
70 | * @param integer $distance
71 | * @param integer $boost
72 | *
73 | * @return StructuredSearch
74 | */
75 | private function near($value, $field = null, $distance = 3, $boost = null)
76 | {
77 | $near = "(near ";
78 |
79 | if ($field) {
80 | $near .= "field='{$field}' ";
81 | }
82 |
83 | if ($distance) {
84 | $near .= "distance='{$distance}' ";
85 | }
86 |
87 | if ($boost) {
88 | $near .= "boost='{$boost}' ";
89 | }
90 |
91 | $near .= "'{$value}')";
92 |
93 | $this->query[] = $near;
94 |
95 | return $this;
96 | }
97 |
98 | /**
99 | * Excludes a document if it matches the specified expression. (Boolean NOT
100 | * operator.) The expression can contain any of the structured query
101 | * operators, or a simple search string.
102 | *
103 | * @param Closure|string $block
104 | *
105 | * @return StructuredSearch
106 | */
107 | public function qNot($block)
108 | {
109 | if (gettype($block) == "object") {
110 |
111 | $block($builder = new self);
112 |
113 | $this->query[] = "(not " . implode('', $builder->getQuery()) . ")";
114 | }
115 | else if (gettype($block) == "string") {
116 | $this->query[] = "(not '{$block}')";
117 | }
118 |
119 | return $this;
120 | }
121 |
122 | /**
123 | * Includes a document if it matches any of the specified expressions.
124 | * (Boolean OR operator.) The expressions can contain any of the structured
125 | * query operators, or a simple search string.
126 | *
127 | * @param Closure|string $block
128 | *
129 | * @return StructuredSearch
130 | */
131 | public function qOr($block)
132 | {
133 | if ($block instanceof Closure) {
134 |
135 | $block($builder = new self);
136 |
137 | $this->query[] = "(or " . implode('', $builder->getQuery()) . ")";
138 | }
139 | else if (gettype($block) == "string") {
140 | $this->query[] = "(or '{$block}')";
141 | }
142 |
143 | return $this;
144 | }
145 |
146 | /**
147 | * Searches a text or text-array field for the specified phrase. If you
148 | * omit the field option, Amazon CloudSearch searches all statically
149 | * configured text and text-array fields by default. Dynamic fields and
150 | * literal fields are not searched by default. You can specify which fields
151 | * you want to search by default by specifying the q.options fields option.
152 | *
153 | * @param string $value
154 | * @param string $field
155 | * @param integer $boost
156 | *
157 | * @return StructuredSearch
158 | */
159 | private function phrase($value, $field = null, $boost = null)
160 | {
161 | $phrase = "(phrase ";
162 |
163 | if ($field) {
164 | $phrase .= "field='{$field}' ";
165 | }
166 |
167 | if ($boost) {
168 | $phrase .= "boost='{$boost}' ";
169 | }
170 |
171 | $phrase .= "'{$value}')";
172 |
173 | $this->query[] = $phrase;
174 |
175 | return $this;
176 | }
177 |
178 | /**
179 | * Searches a text, text-array, literal, or literal-array field for the
180 | * specified prefix followed by zero or more characters. If you omit the
181 | * field option, Amazon CloudSearch searches all statically configured text
182 | * and text-array fields by default. Dynamic fields and literal fields are
183 | * not searched by default. You can specify which fields you want to search
184 | * by default by specifying the q.options fields option.
185 | *
186 | * @param string $value
187 | * @param string $field
188 | * @param integer $boost
189 | *
190 | * @return StructuredSearch
191 | */
192 | private function prefix($value, $field = null, $boost = null)
193 | {
194 | $prefix = "(prefix ";
195 |
196 | if ($field) {
197 | $prefix .= "field='{$field}' ";
198 | }
199 |
200 | if ($boost) {
201 | $prefix .= "boost='{$boost}' ";
202 | }
203 |
204 | $prefix .= "'{$value}')";
205 |
206 | $this->query[] = $prefix;
207 |
208 | return $this;
209 | }
210 |
211 | /**
212 | * Searches a numeric field (double, double-array, int, int-array) or date
213 | * field (date, date-array) for values in the specified range. Matches
214 | * documents that have at least one value in the field within the specified
215 | * range. The field option must be specified.
216 | *
217 | * To specify a range of values, use a comma (,) to separate the upper and
218 | * lower bounds and enclose the range using brackets or braces. A square
219 | * bracket, [ or ], indicates that the bound is included in the range, a
220 | * curly brace, { or }, excludes the bound. You can omit the upper or lower
221 | * bound to specify an open-ended range. When omitting a bound, you must
222 | * use a curly brace.
223 | *
224 | * Dates and times are specified in UTC (Coordinated Universal Time)
225 | * according to IETF RFC3339: yyyy-mm-ddTHH:mm:ss.SSSZ. In UTC, for example,
226 | * 5:00 PM August 23, 1970 is: 1970-08-23T17:00:00Z. Note that you can also
227 | * specify fractional seconds when specifying times in UTC. For example,
228 | * 1967-01-31T23:20:50.650Z.
229 | *
230 | * @param string $field
231 | * @param integer|string $min
232 | * @param integer|string $max
233 | *
234 | * @return StructuredSearch
235 | */
236 | public function range($field, $min, $max)
237 | {
238 | $range = "(range field={$field} ";
239 |
240 | if ($min and !$max) {
241 | $value = "[{$min},}";
242 | }
243 | else if (!$min and $max) {
244 | $value = "{,{$max}]";
245 | }
246 | else if ($min and $max) {
247 | $value = "[{$min},{$max}]";
248 | }
249 | else {
250 | return $this;
251 | }
252 |
253 | $range .= "{$value})";
254 |
255 | $this->query[] = $range;
256 |
257 | return $this;
258 | }
259 |
260 | /**
261 | * Searches the specified field for a string, numeric value, or date. The
262 | * field option must be specified when searching for a value. If you omit
263 | * the field option, Amazon CloudSearch searches all statically configured
264 | * text and text-array fields by default. Dynamic fields and literal fields
265 | * are not searched by default. You can specify which fields you want to
266 | * search by default by specifying the q.options fields option.
267 | *
268 | * Dates and times are specified in UTC (Coordinated Universal Time)
269 | * according to IETF RFC3339: yyyy-mm-ddTHH:mm:ss.SSSZ. In UTC, for example,
270 | * 5:00 PM August 23, 1970 is: 1970-08-23T17:00:00Z. Note that you can also
271 | * specify fractional seconds when specifying times in UTC. For example,
272 | * 1967-01-31T23:20:50.650Z.
273 | *
274 | * @param string $value
275 | * @param string $field
276 | * @param integer $boost
277 | *
278 | * @return StructuredSearch
279 | */
280 | private function term($value, $field = null, $boost = null)
281 | {
282 | $term = "(term ";
283 |
284 | if ($field) {
285 | $term .= "field='{$field}' ";
286 | }
287 |
288 | if ($boost) {
289 | $term .= "boost='{$boost}' ";
290 | }
291 |
292 | $term .= "'{$value}')";
293 |
294 | $this->query[] = $term;
295 |
296 | return $this;
297 | }
298 |
299 | /**
300 | * Query and filterQuery methods are made inaccessible in order to trigger
301 | * the __call magic method. Here we can escape argument strings prior to
302 | * calling the intended function
303 | *
304 | * @param string $method
305 | * @param array $args
306 | *
307 | * @return self
308 | * @throws Exception
309 | */
310 | public function __call($method, $args)
311 | {
312 | // Raise exception if no method
313 | if (!method_exists($this, $method)) {
314 | throw new Exception("Method doesn't exist");
315 | }
316 |
317 | // Escape string arguments
318 | foreach ($args as $key => $value) {
319 | if (gettype($value) == 'string') {
320 | $value = preg_replace('#\\\#i', "\\\\\\", $value);
321 | $value = preg_replace("#'#", "\\'", $value);
322 | $args[$key] = $value;
323 | }
324 | }
325 |
326 | // call the desired the method
327 | call_user_func_array([$this, $method], $args);
328 |
329 | return $this;
330 | }
331 |
332 | /**
333 | * Magic method when casting as string
334 | * Concatenate all query statements into wrapped 'and' statement
335 | *
336 | * @return string
337 | */
338 | public function __toString()
339 | {
340 | return "(and " . implode('', $this->query) . ")";
341 | }
342 | }
--------------------------------------------------------------------------------
/src/Queue.php:
--------------------------------------------------------------------------------
1 | database = $database;
46 | }
47 |
48 | /**
49 | * Push a new job into the queue.
50 | *
51 | * @param string $action
52 | * @param string $entry_id
53 | * @param string $entry_type
54 | *
55 | * @return mixed
56 | */
57 | public function push($action, $entry_id, $entry_type)
58 | {
59 | return $this->database->table($this->table)->updateOrInsert([
60 | 'entry_id' => $entry_id,
61 | 'entry_type' => $entry_type,
62 | 'action' => $action,
63 | ], [
64 | 'entry_id' => $entry_id,
65 | 'entry_type' => $entry_type,
66 | 'action' => $action,
67 | 'status' => self::STATUS_WAITING,
68 | 'created_at' => $this->currentTime(),
69 | ]);
70 | }
71 |
72 | /**
73 | * Get a queue batch.
74 | *
75 | * @return Collection
76 | */
77 | public function getBatch()
78 | {
79 | // Mark all as running
80 | $this->database->table($this->table)
81 | ->where('status', self::STATUS_WAITING)
82 | ->update([
83 | 'status' => self::STATUS_RUNNING,
84 | ]);
85 |
86 | // Start processing
87 | return $this->database->table($this->table)
88 | ->select(['entry_id', 'entry_type', 'action'])
89 | ->where('status', self::STATUS_RUNNING)
90 | ->get()
91 | ->groupBy('action');
92 | }
93 |
94 | /**
95 | * Flush a batch.
96 | *
97 | * @return bool
98 | */
99 | public function flushBatch()
100 | {
101 | return $this->database->table($this->table)
102 | ->where('status', self::STATUS_RUNNING)
103 | ->delete();
104 | }
105 |
106 | /**
107 | * Get the current system time as a UNIX timestamp.
108 | *
109 | * @return int
110 | */
111 | protected function currentTime()
112 | {
113 | return Carbon::now()->getTimestamp();
114 | }
115 | }
--------------------------------------------------------------------------------