var buildSearchMethod = require('./buildSearchMethod.js'); var deprecate = require('./deprecate.js'); var deprecatedMessage = require('./deprecatedMessage.js'); module.exports = IndexCore; /* * Index class constructor. * You should not use this method directly but use initIndex() function */ function IndexCore(algoliasearch, indexName) { this.indexName = indexName; this.as = algoliasearch; this.typeAheadArgs = null; this.typeAheadValueOption = null; // make sure every index instance has it's own cache this.cache = {}; } /* * Clear all queries in cache */ IndexCore.prototype.clearCache = function() { this.cache = {}; }; /* * Search inside the index using XMLHttpRequest request (Using a POST query to * minimize number of OPTIONS queries: Cross-Origin Resource Sharing). * * @param {string} [query] the full text query * @param {object} [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 an array (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 an array (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 an array (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 an array (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 an array, 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 an array (for example `["category:Book","author:John%20Doe"]`). * - facets: List of object attributes that you want to use for faceting. * Comma separated list: `"category,author"` or array `['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. * Comma separated and array are accepted. * - 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. * - restrictSearchableAttributes: List of attributes you want to use for * textual search (must be a subset of the attributesToIndex index setting) * either comma separated or as an array * @param {function} [callback] the result callback called with two arguments: * error: null or Error('message'). If false, the content contains the error. * content: the server answer that contains the list of results. */ IndexCore.prototype.search = buildSearchMethod('query'); /* * -- BETA -- * Search a record similar to the query inside the index using XMLHttpRequest request (Using a POST query to * minimize number of OPTIONS queries: Cross-Origin Resource Sharing). * * @param {string} [query] the similar query * @param {object} [args] (optional) if set, contains an object with query parameters. * All search parameters are supported (see search function), restrictSearchableAttributes and facetFilters * are the two most useful to restrict the similar results and get more relevant content */ IndexCore.prototype.similarSearch = buildSearchMethod('similarQuery'); /* * Browse index content. The response content will have a `cursor` property that you can use * to browse subsequent pages for this query. Use `index.browseFrom(cursor)` when you want. * * @param {string} query - The full text query * @param {Object} [queryParameters] - Any search query parameter * @param {Function} [callback] - The result callback called with two arguments * error: null or Error('message') * content: the server answer with the browse result * @return {Promise|undefined} Returns a promise if no callback given * @example * index.browse('cool songs', { * tagFilters: 'public,comments', * hitsPerPage: 500 * }, callback); * @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} */ IndexCore.prototype.browse = function(query, queryParameters, callback) { var merge = require('./merge.js'); var indexObj = this; var page; var hitsPerPage; // we check variadic calls that are not the one defined // .browse()/.browse(fn) // => page = 0 if (arguments.length === 0 || arguments.length === 1 && typeof arguments[0] === 'function') { page = 0; callback = arguments[0]; query = undefined; } else if (typeof arguments[0] === 'number') { // .browse(2)/.browse(2, 10)/.browse(2, fn)/.browse(2, 10, fn) page = arguments[0]; if (typeof arguments[1] === 'number') { hitsPerPage = arguments[1]; } else if (typeof arguments[1] === 'function') { callback = arguments[1]; hitsPerPage = undefined; } query = undefined; queryParameters = undefined; } else if (typeof arguments[0] === 'object') { // .browse(queryParameters)/.browse(queryParameters, cb) if (typeof arguments[1] === 'function') { callback = arguments[1]; } queryParameters = arguments[0]; query = undefined; } else if (typeof arguments[0] === 'string' && typeof arguments[1] === 'function') { // .browse(query, cb) callback = arguments[1]; queryParameters = undefined; } // otherwise it's a .browse(query)/.browse(query, queryParameters)/.browse(query, queryParameters, cb) // get search query parameters combining various possible calls // to .browse(); queryParameters = merge({}, queryParameters || {}, { page: page, hitsPerPage: hitsPerPage, query: query }); var params = this.as._getSearchParams(queryParameters, ''); return this.as._jsonRequest({ method: 'POST', url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse', body: {params: params}, hostType: 'read', callback: callback }); }; /* * Continue browsing from a previous position (cursor), obtained via a call to `.browse()`. * * @param {string} query - The full text query * @param {Object} [queryParameters] - Any search query parameter * @param {Function} [callback] - The result callback called with two arguments * error: null or Error('message') * content: the server answer with the browse result * @return {Promise|undefined} Returns a promise if no callback given * @example * index.browseFrom('14lkfsakl32', callback); * @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} */ IndexCore.prototype.browseFrom = function(cursor, callback) { return this.as._jsonRequest({ method: 'POST', url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/browse', body: {cursor: cursor}, hostType: 'read', callback: callback }); }; /* * Search for facet values * https://www.algolia.com/doc/rest-api/search#search-for-facet-values * * @param {string} params.facetName Facet name, name of the attribute to search for values in. * Must be declared as a facet * @param {string} params.facetQuery Query for the facet search * @param {string} [params.*] Any search parameter of Algolia, * see https://www.algolia.com/doc/api-client/javascript/search#search-parameters * Pagination is not supported. The page and hitsPerPage parameters will be ignored. * @param callback (optional) */ IndexCore.prototype.searchForFacetValues = function(params, callback) { var clone = require('./clone.js'); var omit = require('./omit.js'); var usage = 'Usage: index.searchForFacetValues({facetName, facetQuery, ...params}[, callback])'; if (params.facetName === undefined || params.facetQuery === undefined) { throw new Error(usage); } var facetName = params.facetName; var filteredParams = omit(clone(params), function(keyName) { return keyName === 'facetName'; }); var searchParameters = this.as._getSearchParams(filteredParams, ''); return this.as._jsonRequest({ method: 'POST', url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/facets/' + encodeURIComponent(facetName) + '/query', hostType: 'read', body: {params: searchParameters}, callback: callback }); }; IndexCore.prototype.searchFacet = deprecate(function(params, callback) { return this.searchForFacetValues(params, callback); }, deprecatedMessage( 'index.searchFacet(params[, callback])', 'index.searchForFacetValues(params[, callback])' )); IndexCore.prototype._search = function(params, url, callback, additionalUA) { return this.as._jsonRequest({ cache: this.cache, method: 'POST', url: url || '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', body: {params: params}, hostType: 'read', fallback: { method: 'GET', url: '/1/indexes/' + encodeURIComponent(this.indexName), body: {params: params} }, callback: callback, additionalUA: additionalUA }); }; /* * Get an object from this index * * @param objectID the unique identifier of the object to retrieve * @param attrs (optional) if set, contains the array of attribute names to retrieve * @param callback (optional) the result callback called with two arguments * error: null or Error('message') * content: the object to retrieve or the error message if a failure occured */ IndexCore.prototype.getObject = function(objectID, attrs, callback) { var indexObj = this; if (arguments.length === 1 || typeof attrs === 'function') { callback = attrs; attrs = undefined; } var params = ''; if (attrs !== undefined) { params = '?attributes='; for (var i = 0; i < attrs.length; ++i) { if (i !== 0) { params += ','; } params += attrs[i]; } } return this.as._jsonRequest({ method: 'GET', url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, hostType: 'read', callback: callback }); }; /* * Get several objects from this index * * @param objectIDs the array of unique identifier of objects to retrieve */ IndexCore.prototype.getObjects = function(objectIDs, attributesToRetrieve, callback) { var isArray = require('isarray'); var map = require('./map.js'); var usage = 'Usage: index.getObjects(arrayOfObjectIDs[, callback])'; if (!isArray(objectIDs)) { throw new Error(usage); } var indexObj = this; if (arguments.length === 1 || typeof attributesToRetrieve === 'function') { callback = attributesToRetrieve; attributesToRetrieve = undefined; } var body = { requests: map(objectIDs, function prepareRequest(objectID) { var request = { indexName: indexObj.indexName, objectID: objectID }; if (attributesToRetrieve) { request.attributesToRetrieve = attributesToRetrieve.join(','); } return request; }) }; return this.as._jsonRequest({ method: 'POST', url: '/1/indexes/*/objects', hostType: 'read', body: body, callback: callback }); }; IndexCore.prototype.as = null; IndexCore.prototype.indexName = null; IndexCore.prototype.typeAheadArgs = null; IndexCore.prototype.typeAheadValueOption = null;