Skip to content

Commit c6b1761

Browse files
committed
Added Algolia driver and support for geo searching.
1 parent 0c3d2a8 commit c6b1761

File tree

8 files changed

+330
-5
lines changed

8 files changed

+330
-5
lines changed

README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Search Package for Laravel 5
22

3-
This package provides a unified API across a variety of different full text search services. It currently supports drivers for [Elasticsearch](http://www.elasticsearch.org/) and [ZendSearch](https://github.com/zendframework/ZendSearch) (good for local use).
3+
This package provides a unified API across a variety of different full text search services. It currently supports drivers for [Elasticsearch](http://www.elasticsearch.org/), [Algolia](https://www.algolia.com/), and [ZendSearch](https://github.com/zendframework/ZendSearch) (good for local use).
44

55
## Installation Via Composer
66

@@ -48,6 +48,7 @@ The following dependencies are needed for the listed search drivers:
4848

4949
* ZendSearch: `zendframework/zendsearch`
5050
* Elasticsearch: `elasticsearch/elasticsearch`
51+
* Algolia: `algolia/algoliasearch-client-php`
5152

5253
#### Default Index
5354

@@ -152,6 +153,20 @@ $results = Search::search('content', 'fox')
152153

153154
> **Note:** Filters do not guarantee an exact match of the entire field value if the value contains multiple words.
154155
156+
#### Geo-Search
157+
158+
Some drivers support location-aware searching:
159+
160+
```php
161+
$results = Search::search('content', 'fox')
162+
->whereLocation(36.16781, -96.023561, 10000)
163+
->get();
164+
```
165+
166+
Where the parameters are `latitude`, `longitude`, and `distance` (in meters).
167+
168+
> **Note:** Currently, only the `algolia` driver supports geo-searching. Ensure each indexed record contains the location information: `_geoloc => ['lat' => 1.23, 'lng' => 1.23]`.
169+
155170
#### Limit Your Result Set
156171

157172
```php

composer.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mmanos/laravel-search",
33
"description": "A search package for Laravel 5.",
4-
"keywords": ["laravel", "search", "lucene", "zendsearch", "elasticsearch", "full", "text"],
4+
"keywords": ["laravel", "search", "lucene", "zendsearch", "elasticsearch", "algolia", "full", "text"],
55
"authors": [
66
{
77
"name": "Mark Manos",
@@ -14,11 +14,13 @@
1414
},
1515
"require-dev": {
1616
"zendframework/zendsearch": "dev-master",
17-
"elasticsearch/elasticsearch": "1.0.*@dev"
17+
"elasticsearch/elasticsearch": "1.0.*@dev",
18+
"algolia/algoliasearch-client-php": "1.1.*"
1819
},
1920
"suggest": {
2021
"zendframework/zendsearch": "Required for ZendSearch driver.",
21-
"elasticsearch/elasticsearch": "Required for ElasticSearch driver."
22+
"elasticsearch/elasticsearch": "Required for ElasticSearch driver.",
23+
"algolia/algoliasearch-client-php": "Required for Algolia driver."
2224
},
2325
"autoload": {
2426
"psr-0": {

src/Mmanos/Search/Index.php

+19
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public static function factory($index, $driver = null)
4848
}
4949

5050
switch ($driver) {
51+
case 'algolia':
52+
return new Index\Algolia($index, 'algolia');
53+
5154
case 'elasticsearch':
5255
return new Index\Elasticsearch($index, 'elasticsearch');
5356

@@ -72,6 +75,22 @@ public function where($field, $value)
7275
return $query->where($field, $value);
7376
}
7477

78+
/**
79+
* Initialize and return a new Query instance on this index
80+
* with the requested geo distance where clause.
81+
*
82+
* @param float $lat
83+
* @param float $long
84+
* @param int $distance_in_meters
85+
*
86+
* @return \Mmanos\Search\Query
87+
*/
88+
public function whereLocation($lat, $long, $distance_in_meters = 10000)
89+
{
90+
$query = new Query($this);
91+
return $query->whereLocation($lat, $long, $distance_in_meters);
92+
}
93+
7594
/**
7695
* Initialize and return a new Query instance on this index
7796
* with the requested search condition.

src/Mmanos/Search/Index/Algolia.php

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<?php namespace Mmanos\Search\Index;
2+
3+
use Config;
4+
5+
class Algolia extends \Mmanos\Search\Index
6+
{
7+
/**
8+
* The Algolia client shared by all instances.
9+
*
10+
* @var \AlgoliaSearch\Client
11+
*/
12+
protected static $client;
13+
14+
/**
15+
* Index instance.
16+
*
17+
* @var \AlgoliaSearch\Index
18+
*/
19+
protected $index;
20+
21+
/**
22+
* An array of stored query totals to help reduce subsequent count calls.
23+
*
24+
* @var array
25+
*/
26+
protected $stored_query_totals = array();
27+
28+
/**
29+
* Get the Algolia client associated with this instance.
30+
*
31+
* @return \AlgoliaSearch\Client
32+
*/
33+
protected function getClient()
34+
{
35+
if (!static::$client) {
36+
static::$client = new \AlgoliaSearch\Client(
37+
Config::get('laravel-search::connections.algolia.config.application_id'),
38+
Config::get('laravel-search::connections.algolia.config.admin_api_key')
39+
);
40+
}
41+
42+
return static::$client;
43+
}
44+
45+
/**
46+
* Get the ZendSearch lucene index instance associated with this instance.
47+
*
48+
* @return \AlgoliaSearch\Index
49+
*/
50+
protected function getIndex()
51+
{
52+
if (!$this->index) {
53+
$this->index = $this->getClient()->initIndex($this->name);
54+
}
55+
56+
return $this->index;
57+
}
58+
59+
/**
60+
* Get a new query instance from the driver.
61+
*
62+
* @return array
63+
*/
64+
public function newQuery()
65+
{
66+
return array(
67+
'terms' => '',
68+
'query' => array(
69+
'facets' => '*',
70+
),
71+
);
72+
}
73+
74+
/**
75+
* Add a search/where clause to the given query based on the given condition.
76+
* Return the given $query instance when finished.
77+
*
78+
* @param array $query
79+
* @param array $condition - field : name of the field
80+
* - value : value to match
81+
* - required : must match
82+
* - prohibited : must not match
83+
* - phrase : match as a phrase
84+
* - filter : filter results on value
85+
* - fuzzy : fuzziness value (0 - 1)
86+
*
87+
* @return array
88+
*/
89+
public function addConditionToQuery($query, array $condition)
90+
{
91+
$value = trim(array_get($condition, 'value'));
92+
$field = array_get($condition, 'field');
93+
94+
if ('xref_id' == $field) {
95+
$field = 'objectID';
96+
}
97+
98+
if (array_get($condition, 'filter')) {
99+
if (is_numeric($value)) {
100+
$query['query']['numericFilters'][] = "{$field}={$value}";
101+
}
102+
else {
103+
$query['query']['facetFilters'][] = "{$field}:{$value}";
104+
}
105+
}
106+
else if (array_get($condition, 'lat')) {
107+
$query['query']['aroundLatLng'] = array_get($condition, 'lat') . ',' . array_get($condition, 'long');
108+
$query['query']['aroundRadius'] = array_get($condition, 'distance');
109+
}
110+
else {
111+
$query['terms'] .= ' ' . $value;
112+
113+
if (!empty($field) && '*' !== $field) {
114+
$field = is_array($field) ? $field : array($field);
115+
$query['query']['restrictSearchableAttributes'] = implode(',', $field);
116+
}
117+
}
118+
119+
return $query;
120+
}
121+
122+
/**
123+
* Execute the given query and return the results.
124+
* Return an array of records where each record is an array
125+
* containing:
126+
* - the record 'id'
127+
* - all parameters stored in the index
128+
* - an optional '_score' value
129+
*
130+
* @param array $query
131+
* @param array $options - limit : max # of records to return
132+
* - offset : # of records to skip
133+
*
134+
* @return array
135+
*/
136+
public function runQuery($query, array $options = array())
137+
{
138+
$original_query = $query;
139+
140+
if (isset($options['limit']) && isset($options['offset'])) {
141+
$query['query']['page'] = ($options['offset'] / $options['limit']);
142+
$query['query']['hitsPerPage'] = $options['limit'];
143+
}
144+
145+
$query['terms'] = trim($query['terms']);
146+
if (isset($query['query']['numericFilters'])) {
147+
$query['query']['numericFilters'] = implode(',', $query['query']['numericFilters']);
148+
}
149+
150+
try {
151+
$response = $this->getIndex()->search(array_get($query, 'terms'), array_get($query, 'query'));
152+
$this->stored_query_totals[md5(serialize($original_query))] = array_get($response, 'nbHits');
153+
} catch (\Exception $e) {
154+
$response = array();
155+
}
156+
157+
$results = array();
158+
159+
if (array_get($response, 'hits')) {
160+
foreach (array_get($response, 'hits') as $hit) {
161+
$hit['id'] = array_get($hit, 'objectID');
162+
$hit['_score'] = 1;
163+
$results[] = $hit;
164+
}
165+
}
166+
167+
return $results;
168+
}
169+
170+
/**
171+
* Execute the given query and return the total number of results.
172+
*
173+
* @param array $query
174+
*
175+
* @return int
176+
*/
177+
public function runCount($query)
178+
{
179+
if (isset($this->stored_query_totals[md5(serialize($query))])) {
180+
return $this->stored_query_totals[md5(serialize($query))];
181+
}
182+
183+
$query['terms'] = trim($query['terms']);
184+
if (isset($query['numericFilters'])) {
185+
$query['numericFilters'] = implode(',', $query['numericFilters']);
186+
}
187+
if (isset($query['facets'])) {
188+
$query['facets'] = implode(',', $query['facets']);
189+
}
190+
191+
try {
192+
return array_get($this->getIndex()->search(array_get($query, 'terms'), array_get($query, 'query')), 'nbHits');
193+
} catch (\Exception $e) {
194+
return 0;
195+
}
196+
}
197+
198+
/**
199+
* Add a new document to the index.
200+
* Any existing document with the given $id should be deleted first.
201+
* $fields should be indexed but not necessarily stored in the index.
202+
* $parameters should be stored in the index but not necessarily indexed.
203+
*
204+
* @param mixed $id
205+
* @param array $fields
206+
* @param array $parameters
207+
*
208+
* @return bool
209+
*/
210+
public function insert($id, array $fields, array $parameters = array())
211+
{
212+
$fields['objectID'] = $id;
213+
214+
$this->getIndex()->saveObject(array_merge($parameters, $fields));
215+
216+
return true;
217+
}
218+
219+
/**
220+
* Delete the document from the index associated with the given $id.
221+
*
222+
* @param mixed $id
223+
*
224+
* @return bool
225+
*/
226+
public function delete($id)
227+
{
228+
try {
229+
$this->getIndex()->deleteObject($id);
230+
} catch (\Exception $e) {
231+
return false;
232+
}
233+
234+
return true;
235+
}
236+
237+
/**
238+
* Delete the entire index.
239+
*
240+
* @return bool
241+
*/
242+
public function deleteIndex()
243+
{
244+
try {
245+
$this->getIndex()->clearIndex();
246+
} catch (\Exception $e) {
247+
return false;
248+
}
249+
250+
return true;
251+
}
252+
}

src/Mmanos/Search/Index/Elasticsearch.php

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public function newQuery()
7171
*/
7272
public function addConditionToQuery($query, array $condition)
7373
{
74+
if (array_get($condition, 'lat')) {
75+
return $query;
76+
}
77+
7478
$value = trim(array_get($condition, 'value'));
7579
$field = array_get($condition, 'field', '_all');
7680

src/Mmanos/Search/Index/Zend.php

+5
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ public function newQuery()
9393
*/
9494
public function addConditionToQuery($query, array $condition)
9595
{
96+
if (array_get($condition, 'lat')) {
97+
return $query;
98+
}
99+
96100
$value = trim($this->escape(array_get($condition, 'value')));
97101
if (array_get($condition, 'phrase') || array_get($condition, 'filter')) {
98102
$value = '"' . $value . '"';
@@ -224,6 +228,7 @@ public function insert($id, array $fields, array $parameters = array())
224228

225229
// Add fields to document to be indexed (but not stored).
226230
foreach ($fields as $field => $value) {
231+
if (is_array($value)) continue;
227232
$doc->addField(\ZendSearch\Lucene\Document\Field::unStored(trim($field), trim($value)));
228233
}
229234

0 commit comments

Comments
 (0)