Skip to content

Commit 5650806

Browse files
committed
Full index searching, geo & regex queries
1 parent 716722a commit 5650806

File tree

7 files changed

+774
-56
lines changed

7 files changed

+774
-56
lines changed

README.md

Lines changed: 198 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ Elasticsearch in laravel as if it were native to Laravel, meaning:
1313
- [Soft Deletes](#soft-deletes)
1414
- [Aggregations](#aggregation)
1515
- [Migrations](#schema/index)
16+
- ES features like [Geo Filtering](#geo) & [Regular Expressions](#regex-in-where)
1617

1718
- No need to write your own DSL queries ([unless you want to](#raw-dsl)!)
18-
- This plugin is not a search wrapper for your existing models
19+
- [Eloquent style searching](#elasticsearching)
1920

2021
> # Alpha release notice
2122
> This package is being released prematurely to an interested community of testers. It is not ready for production just yet only due to a lack of testing mileage. Once deemed stable, the plugin will move to V1. Elasticsearch is a deep topic on its own and there are many native features that have not yet been included. I built this because I needed it but this plugin is for everyone; submit issues (there's no way I could have found all the edge cases on my own) and feel free to submit pull requests.
@@ -31,9 +32,9 @@ Elasticsearch in laravel as if it were native to Laravel, meaning:
3132
____
3233

3334
Installation
34-
-------------
35+
===============
3536

36-
#### (current Alphas)
37+
#### (Current Alphas)
3738

3839
Install the package via Composer:
3940

@@ -56,7 +57,7 @@ $ composer require pdphilip/elasticsearch:v0.6-alpha
5657
```
5758

5859
Configuration
59-
-------------
60+
===============
6061

6162
Proposed .env settings:
6263

@@ -113,7 +114,7 @@ Add the service provider to `config/app.php` (If your Laravel version does not a
113114
```
114115

115116
Eloquent
116-
-------------
117+
===============
117118

118119
### Extending the base model
119120

@@ -344,6 +345,56 @@ Pagination links (Blade)
344345
{{ $products->appends(request()->query())->links() }}
345346
```
346347

348+
349+
350+
Elasticsearch specific queries
351+
-----------------------------
352+
353+
#### Geo:
354+
355+
**GeoBox**
356+
357+
Filters results of all geo points that fall within a box drawn from `topleft[lat,lon]` to `bottomRight[lat,lon]`
358+
359+
- **Method**: `filterGeoBox($field,$topleft,$bottomRight)`
360+
- `$topleft` and `$bottomRight` are arrays that hold [$lat,$lon] coordinates
361+
362+
```php
363+
UserLog::where('status',7)->filterGeoBox('agent.geo',[-10,10],[10,-10])->get();
364+
```
365+
366+
**GeoPoint**
367+
368+
Filters results that fall within a radius distance from a `point[lat,lon]`
369+
370+
- **Method**: `filterGeoPoint($field,$distance,$point)`
371+
- `$distance` is a string value of distance and distance-unit, see [https://www.elastic.co/guide/en/elasticsearch/reference/current/api-conventions.html#distance-units](distance units)
372+
373+
```php
374+
UserLog::where('status',7)->filterGeoPoint('agent.geo','20km',[0,0])->get();
375+
```
376+
377+
**Note:** the field **must be of type geo otherwise your [shards will fail](#error-all-shards-failed) **, make sure to set the geo field in your [migration](#migrations), ex:
378+
379+
```php
380+
Schema::create('user_logs',function (IndexBlueprint $index){
381+
$index->geo('agent.geo');
382+
});
383+
```
384+
385+
386+
387+
#### Regex (in where)
388+
389+
[Syntax](https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html)
390+
391+
```php
392+
Product::whereRegex('color','bl(ue)?(ack)?')->get(); //Returns blue or black
393+
Product::whereRegex('color','bl...*')->get(); //Returns blue or black or blond or blush etc..
394+
```
395+
396+
397+
347398
Saving Models
348399
-------------
349400

@@ -483,6 +534,131 @@ $product->forceDelete();
483534

484535
```
485536

537+
538+
539+
Elasticsearching
540+
===============
541+
542+
The Search Query
543+
----------------
544+
545+
The search query is different from the `where()->get()` methods as search is performed over all (or selected) fields in the index. Building a search query is easy and intuitive to seasoned Eloquenters with a slight twist; simply static call off your model with `term()`, chain your ORM clauses, then end your chain with `search()` to perform your search, ie:
546+
547+
```php
548+
MyModel::term('XYZ')->.........->search()
549+
```
550+
551+
### 1.Term:
552+
553+
**1.1 Simple example**
554+
555+
- To search across all the fields in the **books** index for '**eric**' (case-insensitive if the default analyser is set),
556+
- Results ordered by most relevant first (score in desc order)
557+
558+
```php
559+
Book::term('Eric')->search();
560+
```
561+
562+
**1.2 Multiple terms**
563+
564+
- To search across all the fields in the **books** index for: **eric OR (lean AND startup)**
565+
- ***Note**: You can't start a search query chain with and/or and you can't have subsequent chained terms without and/or - **ordering matters***
566+
567+
```php
568+
Book::term('Eric')->orTerm('Lean')->andTerm('Startup')->search();
569+
```
570+
571+
**1.3 Boosting Terms**
572+
573+
- **Boosting terms: `term(string $term, int $boostFactor)`**
574+
- To search across all fields for **eric OR lean OR startup** but 'eric' is boosted by a factor of 2; **(eric)^2**
575+
- Boosting affects the score and thus the ordering of the results for relevance
576+
- Also note, spaces in terms are treated as OR's between each word
577+
578+
```php
579+
Book::term('Eric',2)->orTerm('Lean Startup')->search();
580+
```
581+
582+
**1.4 Searching over selected fields**
583+
584+
- To search across fields [**title, author and description**] for **eric**.
585+
586+
```php
587+
Book::term('Eric')->fields(['title','author','description'])->search();
588+
```
589+
590+
**1.5 Boosting fields**
591+
592+
- To search across fields [**title, author and description**] for **eric**.
593+
- **title** is boosted by a factor of 3, search hits here will be the most relevant
594+
- **author** is boosted by a factor of 2, search hits here will be the second most relevant
595+
- **description** has no boost, search hits here will be the least relevant
596+
- *The results, as per the default, are ordered by most relevant first (score in desc order)*
597+
598+
```php
599+
Book::term('Eric')->field('title',3)->field('author',2)->field('description')->search();
600+
```
601+
602+
**1.6 Minimum should match**
603+
604+
- Controls how many 'should' clauses the query should match
605+
- Caveats:
606+
- Fields must be specified in your query
607+
- You can have no standard clauses in your query (ex `where()`)
608+
- Won't work on SoftDelete enabled models
609+
- https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html
610+
611+
- Match at least 2 of the 3 terms:
612+
613+
```php
614+
Book::term('Eric')->orTerm('Lean')->andTerm('Startup')->field('title')->field('author')->minShouldMatch(2)->search();
615+
```
616+
617+
**1.7 Min Score**
618+
619+
- Sets a min_score filter for the search
620+
- (Optional, float) Minimum 'relevance score' for matching documents. Documents with a lower 'score' are not included in the search results.
621+
622+
```php
623+
Book::term('Eric')->field('title',3)->field('author',2)->field('description')->minScore(2.1)->search();
624+
```
625+
626+
**1.8 Blend Search with [most] standard eloquent queries**
627+
628+
- Yep:
629+
630+
```php
631+
Book::term('David')->field('title',3)->field('author',2)->field('description')->minScore(2.1)->where('is_active',true)->search();
632+
```
633+
634+
635+
636+
### 2. FuzzyTerm:
637+
638+
- Same usage as `term()` `andTerm()` `orTerm()` but as
639+
- `fuzzyTerm()`
640+
- `orFuzzyTerm()`
641+
- `andFuzzyTerm()`
642+
643+
```php
644+
Book::fuzzyTerm('quikc')->orFuzzyTerm('brwn')->andFuzzyTerm('foks')->search();
645+
```
646+
647+
### 2. RegEx in Search:
648+
649+
https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html
650+
651+
- Same usage as `term()` `andTerm()` `orTerm()` but as
652+
- `regEx()`
653+
- `orRegEx()`
654+
- `andRegEx()`
655+
656+
```php
657+
Book::regEx('joh?n(ath[oa]n)')->andRegEx('doey*')->search();
658+
```
659+
660+
661+
486662
Mutators & Casting
487663
-------------
488664

@@ -493,7 +669,7 @@ See [https://laravel.com/docs/8.x/eloquent-mutators](https://laravel.com/docs/8.
493669
Cool!
494670

495671
Relationships
496-
-------------
672+
==============
497673

498674
Model Relationships are the lifeblood of any Laravel App, for that you can use them with `belongsTo` , `hasMany`
499675
, `hasOne`, `morphOne` and `morphMany` as you have before:
@@ -838,14 +1014,16 @@ $userLog->user->name;
8381014
```
8391015

8401016
Schema/Index
841-
-------------
1017+
==========
1018+
Migrations
1019+
----------
8421020

843-
Since there is very little overlap with how Elasticsearch handles index management to how MySQL and related tencologioes
1021+
Since there is very little overlap with how Elasticsearch handles index management to how MySQL and related technologies
8441022
handle Schema manipulation; the schema feature of this plugin has been written from the ground up to work 100% with
8451023
Elasticsearch.
8461024

847-
You can still create a migration class as normal (and it's recommened that you do), however the `up()` and `down()`
848-
methods will need to encapsualte the following:
1025+
You can still create a migration class as normal (and it's recommended that you do), however the `up()` and `down()`
1026+
methods will need to encapsulate the following:
8491027

8501028
- **Schema** via `PDPhilip\Elasticsearch\Schema\Schema`
8511029
- **IndexBlueprint** via `PDPhilip\Elasticsearch\Schema\IndexBlueprint`
@@ -992,10 +1170,8 @@ Queues
9921170
_[Coming]_
9931171

9941172

995-
Elasticsearch-specific operators
996-
--------------------------------
997-
998-
#### RAW DSL
1173+
RAW DSL
1174+
========
9991175

10001176
BYO query, sure! We'll get out the way and try to return the values in a collection for you:
10011177

@@ -1016,6 +1192,13 @@ return Product::rawSearch($bodyParams); //Will search within the products index
10161192
Elasticsearchisms
10171193
=================
10181194

1195+
#### Error: all shards failed
1196+
1197+
This error usually points to an index mapping issue, ex:
1198+
1199+
- Trying to order on a TEXT field
1200+
- Trying a get filter on a field that is not explicitly set as one
1201+
10191202
#### Elasticsearch's default search limit is to return 10 collections
10201203

10211204
This plugin sets the default limit to 1000, however you can set your own with `MAX_SIZE`:
@@ -1075,7 +1258,7 @@ once and not update immediately or won't need to search for the record immediate
10751258

10761259
### Unsupported Eloquent methods
10771260

1078-
- _[Coming]_
1261+
`upsert()`, `distinct()`, `groupBy()`, `groupByRaw()`
10791262

10801263
Acknowledgements
10811264
-------------

src/DSL/Bridge.php

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@
44

55
use Exception;
66
use Elasticsearch\Client;
7-
use Illuminate\Support\Carbon;
87
use ONGR\ElasticsearchDSL\Aggregation\Matrix\MaxAggregation as MatrixAggregation;
98
use ONGR\ElasticsearchDSL\Aggregation\Metric\AvgAggregation;
109
use ONGR\ElasticsearchDSL\Aggregation\Metric\MaxAggregation;
1110
use ONGR\ElasticsearchDSL\Aggregation\Metric\MinAggregation;
1211
use ONGR\ElasticsearchDSL\Aggregation\Metric\SumAggregation;
13-
use ONGR\ElasticsearchDSL\Query\FullText\QueryStringQuery;
14-
use ONGR\ElasticsearchDSL\Query\MatchAllQuery;
1512
use ONGR\ElasticsearchDSL\Query\TermLevel\IdsQuery;
1613
use ONGR\ElasticsearchDSL\Search;
17-
use ONGR\ElasticsearchDSL\Sort\FieldSort;
14+
1815

1916
class Bridge
2017
{
@@ -32,7 +29,6 @@ class Bridge
3229
private $index;
3330

3431

35-
3632
public function __construct(Client $client, $index, $maxSize)
3733
{
3834
$this->client = $client;
@@ -123,23 +119,30 @@ public function processFind($wheres, $options, $columns): Results
123119
// }
124120
// }
125121
$params = $this->buildParams($this->index, $wheres, $options, $columns);
122+
return $this->_returnSearch($params,__FUNCTION__);
123+
124+
}
125+
126+
public function processSearch($searchParams,$searchOptions,$wheres,$opts,$fields,$cols)
127+
{
128+
$params = $this->buildSearchParams($this->index, $searchParams, $searchOptions,$wheres,$opts,$fields,$cols);
129+
return $this->_returnSearch($params,__FUNCTION__);
130+
131+
}
132+
133+
protected function _returnSearch($params,$source)
134+
{
126135
if (empty($params['size'])) {
127136
$params['size'] = $this->maxSize;
128137
}
129138
try {
130139
$process = $this->client->search($params);
131140

132-
return $this->_sanitizeSearchResponse($process, $params, $this->_queryTag(__FUNCTION__));
141+
return $this->_sanitizeSearchResponse($process, $params, $this->_queryTag($source));
133142
} catch (Exception $e) {
134143

135-
return $this->_returnError($e->getMessage(), $e->getCode(), $params, $this->_queryTag(__FUNCTION__));
144+
return $this->_returnError($e->getMessage(), $e->getCode(), $params, $this->_queryTag($source));
136145
}
137-
138-
}
139-
140-
public function processSearch($term, $options, $columns = [])
141-
{
142-
// $params = $this->_buildParams([], $options);
143146
}
144147

145148
public function processDistinct($column, $wheres): Results
@@ -217,8 +220,6 @@ public function processSave($data, $refresh): Results
217220

218221
}
219222

220-
221-
222223
public function processInsertOne($values, $refresh): Results
223224
{
224225
return $this->processSave($values, $refresh);
@@ -602,7 +603,6 @@ private function _matrixAggregate($wheres, $options, $columns): Results
602603
//======================================================================
603604

604605

605-
606606
private function _queryTag($function)
607607
{
608608
return str_replace('process', '', $function);

0 commit comments

Comments
 (0)