algoliasearch-rails 1.7.1 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2aa68e2724bc3f0c7b43545b8d5156ae476acfde
4
- data.tar.gz: 4e3d1a69ce8cff145fc91b69f845cb2c38bde8bb
3
+ metadata.gz: 9862c9e947777afdb827b6ca9c9879e989573ce3
4
+ data.tar.gz: bbfcbd83a12f2ecaf38ed346a80167b214303d30
5
5
  SHA512:
6
- metadata.gz: 7e685c99823cb8f047b1bfd8d390a5b2a535df639586f6e7e8d92fe5eb60b546370f2697bfbc07a3afbfc067195ec23ad7b81a17645699ac93b974dbb2331811
7
- data.tar.gz: bf4848ce707351534dbe3fe03f952419a721a3ce4e37b7ecaf84c90c574721a1edae0373ec74eab936bf0146ee267fbbcc700a84eefeb13e02a1d1f7b3dce916
6
+ metadata.gz: ce9950505a350f386a8a778213a2352c98a0fac56fbdef99b0ba899270ed939b4bc92202e72ce16ad214fb7cad2ecbd9b816a7b811c3136ce12b263a9373a116
7
+ data.tar.gz: c1d747eedcec3a4fb90d9394e6f1476c72f22142e1dd2551c6661219c5a765f0472cf2c7ee583a115f0da47061eabfca4865ceb1b4be2ac5d4f9533025dfaf24
data/ChangeLog CHANGED
@@ -1,5 +1,10 @@
1
1
  CHANGELOG
2
2
 
3
+ 2014-02-04 1.7.2
4
+
5
+ * Add a ```raw_search``` method, retrieving the JSON raw answer
6
+ * Updated dependencies
7
+
3
8
  2014-01-31 1.7.1
4
9
 
5
10
  * Ensure methods are not conflicting with already defined ones (use algolia_METHOD_NAME)
data/Gemfile.lock CHANGED
@@ -17,10 +17,10 @@ GEM
17
17
  activesupport (= 4.0.2)
18
18
  arel (~> 4.0.0)
19
19
  activerecord-deprecated_finders (1.0.3)
20
- activerecord-jdbc-adapter (1.3.4)
20
+ activerecord-jdbc-adapter (1.3.6)
21
21
  activerecord (>= 2.2)
22
- activerecord-jdbcsqlite3-adapter (1.3.4)
23
- activerecord-jdbc-adapter (~> 1.3.4)
22
+ activerecord-jdbcsqlite3-adapter (1.3.6)
23
+ activerecord-jdbc-adapter (~> 1.3.6)
24
24
  jdbc-sqlite3 (~> 3.7.2)
25
25
  activesupport (4.0.2)
26
26
  i18n (~> 0.6, >= 0.6.4)
@@ -29,7 +29,7 @@ GEM
29
29
  thread_safe (~> 0.1)
30
30
  tzinfo (~> 0.3.37)
31
31
  addressable (2.3.5)
32
- algoliasearch (1.2.1)
32
+ algoliasearch (1.2.2)
33
33
  httpclient (~> 2.3)
34
34
  json (>= 1.5.1)
35
35
  arel (4.0.1)
@@ -40,7 +40,7 @@ GEM
40
40
  autotest-fsevent (0.2.9)
41
41
  sys-uname
42
42
  autotest-growl (0.2.16)
43
- backports (3.4.0)
43
+ backports (3.5.0)
44
44
  builder (3.1.4)
45
45
  coderay (1.1.0)
46
46
  diff-lcs (1.2.5)
@@ -48,14 +48,14 @@ GEM
48
48
  ethon (0.6.2)
49
49
  ffi (>= 1.3.0)
50
50
  mime-types (~> 1.18)
51
- faraday (0.8.8)
51
+ faraday (0.8.9)
52
52
  multipart-post (~> 1.2.0)
53
53
  faraday_middleware (0.9.0)
54
54
  faraday (>= 0.7.4, < 0.9)
55
55
  ffi (1.9.3)
56
56
  ffi (1.9.3-java)
57
57
  ffi2-generators (0.1.1)
58
- gh (0.13.0)
58
+ gh (0.13.2)
59
59
  addressable
60
60
  backports
61
61
  faraday (~> 0.8)
@@ -68,23 +68,26 @@ GEM
68
68
  jdbc-sqlite3 (3.7.2.1)
69
69
  json (1.8.1)
70
70
  json (1.8.1-java)
71
- kaminari (0.15.0)
71
+ kaminari (0.15.1)
72
72
  actionpack (>= 3.0.0)
73
73
  activesupport (>= 3.0.0)
74
74
  launchy (2.4.2)
75
75
  addressable (~> 2.3)
76
+ launchy (2.4.2-java)
77
+ addressable (~> 2.3)
78
+ spoon (~> 0.0.1)
76
79
  method_source (0.8.2)
77
80
  mime-types (1.25.1)
78
81
  minitest (4.7.5)
79
- multi_json (1.8.2)
82
+ multi_json (1.8.4)
80
83
  multipart-post (1.2.0)
81
- net-http-persistent (2.9)
84
+ net-http-persistent (2.9.1)
82
85
  net-http-pipeline (1.0.1)
83
- pry (0.9.12.4)
86
+ pry (0.9.12.6)
84
87
  coderay (~> 1.0)
85
88
  method_source (~> 0.8)
86
89
  slop (~> 3.4)
87
- pry (0.9.12.4-java)
90
+ pry (0.9.12.6-java)
88
91
  coderay (~> 1.0)
89
92
  method_source (~> 0.8)
90
93
  slop (~> 3.4)
@@ -95,7 +98,7 @@ GEM
95
98
  rack-test (0.6.2)
96
99
  rack (>= 1.0)
97
100
  rake (10.1.1)
98
- rdoc (4.1.0)
101
+ rdoc (4.1.1)
99
102
  json (~> 1.4)
100
103
  redgreen (1.2.2)
101
104
  rspec (2.14.1)
@@ -103,9 +106,9 @@ GEM
103
106
  rspec-expectations (~> 2.14.0)
104
107
  rspec-mocks (~> 2.14.0)
105
108
  rspec-core (2.14.7)
106
- rspec-expectations (2.14.4)
109
+ rspec-expectations (2.14.5)
107
110
  diff-lcs (>= 1.1.3, < 2.0)
108
- rspec-mocks (2.14.4)
111
+ rspec-mocks (2.14.5)
109
112
  rubysl (2.0.15)
110
113
  rubysl-abbrev (~> 2.0)
111
114
  rubysl-base64 (~> 2.0)
@@ -264,7 +267,7 @@ GEM
264
267
  rubysl-observer (2.0.0)
265
268
  rubysl-open-uri (2.0.0)
266
269
  rubysl-open3 (2.0.0)
267
- rubysl-openssl (2.0.6)
270
+ rubysl-openssl (2.1.0)
268
271
  rubysl-optparse (2.0.1)
269
272
  rubysl-shellwords (~> 2.0)
270
273
  rubysl-ostruct (2.0.4)
@@ -279,7 +282,7 @@ GEM
279
282
  rubysl-readline (2.0.2)
280
283
  rubysl-resolv (2.0.0)
281
284
  rubysl-rexml (2.0.2)
282
- rubysl-rinda (2.0.0)
285
+ rubysl-rinda (2.0.1)
283
286
  rubysl-rss (2.0.0)
284
287
  rubysl-scanf (2.0.0)
285
288
  rubysl-securerandom (2.0.0)
@@ -318,7 +321,7 @@ GEM
318
321
  atomic
319
322
  thread_safe (0.1.3-java)
320
323
  atomic
321
- travis (1.6.6)
324
+ travis (1.6.7)
322
325
  addressable (~> 2.3)
323
326
  backports
324
327
  faraday (~> 0.8.7)
data/README.md CHANGED
@@ -102,10 +102,20 @@ class Product < ActiveRecord::Base
102
102
  end
103
103
  ```
104
104
 
105
+ A search returns ORM-compliant objects reloading them from your database.
106
+
105
107
  ```ruby
106
108
  p Contact.search("jon doe")
107
109
  ```
108
110
 
111
+ If you want to retrieve the raw JSON answer from the API, without re-loading the objects from the database, you can use:
112
+
113
+ ```ruby
114
+ p Contact.raw_search("jon doe")
115
+ ```
116
+
117
+ By the way, we recommend the usage of our [JavaScript API Client](https://github.com/algolia/algoliasearch-client-js) to perform queries.
118
+
109
119
  **Notes:** All methods injected by the ```AlgoliaSearch``` include are prefixed by ```algolia_``` and aliased to the associated short names if they aren't already defined.
110
120
 
111
121
  ```ruby
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.1
1
+ 1.7.2
@@ -6,7 +6,7 @@
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "algoliasearch-rails"
9
- s.version = "1.7.1"
9
+ s.version = "1.7.2"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.authors = ["Algolia"]
@@ -128,16 +128,19 @@ module AlgoliaSearch
128
128
  alias_method :remove_from_index!, :algolia_remove_from_index! unless method_defined? :remove_from_index!
129
129
  alias_method :clear_index!, :algolia_clear_index! unless method_defined? :clear_index!
130
130
  alias_method :search, :algolia_search unless method_defined? :search
131
+ alias_method :raw_search, :algolia_raw_search unless method_defined? :raw_search
131
132
  alias_method :index, :algolia_index unless method_defined? :index
132
133
  alias_method :index_name, :algolia_index_name unless method_defined? :index_name
133
134
  alias_method :must_reindex?, :algolia_must_reindex? unless method_defined? :must_reindex?
134
135
  end
136
+
137
+ base.cattr_accessor :algolia_options, :algolia_settings, :algolia_index_settings
135
138
  end
136
139
 
137
140
  def algoliasearch(options = {}, &block)
138
- @algolia_index_settings = IndexSettings.new(block_given? ? Proc.new : nil)
139
- @algolia_settings = @algolia_index_settings.to_settings
140
- @algolia_options = { type: algolia_full_const_get(model_name.to_s), per_page: @algolia_index_settings.get_setting(:hitsPerPage) || 10, page: 1 }.merge(options)
141
+ self.algolia_index_settings = IndexSettings.new(block_given? ? Proc.new : nil)
142
+ self.algolia_settings = algolia_index_settings.to_settings
143
+ self.algolia_options = { type: algolia_full_const_get(model_name.to_s), per_page: algolia_index_settings.get_setting(:hitsPerPage) || 10, page: 1 }.merge(options)
141
144
 
142
145
  attr_accessor :highlight_result
143
146
 
@@ -167,9 +170,10 @@ module AlgoliaSearch
167
170
  return if @algolia_without_auto_index_scope
168
171
  algolia_ensure_init
169
172
  last_task = nil
170
- find_in_batches(batch_size: batch_size) do |group|
173
+
174
+ algolia_find_in_batches(batch_size) do |group|
171
175
  group.select! { |o| algolia_indexable?(o) } if algolia_conditional_index?
172
- objects = group.map { |o| @algolia_index_settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o) }
176
+ objects = group.map { |o| algolia_index_settings.get_attributes(o).merge 'objectID' => algolia_object_id_of(o) }
173
177
  last_task = @algolia_index.save_objects(objects)
174
178
  end
175
179
  @algolia_index.wait_task(last_task["taskID"]) if last_task and synchronous == true
@@ -179,9 +183,9 @@ module AlgoliaSearch
179
183
  return if @algolia_without_auto_index_scope || !algolia_indexable?(object)
180
184
  algolia_ensure_init
181
185
  if synchronous
182
- @algolia_index.add_object!(@algolia_index_settings.get_attributes(object), algolia_object_id_of(object))
186
+ @algolia_index.add_object!(algolia_index_settings.get_attributes(object), algolia_object_id_of(object))
183
187
  else
184
- @algolia_index.add_object(@algolia_index_settings.get_attributes(object), algolia_object_id_of(object))
188
+ @algolia_index.add_object(algolia_index_settings.get_attributes(object), algolia_object_id_of(object))
185
189
  end
186
190
  end
187
191
 
@@ -201,17 +205,21 @@ module AlgoliaSearch
201
205
  @algolia_index = nil
202
206
  end
203
207
 
204
- def algolia_search(q, settings = {})
208
+ def algolia_raw_search(q, settings = {})
205
209
  algolia_ensure_init
206
- json = @algolia_index.search(q, Hash[settings.map { |k,v| [k.to_s, v.to_s] }])
210
+ @algolia_index.search(q, Hash[settings.map { |k,v| [k.to_s, v.to_s] }])
211
+ end
212
+
213
+ def algolia_search(q, settings = {})
214
+ json = algolia_raw_search(q, settings)
207
215
  results = json['hits'].map do |hit|
208
- o = @algolia_options[:type].where(algolia_object_id_method => hit['objectID']).first
216
+ o = algolia_options[:type].where(algolia_object_id_method => hit['objectID']).first
209
217
  if o
210
218
  o.highlight_result = hit['_highlightResult']
211
219
  o
212
220
  end
213
221
  end.compact
214
- AlgoliaSearch::Pagination.create(results, json['nbHits'].to_i, @algolia_options)
222
+ AlgoliaSearch::Pagination.create(results, json['nbHits'].to_i, algolia_options)
215
223
  end
216
224
 
217
225
  def algolia_index
@@ -220,14 +228,14 @@ module AlgoliaSearch
220
228
  end
221
229
 
222
230
  def algolia_index_name
223
- name = @algolia_options[:index_name] || model_name.to_s.gsub('::', '_')
224
- name = "#{name}_#{Rails.env.to_s}" if @algolia_options[:per_environment]
231
+ name = algolia_options[:index_name] || model_name.to_s.gsub('::', '_')
232
+ name = "#{name}_#{Rails.env.to_s}" if algolia_options[:per_environment]
225
233
  name
226
234
  end
227
235
 
228
236
  def algolia_must_reindex?(object)
229
237
  return true if algolia_object_id_changed?(object)
230
- @algolia_index_settings.get_attributes(object).each do |k, v|
238
+ algolia_index_settings.get_attributes(object).each do |k, v|
231
239
  changed_method = "#{k}_changed?"
232
240
  return true if object.respond_to?(changed_method) && object.send(changed_method)
233
241
  end
@@ -240,13 +248,13 @@ module AlgoliaSearch
240
248
  return if @algolia_index
241
249
  @algolia_index = Algolia::Index.new(algolia_index_name)
242
250
  current_settings = @algolia_index.get_settings rescue nil # if the index doesn't exist
243
- @algolia_index.set_settings(@algolia_settings) if algolia_index_settings_changed?(current_settings, @algolia_settings)
251
+ @algolia_index.set_settings(algolia_settings) if algolia_index_settings_changed?(current_settings, algolia_settings)
244
252
  end
245
253
 
246
254
  private
247
255
 
248
256
  def algolia_object_id_method
249
- @algolia_options[:id] || @algolia_options[:object_id] || :id
257
+ algolia_options[:id] || algolia_options[:object_id] || :id
250
258
  end
251
259
 
252
260
  def algolia_object_id_of(o)
@@ -285,12 +293,12 @@ module AlgoliaSearch
285
293
  end
286
294
 
287
295
  def algolia_conditional_index?
288
- @algolia_options[:if].present? || @algolia_options[:unless].present?
296
+ algolia_options[:if].present? || algolia_options[:unless].present?
289
297
  end
290
298
 
291
299
  def algolia_indexable?(object)
292
- if_passes = @algolia_options[:if].blank? || algolia_constraint_passes?(object, @algolia_options[:if])
293
- unless_passes = @algolia_options[:unless].blank? || !algolia_constraint_passes?(object, @algolia_options[:unless])
300
+ if_passes = algolia_options[:if].blank? || algolia_constraint_passes?(object, algolia_options[:if])
301
+ unless_passes = algolia_options[:unless].blank? || !algolia_constraint_passes?(object, algolia_options[:unless])
294
302
  if_passes && unless_passes
295
303
  end
296
304
 
@@ -311,6 +319,23 @@ module AlgoliaSearch
311
319
  end
312
320
  end
313
321
  end
322
+
323
+ def algolia_find_in_batches(batch_size, &block)
324
+ if (defined?(::ActiveRecord) && ancestors.include?(::ActiveRecord::Base)) || respond_to?(:find_in_batches)
325
+ find_in_batches(batch_size: batch_size, &block)
326
+ else
327
+ # don't worry, mongoid has its own underlying cursor/streaming mechanism
328
+ items = []
329
+ all.each do |item|
330
+ items << item
331
+ if items.length % batch_size == 0
332
+ yield items
333
+ items = []
334
+ end
335
+ yield items unless items.empty?
336
+ end
337
+ end
338
+ end
314
339
  end
315
340
 
316
341
  # these are the instance methods included
@@ -1,74 +1,216 @@
1
- /*!
2
- * algoliasearch 2.3.6
3
- * https://github.com/algolia/algoliasearch-client-js
4
- * Copyright 2013 Algolia SAS; Licensed MIT
1
+ /*
2
+ * Copyright (c) 2013 Algolia
3
+ * http://www.algolia.com/
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in
13
+ * all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ * THE SOFTWARE.
5
22
  */
6
23
 
7
- var VERSION = "2.3.6";
24
+ var ALGOLIA_VERSION = '2.3.8';
8
25
 
26
+ /*
27
+ * Copyright (c) 2013 Algolia
28
+ * http://www.algolia.com/
29
+ *
30
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ * of this software and associated documentation files (the "Software"), to deal
32
+ * in the Software without restriction, including without limitation the rights
33
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ * copies of the Software, and to permit persons to whom the Software is
35
+ * furnished to do so, subject to the following conditions:
36
+ *
37
+ * The above copyright notice and this permission notice shall be included in
38
+ * all copies or substantial portions of the Software.
39
+ *
40
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46
+ * THE SOFTWARE.
47
+ */
48
+
49
+ /*
50
+ * Algolia Search library initialization
51
+ * @param applicationID the application ID you have in your admin interface
52
+ * @param apiKey a valid API key for the service
53
+ * @param method specify if the protocol used is http or https (http by default to make the first search query faster).
54
+ * You need to use https is you are doing something else than just search queries.
55
+ * @param resolveDNS let you disable first empty query that is launch to warmup the service
56
+ * @param hostsArray (optionnal) the list of hosts that you have received for the service
57
+ */
9
58
  var AlgoliaSearch = function(applicationID, apiKey, method, resolveDNS, hostsArray) {
10
59
  this.applicationID = applicationID;
11
60
  this.apiKey = apiKey;
61
+
12
62
  if (this._isUndefined(hostsArray)) {
13
- hostsArray = [ applicationID + "-1.algolia.io", applicationID + "-2.algolia.io", applicationID + "-3.algolia.io" ];
63
+ hostsArray = [applicationID + '-1.algolia.io',
64
+ applicationID + '-2.algolia.io',
65
+ applicationID + '-3.algolia.io'];
14
66
  }
15
67
  this.hosts = [];
68
+ // Add hosts in random order
16
69
  for (var i = 0; i < hostsArray.length; ++i) {
17
- if (Math.random() > .5) {
70
+ if (Math.random() > 0.5) {
18
71
  this.hosts.reverse();
19
72
  }
20
73
  if (this._isUndefined(method) || method == null) {
21
- this.hosts.push(("https:" == document.location.protocol ? "https" : "http") + "://" + hostsArray[i]);
22
- } else if (method === "https" || method === "HTTPS") {
23
- this.hosts.push("https://" + hostsArray[i]);
74
+ this.hosts.push(('https:' == document.location.protocol ? 'https' : 'http') + '://' + hostsArray[i]);
75
+ } else if (method === 'https' || method === 'HTTPS') {
76
+ this.hosts.push('https://' + hostsArray[i]);
24
77
  } else {
25
- this.hosts.push("http://" + hostsArray[i]);
78
+ this.hosts.push('http://' + hostsArray[i]);
26
79
  }
27
80
  }
28
- if (Math.random() > .5) {
81
+ if (Math.random() > 0.5) {
29
82
  this.hosts.reverse();
30
83
  }
31
84
  if (this._isUndefined(resolveDNS) || resolveDNS) {
32
- this._jsonRequest({
33
- method: "GET",
34
- url: "/1/isalive"
35
- });
85
+ // Perform a call to solve DNS (avoid to slow down the first user query)
86
+ this._jsonRequest({ method: 'GET',
87
+ url: '/1/isalive' });
36
88
  }
37
89
  this.extraHeaders = [];
38
90
  };
39
91
 
92
+ function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) {
93
+
94
+ function _getHitAxplainationForOneAttr_recurse(obj, foundWords) {
95
+ if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) {
96
+ var match = false;
97
+ for (var j = 0; j < obj.matchedWords.length; ++j) {
98
+ var word = obj.matchedWords[j];
99
+ if (!(word in foundWords)) {
100
+ foundWords[word] = 1;
101
+ match = true;
102
+ }
103
+ }
104
+ return match ? [obj.value] : [];
105
+ } else if (obj instanceof Array) {
106
+ var res = [];
107
+ for (var i = 0; i < obj.length; ++i) {
108
+ var array = _getHitAxplainationForOneAttr_recurse(obj[i], foundWords);
109
+ res = res.concat(array);
110
+ }
111
+ return res;
112
+ } else if (typeof obj === 'object') {
113
+ var res = [];
114
+ for (prop in obj) {
115
+ if (obj.hasOwnProperty(prop)){
116
+ res = res.concat(_getHitAxplainationForOneAttr_recurse(obj[prop], foundWords));
117
+ }
118
+ }
119
+ return res;
120
+ }
121
+ return [];
122
+ }
123
+
124
+ function _getHitAxplainationForOneAttr(hit, foundWords, attr) {
125
+ if (attr.indexOf('.') === -1) {
126
+ if (attr in hit._highlightResult) {
127
+ return _getHitAxplainationForOneAttr_recurse(hit._highlightResult[attr], foundWords);
128
+ }
129
+ return [];
130
+ }
131
+ var array = attr.split('.');
132
+ var obj = hit._highlightResult;
133
+ for (var i = 0; i < array.length; ++i) {
134
+ if (array[i] in obj) {
135
+ obj = obj[array[i]];
136
+ } else {
137
+ return [];
138
+ }
139
+ }
140
+ return _getHitAxplainationForOneAttr_recurse(obj, foundWords);
141
+ }
142
+
143
+ var res = {};
144
+ var foundWords = {};
145
+ var title = _getHitAxplainationForOneAttr(hit, foundWords, titleAttribute);
146
+ res.title = (title.length > 0) ? title[0] : "";
147
+ res.subtitles = [];
148
+
149
+ if (typeof otherAttributes !== 'undefined') {
150
+ for (var i = 0; i < otherAttributes.length; ++i) {
151
+ var attr = _getHitAxplainationForOneAttr(hit, foundWords, otherAttributes[i]);
152
+ for (var j = 0; j < attr.length; ++j) {
153
+ res.subtitles.push({ attr: otherAttributes[i], value: attr[j] });
154
+ }
155
+ }
156
+ }
157
+ return res;
158
+ }
159
+
160
+
40
161
  AlgoliaSearch.prototype = {
162
+ /*
163
+ * Delete an index
164
+ *
165
+ * @param indexName the name of index to delete
166
+ * @param callback the result callback with two arguments
167
+ * success: boolean set to true if the request was successfull
168
+ * content: the server answer that contains the task ID
169
+ */
41
170
  deleteIndex: function(indexName, callback) {
42
- this._jsonRequest({
43
- method: "DELETE",
44
- url: "/1/indexes/" + encodeURIComponent(indexName),
45
- callback: callback
46
- });
171
+ this._jsonRequest({ method: 'DELETE',
172
+ url: '/1/indexes/' + encodeURIComponent(indexName),
173
+ callback: callback });
47
174
  },
175
+ /**
176
+ * Move an existing index.
177
+ * @param srcIndexName the name of index to copy.
178
+ * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
179
+ * @param callback the result callback with two arguments
180
+ * success: boolean set to true if the request was successfull
181
+ * content: the server answer that contains the task ID
182
+ */
48
183
  moveIndex: function(srcIndexName, dstIndexName, callback) {
49
- var postObj = {
50
- operation: "move",
51
- destination: dstIndexName
52
- };
53
- this._jsonRequest({
54
- method: "POST",
55
- url: "/1/indexes/" + encodeURIComponent(srcIndexName) + "/operation",
56
- body: postObj,
57
- callback: callback
58
- });
184
+ var postObj = {operation: 'move', destination: dstIndexName};
185
+ this._jsonRequest({ method: 'POST',
186
+ url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
187
+ body: postObj,
188
+ callback: callback });
189
+
59
190
  },
191
+ /**
192
+ * Copy an existing index.
193
+ * @param srcIndexName the name of index to copy.
194
+ * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
195
+ * @param callback the result callback with two arguments
196
+ * success: boolean set to true if the request was successfull
197
+ * content: the server answer that contains the task ID
198
+ */
60
199
  copyIndex: function(srcIndexName, dstIndexName, callback) {
61
- var postObj = {
62
- operation: "copy",
63
- destination: dstIndexName
64
- };
65
- this._jsonRequest({
66
- method: "POST",
67
- url: "/1/indexes/" + encodeURIComponent(srcIndexName) + "/operation",
68
- body: postObj,
69
- callback: callback
70
- });
200
+ var postObj = {operation: 'copy', destination: dstIndexName};
201
+ this._jsonRequest({ method: 'POST',
202
+ url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
203
+ body: postObj,
204
+ callback: callback });
71
205
  },
206
+ /**
207
+ * Return last log entries.
208
+ * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry).
209
+ * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000.
210
+ * @param callback the result callback with two arguments
211
+ * success: boolean set to true if the request was successfull
212
+ * content: the server answer that contains the task ID
213
+ */
72
214
  getLogs: function(callback, offset, length) {
73
215
  if (this._isUndefined(offset)) {
74
216
  offset = 0;
@@ -76,53 +218,110 @@ AlgoliaSearch.prototype = {
76
218
  if (this._isUndefined(length)) {
77
219
  length = 10;
78
220
  }
79
- this._jsonRequest({
80
- method: "GET",
81
- url: "/1/logs?offset=" + offset + "&length=" + length,
82
- callback: callback
83
- });
221
+
222
+ this._jsonRequest({ method: 'GET',
223
+ url: '/1/logs?offset=' + offset + '&length=' + length,
224
+ callback: callback });
84
225
  },
226
+ /*
227
+ * List all existing indexes
228
+ *
229
+ * @param callback the result callback with two arguments
230
+ * success: boolean set to true if the request was successfull
231
+ * content: the server answer with index list or error description if success is false.
232
+ */
85
233
  listIndexes: function(callback) {
86
- this._jsonRequest({
87
- method: "GET",
88
- url: "/1/indexes/",
89
- callback: callback
90
- });
234
+ this._jsonRequest({ method: 'GET',
235
+ url: '/1/indexes/',
236
+ callback: callback });
91
237
  },
238
+
239
+ /*
240
+ * Get the index object initialized
241
+ *
242
+ * @param indexName the name of index
243
+ * @param callback the result callback with one argument (the Index instance)
244
+ */
92
245
  initIndex: function(indexName) {
93
246
  return new this.Index(this, indexName);
94
247
  },
248
+ /*
249
+ * List all existing user keys with their associated ACLs
250
+ *
251
+ * @param callback the result callback with two arguments
252
+ * success: boolean set to true if the request was successfull
253
+ * content: the server answer with user keys list or error description if success is false.
254
+ */
95
255
  listUserKeys: function(callback) {
96
- this._jsonRequest({
97
- method: "GET",
98
- url: "/1/keys",
99
- callback: callback
100
- });
256
+ this._jsonRequest({ method: 'GET',
257
+ url: '/1/keys',
258
+ callback: callback });
101
259
  },
260
+ /*
261
+ * Get ACL of a user key
262
+ *
263
+ * @param callback the result callback with two arguments
264
+ * success: boolean set to true if the request was successfull
265
+ * content: the server answer with user keys list or error description if success is false.
266
+ */
102
267
  getUserKeyACL: function(key, callback) {
103
- this._jsonRequest({
104
- method: "GET",
105
- url: "/1/keys/" + key,
106
- callback: callback
107
- });
268
+ this._jsonRequest({ method: 'GET',
269
+ url: '/1/keys/' + key,
270
+ callback: callback });
108
271
  },
272
+ /*
273
+ * Delete an existing user key
274
+ *
275
+ * @param callback the result callback with two arguments
276
+ * success: boolean set to true if the request was successfull
277
+ * content: the server answer with user keys list or error description if success is false.
278
+ */
109
279
  deleteUserKey: function(key, callback) {
110
- this._jsonRequest({
111
- method: "DELETE",
112
- url: "/1/keys/" + key,
113
- callback: callback
114
- });
280
+ this._jsonRequest({ method: 'DELETE',
281
+ url: '/1/keys/' + key,
282
+ callback: callback });
115
283
  },
284
+ /*
285
+ * Add an existing user key
286
+ *
287
+ * @param acls the list of ACL for this key. Defined by an array of strings that
288
+ * can contains the following values:
289
+ * - search: allow to search (https and http)
290
+ * - addObject: allows to add/update an object in the index (https only)
291
+ * - deleteObject : allows to delete an existing object (https only)
292
+ * - deleteIndex : allows to delete index content (https only)
293
+ * - settings : allows to get index settings (https only)
294
+ * - editSettings : allows to change index settings (https only)
295
+ * @param callback the result callback with two arguments
296
+ * success: boolean set to true if the request was successfull
297
+ * content: the server answer with user keys list or error description if success is false.
298
+ */
116
299
  addUserKey: function(acls, callback) {
117
300
  var aclsObject = {};
118
301
  aclsObject.acl = acls;
119
- this._jsonRequest({
120
- method: "POST",
121
- url: "/1/keys",
122
- body: aclsObject,
123
- callback: callback
124
- });
302
+ this._jsonRequest({ method: 'POST',
303
+ url: '/1/keys',
304
+ body: aclsObject,
305
+ callback: callback });
125
306
  },
307
+ /*
308
+ * Add an existing user key
309
+ *
310
+ * @param acls the list of ACL for this key. Defined by an array of strings that
311
+ * can contains the following values:
312
+ * - search: allow to search (https and http)
313
+ * - addObject: allows to add/update an object in the index (https only)
314
+ * - deleteObject : allows to delete an existing object (https only)
315
+ * - deleteIndex : allows to delete index content (https only)
316
+ * - settings : allows to get index settings (https only)
317
+ * - editSettings : allows to change index settings (https only)
318
+ * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
319
+ * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
320
+ * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
321
+ * @param callback the result callback with two arguments
322
+ * success: boolean set to true if the request was successfull
323
+ * content: the server answer with user keys list or error description if success is false.
324
+ */
126
325
  addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
127
326
  var indexObj = this;
128
327
  var aclsObject = {};
@@ -130,42 +329,64 @@ AlgoliaSearch.prototype = {
130
329
  aclsObject.validity = validity;
131
330
  aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
132
331
  aclsObject.maxHitsPerQuery = maxHitsPerQuery;
133
- this._jsonRequest({
134
- method: "POST",
135
- url: "/1/indexes/" + indexObj.indexName + "/keys",
136
- body: aclsObject,
137
- callback: callback
138
- });
332
+ this._jsonRequest({ method: 'POST',
333
+ url: '/1/indexes/' + indexObj.indexName + '/keys',
334
+ body: aclsObject,
335
+ callback: callback });
139
336
  },
337
+ /*
338
+ * Initialize a new batch of search queries
339
+ */
140
340
  startQueriesBatch: function() {
141
341
  this.batch = [];
142
342
  },
343
+ /*
344
+ * Add a search query in the batch
345
+ *
346
+ * @param query the full text query
347
+ * @param args (optional) if set, contains an object with query parameters:
348
+ * - attributes: an array of object attribute names to retrieve
349
+ * (if not set all attributes are retrieve)
350
+ * - attributesToHighlight: an array of object attribute names to highlight
351
+ * (if not set indexed attributes are highlighted)
352
+ * - minWordSizefor1Typo: the minimum number of characters to accept one typo.
353
+ * Defaults to 3.
354
+ * - minWordSizefor2Typos: the minimum number of characters to accept two typos.
355
+ * Defaults to 7.
356
+ * - getRankingInfo: if set, the result hits will contain ranking information in
357
+ * _rankingInfo attribute
358
+ * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0.
359
+ * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10.
360
+ */
143
361
  addQueryInBatch: function(indexName, query, args) {
144
- var params = "query=" + encodeURIComponent(query);
362
+ var params = 'query=' + encodeURIComponent(query);
145
363
  if (!this._isUndefined(args) && args != null) {
146
364
  params = this._getSearchParams(args, params);
147
365
  }
148
- this.batch.push({
149
- indexName: indexName,
150
- params: params
151
- });
366
+ this.batch.push({ indexName: indexName, params: params });
152
367
  },
368
+ /*
369
+ * Clear all queries in cache
370
+ */
153
371
  clearCache: function() {
154
372
  this.cache = {};
155
373
  },
374
+ /*
375
+ * Launch the batch of queries using XMLHttpRequest.
376
+ * (Optimized for browser using a POST query to minimize number of OPTIONS queries)
377
+ *
378
+ * @param callback the function that will receive results
379
+ * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime.
380
+ */
156
381
  sendQueriesBatch: function(callback, delay) {
157
382
  var as = this;
158
- var params = {
159
- requests: [],
160
- apiKey: this.apiKey,
161
- appID: this.applicationID
162
- };
383
+ var params = {requests: [], apiKey: this.apiKey, appID: this.applicationID};
163
384
  for (var i = 0; i < as.batch.length; ++i) {
164
385
  params.requests.push(as.batch[i]);
165
386
  }
166
387
  window.clearTimeout(as.onDelayTrigger);
167
388
  if (!this._isUndefined(delay) && delay != null && delay > 0) {
168
- var onDelayTrigger = window.setTimeout(function() {
389
+ var onDelayTrigger = window.setTimeout( function() {
169
390
  as._sendQueriesBatch(params, callback);
170
391
  }, delay);
171
392
  as.onDelayTrigger = onDelayTrigger;
@@ -173,34 +394,38 @@ AlgoliaSearch.prototype = {
173
394
  this._sendQueriesBatch(params, callback);
174
395
  }
175
396
  },
397
+ /*
398
+ * Index class constructor.
399
+ * You should not use this method directly but use initIndex() function
400
+ */
176
401
  Index: function(algoliasearch, indexName) {
177
402
  this.indexName = indexName;
178
403
  this.as = algoliasearch;
179
404
  this.typeAheadArgs = null;
180
405
  this.typeAheadValueOption = null;
181
406
  },
407
+
182
408
  setExtraHeader: function(key, value) {
183
- this.extraHeaders.push({
184
- key: key,
185
- value: value
186
- });
409
+ this.extraHeaders.push({ key: key, value: value});
187
410
  },
411
+
188
412
  _sendQueriesBatch: function(params, callback) {
189
- this._jsonRequest({
190
- cache: this.cache,
191
- method: "POST",
192
- url: "/1/indexes/*/queries",
193
- body: params,
194
- callback: callback
195
- });
413
+ this._jsonRequest({ cache: this.cache,
414
+ method: 'POST',
415
+ url: '/1/indexes/*/queries',
416
+ body: params,
417
+ callback: callback });
196
418
  },
419
+ /*
420
+ * Wrapper that try all hosts to maximize the quality of service
421
+ */
197
422
  _jsonRequest: function(opts) {
198
423
  var self = this;
199
424
  var callback = opts.callback;
200
425
  var cache = null;
201
426
  var cacheID = opts.url;
202
427
  if (!this._isUndefined(opts.body)) {
203
- cacheID = opts.url + "_body_" + JSON.stringify(opts.body);
428
+ cacheID = opts.url + '_body_' + JSON.stringify(opts.body);
204
429
  }
205
430
  if (!this._isUndefined(opts.cache)) {
206
431
  cache = opts.cache;
@@ -211,6 +436,7 @@ AlgoliaSearch.prototype = {
211
436
  return;
212
437
  }
213
438
  }
439
+
214
440
  var impl = function(position) {
215
441
  var idx = 0;
216
442
  if (!self._isUndefined(position)) {
@@ -218,20 +444,18 @@ AlgoliaSearch.prototype = {
218
444
  }
219
445
  if (self.hosts.length <= idx) {
220
446
  if (!self._isUndefined(callback)) {
221
- callback(false, {
222
- message: "Cannot contact server"
223
- });
447
+ callback(false, { message: 'Cannot contact server'});
224
448
  }
225
449
  return;
226
450
  }
227
451
  opts.callback = function(retry, success, res, body) {
228
452
  if (!success && !self._isUndefined(body)) {
229
- console.log("Error: " + body.message);
453
+ console.log('Error: ' + body.message);
230
454
  }
231
455
  if (success && !self._isUndefined(opts.cache)) {
232
456
  cache[cacheID] = body;
233
457
  }
234
- if (!success && retry && idx + 1 < self.hosts.length) {
458
+ if (!success && retry && (idx + 1) < self.hosts.length) {
235
459
  impl(idx + 1);
236
460
  } else {
237
461
  if (!self._isUndefined(callback)) {
@@ -244,6 +468,7 @@ AlgoliaSearch.prototype = {
244
468
  };
245
469
  impl();
246
470
  },
471
+
247
472
  _jsonRequestByHost: function(opts) {
248
473
  var body = null;
249
474
  var self = this;
@@ -252,47 +477,53 @@ AlgoliaSearch.prototype = {
252
477
  }
253
478
  var url = opts.hostname + opts.url;
254
479
  var xmlHttp = null;
480
+
255
481
  xmlHttp = new XMLHttpRequest();
256
- if ("withCredentials" in xmlHttp) {
257
- xmlHttp.open(opts.method, url, true);
258
- xmlHttp.setRequestHeader("X-Algolia-API-Key", this.apiKey);
259
- xmlHttp.setRequestHeader("X-Algolia-Application-Id", this.applicationID);
482
+ if ('withCredentials' in xmlHttp) {
483
+ xmlHttp.open(opts.method, url , true);
484
+ xmlHttp.setRequestHeader('X-Algolia-API-Key', this.apiKey);
485
+ xmlHttp.setRequestHeader('X-Algolia-Application-Id', this.applicationID);
260
486
  for (var i = 0; i < this.extraHeaders.length; ++i) {
261
487
  xmlHttp.setRequestHeader(this.extraHeaders[i].key, this.extraHeaders[i].value);
262
488
  }
263
489
  if (body != null) {
264
- xmlHttp.setRequestHeader("Content-type", "application/json");
490
+ xmlHttp.setRequestHeader('Content-type', 'application/json');
265
491
  }
266
- } else if (typeof XDomainRequest != "undefined") {
492
+ } else if (typeof XDomainRequest != 'undefined') {
493
+ // Handle IE8/IE9
494
+ // XDomainRequest only exists in IE, and is IE's way of making CORS requests.
267
495
  xmlHttp = new XDomainRequest();
268
496
  xmlHttp.open(opts.method, url);
269
497
  } else {
270
- console.log("your browser is too old to support CORS requests");
498
+ // very old browser, not supported
499
+ console.log('your browser is too old to support CORS requests');
271
500
  }
272
501
  xmlHttp.send(body);
273
502
  xmlHttp.onload = function(event) {
274
503
  if (!self._isUndefined(event) && event.target != null) {
275
- var retry = event.target.status === 0 || event.target.status === 503;
276
- var success = event.target.status === 200 || event.target.status === 201;
504
+ var retry = (event.target.status === 0 || event.target.status === 503);
505
+ var success = (event.target.status === 200 || event.target.status === 201);
277
506
  opts.callback(retry, success, event.target, event.target.response != null ? JSON.parse(event.target.response) : null);
278
507
  } else {
279
508
  opts.callback(false, true, event, JSON.parse(xmlHttp.responseText));
280
509
  }
281
510
  };
282
511
  xmlHttp.onerror = function() {
283
- opts.callback(true, false, null, {
284
- message: "Could not connect to Host"
285
- });
512
+ opts.callback(true, false, null, { 'message': 'Could not connect to Host'} );
286
513
  };
287
514
  },
515
+
516
+ /*
517
+ * Transform search param object in query string
518
+ */
288
519
  _getSearchParams: function(args, params) {
289
520
  if (this._isUndefined(args) || args == null) {
290
521
  return params;
291
522
  }
292
523
  for (var key in args) {
293
524
  if (key != null && args.hasOwnProperty(key)) {
294
- params += params.length === 0 ? "?" : "&";
295
- params += key + "=" + encodeURIComponent(Object.prototype.toString.call(args[key]) === "[object Array]" ? JSON.stringify(args[key]) : args[key]);
525
+ params += (params.length === 0) ? '?' : '&';
526
+ params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]);
296
527
  }
297
528
  }
298
529
  return params;
@@ -300,6 +531,8 @@ AlgoliaSearch.prototype = {
300
531
  _isUndefined: function(obj) {
301
532
  return obj === void 0;
302
533
  },
534
+
535
+ /// internal attributes
303
536
  applicationID: null,
304
537
  apiKey: null,
305
538
  hosts: [],
@@ -307,311 +540,803 @@ AlgoliaSearch.prototype = {
307
540
  extraHeaders: []
308
541
  };
309
542
 
543
+ /*
544
+ * Contains all the functions related to one index
545
+ * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object
546
+ */
310
547
  AlgoliaSearch.prototype.Index.prototype = {
311
- clearCache: function() {
312
- this.cache = {};
313
- },
314
- addObject: function(content, callback, objectID) {
315
- var indexObj = this;
316
- if (this.as._isUndefined(objectID)) {
317
- this.as._jsonRequest({
318
- method: "POST",
319
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName),
320
- body: content,
321
- callback: callback
322
- });
323
- } else {
324
- this.as._jsonRequest({
325
- method: "PUT",
326
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(objectID),
327
- body: content,
328
- callback: callback
329
- });
330
- }
331
- },
332
- addObjects: function(objects, callback) {
333
- var indexObj = this;
334
- var postObj = {
335
- requests: []
336
- };
337
- for (var i = 0; i < objects.length; ++i) {
338
- var request = {
339
- action: "addObject",
340
- body: objects[i]
341
- };
342
- postObj.requests.push(request);
343
- }
344
- this.as._jsonRequest({
345
- method: "POST",
346
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/batch",
347
- body: postObj,
348
- callback: callback
349
- });
350
- },
351
- getObject: function(objectID, callback, attributes) {
352
- var indexObj = this;
353
- var params = "";
354
- if (!this.as._isUndefined(attributes)) {
355
- params = "?attributes=";
356
- for (var i = 0; i < attributes.length; ++i) {
357
- if (i !== 0) {
358
- params += ",";
548
+ /*
549
+ * Clear all queries in cache
550
+ */
551
+ clearCache: function() {
552
+ this.cache = {};
553
+ },
554
+ /*
555
+ * Add an object in this index
556
+ *
557
+ * @param content contains the javascript object to add inside the index
558
+ * @param callback (optional) the result callback with two arguments:
559
+ * success: boolean set to true if the request was successfull
560
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
561
+ * @param objectID (optional) an objectID you want to attribute to this object
562
+ * (if the attribute already exist the old object will be overwrite)
563
+ */
564
+ addObject: function(content, callback, objectID) {
565
+ var indexObj = this;
566
+ if (this.as._isUndefined(objectID)) {
567
+ this.as._jsonRequest({ method: 'POST',
568
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName),
569
+ body: content,
570
+ callback: callback });
571
+ } else {
572
+ this.as._jsonRequest({ method: 'PUT',
573
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
574
+ body: content,
575
+ callback: callback });
576
+ }
577
+
578
+ },
579
+ /*
580
+ * Add several objects
581
+ *
582
+ * @param objects contains an array of objects to add
583
+ * @param callback (optional) the result callback with two arguments:
584
+ * success: boolean set to true if the request was successfull
585
+ * content: the server answer that updateAt and taskID
586
+ */
587
+ addObjects: function(objects, callback) {
588
+ var indexObj = this;
589
+ var postObj = {requests:[]};
590
+ for (var i = 0; i < objects.length; ++i) {
591
+ var request = { action: 'addObject',
592
+ body: objects[i] };
593
+ postObj.requests.push(request);
594
+ }
595
+ this.as._jsonRequest({ method: 'POST',
596
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
597
+ body: postObj,
598
+ callback: callback });
599
+ },
600
+ /*
601
+ * Get an object from this index
602
+ *
603
+ * @param objectID the unique identifier of the object to retrieve
604
+ * @param callback (optional) the result callback with two arguments
605
+ * success: boolean set to true if the request was successfull
606
+ * content: the object to retrieve or the error message if a failure occured
607
+ * @param attributes (optional) if set, contains the array of attribute names to retrieve
608
+ */
609
+ getObject: function(objectID, callback, attributes) {
610
+ var indexObj = this;
611
+ var params = '';
612
+ if (!this.as._isUndefined(attributes)) {
613
+ params = '?attributes=';
614
+ for (var i = 0; i < attributes.length; ++i) {
615
+ if (i !== 0) {
616
+ params += ',';
617
+ }
618
+ params += attributes[i];
359
619
  }
360
- params += attributes[i];
361
620
  }
362
- }
363
- this.as._jsonRequest({
364
- method: "GET",
365
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(objectID) + params,
366
- callback: callback
367
- });
368
- },
369
- partialUpdateObject: function(partialObject, callback) {
370
- var indexObj = this;
371
- this.as._jsonRequest({
372
- method: "POST",
373
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(partialObject.objectID) + "/partial",
374
- body: partialObject,
375
- callback: callback
376
- });
377
- },
378
- partialUpdateObjects: function(objects, callback) {
379
- var indexObj = this;
380
- var postObj = {
381
- requests: []
382
- };
383
- for (var i = 0; i < objects.length; ++i) {
384
- var request = {
385
- action: "partialUpdateObject",
386
- objectID: encodeURIComponent(objects[i].objectID),
387
- body: objects[i]
388
- };
389
- postObj.requests.push(request);
390
- }
391
- this.as._jsonRequest({
392
- method: "POST",
393
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/batch",
394
- body: postObj,
395
- callback: callback
396
- });
397
- },
398
- saveObject: function(object, callback) {
399
- var indexObj = this;
400
- this.as._jsonRequest({
401
- method: "PUT",
402
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(object.objectID),
403
- body: object,
404
- callback: callback
405
- });
406
- },
407
- saveObjects: function(objects, callback) {
408
- var indexObj = this;
409
- var postObj = {
410
- requests: []
411
- };
412
- for (var i = 0; i < objects.length; ++i) {
413
- var request = {
414
- action: "updateObject",
415
- objectID: encodeURIComponent(objects[i].objectID),
416
- body: objects[i]
417
- };
418
- postObj.requests.push(request);
419
- }
420
- this.as._jsonRequest({
421
- method: "POST",
422
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/batch",
423
- body: postObj,
424
- callback: callback
425
- });
426
- },
427
- deleteObject: function(objectID, callback) {
428
- if (objectID == null || objectID.length === 0) {
429
- callback(false, {
430
- message: "empty objectID"
431
- });
432
- return;
433
- }
434
- var indexObj = this;
435
- this.as._jsonRequest({
436
- method: "DELETE",
437
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(objectID),
438
- callback: callback
439
- });
440
- },
441
- search: function(query, callback, args, delay) {
442
- var indexObj = this;
443
- var params = "query=" + encodeURIComponent(query);
444
- if (!this.as._isUndefined(args) && args != null) {
445
- params = this.as._getSearchParams(args, params);
446
- }
447
- window.clearTimeout(indexObj.onDelayTrigger);
448
- if (!this.as._isUndefined(delay) && delay != null && delay > 0) {
449
- var onDelayTrigger = window.setTimeout(function() {
450
- indexObj._search(params, callback);
451
- }, delay);
452
- indexObj.onDelayTrigger = onDelayTrigger;
453
- } else {
454
- this._search(params, callback);
455
- }
456
- },
457
- browse: function(page, callback, hitsPerPage) {
458
- var indexObj = this;
459
- var params = "?page=" + page;
460
- if (!_.isUndefined(hitsPerPage)) {
461
- params += "&hitsPerPage=" + hitsPerPage;
462
- }
463
- this.as._jsonRequest({
464
- method: "GET",
465
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/browse" + params,
466
- callback: callback
467
- });
468
- },
469
- getTypeaheadTransport: function(args, valueOption) {
470
- this.typeAheadArgs = args;
471
- if (typeof valueOption !== "undefined") {
472
- this.typeAheadValueOption = valueOption;
473
- }
474
- return this;
475
- },
476
- get: function(query, processRemoteData, that, cb, suggestions) {
477
- self = this;
478
- this.search(query, function(success, content) {
479
- if (success) {
480
- for (var i = 0; i < content.hits.length; ++i) {
481
- var obj = content.hits[i], found = false;
482
- if (typeof obj.value === "undefined") {
483
- if (self.typeAheadValueOption != null) {
484
- if (typeof self.typeAheadValueOption === "function") {
485
- obj.value = self.typeAheadValueOption(obj);
486
- found = true;
487
- } else if (typeof obj[self.typeAheadValueOption] !== "undefined") {
488
- obj.value = obj[self.typeAheadValueOption];
489
- found = true;
490
- }
491
- }
492
- if (!found) {
493
- for (var propertyName in obj) {
494
- if (!found && obj.hasOwnProperty(propertyName) && typeof obj[propertyName] === "string") {
495
- obj.value = obj[propertyName];
621
+ this.as._jsonRequest({ method: 'GET',
622
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params,
623
+ callback: callback });
624
+ },
625
+
626
+ /*
627
+ * Update partially an object (only update attributes passed in argument)
628
+ *
629
+ * @param partialObject contains the javascript attributes to override, the
630
+ * object must contains an objectID attribute
631
+ * @param callback (optional) the result callback with two arguments:
632
+ * success: boolean set to true if the request was successfull
633
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
634
+ */
635
+ partialUpdateObject: function(partialObject, callback) {
636
+ var indexObj = this;
637
+ this.as._jsonRequest({ method: 'POST',
638
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial',
639
+ body: partialObject,
640
+ callback: callback });
641
+ },
642
+ /*
643
+ * Partially Override the content of several objects
644
+ *
645
+ * @param objects contains an array of objects to update (each object must contains a objectID attribute)
646
+ * @param callback (optional) the result callback with two arguments:
647
+ * success: boolean set to true if the request was successfull
648
+ * content: the server answer that updateAt and taskID
649
+ */
650
+ partialUpdateObjects: function(objects, callback) {
651
+ var indexObj = this;
652
+ var postObj = {requests:[]};
653
+ for (var i = 0; i < objects.length; ++i) {
654
+ var request = { action: 'partialUpdateObject',
655
+ objectID: objects[i].objectID,
656
+ body: objects[i] };
657
+ postObj.requests.push(request);
658
+ }
659
+ this.as._jsonRequest({ method: 'POST',
660
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
661
+ body: postObj,
662
+ callback: callback });
663
+ },
664
+ /*
665
+ * Override the content of object
666
+ *
667
+ * @param object contains the javascript object to save, the object must contains an objectID attribute
668
+ * @param callback (optional) the result callback with two arguments:
669
+ * success: boolean set to true if the request was successfull
670
+ * content: the server answer that updateAt and taskID
671
+ */
672
+ saveObject: function(object, callback) {
673
+ var indexObj = this;
674
+ this.as._jsonRequest({ method: 'PUT',
675
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID),
676
+ body: object,
677
+ callback: callback });
678
+ },
679
+ /*
680
+ * Override the content of several objects
681
+ *
682
+ * @param objects contains an array of objects to update (each object must contains a objectID attribute)
683
+ * @param callback (optional) the result callback with two arguments:
684
+ * success: boolean set to true if the request was successfull
685
+ * content: the server answer that updateAt and taskID
686
+ */
687
+ saveObjects: function(objects, callback) {
688
+ var indexObj = this;
689
+ var postObj = {requests:[]};
690
+ for (var i = 0; i < objects.length; ++i) {
691
+ var request = { action: 'updateObject',
692
+ objectID: objects[i].objectID,
693
+ body: objects[i] };
694
+ postObj.requests.push(request);
695
+ }
696
+ this.as._jsonRequest({ method: 'POST',
697
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
698
+ body: postObj,
699
+ callback: callback });
700
+ },
701
+ /*
702
+ * Delete an object from the index
703
+ *
704
+ * @param objectID the unique identifier of object to delete
705
+ * @param callback (optional) the result callback with two arguments:
706
+ * success: boolean set to true if the request was successfull
707
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
708
+ */
709
+ deleteObject: function(objectID, callback) {
710
+ if (objectID == null || objectID.length === 0) {
711
+ callback(false, { message: 'empty objectID'});
712
+ return;
713
+ }
714
+ var indexObj = this;
715
+ this.as._jsonRequest({ method: 'DELETE',
716
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
717
+ callback: callback });
718
+ },
719
+ /*
720
+ * Search inside the index using XMLHttpRequest request (Using a POST query to
721
+ * minimize number of OPTIONS queries: Cross-Origin Resource Sharing).
722
+ *
723
+ * @param query the full text query
724
+ * @param callback the result callback with two arguments:
725
+ * success: boolean set to true if the request was successfull. If false, the content contains the error.
726
+ * content: the server answer that contains the list of results.
727
+ * @param args (optional) if set, contains an object with query parameters:
728
+ * - page: (integer) Pagination parameter used to select the page to retrieve.
729
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
730
+ * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20.
731
+ * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).
732
+ * Attributes are separated with a comma (for example "name,address").
733
+ * You can also use a string array encoding (for example ["name","address"]).
734
+ * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index.
735
+ * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query.
736
+ * Attributes are separated by a comma. You can also use a string array encoding (for example ["name","address"]).
737
+ * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted.
738
+ * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted.
739
+ * A matchLevel is returned for each highlighted attribute and can contain:
740
+ * - full: if all the query terms were found in the attribute,
741
+ * - partial: if only some of the query terms were found,
742
+ * - none: if none of the query terms were found.
743
+ * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`).
744
+ * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10).
745
+ * You can also use a string array encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is computed.
746
+ * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3.
747
+ * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7.
748
+ * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute.
749
+ * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma).
750
+ * For example aroundLatLng=47.316669,5.016670).
751
+ * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision
752
+ * (for example if you set aroundPrecision=100, two objects that are distant of less than 100m will be considered as identical for "geo" ranking parameter).
753
+ * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
754
+ * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).
755
+ * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201).
756
+ * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
757
+ * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma.
758
+ * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`.
759
+ * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000.
760
+ * You can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]).
761
+ * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas.
762
+ * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3).
763
+ * You can also use a string array encoding, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3).
764
+ * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}).
765
+ * - facetFilters: filter the query by a list of facets.
766
+ * Facets are separated by commas and each facet is encoded as `attributeName:value`.
767
+ * For example: `facetFilters=category:Book,author:John%20Doe`.
768
+ * You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`).
769
+ * - facets: List of object attributes that you want to use for faceting.
770
+ * Attributes are separated with a comma (for example `"category,author"` ).
771
+ * You can also use a JSON string array encoding (for example ["category","author"]).
772
+ * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter.
773
+ * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**.
774
+ * - queryType: select how the query words are interpreted, it can be one of the following value:
775
+ * - prefixAll: all query words are interpreted as prefixes,
776
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
777
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
778
+ * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query.
779
+ * The list of words is comma separated.
780
+ * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set.
781
+ * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter,
782
+ * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results.
783
+ * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best
784
+ * one is kept and others are removed.
785
+ * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime.
786
+ */
787
+ search: function(query, callback, args, delay) {
788
+ var indexObj = this;
789
+ var params = 'query=' + encodeURIComponent(query);
790
+ if (!this.as._isUndefined(args) && args != null) {
791
+ params = this.as._getSearchParams(args, params);
792
+ }
793
+ window.clearTimeout(indexObj.onDelayTrigger);
794
+ if (!this.as._isUndefined(delay) && delay != null && delay > 0) {
795
+ var onDelayTrigger = window.setTimeout( function() {
796
+ indexObj._search(params, callback);
797
+ }, delay);
798
+ indexObj.onDelayTrigger = onDelayTrigger;
799
+ } else {
800
+ this._search(params, callback);
801
+ }
802
+ },
803
+
804
+ /*
805
+ * Browse all index content
806
+ *
807
+ * @param page Pagination parameter used to select the page to retrieve.
808
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
809
+ * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000.
810
+ */
811
+ browse: function(page, callback, hitsPerPage) {
812
+ var indexObj = this;
813
+ var params = '?page=' + page;
814
+ if (!_.isUndefined(hitsPerPage)) {
815
+ params += '&hitsPerPage=' + hitsPerPage;
816
+ }
817
+ this.as._jsonRequest({ method: 'GET',
818
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params,
819
+ callback: callback });
820
+ },
821
+
822
+ /*
823
+ * Get transport layer for Typeahead.js
824
+ * @param args (optional) if set, contains an object with query parameters (see search for details)
825
+ * @param propertyName(optional) if set, contains the name of property that will be used for
826
+ */
827
+ getTypeaheadTransport: function(args, valueOption) {
828
+ this.typeAheadArgs = args;
829
+ if (typeof valueOption !== 'undefined') {
830
+ this.typeAheadValueOption = valueOption;
831
+ }
832
+ return this;
833
+ },
834
+ /*
835
+ * Update parameter of transport layer for Typeahead.js
836
+ * @param args contains an object with query parameters (see search for details)
837
+ * @param propertyName(optional) if set, contains the name of property that will be used for
838
+ */
839
+ setTypeaheadParams: function(args, valueOption) {
840
+ this.typeAHeadArgs = args;
841
+ if (typeof valueOption !== 'undefined') {
842
+ this.typeAheadValueOption = valueOption;
843
+ }
844
+ },
845
+ // Method used by Typeahead.js.
846
+ get: function(query, processRemoteData, that, cb, suggestions) {
847
+ self = this;
848
+ this.search(query, function(success, content) {
849
+ if (success) {
850
+ for (var i = 0; i < content.hits.length; ++i) {
851
+ // Add an attribute value with the first string
852
+ var obj = content.hits[i],
853
+ found = false;
854
+
855
+ if (typeof obj.value === 'undefined') {
856
+ if (self.typeAheadValueOption != null) {
857
+ if (typeof self.typeAheadValueOption === 'function') {
858
+ obj.value = self.typeAheadValueOption(obj);
859
+ found = true;
860
+ } else if (typeof obj[self.typeAheadValueOption] !== 'undefined') {
861
+ obj.value = obj[self.typeAheadValueOption];
496
862
  found = true;
497
863
  }
498
864
  }
865
+ if (! found) {
866
+ for (var propertyName in obj) {
867
+ if (!found && obj.hasOwnProperty(propertyName) && typeof obj[propertyName] === 'string') {
868
+ obj.value = obj[propertyName];
869
+ found = true;
870
+ }
871
+ }
872
+ }
499
873
  }
874
+ suggestions.push(that._transformDatum(obj));
500
875
  }
501
- suggestions.push(that._transformDatum(obj));
876
+ cb && cb(suggestions);
502
877
  }
503
- cb && cb(suggestions);
504
- }
505
- }, self.typeAheadArgs);
506
- return true;
507
- },
508
- waitTask: function(taskID, callback) {
509
- var indexObj = this;
510
- this.as._jsonRequest({
511
- method: "GET",
512
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/task/" + taskID,
513
- callback: function(success, body) {
514
- if (success && body.status === "published") {
878
+ }, self.typeAheadArgs);
879
+ return true;
880
+ },
881
+ /*
882
+ * Wait the publication of a task on the server.
883
+ * All server task are asynchronous and you can check with this method that the task is published.
884
+ *
885
+ * @param taskID the id of the task returned by server
886
+ * @param callback the result callback with with two arguments:
887
+ * success: boolean set to true if the request was successfull
888
+ * content: the server answer that contains the list of results
889
+ */
890
+ waitTask: function(taskID, callback) {
891
+ var indexObj = this;
892
+ this.as._jsonRequest({ method: 'GET',
893
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID,
894
+ callback: function(success, body) {
895
+ if (success && body.status === 'published') {
515
896
  callback(true, body);
516
897
  } else if (success && body.pendingTask) {
517
898
  return indexObj.waitTask(taskID, callback);
518
899
  } else {
519
900
  callback(false, body);
520
901
  }
521
- }
522
- });
902
+ }});
903
+ },
904
+
905
+ /*
906
+ * This function deletes the index content. Settings and index specific API keys are kept untouched.
907
+ *
908
+ * @param callback (optional) the result callback with two arguments
909
+ * success: boolean set to true if the request was successfull
910
+ * content: the settings object or the error message if a failure occured
911
+ */
912
+ clearIndex: function(callback) {
913
+ var indexObj = this;
914
+ this.as._jsonRequest({ method: 'POST',
915
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear',
916
+ callback: callback });
917
+ },
918
+ /*
919
+ * Get settings of this index
920
+ *
921
+ * @param callback (optional) the result callback with two arguments
922
+ * success: boolean set to true if the request was successfull
923
+ * content: the settings object or the error message if a failure occured
924
+ */
925
+ getSettings: function(callback) {
926
+ var indexObj = this;
927
+ this.as._jsonRequest({ method: 'GET',
928
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
929
+ callback: callback });
930
+ },
931
+
932
+ /*
933
+ * Set settings for this index
934
+ *
935
+ * @param settigns the settings object that can contains :
936
+ * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3).
937
+ * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7).
938
+ * - hitsPerPage: (integer) the number of hits per page (default = 10).
939
+ * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects.
940
+ * If set to null, all attributes are retrieved.
941
+ * - attributesToHighlight: (array of strings) default list of attributes to highlight.
942
+ * If set to null, all indexed attributes are highlighted.
943
+ * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords).
944
+ * By default no snippet is computed. If set to null, no snippet is computed.
945
+ * - attributesToIndex: (array of strings) the list of fields you want to index.
946
+ * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results.
947
+ * This parameter has two important uses:
948
+ * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to
949
+ * retrieve it but you don't want to search in the base64 string.
950
+ * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of
951
+ * the list will be considered more important than matches in attributes further down the list.
952
+ * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable
953
+ * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"].
954
+ * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting.
955
+ * All strings in the attribute selected for faceting are extracted and added as a facet. If set to null, no attribute is used for faceting.
956
+ * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled
957
+ * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results.
958
+ * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best one is kept and others are removed.
959
+ * - ranking: (array of strings) controls the way results are sorted.
960
+ * We have six available criteria:
961
+ * - typo: sort according to number of typos,
962
+ * - geo: sort according to decreassing distance when performing a geo-location based search,
963
+ * - proximity: sort according to the proximity of query words in hits,
964
+ * - attribute: sort according to the order of attributes defined by attributesToIndex,
965
+ * - exact:
966
+ * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others.
967
+ * For example if you search for the "V" TV show, you want to find it with the "V" query and avoid to have all popular TV
968
+ * show starting by the v letter before it.
969
+ * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix).
970
+ * - custom: sort according to a user defined formula set in **customRanking** attribute.
971
+ * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"]
972
+ * - customRanking: (array of strings) lets you specify part of the ranking.
973
+ * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator.
974
+ * For example `"customRanking" => ["desc(population)", "asc(name)"]`
975
+ * - queryType: Select how the query words are interpreted, it can be one of the following value:
976
+ * - prefixAll: all query words are interpreted as prefixes,
977
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
978
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
979
+ * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>").
980
+ * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>").
981
+ * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query.
982
+ * @param callback (optional) the result callback with two arguments
983
+ * success: boolean set to true if the request was successfull
984
+ * content: the server answer or the error message if a failure occured
985
+ */
986
+ setSettings: function(settings, callback) {
987
+ var indexObj = this;
988
+ this.as._jsonRequest({ method: 'PUT',
989
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
990
+ body: settings,
991
+ callback: callback });
992
+ },
993
+ /*
994
+ * List all existing user keys associated to this index
995
+ *
996
+ * @param callback the result callback with two arguments
997
+ * success: boolean set to true if the request was successfull
998
+ * content: the server answer with user keys list or error description if success is false.
999
+ */
1000
+ listUserKeys: function(callback) {
1001
+ var indexObj = this;
1002
+ this.as._jsonRequest({ method: 'GET',
1003
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1004
+ callback: callback });
1005
+ },
1006
+ /*
1007
+ * Get ACL of a user key associated to this index
1008
+ *
1009
+ * @param callback the result callback with two arguments
1010
+ * success: boolean set to true if the request was successfull
1011
+ * content: the server answer with user keys list or error description if success is false.
1012
+ */
1013
+ getUserKeyACL: function(key, callback) {
1014
+ var indexObj = this;
1015
+ this.as._jsonRequest({ method: 'GET',
1016
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1017
+ callback: callback });
1018
+ },
1019
+ /*
1020
+ * Delete an existing user key associated to this index
1021
+ *
1022
+ * @param callback the result callback with two arguments
1023
+ * success: boolean set to true if the request was successfull
1024
+ * content: the server answer with user keys list or error description if success is false.
1025
+ */
1026
+ deleteUserKey: function(key, callback) {
1027
+ var indexObj = this;
1028
+ this.as._jsonRequest({ method: 'DELETE',
1029
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1030
+ callback: callback });
1031
+ },
1032
+ /*
1033
+ * Add an existing user key associated to this index
1034
+ *
1035
+ * @param acls the list of ACL for this key. Defined by an array of strings that
1036
+ * can contains the following values:
1037
+ * - search: allow to search (https and http)
1038
+ * - addObject: allows to add/update an object in the index (https only)
1039
+ * - deleteObject : allows to delete an existing object (https only)
1040
+ * - deleteIndex : allows to delete index content (https only)
1041
+ * - settings : allows to get index settings (https only)
1042
+ * - editSettings : allows to change index settings (https only)
1043
+ * @param callback the result callback with two arguments
1044
+ * success: boolean set to true if the request was successfull
1045
+ * content: the server answer with user keys list or error description if success is false.
1046
+ */
1047
+ addUserKey: function(acls, callback) {
1048
+ var indexObj = this;
1049
+ var aclsObject = {};
1050
+ aclsObject.acl = acls;
1051
+ this.as._jsonRequest({ method: 'POST',
1052
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1053
+ body: aclsObject,
1054
+ callback: callback });
1055
+ },
1056
+ /*
1057
+ * Add an existing user key associated to this index
1058
+ *
1059
+ * @param acls the list of ACL for this key. Defined by an array of strings that
1060
+ * can contains the following values:
1061
+ * - search: allow to search (https and http)
1062
+ * - addObject: allows to add/update an object in the index (https only)
1063
+ * - deleteObject : allows to delete an existing object (https only)
1064
+ * - deleteIndex : allows to delete index content (https only)
1065
+ * - settings : allows to get index settings (https only)
1066
+ * - editSettings : allows to change index settings (https only)
1067
+ * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
1068
+ * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
1069
+ * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
1070
+ * @param callback the result callback with two arguments
1071
+ * success: boolean set to true if the request was successfull
1072
+ * content: the server answer with user keys list or error description if success is false.
1073
+ */
1074
+ addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
1075
+ var indexObj = this;
1076
+ var aclsObject = {};
1077
+ aclsObject.acl = acls;
1078
+ aclsObject.validity = validity;
1079
+ aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
1080
+ aclsObject.maxHitsPerQuery = maxHitsPerQuery;
1081
+ this.as._jsonRequest({ method: 'POST',
1082
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1083
+ body: aclsObject,
1084
+ callback: callback });
1085
+ },
1086
+ ///
1087
+ /// Internal methods only after this line
1088
+ ///
1089
+ _search: function(params, callback) {
1090
+ this.as._jsonRequest({ cache: this.cache,
1091
+ method: 'POST',
1092
+ url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
1093
+ body: {params: params, apiKey: this.as.apiKey, appID: this.as.applicationID},
1094
+ callback: callback });
1095
+ },
1096
+
1097
+ // internal attributes
1098
+ as: null,
1099
+ indexName: null,
1100
+ cache: {},
1101
+ typeAheadArgs: null,
1102
+ typeAheadValueOption: null,
1103
+ emptyConstructor: function() {}
1104
+ };
1105
+
1106
+ /*
1107
+ * Copyright (c) 2014 Algolia
1108
+ * http://www.algolia.com/
1109
+ *
1110
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
1111
+ * of this software and associated documentation files (the "Software"), to deal
1112
+ * in the Software without restriction, including without limitation the rights
1113
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1114
+ * copies of the Software, and to permit persons to whom the Software is
1115
+ * furnished to do so, subject to the following conditions:
1116
+ *
1117
+ * The above copyright notice and this permission notice shall be included in
1118
+ * all copies or substantial portions of the Software.
1119
+ *
1120
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1121
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1122
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1123
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1124
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1125
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1126
+ * THE SOFTWARE.
1127
+ */
1128
+
1129
+ (function($) {
1130
+ var self;
1131
+
1132
+ /**
1133
+ * Algolia Search Helper providing faceting and disjunctive faceting
1134
+ * @param {AlgoliaSearch} client an AlgoliaSearch client
1135
+ * @param {string} index the index name to query
1136
+ * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets
1137
+ */
1138
+ window.AlgoliaSearchHelper = function(client, index, options) {
1139
+ /// Default options
1140
+ var defaults = {
1141
+ facets: [], // list of facets to compute
1142
+ disjunctiveFacets: [], // list of disjunctive facets to compute
1143
+ hitsPerPage: 20 // number of hits per page
1144
+ };
1145
+
1146
+ this.init(client, index, $.extend({}, defaults, options));
1147
+ self = this;
1148
+ };
1149
+
1150
+ AlgoliaSearchHelper.prototype = {
1151
+ /**
1152
+ * Initialize a new AlgoliaSearchHelper
1153
+ * @param {AlgoliaSearch} client an AlgoliaSearch client
1154
+ * @param {string} index the index name to query
1155
+ * @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets
1156
+ * @return {AlgoliaSearchHelper}
1157
+ */
1158
+ init: function(client, index, options) {
1159
+ this.client = client;
1160
+ this.index = index;
1161
+ this.options = options;
1162
+ this.page = 0;
1163
+ this.refinements = {};
1164
+ this.disjunctiveRefinements = {};
523
1165
  },
524
- clearIndex: function(callback) {
525
- var indexObj = this;
526
- this.as._jsonRequest({
527
- method: "POST",
528
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/clear",
529
- callback: callback
530
- });
1166
+
1167
+ /**
1168
+ * Perform a query
1169
+ * @param {string} q the user query
1170
+ * @param {function} searchCallback the result callback called with two arguments:
1171
+ * success: boolean set to true if the request was successfull
1172
+ * content: the query answer with an extra 'disjunctiveFacets' attribute
1173
+ */
1174
+ search: function(q, searchCallback) {
1175
+ this.q = q;
1176
+ this.searchCallback = searchCallback;
1177
+ this.page = 0;
1178
+ this.refinements = {};
1179
+ this.disjunctiveRefinements = {};
1180
+ this._search();
531
1181
  },
532
- getSettings: function(callback) {
533
- var indexObj = this;
534
- this.as._jsonRequest({
535
- method: "GET",
536
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/settings",
537
- callback: callback
538
- });
1182
+
1183
+ /**
1184
+ * Toggle refinement state of a facet
1185
+ * @param {string} facet the facet to refine
1186
+ * @param {string} value the associated value
1187
+ * @return {boolean} true if the facet has been found
1188
+ */
1189
+ toggleRefine: function(facet, value) {
1190
+ for (var i = 0; i < this.options.facets.length; ++i) {
1191
+ if (this.options.facets[i] == facet) {
1192
+ var refinement = facet + ':' + value;
1193
+ this.refinements[refinement] = !this.refinements[refinement];
1194
+ this.page = 0;
1195
+ this._search();
1196
+ return true;
1197
+ }
1198
+ }
1199
+ this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {};
1200
+ for (var j = 0; j < this.options.disjunctiveFacets.length; ++j) {
1201
+ if (this.options.disjunctiveFacets[j] == facet) {
1202
+ this.disjunctiveRefinements[facet][value] = !this.disjunctiveRefinements[facet][value];
1203
+ this.page = 0;
1204
+ this._search();
1205
+ return true;
1206
+ }
1207
+ }
1208
+ return false;
539
1209
  },
540
- setSettings: function(settings, callback) {
541
- var indexObj = this;
542
- this.as._jsonRequest({
543
- method: "PUT",
544
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/settings",
545
- body: settings,
546
- callback: callback
547
- });
1210
+
1211
+ /**
1212
+ * Check the refinement state of a facet
1213
+ * @param {string} facet the facet
1214
+ * @param {string} value the associated value
1215
+ * @return {boolean} true if refined
1216
+ */
1217
+ isRefined: function(facet, value) {
1218
+ var refinement = facet + ':' + value;
1219
+ if (this.refinements[refinement]) {
1220
+ return true;
1221
+ }
1222
+ if (this.disjunctiveRefinements[facet] && this.disjunctiveRefinements[facet][value]) {
1223
+ return true;
1224
+ }
1225
+ return false;
548
1226
  },
549
- listUserKeys: function(callback) {
550
- var indexObj = this;
551
- this.as._jsonRequest({
552
- method: "GET",
553
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys",
554
- callback: callback
555
- });
1227
+
1228
+ /**
1229
+ * Go to next page
1230
+ */
1231
+ nextPage: function() {
1232
+ this._gotoPage(this.page + 1);
556
1233
  },
557
- getUserKeyACL: function(key, callback) {
558
- var indexObj = this;
559
- this.as._jsonRequest({
560
- method: "GET",
561
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys/" + key,
562
- callback: callback
563
- });
1234
+
1235
+ /**
1236
+ * Go to previous page
1237
+ */
1238
+ previousPage: function() {
1239
+ if (this.page > 0) {
1240
+ this._gotoPage(this.page - 1);
1241
+ }
564
1242
  },
565
- deleteUserKey: function(key, callback) {
566
- var indexObj = this;
567
- this.as._jsonRequest({
568
- method: "DELETE",
569
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys/" + key,
570
- callback: callback
571
- });
1243
+
1244
+ ///////////// PRIVATE
1245
+
1246
+ /**
1247
+ * Goto a page
1248
+ * @param {integer} page The page number
1249
+ */
1250
+ _gotoPage: function(page) {
1251
+ this.page = page;
1252
+ this._search();
572
1253
  },
573
- addUserKey: function(acls, callback) {
574
- var indexObj = this;
575
- var aclsObject = {};
576
- aclsObject.acl = acls;
577
- this.as._jsonRequest({
578
- method: "POST",
579
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys",
580
- body: aclsObject,
581
- callback: callback
582
- });
1254
+
1255
+ /**
1256
+ * Perform the underlying queries
1257
+ */
1258
+ _search: function() {
1259
+ this.client.startQueriesBatch();
1260
+ this.client.addQueryInBatch(this.index, this.q, this._getHitsSearchParams());
1261
+ for (var i = 0; i < this.options.disjunctiveFacets.length; ++i) {
1262
+ this.client.addQueryInBatch(this.index, this.q, this._getDisjunctiveFacetSearchParams(this.options.disjunctiveFacets[i]));
1263
+ }
1264
+ this.client.sendQueriesBatch(function(success, content) {
1265
+ if (!success) {
1266
+ self.searchCallback(false, content);
1267
+ return;
1268
+ }
1269
+ var aggregatedAnswer = content.results[0];
1270
+ aggregatedAnswer.disjunctiveFacets = {};
1271
+ for (var i = 1; i < content.results.length; ++i) {
1272
+ for (var facet in content.results[i].facets) {
1273
+ aggregatedAnswer.disjunctiveFacets[facet] = content.results[i].facets[facet];
1274
+ if (self.disjunctiveRefinements[facet]) {
1275
+ for (var value in self.disjunctiveRefinements[facet]) {
1276
+ if (!aggregatedAnswer.disjunctiveFacets[facet][value] && self.disjunctiveRefinements[facet][value]) {
1277
+ aggregatedAnswer.disjunctiveFacets[facet][value] = 0;
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ self.searchCallback(true, aggregatedAnswer);
1284
+ });
583
1285
  },
584
- addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
585
- var indexObj = this;
586
- var aclsObject = {};
587
- aclsObject.acl = acls;
588
- aclsObject.validity = validity;
589
- aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
590
- aclsObject.maxHitsPerQuery = maxHitsPerQuery;
591
- this.as._jsonRequest({
592
- method: "POST",
593
- url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys",
594
- body: aclsObject,
595
- callback: callback
596
- });
597
- },
598
- _search: function(params, callback) {
599
- this.as._jsonRequest({
600
- cache: this.cache,
601
- method: "POST",
602
- url: "/1/indexes/" + encodeURIComponent(this.indexName) + "/query",
603
- body: {
604
- params: params,
605
- apiKey: this.as.apiKey,
606
- appID: this.as.applicationID
607
- },
608
- callback: callback
609
- });
610
- },
611
- as: null,
612
- indexName: null,
613
- cache: {},
614
- typeAheadArgs: null,
615
- typeAheadValueOption: null,
616
- emptyConstructor: function() {}
617
- };
1286
+
1287
+ /**
1288
+ * Build search parameters used to fetch hits
1289
+ * @return {hash}
1290
+ */
1291
+ _getHitsSearchParams: function() {
1292
+ return {
1293
+ hitsPerPage: this.options.hitsPerPage,
1294
+ page: this.page,
1295
+ facets: this.options.facets,
1296
+ facetFilters: this._getFacetFilters()
1297
+ };
1298
+ },
1299
+
1300
+ /**
1301
+ * Build search parameters used to fetch a disjunctive facet
1302
+ * @param {string} facet the associated facet name
1303
+ * @return {hash}
1304
+ */
1305
+ _getDisjunctiveFacetSearchParams: function(facet) {
1306
+ return {
1307
+ hitsPerPage: 1,
1308
+ page: 0,
1309
+ facets: facet,
1310
+ facetFilters: this._getFacetFilters(facet)
1311
+ };
1312
+ },
1313
+
1314
+ /**
1315
+ * Build facetFilters parameter based on current refinements
1316
+ * @param {string} facet if set, the current disjunctive facet
1317
+ * @return {hash}
1318
+ */
1319
+ _getFacetFilters: function(facet) {
1320
+ var facetFilters = [];
1321
+ for (var refinement in this.refinements) {
1322
+ if (this.refinements[refinement]) {
1323
+ facetFilters.push(refinement);
1324
+ }
1325
+ }
1326
+ for (var disjunctiveRefinement in this.disjunctiveRefinements) {
1327
+ if (disjunctiveRefinement != facet) {
1328
+ var refinements = [];
1329
+ for (var value in this.disjunctiveRefinements[disjunctiveRefinement]) {
1330
+ if (this.disjunctiveRefinements[disjunctiveRefinement][value]) {
1331
+ refinements.push(disjunctiveRefinement + ':' + value);
1332
+ }
1333
+ }
1334
+ if (refinements.length > 0) {
1335
+ facetFilters.push(refinements);
1336
+ }
1337
+ }
1338
+ }
1339
+ return facetFilters;
1340
+ }
1341
+ };
1342
+ })(jQuery);