yc-algoliasearch-rails 2.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG.MD +566 -0
  5. data/Gemfile +38 -0
  6. data/Gemfile.lock +213 -0
  7. data/LICENSE +21 -0
  8. data/README.md +1171 -0
  9. data/Rakefile +17 -0
  10. data/algoliasearch-rails.gemspec +95 -0
  11. data/lib/algoliasearch/algolia_job.rb +9 -0
  12. data/lib/algoliasearch/configuration.rb +30 -0
  13. data/lib/algoliasearch/pagination/kaminari.rb +40 -0
  14. data/lib/algoliasearch/pagination/will_paginate.rb +15 -0
  15. data/lib/algoliasearch/pagination.rb +19 -0
  16. data/lib/algoliasearch/railtie.rb +11 -0
  17. data/lib/algoliasearch/tasks/algoliasearch.rake +19 -0
  18. data/lib/algoliasearch/utilities.rb +48 -0
  19. data/lib/algoliasearch/version.rb +3 -0
  20. data/lib/algoliasearch-rails.rb +1083 -0
  21. data/spec/spec_helper.rb +52 -0
  22. data/spec/utilities_spec.rb +30 -0
  23. data/vendor/assets/javascripts/algolia/algoliasearch.angular.js +2678 -0
  24. data/vendor/assets/javascripts/algolia/algoliasearch.angular.min.js +7 -0
  25. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.js +2678 -0
  26. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.min.js +7 -0
  27. data/vendor/assets/javascripts/algolia/algoliasearch.js +2663 -0
  28. data/vendor/assets/javascripts/algolia/algoliasearch.min.js +7 -0
  29. data/vendor/assets/javascripts/algolia/bloodhound.js +727 -0
  30. data/vendor/assets/javascripts/algolia/bloodhound.min.js +7 -0
  31. data/vendor/assets/javascripts/algolia/typeahead.bundle.js +1782 -0
  32. data/vendor/assets/javascripts/algolia/typeahead.bundle.min.js +7 -0
  33. data/vendor/assets/javascripts/algolia/typeahead.jquery.js +1184 -0
  34. data/vendor/assets/javascripts/algolia/typeahead.jquery.min.js +7 -0
  35. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.js +2678 -0
  36. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.min.js +7 -0
  37. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.js +2678 -0
  38. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.min.js +7 -0
  39. data/vendor/assets/javascripts/algolia/v2/algoliasearch.js +2663 -0
  40. data/vendor/assets/javascripts/algolia/v2/algoliasearch.min.js +7 -0
  41. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.js +6277 -0
  42. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.min.js +3 -0
  43. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.js +6223 -0
  44. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.min.js +3 -0
  45. data/vendor/assets/javascripts/algolia/v3/algoliasearch.js +6070 -0
  46. data/vendor/assets/javascripts/algolia/v3/algoliasearch.min.js +3 -0
  47. metadata +174 -0
data/README.md ADDED
@@ -0,0 +1,1171 @@
1
+ <p align="center">
2
+ <a href="https://www.algolia.com">
3
+ <img alt="Algolia for Rails" src="https://raw.githubusercontent.com/algolia/algoliasearch-client-common/master/banners/rails.png"/>
4
+ </a>
5
+ </p>
6
+
7
+ <h4 align="center">The perfect starting point to integrate <a href="https://algolia.com" target="_blank">Algolia</a> within your Rails project</h4>
8
+
9
+ <p align="center">
10
+ <a href="https://circleci.com/gh/algolia/algoliasearch-rails"><img src="https://circleci.com/gh/algolia/algoliasearch-rails.svg?style=shield" alt="CircleCI" /></a>
11
+ <a href="http://badge.fury.io/rb/algoliasearch-rails"><img src="https://badge.fury.io/rb/algoliasearch-rails.svg" alt="Gem Version"/></a>
12
+ <a href="https://codeclimate.com/github/algolia/algoliasearch-rails"><img src="https://codeclimate.com/github/algolia/algoliasearch-rails.svg" alt="Code Climate"/></a>
13
+ <img src="https://img.shields.io/badge/ActiveRecord-yes-blue.svg?style=flat-square" alt="ActiveRecord"/>
14
+ <img src="https://img.shields.io/badge/Mongoid-yes-blue.svg?style=flat-square" alt="Mongoid"/>
15
+ <img src="https://img.shields.io/badge/Sequel-yes-blue.svg?style=flat-square" alt="Sequel"/>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="https://www.algolia.com/doc/framework-integration/rails/getting-started/setup/?language=ruby" target="_blank">Documentation</a> •
20
+ <a href="https://discourse.algolia.com" target="_blank">Community Forum</a> •
21
+ <a href="http://stackoverflow.com/questions/tagged/algolia" target="_blank">Stack Overflow</a> •
22
+ <a href="https://github.com/algolia/algoliasearch-rails/issues" target="_blank">Report a bug</a> •
23
+ <a href="https://www.algolia.com/doc/framework-integration/rails/troubleshooting/faq/" target="_blank">FAQ</a> •
24
+ <a href="https://www.algolia.com/support" target="_blank">Support</a>
25
+ </p>
26
+
27
+
28
+ This gem let you easily integrate the Algolia Search API to your favorite ORM. It's based on the [algoliasearch-client-ruby](https://github.com/algolia/algoliasearch-client-ruby) gem.
29
+ Rails 5.x and 6.x are supported.
30
+
31
+ You might be interested in the sample Ruby on Rails application providing a `autocomplete.js`-based auto-completion and `InstantSearch.js`-based instant search results page: [algoliasearch-rails-example](https://github.com/algolia/algoliasearch-rails-example/).
32
+
33
+
34
+
35
+ ## API Documentation
36
+
37
+ You can find the full reference on [Algolia's website](https://www.algolia.com/doc/framework-integration/rails/).
38
+
39
+
40
+
41
+ 1. **[Setup](#setup)**
42
+ * [Install](#install)
43
+ * [Configuration](#configuration)
44
+ * [Timeouts](#timeouts)
45
+ * [Notes](#notes)
46
+
47
+ 1. **[Usage](#usage)**
48
+ * [Index Schema](#index-schema)
49
+ * [Relevancy](#relevancy)
50
+ * [Indexing](#indexing)
51
+ * [Frontend Search (realtime experience)](#frontend-search-realtime-experience)
52
+ * [Backend Search](#backend-search)
53
+ * [Backend Pagination](#backend-pagination)
54
+ * [Tags](#tags)
55
+ * [Faceting](#faceting)
56
+ * [Faceted search](#faceted-search)
57
+ * [Group by](#group-by)
58
+ * [Geo-Search](#geo-search)
59
+
60
+ 1. **[Options](#options)**
61
+ * [Auto-indexing &amp; asynchronism](#auto-indexing--asynchronism)
62
+ * [Custom index name](#custom-index-name)
63
+ * [Per-environment indices](#per-environment-indices)
64
+ * [Custom attribute definition](#custom-attribute-definition)
65
+ * [Nested objects/relations](#nested-objectsrelations)
66
+ * [Custom <code>objectID</code>](#custom-objectid)
67
+ * [Restrict indexing to a subset of your data](#restrict-indexing-to-a-subset-of-your-data)
68
+ * [Sanitizer](#sanitizer)
69
+ * [UTF-8 Encoding](#utf-8-encoding)
70
+ * [Exceptions](#exceptions)
71
+ * [Configuration example](#configuration-example)
72
+
73
+ 1. **[Indices](#indices)**
74
+ * [Manual indexing](#manual-indexing)
75
+ * [Manual removal](#manual-removal)
76
+ * [Reindexing](#reindexing)
77
+ * [Clearing an index](#clearing-an-index)
78
+ * [Using the underlying index](#using-the-underlying-index)
79
+ * [Primary/replica](#primaryreplica)
80
+ * [Share a single index](#share-a-single-index)
81
+ * [Target multiple indices](#target-multiple-indices)
82
+
83
+ 1. **[Testing](#testing)**
84
+ * [Notes](#notes)
85
+
86
+ 1. **[Troubleshooting](#troubleshooting)**
87
+ * [Frequently asked questions](#frequently-asked-questions)
88
+
89
+
90
+
91
+ # Setup
92
+
93
+
94
+
95
+ ## Install
96
+
97
+ ```sh
98
+ gem install algoliasearch-rails
99
+ ```
100
+
101
+ Add the gem to your <code>Gemfile</code>:
102
+
103
+ ```ruby
104
+ gem "algoliasearch-rails"
105
+ ```
106
+
107
+ And run:
108
+
109
+ ```sh
110
+ bundle install
111
+ ```
112
+
113
+ ## Configuration
114
+
115
+ Create a new file <code>config/initializers/algoliasearch.rb</code> to setup your <code>APPLICATION_ID</code> and <code>API_KEY</code>.
116
+
117
+ ```ruby
118
+ AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey' }
119
+ ```
120
+
121
+ The gem is compatible with [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord), [Mongoid](https://github.com/mongoid/mongoid) and [Sequel](https://github.com/jeremyevans/sequel).
122
+
123
+ ## Timeouts
124
+
125
+ You can configure a various timeout thresholds by setting the following options at initialization time:
126
+
127
+ ```ruby
128
+ AlgoliaSearch.configuration = {
129
+ application_id: 'YourApplicationID',
130
+ api_key: 'YourAPIKey',
131
+ connect_timeout: 2,
132
+ receive_timeout: 30,
133
+ send_timeout: 30,
134
+ batch_timeout: 120,
135
+ search_timeout: 5
136
+ }
137
+ ```
138
+
139
+ ## Notes
140
+
141
+ This gem makes extensive use of Rails' callbacks to trigger the indexing tasks. If you're using methods bypassing `after_validation`, `before_save` or `after_commit` callbacks, it will not index your changes. For example: `update_attribute` doesn't perform validations checks, to perform validations when updating use `update_attributes`.
142
+
143
+ All methods injected by the `AlgoliaSearch` module are prefixed by `algolia_` and aliased to the associated short names if they aren't already defined.
144
+
145
+ ```ruby
146
+ Contact.algolia_reindex! # <=> Contact.reindex!
147
+
148
+ Contact.algolia_search("jon doe") # <=> Contact.search("jon doe")
149
+ ```
150
+
151
+
152
+
153
+ # Usage
154
+
155
+
156
+
157
+ ## Index Schema
158
+
159
+ The following code will create a <code>Contact</code> index and add search capabilities to your <code>Contact</code> model:
160
+
161
+ ```ruby
162
+ class Contact < ActiveRecord::Base
163
+ include AlgoliaSearch
164
+
165
+ algoliasearch do
166
+ attributes :first_name, :last_name, :email
167
+ end
168
+ end
169
+ ```
170
+
171
+ You can either specify the attributes to send (here we restricted to <code>:first_name, :last_name, :email</code>) or not (in that case, all attributes are sent).
172
+
173
+ ```ruby
174
+ class Product < ActiveRecord::Base
175
+ include AlgoliaSearch
176
+
177
+ algoliasearch do
178
+ # all attributes will be sent
179
+ end
180
+ end
181
+ ```
182
+
183
+ You can also use the <code>add_attribute</code> method, to send all model attributes + extra ones:
184
+
185
+ ```ruby
186
+ class Product < ActiveRecord::Base
187
+ include AlgoliaSearch
188
+
189
+ algoliasearch do
190
+ # all attributes + extra_attr will be sent
191
+ add_attribute :extra_attr
192
+ end
193
+
194
+ def extra_attr
195
+ "extra_val"
196
+ end
197
+ end
198
+ ```
199
+
200
+ ## Relevancy
201
+
202
+ We provide many ways to configure your index allowing you to tune your overall index relevancy. The most important ones are the **searchable attributes** and the attributes reflecting **record popularity**.
203
+
204
+ ```ruby
205
+ class Product < ActiveRecord::Base
206
+ include AlgoliaSearch
207
+
208
+ algoliasearch do
209
+ # list of attribute used to build an Algolia record
210
+ attributes :title, :subtitle, :description, :likes_count, :seller_name
211
+
212
+ # the `searchableAttributes` (formerly known as attributesToIndex) setting defines the attributes
213
+ # you want to search in: here `title`, `subtitle` & `description`.
214
+ # You need to list them by order of importance. `description` is tagged as
215
+ # `unordered` to avoid taking the position of a match into account in that attribute.
216
+ searchableAttributes ['title', 'subtitle', 'unordered(description)']
217
+
218
+ # the `customRanking` setting defines the ranking criteria use to compare two matching
219
+ # records in case their text-relevance is equal. It should reflect your record popularity.
220
+ customRanking ['desc(likes_count)']
221
+ end
222
+
223
+ end
224
+ ```
225
+
226
+ ## Indexing
227
+
228
+ To index a model, simple call `reindex` on the class:
229
+
230
+ ```ruby
231
+ Product.reindex
232
+ ```
233
+
234
+ To index all of your models, you can do something like this:
235
+
236
+ ```ruby
237
+ Rails.application.eager_load! # Ensure all models are loaded (required in development).
238
+
239
+ algolia_models = ActiveRecord::Base.descendants.select{ |model| model.respond_to?(:reindex) }
240
+
241
+ algolia_models.each(&:reindex)
242
+ ```
243
+
244
+ ## Frontend Search (realtime experience)
245
+
246
+ Traditional search implementations tend to have search logic and functionality on the backend. This made sense when the search experience consisted of a user entering a search query, executing that search, and then being redirected to a search result page.
247
+
248
+ Implementing search on the backend is no longer necessary. In fact, in most cases it is harmful to performance because of added network and processing latency. We highly recommend the usage of our [JavaScript API Client](https://github.com/algolia/algoliasearch-client-javascript) issuing all search requests directly from the end user's browser, mobile device, or client. It will reduce the overall search latency while offloading your servers at the same time.
249
+
250
+ The JS API client is part of the gem, just require `algolia/v3/algoliasearch.min` somewhere in your JavaScript manifest, for example in `application.js` if you are using Rails 3.1+:
251
+
252
+ ```javascript
253
+ //= require algolia/v3/algoliasearch.min
254
+ ```
255
+
256
+ Then in your JavaScript code you can do:
257
+
258
+ ```js
259
+ var client = algoliasearch(ApplicationID, Search-Only-API-Key);
260
+ var index = client.initIndex('YourIndexName');
261
+ index.search('something', { hitsPerPage: 10, page: 0 })
262
+ .then(function searchDone(content) {
263
+ console.log(content)
264
+ })
265
+ .catch(function searchFailure(err) {
266
+ console.error(err);
267
+ });
268
+ ```
269
+
270
+ **We recently (March 2015) released a new version (V3) of our JavaScript client, if you were using our previous version (V2), [read the migration guide](https://github.com/algolia/algoliasearch-client-javascript/wiki/Migration-guide-from-2.x.x-to-3.x.x)**
271
+
272
+ ## Backend Search
273
+
274
+ ***Notes:*** We recommend the usage of our [JavaScript API Client](https://github.com/algolia/algoliasearch-client-javascript) to perform queries directly from the end-user browser without going through your server.
275
+
276
+ A search returns ORM-compliant objects reloading them from your database. We recommend the usage of our [JavaScript API Client](https://github.com/algolia/algoliasearch-client-javascript) to perform queries to decrease the overall latency and offload your servers.
277
+
278
+ ```ruby
279
+ hits = Contact.search("jon doe")
280
+ p hits
281
+ p hits.raw_answer # to get the original JSON raw answer
282
+ ```
283
+
284
+ A `highlight_result` attribute is added to each ORM object:
285
+
286
+ ```ruby
287
+ hits[0].highlight_result['first_name']['value']
288
+ ```
289
+
290
+ If you want to retrieve the raw JSON answer from the API, without re-loading the objects from the database, you can use:
291
+
292
+ ```ruby
293
+ json_answer = Contact.raw_search("jon doe")
294
+ p json_answer
295
+ p json_answer['hits']
296
+ p json_answer['facets']
297
+ ```
298
+
299
+ Search parameters can be specified either through the index's [settings](https://github.com/algolia/algoliasearch-client-ruby#index-settings-parameters) statically in your model or dynamically at search time specifying [search parameters](https://github.com/algolia/algoliasearch-client-ruby#search) as second argument of the `search` method:
300
+
301
+ ```ruby
302
+ class Contact < ActiveRecord::Base
303
+ include AlgoliaSearch
304
+
305
+ algoliasearch do
306
+ attribute :first_name, :last_name, :email
307
+
308
+ # default search parameters stored in the index settings
309
+ minWordSizefor1Typo 4
310
+ minWordSizefor2Typos 8
311
+ hitsPerPage 42
312
+ end
313
+ end
314
+ ```
315
+
316
+ ```ruby
317
+ # dynamical search parameters
318
+ p Contact.raw_search('jon doe', { hitsPerPage: 5, page: 2 })
319
+ ```
320
+
321
+ ## Backend Pagination
322
+
323
+ Even if we **highly recommend to perform all search (and therefore pagination) operations from your frontend using JavaScript**, we support both [will_paginate](https://github.com/mislav/will_paginate) and [kaminari](https://github.com/amatsuda/kaminari) as pagination backend.
324
+
325
+ To use <code>:will_paginate</code>, specify the <code>:pagination_backend</code> as follow:
326
+
327
+ ```ruby
328
+ AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey', pagination_backend: :will_paginate }
329
+ ```
330
+
331
+ Then, as soon as you use the `search` method, the returning results will be a paginated set:
332
+
333
+ ```ruby
334
+ # in your controller
335
+ @results = MyModel.search('foo', hitsPerPage: 10)
336
+
337
+ # in your views
338
+ # if using will_paginate
339
+ <%= will_paginate @results %>
340
+
341
+ # if using kaminari
342
+ <%= paginate @results %>
343
+ ```
344
+
345
+ ## Tags
346
+
347
+ Use the <code>tags</code> method to add tags to your record:
348
+
349
+ ```ruby
350
+ class Contact < ActiveRecord::Base
351
+ include AlgoliaSearch
352
+
353
+ algoliasearch do
354
+ tags ['trusted']
355
+ end
356
+ end
357
+ ```
358
+
359
+ or using dynamical values:
360
+
361
+ ```ruby
362
+ class Contact < ActiveRecord::Base
363
+ include AlgoliaSearch
364
+
365
+ algoliasearch do
366
+ tags do
367
+ [first_name.blank? || last_name.blank? ? 'partial' : 'full', has_valid_email? ? 'valid_email' : 'invalid_email']
368
+ end
369
+ end
370
+ end
371
+ ```
372
+
373
+ At query time, specify <code>{ tagFilters: 'tagvalue' }</code> or <code>{ tagFilters: ['tagvalue1', 'tagvalue2'] }</code> as search parameters to restrict the result set to specific tags.
374
+
375
+ ## Faceting
376
+
377
+ Facets can be retrieved calling the extra `facets` method of the search answer.
378
+
379
+ ```ruby
380
+ class Contact < ActiveRecord::Base
381
+ include AlgoliaSearch
382
+
383
+ algoliasearch do
384
+ # [...]
385
+
386
+ # specify the list of attributes available for faceting
387
+ attributesForFaceting [:company, :zip_code]
388
+ end
389
+ end
390
+ ```
391
+
392
+ ```ruby
393
+ hits = Contact.search('jon doe', { facets: '*' })
394
+ p hits # ORM-compliant array of objects
395
+ p hits.facets # extra method added to retrieve facets
396
+ p hits.facets['company'] # facet values+count of facet 'company'
397
+ p hits.facets['zip_code'] # facet values+count of facet 'zip_code'
398
+ ```
399
+
400
+ ```ruby
401
+ raw_json = Contact.raw_search('jon doe', { facets: '*' })
402
+ p raw_json['facets']
403
+ ```
404
+
405
+ ## Faceted search
406
+
407
+ You can also search for facet values.
408
+
409
+ ```ruby
410
+ Product.search_for_facet_values('category', 'Headphones') # Array of {value, highlighted, count}
411
+ ```
412
+
413
+ This method can also take any parameter a query can take.
414
+ This will adjust the search to only hits which would have matched the query.
415
+
416
+ ```ruby
417
+ # Only sends back the categories containing red Apple products (and only counts those)
418
+ Product.search_for_facet_values('category', 'phone', {
419
+ query: 'red',
420
+ filters: 'brand:Apple'
421
+ }) # Array of phone categories linked to red Apple products
422
+ ```
423
+
424
+ ## Group by
425
+
426
+ More info on distinct for grouping can be found
427
+ [here](https://www.algolia.com/doc/guides/managing-results/refine-results/grouping/).
428
+
429
+ ```ruby
430
+ class Contact < ActiveRecord::Base
431
+ include AlgoliaSearch
432
+
433
+ algoliasearch do
434
+ # [...]
435
+
436
+ # specify the attribute to be used for distinguishing the records
437
+ # in this case the records will be grouped by company
438
+ attributeForDistinct "company"
439
+ end
440
+ end
441
+ ```
442
+
443
+ ## Geo-Search
444
+
445
+ Use the <code>geoloc</code> method to localize your record:
446
+
447
+ ```ruby
448
+ class Contact < ActiveRecord::Base
449
+ include AlgoliaSearch
450
+
451
+ algoliasearch do
452
+ geoloc :lat_attr, :lng_attr
453
+ end
454
+ end
455
+ ```
456
+
457
+ At query time, specify <code>{ aroundLatLng: "37.33, -121.89", aroundRadius: 50000 }</code> as search parameters to restrict the result set to 50KM around San Jose.
458
+
459
+
460
+
461
+ # Options
462
+
463
+
464
+
465
+ ## Auto-indexing & asynchronism
466
+
467
+ Each time a record is saved, it will be *asynchronously* indexed. On the other hand, each time a record is destroyed, it will be - asynchronously - removed from the index. That means that a network call with the ADD/DELETE operation is sent **synchronously** to the Algolia API but then the engine will **asynchronously** process the operation (so if you do a search just after, the results may not reflect it yet).
468
+
469
+ You can disable auto-indexing and auto-removing setting the following options:
470
+
471
+ ```ruby
472
+ class Contact < ActiveRecord::Base
473
+ include AlgoliaSearch
474
+
475
+ algoliasearch auto_index: false, auto_remove: false do
476
+ attribute :first_name, :last_name, :email
477
+ end
478
+ end
479
+ ```
480
+
481
+ ### Temporary disable auto-indexing
482
+
483
+ You can temporary disable auto-indexing using the <code>without_auto_index</code> scope. This is often used for performance reason.
484
+
485
+ ```ruby
486
+ Contact.delete_all
487
+ Contact.without_auto_index do
488
+ 1.upto(10000) { Contact.create! attributes } # inside this block, auto indexing task will not run.
489
+ end
490
+ Contact.reindex! # will use batch operations
491
+ ```
492
+
493
+ ### Queues & background jobs
494
+
495
+ You can configure the auto-indexing & auto-removal process to use a queue to perform those operations in background. ActiveJob (Rails >=4.2) queues are used by default but you can define your own queuing mechanism:
496
+
497
+ ```ruby
498
+ class Contact < ActiveRecord::Base
499
+ include AlgoliaSearch
500
+
501
+ algoliasearch enqueue: true do # ActiveJob will be triggered using a `algoliasearch` queue
502
+ attribute :first_name, :last_name, :email
503
+ end
504
+ end
505
+ ```
506
+
507
+ ### Things to Consider
508
+
509
+ If you are performing updates & deletions in the background then a record deletion can be committed to your database prior
510
+ to the job actually executing. Thus if you were to load the record to remove it from the database than your ActiveRecord#find will fail with a RecordNotFound.
511
+
512
+ In this case you can bypass loading the record from ActiveRecord and just communicate with the index directly:
513
+
514
+ ```ruby
515
+ class MySidekiqWorker
516
+ def perform(id, remove)
517
+ if remove
518
+ # the record has likely already been removed from your database so we cannot
519
+ # use ActiveRecord#find to load it
520
+ index = Algolia::Index.new("index_name")
521
+ index.delete_object(id)
522
+ else
523
+ # the record should be present
524
+ c = Contact.find(id)
525
+ c.index!
526
+ end
527
+ end
528
+ end
529
+ ```
530
+
531
+ ### With Sidekiq
532
+
533
+ If you're using [Sidekiq](https://github.com/mperham/sidekiq):
534
+
535
+ ```ruby
536
+ class Contact < ActiveRecord::Base
537
+ include AlgoliaSearch
538
+
539
+ algoliasearch enqueue: :trigger_sidekiq_worker do
540
+ attribute :first_name, :last_name, :email
541
+ end
542
+
543
+ def self.trigger_sidekiq_worker(record, remove)
544
+ MySidekiqWorker.perform_async(record.id, remove)
545
+ end
546
+ end
547
+
548
+ class MySidekiqWorker
549
+ def perform(id, remove)
550
+ if remove
551
+ # the record has likely already been removed from your database so we cannot
552
+ # use ActiveRecord#find to load it
553
+ index = Algolia::Index.new("index_name")
554
+ index.delete_object(id)
555
+ else
556
+ # the record should be present
557
+ c = Contact.find(id)
558
+ c.index!
559
+ end
560
+ end
561
+ end
562
+ ```
563
+
564
+ ### With DelayedJob
565
+
566
+ If you're using [delayed_job](https://github.com/collectiveidea/delayed_job):
567
+
568
+ ```ruby
569
+ class Contact < ActiveRecord::Base
570
+ include AlgoliaSearch
571
+
572
+ algoliasearch enqueue: :trigger_delayed_job do
573
+ attribute :first_name, :last_name, :email
574
+ end
575
+
576
+ def self.trigger_delayed_job(record, remove)
577
+ if remove
578
+ record.delay.remove_from_index!
579
+ else
580
+ record.delay.index!
581
+ end
582
+ end
583
+ end
584
+
585
+ ```
586
+
587
+ ### Synchronism & testing
588
+
589
+ You can force indexing and removing to be synchronous (in that case the gem will call the `wait_task` method to ensure the operation has been taken into account once the method returns) by setting the following option: (this is **NOT** recommended, except for testing purpose)
590
+
591
+ ```ruby
592
+ class Contact < ActiveRecord::Base
593
+ include AlgoliaSearch
594
+
595
+ algoliasearch synchronous: true do
596
+ attribute :first_name, :last_name, :email
597
+ end
598
+ end
599
+ ```
600
+
601
+ ## Custom index name
602
+
603
+ By default, the index name will be the class name, e.g. "Contact". You can customize the index name by using the `index_name` option:
604
+
605
+ ```ruby
606
+ class Contact < ActiveRecord::Base
607
+ include AlgoliaSearch
608
+
609
+ algoliasearch index_name: "MyCustomName" do
610
+ attribute :first_name, :last_name, :email
611
+ end
612
+ end
613
+ ```
614
+
615
+ ## Per-environment indices
616
+
617
+ You can suffix the index name with the current Rails environment using the following option:
618
+
619
+ ```ruby
620
+ class Contact < ActiveRecord::Base
621
+ include AlgoliaSearch
622
+
623
+ algoliasearch per_environment: true do # index name will be "Contact_#{Rails.env}"
624
+ attribute :first_name, :last_name, :email
625
+ end
626
+ end
627
+ ```
628
+
629
+ ## Custom attribute definition
630
+
631
+ You can use a block to specify a complex attribute value
632
+
633
+ ```ruby
634
+ class Contact < ActiveRecord::Base
635
+ include AlgoliaSearch
636
+
637
+ algoliasearch do
638
+ attribute :email
639
+ attribute :full_name do
640
+ "#{first_name} #{last_name}"
641
+ end
642
+ add_attribute :full_name2
643
+ end
644
+
645
+ def full_name2
646
+ "#{first_name} #{last_name}"
647
+ end
648
+ end
649
+ ```
650
+
651
+ ***Notes:*** As soon as you use such code to define extra attributes, the gem is not anymore able to detect if the attribute has changed (the code uses Rails's `#{attribute}_changed?` method to detect that). As a consequence, your record will be pushed to the API even if its attributes didn't change. You can work-around this behavior creating a `_changed?` method:
652
+
653
+ ```ruby
654
+ class Contact < ActiveRecord::Base
655
+ include AlgoliaSearch
656
+
657
+ algoliasearch do
658
+ attribute :email
659
+ attribute :full_name do
660
+ "#{first_name} #{last_name}"
661
+ end
662
+ end
663
+
664
+ def full_name_changed?
665
+ first_name_changed? || last_name_changed?
666
+ end
667
+ end
668
+ ```
669
+
670
+ ## Nested objects/relations
671
+
672
+ ### Defining the relationship
673
+
674
+ You can easily embed nested objects defining an extra attribute returning any JSON-compliant object (an array or a hash or a combination of both).
675
+
676
+ ```ruby
677
+ class Profile < ActiveRecord::Base
678
+ include AlgoliaSearch
679
+
680
+ belongs_to :user
681
+ has_many :specializations
682
+
683
+ algoliasearch do
684
+ attribute :user do
685
+ # restrict the nested "user" object to its `name` + `email`
686
+ { name: user.name, email: user.email }
687
+ end
688
+ attribute :public_specializations do
689
+ # build an array of public specialization (include only `title` and `another_attr`)
690
+ specializations.select { |s| s.public? }.map do |s|
691
+ { title: s.title, another_attr: s.another_attr }
692
+ end
693
+ end
694
+ end
695
+
696
+ end
697
+ ```
698
+
699
+ ### Propagating the change from a nested child
700
+
701
+ #### With ActiveRecord
702
+
703
+ With ActiveRecord, we'll be using `touch` and `after_touch` to achieve this.
704
+
705
+ ```ruby
706
+ # app/models/app.rb
707
+ class App < ApplicationRecord
708
+ include AlgoliaSearch
709
+
710
+ belongs_to :author, class_name: :User
711
+ after_touch :index!
712
+
713
+ algoliasearch do
714
+ attribute :title
715
+ attribute :author do
716
+ author.as_json
717
+ end
718
+ end
719
+ end
720
+
721
+ # app/models/user.rb
722
+ class User < ApplicationRecord
723
+ # If your association uses belongs_to
724
+ # - use `touch: true`
725
+ # - do not define an `after_save` hook
726
+ has_many :apps, foreign_key: :author_id
727
+
728
+ after_save { apps.each(&:touch) }
729
+ end
730
+ ```
731
+
732
+ #### With Sequel
733
+
734
+ With Sequel, you can use the `touch` plugin to propagate the changes:
735
+
736
+ ```ruby
737
+ # app/models/app.rb
738
+ class App < Sequel::Model
739
+ include AlgoliaSearch
740
+
741
+ many_to_one :author, class: :User
742
+
743
+ plugin :timestamps
744
+ plugin :touch
745
+
746
+ algoliasearch do
747
+ attribute :title
748
+ attribute :author do
749
+ author.to_hash
750
+ end
751
+ end
752
+ end
753
+
754
+ # app/models/user.rb
755
+ class User < Sequel::Model
756
+ one_to_many :apps, key: :author_id
757
+
758
+ plugin :timestamps
759
+ # Can't use the associations since it won't trigger the after_save
760
+ plugin :touch
761
+
762
+ # Define the associations that need to be touched here
763
+ # Less performant, but allows for the after_save hook to trigger
764
+ def touch_associations
765
+ apps.map(&:touch)
766
+ end
767
+
768
+ def touch
769
+ super
770
+ touch_associations
771
+ end
772
+ end
773
+ ```
774
+
775
+ ## Custom `objectID`
776
+
777
+ By default, the `objectID` is based on your record's `id`. You can change this behavior specifying the `:id` option (be sure to use a uniq field).
778
+
779
+ ```ruby
780
+ class UniqUser < ActiveRecord::Base
781
+ include AlgoliaSearch
782
+
783
+ algoliasearch id: :uniq_name do
784
+ end
785
+ end
786
+ ```
787
+
788
+ ## Restrict indexing to a subset of your data
789
+
790
+ You can add constraints controlling if a record must be indexed by using options the `:if` or `:unless` options.
791
+
792
+ It allows you to do conditional indexing on a per document basis.
793
+
794
+ ```ruby
795
+ class Post < ActiveRecord::Base
796
+ include AlgoliaSearch
797
+
798
+ algoliasearch if: :published?, unless: :deleted? do
799
+ end
800
+
801
+ def published?
802
+ # [...]
803
+ end
804
+
805
+ def deleted?
806
+ # [...]
807
+ end
808
+ end
809
+ ```
810
+
811
+ **Notes:** As soon as you use those constraints, `addObjects` and `deleteObjects` calls will be performed in order to keep the index synced with the DB (The state-less gem doesn't know if the object don't match your constraints anymore or never matched, so we force ADD/DELETE operations to be sent). You can work-around this behavior creating a `_changed?` method:
812
+
813
+ ```ruby
814
+ class Contact < ActiveRecord::Base
815
+ include AlgoliaSearch
816
+
817
+ algoliasearch if: :published do
818
+ end
819
+
820
+ def published
821
+ # true or false
822
+ end
823
+
824
+ def published_changed?
825
+ # return true only if you know that the 'published' state changed
826
+ end
827
+ end
828
+ ```
829
+
830
+ You can index a subset of your records using either:
831
+
832
+ ```ruby
833
+ # will generate batch API calls (recommended)
834
+ MyModel.where('updated_at > ?', 10.minutes.ago).reindex!
835
+ ```
836
+
837
+ or
838
+
839
+ ```ruby
840
+ MyModel.index_objects MyModel.limit(5)
841
+ ```
842
+
843
+ ## Sanitizer
844
+
845
+ You can sanitize all your attributes using the `sanitize` option. It will strip all HTML tags from your attributes.
846
+
847
+ ```ruby
848
+ class User < ActiveRecord::Base
849
+ include AlgoliaSearch
850
+
851
+ algoliasearch per_environment: true, sanitize: true do
852
+ attributes :name, :email, :company
853
+ end
854
+ end
855
+
856
+ ```
857
+
858
+ If you're using Rails 4.2+, you also need to depend on `rails-html-sanitizer`:
859
+
860
+ ```ruby
861
+ gem 'rails-html-sanitizer'
862
+ ```
863
+
864
+ ## UTF-8 Encoding
865
+
866
+ You can force the UTF-8 encoding of all your attributes using the `force_utf8_encoding` option:
867
+
868
+ ```ruby
869
+ class User < ActiveRecord::Base
870
+ include AlgoliaSearch
871
+
872
+ algoliasearch force_utf8_encoding: true do
873
+ attributes :name, :email, :company
874
+ end
875
+ end
876
+
877
+ ```
878
+
879
+ ***Notes:*** This option is not compatible with Ruby 1.8
880
+
881
+ ## Exceptions
882
+
883
+ You can disable exceptions that could be raised while trying to reach Algolia's API by using the `raise_on_failure` option:
884
+
885
+ ```ruby
886
+ class Contact < ActiveRecord::Base
887
+ include AlgoliaSearch
888
+
889
+ # only raise exceptions in development env
890
+ algoliasearch raise_on_failure: Rails.env.development? do
891
+ attribute :first_name, :last_name, :email
892
+ end
893
+ end
894
+ ```
895
+
896
+ ## Configuration example
897
+
898
+ Here is a real-word configuration example (from [HN Search](https://github.com/algolia/hn-search)):
899
+
900
+ ```ruby
901
+ class Item < ActiveRecord::Base
902
+ include AlgoliaSearch
903
+
904
+ algoliasearch per_environment: true do
905
+ # the list of attributes sent to Algolia's API
906
+ attribute :created_at, :title, :url, :author, :points, :story_text, :comment_text, :author, :num_comments, :story_id, :story_title
907
+
908
+ # integer version of the created_at datetime field, to use numerical filtering
909
+ attribute :created_at_i do
910
+ created_at.to_i
911
+ end
912
+
913
+ # `title` is more important than `{story,comment}_text`, `{story,comment}_text` more than `url`, `url` more than `author`
914
+ # btw, do not take into account position in most fields to avoid first word match boost
915
+ searchableAttributes ['unordered(title)', 'unordered(story_text)', 'unordered(comment_text)', 'unordered(url)', 'author']
916
+
917
+ # tags used for filtering
918
+ tags do
919
+ [item_type, "author_#{author}", "story_#{story_id}"]
920
+ end
921
+
922
+ # use associated number of HN points to sort results (last sort criteria)
923
+ customRanking ['desc(points)', 'desc(num_comments)']
924
+
925
+ # google+, $1.5M raises, C#: we love you
926
+ separatorsToIndex '+#$'
927
+ end
928
+
929
+ def story_text
930
+ item_type_cd != Item.comment ? text : nil
931
+ end
932
+
933
+ def story_title
934
+ comment? && story ? story.title : nil
935
+ end
936
+
937
+ def story_url
938
+ comment? && story ? story.url : nil
939
+ end
940
+
941
+ def comment_text
942
+ comment? ? text : nil
943
+ end
944
+
945
+ def comment?
946
+ item_type_cd == Item.comment
947
+ end
948
+
949
+ # [...]
950
+ end
951
+ ```
952
+
953
+
954
+
955
+ # Indices
956
+
957
+
958
+
959
+ ## Manual indexing
960
+
961
+ You can trigger indexing using the <code>index!</code> instance method.
962
+
963
+ ```ruby
964
+ c = Contact.create!(params[:contact])
965
+ c.index!
966
+ ```
967
+
968
+ ## Manual removal
969
+
970
+ And trigger index removing using the <code>remove_from_index!</code> instance method.
971
+
972
+ ```ruby
973
+ c.remove_from_index!
974
+ c.destroy
975
+ ```
976
+
977
+ ## Reindexing
978
+
979
+ The gem provides 2 ways to reindex all your objects:
980
+
981
+ ### Atomical reindexing
982
+
983
+ To reindex all your records (taking into account the deleted objects), the `reindex` class method indices all your objects to a temporary index called `<INDEX_NAME>.tmp` and moves the temporary index to the final one once everything is indexed (atomically). This is the safest way to reindex all your content.
984
+
985
+ ```ruby
986
+ Contact.reindex
987
+ ```
988
+
989
+ **Notes**: if you're using an index-specific API key, ensure you're allowing both `<INDEX_NAME>` and `<INDEX_NAME>.tmp`.
990
+
991
+ **Warning:** You should not use such an atomic reindexing operation while scoping/filtering the model because this operation **replaces the entire index**, keeping the filtered objects only. ie: Don't do `MyModel.where(...).reindex` but do `MyModel.where(...).reindex!` (with the trailing `!`)!!!
992
+
993
+ ### Regular reindexing
994
+
995
+ To reindex all your objects in place (without temporary index and therefore without deleting removed objects), use the `reindex!` class method:
996
+
997
+ ```ruby
998
+ Contact.reindex!
999
+ ```
1000
+
1001
+ ## Clearing an index
1002
+
1003
+ To clear an index, use the <code>clear_index!</code> class method:
1004
+
1005
+ ```ruby
1006
+ Contact.clear_index!
1007
+ ```
1008
+
1009
+ ## Using the underlying index
1010
+
1011
+ You can access the underlying `index` object by calling the `index` class method:
1012
+
1013
+ ```ruby
1014
+ index = Contact.index
1015
+ # index.get_settings, index.partial_update_object, ...
1016
+ ```
1017
+
1018
+ ## Primary/replica
1019
+
1020
+ You can define replica indices using the <code>add_replica</code> method.
1021
+ Use `inherit: true` on the replica block if you want it to inherit from the primary settings.
1022
+
1023
+ ```ruby
1024
+ class Book < ActiveRecord::Base
1025
+ attr_protected
1026
+
1027
+ include AlgoliaSearch
1028
+
1029
+ algoliasearch per_environment: true do
1030
+ searchableAttributes [:name, :author, :editor]
1031
+
1032
+ # define a replica index to search by `author` only
1033
+ add_replica 'Book_by_author', per_environment: true do
1034
+ searchableAttributes [:author]
1035
+ end
1036
+
1037
+ # define a replica index with custom ordering but same settings than the main block
1038
+ add_replica 'Book_custom_order', inherit: true, per_environment: true do
1039
+ customRanking ['asc(rank)']
1040
+ end
1041
+ end
1042
+
1043
+ end
1044
+ ```
1045
+
1046
+ To search using a replica, use the following code:
1047
+
1048
+ ```ruby
1049
+ Book.raw_search 'foo bar', replica: 'Book_by_editor'
1050
+ # or
1051
+ Book.search 'foo bar', replica: 'Book_by_editor'
1052
+ ```
1053
+
1054
+ ## Share a single index
1055
+
1056
+ It can make sense to share an index between several models. In order to implement that, you'll need to ensure you don't have any conflict with the `objectID` of the underlying models.
1057
+
1058
+ ```ruby
1059
+ class Student < ActiveRecord::Base
1060
+ attr_protected
1061
+
1062
+ include AlgoliaSearch
1063
+
1064
+ algoliasearch index_name: 'people', id: :algolia_id do
1065
+ # [...]
1066
+ end
1067
+
1068
+ private
1069
+ def algolia_id
1070
+ "student_#{id}" # ensure the teacher & student IDs are not conflicting
1071
+ end
1072
+ end
1073
+
1074
+ class Teacher < ActiveRecord::Base
1075
+ attr_protected
1076
+
1077
+ include AlgoliaSearch
1078
+
1079
+ algoliasearch index_name: 'people', id: :algolia_id do
1080
+ # [...]
1081
+ end
1082
+
1083
+ private
1084
+ def algolia_id
1085
+ "teacher_#{id}" # ensure the teacher & student IDs are not conflicting
1086
+ end
1087
+ end
1088
+ ```
1089
+
1090
+ ***Notes:*** If you target a single index from several models, you must never use `MyModel.reindex` and only use `MyModel.reindex!`. The `reindex` method uses a temporary index to perform an atomic reindexing: if you use it, the resulting index will only contain records for the current model because it will not reindex the others.
1091
+
1092
+ ## Target multiple indices
1093
+
1094
+ You can index a record in several indices using the <code>add_index</code> method:
1095
+
1096
+ ```ruby
1097
+ class Book < ActiveRecord::Base
1098
+ attr_protected
1099
+
1100
+ include AlgoliaSearch
1101
+
1102
+ PUBLIC_INDEX_NAME = "Book_#{Rails.env}"
1103
+ SECURED_INDEX_NAME = "SecuredBook_#{Rails.env}"
1104
+
1105
+ # store all books in index 'SECURED_INDEX_NAME'
1106
+ algoliasearch index_name: SECURED_INDEX_NAME do
1107
+ searchableAttributes [:name, :author]
1108
+ # convert security to tags
1109
+ tags do
1110
+ [released ? 'public' : 'private', premium ? 'premium' : 'standard']
1111
+ end
1112
+
1113
+ # store all 'public' (released and not premium) books in index 'PUBLIC_INDEX_NAME'
1114
+ add_index PUBLIC_INDEX_NAME, if: :public? do
1115
+ searchableAttributes [:name, :author]
1116
+ end
1117
+ end
1118
+
1119
+ private
1120
+ def public?
1121
+ released && !premium
1122
+ end
1123
+
1124
+ end
1125
+ ```
1126
+
1127
+ To search using an extra index, use the following code:
1128
+
1129
+ ```ruby
1130
+ Book.raw_search 'foo bar', index: 'Book_by_editor'
1131
+ # or
1132
+ Book.search 'foo bar', index: 'Book_by_editor'
1133
+ ```
1134
+
1135
+
1136
+
1137
+ # Testing
1138
+
1139
+
1140
+
1141
+ ## Notes
1142
+
1143
+ To run the specs, please set the <code>ALGOLIA_APPLICATION_ID</code> and <code>ALGOLIA_API_KEY</code> environment variables. Since the tests are creating and removing indices, DO NOT use your production account.
1144
+
1145
+ You may want to disable all indexing (add, update & delete operations) API calls, you can set the `disable_indexing` option:
1146
+
1147
+ ```ruby
1148
+ class User < ActiveRecord::Base
1149
+ include AlgoliaSearch
1150
+
1151
+ algoliasearch per_environment: true, disable_indexing: Rails.env.test? do
1152
+ end
1153
+ end
1154
+
1155
+ class User < ActiveRecord::Base
1156
+ include AlgoliaSearch
1157
+
1158
+ algoliasearch per_environment: true, disable_indexing: Proc.new { Rails.env.test? || more_complex_condition } do
1159
+ end
1160
+ end
1161
+ ```
1162
+
1163
+
1164
+ ## ❓ Troubleshooting
1165
+
1166
+ Encountering an issue? Before reaching out to support, we recommend heading to our [FAQ](https://www.algolia.com/doc/api-client/troubleshooting/faq/ruby/) where you will find answers for the most common issues and gotchas with the client.
1167
+
1168
+ ## Use the Dockerfile
1169
+
1170
+ If you want to contribute to this project without installing all its dependencies, you can use our Docker image. Please check our [dedicated guide](DOCKER_README.MD) to learn more.
1171
+