algoliasearch-rails 1.7.1 → 1.7.2

Sign up to get free protection for your applications and to get access to all the features.
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);