algoliasearch
Advanced tools
Comparing version 2.3.7 to 2.4.0
@@ -1,36 +0,88 @@ | ||
/*! | ||
* algoliasearch 2.3.6 | ||
* https://github.com/algolia/algoliasearch-client-js | ||
* Copyright 2013 Algolia SAS; Licensed MIT | ||
/* | ||
* Copyright (c) 2013 Algolia | ||
* http://www.algolia.com/ | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
var VERSION = "2.3.6"; | ||
var ALGOLIA_VERSION = '2.4.0'; | ||
/* | ||
* Copyright (c) 2013 Algolia | ||
* http://www.algolia.com/ | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
/* | ||
* Algolia Search library initialization | ||
* @param applicationID the application ID you have in your admin interface | ||
* @param apiKey a valid API key for the service | ||
* @param method specify if the protocol used is http or https (http by default to make the first search query faster). | ||
* You need to use https is you are doing something else than just search queries. | ||
* @param resolveDNS let you disable first empty query that is launch to warmup the service | ||
* @param hostsArray (optionnal) the list of hosts that you have received for the service | ||
*/ | ||
var AlgoliaSearch = function(applicationID, apiKey, method, resolveDNS, hostsArray) { | ||
this.applicationID = applicationID; | ||
this.apiKey = apiKey; | ||
if (this._isUndefined(hostsArray)) { | ||
hostsArray = [ applicationID + "-1.algolia.io", applicationID + "-2.algolia.io", applicationID + "-3.algolia.io" ]; | ||
hostsArray = [applicationID + '-1.algolia.io', | ||
applicationID + '-2.algolia.io', | ||
applicationID + '-3.algolia.io']; | ||
} | ||
this.hosts = []; | ||
// Add hosts in random order | ||
for (var i = 0; i < hostsArray.length; ++i) { | ||
if (Math.random() > .5) { | ||
if (Math.random() > 0.5) { | ||
this.hosts.reverse(); | ||
} | ||
if (this._isUndefined(method) || method == null) { | ||
this.hosts.push(("https:" == document.location.protocol ? "https" : "http") + "://" + hostsArray[i]); | ||
} else if (method === "https" || method === "HTTPS") { | ||
this.hosts.push("https://" + hostsArray[i]); | ||
this.hosts.push(('https:' == document.location.protocol ? 'https' : 'http') + '://' + hostsArray[i]); | ||
} else if (method === 'https' || method === 'HTTPS') { | ||
this.hosts.push('https://' + hostsArray[i]); | ||
} else { | ||
this.hosts.push("http://" + hostsArray[i]); | ||
this.hosts.push('http://' + hostsArray[i]); | ||
} | ||
} | ||
if (Math.random() > .5) { | ||
if (Math.random() > 0.5) { | ||
this.hosts.reverse(); | ||
} | ||
if (this._isUndefined(resolveDNS) || resolveDNS) { | ||
this._jsonRequest({ | ||
method: "GET", | ||
url: "/1/isalive" | ||
}); | ||
// Perform a call to solve DNS (avoid to slow down the first user query) | ||
this._jsonRequest({ method: 'GET', | ||
url: '/1/isalive' }); | ||
} | ||
@@ -40,34 +92,124 @@ this.extraHeaders = []; | ||
function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) { | ||
function _getHitAxplainationForOneAttr_recurse(obj, foundWords) { | ||
if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) { | ||
var match = false; | ||
for (var j = 0; j < obj.matchedWords.length; ++j) { | ||
var word = obj.matchedWords[j]; | ||
if (!(word in foundWords)) { | ||
foundWords[word] = 1; | ||
match = true; | ||
} | ||
} | ||
return match ? [obj.value] : []; | ||
} else if (obj instanceof Array) { | ||
var res = []; | ||
for (var i = 0; i < obj.length; ++i) { | ||
var array = _getHitAxplainationForOneAttr_recurse(obj[i], foundWords); | ||
res = res.concat(array); | ||
} | ||
return res; | ||
} else if (typeof obj === 'object') { | ||
var res = []; | ||
for (prop in obj) { | ||
if (obj.hasOwnProperty(prop)){ | ||
res = res.concat(_getHitAxplainationForOneAttr_recurse(obj[prop], foundWords)); | ||
} | ||
} | ||
return res; | ||
} | ||
return []; | ||
} | ||
function _getHitAxplainationForOneAttr(hit, foundWords, attr) { | ||
if (attr.indexOf('.') === -1) { | ||
if (attr in hit._highlightResult) { | ||
return _getHitAxplainationForOneAttr_recurse(hit._highlightResult[attr], foundWords); | ||
} | ||
return []; | ||
} | ||
var array = attr.split('.'); | ||
var obj = hit._highlightResult; | ||
for (var i = 0; i < array.length; ++i) { | ||
if (array[i] in obj) { | ||
obj = obj[array[i]]; | ||
} else { | ||
return []; | ||
} | ||
} | ||
return _getHitAxplainationForOneAttr_recurse(obj, foundWords); | ||
} | ||
var res = {}; | ||
var foundWords = {}; | ||
var title = _getHitAxplainationForOneAttr(hit, foundWords, titleAttribute); | ||
res.title = (title.length > 0) ? title[0] : ""; | ||
res.subtitles = []; | ||
if (typeof otherAttributes !== 'undefined') { | ||
for (var i = 0; i < otherAttributes.length; ++i) { | ||
var attr = _getHitAxplainationForOneAttr(hit, foundWords, otherAttributes[i]); | ||
for (var j = 0; j < attr.length; ++j) { | ||
res.subtitles.push({ attr: otherAttributes[i], value: attr[j] }); | ||
} | ||
} | ||
} | ||
return res; | ||
} | ||
AlgoliaSearch.prototype = { | ||
/* | ||
* Delete an index | ||
* | ||
* @param indexName the name of index to delete | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains the task ID | ||
*/ | ||
deleteIndex: function(indexName, callback) { | ||
this._jsonRequest({ | ||
method: "DELETE", | ||
url: "/1/indexes/" + encodeURIComponent(indexName), | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'DELETE', | ||
url: '/1/indexes/' + encodeURIComponent(indexName), | ||
callback: callback }); | ||
}, | ||
/** | ||
* Move an existing index. | ||
* @param srcIndexName the name of index to copy. | ||
* @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains the task ID | ||
*/ | ||
moveIndex: function(srcIndexName, dstIndexName, callback) { | ||
var postObj = { | ||
operation: "move", | ||
destination: dstIndexName | ||
}; | ||
this._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(srcIndexName) + "/operation", | ||
body: postObj, | ||
callback: callback | ||
}); | ||
var postObj = {operation: 'move', destination: dstIndexName}; | ||
this._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', | ||
body: postObj, | ||
callback: callback }); | ||
}, | ||
/** | ||
* Copy an existing index. | ||
* @param srcIndexName the name of index to copy. | ||
* @param dstIndexName the new index name that will contains a copy of srcIndexName (destination will be overriten if it already exist). | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains the task ID | ||
*/ | ||
copyIndex: function(srcIndexName, dstIndexName, callback) { | ||
var postObj = { | ||
operation: "copy", | ||
destination: dstIndexName | ||
}; | ||
this._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(srcIndexName) + "/operation", | ||
body: postObj, | ||
callback: callback | ||
}); | ||
var postObj = {operation: 'copy', destination: dstIndexName}; | ||
this._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', | ||
body: postObj, | ||
callback: callback }); | ||
}, | ||
/** | ||
* Return last log entries. | ||
* @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). | ||
* @param length Specify the maximum number of entries to retrieve starting at offset. Maximum allowed value: 1000. | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains the task ID | ||
*/ | ||
getLogs: function(callback, offset, length) { | ||
@@ -80,49 +222,106 @@ if (this._isUndefined(offset)) { | ||
} | ||
this._jsonRequest({ | ||
method: "GET", | ||
url: "/1/logs?offset=" + offset + "&length=" + length, | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'GET', | ||
url: '/1/logs?offset=' + offset + '&length=' + length, | ||
callback: callback }); | ||
}, | ||
/* | ||
* List all existing indexes | ||
* | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with index list or error description if success is false. | ||
*/ | ||
listIndexes: function(callback) { | ||
this._jsonRequest({ | ||
method: "GET", | ||
url: "/1/indexes/", | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'GET', | ||
url: '/1/indexes/', | ||
callback: callback }); | ||
}, | ||
/* | ||
* Get the index object initialized | ||
* | ||
* @param indexName the name of index | ||
* @param callback the result callback with one argument (the Index instance) | ||
*/ | ||
initIndex: function(indexName) { | ||
return new this.Index(this, indexName); | ||
}, | ||
/* | ||
* List all existing user keys with their associated ACLs | ||
* | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
listUserKeys: function(callback) { | ||
this._jsonRequest({ | ||
method: "GET", | ||
url: "/1/keys", | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'GET', | ||
url: '/1/keys', | ||
callback: callback }); | ||
}, | ||
/* | ||
* Get ACL of a user key | ||
* | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
getUserKeyACL: function(key, callback) { | ||
this._jsonRequest({ | ||
method: "GET", | ||
url: "/1/keys/" + key, | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'GET', | ||
url: '/1/keys/' + key, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Delete an existing user key | ||
* | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
deleteUserKey: function(key, callback) { | ||
this._jsonRequest({ | ||
method: "DELETE", | ||
url: "/1/keys/" + key, | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'DELETE', | ||
url: '/1/keys/' + key, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Add an existing user key | ||
* | ||
* @param acls the list of ACL for this key. Defined by an array of strings that | ||
* can contains the following values: | ||
* - search: allow to search (https and http) | ||
* - addObject: allows to add/update an object in the index (https only) | ||
* - deleteObject : allows to delete an existing object (https only) | ||
* - deleteIndex : allows to delete index content (https only) | ||
* - settings : allows to get index settings (https only) | ||
* - editSettings : allows to change index settings (https only) | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
addUserKey: function(acls, callback) { | ||
var aclsObject = {}; | ||
aclsObject.acl = acls; | ||
this._jsonRequest({ | ||
method: "POST", | ||
url: "/1/keys", | ||
body: aclsObject, | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'POST', | ||
url: '/1/keys', | ||
body: aclsObject, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Add an existing user key | ||
* | ||
* @param acls the list of ACL for this key. Defined by an array of strings that | ||
* can contains the following values: | ||
* - search: allow to search (https and http) | ||
* - addObject: allows to add/update an object in the index (https only) | ||
* - deleteObject : allows to delete an existing object (https only) | ||
* - deleteIndex : allows to delete index content (https only) | ||
* - settings : allows to get index settings (https only) | ||
* - editSettings : allows to change index settings (https only) | ||
* @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) | ||
* @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. | ||
* @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { | ||
@@ -135,32 +334,54 @@ var indexObj = this; | ||
aclsObject.maxHitsPerQuery = maxHitsPerQuery; | ||
this._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + indexObj.indexName + "/keys", | ||
body: aclsObject, | ||
callback: callback | ||
}); | ||
this._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + indexObj.indexName + '/keys', | ||
body: aclsObject, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Initialize a new batch of search queries | ||
*/ | ||
startQueriesBatch: function() { | ||
this.batch = []; | ||
}, | ||
/* | ||
* Add a search query in the batch | ||
* | ||
* @param query the full text query | ||
* @param args (optional) if set, contains an object with query parameters: | ||
* - attributes: an array of object attribute names to retrieve | ||
* (if not set all attributes are retrieve) | ||
* - attributesToHighlight: an array of object attribute names to highlight | ||
* (if not set indexed attributes are highlighted) | ||
* - minWordSizefor1Typo: the minimum number of characters to accept one typo. | ||
* Defaults to 3. | ||
* - minWordSizefor2Typos: the minimum number of characters to accept two typos. | ||
* Defaults to 7. | ||
* - getRankingInfo: if set, the result hits will contain ranking information in | ||
* _rankingInfo attribute | ||
* - page: (pagination parameter) page to retrieve (zero base). Defaults to 0. | ||
* - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10. | ||
*/ | ||
addQueryInBatch: function(indexName, query, args) { | ||
var params = "query=" + encodeURIComponent(query); | ||
var params = 'query=' + encodeURIComponent(query); | ||
if (!this._isUndefined(args) && args != null) { | ||
params = this._getSearchParams(args, params); | ||
} | ||
this.batch.push({ | ||
indexName: indexName, | ||
params: params | ||
}); | ||
this.batch.push({ indexName: indexName, params: params }); | ||
}, | ||
/* | ||
* Clear all queries in cache | ||
*/ | ||
clearCache: function() { | ||
this.cache = {}; | ||
}, | ||
/* | ||
* Launch the batch of queries using XMLHttpRequest. | ||
* (Optimized for browser using a POST query to minimize number of OPTIONS queries) | ||
* | ||
* @param callback the function that will receive results | ||
* @param delay (optional) if set, wait for this delay (in ms) and only send the batch if there was no other in the meantime. | ||
*/ | ||
sendQueriesBatch: function(callback, delay) { | ||
var as = this; | ||
var params = { | ||
requests: [], | ||
apiKey: this.apiKey, | ||
appID: this.applicationID | ||
}; | ||
var params = {requests: [], apiKey: this.apiKey, appID: this.applicationID}; | ||
for (var i = 0; i < as.batch.length; ++i) { | ||
@@ -171,3 +392,3 @@ params.requests.push(as.batch[i]); | ||
if (!this._isUndefined(delay) && delay != null && delay > 0) { | ||
var onDelayTrigger = window.setTimeout(function() { | ||
var onDelayTrigger = window.setTimeout( function() { | ||
as._sendQueriesBatch(params, callback); | ||
@@ -180,2 +401,6 @@ }, delay); | ||
}, | ||
/* | ||
* Index class constructor. | ||
* You should not use this method directly but use initIndex() function | ||
*/ | ||
Index: function(algoliasearch, indexName) { | ||
@@ -187,17 +412,17 @@ this.indexName = indexName; | ||
}, | ||
setExtraHeader: function(key, value) { | ||
this.extraHeaders.push({ | ||
key: key, | ||
value: value | ||
}); | ||
this.extraHeaders.push({ key: key, value: value}); | ||
}, | ||
_sendQueriesBatch: function(params, callback) { | ||
this._jsonRequest({ | ||
cache: this.cache, | ||
method: "POST", | ||
url: "/1/indexes/*/queries", | ||
body: params, | ||
callback: callback | ||
}); | ||
this._jsonRequest({ cache: this.cache, | ||
method: 'POST', | ||
url: '/1/indexes/*/queries', | ||
body: params, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Wrapper that try all hosts to maximize the quality of service | ||
*/ | ||
_jsonRequest: function(opts) { | ||
@@ -209,3 +434,3 @@ var self = this; | ||
if (!this._isUndefined(opts.body)) { | ||
cacheID = opts.url + "_body_" + JSON.stringify(opts.body); | ||
cacheID = opts.url + '_body_' + JSON.stringify(opts.body); | ||
} | ||
@@ -221,2 +446,3 @@ if (!this._isUndefined(opts.cache)) { | ||
} | ||
var impl = function(position) { | ||
@@ -229,5 +455,3 @@ var idx = 0; | ||
if (!self._isUndefined(callback)) { | ||
callback(false, { | ||
message: "Cannot contact server" | ||
}); | ||
callback(false, { message: 'Cannot contact server'}); | ||
} | ||
@@ -238,3 +462,3 @@ return; | ||
if (!success && !self._isUndefined(body)) { | ||
console.log("Error: " + body.message); | ||
console.log('Error: ' + body.message); | ||
} | ||
@@ -244,3 +468,3 @@ if (success && !self._isUndefined(opts.cache)) { | ||
} | ||
if (!success && retry && idx + 1 < self.hosts.length) { | ||
if (!success && retry && (idx + 1) < self.hosts.length) { | ||
impl(idx + 1); | ||
@@ -258,2 +482,3 @@ } else { | ||
}, | ||
_jsonRequestByHost: function(opts) { | ||
@@ -267,7 +492,8 @@ var body = null; | ||
var xmlHttp = null; | ||
xmlHttp = new XMLHttpRequest(); | ||
if ("withCredentials" in xmlHttp) { | ||
xmlHttp.open(opts.method, url, true); | ||
xmlHttp.setRequestHeader("X-Algolia-API-Key", this.apiKey); | ||
xmlHttp.setRequestHeader("X-Algolia-Application-Id", this.applicationID); | ||
if ('withCredentials' in xmlHttp) { | ||
xmlHttp.open(opts.method, url , true); | ||
xmlHttp.setRequestHeader('X-Algolia-API-Key', this.apiKey); | ||
xmlHttp.setRequestHeader('X-Algolia-Application-Id', this.applicationID); | ||
for (var i = 0; i < this.extraHeaders.length; ++i) { | ||
@@ -277,9 +503,12 @@ xmlHttp.setRequestHeader(this.extraHeaders[i].key, this.extraHeaders[i].value); | ||
if (body != null) { | ||
xmlHttp.setRequestHeader("Content-type", "application/json"); | ||
xmlHttp.setRequestHeader('Content-type', 'application/json'); | ||
} | ||
} else if (typeof XDomainRequest != "undefined") { | ||
} else if (typeof XDomainRequest != 'undefined') { | ||
// Handle IE8/IE9 | ||
// XDomainRequest only exists in IE, and is IE's way of making CORS requests. | ||
xmlHttp = new XDomainRequest(); | ||
xmlHttp.open(opts.method, url); | ||
} else { | ||
console.log("your browser is too old to support CORS requests"); | ||
// very old browser, not supported | ||
console.log('your browser is too old to support CORS requests'); | ||
} | ||
@@ -289,4 +518,4 @@ xmlHttp.send(body); | ||
if (!self._isUndefined(event) && event.target != null) { | ||
var retry = event.target.status === 0 || event.target.status === 503; | ||
var success = event.target.status === 200 || event.target.status === 201; | ||
var retry = (event.target.status === 0 || event.target.status === 503); | ||
var success = (event.target.status === 200 || event.target.status === 201); | ||
opts.callback(retry, success, event.target, event.target.response != null ? JSON.parse(event.target.response) : null); | ||
@@ -298,7 +527,9 @@ } else { | ||
xmlHttp.onerror = function() { | ||
opts.callback(true, false, null, { | ||
message: "Could not connect to Host" | ||
}); | ||
opts.callback(true, false, null, { 'message': 'Could not connect to Host'} ); | ||
}; | ||
}, | ||
/* | ||
* Transform search param object in query string | ||
*/ | ||
_getSearchParams: function(args, params) { | ||
@@ -310,4 +541,4 @@ if (this._isUndefined(args) || args == null) { | ||
if (key != null && args.hasOwnProperty(key)) { | ||
params += params.length === 0 ? "?" : "&"; | ||
params += key + "=" + encodeURIComponent(Object.prototype.toString.call(args[key]) === "[object Array]" ? JSON.stringify(args[key]) : args[key]); | ||
params += (params.length === 0) ? '?' : '&'; | ||
params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? JSON.stringify(args[key]) : args[key]); | ||
} | ||
@@ -320,2 +551,4 @@ } | ||
}, | ||
/// internal attributes | ||
applicationID: null, | ||
@@ -328,207 +561,311 @@ apiKey: null, | ||
/* | ||
* Contains all the functions related to one index | ||
* You should use AlgoliaSearch.initIndex(indexName) to retrieve this object | ||
*/ | ||
AlgoliaSearch.prototype.Index.prototype = { | ||
clearCache: function() { | ||
this.cache = {}; | ||
}, | ||
addObject: function(content, callback, objectID) { | ||
var indexObj = this; | ||
if (this.as._isUndefined(objectID)) { | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName), | ||
body: content, | ||
callback: callback | ||
}); | ||
} else { | ||
this.as._jsonRequest({ | ||
method: "PUT", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(objectID), | ||
body: content, | ||
callback: callback | ||
}); | ||
} | ||
}, | ||
addObjects: function(objects, callback) { | ||
var indexObj = this; | ||
var postObj = { | ||
requests: [] | ||
}; | ||
for (var i = 0; i < objects.length; ++i) { | ||
var request = { | ||
action: "addObject", | ||
body: objects[i] | ||
}; | ||
postObj.requests.push(request); | ||
} | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/batch", | ||
body: postObj, | ||
callback: callback | ||
}); | ||
}, | ||
getObject: function(objectID, callback, attributes) { | ||
var indexObj = this; | ||
var params = ""; | ||
if (!this.as._isUndefined(attributes)) { | ||
params = "?attributes="; | ||
for (var i = 0; i < attributes.length; ++i) { | ||
if (i !== 0) { | ||
params += ","; | ||
} | ||
params += attributes[i]; | ||
/* | ||
* Clear all queries in cache | ||
*/ | ||
clearCache: function() { | ||
this.cache = {}; | ||
}, | ||
/* | ||
* Add an object in this index | ||
* | ||
* @param content contains the javascript object to add inside the index | ||
* @param callback (optional) the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains 3 elements: createAt, taskId and objectID | ||
* @param objectID (optional) an objectID you want to attribute to this object | ||
* (if the attribute already exist the old object will be overwrite) | ||
*/ | ||
addObject: function(content, callback, objectID) { | ||
var indexObj = this; | ||
if (this.as._isUndefined(objectID)) { | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName), | ||
body: content, | ||
callback: callback }); | ||
} else { | ||
this.as._jsonRequest({ method: 'PUT', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), | ||
body: content, | ||
callback: callback }); | ||
} | ||
} | ||
this.as._jsonRequest({ | ||
method: "GET", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(objectID) + params, | ||
callback: callback | ||
}); | ||
}, | ||
partialUpdateObject: function(partialObject, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(partialObject.objectID) + "/partial", | ||
body: partialObject, | ||
callback: callback | ||
}); | ||
}, | ||
partialUpdateObjects: function(objects, callback) { | ||
var indexObj = this; | ||
var postObj = { | ||
requests: [] | ||
}; | ||
for (var i = 0; i < objects.length; ++i) { | ||
var request = { | ||
action: "partialUpdateObject", | ||
objectID: encodeURIComponent(objects[i].objectID), | ||
body: objects[i] | ||
}; | ||
postObj.requests.push(request); | ||
} | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/batch", | ||
body: postObj, | ||
callback: callback | ||
}); | ||
}, | ||
saveObject: function(object, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "PUT", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(object.objectID), | ||
body: object, | ||
callback: callback | ||
}); | ||
}, | ||
saveObjects: function(objects, callback) { | ||
var indexObj = this; | ||
var postObj = { | ||
requests: [] | ||
}; | ||
for (var i = 0; i < objects.length; ++i) { | ||
var request = { | ||
action: "updateObject", | ||
objectID: encodeURIComponent(objects[i].objectID), | ||
body: objects[i] | ||
}; | ||
postObj.requests.push(request); | ||
} | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/batch", | ||
body: postObj, | ||
callback: callback | ||
}); | ||
}, | ||
deleteObject: function(objectID, callback) { | ||
if (objectID == null || objectID.length === 0) { | ||
callback(false, { | ||
message: "empty objectID" | ||
}); | ||
return; | ||
} | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "DELETE", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/" + encodeURIComponent(objectID), | ||
callback: callback | ||
}); | ||
}, | ||
search: function(query, callback, args, delay) { | ||
var indexObj = this; | ||
var params = "query=" + encodeURIComponent(query); | ||
if (!this.as._isUndefined(args) && args != null) { | ||
params = this.as._getSearchParams(args, params); | ||
} | ||
window.clearTimeout(indexObj.onDelayTrigger); | ||
if (!this.as._isUndefined(delay) && delay != null && delay > 0) { | ||
var onDelayTrigger = window.setTimeout(function() { | ||
indexObj._search(params, callback); | ||
}, delay); | ||
indexObj.onDelayTrigger = onDelayTrigger; | ||
} else { | ||
this._search(params, callback); | ||
} | ||
}, | ||
browse: function(page, callback, hitsPerPage) { | ||
var indexObj = this; | ||
var params = "?page=" + page; | ||
if (!_.isUndefined(hitsPerPage)) { | ||
params += "&hitsPerPage=" + hitsPerPage; | ||
} | ||
this.as._jsonRequest({ | ||
method: "GET", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/browse" + params, | ||
callback: callback | ||
}); | ||
}, | ||
getTypeaheadTransport: function(args, valueOption) { | ||
this.typeAheadArgs = args; | ||
if (typeof valueOption !== "undefined") { | ||
this.typeAheadValueOption = valueOption; | ||
} | ||
return this; | ||
}, | ||
get: function(query, processRemoteData, that, cb, suggestions) { | ||
self = this; | ||
this.search(query, function(success, content) { | ||
if (success) { | ||
for (var i = 0; i < content.hits.length; ++i) { | ||
var obj = content.hits[i], found = false; | ||
if (typeof obj.value === "undefined") { | ||
if (self.typeAheadValueOption != null) { | ||
if (typeof self.typeAheadValueOption === "function") { | ||
obj.value = self.typeAheadValueOption(obj); | ||
found = true; | ||
} else if (typeof obj[self.typeAheadValueOption] !== "undefined") { | ||
obj.value = obj[self.typeAheadValueOption]; | ||
found = true; | ||
} | ||
} | ||
if (!found) { | ||
for (var propertyName in obj) { | ||
if (!found && obj.hasOwnProperty(propertyName) && typeof obj[propertyName] === "string") { | ||
obj.value = obj[propertyName]; | ||
found = true; | ||
} | ||
} | ||
} | ||
}, | ||
/* | ||
* Add several objects | ||
* | ||
* @param objects contains an array of objects to add | ||
* @param callback (optional) the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that updateAt and taskID | ||
*/ | ||
addObjects: function(objects, callback) { | ||
var indexObj = this; | ||
var postObj = {requests:[]}; | ||
for (var i = 0; i < objects.length; ++i) { | ||
var request = { action: 'addObject', | ||
body: objects[i] }; | ||
postObj.requests.push(request); | ||
} | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', | ||
body: postObj, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Get an object from this index | ||
* | ||
* @param objectID the unique identifier of the object to retrieve | ||
* @param callback (optional) the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the object to retrieve or the error message if a failure occured | ||
* @param attributes (optional) if set, contains the array of attribute names to retrieve | ||
*/ | ||
getObject: function(objectID, callback, attributes) { | ||
var indexObj = this; | ||
var params = ''; | ||
if (!this.as._isUndefined(attributes)) { | ||
params = '?attributes='; | ||
for (var i = 0; i < attributes.length; ++i) { | ||
if (i !== 0) { | ||
params += ','; | ||
} | ||
suggestions.push(that._transformDatum(obj)); | ||
params += attributes[i]; | ||
} | ||
cb && cb(suggestions); | ||
} | ||
}, self.typeAheadArgs); | ||
return true; | ||
}, | ||
waitTask: function(taskID, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "GET", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/task/" + taskID, | ||
callback: function(success, body) { | ||
if (success && body.status === "published") { | ||
this.as._jsonRequest({ method: 'GET', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Update partially an object (only update attributes passed in argument) | ||
* | ||
* @param partialObject contains the javascript attributes to override, the | ||
* object must contains an objectID attribute | ||
* @param callback (optional) the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains 3 elements: createAt, taskId and objectID | ||
*/ | ||
partialUpdateObject: function(partialObject, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial', | ||
body: partialObject, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Partially Override the content of several objects | ||
* | ||
* @param objects contains an array of objects to update (each object must contains a objectID attribute) | ||
* @param callback (optional) the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that updateAt and taskID | ||
*/ | ||
partialUpdateObjects: function(objects, callback) { | ||
var indexObj = this; | ||
var postObj = {requests:[]}; | ||
for (var i = 0; i < objects.length; ++i) { | ||
var request = { action: 'partialUpdateObject', | ||
objectID: objects[i].objectID, | ||
body: objects[i] }; | ||
postObj.requests.push(request); | ||
} | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', | ||
body: postObj, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Override the content of object | ||
* | ||
* @param object contains the javascript object to save, the object must contains an objectID attribute | ||
* @param callback (optional) the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that updateAt and taskID | ||
*/ | ||
saveObject: function(object, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'PUT', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), | ||
body: object, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Override the content of several objects | ||
* | ||
* @param objects contains an array of objects to update (each object must contains a objectID attribute) | ||
* @param callback (optional) the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that updateAt and taskID | ||
*/ | ||
saveObjects: function(objects, callback) { | ||
var indexObj = this; | ||
var postObj = {requests:[]}; | ||
for (var i = 0; i < objects.length; ++i) { | ||
var request = { action: 'updateObject', | ||
objectID: objects[i].objectID, | ||
body: objects[i] }; | ||
postObj.requests.push(request); | ||
} | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', | ||
body: postObj, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Delete an object from the index | ||
* | ||
* @param objectID the unique identifier of object to delete | ||
* @param callback (optional) the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains 3 elements: createAt, taskId and objectID | ||
*/ | ||
deleteObject: function(objectID, callback) { | ||
if (objectID == null || objectID.length === 0) { | ||
callback(false, { message: 'empty objectID'}); | ||
return; | ||
} | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'DELETE', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), | ||
callback: callback }); | ||
}, | ||
/* | ||
* Search inside the index using XMLHttpRequest request (Using a POST query to | ||
* minimize number of OPTIONS queries: Cross-Origin Resource Sharing). | ||
* | ||
* @param query the full text query | ||
* @param callback the result callback with two arguments: | ||
* success: boolean set to true if the request was successfull. If false, the content contains the error. | ||
* content: the server answer that contains the list of results. | ||
* @param args (optional) if set, contains an object with query parameters: | ||
* - page: (integer) Pagination parameter used to select the page to retrieve. | ||
* Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 | ||
* - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. | ||
* - attributesToRetrieve: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size). | ||
* Attributes are separated with a comma (for example "name,address"). | ||
* You can also use a string array encoding (for example ["name","address"]). | ||
* By default, all attributes are retrieved. You can also use '*' to retrieve all values when an attributesToRetrieve setting is specified for your index. | ||
* - attributesToHighlight: a string that contains the list of attributes you want to highlight according to the query. | ||
* Attributes are separated by a comma. You can also use a string array encoding (for example ["name","address"]). | ||
* If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. | ||
* You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. | ||
* A matchLevel is returned for each highlighted attribute and can contain: | ||
* - full: if all the query terms were found in the attribute, | ||
* - partial: if only some of the query terms were found, | ||
* - none: if none of the query terms were found. | ||
* - attributesToSnippet: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). | ||
* Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). | ||
* You can also use a string array encoding (Example: attributesToSnippet: ["name:10","content:10"]). By default no snippet is computed. | ||
* - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. Defaults to 3. | ||
* - minWordSizefor2Typos: the minimum number of characters in a query word to accept two typos in this word. Defaults to 7. | ||
* - getRankingInfo: if set to 1, the result hits will contain ranking information in _rankingInfo attribute. | ||
* - aroundLatLng: search for entries around a given latitude/longitude (specified as two floats separated by a comma). | ||
* For example aroundLatLng=47.316669,5.016670). | ||
* You can specify the maximum distance in meters with the aroundRadius parameter (in meters) and the precision for ranking with aroundPrecision | ||
* (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). | ||
* At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) | ||
* - insideBoundingBox: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). | ||
* For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). | ||
* At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) | ||
* - numericFilters: a string that contains the list of numeric filters you want to apply separated by a comma. | ||
* The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. | ||
* You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. | ||
* You can also use a string array encoding (for example numericFilters: ["price>100","price<1000"]). | ||
* - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. | ||
* To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). | ||
* You can also use a string array encoding, for example tagFilters: ["tag1",["tag2","tag3"]] means tag1 AND (tag2 OR tag3). | ||
* At indexing, tags should be added in the _tags** attribute of objects (for example {"_tags":["tag1","tag2"]}). | ||
* - facetFilters: filter the query by a list of facets. | ||
* Facets are separated by commas and each facet is encoded as `attributeName:value`. | ||
* For example: `facetFilters=category:Book,author:John%20Doe`. | ||
* You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`). | ||
* - facets: List of object attributes that you want to use for faceting. | ||
* Attributes are separated with a comma (for example `"category,author"` ). | ||
* You can also use a JSON string array encoding (for example ["category","author"]). | ||
* Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. | ||
* You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. | ||
* - queryType: select how the query words are interpreted, it can be one of the following value: | ||
* - prefixAll: all query words are interpreted as prefixes, | ||
* - prefixLast: only the last word is interpreted as a prefix (default behavior), | ||
* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. | ||
* - optionalWords: a string that contains the list of words that should be considered as optional when found in the query. | ||
* The list of words is comma separated. | ||
* - distinct: If set to 1, enable the distinct feature (disabled by default) if the attributeForDistinct index setting is set. | ||
* This feature is similar to the SQL "distinct" keyword: when enabled in a query with the distinct=1 parameter, | ||
* all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. | ||
* 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. | ||
* @param delay (optional) if set, wait for this delay (in ms) and only send the query if there was no other in the meantime. | ||
*/ | ||
search: function(query, callback, args, delay) { | ||
var indexObj = this; | ||
var params = 'query=' + encodeURIComponent(query); | ||
if (!this.as._isUndefined(args) && args != null) { | ||
params = this.as._getSearchParams(args, params); | ||
} | ||
window.clearTimeout(indexObj.onDelayTrigger); | ||
if (!this.as._isUndefined(delay) && delay != null && delay > 0) { | ||
var onDelayTrigger = window.setTimeout( function() { | ||
indexObj._search(params, callback); | ||
}, delay); | ||
indexObj.onDelayTrigger = onDelayTrigger; | ||
} else { | ||
this._search(params, callback); | ||
} | ||
}, | ||
/* | ||
* Browse all index content | ||
* | ||
* @param page Pagination parameter used to select the page to retrieve. | ||
* Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 | ||
* @param hitsPerPage: Pagination parameter used to select the number of hits per page. Defaults to 1000. | ||
*/ | ||
browse: function(page, callback, hitsPerPage) { | ||
var indexObj = this; | ||
var params = '?page=' + page; | ||
if (!_.isUndefined(hitsPerPage)) { | ||
params += '&hitsPerPage=' + hitsPerPage; | ||
} | ||
this.as._jsonRequest({ method: 'GET', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse' + params, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Get a Typeahead.js adapter | ||
* @param searchParams contains an object with query parameters (see search for details) | ||
*/ | ||
ttAdapter: function(params) { | ||
var self = this; | ||
return function(query, cb) { | ||
self.search(query, function(success, content) { | ||
if (success) { | ||
cb(content.hits); | ||
} | ||
}, params); | ||
}; | ||
}, | ||
/* | ||
* Wait the publication of a task on the server. | ||
* All server task are asynchronous and you can check with this method that the task is published. | ||
* | ||
* @param taskID the id of the task returned by server | ||
* @param callback the result callback with with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer that contains the list of results | ||
*/ | ||
waitTask: function(taskID, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'GET', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID, | ||
callback: function(success, body) { | ||
if (success && body.status === 'published') { | ||
callback(true, body); | ||
@@ -540,98 +877,442 @@ } else if (success && body.pendingTask) { | ||
} | ||
} | ||
}); | ||
}}); | ||
}, | ||
/* | ||
* This function deletes the index content. Settings and index specific API keys are kept untouched. | ||
* | ||
* @param callback (optional) the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the settings object or the error message if a failure occured | ||
*/ | ||
clearIndex: function(callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', | ||
callback: callback }); | ||
}, | ||
/* | ||
* Get settings of this index | ||
* | ||
* @param callback (optional) the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the settings object or the error message if a failure occured | ||
*/ | ||
getSettings: function(callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'GET', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', | ||
callback: callback }); | ||
}, | ||
/* | ||
* Set settings for this index | ||
* | ||
* @param settigns the settings object that can contains : | ||
* - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). | ||
* - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). | ||
* - hitsPerPage: (integer) the number of hits per page (default = 10). | ||
* - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. | ||
* If set to null, all attributes are retrieved. | ||
* - attributesToHighlight: (array of strings) default list of attributes to highlight. | ||
* If set to null, all indexed attributes are highlighted. | ||
* - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number of words to return (syntax is attributeName:nbWords). | ||
* By default no snippet is computed. If set to null, no snippet is computed. | ||
* - attributesToIndex: (array of strings) the list of fields you want to index. | ||
* If set to null, all textual and numerical attributes of your objects are indexed, but you should update it to get optimal results. | ||
* This parameter has two important uses: | ||
* - Limit the attributes to index: For example if you store a binary image in base64, you want to store it and be able to | ||
* retrieve it but you don't want to search in the base64 string. | ||
* - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in attributes at the beginning of | ||
* the list will be considered more important than matches in attributes further down the list. | ||
* In one attribute, matching text at the beginning of the attribute will be considered more important than text after, you can disable | ||
* this behavior if you add your attribute inside `unordered(AttributeName)`, for example attributesToIndex: ["title", "unordered(text)"]. | ||
* - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. | ||
* 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. | ||
* - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature is similar to the SQL "distinct" keyword: when enabled | ||
* in query with the distinct=1 parameter, all hits containing a duplicate value for this attribute are removed from results. | ||
* 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. | ||
* - ranking: (array of strings) controls the way results are sorted. | ||
* We have six available criteria: | ||
* - typo: sort according to number of typos, | ||
* - geo: sort according to decreassing distance when performing a geo-location based search, | ||
* - proximity: sort according to the proximity of query words in hits, | ||
* - attribute: sort according to the order of attributes defined by attributesToIndex, | ||
* - exact: | ||
* - if the user query contains one word: sort objects having an attribute that is exactly the query word before others. | ||
* 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 | ||
* show starting by the v letter before it. | ||
* - if the user query contains multiple words: sort according to the number of words that matched exactly (and not as a prefix). | ||
* - custom: sort according to a user defined formula set in **customRanking** attribute. | ||
* The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] | ||
* - customRanking: (array of strings) lets you specify part of the ranking. | ||
* The syntax of this condition is an array of strings containing attributes prefixed by asc (ascending order) or desc (descending order) operator. | ||
* For example `"customRanking" => ["desc(population)", "asc(name)"]` | ||
* - queryType: Select how the query words are interpreted, it can be one of the following value: | ||
* - prefixAll: all query words are interpreted as prefixes, | ||
* - prefixLast: only the last word is interpreted as a prefix (default behavior), | ||
* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. | ||
* - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in the query result (default to "<em>"). | ||
* - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in the query result (default to "</em>"). | ||
* - optionalWords: (array of strings) Specify a list of words that should be considered as optional when found in the query. | ||
* @param callback (optional) the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer or the error message if a failure occured | ||
*/ | ||
setSettings: function(settings, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'PUT', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings', | ||
body: settings, | ||
callback: callback }); | ||
}, | ||
/* | ||
* List all existing user keys associated to this index | ||
* | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
listUserKeys: function(callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'GET', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', | ||
callback: callback }); | ||
}, | ||
/* | ||
* Get ACL of a user key associated to this index | ||
* | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
getUserKeyACL: function(key, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'GET', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Delete an existing user key associated to this index | ||
* | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
deleteUserKey: function(key, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ method: 'DELETE', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Add an existing user key associated to this index | ||
* | ||
* @param acls the list of ACL for this key. Defined by an array of strings that | ||
* can contains the following values: | ||
* - search: allow to search (https and http) | ||
* - addObject: allows to add/update an object in the index (https only) | ||
* - deleteObject : allows to delete an existing object (https only) | ||
* - deleteIndex : allows to delete index content (https only) | ||
* - settings : allows to get index settings (https only) | ||
* - editSettings : allows to change index settings (https only) | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
addUserKey: function(acls, callback) { | ||
var indexObj = this; | ||
var aclsObject = {}; | ||
aclsObject.acl = acls; | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', | ||
body: aclsObject, | ||
callback: callback }); | ||
}, | ||
/* | ||
* Add an existing user key associated to this index | ||
* | ||
* @param acls the list of ACL for this key. Defined by an array of strings that | ||
* can contains the following values: | ||
* - search: allow to search (https and http) | ||
* - addObject: allows to add/update an object in the index (https only) | ||
* - deleteObject : allows to delete an existing object (https only) | ||
* - deleteIndex : allows to delete index content (https only) | ||
* - settings : allows to get index settings (https only) | ||
* - editSettings : allows to change index settings (https only) | ||
* @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key) | ||
* @param maxQueriesPerIPPerHour Specify the maximum number of API calls allowed from an IP address per hour. | ||
* @param maxHitsPerQuery Specify the maximum number of hits this API key can retrieve in one call. | ||
* @param callback the result callback with two arguments | ||
* success: boolean set to true if the request was successfull | ||
* content: the server answer with user keys list or error description if success is false. | ||
*/ | ||
addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { | ||
var indexObj = this; | ||
var aclsObject = {}; | ||
aclsObject.acl = acls; | ||
aclsObject.validity = validity; | ||
aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; | ||
aclsObject.maxHitsPerQuery = maxHitsPerQuery; | ||
this.as._jsonRequest({ method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', | ||
body: aclsObject, | ||
callback: callback }); | ||
}, | ||
/// | ||
/// Internal methods only after this line | ||
/// | ||
_search: function(params, callback) { | ||
this.as._jsonRequest({ cache: this.cache, | ||
method: 'POST', | ||
url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', | ||
body: {params: params, apiKey: this.as.apiKey, appID: this.as.applicationID}, | ||
callback: callback }); | ||
}, | ||
// internal attributes | ||
as: null, | ||
indexName: null, | ||
cache: {}, | ||
typeAheadArgs: null, | ||
typeAheadValueOption: null, | ||
emptyConstructor: function() {} | ||
}; | ||
/* | ||
* Copyright (c) 2014 Algolia | ||
* http://www.algolia.com/ | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
(function($) { | ||
var self; | ||
/** | ||
* Algolia Search Helper providing faceting and disjunctive faceting | ||
* @param {AlgoliaSearch} client an AlgoliaSearch client | ||
* @param {string} index the index name to query | ||
* @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets | ||
*/ | ||
window.AlgoliaSearchHelper = function(client, index, options) { | ||
/// Default options | ||
var defaults = { | ||
facets: [], // list of facets to compute | ||
disjunctiveFacets: [], // list of disjunctive facets to compute | ||
hitsPerPage: 20 // number of hits per page | ||
}; | ||
this.init(client, index, $.extend({}, defaults, options)); | ||
self = this; | ||
}; | ||
AlgoliaSearchHelper.prototype = { | ||
/** | ||
* Initialize a new AlgoliaSearchHelper | ||
* @param {AlgoliaSearch} client an AlgoliaSearch client | ||
* @param {string} index the index name to query | ||
* @param {hash} options an associative array defining the hitsPerPage, list of facets and list of disjunctive facets | ||
* @return {AlgoliaSearchHelper} | ||
*/ | ||
init: function(client, index, options) { | ||
this.client = client; | ||
this.index = index; | ||
this.options = options; | ||
this.page = 0; | ||
this.refinements = {}; | ||
this.disjunctiveRefinements = {}; | ||
}, | ||
clearIndex: function(callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/clear", | ||
callback: callback | ||
}); | ||
/** | ||
* Perform a query | ||
* @param {string} q the user query | ||
* @param {function} searchCallback the result callback called with two arguments: | ||
* success: boolean set to true if the request was successfull | ||
* content: the query answer with an extra 'disjunctiveFacets' attribute | ||
*/ | ||
search: function(q, searchCallback) { | ||
this.q = q; | ||
this.searchCallback = searchCallback; | ||
this.page = 0; | ||
this.refinements = {}; | ||
this.disjunctiveRefinements = {}; | ||
this._search(); | ||
}, | ||
getSettings: function(callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "GET", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/settings", | ||
callback: callback | ||
}); | ||
/** | ||
* Toggle refinement state of a facet | ||
* @param {string} facet the facet to refine | ||
* @param {string} value the associated value | ||
* @return {boolean} true if the facet has been found | ||
*/ | ||
toggleRefine: function(facet, value) { | ||
for (var i = 0; i < this.options.facets.length; ++i) { | ||
if (this.options.facets[i] == facet) { | ||
var refinement = facet + ':' + value; | ||
this.refinements[refinement] = !this.refinements[refinement]; | ||
this.page = 0; | ||
this._search(); | ||
return true; | ||
} | ||
} | ||
this.disjunctiveRefinements[facet] = this.disjunctiveRefinements[facet] || {}; | ||
for (var j = 0; j < this.options.disjunctiveFacets.length; ++j) { | ||
if (this.options.disjunctiveFacets[j] == facet) { | ||
this.disjunctiveRefinements[facet][value] = !this.disjunctiveRefinements[facet][value]; | ||
this.page = 0; | ||
this._search(); | ||
return true; | ||
} | ||
} | ||
return false; | ||
}, | ||
setSettings: function(settings, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "PUT", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/settings", | ||
body: settings, | ||
callback: callback | ||
}); | ||
/** | ||
* Check the refinement state of a facet | ||
* @param {string} facet the facet | ||
* @param {string} value the associated value | ||
* @return {boolean} true if refined | ||
*/ | ||
isRefined: function(facet, value) { | ||
var refinement = facet + ':' + value; | ||
if (this.refinements[refinement]) { | ||
return true; | ||
} | ||
if (this.disjunctiveRefinements[facet] && this.disjunctiveRefinements[facet][value]) { | ||
return true; | ||
} | ||
return false; | ||
}, | ||
listUserKeys: function(callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "GET", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys", | ||
callback: callback | ||
}); | ||
/** | ||
* Go to next page | ||
*/ | ||
nextPage: function() { | ||
this._gotoPage(this.page + 1); | ||
}, | ||
getUserKeyACL: function(key, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "GET", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys/" + key, | ||
callback: callback | ||
}); | ||
/** | ||
* Go to previous page | ||
*/ | ||
previousPage: function() { | ||
if (this.page > 0) { | ||
this._gotoPage(this.page - 1); | ||
} | ||
}, | ||
deleteUserKey: function(key, callback) { | ||
var indexObj = this; | ||
this.as._jsonRequest({ | ||
method: "DELETE", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys/" + key, | ||
callback: callback | ||
}); | ||
///////////// PRIVATE | ||
/** | ||
* Goto a page | ||
* @param {integer} page The page number | ||
*/ | ||
_gotoPage: function(page) { | ||
this.page = page; | ||
this._search(); | ||
}, | ||
addUserKey: function(acls, callback) { | ||
var indexObj = this; | ||
var aclsObject = {}; | ||
aclsObject.acl = acls; | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys", | ||
body: aclsObject, | ||
callback: callback | ||
}); | ||
/** | ||
* Perform the underlying queries | ||
*/ | ||
_search: function() { | ||
this.client.startQueriesBatch(); | ||
this.client.addQueryInBatch(this.index, this.q, this._getHitsSearchParams()); | ||
for (var i = 0; i < this.options.disjunctiveFacets.length; ++i) { | ||
this.client.addQueryInBatch(this.index, this.q, this._getDisjunctiveFacetSearchParams(this.options.disjunctiveFacets[i])); | ||
} | ||
this.client.sendQueriesBatch(function(success, content) { | ||
if (!success) { | ||
self.searchCallback(false, content); | ||
return; | ||
} | ||
var aggregatedAnswer = content.results[0]; | ||
aggregatedAnswer.disjunctiveFacets = {}; | ||
for (var i = 1; i < content.results.length; ++i) { | ||
for (var facet in content.results[i].facets) { | ||
aggregatedAnswer.disjunctiveFacets[facet] = content.results[i].facets[facet]; | ||
if (self.disjunctiveRefinements[facet]) { | ||
for (var value in self.disjunctiveRefinements[facet]) { | ||
if (!aggregatedAnswer.disjunctiveFacets[facet][value] && self.disjunctiveRefinements[facet][value]) { | ||
aggregatedAnswer.disjunctiveFacets[facet][value] = 0; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
self.searchCallback(true, aggregatedAnswer); | ||
}); | ||
}, | ||
addUserKeyWithValidity: function(acls, validity, maxQueriesPerIPPerHour, maxHitsPerQuery, callback) { | ||
var indexObj = this; | ||
var aclsObject = {}; | ||
aclsObject.acl = acls; | ||
aclsObject.validity = validity; | ||
aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour; | ||
aclsObject.maxHitsPerQuery = maxHitsPerQuery; | ||
this.as._jsonRequest({ | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(indexObj.indexName) + "/keys", | ||
body: aclsObject, | ||
callback: callback | ||
}); | ||
/** | ||
* Build search parameters used to fetch hits | ||
* @return {hash} | ||
*/ | ||
_getHitsSearchParams: function() { | ||
return { | ||
hitsPerPage: this.options.hitsPerPage, | ||
page: this.page, | ||
facets: this.options.facets, | ||
facetFilters: this._getFacetFilters() | ||
}; | ||
}, | ||
_search: function(params, callback) { | ||
this.as._jsonRequest({ | ||
cache: this.cache, | ||
method: "POST", | ||
url: "/1/indexes/" + encodeURIComponent(this.indexName) + "/query", | ||
body: { | ||
params: params, | ||
apiKey: this.as.apiKey, | ||
appID: this.as.applicationID | ||
}, | ||
callback: callback | ||
}); | ||
/** | ||
* Build search parameters used to fetch a disjunctive facet | ||
* @param {string} facet the associated facet name | ||
* @return {hash} | ||
*/ | ||
_getDisjunctiveFacetSearchParams: function(facet) { | ||
return { | ||
hitsPerPage: 1, | ||
page: 0, | ||
facets: facet, | ||
facetFilters: this._getFacetFilters(facet) | ||
}; | ||
}, | ||
as: null, | ||
indexName: null, | ||
cache: {}, | ||
typeAheadArgs: null, | ||
typeAheadValueOption: null, | ||
emptyConstructor: function() {} | ||
}; | ||
/** | ||
* Build facetFilters parameter based on current refinements | ||
* @param {string} facet if set, the current disjunctive facet | ||
* @return {hash} | ||
*/ | ||
_getFacetFilters: function(facet) { | ||
var facetFilters = []; | ||
for (var refinement in this.refinements) { | ||
if (this.refinements[refinement]) { | ||
facetFilters.push(refinement); | ||
} | ||
} | ||
for (var disjunctiveRefinement in this.disjunctiveRefinements) { | ||
if (disjunctiveRefinement != facet) { | ||
var refinements = []; | ||
for (var value in this.disjunctiveRefinements[disjunctiveRefinement]) { | ||
if (this.disjunctiveRefinements[disjunctiveRefinement][value]) { | ||
refinements.push(disjunctiveRefinement + ':' + value); | ||
} | ||
} | ||
if (refinements.length > 0) { | ||
facetFilters.push(refinements); | ||
} | ||
} | ||
} | ||
return facetFilters; | ||
} | ||
}; | ||
})(jQuery); |
/*! | ||
* algoliasearch 2.3.6 | ||
* algoliasearch 2.4.0 | ||
* https://github.com/algolia/algoliasearch-client-js | ||
* Copyright 2013 Algolia SAS; Licensed MIT | ||
* Copyright 2014 Algolia SAS; Licensed MIT | ||
*/ | ||
var VERSION="2.3.6",AlgoliaSearch=function(a,b,c,d,e){this.applicationID=a,this.apiKey=b,this._isUndefined(e)&&(e=[a+"-1.algolia.io",a+"-2.algolia.io",a+"-3.algolia.io"]),this.hosts=[];for(var f=0;f<e.length;++f)Math.random()>.5&&this.hosts.reverse(),this._isUndefined(c)||null==c?this.hosts.push(("https:"==document.location.protocol?"https":"http")+"://"+e[f]):"https"===c||"HTTPS"===c?this.hosts.push("https://"+e[f]):this.hosts.push("http://"+e[f]);Math.random()>.5&&this.hosts.reverse(),(this._isUndefined(d)||d)&&this._jsonRequest({method:"GET",url:"/1/isalive"}),this.extraHeaders=[]};AlgoliaSearch.prototype={deleteIndex:function(a,b){this._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(a),callback:b})},moveIndex:function(a,b,c){var d={operation:"move",destination:b};this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(a)+"/operation",body:d,callback:c})},copyIndex:function(a,b,c){var d={operation:"copy",destination:b};this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(a)+"/operation",body:d,callback:c})},getLogs:function(a,b,c){this._isUndefined(b)&&(b=0),this._isUndefined(c)&&(c=10),this._jsonRequest({method:"GET",url:"/1/logs?offset="+b+"&length="+c,callback:a})},listIndexes:function(a){this._jsonRequest({method:"GET",url:"/1/indexes/",callback:a})},initIndex:function(a){return new this.Index(this,a)},listUserKeys:function(a){this._jsonRequest({method:"GET",url:"/1/keys",callback:a})},getUserKeyACL:function(a,b){this._jsonRequest({method:"GET",url:"/1/keys/"+a,callback:b})},deleteUserKey:function(a,b){this._jsonRequest({method:"DELETE",url:"/1/keys/"+a,callback:b})},addUserKey:function(a,b){var c={};c.acl=a,this._jsonRequest({method:"POST",url:"/1/keys",body:c,callback:b})},addUserKeyWithValidity:function(a,b,c,d,e){var f=this,g={};g.acl=a,g.validity=b,g.maxQueriesPerIPPerHour=c,g.maxHitsPerQuery=d,this._jsonRequest({method:"POST",url:"/1/indexes/"+f.indexName+"/keys",body:g,callback:e})},startQueriesBatch:function(){this.batch=[]},addQueryInBatch:function(a,b,c){var d="query="+encodeURIComponent(b);this._isUndefined(c)||null==c||(d=this._getSearchParams(c,d)),this.batch.push({indexName:a,params:d})},clearCache:function(){this.cache={}},sendQueriesBatch:function(a,b){for(var c=this,d={requests:[],apiKey:this.apiKey,appID:this.applicationID},e=0;e<c.batch.length;++e)d.requests.push(c.batch[e]);if(window.clearTimeout(c.onDelayTrigger),!this._isUndefined(b)&&null!=b&&b>0){var f=window.setTimeout(function(){c._sendQueriesBatch(d,a)},b);c.onDelayTrigger=f}else this._sendQueriesBatch(d,a)},Index:function(a,b){this.indexName=b,this.as=a,this.typeAheadArgs=null,this.typeAheadValueOption=null},setExtraHeader:function(a,b){this.extraHeaders.push({key:a,value:b})},_sendQueriesBatch:function(a,b){this._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/*/queries",body:a,callback:b})},_jsonRequest:function(a){var b=this,c=a.callback,d=null,e=a.url;if(this._isUndefined(a.body)||(e=a.url+"_body_"+JSON.stringify(a.body)),!this._isUndefined(a.cache)&&(d=a.cache,!this._isUndefined(d[e])))return this._isUndefined(c)||c(!0,d[e]),void 0;var f=function(g){var h=0;return b._isUndefined(g)||(h=g),b.hosts.length<=h?(b._isUndefined(c)||c(!1,{message:"Cannot contact server"}),void 0):(a.callback=function(g,i,j,k){i||b._isUndefined(k)||console.log("Error: "+k.message),i&&!b._isUndefined(a.cache)&&(d[e]=k),!i&&g&&h+1<b.hosts.length?f(h+1):b._isUndefined(c)||c(i,k)},a.hostname=b.hosts[h],b._jsonRequestByHost(a),void 0)};f()},_jsonRequestByHost:function(a){var b=null,c=this;this._isUndefined(a.body)||(b=JSON.stringify(a.body));var d=a.hostname+a.url,e=null;if(e=new XMLHttpRequest,"withCredentials"in e){e.open(a.method,d,!0),e.setRequestHeader("X-Algolia-API-Key",this.apiKey),e.setRequestHeader("X-Algolia-Application-Id",this.applicationID);for(var f=0;f<this.extraHeaders.length;++f)e.setRequestHeader(this.extraHeaders[f].key,this.extraHeaders[f].value);null!=b&&e.setRequestHeader("Content-type","application/json")}else"undefined"!=typeof XDomainRequest?(e=new XDomainRequest,e.open(a.method,d)):console.log("your browser is too old to support CORS requests");e.send(b),e.onload=function(b){if(c._isUndefined(b)||null==b.target)a.callback(!1,!0,b,JSON.parse(e.responseText));else{var d=0===b.target.status||503===b.target.status,f=200===b.target.status||201===b.target.status;a.callback(d,f,b.target,null!=b.target.response?JSON.parse(b.target.response):null)}},e.onerror=function(){a.callback(!0,!1,null,{message:"Could not connect to Host"})}},_getSearchParams:function(a,b){if(this._isUndefined(a)||null==a)return b;for(var c in a)null!=c&&a.hasOwnProperty(c)&&(b+=0===b.length?"?":"&",b+=c+"="+encodeURIComponent("[object Array]"===Object.prototype.toString.call(a[c])?JSON.stringify(a[c]):a[c]));return b},_isUndefined:function(a){return void 0===a},applicationID:null,apiKey:null,hosts:[],cache:{},extraHeaders:[]},AlgoliaSearch.prototype.Index.prototype={clearCache:function(){this.cache={}},addObject:function(a,b,c){var d=this;this.as._isUndefined(c)?this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(d.indexName),body:a,callback:b}):this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/"+encodeURIComponent(c),body:a,callback:b})},addObjects:function(a,b){for(var c=this,d={requests:[]},e=0;e<a.length;++e){var f={action:"addObject",body:a[e]};d.requests.push(f)}this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/batch",body:d,callback:b})},getObject:function(a,b,c){var d=this,e="";if(!this.as._isUndefined(c)){e="?attributes=";for(var f=0;f<c.length;++f)0!==f&&(e+=","),e+=c[f]}this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/"+encodeURIComponent(a)+e,callback:b})},partialUpdateObject:function(a,b){var c=this;this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/"+encodeURIComponent(a.objectID)+"/partial",body:a,callback:b})},partialUpdateObjects:function(a,b){for(var c=this,d={requests:[]},e=0;e<a.length;++e){var f={action:"partialUpdateObject",objectID:encodeURIComponent(a[e].objectID),body:a[e]};d.requests.push(f)}this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/batch",body:d,callback:b})},saveObject:function(a,b){var c=this;this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/"+encodeURIComponent(a.objectID),body:a,callback:b})},saveObjects:function(a,b){for(var c=this,d={requests:[]},e=0;e<a.length;++e){var f={action:"updateObject",objectID:encodeURIComponent(a[e].objectID),body:a[e]};d.requests.push(f)}this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/batch",body:d,callback:b})},deleteObject:function(a,b){if(null==a||0===a.length)return b(!1,{message:"empty objectID"}),void 0;var c=this;this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/"+encodeURIComponent(a),callback:b})},search:function(a,b,c,d){var e=this,f="query="+encodeURIComponent(a);if(this.as._isUndefined(c)||null==c||(f=this.as._getSearchParams(c,f)),window.clearTimeout(e.onDelayTrigger),!this.as._isUndefined(d)&&null!=d&&d>0){var g=window.setTimeout(function(){e._search(f,b)},d);e.onDelayTrigger=g}else this._search(f,b)},browse:function(a,b,c){var d=this,e="?page="+a;_.isUndefined(c)||(e+="&hitsPerPage="+c),this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/browse"+e,callback:b})},getTypeaheadTransport:function(a,b){return this.typeAheadArgs=a,"undefined"!=typeof b&&(this.typeAheadValueOption=b),this},get:function(a,b,c,d,e){return self=this,this.search(a,function(a,b){if(a){for(var f=0;f<b.hits.length;++f){var g=b.hits[f],h=!1;if("undefined"==typeof g.value&&(null!=self.typeAheadValueOption&&("function"==typeof self.typeAheadValueOption?(g.value=self.typeAheadValueOption(g),h=!0):"undefined"!=typeof g[self.typeAheadValueOption]&&(g.value=g[self.typeAheadValueOption],h=!0)),!h))for(var i in g)!h&&g.hasOwnProperty(i)&&"string"==typeof g[i]&&(g.value=g[i],h=!0);e.push(c._transformDatum(g))}d&&d(e)}},self.typeAheadArgs),!0},waitTask:function(a,b){var c=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/task/"+a,callback:function(d,e){if(d&&"published"===e.status)b(!0,e);else{if(d&&e.pendingTask)return c.waitTask(a,b);b(!1,e)}}})},clearIndex:function(a){var b=this;this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/clear",callback:a})},getSettings:function(a){var b=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/settings",callback:a})},setSettings:function(a,b){var c=this;this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/settings",body:a,callback:b})},listUserKeys:function(a){var b=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/keys",callback:a})},getUserKeyACL:function(a,b){var c=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},deleteUserKey:function(a,b){var c=this;this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},addUserKey:function(a,b){var c=this,d={};d.acl=a,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys",body:d,callback:b})},addUserKeyWithValidity:function(a,b,c,d,e){var f=this,g={};g.acl=a,g.validity=b,g.maxQueriesPerIPPerHour=c,g.maxHitsPerQuery=d,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(f.indexName)+"/keys",body:g,callback:e})},_search:function(a,b){this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:{params:a,apiKey:this.as.apiKey,appID:this.as.applicationID},callback:b})},as:null,indexName:null,cache:{},typeAheadArgs:null,typeAheadValueOption:null,emptyConstructor:function(){}}; | ||
function AlgoliaExplainResults(a,b,c){function d(a,b){if("object"==typeof a&&"matchedWords"in a&&"value"in a){for(var c=!1,e=0;e<a.matchedWords.length;++e){var f=a.matchedWords[e];f in b||(b[f]=1,c=!0)}return c?[a.value]:[]}if(a instanceof Array){for(var g=[],h=0;h<a.length;++h){var i=d(a[h],b);g=g.concat(i)}return g}if("object"==typeof a){var g=[];for(prop in a)a.hasOwnProperty(prop)&&(g=g.concat(d(a[prop],b)));return g}return[]}function e(a,b,c){if(-1===c.indexOf("."))return c in a._highlightResult?d(a._highlightResult[c],b):[];for(var e=c.split("."),f=a._highlightResult,g=0;g<e.length;++g){if(!(e[g]in f))return[];f=f[e[g]]}return d(f,b)}var f={},g={},h=e(a,g,b);if(f.title=h.length>0?h[0]:"",f.subtitles=[],"undefined"!=typeof c)for(var i=0;i<c.length;++i)for(var j=e(a,g,c[i]),k=0;k<j.length;++k)f.subtitles.push({attr:c[i],value:j[k]});return f}var ALGOLIA_VERSION="2.4.0",AlgoliaSearch=function(a,b,c,d,e){this.applicationID=a,this.apiKey=b,this._isUndefined(e)&&(e=[a+"-1.algolia.io",a+"-2.algolia.io",a+"-3.algolia.io"]),this.hosts=[];for(var f=0;f<e.length;++f)Math.random()>.5&&this.hosts.reverse(),this._isUndefined(c)||null==c?this.hosts.push(("https:"==document.location.protocol?"https":"http")+"://"+e[f]):"https"===c||"HTTPS"===c?this.hosts.push("https://"+e[f]):this.hosts.push("http://"+e[f]);Math.random()>.5&&this.hosts.reverse(),(this._isUndefined(d)||d)&&this._jsonRequest({method:"GET",url:"/1/isalive"}),this.extraHeaders=[]};AlgoliaSearch.prototype={deleteIndex:function(a,b){this._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(a),callback:b})},moveIndex:function(a,b,c){var d={operation:"move",destination:b};this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(a)+"/operation",body:d,callback:c})},copyIndex:function(a,b,c){var d={operation:"copy",destination:b};this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(a)+"/operation",body:d,callback:c})},getLogs:function(a,b,c){this._isUndefined(b)&&(b=0),this._isUndefined(c)&&(c=10),this._jsonRequest({method:"GET",url:"/1/logs?offset="+b+"&length="+c,callback:a})},listIndexes:function(a){this._jsonRequest({method:"GET",url:"/1/indexes/",callback:a})},initIndex:function(a){return new this.Index(this,a)},listUserKeys:function(a){this._jsonRequest({method:"GET",url:"/1/keys",callback:a})},getUserKeyACL:function(a,b){this._jsonRequest({method:"GET",url:"/1/keys/"+a,callback:b})},deleteUserKey:function(a,b){this._jsonRequest({method:"DELETE",url:"/1/keys/"+a,callback:b})},addUserKey:function(a,b){var c={};c.acl=a,this._jsonRequest({method:"POST",url:"/1/keys",body:c,callback:b})},addUserKeyWithValidity:function(a,b,c,d,e){var f=this,g={};g.acl=a,g.validity=b,g.maxQueriesPerIPPerHour=c,g.maxHitsPerQuery=d,this._jsonRequest({method:"POST",url:"/1/indexes/"+f.indexName+"/keys",body:g,callback:e})},startQueriesBatch:function(){this.batch=[]},addQueryInBatch:function(a,b,c){var d="query="+encodeURIComponent(b);this._isUndefined(c)||null==c||(d=this._getSearchParams(c,d)),this.batch.push({indexName:a,params:d})},clearCache:function(){this.cache={}},sendQueriesBatch:function(a,b){for(var c=this,d={requests:[],apiKey:this.apiKey,appID:this.applicationID},e=0;e<c.batch.length;++e)d.requests.push(c.batch[e]);if(window.clearTimeout(c.onDelayTrigger),!this._isUndefined(b)&&null!=b&&b>0){var f=window.setTimeout(function(){c._sendQueriesBatch(d,a)},b);c.onDelayTrigger=f}else this._sendQueriesBatch(d,a)},Index:function(a,b){this.indexName=b,this.as=a,this.typeAheadArgs=null,this.typeAheadValueOption=null},setExtraHeader:function(a,b){this.extraHeaders.push({key:a,value:b})},_sendQueriesBatch:function(a,b){this._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/*/queries",body:a,callback:b})},_jsonRequest:function(a){var b=this,c=a.callback,d=null,e=a.url;if(this._isUndefined(a.body)||(e=a.url+"_body_"+JSON.stringify(a.body)),!this._isUndefined(a.cache)&&(d=a.cache,!this._isUndefined(d[e])))return this._isUndefined(c)||c(!0,d[e]),void 0;var f=function(g){var h=0;return b._isUndefined(g)||(h=g),b.hosts.length<=h?(b._isUndefined(c)||c(!1,{message:"Cannot contact server"}),void 0):(a.callback=function(g,i,j,k){i||b._isUndefined(k)||console.log("Error: "+k.message),i&&!b._isUndefined(a.cache)&&(d[e]=k),!i&&g&&h+1<b.hosts.length?f(h+1):b._isUndefined(c)||c(i,k)},a.hostname=b.hosts[h],b._jsonRequestByHost(a),void 0)};f()},_jsonRequestByHost:function(a){var b=null,c=this;this._isUndefined(a.body)||(b=JSON.stringify(a.body));var d=a.hostname+a.url,e=null;if(e=new XMLHttpRequest,"withCredentials"in e){e.open(a.method,d,!0),e.setRequestHeader("X-Algolia-API-Key",this.apiKey),e.setRequestHeader("X-Algolia-Application-Id",this.applicationID);for(var f=0;f<this.extraHeaders.length;++f)e.setRequestHeader(this.extraHeaders[f].key,this.extraHeaders[f].value);null!=b&&e.setRequestHeader("Content-type","application/json")}else"undefined"!=typeof XDomainRequest?(e=new XDomainRequest,e.open(a.method,d)):console.log("your browser is too old to support CORS requests");e.send(b),e.onload=function(b){if(c._isUndefined(b)||null==b.target)a.callback(!1,!0,b,JSON.parse(e.responseText));else{var d=0===b.target.status||503===b.target.status,f=200===b.target.status||201===b.target.status;a.callback(d,f,b.target,null!=b.target.response?JSON.parse(b.target.response):null)}},e.onerror=function(){a.callback(!0,!1,null,{message:"Could not connect to Host"})}},_getSearchParams:function(a,b){if(this._isUndefined(a)||null==a)return b;for(var c in a)null!=c&&a.hasOwnProperty(c)&&(b+=0===b.length?"?":"&",b+=c+"="+encodeURIComponent("[object Array]"===Object.prototype.toString.call(a[c])?JSON.stringify(a[c]):a[c]));return b},_isUndefined:function(a){return void 0===a},applicationID:null,apiKey:null,hosts:[],cache:{},extraHeaders:[]},AlgoliaSearch.prototype.Index.prototype={clearCache:function(){this.cache={}},addObject:function(a,b,c){var d=this;this.as._isUndefined(c)?this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(d.indexName),body:a,callback:b}):this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/"+encodeURIComponent(c),body:a,callback:b})},addObjects:function(a,b){for(var c=this,d={requests:[]},e=0;e<a.length;++e){var f={action:"addObject",body:a[e]};d.requests.push(f)}this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/batch",body:d,callback:b})},getObject:function(a,b,c){var d=this,e="";if(!this.as._isUndefined(c)){e="?attributes=";for(var f=0;f<c.length;++f)0!==f&&(e+=","),e+=c[f]}this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/"+encodeURIComponent(a)+e,callback:b})},partialUpdateObject:function(a,b){var c=this;this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/"+encodeURIComponent(a.objectID)+"/partial",body:a,callback:b})},partialUpdateObjects:function(a,b){for(var c=this,d={requests:[]},e=0;e<a.length;++e){var f={action:"partialUpdateObject",objectID:a[e].objectID,body:a[e]};d.requests.push(f)}this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/batch",body:d,callback:b})},saveObject:function(a,b){var c=this;this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/"+encodeURIComponent(a.objectID),body:a,callback:b})},saveObjects:function(a,b){for(var c=this,d={requests:[]},e=0;e<a.length;++e){var f={action:"updateObject",objectID:a[e].objectID,body:a[e]};d.requests.push(f)}this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/batch",body:d,callback:b})},deleteObject:function(a,b){if(null==a||0===a.length)return b(!1,{message:"empty objectID"}),void 0;var c=this;this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/"+encodeURIComponent(a),callback:b})},search:function(a,b,c,d){var e=this,f="query="+encodeURIComponent(a);if(this.as._isUndefined(c)||null==c||(f=this.as._getSearchParams(c,f)),window.clearTimeout(e.onDelayTrigger),!this.as._isUndefined(d)&&null!=d&&d>0){var g=window.setTimeout(function(){e._search(f,b)},d);e.onDelayTrigger=g}else this._search(f,b)},browse:function(a,b,c){var d=this,e="?page="+a;_.isUndefined(c)||(e+="&hitsPerPage="+c),this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(d.indexName)+"/browse"+e,callback:b})},ttAdapter:function(a){var b=this;return function(c,d){b.search(c,function(a,b){a&&d(b.hits)},a)}},waitTask:function(a,b){var c=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/task/"+a,callback:function(d,e){if(d&&"published"===e.status)b(!0,e);else{if(d&&e.pendingTask)return c.waitTask(a,b);b(!1,e)}}})},clearIndex:function(a){var b=this;this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/clear",callback:a})},getSettings:function(a){var b=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/settings",callback:a})},setSettings:function(a,b){var c=this;this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/settings",body:a,callback:b})},listUserKeys:function(a){var b=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(b.indexName)+"/keys",callback:a})},getUserKeyACL:function(a,b){var c=this;this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},deleteUserKey:function(a,b){var c=this;this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys/"+a,callback:b})},addUserKey:function(a,b){var c=this,d={};d.acl=a,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(c.indexName)+"/keys",body:d,callback:b})},addUserKeyWithValidity:function(a,b,c,d,e){var f=this,g={};g.acl=a,g.validity=b,g.maxQueriesPerIPPerHour=c,g.maxHitsPerQuery=d,this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(f.indexName)+"/keys",body:g,callback:e})},_search:function(a,b){this.as._jsonRequest({cache:this.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/query",body:{params:a,apiKey:this.as.apiKey,appID:this.as.applicationID},callback:b})},as:null,indexName:null,cache:{},typeAheadArgs:null,typeAheadValueOption:null,emptyConstructor:function(){}},function(a){var b;window.AlgoliaSearchHelper=function(c,d,e){var f={facets:[],disjunctiveFacets:[],hitsPerPage:20};this.init(c,d,a.extend({},f,e)),b=this},AlgoliaSearchHelper.prototype={init:function(a,b,c){this.client=a,this.index=b,this.options=c,this.page=0,this.refinements={},this.disjunctiveRefinements={}},search:function(a,b){this.q=a,this.searchCallback=b,this.page=0,this.refinements={},this.disjunctiveRefinements={},this._search()},toggleRefine:function(a,b){for(var c=0;c<this.options.facets.length;++c)if(this.options.facets[c]==a){var d=a+":"+b;return this.refinements[d]=!this.refinements[d],this.page=0,this._search(),!0}this.disjunctiveRefinements[a]=this.disjunctiveRefinements[a]||{};for(var e=0;e<this.options.disjunctiveFacets.length;++e)if(this.options.disjunctiveFacets[e]==a)return this.disjunctiveRefinements[a][b]=!this.disjunctiveRefinements[a][b],this.page=0,this._search(),!0;return!1},isRefined:function(a,b){var c=a+":"+b;return this.refinements[c]?!0:this.disjunctiveRefinements[a]&&this.disjunctiveRefinements[a][b]?!0:!1},nextPage:function(){this._gotoPage(this.page+1)},previousPage:function(){this.page>0&&this._gotoPage(this.page-1)},_gotoPage:function(a){this.page=a,this._search()},_search:function(){this.client.startQueriesBatch(),this.client.addQueryInBatch(this.index,this.q,this._getHitsSearchParams());for(var a=0;a<this.options.disjunctiveFacets.length;++a)this.client.addQueryInBatch(this.index,this.q,this._getDisjunctiveFacetSearchParams(this.options.disjunctiveFacets[a]));this.client.sendQueriesBatch(function(a,c){if(!a)return b.searchCallback(!1,c),void 0;var d=c.results[0];d.disjunctiveFacets={};for(var e=1;e<c.results.length;++e)for(var f in c.results[e].facets)if(d.disjunctiveFacets[f]=c.results[e].facets[f],b.disjunctiveRefinements[f])for(var g in b.disjunctiveRefinements[f])!d.disjunctiveFacets[f][g]&&b.disjunctiveRefinements[f][g]&&(d.disjunctiveFacets[f][g]=0);b.searchCallback(!0,d)})},_getHitsSearchParams:function(){return{hitsPerPage:this.options.hitsPerPage,page:this.page,facets:this.options.facets,facetFilters:this._getFacetFilters()}},_getDisjunctiveFacetSearchParams:function(a){return{hitsPerPage:1,page:0,facets:a,facetFilters:this._getFacetFilters(a)}},_getFacetFilters:function(a){var b=[];for(var c in this.refinements)this.refinements[c]&&b.push(c);for(var d in this.disjunctiveRefinements)if(d!=a){var e=[];for(var f in this.disjunctiveRefinements[d])this.disjunctiveRefinements[d][f]&&e.push(d+":"+f);e.length>0&&b.push(e)}return b}}}(jQuery); |
@@ -5,3 +5,4 @@ var semver = require('semver'), | ||
'src/version.js', | ||
'src/algoliasearch.js' | ||
'src/algoliasearch.js', | ||
'src/algoliasearch.helper.js' | ||
]; | ||
@@ -19,3 +20,3 @@ | ||
' * https://github.com/algolia/algoliasearch-client-js', | ||
' * Copyright 2013 Algolia SAS; Licensed MIT', | ||
' * Copyright 2014 Algolia SAS; Licensed MIT', | ||
' */\n\n' | ||
@@ -28,11 +29,2 @@ ].join('\n'), | ||
}, | ||
js: { | ||
options: { | ||
mangle: false, | ||
beautify: true, | ||
compress: false | ||
}, | ||
src: jsFiles, | ||
dest: '<%= buildDir %>/algoliasearch.js' | ||
}, | ||
jsmin: { | ||
@@ -48,2 +40,11 @@ options: { | ||
concat: { | ||
options: { | ||
}, | ||
dist: { | ||
src: jsFiles, | ||
dest: '<%= buildDir %>/algoliasearch.js' | ||
} | ||
}, | ||
sed: { | ||
@@ -53,3 +54,3 @@ version: { | ||
replacement: '<%= version %>', | ||
path: ['<%= uglify.js.dest %>', '<%= uglify.jsmin.dest %>'] | ||
path: ['<%= concat.dist.dest %>', '<%= uglify.jsmin.dest %>'] | ||
} | ||
@@ -79,3 +80,3 @@ }, | ||
specs: 'test/*_spec.js', | ||
template: "SpecRunner.tmpl", | ||
template: 'SpecRunner.tmpl', | ||
templateOptions: { | ||
@@ -131,3 +132,3 @@ application_id: process.env.ALGOLIA_APPLICATION_ID, | ||
grunt.registerTask('default', 'build'); | ||
grunt.registerTask('build', ['uglify', 'sed:version']); | ||
grunt.registerTask('build', ['uglify', 'concat', 'sed:version']); | ||
grunt.registerTask('server', 'connect:server'); | ||
@@ -134,0 +135,0 @@ grunt.registerTask('lint', 'jshint'); |
@@ -47,3 +47,3 @@ { | ||
}, | ||
"version": "2.3.7" | ||
"version": "2.4.0" | ||
} |
153
README.md
@@ -1,4 +0,7 @@ | ||
Algolia Search API Client for Javascript | ||
Algolia Search API Client for JavaScript | ||
================== | ||
This Javascript client let you easily use the [Algolia Search API](http://www.algolia.com) in a browser, it is compatible with most browsers: | ||
@@ -16,3 +19,4 @@ | ||
Algolia Search is a search API that provides hosted full-text, numerical and faceted search. | ||
[Algolia Search](http://www.algolia.com) is a search API that provides hosted full-text, numerical and faceted search. | ||
Algolia’s Search API makes it easy to deliver a great search experience in your apps & websites providing: | ||
@@ -29,5 +33,25 @@ | ||
[![Build Status](https://travis-ci.org/algolia/algoliasearch-client-js.png?branch=master)](https://travis-ci.org/algolia/algoliasearch-client-js) [![NPM version](https://badge.fury.io/js/algoliasearch.png)](http://badge.fury.io/js/algoliasearch) | ||
[![Build Status](https://travis-ci.org/algolia/algoliasearch-client-js.png?branch=master)](https://travis-ci.org/algolia/algoliasearch-client-js) [![NPM version](https://badge.fury.io/js/algoliasearch.png)](http://badge.fury.io/js/algoliasearch) | ||
Table of Content | ||
------------- | ||
**Get started** | ||
1. [Setup](#setup) | ||
1. [Quick Start](#quick-start) | ||
1. [General Principle](#general-principle)"]) | ||
1. [Online documentation](#online-documentation) | ||
**Commands reference** | ||
1. [Search](#search) | ||
Setup | ||
@@ -37,2 +61,5 @@ ------------- | ||
1. Download the [client](https://github.com/algolia/algoliasearch-client-js/archive/master.zip) and add a script include of `algoliasearch.min.js` | ||
@@ -49,2 +76,4 @@ 2. Initialize the client with your ApplicationID and API-Key. You can find all of them on [your Algolia account](http://www.algolia.com/users/edit). | ||
Quick Start | ||
@@ -55,3 +84,3 @@ ------------- | ||
You can then update the ```example/autocomplete.html``` file with your ```ApplicationID```, ```API-Key``` and ```index name``` to test the autocomplete feature. This version is based on [typeahead.js](http://twitter.github.io/typeahead.js/) version 0.9.3 with a small [patch](https://github.com/algolia/typeahead.js/commit/4edb95e8beb390e92720196a29186d83b8dba9d9) to allow usage of Algolia JS client. | ||
You can then update the ```example/autocomplete.html``` file with your ```ApplicationID```, ```API-Key``` and ```index name``` to test the autocomplete feature. | ||
@@ -61,2 +90,3 @@ You can also update the ```example/instantsearch.html``` file with your ```ApplicationID```, ```API-Key``` and ```index name``` to test an instant-search example. | ||
General Principle | ||
@@ -67,106 +97,28 @@ ------------- | ||
1. **sucess**: a boolean that is set to false when an error was found. | ||
1. **success**: a boolean that is set to false when an error was found. | ||
2. **content**: the object containing the answer (if an error was found, you can retrieve the error message in `content.message`) | ||
Search | ||
------------- | ||
To perform a search, you just need to initialize the index and perform a call to the search function.<br/> | ||
You can use the following optional arguments: | ||
### Query parameters | ||
#### Full Text Search parameters | ||
Online Documentation | ||
---------------- | ||
* **query**: (string) The instant-search query string, all words of the query are interpreted as prefixes (for example "John Mc" will match "John Mccamey" and "Johnathan Mccamey"). If no query parameter is set, retrieves all objects. | ||
* **queryType**: select how the query words are interpreted, it can be one of the following value: | ||
* **prefixAll**: all query words are interpreted as prefixes, | ||
* **prefixLast**: only the last word is interpreted as a prefix (default behavior), | ||
* **prefixNone**: no query word is interpreted as a prefix. This option is not recommended. | ||
* **optionalWords**: a string that contains the list of words that should be considered as optional when found in the query. The list of words is comma separated. | ||
* **minWordSizefor1Typo**: the minimum number of characters in a query word to accept one typo in this word.<br/>Defaults to 3. | ||
* **minWordSizefor2Typos**: the minimum number of characters in a query word to accept two typos in this word.<br/>Defaults to 7. | ||
Check our [online documentation](http://www.algolia.com/doc): | ||
* [Initial Import](http://www.algolia.com/doc#InitialImport) | ||
* [Ranking & Relevance](http://www.algolia.com/doc#RankingRelevance) | ||
* [Settings](http://www.algolia.com/doc#Settings) | ||
* [Search](http://www.algolia.com/doc#Search) | ||
* [Incremental Updates](http://www.algolia.com/doc#IncrementalUpdates) | ||
* [Reindexing](http://www.algolia.com/doc#Reindexing) | ||
* [Numeric-Search](http://www.algolia.com/doc#Numeric-Search) | ||
* [Category-Search](http://www.algolia.com/doc#Category-Search) | ||
* [Faceting](http://www.algolia.com/doc#Faceting) | ||
* [Geo-Search](http://www.algolia.com/doc#Geo-Search) | ||
* [Security](http://www.algolia.com/doc#Security) | ||
* [Indexing Several Types](http://www.algolia.com/doc#IndexingSeveralTypes) | ||
* [REST API](http://www.algolia.com/doc/rest) | ||
#### Pagination parameters | ||
* **page**: (integer) Pagination parameter used to select the page to retrieve.<br/>Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set `page=9` | ||
* **hitsPerPage**: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. | ||
#### Geo-search parameters | ||
* **aroundLatLng**: search for entries around a given latitude/longitude (specified as two floats separated by a comma).<br/>For example `aroundLatLng=47.316669,5.016670`).<br/>You can specify the maximum distance in meters with the **aroundRadius** parameter (in meters) and the precision for ranking with **aroundPrecision** (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).<br/>At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form `{"_geoloc":{"lat":48.853409, "lng":2.348800}}`) | ||
* **insideBoundingBox**: search entries inside a given area defined by the two extreme points of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng).<br/>For example `insideBoundingBox=47.3165,4.9665,47.3424,5.0201`).<br/>At indexing, you should specify geoloc of an object with the _geoloc attribute (in the form `{"_geoloc":{"lat":48.853409, "lng":2.348800}}`) | ||
#### Parameters to control results content | ||
* **attributesToRetrieve**: a string that contains the list of object attributes you want to retrieve (let you minimize the answer size).<br/> Attributes are separated with a comma (for example `"name,address"`), you can also use a string array encoding (for example `["name","address"]` ). By default, all attributes are retrieved. You can also use `*` to retrieve all values when an **attributesToRetrieve** setting is specified for your index. | ||
* **attributesToHighlight**: a string that contains the list of attributes you want to highlight according to the query. Attributes are separated by a comma. You can also use a string array encoding (for example `["name","address"]`). If an attribute has no match for the query, the raw value is returned. By default all indexed text attributes are highlighted. You can use `*` if you want to highlight all textual attributes. Numerical attributes are not highlighted. A matchLevel is returned for each highlighted attribute and can contain: | ||
* **full**: if all the query terms were found in the attribute, | ||
* **partial**: if only some of the query terms were found, | ||
* **none**: if none of the query terms were found. | ||
* **attributesToSnippet**: a string that contains the list of attributes to snippet alongside the number of words to return (syntax is `attributeName:nbWords`). Attributes are separated by a comma (Example: `attributesToSnippet=name:10,content:10`). <br/>You can also use a string array encoding (Example: `attributesToSnippet: ["name:10","content:10"]`). By default no snippet is computed. | ||
* **getRankingInfo**: if set to 1, the result hits will contain ranking information in **_rankingInfo** attribute. | ||
#### Numeric search parameters | ||
* **numericFilters**: a string that contains the list of numeric filters you want to apply separated by a comma. The syntax of one filter is `attributeName` followed by `operand` followed by `value`. Supported operands are `<`, `<=`, `=`, `>` and `>=`. | ||
You can have multiple conditions on one attribute like for example `numericFilters=price>100,price<1000`. You can also use a string array encoding (for example `numericFilters: ["price>100","price<1000"]`). | ||
#### Category search parameters | ||
* **tagFilters**: filter the query by a set of tags. You can AND tags by separating them by commas. To OR tags, you must add parentheses. For example, `tags=tag1,(tag2,tag3)` means *tag1 AND (tag2 OR tag3)*. You can also use a string array encoding, for example `tagFilters: ["tag1",["tag2","tag3"]]` means *tag1 AND (tag2 OR tag3)*.<br/>At indexing, tags should be added in the **_tags** attribute of objects (for example `{"_tags":["tag1","tag2"]}`). | ||
#### Faceting parameters | ||
* **facetFilters**: filter the query by a list of facets. Facets are separated by commas and each facet is encoded as `attributeName:value`. For example: `facetFilters=category:Book,author:John%20Doe`. You can also use a string array encoding (for example `["category:Book","author:John%20Doe"]`). | ||
* **facets**: List of object attributes that you want to use for faceting. <br/>Attributes are separated with a comma (for example `"category,author"` ). You can also use a JSON string array encoding (for example `["category","author"]` ). Only attributes that have been added in **attributesForFaceting** index setting can be used in this parameter. You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. | ||
#### Distinct parameter | ||
* **distinct**: If set to 1, enable the distinct feature (disabled by default) if the `attributeForDistinct` index setting is set. This feature is similar to the SQL "distinct" keyword: when enabled in a query with the `distinct=1` parameter, all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. 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. | ||
```javascript | ||
index = client.initIndex('contacts'); | ||
index.search('query string', function(success, content) { | ||
for (var h in content.hits) { | ||
console.log('Hit(' + content.hits[h].objectID + '): ' + content.hits[h].toString()); | ||
} | ||
}); | ||
index.search('query string', function(success, content) { | ||
for (var h in content.hits) { | ||
console.log('Hit(' + content.hits[h].objectID + '): ' + content.hits[h].toString()); | ||
} | ||
}, {'attributesToRetrieve': 'firstname,lastname', 'hitsPerPage': 50}); | ||
``` | ||
The server response will look like: | ||
```javascript | ||
{ | ||
"hits": [ | ||
{ | ||
"firstname": "Jimmie", | ||
"lastname": "Barninger", | ||
"objectID": "433", | ||
"_highlightResult": { | ||
"firstname": { | ||
"value": "<em>Jimmie</em>", | ||
"matchLevel": "partial" | ||
}, | ||
"lastname": { | ||
"value": "Barninger", | ||
"matchLevel": "none" | ||
}, | ||
"company": { | ||
"value": "California <em>Paint</em> & Wlpaper Str", | ||
"matchLevel": "partial" | ||
} | ||
} | ||
} | ||
], | ||
"page": 0, | ||
"nbHits": 1, | ||
"nbPages": 1, | ||
"hitsPerPage": 20, | ||
"processingTimeMS": 1, | ||
"query": "jimmie paint", | ||
"params": "query=jimmie+paint&atributesToRetrieve=firstname,lastname&hitsPerPage=50" | ||
} | ||
``` | ||
Update the index | ||
@@ -187,1 +139,4 @@ ------------- | ||
``` | ||
@@ -36,2 +36,3 @@ /* | ||
this.apiKey = apiKey; | ||
if (this._isUndefined(hostsArray)) { | ||
@@ -67,2 +68,71 @@ hostsArray = [applicationID + '-1.algolia.io', | ||
function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) { | ||
function _getHitAxplainationForOneAttr_recurse(obj, foundWords) { | ||
if (typeof obj === 'object' && 'matchedWords' in obj && 'value' in obj) { | ||
var match = false; | ||
for (var j = 0; j < obj.matchedWords.length; ++j) { | ||
var word = obj.matchedWords[j]; | ||
if (!(word in foundWords)) { | ||
foundWords[word] = 1; | ||
match = true; | ||
} | ||
} | ||
return match ? [obj.value] : []; | ||
} else if (obj instanceof Array) { | ||
var res = []; | ||
for (var i = 0; i < obj.length; ++i) { | ||
var array = _getHitAxplainationForOneAttr_recurse(obj[i], foundWords); | ||
res = res.concat(array); | ||
} | ||
return res; | ||
} else if (typeof obj === 'object') { | ||
var res = []; | ||
for (prop in obj) { | ||
if (obj.hasOwnProperty(prop)){ | ||
res = res.concat(_getHitAxplainationForOneAttr_recurse(obj[prop], foundWords)); | ||
} | ||
} | ||
return res; | ||
} | ||
return []; | ||
} | ||
function _getHitAxplainationForOneAttr(hit, foundWords, attr) { | ||
if (attr.indexOf('.') === -1) { | ||
if (attr in hit._highlightResult) { | ||
return _getHitAxplainationForOneAttr_recurse(hit._highlightResult[attr], foundWords); | ||
} | ||
return []; | ||
} | ||
var array = attr.split('.'); | ||
var obj = hit._highlightResult; | ||
for (var i = 0; i < array.length; ++i) { | ||
if (array[i] in obj) { | ||
obj = obj[array[i]]; | ||
} else { | ||
return []; | ||
} | ||
} | ||
return _getHitAxplainationForOneAttr_recurse(obj, foundWords); | ||
} | ||
var res = {}; | ||
var foundWords = {}; | ||
var title = _getHitAxplainationForOneAttr(hit, foundWords, titleAttribute); | ||
res.title = (title.length > 0) ? title[0] : ""; | ||
res.subtitles = []; | ||
if (typeof otherAttributes !== 'undefined') { | ||
for (var i = 0; i < otherAttributes.length; ++i) { | ||
var attr = _getHitAxplainationForOneAttr(hit, foundWords, otherAttributes[i]); | ||
for (var j = 0; j < attr.length; ++j) { | ||
res.subtitles.push({ attr: otherAttributes[i], value: attr[j] }); | ||
} | ||
} | ||
} | ||
return res; | ||
} | ||
AlgoliaSearch.prototype = { | ||
@@ -730,49 +800,16 @@ /* | ||
/* | ||
* Get transport layer for Typeahead.js | ||
* @param args (optional) if set, contains an object with query parameters (see search for details) | ||
* @param propertyName(optional) if set, contains the name of property that will be used for | ||
* Get a Typeahead.js adapter | ||
* @param searchParams contains an object with query parameters (see search for details) | ||
*/ | ||
getTypeaheadTransport: function(args, valueOption) { | ||
this.typeAheadArgs = args; | ||
if (typeof valueOption !== 'undefined') { | ||
this.typeAheadValueOption = valueOption; | ||
} | ||
return this; | ||
ttAdapter: function(params) { | ||
var self = this; | ||
return function(query, cb) { | ||
self.search(query, function(success, content) { | ||
if (success) { | ||
cb(content.hits); | ||
} | ||
}, params); | ||
}; | ||
}, | ||
// Method used by Typeahead.js. | ||
get: function(query, processRemoteData, that, cb, suggestions) { | ||
self = this; | ||
this.search(query, function(success, content) { | ||
if (success) { | ||
for (var i = 0; i < content.hits.length; ++i) { | ||
// Add an attribute value with the first string | ||
var obj = content.hits[i], | ||
found = false; | ||
if (typeof obj.value === 'undefined') { | ||
if (self.typeAheadValueOption != null) { | ||
if (typeof self.typeAheadValueOption === 'function') { | ||
obj.value = self.typeAheadValueOption(obj); | ||
found = true; | ||
} else if (typeof obj[self.typeAheadValueOption] !== 'undefined') { | ||
obj.value = obj[self.typeAheadValueOption]; | ||
found = true; | ||
} | ||
} | ||
if (! found) { | ||
for (var propertyName in obj) { | ||
if (!found && obj.hasOwnProperty(propertyName) && typeof obj[propertyName] === 'string') { | ||
obj.value = obj[propertyName]; | ||
found = true; | ||
} | ||
} | ||
} | ||
} | ||
suggestions.push(that._transformDatum(obj)); | ||
} | ||
cb && cb(suggestions); | ||
} | ||
}, self.typeAheadArgs); | ||
return true; | ||
}, | ||
/* | ||
@@ -779,0 +816,0 @@ * Wait the publication of a task on the server. |
@@ -24,2 +24,2 @@ /* | ||
var VERSION = '%VERSION%'; | ||
var ALGOLIA_VERSION = '%VERSION%'; |
@@ -33,3 +33,3 @@ describe('Algolia', function () { | ||
return complete; | ||
}, "ajax", 10000); | ||
}, 'ajax', 10000); | ||
runs(function() { | ||
@@ -36,0 +36,0 @@ expect(complete).toBe(true); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 2 instances in 1 package
225745
3797
1
20
135