UNPKG

algoliasearch

Version:
1,096 lines (1,036 loc) 63.2 kB
/* * 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 });