algoliasearch-rails 1.11.17 → 1.11.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog +4 -0
  3. data/README.md +48 -6
  4. data/VERSION +1 -1
  5. data/algoliasearch-rails.gemspec +22 -6
  6. data/vendor/assets/javascripts/algolia/algoliasearch.angular.js +1446 -1378
  7. data/vendor/assets/javascripts/algolia/algoliasearch.angular.min.js +2 -2
  8. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.js +1446 -1378
  9. data/vendor/assets/javascripts/algolia/algoliasearch.jquery.min.js +2 -2
  10. data/vendor/assets/javascripts/algolia/algoliasearch.js +1446 -1378
  11. data/vendor/assets/javascripts/algolia/algoliasearch.min.js +2 -2
  12. data/vendor/assets/javascripts/algolia/bloodhound.min.js +7 -0
  13. data/vendor/assets/javascripts/algolia/typeahead.jquery.min.js +7 -0
  14. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.js +2667 -0
  15. data/vendor/assets/javascripts/algolia/v2/algoliasearch.angular.min.js +7 -0
  16. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.js +2667 -0
  17. data/vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.min.js +7 -0
  18. data/vendor/assets/javascripts/algolia/v2/algoliasearch.js +2653 -0
  19. data/vendor/assets/javascripts/algolia/v2/algoliasearch.min.js +7 -0
  20. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.js +1717 -0
  21. data/vendor/assets/javascripts/algolia/v3/algoliasearch.angular.min.js +37 -0
  22. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.js +1702 -0
  23. data/vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.min.js +37 -0
  24. data/vendor/assets/javascripts/algolia/v3/algoliasearch.js +2732 -0
  25. data/vendor/assets/javascripts/algolia/v3/algoliasearch.min.js +50 -0
  26. metadata +36 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 55fbc5ac8483cb4609a0102a438d0d988f14950e
4
- data.tar.gz: 487ba5769e40b3dee94861d2478d4809ce018c7a
3
+ metadata.gz: b90eda1033e39094342631bc26f4356b2fb77723
4
+ data.tar.gz: b2b8a9acdc2814b526f69c30c7bb85de3998f039
5
5
  SHA512:
6
- metadata.gz: 007e97d90dff45d6fecb0e769bfd36f87d5721d5d5861a9225dc4a3758a0be167244019380fc753e3208d4ed2be157188dc49b8d0789a93db1ed88d16a6f720f
7
- data.tar.gz: 68dbf288873f169a4f135f7ad4634266a27cb6a8123ed4624cce3f0c890e5fa9e9936dd45afcec4645e0be92eef62e46d37ea670964666a87f8e70543365eb34
6
+ metadata.gz: fae998c2943cbaa65c7fd588a96aaee86fe7b5c8937dda84c3708e5c09fe6c94367361230a3e264023f4d6d585cc3b7641ac41b11b14a7e4b4cc689e23cfc7c3
7
+ data.tar.gz: d079747d3a7fcc582b4dce2ec76a225abf6c9c46dd8f340d893448e67e7d87d73a231ab2bb89b3c5631993f5dc9dc6964191fc9473f9cdac74b8382ba2104ef9
data/ChangeLog CHANGED
@@ -1,5 +1,9 @@
1
1
  CHANGELOG
2
2
 
3
+ 2015-03-31 1.11.18
4
+
5
+ * Embed AlgoliaSearch JS API client v3 as well. Default is still the v2 to keep the backward compatibility.
6
+
3
7
  2015-03-08 1.11.17
4
8
 
5
9
  * Add missing index settings (including removeWordsIfNoResults, unretrievableAttributes and ignorePlurals)
data/README.md CHANGED
@@ -14,6 +14,7 @@ Table of Content
14
14
  1. [Install](#install)
15
15
  1. [Setup](#setup)
16
16
  1. [Quick Start](#quick-start)
17
+ 1. [Ranking & Relevance](#ranking--relevance)
17
18
  1. [Options](#options)
18
19
  1. [Configuration example](#configuration-example)
19
20
  1. [Indexing](#indexing)
@@ -105,15 +106,56 @@ class Product < ActiveRecord::Base
105
106
  end
106
107
  ```
107
108
 
109
+ #### Ranking & Relevance
110
+
111
+ We provide many ways to configure your index allowing you to tune your overall index relevancy. The most important ones are the **searchable attributes** and the attributes reflecting **record popularity**.
112
+
113
+ ```ruby
114
+ class Product < ActiveRecord::Base
115
+ include AlgoliaSearch
116
+
117
+ algoliasearch do
118
+ # list of attribute used to build an Algolia record
119
+ attributes :title, :subtitle, :description, :likes_count, :seller_name
120
+
121
+ # the attributesToIndex` setting defines the attributes
122
+ # you want to search in: here `title`, `subtitle` & `description`.
123
+ # You need to list them by order of importance. `description` is tagged as
124
+ # `unordered` to avoid taking the position of a match into account in that attribute.
125
+ attributesToIndex ['title', 'subtitle', 'unordered(description)']
126
+
127
+ # the `customRanking` setting defines the ranking criteria use to compare two matching
128
+ # records in case their text-relevance is equal. It should reflect your record popularity.
129
+ customRanking ['desc(likes_count)']
130
+ end
131
+
132
+ end
133
+ ```
134
+
108
135
  #### Frontend Search (realtime experience)
109
136
 
137
+ Traditional search implementations tend to have search logic and functionality on the backend. This made sense when the search experience consisted of a user entering a search query, executing that search, and then being redirected to a search result page.
138
+
139
+ Implementing search on the backend is no longer necessary. In fact, in most cases it is harmful to performance because of added network and processing latency. We highly recommend the usage of our [JavaScript API Client](https://github.com/algolia/algoliasearch-client-js) issuing all search requests directly from the end user's browser, mobile device, or client. It will reduce the overall search latency while offloading your servers at the same time.
110
140
 
111
- We recommend the usage of our [JavaScript API Client](https://github.com/algolia/algoliasearch-client-js) to perform queries. The JS API client is part of the gem, just require ```algolia/algoliasearch.min``` somewhere in your JavaScript manifest, for example in ```application.js``` if you are using Rails 3.1+:
141
+ The JS API client is part of the gem, just require ```algolia/v3/algoliasearch.min``` somewhere in your JavaScript manifest, for example in ```application.js``` if you are using Rails 3.1+:
112
142
 
113
143
  ```javascript
114
- //= require algolia/algoliasearch.min
144
+ //= require algolia/v3/algoliasearch.min
115
145
  ```
116
146
 
147
+ Then in your JavaScript code you can do:
148
+
149
+ ```js
150
+ var client = new AlgoliaSearch('ApplicationID', 'Search-Only-API-Key');
151
+ var index = client.initIndex('YourIndexName');
152
+ index.search('something', function(success, hits) {
153
+ console.log(success, hits)
154
+ }, { hitsPerPage: 10, page: 0 });
155
+ ```
156
+
157
+ **We recently (March 2015) released a new version (V3) of our JavaScript client, if you were using our previous version (V2), [read the migration guide](https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x)**
158
+
117
159
  #### Backend Search
118
160
 
119
161
  A search returns ORM-compliant objects reloading them from your database.
@@ -393,7 +435,7 @@ class Item < ActiveRecord::Base
393
435
 
394
436
  algoliasearch per_environment: true do
395
437
  # the list of attributes sent to Algolia's API
396
- attribute :created_at, :title, :url, :author, :points, :story_text, :comment_text, :author, :num_comments, :story_id, :story_title, :
438
+ attribute :created_at, :title, :url, :author, :points, :story_text, :comment_text, :author, :num_comments, :story_id, :story_title
397
439
 
398
440
  # integer version of the created_at datetime field, to use numerical filtering
399
441
  attribute :created_at_i do
@@ -703,10 +745,10 @@ At query time, specify <code>{ aroundLatLng: "37.33, -121.89", aroundRadius: 500
703
745
  Typeahead UI
704
746
  -------------
705
747
 
706
- Require ```algolia/algoliasearch.min``` (see [algoliasearch-client-js](https://github.com/algolia/algoliasearch-client-js)) and ```algolia/typeahead.jquery.js``` somewhere in your JavaScript manifest, for example in ```application.js``` if you are using Rails 3.1+:
748
+ Require ```algolia/v3/algoliasearch.min``` (see [algoliasearch-client-js](https://github.com/algolia/algoliasearch-client-js)) and ```algolia/typeahead.jquery.js``` somewhere in your JavaScript manifest, for example in ```application.js``` if you are using Rails 3.1+:
707
749
 
708
750
  ```javascript
709
- //= require algolia/algoliasearch.min
751
+ //= require algolia/v3/algoliasearch.min
710
752
  //= require algolia/typeahead.jquery
711
753
  ```
712
754
 
@@ -723,7 +765,7 @@ Turns any ```input[type="text"]``` element into a typeahead, for example:
723
765
 
724
766
  <script type="text/javascript">
725
767
  $(document).ready(function() {
726
- var client = new AlgoliaSearch('YourApplicationID', 'SearchOnlyApplicationKey');
768
+ var client = algoliasearch('YourApplicationID', 'SearchOnlyApplicationKey');
727
769
  var template = Hogan.compile('{{{_highlightResult.email.value}}} ({{{_highlightResult.first_name.value}}} {{{_highlightResult.last_name.value}}})');
728
770
  $('input#user_email').typeahead(null, {
729
771
  source: client.initIndex('<%= Contact.index_name %>').ttAdapter(),
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.11.17
1
+ 1.11.18
@@ -38,16 +38,32 @@ Gem::Specification.new do |s|
38
38
  "lib/algoliasearch/utilities.rb",
39
39
  "spec/spec_helper.rb",
40
40
  "spec/utilities_spec.rb",
41
- "vendor/assets/javascripts/algolia/algoliasearch.js",
42
- "vendor/assets/javascripts/algolia/algoliasearch.min.js",
43
- "vendor/assets/javascripts/algolia/algoliasearch.jquery.js",
44
- "vendor/assets/javascripts/algolia/algoliasearch.jquery.min.js",
45
41
  "vendor/assets/javascripts/algolia/algoliasearch.angular.js",
46
42
  "vendor/assets/javascripts/algolia/algoliasearch.angular.min.js",
47
- "vendor/assets/javascripts/algolia/typeahead.jquery.js",
43
+ "vendor/assets/javascripts/algolia/algoliasearch.jquery.js",
44
+ "vendor/assets/javascripts/algolia/algoliasearch.jquery.min.js",
45
+ "vendor/assets/javascripts/algolia/algoliasearch.js",
46
+ "vendor/assets/javascripts/algolia/algoliasearch.min.js",
47
+ "vendor/assets/javascripts/algolia/bloodhound.js",
48
+ "vendor/assets/javascripts/algolia/bloodhound.min.js",
48
49
  "vendor/assets/javascripts/algolia/typeahead.bundle.js",
49
50
  "vendor/assets/javascripts/algolia/typeahead.bundle.min.js",
50
- "vendor/assets/javascripts/algolia/bloodhound.js"
51
+ "vendor/assets/javascripts/algolia/typeahead.jquery.js",
52
+ "vendor/assets/javascripts/algolia/typeahead.jquery.min.js",
53
+ "vendor/assets/javascripts/algolia/v2",
54
+ "vendor/assets/javascripts/algolia/v2/algoliasearch.angular.js",
55
+ "vendor/assets/javascripts/algolia/v2/algoliasearch.angular.min.js",
56
+ "vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.js",
57
+ "vendor/assets/javascripts/algolia/v2/algoliasearch.jquery.min.js",
58
+ "vendor/assets/javascripts/algolia/v2/algoliasearch.js",
59
+ "vendor/assets/javascripts/algolia/v2/algoliasearch.min.js",
60
+ "vendor/assets/javascripts/algolia/v3",
61
+ "vendor/assets/javascripts/algolia/v3/algoliasearch.angular.js",
62
+ "vendor/assets/javascripts/algolia/v3/algoliasearch.angular.min.js",
63
+ "vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.js",
64
+ "vendor/assets/javascripts/algolia/v3/algoliasearch.jquery.min.js",
65
+ "vendor/assets/javascripts/algolia/v3/algoliasearch.js",
66
+ "vendor/assets/javascripts/algolia/v3/algoliasearch.min.js"
51
67
  ]
52
68
  s.homepage = "http://github.com/algolia/algoliasearch-rails"
53
69
  s.licenses = ["MIT"]
@@ -21,7 +21,7 @@
21
21
  * THE SOFTWARE.
22
22
  */
23
23
 
24
- var ALGOLIA_VERSION = '2.9.2';
24
+ var ALGOLIA_VERSION = '2.9.4';
25
25
 
26
26
  /*
27
27
  * Copyright (c) 2013 Algolia
@@ -58,892 +58,959 @@ var ALGOLIA_VERSION = '2.9.2';
58
58
  * - dsnHost (optional) override the automatic computation of dsn hostname
59
59
  */
60
60
  var AlgoliaSearch = function(applicationID, apiKey, methodOrOptions, resolveDNS, hosts) {
61
- var self = this;
62
- this.applicationID = applicationID;
63
- this.apiKey = apiKey;
64
- this.dsn = true;
65
- this.dsnHost = null;
66
- this.hosts = [];
67
- this.currentHostIndex = 0;
68
- this.requestTimeoutInMs = 2000;
69
- this.extraHeaders = [];
70
- this.jsonp = null;
71
- this.options = {};
72
-
73
- // make sure every client instance has it's own cache
74
- this.cache = {};
75
-
76
- var method;
77
- var tld = 'net';
78
- if (typeof methodOrOptions === 'string') { // Old initialization
79
- method = methodOrOptions;
80
- } else {
81
- // Take all option from the hash
82
- var options = methodOrOptions || {};
83
- this.options = options;
84
- if (!this._isUndefined(options.method)) {
85
- method = options.method;
86
- }
87
- if (!this._isUndefined(options.tld)) {
88
- tld = options.tld;
89
- }
90
- if (!this._isUndefined(options.dsn)) {
91
- this.dsn = options.dsn;
92
- }
93
- if (!this._isUndefined(options.hosts)) {
94
- hosts = options.hosts;
95
- }
96
- if (!this._isUndefined(options.dsnHost)) {
97
- this.dsnHost = options.dsnHost;
98
- }
99
- if (!this._isUndefined(options.requestTimeoutInMs)) {
100
- this.requestTimeoutInMs = +options.requestTimeoutInMs;
101
- }
102
- if (!this._isUndefined(options.jsonp)) {
103
- this.jsonp = options.jsonp;
104
- }
61
+ var self = this;
62
+ this.applicationID = applicationID;
63
+ this.apiKey = apiKey;
64
+ this.dsn = true;
65
+ this.dsnHost = null;
66
+ this.hosts = [];
67
+ this.currentHostIndex = 0;
68
+ this.requestTimeoutInMs = 2000;
69
+ this.extraHeaders = [];
70
+ this.jsonp = null;
71
+ this.options = {};
72
+
73
+ // make sure every client instance has it's own cache
74
+ this.cache = {};
75
+
76
+ var method;
77
+ var tld = 'net';
78
+ if (typeof methodOrOptions === 'string') { // Old initialization
79
+ method = methodOrOptions;
80
+ } else {
81
+ // Take all option from the hash
82
+ var options = methodOrOptions || {};
83
+ this.options = options;
84
+ if (!this._isUndefined(options.method)) {
85
+ method = options.method;
105
86
  }
106
- // If hosts is undefined, initialize it with applicationID
107
- if (this._isUndefined(hosts)) {
108
- hosts = [
109
- this.applicationID + '-1.algolia.' + tld,
110
- this.applicationID + '-2.algolia.' + tld,
111
- this.applicationID + '-3.algolia.' + tld
112
- ];
113
- }
114
- // detect is we use http or https
115
- this.host_protocol = 'http://';
116
- if (this._isUndefined(method) || method === null) {
117
- this.host_protocol = ('https:' == document.location.protocol ? 'https' : 'http') + '://';
118
- } else if (method === 'https' || method === 'HTTPS') {
119
- this.host_protocol = 'https://';
120
- }
121
- // Add hosts in random order
122
- for (var i = 0; i < hosts.length; ++i) {
123
- if (Math.random() > 0.5) {
124
- this.hosts.reverse();
125
- }
126
- this.hosts.push(this.host_protocol + hosts[i]);
87
+ if (!this._isUndefined(options.tld)) {
88
+ tld = options.tld;
127
89
  }
128
- if (Math.random() > 0.5) {
129
- this.hosts.reverse();
90
+ if (!this._isUndefined(options.dsn)) {
91
+ this.dsn = options.dsn;
130
92
  }
131
- // then add Distributed Search Network host if there is one
132
- if (this.dsn || this.dsnHost != null) {
133
- if (this.dsnHost) {
134
- this.hosts.unshift(this.host_protocol + this.dsnHost);
135
- } else {
136
- this.hosts.unshift(this.host_protocol + this.applicationID + '-dsn.algolia.' + tld);
137
- }
93
+ if (!this._isUndefined(options.hosts)) {
94
+ hosts = options.hosts;
95
+ }
96
+ if (!this._isUndefined(options.dsnHost)) {
97
+ this.dsnHost = options.dsnHost;
98
+ }
99
+ if (!this._isUndefined(options.requestTimeoutInMs)) {
100
+ this.requestTimeoutInMs = +options.requestTimeoutInMs;
101
+ }
102
+ if (!this._isUndefined(options.jsonp)) {
103
+ this.jsonp = options.jsonp;
138
104
  }
139
- // angular dependencies injection
140
- if (this.options.angular) {
141
- this.options.angular.$injector.invoke(['$http', '$q', function ($http, $q) {
142
- self.options.angular.$q = $q;
143
- self.options.angular.$http = $http;
144
- }]);
105
+ }
106
+ // If hosts is undefined, initialize it with applicationID
107
+ if (this._isUndefined(hosts)) {
108
+ hosts = [
109
+ this.applicationID + '-1.algolia.' + tld,
110
+ this.applicationID + '-2.algolia.' + tld,
111
+ this.applicationID + '-3.algolia.' + tld
112
+ ];
113
+ }
114
+ // detect is we use http or https
115
+ this.host_protocol = 'http://';
116
+ if (this._isUndefined(method) || method === null) {
117
+ this.host_protocol = ('https:' == document.location.protocol ? 'https' : 'http') + '://';
118
+ } else if (method === 'https' || method === 'HTTPS') {
119
+ this.host_protocol = 'https://';
120
+ }
121
+ // Add hosts in random order
122
+ for (var i = 0; i < hosts.length; ++i) {
123
+ if (Math.random() > 0.5) {
124
+ this.hosts.reverse();
125
+ }
126
+ this.hosts.push(this.host_protocol + hosts[i]);
127
+ }
128
+ if (Math.random() > 0.5) {
129
+ this.hosts.reverse();
130
+ }
131
+ // then add Distributed Search Network host if there is one
132
+ if (this.dsn || this.dsnHost != null) {
133
+ if (this.dsnHost) {
134
+ this.hosts.unshift(this.host_protocol + this.dsnHost);
135
+ } else {
136
+ this.hosts.unshift(this.host_protocol + this.applicationID + '-dsn.algolia.' + tld);
145
137
  }
138
+ }
139
+ // angular dependencies injection
140
+ if (this.options.angular) {
141
+ this.options.angular.$injector.invoke(['$http', '$q', function ($http, $q) {
142
+ self.options.angular.$q = $q;
143
+ self.options.angular.$http = $http;
144
+ }]);
145
+ }
146
146
  };
147
147
 
148
+ // This holds the number of JSONP requests done accross clients
149
+ // It's used as part of the ?callback=JSONP_$JSONPCounter when we do JSONP requests
150
+ AlgoliaSearch.JSONPCounter = 0;
151
+
148
152
  function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) {
149
153
 
150
- function _getHitExplanationForOneAttr_recurse(obj, foundWords) {
154
+ function _getHitExplanationForOneAttr_recurse(obj, foundWords) {
155
+ var res = [];
156
+ if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) {
157
+ var match = false;
158
+ for (var j = 0; j < obj.matchedWords.length; ++j) {
159
+ var word = obj.matchedWords[j];
160
+ if (!(word in foundWords)) {
161
+ foundWords[word] = 1;
162
+ match = true;
163
+ }
164
+ }
165
+ if (match) {
166
+ res.push(obj.value);
167
+ }
168
+ } else if (Object.prototype.toString.call(obj) === '[object Array]') {
169
+ for (var i = 0; i < obj.length; ++i) {
170
+ var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords);
171
+ res = res.concat(array);
172
+ }
173
+ } else if (typeof obj === 'object') {
174
+ for (var prop in obj) {
175
+ if (obj.hasOwnProperty(prop)){
176
+ res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords));
177
+ }
178
+ }
179
+ }
180
+ return res;
181
+ }
182
+
183
+ function _getHitExplanationForOneAttr(hit, foundWords, attr) {
184
+ var base = hit._highlightResult || hit;
185
+ if (attr.indexOf('.') === -1) {
186
+ if (attr in base) {
187
+ return _getHitExplanationForOneAttr_recurse(base[attr], foundWords);
188
+ }
189
+ return [];
190
+ }
191
+ var array = attr.split('.');
192
+ var obj = base;
193
+ for (var i = 0; i < array.length; ++i) {
194
+ if (Object.prototype.toString.call(obj) === '[object Array]') {
151
195
  var res = [];
152
- if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) {
153
- var match = false;
154
- for (var j = 0; j < obj.matchedWords.length; ++j) {
155
- var word = obj.matchedWords[j];
156
- if (!(word in foundWords)) {
157
- foundWords[word] = 1;
158
- match = true;
159
- }
160
- }
161
- if (match) {
162
- res.push(obj.value);
163
- }
164
- } else if (Object.prototype.toString.call(obj) === '[object Array]') {
165
- for (var i = 0; i < obj.length; ++i) {
166
- var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords);
167
- res = res.concat(array);
168
- }
169
- } else if (typeof obj === 'object') {
170
- for (var prop in obj) {
171
- if (obj.hasOwnProperty(prop)){
172
- res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords));
173
- }
174
- }
196
+ for (var j = 0; j < obj.length; ++j) {
197
+ res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.')));
175
198
  }
176
199
  return res;
200
+ }
201
+ if (array[i] in obj) {
202
+ obj = obj[array[i]];
203
+ } else {
204
+ return [];
205
+ }
206
+ }
207
+ return _getHitExplanationForOneAttr_recurse(obj, foundWords);
208
+ }
209
+
210
+ var res = {};
211
+ var foundWords = {};
212
+ var title = _getHitExplanationForOneAttr(hit, foundWords, titleAttribute);
213
+ res.title = (title.length > 0) ? title[0] : '';
214
+ res.subtitles = [];
215
+
216
+ if (typeof otherAttributes !== 'undefined') {
217
+ for (var i = 0; i < otherAttributes.length; ++i) {
218
+ var attr = _getHitExplanationForOneAttr(hit, foundWords, otherAttributes[i]);
219
+ for (var j = 0; j < attr.length; ++j) {
220
+ res.subtitles.push({ attr: otherAttributes[i], value: attr[j] });
221
+ }
177
222
  }
223
+ }
224
+ return res;
225
+ }
178
226
 
179
- function _getHitExplanationForOneAttr(hit, foundWords, attr) {
180
- var base = hit._highlightResult || hit;
181
- if (attr.indexOf('.') === -1) {
182
- if (attr in base) {
183
- return _getHitExplanationForOneAttr_recurse(base[attr], foundWords);
184
- }
185
- return [];
186
- }
187
- var array = attr.split('.');
188
- var obj = base;
189
- for (var i = 0; i < array.length; ++i) {
190
- if (Object.prototype.toString.call(obj) === '[object Array]') {
191
- var res = [];
192
- for (var j = 0; j < obj.length; ++j) {
193
- res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.')));
194
- }
195
- return res;
196
- }
197
- if (array[i] in obj) {
198
- obj = obj[array[i]];
199
- } else {
200
- return [];
201
- }
202
- }
203
- return _getHitExplanationForOneAttr_recurse(obj, foundWords);
227
+
228
+ AlgoliaSearch.prototype = {
229
+ /*
230
+ * Delete an index
231
+ *
232
+ * @param indexName the name of index to delete
233
+ * @param callback the result callback with two arguments
234
+ * success: boolean set to true if the request was successfull
235
+ * content: the server answer that contains the task ID
236
+ */
237
+ deleteIndex: function(indexName, callback) {
238
+ return this._jsonRequest({ method: 'DELETE',
239
+ url: '/1/indexes/' + encodeURIComponent(indexName),
240
+ callback: callback });
241
+ },
242
+ /**
243
+ * Move an existing index.
244
+ * @param srcIndexName the name of index to copy.
245
+ * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
246
+ * @param callback the result callback with two arguments
247
+ * success: boolean set to true if the request was successfull
248
+ * content: the server answer that contains the task ID
249
+ */
250
+ moveIndex: function(srcIndexName, dstIndexName, callback) {
251
+ var postObj = {operation: 'move', destination: dstIndexName};
252
+ return this._jsonRequest({ method: 'POST',
253
+ url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
254
+ body: postObj,
255
+ callback: callback });
256
+
257
+ },
258
+ /**
259
+ * Copy an existing index.
260
+ * @param srcIndexName the name of index to copy.
261
+ * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
262
+ * @param callback the result callback with two arguments
263
+ * success: boolean set to true if the request was successfull
264
+ * content: the server answer that contains the task ID
265
+ */
266
+ copyIndex: function(srcIndexName, dstIndexName, callback) {
267
+ var postObj = {operation: 'copy', destination: dstIndexName};
268
+ return this._jsonRequest({ method: 'POST',
269
+ url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
270
+ body: postObj,
271
+ callback: callback });
272
+ },
273
+ /**
274
+ * Return last log entries.
275
+ * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry).
276
+ * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000.
277
+ * @param callback the result callback with two arguments
278
+ * success: boolean set to true if the request was successfull
279
+ * content: the server answer that contains the task ID
280
+ */
281
+ getLogs: function(callback, offset, length) {
282
+ if (this._isUndefined(offset)) {
283
+ offset = 0;
284
+ }
285
+ if (this._isUndefined(length)) {
286
+ length = 10;
204
287
  }
205
288
 
206
- var res = {};
207
- var foundWords = {};
208
- var title = _getHitExplanationForOneAttr(hit, foundWords, titleAttribute);
209
- res.title = (title.length > 0) ? title[0] : '';
210
- res.subtitles = [];
289
+ return this._jsonRequest({ method: 'GET',
290
+ url: '/1/logs?offset=' + offset + '&length=' + length,
291
+ callback: callback });
292
+ },
293
+ /*
294
+ * List all existing indexes (paginated)
295
+ *
296
+ * @param callback the result callback with two arguments
297
+ * success: boolean set to true if the request was successfull
298
+ * content: the server answer with index list or error description if success is false.
299
+ * @param page The page to retrieve, starting at 0.
300
+ */
301
+ listIndexes: function(callback, page) {
302
+ var params = typeof page !== 'undefined' ? '?page=' + page : '';
303
+ return this._jsonRequest({ method: 'GET',
304
+ url: '/1/indexes' + params,
305
+ callback: callback });
306
+ },
307
+
308
+ /*
309
+ * Get the index object initialized
310
+ *
311
+ * @param indexName the name of index
312
+ * @param callback the result callback with one argument (the Index instance)
313
+ */
314
+ initIndex: function(indexName) {
315
+ return new this.Index(this, indexName);
316
+ },
317
+ /*
318
+ * List all existing user keys with their associated ACLs
319
+ *
320
+ * @param callback the result callback with two arguments
321
+ * success: boolean set to true if the request was successfull
322
+ * content: the server answer with user keys list or error description if success is false.
323
+ */
324
+ listUserKeys: function(callback) {
325
+ return this._jsonRequest({ method: 'GET',
326
+ url: '/1/keys',
327
+ callback: callback });
328
+ },
329
+ /*
330
+ * Get ACL of a user key
331
+ *
332
+ * @param callback the result callback with two arguments
333
+ * success: boolean set to true if the request was successfull
334
+ * content: the server answer with user keys list or error description if success is false.
335
+ */
336
+ getUserKeyACL: function(key, callback) {
337
+ return this._jsonRequest({ method: 'GET',
338
+ url: '/1/keys/' + key,
339
+ callback: callback });
340
+ },
341
+ /*
342
+ * Delete an existing user key
343
+ *
344
+ * @param callback the result callback with two arguments
345
+ * success: boolean set to true if the request was successfull
346
+ * content: the server answer with user keys list or error description if success is false.
347
+ */
348
+ deleteUserKey: function(key, callback) {
349
+ return this._jsonRequest({ method: 'DELETE',
350
+ url: '/1/keys/' + key,
351
+ callback: callback });
352
+ },
353
+ /*
354
+ * Add an existing user key
355
+ *
356
+ * @param acls the list of ACL for this key. Defined by an array of strings that
357
+ * can contains the following values:
358
+ * - search: allow to search (https and http)
359
+ * - addObject: allows to add/update an object in the index (https only)
360
+ * - deleteObject : allows to delete an existing object (https only)
361
+ * - deleteIndex : allows to delete index content (https only)
362
+ * - settings : allows to get index settings (https only)
363
+ * - editSettings : allows to change index settings (https only)
364
+ * @param callback the result callback with two arguments
365
+ * success: boolean set to true if the request was successfull
366
+ * content: the server answer with user keys list or error description if success is false.
367
+ */
368
+ addUserKey: function(acls, callback) {
369
+ return this.addUserKeyWithValidity(acls, 0, 0, 0, callback);
370
+ },
371
+ /*
372
+ * Add an existing user key
373
+ *
374
+ * @param acls the list of ACL for this key. Defined by an array of strings that
375
+ * can contains the following values:
376
+ * - search: allow to search (https and http)
377
+ * - addObject: allows to add/update an object in the index (https only)
378
+ * - deleteObject : allows to delete an existing object (https only)
379
+ * - deleteIndex : allows to delete index content (https only)
380
+ * - settings : allows to get index settings (https only)
381
+ * - editSettings : allows to change index settings (https only)
382
+ * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
383
+ * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
384
+ * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
385
+ * @param callback the result callback with two arguments
386
+ * success: boolean set to true if the request was successfull
387
+ * content: the server answer with user keys list or error description if success is false.
388
+ */
389
+ addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
390
+ var aclsObject = {};
391
+ aclsObject.acl = acls;
392
+ aclsObject.validity = validity;
393
+ aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
394
+ aclsObject.maxHitsPerQuery = maxHitsPerQuery;
395
+ return this._jsonRequest({ method: 'POST',
396
+ url: '/1/keys',
397
+ body: aclsObject,
398
+ callback: callback });
399
+ },
211
400
 
212
- if (typeof otherAttributes !== 'undefined') {
213
- for (var i = 0; i < otherAttributes.length; ++i) {
214
- var attr = _getHitExplanationForOneAttr(hit, foundWords, otherAttributes[i]);
215
- for (var j = 0; j < attr.length; ++j) {
216
- res.subtitles.push({ attr: otherAttributes[i], value: attr[j] });
217
- }
401
+ /**
402
+ * Set the extra security tagFilters header
403
+ * @param {string|array} tags The list of tags defining the current security filters
404
+ */
405
+ setSecurityTags: function(tags) {
406
+ if (Object.prototype.toString.call(tags) === '[object Array]') {
407
+ var strTags = [];
408
+ for (var i = 0; i < tags.length; ++i) {
409
+ if (Object.prototype.toString.call(tags[i]) === '[object Array]') {
410
+ var oredTags = [];
411
+ for (var j = 0; j < tags[i].length; ++j) {
412
+ oredTags.push(tags[i][j]);
413
+ }
414
+ strTags.push('(' + oredTags.join(',') + ')');
415
+ } else {
416
+ strTags.push(tags[i]);
218
417
  }
418
+ }
419
+ tags = strTags.join(',');
219
420
  }
220
- return res;
221
- }
421
+ this.tagFilters = tags;
422
+ },
222
423
 
424
+ /**
425
+ * Set the extra user token header
426
+ * @param {string} userToken The token identifying a uniq user (used to apply rate limits)
427
+ */
428
+ setUserToken: function(userToken) {
429
+ this.userToken = userToken;
430
+ },
223
431
 
224
- AlgoliaSearch.prototype = {
225
- /*
226
- * Delete an index
227
- *
228
- * @param indexName the name of index to delete
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 that contains the task ID
232
- */
233
- deleteIndex: function(indexName, callback) {
234
- return this._jsonRequest({ method: 'DELETE',
235
- url: '/1/indexes/' + encodeURIComponent(indexName),
236
- callback: callback });
237
- },
238
- /**
239
- * Move an existing index.
240
- * @param srcIndexName the name of index to copy.
241
- * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
242
- * @param callback the result callback with two arguments
243
- * success: boolean set to true if the request was successfull
244
- * content: the server answer that contains the task ID
245
- */
246
- moveIndex: function(srcIndexName, dstIndexName, callback) {
247
- var postObj = {operation: 'move', destination: dstIndexName};
248
- return this._jsonRequest({ method: 'POST',
249
- url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
250
- body: postObj,
251
- callback: callback });
432
+ /*
433
+ * Initialize a new batch of search queries
434
+ */
435
+ startQueriesBatch: function() {
436
+ this.batch = [];
437
+ },
438
+ /*
439
+ * Add a search query in the batch
440
+ *
441
+ * @param query the full text query
442
+ * @param args (optional) if set, contains an object with query parameters:
443
+ * - attributes: an array of object attribute names to retrieve
444
+ * (if not set all attributes are retrieve)
445
+ * - attributesToHighlight: an array of object attribute names to highlight
446
+ * (if not set indexed attributes are highlighted)
447
+ * - minWordSizefor1Typo: the minimum number of characters to accept one typo.
448
+ * Defaults to 3.
449
+ * - minWordSizefor2Typos: the minimum number of characters to accept two typos.
450
+ * Defaults to 7.
451
+ * - getRankingInfo: if set, the result hits will contain ranking information in
452
+ * _rankingInfo attribute
453
+ * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0.
454
+ * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10.
455
+ */
456
+ addQueryInBatch: function(indexName, query, args) {
457
+ var params = 'query=' + encodeURIComponent(query);
458
+ if (!this._isUndefined(args) && args !== null) {
459
+ params = this._getSearchParams(args, params);
460
+ }
461
+ this.batch.push({ indexName: indexName, params: params });
462
+ },
463
+ /*
464
+ * Clear all queries in cache
465
+ */
466
+ clearCache: function() {
467
+ this.cache = {};
468
+ },
469
+ /*
470
+ * Launch the batch of queries using XMLHttpRequest.
471
+ * (Optimized for browser using a POST query to minimize number of OPTIONS queries)
472
+ *
473
+ * @param callback the function that will receive results
474
+ * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime.
475
+ */
476
+ sendQueriesBatch: function(callback, delay) {
477
+ var as = this;
478
+ var params = {requests: []};
479
+ for (var i = 0; i < as.batch.length; ++i) {
480
+ params.requests.push(as.batch[i]);
481
+ }
482
+ window.clearTimeout(as.onDelayTrigger);
483
+ if (!this._isUndefined(delay) && delay !== null && delay > 0) {
484
+ var onDelayTrigger = window.setTimeout( function() {
485
+ as._sendQueriesBatch(params, callback);
486
+ }, delay);
487
+ as.onDelayTrigger = onDelayTrigger;
488
+ } else {
489
+ return this._sendQueriesBatch(params, callback);
490
+ }
491
+ },
252
492
 
253
- },
254
- /**
255
- * Copy an existing index.
256
- * @param srcIndexName the name of index to copy.
257
- * @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist).
258
- * @param callback the result callback with two arguments
259
- * success: boolean set to true if the request was successfull
260
- * content: the server answer that contains the task ID
261
- */
262
- copyIndex: function(srcIndexName, dstIndexName, callback) {
263
- var postObj = {operation: 'copy', destination: dstIndexName};
264
- return this._jsonRequest({ method: 'POST',
265
- url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation',
266
- body: postObj,
267
- callback: callback });
268
- },
269
- /**
270
- * Return last log entries.
271
- * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry).
272
- * @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000.
273
- * @param callback the result callback with two arguments
274
- * success: boolean set to true if the request was successfull
275
- * content: the server answer that contains the task ID
276
- */
277
- getLogs: function(callback, offset, length) {
278
- if (this._isUndefined(offset)) {
279
- offset = 0;
280
- }
281
- if (this._isUndefined(length)) {
282
- length = 10;
283
- }
493
+ /**
494
+ * Set the number of milliseconds a request can take before automatically being terminated.
495
+ *
496
+ * @param {Number} milliseconds
497
+ */
498
+ setRequestTimeout: function(milliseconds)
499
+ {
500
+ if (milliseconds) {
501
+ this.requestTimeoutInMs = parseInt(milliseconds, 10);
502
+ }
503
+ },
284
504
 
285
- return this._jsonRequest({ method: 'GET',
286
- url: '/1/logs?offset=' + offset + '&length=' + length,
287
- callback: callback });
288
- },
289
- /*
290
- * List all existing indexes (paginated)
291
- *
292
- * @param callback the result callback with two arguments
293
- * success: boolean set to true if the request was successfull
294
- * content: the server answer with index list or error description if success is false.
295
- * @param page The page to retrieve, starting at 0.
296
- */
297
- listIndexes: function(callback, page) {
298
- var params = typeof page !== 'undefined' ? '?page=' + page : '';
299
- return this._jsonRequest({ method: 'GET',
300
- url: '/1/indexes' + params,
301
- callback: callback });
302
- },
505
+ /*
506
+ * Index class constructor.
507
+ * You should not use this method directly but use initIndex() function
508
+ */
509
+ Index: function(algoliasearch, indexName) {
510
+ this.indexName = indexName;
511
+ this.as = algoliasearch;
512
+ this.typeAheadArgs = null;
513
+ this.typeAheadValueOption = null;
303
514
 
304
- /*
305
- * Get the index object initialized
306
- *
307
- * @param indexName the name of index
308
- * @param callback the result callback with one argument (the Index instance)
309
- */
310
- initIndex: function(indexName) {
311
- return new this.Index(this, indexName);
312
- },
313
- /*
314
- * List all existing user keys with their associated ACLs
315
- *
316
- * @param callback the result callback with two arguments
317
- * success: boolean set to true if the request was successfull
318
- * content: the server answer with user keys list or error description if success is false.
319
- */
320
- listUserKeys: function(callback) {
321
- return this._jsonRequest({ method: 'GET',
322
- url: '/1/keys',
323
- callback: callback });
324
- },
325
- /*
326
- * Get ACL of a user key
327
- *
328
- * @param callback the result callback with two arguments
329
- * success: boolean set to true if the request was successfull
330
- * content: the server answer with user keys list or error description if success is false.
331
- */
332
- getUserKeyACL: function(key, callback) {
333
- return this._jsonRequest({ method: 'GET',
334
- url: '/1/keys/' + key,
335
- callback: callback });
336
- },
337
- /*
338
- * Delete an existing user key
339
- *
340
- * @param callback the result callback with two arguments
341
- * success: boolean set to true if the request was successfull
342
- * content: the server answer with user keys list or error description if success is false.
343
- */
344
- deleteUserKey: function(key, callback) {
345
- return this._jsonRequest({ method: 'DELETE',
346
- url: '/1/keys/' + key,
347
- callback: callback });
348
- },
349
- /*
350
- * Add an existing user key
351
- *
352
- * @param acls the list of ACL for this key. Defined by an array of strings that
353
- * can contains the following values:
354
- * - search: allow to search (https and http)
355
- * - addObject: allows to add/update an object in the index (https only)
356
- * - deleteObject : allows to delete an existing object (https only)
357
- * - deleteIndex : allows to delete index content (https only)
358
- * - settings : allows to get index settings (https only)
359
- * - editSettings : allows to change index settings (https only)
360
- * @param callback the result callback with two arguments
361
- * success: boolean set to true if the request was successfull
362
- * content: the server answer with user keys list or error description if success is false.
363
- */
364
- addUserKey: function(acls, callback) {
365
- var aclsObject = {};
366
- aclsObject.acl = acls;
367
- return this._jsonRequest({ method: 'POST',
368
- url: '/1/keys',
369
- body: aclsObject,
370
- callback: callback });
371
- },
372
- /*
373
- * Add an existing user key
374
- *
375
- * @param acls the list of ACL for this key. Defined by an array of strings that
376
- * can contains the following values:
377
- * - search: allow to search (https and http)
378
- * - addObject: allows to add/update an object in the index (https only)
379
- * - deleteObject : allows to delete an existing object (https only)
380
- * - deleteIndex : allows to delete index content (https only)
381
- * - settings : allows to get index settings (https only)
382
- * - editSettings : allows to change index settings (https only)
383
- * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
384
- * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
385
- * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
386
- * @param callback the result callback with two arguments
387
- * success: boolean set to true if the request was successfull
388
- * content: the server answer with user keys list or error description if success is false.
389
- */
390
- addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
391
- var indexObj = this;
392
- var aclsObject = {};
393
- aclsObject.acl = acls;
394
- aclsObject.validity = validity;
395
- aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
396
- aclsObject.maxHitsPerQuery = maxHitsPerQuery;
397
- return this._jsonRequest({ method: 'POST',
398
- url: '/1/indexes/' + indexObj.indexName + '/keys',
399
- body: aclsObject,
400
- callback: callback });
401
- },
515
+ // make sure every index instance has it's own cache
516
+ this.cache = {};
517
+ },
518
+ /**
519
+ * Add an extra field to the HTTP request
520
+ *
521
+ * @param key the header field name
522
+ * @param value the header field value
523
+ */
524
+ setExtraHeader: function(key, value) {
525
+ this.extraHeaders.push({ key: key, value: value});
526
+ },
402
527
 
403
- /**
404
- * Set the extra security tagFilters header
405
- * @param {string|array} tags The list of tags defining the current security filters
406
- */
407
- setSecurityTags: function(tags) {
408
- if (Object.prototype.toString.call(tags) === '[object Array]') {
409
- var strTags = [];
410
- for (var i = 0; i < tags.length; ++i) {
411
- if (Object.prototype.toString.call(tags[i]) === '[object Array]') {
412
- var oredTags = [];
413
- for (var j = 0; j < tags[i].length; ++j) {
414
- oredTags.push(tags[i][j]);
415
- }
416
- strTags.push('(' + oredTags.join(',') + ')');
417
- } else {
418
- strTags.push(tags[i]);
419
- }
420
- }
421
- tags = strTags.join(',');
528
+ _sendQueriesBatch: function(params, callback) {
529
+ if (this.jsonp === null) {
530
+ var self = this;
531
+ return this._jsonRequest({ cache: this.cache,
532
+ method: 'POST',
533
+ url: '/1/indexes/*/queries',
534
+ body: params,
535
+ callback: function(success, content) {
536
+ if (!success) {
537
+ // retry first with JSONP
538
+ self.jsonp = true;
539
+ self._sendQueriesBatch(params, callback);
540
+ } else {
541
+ self.jsonp = false;
542
+ callback && callback(success, content);
543
+ }
422
544
  }
423
- this.tagFilters = tags;
424
- },
425
-
426
- /**
427
- * Set the extra user token header
428
- * @param {string} userToken The token identifying a uniq user (used to apply rate limits)
429
- */
430
- setUserToken: function(userToken) {
431
- this.userToken = userToken;
432
- },
545
+ });
546
+ } else if (this.jsonp) {
547
+ var jsonpParams = '';
548
+ for (var i = 0; i < params.requests.length; ++i) {
549
+ var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params;
550
+ jsonpParams += i + '=' + encodeURIComponent(q) + '&';
551
+ }
552
+ var pObj = {params: jsonpParams};
553
+ return this._jsonRequest({ cache: this.cache,
554
+ method: 'GET',
555
+ url: '/1/indexes/*',
556
+ body: pObj,
557
+ callback: callback });
558
+ } else {
559
+ return this._jsonRequest({ cache: this.cache,
560
+ method: 'POST',
561
+ url: '/1/indexes/*/queries',
562
+ body: params,
563
+ callback: callback});
564
+ }
565
+ },
566
+ /*
567
+ * Wrapper that try all hosts to maximize the quality of service
568
+ */
569
+ _jsonRequest: function(opts) {
570
+ var self = this;
571
+ var callback = opts.callback;
572
+ var cache = null;
573
+ var cacheID = opts.url;
574
+ var deferred = null;
575
+ if (this.options.jQuery) {
576
+ deferred = this.options.jQuery.$.Deferred();
577
+ deferred.promise = deferred.promise(); // promise is a property in angular
578
+ } else if (this.options.angular) {
579
+ deferred = this.options.angular.$q.defer();
580
+ }
433
581
 
434
- /*
435
- * Initialize a new batch of search queries
436
- */
437
- startQueriesBatch: function() {
438
- this.batch = [];
439
- },
440
- /*
441
- * Add a search query in the batch
442
- *
443
- * @param query the full text query
444
- * @param args (optional) if set, contains an object with query parameters:
445
- * - attributes: an array of object attribute names to retrieve
446
- * (if not set all attributes are retrieve)
447
- * - attributesToHighlight: an array of object attribute names to highlight
448
- * (if not set indexed attributes are highlighted)
449
- * - minWordSizefor1Typo: the minimum number of characters to accept one typo.
450
- * Defaults to 3.
451
- * - minWordSizefor2Typos: the minimum number of characters to accept two typos.
452
- * Defaults to 7.
453
- * - getRankingInfo: if set, the result hits will contain ranking information in
454
- * _rankingInfo attribute
455
- * - page: (pagination parameter) page to retrieve (zero base). Defaults to 0.
456
- * - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10.
457
- */
458
- addQueryInBatch: function(indexName, query, args) {
459
- var params = 'query=' + encodeURIComponent(query);
460
- if (!this._isUndefined(args) && args !== null) {
461
- params = this._getSearchParams(args, params);
582
+ if (!this._isUndefined(opts.body)) {
583
+ cacheID = opts.url + '_body_' + JSON.stringify(opts.body);
584
+ }
585
+ if (!this._isUndefined(opts.cache)) {
586
+ cache = opts.cache;
587
+ if (!this._isUndefined(cache[cacheID])) {
588
+ if (!this._isUndefined(callback) && callback) {
589
+ setTimeout(function () { callback(true, cache[cacheID]); }, 1);
462
590
  }
463
- this.batch.push({ indexName: indexName, params: params });
464
- },
465
- /*
466
- * Clear all queries in cache
467
- */
468
- clearCache: function() {
469
- this.cache = {};
470
- },
471
- /*
472
- * Launch the batch of queries using XMLHttpRequest.
473
- * (Optimized for browser using a POST query to minimize number of OPTIONS queries)
474
- *
475
- * @param callback the function that will receive results
476
- * @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime.
477
- */
478
- sendQueriesBatch: function(callback, delay) {
479
- var as = this;
480
- var params = {requests: []};
481
- for (var i = 0; i < as.batch.length; ++i) {
482
- params.requests.push(as.batch[i]);
591
+ deferred && deferred.resolve(cache[cacheID]);
592
+ return deferred && deferred.promise;
593
+ }
594
+ }
595
+
596
+ opts.successiveRetryCount = 0;
597
+ var impl = function() {
598
+ if (opts.successiveRetryCount >= self.hosts.length) {
599
+ var error = { message: 'Cannot connect the Algolia\'s Search API. Please send an email to support@algolia.com to report the issue.' };
600
+ if (!self._isUndefined(callback) && callback) {
601
+ opts.successiveRetryCount = 0;
602
+ callback(false, error);
483
603
  }
484
- window.clearTimeout(as.onDelayTrigger);
485
- if (!this._isUndefined(delay) && delay !== null && delay > 0) {
486
- var onDelayTrigger = window.setTimeout( function() {
487
- as._sendQueriesBatch(params, callback);
488
- }, delay);
489
- as.onDelayTrigger = onDelayTrigger;
604
+ deferred && deferred.reject(error);
605
+ return;
606
+ }
607
+ opts.callback = function(retry, success, body) {
608
+ if (success && !self._isUndefined(opts.cache)) {
609
+ cache[cacheID] = body;
610
+ }
611
+ if (!success && retry) {
612
+ self.currentHostIndex = ++self.currentHostIndex % self.hosts.length;
613
+ opts.successiveRetryCount += 1;
614
+ impl();
490
615
  } else {
491
- return this._sendQueriesBatch(params, callback);
616
+ opts.successiveRetryCount = 0;
617
+ deferred && (success ? deferred.resolve(body) : deferred.reject(body));
618
+ if (!self._isUndefined(callback) && callback) {
619
+ callback(success, body);
620
+ }
492
621
  }
493
- },
622
+ };
623
+ opts.hostname = self.hosts[self.currentHostIndex];
624
+ self._jsonRequestByHost(opts);
625
+ };
626
+ impl();
494
627
 
495
- /**
496
- * Set the number of milliseconds a request can take before automatically being terminated.
497
- *
498
- * @param {Number} milliseconds
499
- */
500
- setRequestTimeout: function(milliseconds)
501
- {
502
- if (milliseconds) {
503
- this.requestTimeoutInMs = parseInt(milliseconds, 10);
504
- }
505
- },
628
+ return deferred && deferred.promise;
629
+ },
506
630
 
507
- /*
508
- * Index class constructor.
509
- * You should not use this method directly but use initIndex() function
510
- */
511
- Index: function(algoliasearch, indexName) {
512
- this.indexName = indexName;
513
- this.as = algoliasearch;
514
- this.typeAheadArgs = null;
515
- this.typeAheadValueOption = null;
516
-
517
- // make sure every index instance has it's own cache
518
- this.cache = {};
519
- },
520
- /**
521
- * Add an extra field to the HTTP request
522
- *
523
- * @param key the header field name
524
- * @param value the header field value
525
- */
526
- setExtraHeader: function(key, value) {
527
- this.extraHeaders.push({ key: key, value: value});
528
- },
631
+ _jsonRequestByHost: function(opts) {
632
+ var self = this;
633
+ var url = opts.hostname + opts.url;
634
+
635
+ if (this.jsonp) {
636
+ this._makeJsonpRequestByHost(url, opts);
637
+ } else if (this.options.jQuery) {
638
+ this._makejQueryRequestByHost(url, opts);
639
+ } else if (this.options.angular) {
640
+ this._makeAngularRequestByHost(url, opts);
641
+ } else {
642
+ this._makeXmlHttpRequestByHost(url, opts);
643
+ }
644
+ },
529
645
 
530
- _sendQueriesBatch: function(params, callback) {
531
-
532
- if (this.jsonp === null) {
533
- var self = this;
534
- return this._jsonRequest({ cache: this.cache,
535
- method: 'POST',
536
- url: '/1/indexes/*/queries',
537
- body: params,
538
- callback: function(success, content) {
539
- if (!success) {
540
- // retry first with JSONP
541
- self.jsonp = true;
542
- self._sendQueriesBatch(params, callback);
543
- } else {
544
- self.jsonp = false;
545
- callback && callback(success, content);
546
- }
547
- }
548
- });
549
- } else if (this.jsonp) {
550
- var jsonpParams = '';
551
- for (var i = 0; i < params.requests.length; ++i) {
552
- var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params;
553
- jsonpParams += i + '=' + encodeURIComponent(q) + '&';
554
- }
555
- var pObj = {params: jsonpParams};
556
- return this._jsonRequest({ cache: this.cache,
557
- method: 'GET',
558
- url: '/1/indexes/*',
559
- body: pObj,
560
- callback: callback });
561
- } else {
562
- return this._jsonRequest({ cache: this.cache,
563
- method: 'POST',
564
- url: '/1/indexes/*/queries',
565
- body: params,
566
- callback: callback});
567
- }
568
- },
569
- /*
570
- * Wrapper that try all hosts to maximize the quality of service
571
- */
572
- _jsonRequest: function(opts) {
573
- var self = this;
574
- var callback = opts.callback;
575
- var cache = null;
576
- var cacheID = opts.url;
577
- var deferred = null;
578
- if (this.options.jQuery) {
579
- deferred = this.options.jQuery.$.Deferred();
580
- deferred.promise = deferred.promise(); // promise is a property in angular
581
- } else if (this.options.angular) {
582
- deferred = this.options.angular.$q.defer();
583
- }
646
+ /**
647
+ * Make a $http
648
+ *
649
+ * @param url request url (includes endpoint and path)
650
+ * @param opts all request opts
651
+ */
652
+ _makeAngularRequestByHost: function(url, opts) {
653
+ var self = this;
654
+ var body = null;
584
655
 
585
- if (!this._isUndefined(opts.body)) {
586
- cacheID = opts.url + '_body_' + JSON.stringify(opts.body);
587
- }
588
- if (!this._isUndefined(opts.cache)) {
589
- cache = opts.cache;
590
- if (!this._isUndefined(cache[cacheID])) {
591
- if (!this._isUndefined(callback) && callback) {
592
- setTimeout(function () { callback(true, cache[cacheID]); }, 1);
593
- }
594
- deferred && deferred.resolve(cache[cacheID]);
595
- return deferred && deferred.promise;
596
- }
597
- }
656
+ if (!this._isUndefined(opts.body)) {
657
+ body = JSON.stringify(opts.body);
658
+ }
598
659
 
599
- opts.successiveRetryCount = 0;
600
- var impl = function() {
601
- if (opts.successiveRetryCount >= self.hosts.length) {
602
- var error = { message: 'Cannot connect the Algolia\'s Search API. Please send an email to support@algolia.com to report the issue.' };
603
- if (!self._isUndefined(callback) && callback) {
604
- opts.successiveRetryCount = 0;
605
- callback(false, error);
606
- }
607
- deferred && deferred.reject(error);
608
- return;
609
- }
610
- opts.callback = function(retry, success, obj, body) {
611
- if (success && !self._isUndefined(opts.cache)) {
612
- cache[cacheID] = body;
613
- }
614
- if (!success && retry) {
615
- self.currentHostIndex = ++self.currentHostIndex % self.hosts.length;
616
- opts.successiveRetryCount += 1;
617
- impl();
618
- } else {
619
- opts.successiveRetryCount = 0;
620
- deferred && (success ? deferred.resolve(body) : deferred.reject(body));
621
- if (!self._isUndefined(callback) && callback) {
622
- callback(success, body);
623
- }
624
- }
625
- };
626
- opts.hostname = self.hosts[self.currentHostIndex];
627
- self._jsonRequestByHost(opts);
628
- };
629
- impl();
660
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
661
+ url += '&X-Algolia-Application-Id=' + this.applicationID;
662
+ if (this.userToken) {
663
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
664
+ }
665
+ if (this.tagFilters) {
666
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
667
+ }
668
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
669
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
670
+ }
671
+ this.options.angular.$http({
672
+ url: url,
673
+ method: opts.method,
674
+ data: body,
675
+ cache: false,
676
+ timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1))
677
+ }).then(function(response) {
678
+ opts.callback(false, true, response.data);
679
+ }, function(response) {
680
+ if (response.status === 0) {
681
+ // xhr.timeout is not handled by Angular.js right now
682
+ // let's retry
683
+ opts.callback(true, false, response.data);
684
+ } else if (response.status == 400 || response.status === 403 || response.status === 404) {
685
+ opts.callback(false, false, response.data);
686
+ } else {
687
+ opts.callback(true, false, response.data);
688
+ }
689
+ });
690
+ },
630
691
 
631
- return deferred && deferred.promise;
632
- },
692
+ /**
693
+ * Make a $.ajax
694
+ *
695
+ * @param url request url (includes endpoint and path)
696
+ * @param opts all request opts
697
+ */
698
+ _makejQueryRequestByHost: function(url, opts) {
699
+ var self = this;
700
+ var body = null;
633
701
 
634
- _jsonRequestByHost: function(opts) {
635
- var self = this;
636
- var url = opts.hostname + opts.url;
702
+ if (!this._isUndefined(opts.body)) {
703
+ body = JSON.stringify(opts.body);
704
+ }
637
705
 
638
- if (this.jsonp) {
639
- this._makeJsonpRequestByHost(url, opts);
640
- } else if (this.options.jQuery) {
641
- this._makejQueryRequestByHost(url, opts);
642
- } else if (this.options.angular) {
643
- this._makeAngularRequestByHost(url, opts);
706
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
707
+ url += '&X-Algolia-Application-Id=' + this.applicationID;
708
+ if (this.userToken) {
709
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
710
+ }
711
+ if (this.tagFilters) {
712
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
713
+ }
714
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
715
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
716
+ }
717
+ this.options.jQuery.$.ajax(url, {
718
+ type: opts.method,
719
+ timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)),
720
+ dataType: 'json',
721
+ data: body,
722
+ error: function(xhr, textStatus, error) {
723
+ if (textStatus === 'timeout') {
724
+ opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } );
725
+ } else if (xhr.status === 400 || xhr.status === 403 || xhr.status === 404) {
726
+ opts.callback(false, false, xhr.responseJSON );
644
727
  } else {
645
- this._makeXmlHttpRequestByHost(url, opts);
728
+ opts.callback(true, false, { 'message': error } );
646
729
  }
647
- },
730
+ },
731
+ success: function(data, textStatus, xhr) {
732
+ opts.callback(false, true, data);
733
+ }
734
+ });
735
+ },
648
736
 
649
- /**
650
- * Make a $http
651
- *
652
- * @param url request url (includes endpoint and path)
653
- * @param opts all request opts
654
- */
655
- _makeAngularRequestByHost: function(url, opts) {
656
- var self = this;
657
- var body = null;
737
+ /**
738
+ * Make a JSONP request
739
+ *
740
+ * @param url request url (includes endpoint and path)
741
+ * @param opts all request options
742
+ */
743
+ _makeJsonpRequestByHost: function(url, opts) {
744
+ if (opts.method !== 'GET') {
745
+ opts.callback(true, false, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' });
746
+ return;
747
+ }
658
748
 
659
- if (!this._isUndefined(opts.body)) {
660
- body = JSON.stringify(opts.body);
661
- }
749
+ var cbCalled = false;
750
+ var timedOut = false;
662
751
 
663
- url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
664
- url += '&X-Algolia-Application-Id=' + this.applicationID;
665
- if (this.userToken) {
666
- url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
667
- }
668
- if (this.tagFilters) {
669
- url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
670
- }
671
- for (var i = 0; i < this.extraHeaders.length; ++i) {
672
- url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
673
- }
674
- this.options.angular.$http({
675
- url: url,
676
- method: opts.method,
677
- data: body,
678
- cache: false,
679
- timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1))
680
- }).then(function(response) {
681
- opts.callback(false, true, null, response.data);
682
- }, function(response) {
683
- if (response.status === 0) {
684
- // xhr.timeout is not handled by Angular.js right now
685
- // let's retry
686
- opts.callback(true, false, null, response.data);
687
- } else if (response.status == 400 || response.status === 403 || response.status === 404) {
688
- opts.callback(false, false, null, response.data);
689
- } else {
690
- opts.callback(true, false, null, response.data);
691
- }
692
- });
693
- },
752
+ AlgoliaSearch.JSONPCounter += 1;
753
+ var head = document.getElementsByTagName('head')[0];
754
+ var script = document.createElement('script');
755
+ var cb = 'algoliaJSONP_' + AlgoliaSearch.JSONPCounter;
756
+ var done = false;
757
+ var ontimeout;
758
+ var success;
759
+ var clean;
694
760
 
695
- /**
696
- * Make a $.ajax
697
- *
698
- * @param url request url (includes endpoint and path)
699
- * @param opts all request opts
700
- */
701
- _makejQueryRequestByHost: function(url, opts) {
702
- var self = this;
703
- var body = null;
761
+ window[cb] = function(data) {
762
+ try { delete window[cb]; } catch (e) { window[cb] = undefined; }
704
763
 
705
- if (!this._isUndefined(opts.body)) {
706
- body = JSON.stringify(opts.body);
707
- }
764
+ if (timedOut) {
765
+ return;
766
+ }
708
767
 
709
- url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
710
- url += '&X-Algolia-Application-Id=' + this.applicationID;
711
- if (this.userToken) {
712
- url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
713
- }
714
- if (this.tagFilters) {
715
- url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
716
- }
717
- for (var i = 0; i < this.extraHeaders.length; ++i) {
718
- url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
719
- }
720
- this.options.jQuery.$.ajax(url, {
721
- type: opts.method,
722
- timeout: (this.requestTimeoutInMs * (opts.successiveRetryCount + 1)),
723
- dataType: 'json',
724
- data: body,
725
- error: function(xhr, textStatus, error) {
726
- if (textStatus === 'timeout') {
727
- opts.callback(true, false, null, { 'message': 'Timeout - Could not connect to endpoint ' + url } );
728
- } else if (xhr.status === 400 || xhr.status === 403 || xhr.status === 404) {
729
- opts.callback(false, false, null, xhr.responseJSON );
730
- } else {
731
- opts.callback(true, false, null, { 'message': error } );
732
- }
733
- },
734
- success: function(data, textStatus, xhr) {
735
- opts.callback(false, true, null, data);
736
- }
737
- });
738
- },
768
+ var status =
769
+ data && data.message && data.status ||
770
+ data && 200;
739
771
 
740
- /**
741
- * Make a JSONP request
742
- *
743
- * @param url request url (includes endpoint and path)
744
- * @param opts all request options
745
- */
746
- _makeJsonpRequestByHost: function(url, opts) {
747
- if (opts.method !== 'GET') {
748
- opts.callback(true, false, null, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' });
749
- return;
750
- }
772
+ var ok = status === 200;
773
+ var retry = !ok && status !== 400 && status !== 403 && status !== 404;
774
+ cbCalled = true;
775
+ opts.callback(retry, ok, data);
776
+ };
751
777
 
752
- this.jsonpCounter = this.jsonpCounter || 0;
753
- this.jsonpCounter += 1;
754
- var head = document.getElementsByTagName('head')[0];
755
- var script = document.createElement('script');
756
- var cb = 'algoliaJSONP_' + this.jsonpCounter;
757
- var done = false;
758
- var ontimeout = null;
759
-
760
- window[cb] = function(data) {
761
- opts.callback(false, true, null, data);
762
- try { delete window[cb]; } catch (e) { window[cb] = undefined; }
763
- };
778
+ script.type = 'text/javascript';
779
+ url += '?callback=' + cb + '&X-Algolia-Application-Id=' + this.applicationID + '&X-Algolia-API-Key=' + this.apiKey;
764
780
 
765
- script.type = 'text/javascript';
766
- script.src = url + '?callback=' + cb + '&X-Algolia-Application-Id=' + this.applicationID + '&X-Algolia-API-Key=' + this.apiKey;
781
+ if (this.tagFilters) {
782
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
783
+ }
767
784
 
768
- if (this.tagFilters) {
769
- script.src += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
770
- }
785
+ if (this.userToken) {
786
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
787
+ }
771
788
 
772
- if (this.userToken) {
773
- script.src += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
774
- }
775
- for (var i = 0; i < this.extraHeaders.length; ++i) {
776
- script.src += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
777
- }
789
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
790
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
791
+ }
778
792
 
793
+ if (opts.body && opts.body.params) {
794
+ url += '&' + opts.body.params;
795
+ }
779
796
 
780
- if (opts.body && opts.body.params) {
781
- script.src += '&' + opts.body.params;
782
- }
797
+ ontimeout = setTimeout(function() {
798
+ timedOut = true;
799
+ clean();
783
800
 
784
- ontimeout = setTimeout(function() {
785
- script.onload = script.onreadystatechange = script.onerror = null;
786
- window[cb] = function(data) {
787
- try { delete window[cb]; } catch (e) { window[cb] = undefined; }
788
- };
801
+ opts.callback(true, false, { 'message': 'Timeout - Failed to load JSONP script.' });
802
+ }, this.requestTimeoutInMs);
789
803
 
790
- opts.callback(true, false, null, { 'message': 'Timeout - Failed to load JSONP script.' });
791
- head.removeChild(script);
804
+ success = function() {
805
+ if (done || timedOut) {
806
+ return;
807
+ }
792
808
 
793
- clearTimeout(ontimeout);
794
- ontimeout = null;
809
+ done = true;
810
+ clean();
795
811
 
796
- }, this.requestTimeoutInMs);
812
+ // script loaded but did not call the fn => script loading error
813
+ if (!cbCalled) {
814
+ opts.callback(true, false, { 'message': 'Failed to load JSONP script.' });
815
+ }
816
+ };
797
817
 
798
- script.onload = script.onreadystatechange = function() {
799
- clearTimeout(ontimeout);
800
- ontimeout = null;
818
+ clean = function() {
819
+ clearTimeout(ontimeout);
820
+ script.onload = null;
821
+ script.onreadystatechange = null;
822
+ script.onerror = null;
823
+ head.removeChild(script);
801
824
 
802
- if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) {
803
- done = true;
825
+ try {
826
+ delete window[cb];
827
+ delete window[cb + '_loaded'];
828
+ } catch (e) {
829
+ window[cb] = null;
830
+ window[cb + '_loaded'] = null;
831
+ }
832
+ };
804
833
 
805
- if (typeof window[cb + '_loaded'] === 'undefined') {
806
- opts.callback(true, false, null, { 'message': 'Failed to load JSONP script.' });
807
- try { delete window[cb]; } catch (e) { window[cb] = undefined; }
808
- } else {
809
- try { delete window[cb + '_loaded']; } catch (e) { window[cb + '_loaded'] = undefined; }
810
- }
811
- script.onload = script.onreadystatechange = null; // Handle memory leak in IE
812
- head.removeChild(script);
813
- }
814
- };
834
+ // script onreadystatechange needed only for
835
+ // <= IE8
836
+ // https://github.com/angular/angular.js/issues/4523
837
+ script.onreadystatechange = function() {
838
+ if (this.readyState === 'loaded' || this.readyState === 'complete') {
839
+ success();
840
+ }
841
+ };
815
842
 
816
- script.onerror = function() {
817
- clearTimeout(ontimeout);
818
- ontimeout = null;
843
+ script.onload = function() {
844
+ success();
845
+ };
819
846
 
820
- opts.callback(true, false, null, { 'message': 'Failed to load JSONP script.' });
821
- head.removeChild(script);
822
- try { delete window[cb]; } catch (e) { window[cb] = undefined; }
823
- };
847
+ script.onerror = function() {
848
+ if (done || timedOut) {
849
+ return;
850
+ }
824
851
 
825
- head.appendChild(script);
826
- },
852
+ clean();
853
+ opts.callback(true, false, { 'message': 'Failed to load JSONP script.' });
854
+ };
827
855
 
828
- /**
829
- * Make a XmlHttpRequest
830
- *
831
- * @param url request url (includes endpoint and path)
832
- * @param opts all request opts
833
- */
834
- _makeXmlHttpRequestByHost: function(url, opts) {
835
- var self = this;
836
- var xmlHttp = window.XMLHttpRequest ? new XMLHttpRequest() : {};
837
- var body = null;
838
- var ontimeout = null;
839
-
840
- if (!this._isUndefined(opts.body)) {
841
- body = JSON.stringify(opts.body);
842
- }
856
+ script.async = true;
857
+ script.defer = true;
858
+ script.src = url;
843
859
 
844
- url += ((url.indexOf('?') === -1) ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
845
- url += '&X-Algolia-Application-Id=' + this.applicationID;
846
- if (this.userToken) {
847
- url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
848
- }
849
- if (this.tagFilters) {
850
- url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
851
- }
852
- for (var i = 0; i < this.extraHeaders.length; ++i) {
853
- url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
854
- }
855
- if ('withCredentials' in xmlHttp) {
856
- xmlHttp.open(opts.method, url, true);
857
- xmlHttp.timeout = this.requestTimeoutInMs * (opts.successiveRetryCount + 1);
858
- if (body !== null) {
859
- /* This content type is specified to follow CORS 'simple header' directive */
860
- xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
861
- }
862
- } else if (typeof XDomainRequest !== 'undefined') {
863
- // Handle IE8/IE9
864
- // XDomainRequest only exists in IE, and is IE's way of making CORS requests.
865
- xmlHttp = new XDomainRequest();
866
- xmlHttp.open(opts.method, url);
867
- } else {
868
- // very old browser, not supported
869
- opts.callback(false, false, null, { 'message': 'CORS not supported' });
870
- return;
871
- }
860
+ head.appendChild(script);
861
+ },
872
862
 
873
- ontimeout = setTimeout(function() {
874
- xmlHttp.abort();
875
- // Prevent Internet Explorer 9, JScript Error c00c023f
876
- if (xmlHttp.aborted === true) {
877
- stopLoadAnimation();
878
- return;
879
- }
880
- opts.callback(true, false, null, { 'message': 'Timeout - Could not connect to endpoint ' + url } );
863
+ /**
864
+ * Make a XmlHttpRequest
865
+ *
866
+ * @param url request url (includes endpoint and path)
867
+ * @param opts all request opts
868
+ */
869
+ _makeXmlHttpRequestByHost: function(url, opts) {
870
+ // no cors or XDomainRequest, no request
871
+ if (!this._support.cors && !this._support.hasXDomainRequest) {
872
+ // very old browser, not supported
873
+ opts.callback(false, false, { 'message': 'CORS not supported' });
874
+ return;
875
+ }
881
876
 
882
- clearTimeout(ontimeout);
883
- ontimeout = null;
877
+ var body = null;
878
+ var request = this._support.cors ? new XMLHttpRequest() : new XDomainRequest();
879
+ var ontimeout;
880
+ var self = this;
881
+ var timedOut;
882
+ var timeoutListener;
884
883
 
885
- }, this.requestTimeoutInMs * (opts.successiveRetryCount + 1));
884
+ if (!this._isUndefined(opts.body)) {
885
+ body = JSON.stringify(opts.body);
886
+ }
886
887
 
887
- xmlHttp.onload = function(event) {
888
- clearTimeout(ontimeout);
889
- ontimeout = null;
888
+ url += (url.indexOf('?') === -1 ? '?' : '&') + 'X-Algolia-API-Key=' + this.apiKey;
889
+ url += '&X-Algolia-Application-Id=' + this.applicationID;
890
890
 
891
- if (!self._isUndefined(event) && event.target !== null) {
892
- var success = false;
893
- var response = null;
891
+ if (this.userToken) {
892
+ url += '&X-Algolia-UserToken=' + encodeURIComponent(this.userToken);
893
+ }
894
894
 
895
- if (typeof XDomainRequest !== 'undefined') {
896
- // Handle CORS requests IE8/IE9
897
- response = event.target.responseText;
898
- success = (response && response.length > 0);
899
- } else {
900
- response = event.target.response;
901
- success = (event.target.status === 200 || event.target.status === 201);
902
- }
895
+ if (this.tagFilters) {
896
+ url += '&X-Algolia-TagFilters=' + encodeURIComponent(this.tagFilters);
897
+ }
903
898
 
904
- var retry = !success && event.target.status !== 400 && event.target.status !== 403 && event.target.status !== 404;
905
- opts.callback(retry, success, event.target, response ? JSON.parse(response) : null);
906
- } else {
907
- opts.callback(false, true, event, JSON.parse(xmlHttp.responseText));
908
- }
909
- };
910
- xmlHttp.ontimeout = function(event) { // stop the network call but rely on ontimeout to call opt.callback
911
- };
912
- xmlHttp.onerror = function(event) {
913
- clearTimeout(ontimeout);
914
- ontimeout = null;
915
- opts.callback(true, false, null, { 'message': 'Could not connect to host', 'error': event } );
916
- };
899
+ for (var i = 0; i < this.extraHeaders.length; ++i) {
900
+ url += '&' + this.extraHeaders[i].key + '=' + this.extraHeaders[i].value;
901
+ }
917
902
 
918
- xmlHttp.send(body);
919
- },
903
+ timeoutListener = function() {
904
+ if (!self._support.timeout) {
905
+ timedOut = true;
906
+ request.abort();
907
+ }
920
908
 
921
- /*
922
- * Transform search param object in query string
923
- */
924
- _getSearchParams: function(args, params) {
925
- if (this._isUndefined(args) || args === null) {
926
- return params;
927
- }
928
- for (var key in args) {
929
- if (key !== null && args.hasOwnProperty(key)) {
930
- params += (params.length === 0) ? '?' : '&';
931
- params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]);
932
- }
933
- }
934
- return params;
935
- },
936
- _isUndefined: function(obj) {
937
- return obj === void 0;
938
- },
909
+ opts.callback(true, false, { 'message': 'Timeout - Could not connect to endpoint ' + url } );
910
+ };
911
+
912
+ // do not rely on default XHR async flag, as some analytics code like hotjar
913
+ // breaks it and set it to false by default
914
+ if (request instanceof XMLHttpRequest) {
915
+ request.open(opts.method, url, true);
916
+ } else {
917
+ request.open(opts.method, url);
918
+ }
919
+
920
+ if (this._support.cors && body !== null && opts.method !== 'GET') {
921
+ request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
922
+ }
923
+
924
+ // event object not received in IE8, at least
925
+ // but we do not use it, still important to note
926
+ request.onload = function(/*event*/) {
927
+ // When browser does not supports request.timeout, we can
928
+ // have both a load and timeout event
929
+ if (timedOut) {
930
+ return;
931
+ }
932
+
933
+ if (!self._support.timeout) {
934
+ clearTimeout(ontimeout);
935
+ }
936
+
937
+ var response = null;
938
+
939
+ try {
940
+ response = JSON.parse(request.responseText);
941
+ } catch(e) {}
939
942
 
940
- /// internal attributes
941
- applicationID: null,
942
- apiKey: null,
943
- tagFilters: null,
944
- userToken: null,
945
- hosts: [],
946
- extraHeaders: []
943
+ var status =
944
+ // XHR provides a `status` property
945
+ request.status ||
946
+
947
+ // XDR does not have a `status` property,
948
+ // we rely on our own API response `status`, only
949
+ // provided when an error occurs, so we expect a .message
950
+ response && response.message && response.status ||
951
+
952
+ // XDR default to success when no response.status
953
+ response && 200;
954
+
955
+ var success = status === 200 || status === 201;
956
+ var retry = !success && status !== 400 && status !== 403 && status !== 404;
957
+
958
+ opts.callback(retry, success, response);
959
+ };
960
+
961
+ if (this._support.timeout) {
962
+ // .timeout supported by both XHR and XDR,
963
+ // we do receive timeout event, tested
964
+ request.timeout = this.requestTimeoutInMs * (opts.successiveRetryCount + 1);
965
+
966
+ request.ontimeout = timeoutListener;
967
+ } else {
968
+ ontimeout = setTimeout(timeoutListener, this.requestTimeoutInMs * (opts.successiveRetryCount + 1));
969
+ }
970
+
971
+ request.onerror = function(event) {
972
+ if (timedOut) {
973
+ return;
974
+ }
975
+
976
+ if (!self._support.timeout) {
977
+ clearTimeout(ontimeout);
978
+ }
979
+
980
+ // error event is trigerred both with XDR/XHR on:
981
+ // - DNS error
982
+ // - unallowed cross domain request
983
+ opts.callback(true, false, { 'message': 'Could not connect to host', 'error': event } );
984
+ };
985
+
986
+ request.send(body);
987
+ },
988
+
989
+ /*
990
+ * Transform search param object in query string
991
+ */
992
+ _getSearchParams: function(args, params) {
993
+ if (this._isUndefined(args) || args === null) {
994
+ return params;
995
+ }
996
+ for (var key in args) {
997
+ if (key !== null && args.hasOwnProperty(key)) {
998
+ params += (params.length === 0) ? '?' : '&';
999
+ params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]);
1000
+ }
1001
+ }
1002
+ return params;
1003
+ },
1004
+ _isUndefined: function(obj) {
1005
+ return obj === void 0;
1006
+ },
1007
+
1008
+ _support: {
1009
+ hasXMLHttpRequest: 'XMLHttpRequest' in window,
1010
+ hasXDomainRequest: 'XDomainRequest' in window,
1011
+ cors: 'withCredentials' in new XMLHttpRequest(),
1012
+ timeout: 'timeout' in new XMLHttpRequest()
1013
+ }
947
1014
  };
948
1015
 
949
1016
  /*
@@ -951,575 +1018,571 @@ AlgoliaSearch.prototype = {
951
1018
  * You should use AlgoliaSearch.initIndex(indexName) to retrieve this object
952
1019
  */
953
1020
  AlgoliaSearch.prototype.Index.prototype = {
954
- /*
955
- * Clear all queries in cache
956
- */
957
- clearCache: function() {
958
- this.cache = {};
959
- },
960
- /*
961
- * Add an object in this index
962
- *
963
- * @param content contains the javascript object to add inside the index
964
- * @param callback (optional) the result callback with two arguments:
965
- * success: boolean set to true if the request was successfull
966
- * content: the server answer that contains 3 elements: createAt, taskId and objectID
967
- * @param objectID (optional) an objectID you want to attribute to this object
968
- * (if the attribute already exist the old object will be overwrite)
969
- */
970
- addObject: function(content, callback, objectID) {
971
- var indexObj = this;
972
- if (this.as._isUndefined(objectID)) {
973
- return this.as._jsonRequest({ method: 'POST',
974
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName),
975
- body: content,
976
- callback: callback });
977
- } else {
978
- return this.as._jsonRequest({ method: 'PUT',
979
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
980
- body: content,
981
- callback: callback });
982
- }
1021
+ /*
1022
+ * Clear all queries in cache
1023
+ */
1024
+ clearCache: function() {
1025
+ this.cache = {};
1026
+ },
1027
+ /*
1028
+ * Add an object in this index
1029
+ *
1030
+ * @param content contains the javascript object to add inside the index
1031
+ * @param callback (optional) the result callback with two arguments:
1032
+ * success: boolean set to true if the request was successfull
1033
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
1034
+ * @param objectID (optional) an objectID you want to attribute to this object
1035
+ * (if the attribute already exist the old object will be overwrite)
1036
+ */
1037
+ addObject: function(content, callback, objectID) {
1038
+ var indexObj = this;
1039
+ if (this.as._isUndefined(objectID)) {
1040
+ return this.as._jsonRequest({ method: 'POST',
1041
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName),
1042
+ body: content,
1043
+ callback: callback });
1044
+ } else {
1045
+ return this.as._jsonRequest({ method: 'PUT',
1046
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
1047
+ body: content,
1048
+ callback: callback });
1049
+ }
983
1050
 
984
- },
985
- /*
986
- * Add several objects
987
- *
988
- * @param objects contains an array of objects to add
989
- * @param callback (optional) the result callback with two arguments:
990
- * success: boolean set to true if the request was successfull
991
- * content: the server answer that updateAt and taskID
992
- */
993
- addObjects: function(objects, callback) {
994
- var indexObj = this;
995
- var postObj = {requests:[]};
996
- for (var i = 0; i < objects.length; ++i) {
997
- var request = { action: 'addObject',
998
- body: objects[i] };
999
- postObj.requests.push(request);
1000
- }
1001
- return this.as._jsonRequest({ method: 'POST',
1002
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1003
- body: postObj,
1004
- callback: callback });
1005
- },
1006
- /*
1007
- * Get an object from this index
1008
- *
1009
- * @param objectID the unique identifier of the object to retrieve
1010
- * @param callback (optional) the result callback with two arguments
1011
- * success: boolean set to true if the request was successfull
1012
- * content: the object to retrieve or the error message if a failure occured
1013
- * @param attributes (optional) if set, contains the array of attribute names to retrieve
1014
- */
1015
- getObject: function(objectID, callback, attributes) {
1016
- if (Object.prototype.toString.call(callback) === '[object Array]' && !attributes) {
1017
- attributes = callback;
1018
- callback = null;
1019
- }
1020
- var indexObj = this;
1021
- var params = '';
1022
- if (!this.as._isUndefined(attributes)) {
1023
- params = '?attributes=';
1024
- for (var i = 0; i < attributes.length; ++i) {
1025
- if (i !== 0) {
1026
- params += ',';
1027
- }
1028
- params += attributes[i];
1029
- }
1030
- }
1031
- if (this.as.jsonp === null) {
1032
- return this.as._jsonRequest({ method: 'GET',
1033
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params,
1034
- callback: callback });
1035
- } else {
1036
- var pObj = {params: params};
1037
- return this.as._jsonRequest({ method: 'GET',
1038
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
1039
- callback: callback,
1040
- body: pObj});
1041
- }
1042
- },
1043
-
1044
- /*
1045
- * Update partially an object (only update attributes passed in argument)
1046
- *
1047
- * @param partialObject contains the javascript attributes to override, the
1048
- * object must contains an objectID attribute
1049
- * @param callback (optional) the result callback with two arguments:
1050
- * success: boolean set to true if the request was successfull
1051
- * content: the server answer that contains 3 elements: createAt, taskId and objectID
1052
- */
1053
- partialUpdateObject: function(partialObject, callback) {
1054
- var indexObj = this;
1055
- return this.as._jsonRequest({ method: 'POST',
1056
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial',
1057
- body: partialObject,
1058
- callback: callback });
1059
- },
1060
- /*
1061
- * Partially Override the content of several objects
1062
- *
1063
- * @param objects contains an array of objects to update (each object must contains a objectID attribute)
1064
- * @param callback (optional) the result callback with two arguments:
1065
- * success: boolean set to true if the request was successfull
1066
- * content: the server answer that updateAt and taskID
1067
- */
1068
- partialUpdateObjects: function(objects, callback) {
1069
- var indexObj = this;
1070
- var postObj = {requests:[]};
1071
- for (var i = 0; i < objects.length; ++i) {
1072
- var request = { action: 'partialUpdateObject',
1073
- objectID: objects[i].objectID,
1074
- body: objects[i] };
1075
- postObj.requests.push(request);
1076
- }
1077
- return this.as._jsonRequest({ method: 'POST',
1078
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1079
- body: postObj,
1080
- callback: callback });
1081
- },
1082
- /*
1083
- * Override the content of object
1084
- *
1085
- * @param object contains the javascript object to save, the object must contains an objectID attribute
1086
- * @param callback (optional) the result callback with two arguments:
1087
- * success: boolean set to true if the request was successfull
1088
- * content: the server answer that updateAt and taskID
1089
- */
1090
- saveObject: function(object, callback) {
1091
- var indexObj = this;
1092
- return this.as._jsonRequest({ method: 'PUT',
1093
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID),
1094
- body: object,
1095
- callback: callback });
1096
- },
1097
- /*
1098
- * Override the content of several objects
1099
- *
1100
- * @param objects contains an array of objects to update (each object must contains a objectID attribute)
1101
- * @param callback (optional) the result callback with two arguments:
1102
- * success: boolean set to true if the request was successfull
1103
- * content: the server answer that updateAt and taskID
1104
- */
1105
- saveObjects: function(objects, callback) {
1106
- var indexObj = this;
1107
- var postObj = {requests:[]};
1108
- for (var i = 0; i < objects.length; ++i) {
1109
- var request = { action: 'updateObject',
1110
- objectID: objects[i].objectID,
1111
- body: objects[i] };
1112
- postObj.requests.push(request);
1113
- }
1114
- return this.as._jsonRequest({ method: 'POST',
1115
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1116
- body: postObj,
1117
- callback: callback });
1118
- },
1119
- /*
1120
- * Delete an object from the index
1121
- *
1122
- * @param objectID the unique identifier of object to delete
1123
- * @param callback (optional) the result callback with two arguments:
1124
- * success: boolean set to true if the request was successfull
1125
- * content: the server answer that contains 3 elements: createAt, taskId and objectID
1126
- */
1127
- deleteObject: function(objectID, callback) {
1128
- if (objectID === null || objectID.length === 0) {
1129
- callback(false, { message: 'empty objectID'});
1130
- return;
1131
- }
1132
- var indexObj = this;
1133
- return this.as._jsonRequest({ method: 'DELETE',
1134
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
1135
- callback: callback });
1136
- },
1137
- /*
1138
- * Search inside the index using XMLHttpRequest request (Using a POST query to
1139
- * minimize number of OPTIONS queries: Cross-Origin Resource Sharing).
1140
- *
1141
- * @param query the full text query
1142
- * @param callback the result callback with two arguments:
1143
- * success: boolean set to true if the request was successfull. If false, the content contains the error.
1144
- * content: the server answer that contains the list of results.
1145
- * @param args (optional) if set, contains an object with query parameters:
1146
- * - page: (integer) Pagination parameter used to select the page to retrieve.
1147
- * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
1148
- * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20.
1149
- * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).
1150
- * Attributes are separated with a comma (for example "name,address").
1151
- * You can also use a string array encoding (for example ["name","address"]).
1152
- * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index.
1153
- * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query.
1154
- * Attributes are separated by a comma. You can also use a string array encoding (for example ["name","address"]).
1155
- * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted.
1156
- * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted.
1157
- * A matchLevel is returned for each highlighted attribute and can contain:
1158
- * - full: if all the query terms were found in the attribute,
1159
- * - partial: if only some of the query terms were found,
1160
- * - none: if none of the query terms were found.
1161
- * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`).
1162
- * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10).
1163
- * You can also use a string array encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is computed.
1164
- * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3.
1165
- * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7.
1166
- * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute.
1167
- * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma).
1168
- * For example aroundLatLng=47.316669,5.016670).
1169
- * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision
1170
- * (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).
1171
- * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
1172
- * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).
1173
- * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201).
1174
- * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
1175
- * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma.
1176
- * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`.
1177
- * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000.
1178
- * You can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]).
1179
- * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas.
1180
- * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3).
1181
- * You can also use a string array encoding, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3).
1182
- * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}).
1183
- * - facetFilters: filter the query by a list of facets.
1184
- * Facets are separated by commas and each facet is encoded as `attributeName:value`.
1185
- * For example: `facetFilters=category:Book,author:John%20Doe`.
1186
- * You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`).
1187
- * - facets: List of object attributes that you want to use for faceting.
1188
- * Attributes are separated with a comma (for example `"category,author"` ).
1189
- * You can also use a JSON string array encoding (for example ["category","author"]).
1190
- * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter.
1191
- * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**.
1192
- * - queryType: select how the query words are interpreted, it can be one of the following value:
1193
- * - prefixAll: all query words are interpreted as prefixes,
1194
- * - prefixLast: only the last word is interpreted as a prefix (default behavior),
1195
- * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
1196
- * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query.
1197
- * The list of words is comma separated.
1198
- * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set.
1199
- * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter,
1200
- * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results.
1201
- * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best
1202
- * one is kept and others are removed.
1203
- * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime.
1204
- */
1205
- search: function(query, callback, args, delay) {
1206
- if (query === undefined || query === null) {
1207
- query = '';
1208
- }
1051
+ },
1052
+ /*
1053
+ * Add several objects
1054
+ *
1055
+ * @param objects contains an array of objects to add
1056
+ * @param callback (optional) the result callback with two arguments:
1057
+ * success: boolean set to true if the request was successfull
1058
+ * content: the server answer that updateAt and taskID
1059
+ */
1060
+ addObjects: function(objects, callback) {
1061
+ var indexObj = this;
1062
+ var postObj = {requests:[]};
1063
+ for (var i = 0; i < objects.length; ++i) {
1064
+ var request = { action: 'addObject',
1065
+ body: objects[i] };
1066
+ postObj.requests.push(request);
1067
+ }
1068
+ return this.as._jsonRequest({ method: 'POST',
1069
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1070
+ body: postObj,
1071
+ callback: callback });
1072
+ },
1073
+ /*
1074
+ * Get an object from this index
1075
+ *
1076
+ * @param objectID the unique identifier of the object to retrieve
1077
+ * @param callback (optional) the result callback with two arguments
1078
+ * success: boolean set to true if the request was successfull
1079
+ * content: the object to retrieve or the error message if a failure occured
1080
+ * @param attributes (optional) if set, contains the array of attribute names to retrieve
1081
+ */
1082
+ getObject: function(objectID, callback, attributes) {
1083
+ if (Object.prototype.toString.call(callback) === '[object Array]' && !attributes) {
1084
+ attributes = callback;
1085
+ callback = null;
1086
+ }
1087
+ var indexObj = this;
1088
+ var params = '';
1089
+ if (!this.as._isUndefined(attributes)) {
1090
+ params = '?attributes=';
1091
+ for (var i = 0; i < attributes.length; ++i) {
1092
+ if (i !== 0) {
1093
+ params += ',';
1094
+ }
1095
+ params += attributes[i];
1096
+ }
1097
+ }
1209
1098
 
1210
- // no query = getAllObjects
1211
- if (typeof query === 'function') {
1212
- callback = query;
1213
- query = '';
1214
- }
1099
+ return this.as._jsonRequest({ method: 'GET',
1100
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params,
1101
+ callback: callback });
1102
+ },
1103
+
1104
+ /*
1105
+ * Update partially an object (only update attributes passed in argument)
1106
+ *
1107
+ * @param partialObject contains the javascript attributes to override, the
1108
+ * object must contains an objectID attribute
1109
+ * @param callback (optional) the result callback with two arguments:
1110
+ * success: boolean set to true if the request was successfull
1111
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
1112
+ */
1113
+ partialUpdateObject: function(partialObject, callback) {
1114
+ var indexObj = this;
1115
+ return this.as._jsonRequest({ method: 'POST',
1116
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial',
1117
+ body: partialObject,
1118
+ callback: callback });
1119
+ },
1120
+ /*
1121
+ * Partially Override the content of several objects
1122
+ *
1123
+ * @param objects contains an array of objects to update (each object must contains a objectID attribute)
1124
+ * @param callback (optional) the result callback with two arguments:
1125
+ * success: boolean set to true if the request was successfull
1126
+ * content: the server answer that updateAt and taskID
1127
+ */
1128
+ partialUpdateObjects: function(objects, callback) {
1129
+ var indexObj = this;
1130
+ var postObj = {requests:[]};
1131
+ for (var i = 0; i < objects.length; ++i) {
1132
+ var request = { action: 'partialUpdateObject',
1133
+ objectID: objects[i].objectID,
1134
+ body: objects[i] };
1135
+ postObj.requests.push(request);
1136
+ }
1137
+ return this.as._jsonRequest({ method: 'POST',
1138
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1139
+ body: postObj,
1140
+ callback: callback });
1141
+ },
1142
+ /*
1143
+ * Override the content of object
1144
+ *
1145
+ * @param object contains the javascript object to save, the object must contains an objectID attribute
1146
+ * @param callback (optional) the result callback with two arguments:
1147
+ * success: boolean set to true if the request was successfull
1148
+ * content: the server answer that updateAt and taskID
1149
+ */
1150
+ saveObject: function(object, callback) {
1151
+ var indexObj = this;
1152
+ return this.as._jsonRequest({ method: 'PUT',
1153
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID),
1154
+ body: object,
1155
+ callback: callback });
1156
+ },
1157
+ /*
1158
+ * Override the content of several objects
1159
+ *
1160
+ * @param objects contains an array of objects to update (each object must contains a objectID attribute)
1161
+ * @param callback (optional) the result callback with two arguments:
1162
+ * success: boolean set to true if the request was successfull
1163
+ * content: the server answer that updateAt and taskID
1164
+ */
1165
+ saveObjects: function(objects, callback) {
1166
+ var indexObj = this;
1167
+ var postObj = {requests:[]};
1168
+ for (var i = 0; i < objects.length; ++i) {
1169
+ var request = { action: 'updateObject',
1170
+ objectID: objects[i].objectID,
1171
+ body: objects[i] };
1172
+ postObj.requests.push(request);
1173
+ }
1174
+ return this.as._jsonRequest({ method: 'POST',
1175
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch',
1176
+ body: postObj,
1177
+ callback: callback });
1178
+ },
1179
+ /*
1180
+ * Delete an object from the index
1181
+ *
1182
+ * @param objectID the unique identifier of object to delete
1183
+ * @param callback (optional) the result callback with two arguments:
1184
+ * success: boolean set to true if the request was successfull
1185
+ * content: the server answer that contains 3 elements: createAt, taskId and objectID
1186
+ */
1187
+ deleteObject: function(objectID, callback) {
1188
+ if (objectID === null || objectID.length === 0) {
1189
+ callback(false, { message: 'empty objectID'});
1190
+ return;
1191
+ }
1192
+ var indexObj = this;
1193
+ return this.as._jsonRequest({ method: 'DELETE',
1194
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID),
1195
+ callback: callback });
1196
+ },
1197
+ /*
1198
+ * Search inside the index using XMLHttpRequest request (Using a POST query to
1199
+ * minimize number of OPTIONS queries: Cross-Origin Resource Sharing).
1200
+ *
1201
+ * @param query the full text query
1202
+ * @param callback the result callback with two arguments:
1203
+ * success: boolean set to true if the request was successfull. If false, the content contains the error.
1204
+ * content: the server answer that contains the list of results.
1205
+ * @param args (optional) if set, contains an object with query parameters:
1206
+ * - page: (integer) Pagination parameter used to select the page to retrieve.
1207
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
1208
+ * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20.
1209
+ * - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).
1210
+ * Attributes are separated with a comma (for example "name,address").
1211
+ * You can also use an array (for example ["name","address"]).
1212
+ * By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index.
1213
+ * - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query.
1214
+ * Attributes are separated by a comma. You can also use an array (for example ["name","address"]).
1215
+ * If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted.
1216
+ * You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted.
1217
+ * A matchLevel is returned for each highlighted attribute and can contain:
1218
+ * - full: if all the query terms were found in the attribute,
1219
+ * - partial: if only some of the query terms were found,
1220
+ * - none: if none of the query terms were found.
1221
+ * - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`).
1222
+ * Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10).
1223
+ * You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). By default no snippet is computed.
1224
+ * - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3.
1225
+ * - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7.
1226
+ * - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute.
1227
+ * - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma).
1228
+ * For example aroundLatLng=47.316669,5.016670).
1229
+ * You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision
1230
+ * (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).
1231
+ * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
1232
+ * - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).
1233
+ * For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201).
1234
+ * At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}})
1235
+ * - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma.
1236
+ * The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`.
1237
+ * You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000.
1238
+ * You can also use an array (for example numericFilters: ["price>100","price<1000"]).
1239
+ * - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas.
1240
+ * To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3).
1241
+ * You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3).
1242
+ * At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}).
1243
+ * - facetFilters: filter the query by a list of facets.
1244
+ * Facets are separated by commas and each facet is encoded as `attributeName:value`.
1245
+ * For example: `facetFilters=category:Book,author:John%20Doe`.
1246
+ * You can also use an array (for example `["category:Book","author:John%20Doe"]`).
1247
+ * - facets: List of object attributes that you want to use for faceting.
1248
+ * Comma separated list: `"category,author"` or array `['category','author']`
1249
+ * Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter.
1250
+ * You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**.
1251
+ * - queryType: select how the query words are interpreted, it can be one of the following value:
1252
+ * - prefixAll: all query words are interpreted as prefixes,
1253
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
1254
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
1255
+ * - optionalWords: a string that contains the list of words that should be considered as optional when found in the query.
1256
+ * Comma separated and array are accepted.
1257
+ * - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set.
1258
+ * This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter,
1259
+ * all hits containing a duplicate value for the attributeForDistinct attribute are removed from results.
1260
+ * For example, if the chosen attribute is show_name and several hits have the same value for show_name, then only the best
1261
+ * one is kept and others are removed.
1262
+ * - restrictSearchableAttributes: List of attributes you want to use for textual search (must be a subset of the attributesToIndex index setting)
1263
+ * either comma separated or as an array
1264
+ * @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime.
1265
+ */
1266
+ search: function(query, callback, args, delay) {
1267
+ if (query === undefined || query === null) {
1268
+ query = '';
1269
+ }
1215
1270
 
1216
- if (typeof callback === 'object' && (this.as._isUndefined(args) || !args)) {
1217
- args = callback;
1218
- callback = null;
1219
- }
1271
+ // no query = getAllObjects
1272
+ if (typeof query === 'function') {
1273
+ callback = query;
1274
+ query = '';
1275
+ }
1220
1276
 
1221
- var indexObj = this;
1222
- var params = 'query=' + encodeURIComponent(query);
1223
- if (!this.as._isUndefined(args) && args !== null) {
1224
- params = this.as._getSearchParams(args, params);
1225
- }
1226
- window.clearTimeout(indexObj.onDelayTrigger);
1227
- if (!this.as._isUndefined(delay) && delay !== null && delay > 0) {
1228
- var onDelayTrigger = window.setTimeout( function() {
1229
- indexObj._search(params, callback);
1230
- }, delay);
1231
- indexObj.onDelayTrigger = onDelayTrigger;
1232
- } else {
1233
- return this._search(params, callback);
1234
- }
1235
- },
1236
-
1237
- /*
1238
- * Browse all index content
1239
- *
1240
- * @param page Pagination parameter used to select the page to retrieve.
1241
- * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
1242
- * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000.
1243
- */
1244
- browse: function(page, callback, hitsPerPage) {
1245
- if (+callback > 0 && (this.as._isUndefined(hitsPerPage) || !hitsPerPage)) {
1246
- hitsPerPage = callback;
1247
- callback = null;
1248
- }
1249
- var indexObj = this;
1250
- var params = '?page=' + page;
1251
- if (!this.as._isUndefined(hitsPerPage)) {
1252
- params += '&hitsPerPage=' + hitsPerPage;
1253
- }
1254
- return this.as._jsonRequest({ method: 'GET',
1255
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params,
1256
- callback: callback });
1257
- },
1258
-
1259
- /*
1260
- * Get a Typeahead.js adapter
1261
- * @param searchParams contains an object with query parameters (see search for details)
1262
- */
1263
- ttAdapter: function(params) {
1264
- var self = this;
1265
- return function(query, cb) {
1266
- self.search(query, function(success, content) {
1267
- if (success) {
1268
- cb(content.hits);
1269
- }
1270
- }, params);
1271
- };
1272
- },
1273
-
1274
- /*
1275
- * Wait the publication of a task on the server.
1276
- * All server task are asynchronous and you can check with this method that the task is published.
1277
- *
1278
- * @param taskID the id of the task returned by server
1279
- * @param callback the result callback with with two arguments:
1280
- * success: boolean set to true if the request was successfull
1281
- * content: the server answer that contains the list of results
1282
- */
1283
- waitTask: function(taskID, callback) {
1284
- var indexObj = this;
1285
- return this.as._jsonRequest({ method: 'GET',
1286
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID,
1287
- callback: function(success, body) {
1288
- if (success) {
1289
- if (body.status === 'published') {
1290
- callback(true, body);
1291
- } else {
1292
- setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100);
1293
- }
1294
- } else {
1295
- callback(false, body);
1296
- }
1297
- }});
1298
- },
1299
-
1300
- /*
1301
- * This function deletes the index content. Settings and index specific API keys are kept untouched.
1302
- *
1303
- * @param callback (optional) the result callback with two arguments
1304
- * success: boolean set to true if the request was successfull
1305
- * content: the settings object or the error message if a failure occured
1306
- */
1307
- clearIndex: function(callback) {
1308
- var indexObj = this;
1309
- return this.as._jsonRequest({ method: 'POST',
1310
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear',
1311
- callback: callback });
1312
- },
1313
- /*
1314
- * Get settings of this index
1315
- *
1316
- * @param callback (optional) the result callback with two arguments
1317
- * success: boolean set to true if the request was successfull
1318
- * content: the settings object or the error message if a failure occured
1319
- */
1320
- getSettings: function(callback) {
1321
- var indexObj = this;
1322
- return this.as._jsonRequest({ method: 'GET',
1323
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
1324
- callback: callback });
1325
- },
1326
-
1327
- /*
1328
- * Set settings for this index
1329
- *
1330
- * @param settigns the settings object that can contains :
1331
- * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3).
1332
- * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7).
1333
- * - hitsPerPage: (integer) the number of hits per page (default = 10).
1334
- * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects.
1335
- * If set to null, all attributes are retrieved.
1336
- * - attributesToHighlight: (array of strings) default list of attributes to highlight.
1337
- * If set to null, all indexed attributes are highlighted.
1338
- * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords).
1339
- * By default no snippet is computed. If set to null, no snippet is computed.
1340
- * - attributesToIndex: (array of strings) the list of fields you want to index.
1341
- * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results.
1342
- * This parameter has two important uses:
1343
- * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to
1344
- * retrieve it but you don't want to search in the base64 string.
1345
- * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of
1346
- * the list will be considered more important than matches in attributes further down the list.
1347
- * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable
1348
- * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"].
1349
- * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting.
1350
- * 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.
1351
- * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled
1352
- * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results.
1353
- * 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.
1354
- * - ranking: (array of strings) controls the way results are sorted.
1355
- * We have six available criteria:
1356
- * - typo: sort according to number of typos,
1357
- * - geo: sort according to decreassing distance when performing a geo-location based search,
1358
- * - proximity: sort according to the proximity of query words in hits,
1359
- * - attribute: sort according to the order of attributes defined by attributesToIndex,
1360
- * - exact:
1361
- * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others.
1362
- * 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
1363
- * show starting by the v letter before it.
1364
- * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix).
1365
- * - custom: sort according to a user defined formula set in **customRanking** attribute.
1366
- * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"]
1367
- * - customRanking: (array of strings) lets you specify part of the ranking.
1368
- * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator.
1369
- * For example `"customRanking" => ["desc(population)", "asc(name)"]`
1370
- * - queryType: Select how the query words are interpreted, it can be one of the following value:
1371
- * - prefixAll: all query words are interpreted as prefixes,
1372
- * - prefixLast: only the last word is interpreted as a prefix (default behavior),
1373
- * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
1374
- * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>").
1375
- * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>").
1376
- * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query.
1377
- * @param callback (optional) the result callback with two arguments
1378
- * success: boolean set to true if the request was successfull
1379
- * content: the server answer or the error message if a failure occured
1380
- */
1381
- setSettings: function(settings, callback) {
1382
- var indexObj = this;
1383
- return this.as._jsonRequest({ method: 'PUT',
1384
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
1385
- body: settings,
1386
- callback: callback });
1387
- },
1388
- /*
1389
- * List all existing user keys associated to this index
1390
- *
1391
- * @param callback the result callback with two arguments
1392
- * success: boolean set to true if the request was successfull
1393
- * content: the server answer with user keys list or error description if success is false.
1394
- */
1395
- listUserKeys: function(callback) {
1396
- var indexObj = this;
1397
- return this.as._jsonRequest({ method: 'GET',
1398
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1399
- callback: callback });
1400
- },
1401
- /*
1402
- * Get ACL of a user key associated to this index
1403
- *
1404
- * @param callback the result callback with two arguments
1405
- * success: boolean set to true if the request was successfull
1406
- * content: the server answer with user keys list or error description if success is false.
1407
- */
1408
- getUserKeyACL: function(key, callback) {
1409
- var indexObj = this;
1410
- return this.as._jsonRequest({ method: 'GET',
1411
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1412
- callback: callback });
1413
- },
1414
- /*
1415
- * Delete an existing user key associated to this index
1416
- *
1417
- * @param callback the result callback with two arguments
1418
- * success: boolean set to true if the request was successfull
1419
- * content: the server answer with user keys list or error description if success is false.
1420
- */
1421
- deleteUserKey: function(key, callback) {
1422
- var indexObj = this;
1423
- return this.as._jsonRequest({ method: 'DELETE',
1424
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1425
- callback: callback });
1426
- },
1427
- /*
1428
- * Add an existing user key associated to this index
1429
- *
1430
- * @param acls the list of ACL for this key. Defined by an array of strings that
1431
- * can contains the following values:
1432
- * - search: allow to search (https and http)
1433
- * - addObject: allows to add/update an object in the index (https only)
1434
- * - deleteObject : allows to delete an existing object (https only)
1435
- * - deleteIndex : allows to delete index content (https only)
1436
- * - settings : allows to get index settings (https only)
1437
- * - editSettings : allows to change index settings (https only)
1438
- * @param callback the result callback with two arguments
1439
- * success: boolean set to true if the request was successfull
1440
- * content: the server answer with user keys list or error description if success is false.
1441
- */
1442
- addUserKey: function(acls, callback) {
1443
- var indexObj = this;
1444
- var aclsObject = {};
1445
- aclsObject.acl = acls;
1446
- return this.as._jsonRequest({ method: 'POST',
1447
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1448
- body: aclsObject,
1449
- callback: callback });
1450
- },
1451
- /*
1452
- * Add an existing user key associated to this index
1453
- *
1454
- * @param acls the list of ACL for this key. Defined by an array of strings that
1455
- * can contains the following values:
1456
- * - search: allow to search (https and http)
1457
- * - addObject: allows to add/update an object in the index (https only)
1458
- * - deleteObject : allows to delete an existing object (https only)
1459
- * - deleteIndex : allows to delete index content (https only)
1460
- * - settings : allows to get index settings (https only)
1461
- * - editSettings : allows to change index settings (https only)
1462
- * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
1463
- * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
1464
- * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
1465
- * @param callback the result callback with two arguments
1466
- * success: boolean set to true if the request was successfull
1467
- * content: the server answer with user keys list or error description if success is false.
1468
- */
1469
- addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
1470
- var indexObj = this;
1471
- var aclsObject = {};
1472
- aclsObject.acl = acls;
1473
- aclsObject.validity = validity;
1474
- aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
1475
- aclsObject.maxHitsPerQuery = maxHitsPerQuery;
1476
- return this.as._jsonRequest({ method: 'POST',
1477
- url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1478
- body: aclsObject,
1479
- callback: callback });
1480
- },
1481
- ///
1482
- /// Internal methods only after this line
1483
- ///
1484
- _search: function(params, callback) {
1485
- var pObj = {params: params};
1486
- if (this.as.jsonp === null) {
1487
- var self = this;
1488
- return this.as._jsonRequest({ cache: this.cache,
1489
- method: 'POST',
1490
- url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
1491
- body: pObj,
1492
- callback: function(success, content) {
1493
- if (!success) {
1494
- // retry first with JSONP
1495
- self.as.jsonp = true;
1496
- self._search(params, callback);
1497
- } else {
1498
- self.as.jsonp = false;
1499
- callback && callback(success, content);
1500
- }
1501
- }
1502
- });
1503
- } else if (this.as.jsonp) {
1504
- return this.as._jsonRequest({ cache: this.cache,
1505
- method: 'GET',
1506
- url: '/1/indexes/' + encodeURIComponent(this.indexName),
1507
- body: pObj,
1508
- callback: callback });
1509
- } else {
1510
- return this.as._jsonRequest({ cache: this.cache,
1511
- method: 'POST',
1512
- url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
1513
- body: pObj,
1514
- callback: callback});
1515
- }
1516
- },
1277
+ if (typeof callback === 'object' && (this.as._isUndefined(args) || !args)) {
1278
+ args = callback;
1279
+ callback = null;
1280
+ }
1281
+
1282
+ var indexObj = this;
1283
+ var params = 'query=' + encodeURIComponent(query);
1284
+ if (!this.as._isUndefined(args) && args !== null) {
1285
+ params = this.as._getSearchParams(args, params);
1286
+ }
1287
+ window.clearTimeout(indexObj.onDelayTrigger);
1288
+ if (!this.as._isUndefined(delay) && delay !== null && delay > 0) {
1289
+ var onDelayTrigger = window.setTimeout( function() {
1290
+ indexObj._search(params, callback);
1291
+ }, delay);
1292
+ indexObj.onDelayTrigger = onDelayTrigger;
1293
+ } else {
1294
+ return this._search(params, callback);
1295
+ }
1296
+ },
1297
+
1298
+ /*
1299
+ * Browse all index content
1300
+ *
1301
+ * @param page Pagination parameter used to select the page to retrieve.
1302
+ * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9
1303
+ * @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000.
1304
+ */
1305
+ browse: function(page, callback, hitsPerPage) {
1306
+ if (+callback > 0 && (this.as._isUndefined(hitsPerPage) || !hitsPerPage)) {
1307
+ hitsPerPage = callback;
1308
+ callback = null;
1309
+ }
1310
+ var indexObj = this;
1311
+ var params = '?page=' + page;
1312
+ if (!this.as._isUndefined(hitsPerPage)) {
1313
+ params += '&hitsPerPage=' + hitsPerPage;
1314
+ }
1315
+ return this.as._jsonRequest({ method: 'GET',
1316
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params,
1317
+ callback: callback });
1318
+ },
1319
+
1320
+ /*
1321
+ * Get a Typeahead.js adapter
1322
+ * @param searchParams contains an object with query parameters (see search for details)
1323
+ */
1324
+ ttAdapter: function(params) {
1325
+ var self = this;
1326
+ return function(query, cb) {
1327
+ self.search(query, function(success, content) {
1328
+ if (success) {
1329
+ cb(content.hits);
1330
+ } else {
1331
+ cb(content && content.message);
1332
+ }
1333
+ }, params);
1334
+ };
1335
+ },
1336
+
1337
+ /*
1338
+ * Wait the publication of a task on the server.
1339
+ * All server task are asynchronous and you can check with this method that the task is published.
1340
+ *
1341
+ * @param taskID the id of the task returned by server
1342
+ * @param callback the result callback with with two arguments:
1343
+ * success: boolean set to true if the request was successfull
1344
+ * content: the server answer that contains the list of results
1345
+ */
1346
+ waitTask: function(taskID, callback) {
1347
+ var indexObj = this;
1348
+ return this.as._jsonRequest({ method: 'GET',
1349
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID,
1350
+ callback: function(success, body) {
1351
+ if (success) {
1352
+ if (body.status === 'published') {
1353
+ callback(true, body);
1354
+ } else {
1355
+ setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100);
1356
+ }
1357
+ } else {
1358
+ callback(false, body);
1359
+ }
1360
+ }});
1361
+ },
1362
+
1363
+ /*
1364
+ * This function deletes the index content. Settings and index specific API keys are kept untouched.
1365
+ *
1366
+ * @param callback (optional) the result callback with two arguments
1367
+ * success: boolean set to true if the request was successfull
1368
+ * content: the settings object or the error message if a failure occured
1369
+ */
1370
+ clearIndex: function(callback) {
1371
+ var indexObj = this;
1372
+ return this.as._jsonRequest({ method: 'POST',
1373
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear',
1374
+ callback: callback });
1375
+ },
1376
+ /*
1377
+ * Get settings of this index
1378
+ *
1379
+ * @param callback (optional) the result callback with two arguments
1380
+ * success: boolean set to true if the request was successfull
1381
+ * content: the settings object or the error message if a failure occured
1382
+ */
1383
+ getSettings: function(callback) {
1384
+ var indexObj = this;
1385
+ return this.as._jsonRequest({ method: 'GET',
1386
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
1387
+ callback: callback });
1388
+ },
1389
+
1390
+ /*
1391
+ * Set settings for this index
1392
+ *
1393
+ * @param settigns the settings object that can contains :
1394
+ * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3).
1395
+ * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7).
1396
+ * - hitsPerPage: (integer) the number of hits per page (default = 10).
1397
+ * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects.
1398
+ * If set to null, all attributes are retrieved.
1399
+ * - attributesToHighlight: (array of strings) default list of attributes to highlight.
1400
+ * If set to null, all indexed attributes are highlighted.
1401
+ * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords).
1402
+ * By default no snippet is computed. If set to null, no snippet is computed.
1403
+ * - attributesToIndex: (array of strings) the list of fields you want to index.
1404
+ * If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results.
1405
+ * This parameter has two important uses:
1406
+ * - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to
1407
+ * retrieve it but you don't want to search in the base64 string.
1408
+ * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of
1409
+ * the list will be considered more important than matches in attributes further down the list.
1410
+ * In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable
1411
+ * this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"].
1412
+ * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting.
1413
+ * 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.
1414
+ * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled
1415
+ * in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results.
1416
+ * 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.
1417
+ * - ranking: (array of strings) controls the way results are sorted.
1418
+ * We have six available criteria:
1419
+ * - typo: sort according to number of typos,
1420
+ * - geo: sort according to decreassing distance when performing a geo-location based search,
1421
+ * - proximity: sort according to the proximity of query words in hits,
1422
+ * - attribute: sort according to the order of attributes defined by attributesToIndex,
1423
+ * - exact:
1424
+ * - if the user query contains one word: sort objects having an attribute that is exactly the query word before others.
1425
+ * 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
1426
+ * show starting by the v letter before it.
1427
+ * - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix).
1428
+ * - custom: sort according to a user defined formula set in **customRanking** attribute.
1429
+ * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"]
1430
+ * - customRanking: (array of strings) lets you specify part of the ranking.
1431
+ * The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator.
1432
+ * For example `"customRanking" => ["desc(population)", "asc(name)"]`
1433
+ * - queryType: Select how the query words are interpreted, it can be one of the following value:
1434
+ * - prefixAll: all query words are interpreted as prefixes,
1435
+ * - prefixLast: only the last word is interpreted as a prefix (default behavior),
1436
+ * - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
1437
+ * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>").
1438
+ * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>").
1439
+ * - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query.
1440
+ * @param callback (optional) the result callback with two arguments
1441
+ * success: boolean set to true if the request was successfull
1442
+ * content: the server answer or the error message if a failure occured
1443
+ */
1444
+ setSettings: function(settings, callback) {
1445
+ var indexObj = this;
1446
+ return this.as._jsonRequest({ method: 'PUT',
1447
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings',
1448
+ body: settings,
1449
+ callback: callback });
1450
+ },
1451
+ /*
1452
+ * List all existing user keys associated to this index
1453
+ *
1454
+ * @param callback the result callback with two arguments
1455
+ * success: boolean set to true if the request was successfull
1456
+ * content: the server answer with user keys list or error description if success is false.
1457
+ */
1458
+ listUserKeys: function(callback) {
1459
+ var indexObj = this;
1460
+ return this.as._jsonRequest({ method: 'GET',
1461
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1462
+ callback: callback });
1463
+ },
1464
+ /*
1465
+ * Get ACL of a user key associated to this index
1466
+ *
1467
+ * @param callback the result callback with two arguments
1468
+ * success: boolean set to true if the request was successfull
1469
+ * content: the server answer with user keys list or error description if success is false.
1470
+ */
1471
+ getUserKeyACL: function(key, callback) {
1472
+ var indexObj = this;
1473
+ return this.as._jsonRequest({ method: 'GET',
1474
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1475
+ callback: callback });
1476
+ },
1477
+ /*
1478
+ * Delete an existing user key associated to this index
1479
+ *
1480
+ * @param callback the result callback with two arguments
1481
+ * success: boolean set to true if the request was successfull
1482
+ * content: the server answer with user keys list or error description if success is false.
1483
+ */
1484
+ deleteUserKey: function(key, callback) {
1485
+ var indexObj = this;
1486
+ return this.as._jsonRequest({ method: 'DELETE',
1487
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key,
1488
+ callback: callback });
1489
+ },
1490
+ /*
1491
+ * Add an existing user key associated to this index
1492
+ *
1493
+ * @param acls the list of ACL for this key. Defined by an array of strings that
1494
+ * can contains the following values:
1495
+ * - search: allow to search (https and http)
1496
+ * - addObject: allows to add/update an object in the index (https only)
1497
+ * - deleteObject : allows to delete an existing object (https only)
1498
+ * - deleteIndex : allows to delete index content (https only)
1499
+ * - settings : allows to get index settings (https only)
1500
+ * - editSettings : allows to change index settings (https only)
1501
+ * @param callback the result callback with two arguments
1502
+ * success: boolean set to true if the request was successfull
1503
+ * content: the server answer with user keys list or error description if success is false.
1504
+ */
1505
+ addUserKey: function(acls, callback) {
1506
+ var indexObj = this;
1507
+ var aclsObject = {};
1508
+ aclsObject.acl = acls;
1509
+ return this.as._jsonRequest({ method: 'POST',
1510
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1511
+ body: aclsObject,
1512
+ callback: callback });
1513
+ },
1514
+ /*
1515
+ * Add an existing user key associated to this index
1516
+ *
1517
+ * @param acls the list of ACL for this key. Defined by an array of strings that
1518
+ * can contains the following values:
1519
+ * - search: allow to search (https and http)
1520
+ * - addObject: allows to add/update an object in the index (https only)
1521
+ * - deleteObject : allows to delete an existing object (https only)
1522
+ * - deleteIndex : allows to delete index content (https only)
1523
+ * - settings : allows to get index settings (https only)
1524
+ * - editSettings : allows to change index settings (https only)
1525
+ * @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
1526
+ * @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour.
1527
+ * @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call.
1528
+ * @param callback the result callback with two arguments
1529
+ * success: boolean set to true if the request was successfull
1530
+ * content: the server answer with user keys list or error description if success is false.
1531
+ */
1532
+ addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) {
1533
+ var indexObj = this;
1534
+ var aclsObject = {};
1535
+ aclsObject.acl = acls;
1536
+ aclsObject.validity = validity;
1537
+ aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
1538
+ aclsObject.maxHitsPerQuery = maxHitsPerQuery;
1539
+ return this.as._jsonRequest({ method: 'POST',
1540
+ url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys',
1541
+ body: aclsObject,
1542
+ callback: callback });
1543
+ },
1544
+ ///
1545
+ /// Internal methods only after this line
1546
+ ///
1547
+ _search: function(params, callback) {
1548
+ var pObj = {params: params};
1549
+ if (this.as.jsonp === null) {
1550
+ var self = this;
1551
+ return this.as._jsonRequest({ cache: this.cache,
1552
+ method: 'POST',
1553
+ url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
1554
+ body: pObj,
1555
+ callback: function(success, content) {
1556
+ var status = content && content.status;
1557
+ if (success || status && Math.floor(status / 100) === 4 || Math.floor(status / 100) === 1) {
1558
+ self.as.jsonp = false;
1559
+ callback && callback(success, content);
1560
+ } else {
1561
+ self.as.jsonp = true;
1562
+ self._search(params, callback);
1563
+ }
1564
+ }
1565
+ });
1566
+ } else if (this.as.jsonp) {
1567
+ return this.as._jsonRequest({ cache: this.cache,
1568
+ method: 'GET',
1569
+ url: '/1/indexes/' + encodeURIComponent(this.indexName),
1570
+ body: pObj,
1571
+ callback: callback });
1572
+ } else {
1573
+ return this.as._jsonRequest({ cache: this.cache,
1574
+ method: 'POST',
1575
+ url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query',
1576
+ body: pObj,
1577
+ callback: callback});
1578
+ }
1579
+ },
1517
1580
 
1518
- // internal attributes
1519
- as: null,
1520
- indexName: null,
1521
- typeAheadArgs: null,
1522
- typeAheadValueOption: null
1581
+ // internal attributes
1582
+ as: null,
1583
+ indexName: null,
1584
+ typeAheadArgs: null,
1585
+ typeAheadValueOption: null
1523
1586
  };
1524
1587
 
1525
1588
  /*
@@ -1876,7 +1939,7 @@ AlgoliaSearch.prototype.Index.prototype = {
1876
1939
  }
1877
1940
  var aggregatedAnswer = content.results[0];
1878
1941
  aggregatedAnswer.disjunctiveFacets = aggregatedAnswer.disjunctiveFacets || {};
1879
- aggregatedAnswer.facetStats = aggregatedAnswer.facetStats || {};
1942
+ aggregatedAnswer.facets_stats = aggregatedAnswer.facets_stats || {};
1880
1943
  // create disjunctive facets from facets (disjunctive facets without refinements)
1881
1944
  for (var facet in unusedDisjunctiveFacets) {
1882
1945
  if (aggregatedAnswer.facets[facet] && !aggregatedAnswer.disjunctiveFacets[facet]) {
@@ -1903,9 +1966,13 @@ AlgoliaSearch.prototype.Index.prototype = {
1903
1966
  }
1904
1967
  // aggregate the disjunctive facets stats
1905
1968
  for (var stats in content.results[i + 1].facets_stats) {
1906
- aggregatedAnswer.facetStats[stats] = content.results[i + 1].facets_stats[stats];
1969
+ aggregatedAnswer.facets_stats[stats] = content.results[i + 1].facets_stats[stats];
1907
1970
  }
1908
1971
  }
1972
+
1973
+ // Backward compatibility
1974
+ aggregatedAnswer.facetStats = aggregatedAnswer.facets_stats;
1975
+
1909
1976
  // add the excludes
1910
1977
  for (var exclude in self.excludes) {
1911
1978
  if (self.excludes[exclude]) {
@@ -1969,7 +2036,8 @@ AlgoliaSearch.prototype.Index.prototype = {
1969
2036
  attributesToHighlight: [],
1970
2037
  attributesToSnippet: [],
1971
2038
  facets: facet,
1972
- facetFilters: this._getFacetFilters(facet)
2039
+ facetFilters: this._getFacetFilters(facet),
2040
+ analytics: false
1973
2041
  });
1974
2042
  },
1975
2043