algoliasearch
Version:
Algolia Search API Client
1,096 lines (1,036 loc) • 63.2 kB
JavaScript
/*
* 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) {
var self = this;
this.applicationID = applicationID;
this.apiKey = apiKey;
this.hosts = [];
if (this._isUndefined(hostsArray)) {
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() > 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]);
} else {
this.hosts.push('http://' + hostsArray[i]);
}
}
if (Math.random() > 0.5) {
this.hosts.reverse();
}
// resolve DNS + check CORS support (JSONP fallback)
this.requestTimeoutInMs = 2000;
this.currentHostIndex = 0;
this.jsonp = null;
this.jsonpWait = 0;
this._jsonRequest({
method: 'GET',
url: '/1/isalive',
callback: function(success, content) {
self.jsonp = !success;
},
removeCustomHTTPHeaders: true
});
this.extraHeaders = [];
};
function AlgoliaExplainResults(hit, titleAttribute, otherAttributes) {
function _getHitExplanationForOneAttr_recurse(obj, foundWords) {
var res = [];
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;
}
}
if (match) {
res.push(obj.value);
}
} else if (Object.prototype.toString.call(obj) === '[object Array]') {
for (var i = 0; i < obj.length; ++i) {
var array = _getHitExplanationForOneAttr_recurse(obj[i], foundWords);
res = res.concat(array);
}
} else if (typeof obj === 'object') {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)){
res = res.concat(_getHitExplanationForOneAttr_recurse(obj[prop], foundWords));
}
}
}
return res;
}
function _getHitExplanationForOneAttr(hit, foundWords, attr) {
var base = hit._highlightResult || hit;
if (attr.indexOf('.') === -1) {
if (attr in base) {
return _getHitExplanationForOneAttr_recurse(base[attr], foundWords);
}
return [];
}
var array = attr.split('.');
var obj = base;
for (var i = 0; i < array.length; ++i) {
if (Object.prototype.toString.call(obj) === '[object Array]') {
var res = [];
for (var j = 0; j < obj.length; ++j) {
res = res.concat(_getHitExplanationForOneAttr(obj[j], foundWords, array.slice(i).join('.')));
}
return res;
}
if (array[i] in obj) {
obj = obj[array[i]];
} else {
return [];
}
}
return _getHitExplanationForOneAttr_recurse(obj, foundWords);
}
var res = {};
var foundWords = {};
var title = _getHitExplanationForOneAttr(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 = _getHitExplanationForOneAttr(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 });
},
/**
* 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 });
},
/**
* 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 });
},
/**
* 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) {
if (this._isUndefined(offset)) {
offset = 0;
}
if (this._isUndefined(length)) {
length = 10;
}
this._jsonRequest({ method: 'GET',
url: '/1/logs?offset=' + offset + '&length=' + length,
callback: callback });
},
/*
* List all existing indexes (paginated)
*
* @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.
* @param page The page to retrieve, starting at 0.
*/
listIndexes: function(callback, page) {
var params = page ? '?page=' + page : '';
this._jsonRequest({ method: 'GET',
url: '/1/indexes' + params,
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 });
},
/*
* 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 });
},
/*
* 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 });
},
/*
* 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 });
},
/*
* 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) {
var indexObj = this;
var aclsObject = {};
aclsObject.acl = acls;
aclsObject.validity = validity;
aclsObject.maxQueriesPerIPPerHour = maxQueriesPerIPPerHour;
aclsObject.maxHitsPerQuery = maxHitsPerQuery;
this._jsonRequest({ method: 'POST',
url: '/1/indexes/' + indexObj.indexName + '/keys',
body: aclsObject,
callback: callback });
},
/**
* Set the extra security tagFilters header
* @param {string|array} tags The list of tags defining the current security filters
*/
setSecurityTags: function(tags) {
if (Object.prototype.toString.call(tags) === '[object Array]') {
var strTags = [];
for (var i = 0; i < tags.length; ++i) {
if (Object.prototype.toString.call(tags[i]) === '[object Array]') {
var oredTags = [];
for (var j = 0; j < tags[i].length; ++j) {
oredTags.push(tags[i][j]);
}
strTags.push('(' + oredTags.join(',') + ')');
} else {
strTags.push(tags[i]);
}
}
tags = strTags.join(',');
}
this.tagFilters = tags;
},
/**
* Set the extra user token header
* @param {string} userToken The token identifying a uniq user (used to apply rate limits)
*/
setUserToken: function(userToken) {
this.userToken = userToken;
},
/*
* 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);
if (!this._isUndefined(args) && args !== null) {
params = this._getSearchParams(args, 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};
if (this.userToken) {
params['X-Algolia-UserToken'] = this.userToken;
}
if (this.tagFilters) {
params['X-Algolia-TagFilters'] = this.tagFilters;
}
for (var i = 0; i < as.batch.length; ++i) {
params.requests.push(as.batch[i]);
}
window.clearTimeout(as.onDelayTrigger);
if (!this._isUndefined(delay) && delay !== null && delay > 0) {
var onDelayTrigger = window.setTimeout( function() {
as._sendQueriesBatch(params, callback);
}, delay);
as.onDelayTrigger = onDelayTrigger;
} else {
this._sendQueriesBatch(params, callback);
}
},
/**
* Set the number of milliseconds a request can take before automatically being terminated.
*
* @param {Number} milliseconds
*/
setRequestTimeout: function(milliseconds)
{
if (milliseconds) {
this.requestTimeoutInMs = parseInt(milliseconds, 10);
}
},
/*
* Index class constructor.
* You should not use this method directly but use initIndex() function
*/
Index: function(algoliasearch, indexName) {
this.indexName = indexName;
this.as = algoliasearch;
this.typeAheadArgs = null;
this.typeAheadValueOption = null;
},
/**
* Add an extra field to the HTTP request
*
* @param key the header field name
* @param value the header field value
*/
setExtraHeader: function(key, value) {
this.extraHeaders.push({ key: key, value: value});
},
_sendQueriesBatch: function(params, callback) {
if (this.jsonp === null) {
var self = this;
this._waitReady(function() { self._sendQueriesBatch(params, callback); });
return;
}
if (this.jsonp) {
var jsonpParams = '';
for (var i = 0; i < params.requests.length; ++i) {
var q = '/1/indexes/' + encodeURIComponent(params.requests[i].indexName) + '?' + params.requests[i].params;
jsonpParams += i + '=' + encodeURIComponent(q) + '&';
}
this._jsonRequest({ cache: this.cache,
method: 'GET', jsonp: true,
url: '/1/indexes/*',
body: { params: jsonpParams },
callback: callback });
} else {
this._jsonRequest({ cache: this.cache,
method: 'POST',
url: '/1/indexes/*/queries',
body: params,
callback: callback,
removeCustomHTTPHeaders: true});
}
},
/*
* Wrapper that try all hosts to maximize the quality of service
*/
_jsonRequest: function(opts) {
var successiveRetryCount = 0;
var self = this;
var callback = opts.callback;
var cache = null;
var cacheID = opts.url;
if (!this._isUndefined(opts.body)) {
cacheID = opts.url + '_body_' + JSON.stringify(opts.body);
}
if (!this._isUndefined(opts.cache)) {
cache = opts.cache;
if (!this._isUndefined(cache[cacheID])) {
if (!this._isUndefined(callback)) {
setTimeout(function () { callback(true, cache[cacheID]); }, 1);
}
return;
}
}
var impl = function() {
if (successiveRetryCount >= self.hosts.length) {
console && console.log('Cannot connect the Algolia\'s InstantSearch API. Please send an email to support@algolia.com to report the issue.');
return;
}
opts.callback = function(retry, success, res, body) {
if (!success && !self._isUndefined(body)) {
console && console.log('Error: ' + body.message);
}
if (success && !self._isUndefined(opts.cache)) {
cache[cacheID] = body;
}
if (!success && retry && self.currentHostIndex <= self.hosts.length) {
self.currentHostIndex = ++self.currentHostIndex % self.hosts.length;
successiveRetryCount += 1;
console && console.log('self.currentHostIndex', self.currentHostIndex, successiveRetryCount);
impl();
} else {
if (!self._isUndefined(callback)) {
successiveRetryCount = 0;
callback(success, body);
}
}
};
opts.hostname = self.hosts[self.currentHostIndex];
self._jsonRequestByHost(opts);
};
impl();
},
_jsonRequestByHost: function(opts) {
var self = this;
var url = opts.hostname + opts.url;
if (this.jsonp) {
this._makeJsonpRequestByHost(url, opts);
} else {
this._makeXmlHttpRequestByHost(url, opts);
}
},
/**
* Make a JSONP request
*
* @param url request url (includes endpoint and path)
* @param opts all request options
*/
_makeJsonpRequestByHost: function(url, opts) {
if (!opts.jsonp) {
opts.callback(true, false, null, { 'message': 'Method ' + opts.method + ' ' + url + ' is not supported by JSONP.' });
return;
}
this.jsonpCounter = this.jsonpCounter || 0;
this.jsonpCounter += 1;
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
var cb = 'algoliaJSONP_' + this.jsonpCounter;
var done = false;
var ontimeout = null;
window[cb] = function(data) {
opts.callback(false, true, null, data);
try { delete window[cb]; } catch (e) { window[cb] = undefined; }
};
script.type = 'text/javascript';
script.src = url + '?callback=' + cb + ',' + this.applicationID + ',' + this.apiKey;
if (opts['X-Algolia-TagFilters']) {
script.src += '&X-Algolia-TagFilters=' + opts['X-Algolia-TagFilters'];
}
if (opts['X-Algolia-UserToken']) {
script.src += '&X-Algolia-UserToken=' + opts['X-Algolia-UserToken'];
}
if (opts.body && opts.body.params) {
script.src += '&' + opts.body.params;
}
ontimeout = setTimeout(function() {
script.onload = script.onreadystatechange = script.onerror = null;
window[cb] = function(data) {
try { delete window[cb]; } catch (e) { window[cb] = undefined; }
};
opts.callback(true, false, null, { 'message': 'Timeout - Failed to load JSONP script.' });
head.removeChild(script);
clearTimeout(ontimeout);
ontimeout = null;
}, this.requestTimeoutInMs);
script.onload = script.onreadystatechange = function() {
clearTimeout(ontimeout);
ontimeout = null;
if (!done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete')) {
done = true;
if (typeof window[cb + '_loaded'] === 'undefined') {
opts.callback(true, false, null, { 'message': 'Failed to load JSONP script.' });
try { delete window[cb]; } catch (e) { window[cb] = undefined; }
} else {
try { delete window[cb + '_loaded']; } catch (e) { window[cb + '_loaded'] = undefined; }
}
script.onload = script.onreadystatechange = null; // Handle memory leak in IE
head.removeChild(script);
}
};
script.onerror = function() {
clearTimeout(ontimeout);
ontimeout = null;
opts.callback(true, false, null, { 'message': 'Failed to load JSONP script.' });
head.removeChild(script);
try { delete window[cb]; } catch (e) { window[cb] = undefined; }
};
head.appendChild(script);
},
/**
* Make a XmlHttpRequest
*
* @param url request url (includes endpoint and path)
* @param opts all request opts
*/
_makeXmlHttpRequestByHost: function(url, opts) {
var self = this;
var xmlHttp = window.XMLHttpRequest ? new XMLHttpRequest() : {};
var body = null;
var ontimeout = null;
if (!this._isUndefined(opts.body)) {
body = JSON.stringify(opts.body);
}
if ('withCredentials' in xmlHttp) {
xmlHttp.open(opts.method, url , true);
if (this._isUndefined(opts.removeCustomHTTPHeaders) || !opts.removeCustomHTTPHeaders) {
xmlHttp.setRequestHeader('X-Algolia-API-Key', this.apiKey);
xmlHttp.setRequestHeader('X-Algolia-Application-Id', this.applicationID);
}
xmlHttp.timeout = this.requestTimeoutInMs;
for (var i = 0; i < this.extraHeaders.length; ++i) {
xmlHttp.setRequestHeader(this.extraHeaders[i].key, this.extraHeaders[i].value);
}
if (body !== null) {
xmlHttp.setRequestHeader('Content-type', 'application/json');
}
} 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 {
// very old browser, not supported
console && console.log('Your browser is too old to support CORS requests');
opts.callback(false, false, null, { 'message': 'CORS not supported' });
return;
}
ontimeout = setTimeout(function() {
xmlHttp.abort();
// Prevent Internet Explorer 9, JScript Error c00c023f
if (xmlHttp.aborted === true) {
stopLoadAnimation();
return;
}
opts.callback(true, false, null, { 'message': 'Timeout - Could not connect to endpoint ' + url } );
clearTimeout(ontimeout);
ontimeout = null;
}, this.requestTimeoutInMs);
xmlHttp.onload = function(event) {
clearTimeout(ontimeout);
ontimeout = null;
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);
opts.callback(retry, success, event.target, event.target.response !== null ? JSON.parse(event.target.response) : null);
} else {
opts.callback(false, true, event, JSON.parse(xmlHttp.responseText));
}
};
xmlHttp.ontimeout = function(event) { // stop the network call but rely on ontimeout to call opt.callback
}
xmlHttp.onerror = function(event) {
clearTimeout(ontimeout);
ontimeout = null;
opts.callback(true, false, null, { 'message': 'Could not connect to host', 'error': event } );
};
xmlHttp.send(body);
},
/**
* Wait until JSONP flag has been set to perform the first query
*/
_waitReady: function(cb) {
if (this.jsonp === null) {
this.jsonpWait += 100;
if (this.jsonpWait > 2000) {
this.jsonp = true;
}
setTimeout(cb, 100);
}
},
/*
* Transform search param object in query string
*/
_getSearchParams: function(args, params) {
if (this._isUndefined(args) || args === null) {
return params;
}
for (var key in args) {
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]);
}
}
return params;
},
_isUndefined: function(obj) {
return obj === void 0;
},
/// internal attributes
applicationID: null,
apiKey: null,
tagFilters: null,
userToken: null,
hosts: [],
cache: {},
extraHeaders: []
};
/*
* Contains all the functions related to one index
* You should use AlgoliaSearch.initIndex(indexName) to retrieve this object
*/
AlgoliaSearch.prototype.Index.prototype = {
/*
* 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 });
}
},
/*
* 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) {
if (this.as.jsonp === null) {
var self = this;
this.as._waitReady(function() { self.getObject(objectID, callback, attributes); });
return;
}
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];
}
}
this.as._jsonRequest({ method: 'GET', jsonp: true,
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) {
if (body.status === 'published') {
callback(true, body);
} else {
setTimeout(function() { indexObj.waitTask(taskID, callback); }, 100);
}
} else {
callback(false, body);
}
}});
},
/*
* 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 });