├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── Mmanos │ └── Search │ │ ├── Facade.php │ │ ├── Index.php │ │ ├── Index │ │ ├── Algolia.php │ │ ├── Elasticsearch.php │ │ └── Zend.php │ │ ├── Query.php │ │ ├── Search.php │ │ └── SearchServiceProvider.php └── config │ ├── .gitkeep │ └── search.php └── tests └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Mark Manos 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Search Package for Laravel 5 2 | 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). 4 | 5 | ## Installation Via Composer 6 | 7 | Add this to you composer.json file, in the require object: 8 | 9 | ```javascript 10 | "mmanos/laravel-search": "dev-master" 11 | ``` 12 | 13 | After that, run composer install to install the package. 14 | 15 | Add the service provider to `app/config/app.php`, within the `providers` array. 16 | 17 | ```php 18 | 'providers' => array( 19 | // ... 20 | 'Mmanos\Search\SearchServiceProvider', 21 | ) 22 | ``` 23 | 24 | Add a class alias to `app/config/app.php`, within the `aliases` array. 25 | 26 | ```php 27 | 'aliases' => array( 28 | // ... 29 | 'Search' => 'Mmanos\Search\Facade', 30 | ) 31 | ``` 32 | 33 | ## Laravel 4 34 | 35 | Use the `0.0` branch or the `v0.*` tags for Laravel 4 support. 36 | 37 | ## Configuration 38 | 39 | Publish the default config file to your application so you can make modifications. 40 | 41 | ```console 42 | $ php artisan vendor:publish 43 | ``` 44 | 45 | #### Dependencies 46 | 47 | The following dependencies are needed for the listed search drivers: 48 | 49 | * ZendSearch: `zendframework/zendsearch` 50 | * Elasticsearch: `elasticsearch/elasticsearch` 51 | * Algolia: `algolia/algoliasearch-client-php` 52 | 53 | #### Default Index 54 | 55 | This package provides a convenient syntax for working with a "default" index. Edit the `default_index` field in the config file to change this value. If you need to work with more than one index, see *Working With Multiple Indicies* below. 56 | 57 | ## Indexing Operations 58 | 59 | Indexing is very easy with this package. Simply provide a unique identifier for the document and an associative array of fields to index. 60 | 61 | The index will be **created automatically** if it does not exist the first time you access it. 62 | 63 | #### Index A Document 64 | 65 | Add a document to the "default" index with an `id` of "1". 66 | 67 | ```php 68 | Search::insert(1, array( 69 | 'title' => 'My title', 70 | 'content' => 'The quick brown fox...', 71 | 'status' => 'published', 72 | )); 73 | ``` 74 | 75 | > **Note:** `id` may be a string or an integer. This id is used to delete records and is also returned in search results. 76 | 77 | #### Store Extra Parameters With A Document 78 | 79 | You may store extra parameters with a document so they can be retrieved at a later point from search results. This can be useful for referencing timestamps or other record identifiers. 80 | 81 | ```php 82 | Search::insert( 83 | "post-1", 84 | array( 85 | 'title' => 'My title', 86 | 'content' => 'The quick brown fox...', 87 | 'status' => 'published', 88 | ), 89 | array( 90 | 'created_at' => time(), 91 | 'creator_id' => 5, 92 | ) 93 | ); 94 | ``` 95 | 96 | > **Note:** Extra parameters are not indexed but are stored in the index for future retrieval. 97 | 98 | #### Delete A Document 99 | 100 | Delete a document from the "default" index with an `id` of "1": 101 | 102 | ```php 103 | Search::delete(1); 104 | ``` 105 | 106 | #### Delete An Index 107 | 108 | ```php 109 | Search::deleteIndex(); 110 | ``` 111 | 112 | ## Search Operations 113 | 114 | #### Search For A Document 115 | 116 | Search the "default" index for documents who's `content` field contains the word "fox": 117 | 118 | ```php 119 | $results = Search::search('content', 'fox')->get(); 120 | ``` 121 | 122 | #### Search More Than One Field 123 | 124 | ```php 125 | $results = Search::search(array('title', 'content'), 'fox')->get(); 126 | ``` 127 | 128 | #### Search All Fields 129 | 130 | ```php 131 | $results = Search::search(null, 'fox')->get(); 132 | ``` 133 | 134 | #### Perform A Fuzzy Search 135 | 136 | Perform a fuzzy search to find results with similar, but not exact, spelling. For example, you want to return documents containing the word "updates" by searching for the word "update": 137 | 138 | ```php 139 | $results = Search::search('content', 'update', array('fuzzy'=>true))->get(); 140 | ``` 141 | 142 | > **Note:** You may also pass a numeric value between 0 and 1 for the fuzzy parameter, where a value closer to 1 requires a higher similarity. Defaults to 0.5. 143 | 144 | #### Apply A Filter To Your Query 145 | 146 | You can apply filters to your search queries as well. Filters attempt to match the value you specify as an entire "phrase". 147 | 148 | ```php 149 | $results = Search::search('content', 'fox') 150 | ->where('status', 'published') 151 | ->get(); 152 | ``` 153 | 154 | > **Note:** Filters do not guarantee an exact match of the entire field value if the value contains multiple words. 155 | 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 | 170 | #### Limit Your Result Set 171 | 172 | ```php 173 | $results = Search::search('content', 'fox') 174 | ->where('status', 'published') 175 | ->limit(10) // Limit 10 176 | ->get(); 177 | 178 | $results = Search::search('content', 'fox') 179 | ->where('status', 'published') 180 | ->limit(10, 30) // Limit 10, offset 30 181 | ->get(); 182 | ``` 183 | 184 | #### Paginate Your Result Set 185 | 186 | You can also paginate your result set using a Laravel paginator instance. 187 | 188 | ```php 189 | $paginator = Search::search('content', 'fox')->paginate(15); 190 | ``` 191 | 192 | #### Limit The Fields You Want Back From The Response 193 | 194 | ```php 195 | $results = Search::select('id', 'created_at') 196 | ->search('content', 'fox') 197 | ->get(); 198 | ``` 199 | 200 | #### Chain Multiple Searches And Filters 201 | 202 | ```php 203 | $results = Search::select('id', 'created_at') 204 | ->where('title', 'My title') 205 | ->where('status', 'published') 206 | ->search('content', 'fox') 207 | ->search('content', 'quick') 208 | ->limit(10) 209 | ->get(); 210 | ``` 211 | 212 | > **Note:** Chained filters/searches are constructed as boolean queries where each **must** provide a match. 213 | 214 | #### Delete All Documents That Match A Query 215 | 216 | ```php 217 | Search::search('content', 'fox')->delete(); 218 | ``` 219 | 220 | ## Working With Multiple Indicies 221 | 222 | If you need to work with more than one index, you may access all of the same methods mentioned above after you specify the index name. 223 | 224 | Add a document to an index called "posts": 225 | 226 | ```php 227 | Search::index('posts')->insert(1, array( 228 | 'title' => 'My title', 229 | 'content' => 'The quick brown fox...', 230 | 'status' => 'published', 231 | )); 232 | ``` 233 | 234 | Search the "posts" index for documents who's `content` field contains the word "fox" and who's `status` is "published": 235 | 236 | ```php 237 | $results = Search::index('posts')->search('content', 'fox') 238 | ->where('status', 'published') 239 | ->get(); 240 | ``` 241 | 242 | Delete a document from the "posts" index with an `id` of "1": 243 | 244 | ```php 245 | Search::index('posts')->delete(1); 246 | ``` 247 | 248 | Delete the entire "posts" index: 249 | 250 | ```php 251 | Search::index('posts')->deleteIndex(); 252 | ``` 253 | 254 | ## Advanced Query Callbacks 255 | 256 | If you need more control over a search query you may add a callback function which will be called after all conditions have been added to the query but before the query has been executed. You can then make changes to the native query instance and return it to be executed. 257 | 258 | ```php 259 | $results = Search::index('posts')->select('id', 'created_at') 260 | ->search('content', 'fox') 261 | ->addCallback(function ($query) { 262 | // Make changes to $query... 263 | return $query; 264 | }) 265 | ->get(); 266 | ``` 267 | 268 | Since each driver has it's own native `$query` object/array, you may only want to execute your callback for one of the drivers: 269 | 270 | ```php 271 | $results = Search::index('posts')->select('id', 'created_at') 272 | ->search('content', 'fox') 273 | ->addCallback(function ($query) { 274 | // Adjust pagination for an elasticsearch query array. 275 | $query['from'] = 0; 276 | $query['size'] = 20; 277 | return $query; 278 | }, 'elasticsearch') 279 | ->get(); 280 | ``` 281 | 282 | > **Note:** You may also pass an array of drivers as the second parameter. 283 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmanos/laravel-search", 3 | "description": "A search package for Laravel 5.", 4 | "keywords": ["laravel", "search", "lucene", "zendsearch", "elasticsearch", "algolia", "full", "text"], 5 | "authors": [ 6 | { 7 | "name": "Mark Manos", 8 | "email": "mark@airpac.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.3.0", 13 | "illuminate/support": "~5.0" 14 | }, 15 | "require-dev": { 16 | "zendframework/zendsearch": "dev-master", 17 | "elasticsearch/elasticsearch": "1.0.*@dev", 18 | "algolia/algoliasearch-client-php": "1.1.*" 19 | }, 20 | "suggest": { 21 | "zendframework/zendsearch": "Required for ZendSearch driver.", 22 | "elasticsearch/elasticsearch": "Required for ElasticSearch driver.", 23 | "algolia/algoliasearch-client-php": "Required for Algolia driver." 24 | }, 25 | "autoload": { 26 | "psr-0": { 27 | "Mmanos\\Search\\": "src/" 28 | } 29 | }, 30 | "minimum-stability": "stable", 31 | "license": "MIT" 32 | } -------------------------------------------------------------------------------- /src/Mmanos/Search/Facade.php: -------------------------------------------------------------------------------- 1 | name = $name; 32 | $this->driver = $driver; 33 | } 34 | 35 | /** 36 | * Return an instance of the correct index driver for the 37 | * given index name. 38 | * 39 | * @param string $index 40 | * @param string $driver 41 | * 42 | * @return \Mmanos\Search\Index 43 | */ 44 | public static function factory($index, $driver = null) 45 | { 46 | if (null === $driver) { 47 | $driver = Config::get('search.default', 'zend'); 48 | } 49 | 50 | switch ($driver) { 51 | case 'algolia': 52 | return new Index\Algolia($index, 'algolia'); 53 | 54 | case 'elasticsearch': 55 | return new Index\Elasticsearch($index, 'elasticsearch'); 56 | 57 | case 'zend': 58 | default: 59 | return new Index\Zend($index, 'zend'); 60 | } 61 | } 62 | 63 | /** 64 | * Initialize and return a new Query instance on this index. 65 | * 66 | * @return \Mmanos\Search\Query 67 | */ 68 | public function query() 69 | { 70 | return new Query($this); 71 | } 72 | 73 | /** 74 | * Initialize and return a new Query instance on this index 75 | * with the requested where condition. 76 | * 77 | * @param string $field 78 | * @param mixed $value 79 | * 80 | * @return \Mmanos\Search\Query 81 | */ 82 | public function where($field, $value) 83 | { 84 | return $this->query()->where($field, $value); 85 | } 86 | 87 | /** 88 | * Initialize and return a new Query instance on this index 89 | * with the requested geo distance where clause. 90 | * 91 | * @param float $lat 92 | * @param float $long 93 | * @param int $distance_in_meters 94 | * 95 | * @return \Mmanos\Search\Query 96 | */ 97 | public function whereLocation($lat, $long, $distance_in_meters = 10000) 98 | { 99 | return $this->query()->whereLocation($lat, $long, $distance_in_meters); 100 | } 101 | 102 | /** 103 | * Initialize and return a new Query instance on this index 104 | * with the requested search condition. 105 | * 106 | * @param string $field 107 | * @param mixed $value 108 | * @param array $options - required : requires a match (default) 109 | * - prohibited : requires a non-match 110 | * - phrase : match the $value as a phrase 111 | * 112 | * @return \Mmanos\Search\Query 113 | */ 114 | public function search($field, $value, array $options = array()) 115 | { 116 | return $this->query()->search($field, $value, $options); 117 | } 118 | 119 | /** 120 | * Initialize and return a new Query instance on this index 121 | * with the requested select condition. 122 | * 123 | * @param array $columns 124 | * 125 | * @return \Mmanos\Search\Query 126 | */ 127 | public function select($columns = array('*')) 128 | { 129 | return $this->query()->select(is_array($columns) ? $columns : func_get_args()); 130 | } 131 | 132 | /** 133 | * Create the index. 134 | * 135 | * @param array $fields 136 | * 137 | * @return bool 138 | */ 139 | abstract public function createIndex(array $fields = array()); 140 | 141 | /** 142 | * Get a new query instance from the driver. 143 | * 144 | * @return mixed 145 | */ 146 | abstract public function newQuery(); 147 | 148 | /** 149 | * Add a search/where clause to the given query based on the given condition. 150 | * Return the given $query instance when finished. 151 | * 152 | * @param mixed $query 153 | * @param array $condition - field : name of the field 154 | * - value : value to match 155 | * - required : must match 156 | * - prohibited : must not match 157 | * - phrase : match as a phrase 158 | * - filter : filter results on value 159 | * - fuzzy : fuzziness value (0 - 1) 160 | * 161 | * @return mixed 162 | */ 163 | abstract public function addConditionToQuery($query, array $condition); 164 | 165 | /** 166 | * Execute the given query and return the results. 167 | * Return an array of records where each record is an array 168 | * containing: 169 | * - the record 'id' 170 | * - all parameters stored in the index 171 | * - an optional '_score' value 172 | * 173 | * @param mixed $query 174 | * @param array $options - limit : max # of records to return 175 | * - offset : # of records to skip 176 | * 177 | * @return array 178 | */ 179 | abstract public function runQuery($query, array $options = array()); 180 | 181 | /** 182 | * Execute the given query and return the total number of results. 183 | * 184 | * @param mixed $query 185 | * 186 | * @return int 187 | */ 188 | abstract public function runCount($query); 189 | 190 | /** 191 | * Add a new document to the index. 192 | * Any existing document with the given $id should be deleted first. 193 | * $fields should be indexed but not necessarily stored in the index. 194 | * $parameters should be stored in the index but not necessarily indexed. 195 | * 196 | * @param mixed $id 197 | * @param array $fields 198 | * @param array $parameters 199 | * 200 | * @return bool 201 | */ 202 | abstract public function insert($id, array $fields, array $parameters = array()); 203 | 204 | /** 205 | * Delete the document from the index associated with the given $id. 206 | * 207 | * @param mixed $id 208 | * 209 | * @return bool 210 | */ 211 | abstract public function delete($id); 212 | 213 | /** 214 | * Delete the entire index. 215 | * 216 | * @return bool 217 | */ 218 | abstract public function deleteIndex(); 219 | } 220 | -------------------------------------------------------------------------------- /src/Mmanos/Search/Index/Algolia.php: -------------------------------------------------------------------------------- 1 | index) { 65 | $this->index = $this->getClient()->initIndex($this->name); 66 | } 67 | 68 | return $this->index; 69 | } 70 | 71 | /** 72 | * Get a new query instance from the driver. 73 | * 74 | * @return array 75 | */ 76 | public function newQuery() 77 | { 78 | return array( 79 | 'terms' => '', 80 | 'query' => array( 81 | 'facets' => '*', 82 | ), 83 | ); 84 | } 85 | 86 | /** 87 | * Add a search/where clause to the given query based on the given condition. 88 | * Return the given $query instance when finished. 89 | * 90 | * @param array $query 91 | * @param array $condition - field : name of the field 92 | * - value : value to match 93 | * - required : must match 94 | * - prohibited : must not match 95 | * - phrase : match as a phrase 96 | * - filter : filter results on value 97 | * - fuzzy : fuzziness value (0 - 1) 98 | * 99 | * @return array 100 | */ 101 | public function addConditionToQuery($query, array $condition) 102 | { 103 | $value = trim(array_get($condition, 'value')); 104 | $field = array_get($condition, 'field'); 105 | 106 | if ('xref_id' == $field) { 107 | $field = 'objectID'; 108 | } 109 | 110 | if (array_get($condition, 'filter')) { 111 | if (is_numeric($value)) { 112 | $query['query']['numericFilters'][] = "{$field}={$value}"; 113 | } 114 | else { 115 | $query['query']['facetFilters'][] = "{$field}:{$value}"; 116 | } 117 | } 118 | else if (array_get($condition, 'lat')) { 119 | $query['query']['aroundLatLng'] = array_get($condition, 'lat') . ',' . array_get($condition, 'long'); 120 | $query['query']['aroundRadius'] = array_get($condition, 'distance'); 121 | } 122 | else { 123 | $query['terms'] .= ' ' . $value; 124 | 125 | if (!empty($field) && '*' !== $field) { 126 | $field = is_array($field) ? $field : array($field); 127 | $query['query']['restrictSearchableAttributes'] = implode(',', $field); 128 | } 129 | } 130 | 131 | return $query; 132 | } 133 | 134 | /** 135 | * Execute the given query and return the results. 136 | * Return an array of records where each record is an array 137 | * containing: 138 | * - the record 'id' 139 | * - all parameters stored in the index 140 | * - an optional '_score' value 141 | * 142 | * @param array $query 143 | * @param array $options - limit : max # of records to return 144 | * - offset : # of records to skip 145 | * 146 | * @return array 147 | */ 148 | public function runQuery($query, array $options = array()) 149 | { 150 | $original_query = $query; 151 | 152 | if (isset($options['limit']) && isset($options['offset'])) { 153 | $query['query']['page'] = ($options['offset'] / $options['limit']); 154 | $query['query']['hitsPerPage'] = $options['limit']; 155 | } 156 | 157 | $query['terms'] = trim($query['terms']); 158 | if (isset($query['query']['numericFilters'])) { 159 | $query['query']['numericFilters'] = implode(',', $query['query']['numericFilters']); 160 | } 161 | 162 | try { 163 | $response = $this->getIndex()->search(array_get($query, 'terms'), array_get($query, 'query')); 164 | $this->stored_query_totals[md5(serialize($original_query))] = array_get($response, 'nbHits'); 165 | } catch (\Exception $e) { 166 | $response = array(); 167 | } 168 | 169 | $results = array(); 170 | 171 | if (array_get($response, 'hits')) { 172 | foreach (array_get($response, 'hits') as $hit) { 173 | $hit['id'] = array_get($hit, 'objectID'); 174 | $hit['_score'] = 1; 175 | $results[] = $hit; 176 | } 177 | } 178 | 179 | return $results; 180 | } 181 | 182 | /** 183 | * Execute the given query and return the total number of results. 184 | * 185 | * @param array $query 186 | * 187 | * @return int 188 | */ 189 | public function runCount($query) 190 | { 191 | if (isset($this->stored_query_totals[md5(serialize($query))])) { 192 | return $this->stored_query_totals[md5(serialize($query))]; 193 | } 194 | 195 | $query['terms'] = trim($query['terms']); 196 | if (isset($query['numericFilters'])) { 197 | $query['numericFilters'] = implode(',', $query['numericFilters']); 198 | } 199 | if (isset($query['facets'])) { 200 | $query['facets'] = implode(',', $query['facets']); 201 | } 202 | 203 | try { 204 | return array_get($this->getIndex()->search(array_get($query, 'terms'), array_get($query, 'query')), 'nbHits'); 205 | } catch (\Exception $e) { 206 | return 0; 207 | } 208 | } 209 | 210 | /** 211 | * Add a new document to the index. 212 | * Any existing document with the given $id should be deleted first. 213 | * $fields should be indexed but not necessarily stored in the index. 214 | * $parameters should be stored in the index but not necessarily indexed. 215 | * 216 | * @param mixed $id 217 | * @param array $fields 218 | * @param array $parameters 219 | * 220 | * @return bool 221 | */ 222 | public function insert($id, array $fields, array $parameters = array()) 223 | { 224 | $fields['objectID'] = $id; 225 | 226 | $this->getIndex()->saveObject(array_merge($parameters, $fields)); 227 | 228 | return true; 229 | } 230 | 231 | /** 232 | * Delete the document from the index associated with the given $id. 233 | * 234 | * @param mixed $id 235 | * 236 | * @return bool 237 | */ 238 | public function delete($id) 239 | { 240 | try { 241 | $this->getIndex()->deleteObject($id); 242 | } catch (\Exception $e) { 243 | return false; 244 | } 245 | 246 | return true; 247 | } 248 | 249 | /** 250 | * Delete the entire index. 251 | * 252 | * @return bool 253 | */ 254 | public function deleteIndex() 255 | { 256 | try { 257 | $this->getIndex()->clearIndex(); 258 | } catch (\Exception $e) { 259 | return false; 260 | } 261 | 262 | return true; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Mmanos/Search/Index/Elasticsearch.php: -------------------------------------------------------------------------------- 1 | array('type' => 'geo_point')); 54 | 55 | foreach ($fields as $field) { 56 | $properties[$field] = array('type' => 'string'); 57 | } 58 | 59 | $body['mappings'][static::$default_type]['properties'] = $properties; 60 | 61 | $this->getClient()->indices()->create(array( 62 | 'index' => $this->name, 63 | 'body' => $body, 64 | )); 65 | 66 | return true; 67 | } 68 | 69 | /** 70 | * Get a new query instance from the driver. 71 | * 72 | * @return array 73 | */ 74 | public function newQuery() 75 | { 76 | return array( 77 | 'index' => $this->name, 78 | 'body' => array('query' => array()), 79 | ); 80 | } 81 | 82 | /** 83 | * Add a search/where clause to the given query based on the given condition. 84 | * Return the given $query instance when finished. 85 | * 86 | * @param array $query 87 | * @param array $condition - field : name of the field 88 | * - value : value to match 89 | * - required : must match 90 | * - prohibited : must not match 91 | * - phrase : match as a phrase 92 | * - filter : filter results on value 93 | * - fuzzy : fuzziness value (0 - 1) 94 | * 95 | * @return array 96 | */ 97 | public function addConditionToQuery($query, array $condition) 98 | { 99 | $value = trim(array_get($condition, 'value')); 100 | $field = array_get($condition, 'field', '_all'); 101 | 102 | if ($field && 'xref_id' == $field) { 103 | $query['id'] = $value; 104 | return $query; 105 | } 106 | 107 | if (empty($field) || '*' === $field) { 108 | $field = '_all'; 109 | } 110 | $field = (array) $field; 111 | 112 | $occur = empty($condition['required']) ? 'should' : 'must'; 113 | $occur = empty($condition['prohibited']) ? $occur : 'must_not'; 114 | 115 | if (isset($condition['fuzzy']) && false !== $condition['fuzzy']) { 116 | $fuzziness = .5; 117 | if (is_numeric($condition['fuzzy']) 118 | && $condition['fuzzy'] >= 0 119 | && $condition['fuzzy'] <= 1 120 | ) { 121 | $fuzziness = $condition['fuzzy']; 122 | } 123 | $match_type = 'multi_match'; 124 | $definition = array( 125 | 'query' => $value, 126 | 'fields' => $field, 127 | 'prefix_length' => 2, 128 | 'fuzziness' => $fuzziness, 129 | ); 130 | } 131 | elseif (array_get($condition, 'lat')) { 132 | $definition = array( 133 | 'distance' => $condition['distance'].'m', 134 | '_geoloc' => array( 135 | 'lat' => $condition['lat'], 136 | 'lon' => $condition['long'], 137 | ), 138 | ); 139 | 140 | $query['body']['query']['filtered']['filter']['geo_distance'] = $definition; 141 | 142 | return $query; 143 | } 144 | else { 145 | $is_phrase = (!empty($condition['phrase']) || !empty($condition['filter'])); 146 | $match_type = 'multi_match'; 147 | $definition = array( 148 | 'query' => $value, 149 | 'fields' => $field, 150 | 'type' => $is_phrase ? 'phrase' : 'best_fields', 151 | ); 152 | } 153 | 154 | $query['body']['query']['filtered']['query']['bool'][$occur][][$match_type] = $definition; 155 | 156 | return $query; 157 | } 158 | 159 | /** 160 | * Execute the given query and return the results. 161 | * Return an array of records where each record is an array 162 | * containing: 163 | * - the record 'id' 164 | * - all parameters stored in the index 165 | * - an optional '_score' value 166 | * 167 | * @param array $query 168 | * @param array $options - limit : max # of records to return 169 | * - offset : # of records to skip 170 | * 171 | * @return array 172 | */ 173 | public function runQuery($query, array $options = array()) 174 | { 175 | $original_query = $query; 176 | 177 | if (isset($options['columns']) && !in_array('*', $options['columns'])) { 178 | $query['_source'] = $options['columns']; 179 | } 180 | 181 | if (isset($options['limit']) && isset($options['offset'])) { 182 | $query['from'] = $options['offset']; 183 | $query['size'] = $options['limit']; 184 | } 185 | 186 | if (isset($query['id'])) { 187 | try { 188 | $response = $this->getClient()->get(array( 189 | 'index' => array_get($query, 'index'), 190 | 'type' => static::$default_type, 191 | 'id' => array_get($query, 'id'), 192 | )); 193 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 194 | $response = array(); 195 | } 196 | 197 | if (empty($response)) { 198 | $this->stored_query_totals[md5(serialize($original_query))] = 0; 199 | return array(); 200 | } 201 | 202 | $this->stored_query_totals[md5(serialize($original_query))] = 1; 203 | 204 | $parameters = array_get($response, '_source._parameters'); 205 | 206 | if (!empty($parameters)) { 207 | $parameters = json_decode(base64_decode($parameters), true); 208 | } 209 | else { 210 | $parameters = array(); 211 | } 212 | 213 | return array(array_merge( 214 | array( 215 | 'id' => array_get($response, '_id'), 216 | ), 217 | $parameters 218 | )); 219 | } 220 | 221 | try { 222 | $response = $this->getClient()->search($query); 223 | $this->stored_query_totals[md5(serialize($original_query))] = array_get($response, 'hits.total'); 224 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 225 | $response = array(); 226 | } 227 | 228 | $results = array(); 229 | 230 | if (array_get($response, 'hits.hits')) { 231 | foreach (array_get($response, 'hits.hits') as $hit) { 232 | $fields = array( 233 | 'id' => array_get($hit, '_id'), 234 | '_score' => array_get($hit, '_score'), 235 | ); 236 | $source = array_get($hit, '_source', array()); 237 | 238 | foreach ($source as $name => $value) { 239 | $fields[$name] = $value; 240 | } 241 | 242 | $parameters = array_get($hit, '_source._parameters'); 243 | 244 | if (!empty($parameters)) { 245 | $parameters = json_decode(base64_decode($parameters), true); 246 | } 247 | else { 248 | $parameters = array(); 249 | } 250 | 251 | $results[] = array_merge($fields, $parameters); 252 | } 253 | } 254 | 255 | return $results; 256 | } 257 | 258 | /** 259 | * Execute the given query and return the total number of results. 260 | * 261 | * @param array $query 262 | * 263 | * @return int 264 | */ 265 | public function runCount($query) 266 | { 267 | if (isset($this->stored_query_totals[md5(serialize($query))])) { 268 | return $this->stored_query_totals[md5(serialize($query))]; 269 | } 270 | 271 | try { 272 | return array_get($this->getClient()->search($query), 'hits.total'); 273 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 274 | return 0; 275 | } 276 | } 277 | 278 | /** 279 | * Add a new document to the index. 280 | * Any existing document with the given $id should be deleted first. 281 | * $fields should be indexed but not necessarily stored in the index. 282 | * $parameters should be stored in the index but not necessarily indexed. 283 | * 284 | * @param mixed $id 285 | * @param array $fields 286 | * @param array $parameters 287 | * 288 | * @return bool 289 | */ 290 | public function insert($id, array $fields, array $parameters = array()) 291 | { 292 | try { 293 | $this->delete($id); 294 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) {} 295 | 296 | if (!empty($parameters)) { 297 | $fields['_parameters'] = base64_encode(json_encode($parameters)); 298 | } 299 | 300 | $this->getClient()->index(array( 301 | 'index' => $this->name, 302 | 'type' => static::$default_type, 303 | 'id' => $id, 304 | 'body' => $fields, 305 | )); 306 | 307 | return true; 308 | } 309 | 310 | /** 311 | * Delete the document from the index associated with the given $id. 312 | * 313 | * @param mixed $id 314 | * 315 | * @return bool 316 | */ 317 | public function delete($id) 318 | { 319 | try { 320 | $this->getClient()->get(array( 321 | 'index' => $this->name, 322 | 'type' => static::$default_type, 323 | 'id' => $id, 324 | )); 325 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 326 | return false; 327 | } 328 | 329 | $doc = $this->getClient()->delete(array( 330 | 'index' => $this->name, 331 | 'type' => static::$default_type, 332 | 'id' => $id, 333 | )); 334 | 335 | return true; 336 | } 337 | 338 | /** 339 | * Delete the entire index. 340 | * 341 | * @return bool 342 | */ 343 | public function deleteIndex() 344 | { 345 | try { 346 | $this->getClient()->indices()->delete(array( 347 | 'index' => $this->name, 348 | )); 349 | } catch (\Elasticsearch\Common\Exceptions\Missing404Exception $e) { 350 | return false; 351 | } 352 | 353 | return true; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/Mmanos/Search/Index/Zend.php: -------------------------------------------------------------------------------- 1 | index) { 56 | $path = rtrim(Config::get('search.connections.zend.path'), '/') . '/' . $this->name; 57 | 58 | try { 59 | $this->index = \ZendSearch\Lucene\Lucene::open($path); 60 | } catch (\ZendSearch\Exception\ExceptionInterface $e) { 61 | $this->index = \ZendSearch\Lucene\Lucene::create($path); 62 | } catch (\ErrorException $e) { 63 | if (!file_exists($path)) { 64 | throw new \Exception( 65 | "'path' directory does not exist for the 'zend' search driver: '" 66 | . rtrim(Config::get('search.connections.zend.path'), '/') 67 | . "'" 68 | ); 69 | } 70 | throw $e; 71 | } 72 | 73 | \ZendSearch\Lucene\Analysis\Analyzer\Analyzer::setDefault( 74 | new \ZendSearch\Lucene\Analysis\Analyzer\Common\Utf8Num\CaseInsensitive() 75 | ); 76 | } 77 | 78 | return $this->index; 79 | } 80 | 81 | /** 82 | * Get a new query instance from the driver. 83 | * 84 | * @return \ZendSearch\Lucene\Search\Query\Boolean 85 | */ 86 | public function newQuery() 87 | { 88 | return new \ZendSearch\Lucene\Search\Query\Boolean; 89 | } 90 | 91 | /** 92 | * Add a search/where clause to the given query based on the given condition. 93 | * Return the given $query instance when finished. 94 | * 95 | * @param \ZendSearch\Lucene\Search\Query\Boolean $query 96 | * @param array $condition - field : name of the field 97 | * - value : value to match 98 | * - required : must match 99 | * - prohibited : must not match 100 | * - phrase : match as a phrase 101 | * - filter : filter results on value 102 | * - fuzzy : fuzziness value (0 - 1) 103 | * 104 | * @return \ZendSearch\Lucene\Search\Query\Boolean 105 | */ 106 | public function addConditionToQuery($query, array $condition) 107 | { 108 | if (array_get($condition, 'lat')) { 109 | return $query; 110 | } 111 | 112 | $value = trim($this->escape(array_get($condition, 'value'))); 113 | if (array_get($condition, 'phrase') || array_get($condition, 'filter')) { 114 | $value = '"' . $value . '"'; 115 | } 116 | if (isset($condition['fuzzy']) && false !== $condition['fuzzy']) { 117 | $fuzziness = ''; 118 | if (is_numeric($condition['fuzzy']) 119 | && $condition['fuzzy'] >= 0 120 | && $condition['fuzzy'] <= 1 121 | ) { 122 | $fuzziness = $condition['fuzzy']; 123 | } 124 | 125 | $words = array(); 126 | foreach (explode(' ', $value) as $word) { 127 | $words[] = $word . '~' . $fuzziness; 128 | } 129 | $value = implode(' ', $words); 130 | } 131 | 132 | $sign = null; 133 | if (!empty($condition['required'])) { 134 | $sign = true; 135 | } 136 | else if (!empty($condition['prohibited'])) { 137 | $sign = false; 138 | } 139 | 140 | $field = array_get($condition, 'field'); 141 | if (empty($field) || '*' === $field) { 142 | $field = null; 143 | } 144 | 145 | if (is_array($field)) { 146 | $values = array(); 147 | foreach ($field as $f) { 148 | $values[] = trim($f) . ':(' . $value . ')'; 149 | } 150 | $value = implode(' OR ', $values); 151 | } 152 | else if ($field) { 153 | $value = trim(array_get($condition, 'field')) . ':(' . $value . ')'; 154 | } 155 | 156 | $query->addSubquery(\ZendSearch\Lucene\Search\QueryParser::parse($value), $sign); 157 | 158 | return $query; 159 | } 160 | 161 | /** 162 | * Execute the given query and return the results. 163 | * Return an array of records where each record is an array 164 | * containing: 165 | * - the record 'id' 166 | * - all parameters stored in the index 167 | * - an optional '_score' value 168 | * 169 | * @param \ZendSearch\Lucene\Search\Query\Boolean $query 170 | * @param array $options - limit : max # of records to return 171 | * - offset : # of records to skip 172 | * 173 | * @return array 174 | */ 175 | public function runQuery($query, array $options = array()) 176 | { 177 | $response = $this->getIndex()->find($query); 178 | 179 | $this->stored_query_totals[md5(serialize($query))] = count($response); 180 | 181 | $results = array(); 182 | 183 | if (!empty($response)) { 184 | foreach ($response as $hit) { 185 | $fields = array( 186 | 'id' => $hit->xref_id, 187 | '_score' => $hit->score, 188 | ); 189 | 190 | foreach ($hit->getDocument()->getFieldNames() as $name) { 191 | if ($name == 'xref_id') continue; 192 | 193 | $fields[$name] = $hit->getDocument()->getFieldValue($name); 194 | } 195 | 196 | $results[] = array_merge( 197 | $fields, 198 | json_decode(base64_decode($hit->_parameters), true) 199 | ); 200 | } 201 | } 202 | 203 | if (isset($options['limit']) && isset($options['offset'])) { 204 | $results = array_slice($results, $options['offset'], $options['limit']); 205 | } 206 | 207 | return $results; 208 | } 209 | 210 | /** 211 | * Execute the given query and return the total number of results. 212 | * 213 | * @param \ZendSearch\Lucene\Search\Query\Boolean $query 214 | * 215 | * @return int 216 | */ 217 | public function runCount($query) 218 | { 219 | if (isset($this->stored_query_totals[md5(serialize($query))])) { 220 | return $this->stored_query_totals[md5(serialize($query))]; 221 | } 222 | 223 | return count($this->runQuery($query)); 224 | } 225 | 226 | /** 227 | * Add a new document to the index. 228 | * Any existing document with the given $id should be deleted first. 229 | * $fields should be indexed but not necessarily stored in the index. 230 | * $parameters should be stored in the index but not necessarily indexed. 231 | * 232 | * @param mixed $id 233 | * @param array $fields 234 | * @param array $parameters 235 | * 236 | * @return bool 237 | */ 238 | public function insert($id, array $fields, array $parameters = array()) 239 | { 240 | // Remove any existing documents. 241 | $this->delete($id); 242 | 243 | // Create new document. 244 | $doc = new \ZendSearch\Lucene\Document(); 245 | 246 | // Add id parameters. 247 | $doc->addField(\ZendSearch\Lucene\Document\Field::keyword('xref_id', $id)); 248 | 249 | // Add fields to document to be indexed and stored. 250 | foreach ($fields as $field => $value) { 251 | if (is_array($value)) { 252 | $value = implode(' ', $value); 253 | } 254 | 255 | $doc->addField(\ZendSearch\Lucene\Document\Field::text(trim($field), trim($value))); 256 | } 257 | 258 | // Add parameters to document to be stored (but not indexed). 259 | $doc->addField(\ZendSearch\Lucene\Document\Field::unIndexed('_parameters', base64_encode(json_encode($parameters)))); 260 | 261 | // Add document to index. 262 | $this->getIndex()->addDocument($doc); 263 | 264 | return true; 265 | } 266 | 267 | /** 268 | * Delete the document from the index associated with the given $id. 269 | * 270 | * @param mixed $id 271 | * 272 | * @return bool 273 | */ 274 | public function delete($id) 275 | { 276 | $id = trim($this->escape($id)); 277 | $hits = $this->getIndex()->find("xref_id:\"{$id}\""); 278 | if (empty($hits)) { 279 | return false; 280 | } 281 | 282 | foreach ($hits as $hit) { 283 | $this->getIndex()->delete($hit->id); 284 | } 285 | 286 | return true; 287 | } 288 | 289 | /** 290 | * Delete the entire index. 291 | * 292 | * @return bool 293 | */ 294 | public function deleteIndex() 295 | { 296 | $path = rtrim(Config::get('search.connections.zend.path'), '/') . '/' . $this->name; 297 | if (!file_exists($path) || !is_dir($path)) { 298 | return false; 299 | } 300 | 301 | $this->rmdir($path); 302 | $this->index = null; 303 | 304 | return true; 305 | } 306 | 307 | /** 308 | * Helper method to recursively remove an index directory. 309 | * 310 | * @param string $dir 311 | * 312 | * @return void 313 | */ 314 | protected function rmdir($dir) 315 | { 316 | foreach (glob($dir . '/*') as $file) { 317 | if (is_dir($file)) { 318 | $this->rmdir($file); 319 | } 320 | else { 321 | unlink($file); 322 | } 323 | } 324 | 325 | rmdir($dir); 326 | } 327 | 328 | /** 329 | * Helper method to escape all ZendSearch special characters. 330 | * 331 | * @param string $str 332 | * 333 | * @return string 334 | */ 335 | protected function escape($str) 336 | { 337 | $str = str_replace('\\', '\\\\', $str); 338 | $str = str_replace('+', '\+', $str); 339 | $str = str_replace('-', '\-', $str); 340 | $str = str_replace('&&', '\&&', $str); 341 | $str = str_replace('||', '\||', $str); 342 | $str = str_replace('!', '\!', $str); 343 | $str = str_replace('(', '\(', $str); 344 | $str = str_replace(')', '\)', $str); 345 | $str = str_replace('{', '\{', $str); 346 | $str = str_replace('}', '\}', $str); 347 | $str = str_replace('[', '\[', $str); 348 | $str = str_replace(']', '\]', $str); 349 | $str = str_replace('^', '\^', $str); 350 | $str = str_replace('"', '\"', $str); 351 | $str = str_replace('~', '\~', $str); 352 | $str = str_replace('*', '\*', $str); 353 | $str = str_replace('?', '\?', $str); 354 | $str = str_replace(':', '\:', $str); 355 | 356 | $str = str_ireplace( 357 | array(' and ', ' or ', ' not ', ' to '), 358 | '', 359 | $str 360 | ); 361 | 362 | return $str; 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/Mmanos/Search/Query.php: -------------------------------------------------------------------------------- 1 | index = $index; 76 | $this->query = $this->index->newQuery(); 77 | } 78 | 79 | /** 80 | * Add a basic where clause to the query. A where clause filter attemtps 81 | * to match the value you specify as an entire "phrase". It does not 82 | * guarantee an exact match of the entire field value. 83 | * 84 | * @param string $field 85 | * @param mixed $value 86 | * 87 | * @return \Mmanos\Search\Query 88 | */ 89 | public function where($field, $value) 90 | { 91 | $this->query = $this->index->addConditionToQuery($this->query, array( 92 | 'field' => $field, 93 | 'value' => $value, 94 | 'required' => true, 95 | 'filter' => true, 96 | )); 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Add a geo distance where clause to the query. 103 | * 104 | * @param float $lat 105 | * @param float $long 106 | * @param int $distance_in_meters 107 | * 108 | * @return \Mmanos\Search\Query 109 | */ 110 | public function whereLocation($lat, $long, $distance_in_meters = 10000) 111 | { 112 | $this->query = $this->index->addConditionToQuery($this->query, array( 113 | 'lat' => $lat, 114 | 'long' => $long, 115 | 'distance' => $distance_in_meters, 116 | )); 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Add a basic search clause to the query. 123 | * 124 | * @param string $field 125 | * @param mixed $value 126 | * @param array $options - required : requires a match (default) 127 | * - prohibited : requires a non-match 128 | * - phrase : match the $value as a phrase 129 | * - fuzzy : perform a fuzzy search (true, or numeric between 0-1) 130 | * 131 | * @return \Mmanos\Search\Query 132 | */ 133 | public function search($field, $value, array $options = array()) 134 | { 135 | $this->query = $this->index->addConditionToQuery($this->query, array( 136 | 'field' => $field, 137 | 'value' => $value, 138 | 'required' => array_get($options, 'required', true), 139 | 'prohibited' => array_get($options, 'prohibited', false), 140 | 'phrase' => array_get($options, 'phrase', false), 141 | 'fuzzy' => array_get($options, 'fuzzy', null), 142 | )); 143 | 144 | return $this; 145 | } 146 | 147 | /** 148 | * Add a custom callback fn to be called just before the query is executed. 149 | * 150 | * @param Closure $callback 151 | * @param array|string $driver 152 | * 153 | * @return \Mmanos\Search\Query 154 | */ 155 | public function addCallback($callback, $driver = null) 156 | { 157 | if (!empty($driver)) { 158 | if (is_array($driver)) { 159 | if (!in_array($this->index->driver, $driver)) { 160 | return $this; 161 | } 162 | } 163 | else if ($driver != $this->index->driver) { 164 | return $this; 165 | } 166 | } 167 | 168 | $this->callbacks[] = $callback; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Set the columns to be selected. 175 | * 176 | * @param array $columns 177 | * 178 | * @return \Mmanos\Search\Query 179 | */ 180 | public function select($columns = array('*')) 181 | { 182 | $this->columns = is_array($columns) ? $columns : func_get_args(); 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * Set the "limit" and "offset" value of the query. 189 | * 190 | * @param int $limit 191 | * @param int $offset 192 | * 193 | * @return \Mmanos\Search\Query 194 | */ 195 | public function limit($limit, $offset = 0) 196 | { 197 | $this->limit = $limit; 198 | $this->offset = $offset; 199 | 200 | return $this; 201 | } 202 | 203 | /** 204 | * Execute the current query and perform delete operations on each 205 | * document found. 206 | * 207 | * @return void 208 | */ 209 | public function delete() 210 | { 211 | $this->columns = null; 212 | $results = $this->get(); 213 | 214 | foreach ($results as $result) { 215 | $this->index->delete(array_get($result, 'id')); 216 | } 217 | } 218 | 219 | /** 220 | * Execute the current query and return a paginator for the results. 221 | * 222 | * @param int $num 223 | * 224 | * @return \Illuminate\Pagination\LengthAwarePaginator 225 | */ 226 | public function paginate($num = 15) 227 | { 228 | $page = (int) Input::get('page', 1); 229 | 230 | $this->limit($num, ($page - 1) * $num); 231 | 232 | return new LengthAwarePaginator($this->get(), $this->count(), $num, $page); 233 | } 234 | 235 | /** 236 | * Execute the current query and return the total number of results. 237 | * 238 | * @return int 239 | */ 240 | public function count() 241 | { 242 | $this->executeCallbacks(); 243 | 244 | return $this->index->runCount($this->query); 245 | } 246 | 247 | /** 248 | * Execute the current query and return the results. 249 | * 250 | * @return array 251 | */ 252 | public function get() 253 | { 254 | $options = array(); 255 | 256 | if ($this->columns) { 257 | $options['columns'] = $this->columns; 258 | } 259 | 260 | if ($this->limit) { 261 | $options['limit'] = $this->limit; 262 | $options['offset'] = $this->offset; 263 | } 264 | 265 | $this->executeCallbacks(); 266 | 267 | $results = $this->index->runQuery($this->query, $options); 268 | 269 | if ($this->columns && !in_array('*', $this->columns)) { 270 | $new_results = array(); 271 | foreach ($results as $result) { 272 | $new_result = array(); 273 | foreach ($this->columns as $field) { 274 | if (array_key_exists($field, $result)) { 275 | $new_result[$field] = $result[$field]; 276 | } 277 | } 278 | $new_results[] = $new_result; 279 | } 280 | $results = $new_results; 281 | } 282 | 283 | return $results; 284 | } 285 | 286 | /** 287 | * Execute any callback functions. Only execute once. 288 | * 289 | * @return void 290 | */ 291 | protected function executeCallbacks() 292 | { 293 | if ($this->callbacks_executed) { 294 | return; 295 | } 296 | 297 | $this->callbacks_executed = true; 298 | 299 | foreach ($this->callbacks as $callback) { 300 | if ($q = call_user_func($callback, $this->query)) { 301 | $this->query = $q; 302 | } 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/Mmanos/Search/Search.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 35 | } 36 | 37 | /** 38 | * Return the instance associated with the requested index name. 39 | * Will create one if needed. 40 | * 41 | * @param string $index 42 | * 43 | * @return \Mmanos\Search\Index 44 | */ 45 | public function index($index = null) 46 | { 47 | if (null === $index) { 48 | $index = Config::get('search.default_index', 'default'); 49 | } 50 | 51 | if (!isset($this->indexes[$index])) { 52 | $this->indexes[$index] = Index::factory($index, $this->driver); 53 | } 54 | 55 | return $this->indexes[$index]; 56 | } 57 | 58 | /** 59 | * Provide convenient access to methods on the "default_index". 60 | * 61 | * @param string $method 62 | * @param array $parameters 63 | * 64 | * @return mixed 65 | */ 66 | public function __call($method, array $parameters) 67 | { 68 | return call_user_func_array(array($this->index(), $method), $parameters); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Mmanos/Search/SearchServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 15 | __DIR__.'/../../config/search.php' => config_path('search.php') 16 | ]); 17 | } 18 | 19 | /** 20 | * Register the service provider. 21 | * 22 | * @return void 23 | */ 24 | public function register() 25 | { 26 | $this->app->bind('search', function ($app) { 27 | return new \Mmanos\Search\Search(); 28 | }); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmanos/laravel-search/0a6e8a80f3b93fd82aa619ec58c978e40ed9ff10/src/config/.gitkeep -------------------------------------------------------------------------------- /src/config/search.php: -------------------------------------------------------------------------------- 1 | 'zend', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Default Index 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Specify the default index to use when an index is not specified. 26 | | 27 | */ 28 | 29 | 'default_index' => 'default', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Search Connections 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may configure the connection information for each server that 37 | | is used by your application. A default configuration has been added 38 | | for each back-end shipped with this package. You are free to add more. 39 | | 40 | */ 41 | 42 | 'connections' => array( 43 | 44 | 'zend' => array( 45 | 'driver' => 'zend', 46 | 'path' => storage_path().'/search', 47 | ), 48 | 49 | 'elasticsearch' => array( 50 | 'driver' => 'elasticsearch', 51 | 'config' => array( 52 | 'hosts' => array('localhost:9200'), 53 | ), 54 | ), 55 | 56 | 'algolia' => array( 57 | 'driver' => 'algolia', 58 | 'config' => array( 59 | 'application_id' => '', 60 | 'admin_api_key' => '', 61 | ), 62 | ), 63 | 64 | ), 65 | 66 | ); 67 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmanos/laravel-search/0a6e8a80f3b93fd82aa619ec58c978e40ed9ff10/tests/.gitkeep --------------------------------------------------------------------------------