'use strict';
var EventEmitter = require('@algolia/events');
var DerivedHelper = require('./DerivedHelper');
var escapeFacetValue = require('./functions/escapeFacetValue').escapeFacetValue;
var inherits = require('./functions/inherits');
var merge = require('./functions/merge');
var objectHasKeys = require('./functions/objectHasKeys');
var omit = require('./functions/omit');
var RecommendParameters = require('./RecommendParameters');
var requestBuilder = require('./requestBuilder');
var SearchParameters = require('./SearchParameters');
var SearchResults = require('./SearchResults');
var version = require('./version');
function AlgoliaSearchHelper(client, index, options, searchResultsOptions) {
if (typeof client.addAlgoliaAgent === 'function') {
client.addAlgoliaAgent('JS Helper (' + version + ')');
}
this.setClient(client);
var opts = options || {};
opts.index = index;
this.state = SearchParameters.make(opts);
this.recommendState = new RecommendParameters({
params: opts.recommendState,
});
this.lastResults = null;
this.lastRecommendResults = null;
this._queryId = 0;
this._recommendQueryId = 0;
this._lastQueryIdReceived = -1;
this._lastRecommendQueryIdReceived = -1;
this.derivedHelpers = [];
this._currentNbQueries = 0;
this._currentNbRecommendQueries = 0;
this._searchResultsOptions = searchResultsOptions;
}
inherits(AlgoliaSearchHelper, EventEmitter);
AlgoliaSearchHelper.prototype.search = function () {
this._search({ onlyWithDerivedHelpers: false });
return this;
};
AlgoliaSearchHelper.prototype.searchOnlyWithDerivedHelpers = function () {
this._search({ onlyWithDerivedHelpers: true });
return this;
};
AlgoliaSearchHelper.prototype.recommend = function () {
this._recommend();
return this;
};
AlgoliaSearchHelper.prototype.getQuery = function () {
var state = this.state;
return requestBuilder._getHitsSearchParams(state);
};
AlgoliaSearchHelper.prototype.searchOnce = function (options, cb) {
var tempState = !options
? this.state
: this.state.setQueryParameters(options);
var queries = requestBuilder._getQueries(tempState.index, tempState);
var self = this;
this._currentNbQueries++;
this.emit('searchOnce', {
state: tempState,
});
if (cb) {
this.client
.search(queries)
.then(function (content) {
self._currentNbQueries--;
if (self._currentNbQueries === 0) {
self.emit('searchQueueEmpty');
}
cb(null, new SearchResults(tempState, content.results), tempState);
})
.catch(function (err) {
self._currentNbQueries--;
if (self._currentNbQueries === 0) {
self.emit('searchQueueEmpty');
}
cb(err, null, tempState);
});
return undefined;
}
return this.client.search(queries).then(
function (content) {
self._currentNbQueries--;
if (self._currentNbQueries === 0) self.emit('searchQueueEmpty');
return {
content: new SearchResults(tempState, content.results),
state: tempState,
_originalResponse: content,
};
},
function (e) {
self._currentNbQueries--;
if (self._currentNbQueries === 0) self.emit('searchQueueEmpty');
throw e;
}
);
};
AlgoliaSearchHelper.prototype.findAnswers = function (options) {
console.warn('[algoliasearch-helper] answers is no longer supported');
var state = this.state;
var derivedHelper = this.derivedHelpers[0];
if (!derivedHelper) {
return Promise.resolve([]);
}
var derivedState = derivedHelper.getModifiedState(state);
var data = merge(
{
attributesForPrediction: options.attributesForPrediction,
nbHits: options.nbHits,
},
{
params: omit(requestBuilder._getHitsSearchParams(derivedState), [
'attributesToSnippet',
'hitsPerPage',
'restrictSearchableAttributes',
'snippetEllipsisText',
]),
}
);
var errorMessage =
'search for answers was called, but this client does not have a function client.initIndex(index).findAnswers';
if (typeof this.client.initIndex !== 'function') {
throw new Error(errorMessage);
}
var index = this.client.initIndex(derivedState.index);
if (typeof index.findAnswers !== 'function') {
throw new Error(errorMessage);
}
return index.findAnswers(derivedState.query, options.queryLanguages, data);
};
AlgoliaSearchHelper.prototype.searchForFacetValues = function (
facet,
query,
maxFacetHits,
userState
) {
var clientHasSFFV = typeof this.client.searchForFacetValues === 'function';
var clientHasInitIndex = typeof this.client.initIndex === 'function';
if (
!clientHasSFFV &&
!clientHasInitIndex &&
typeof this.client.search !== 'function'
) {
throw new Error(
'search for facet values (searchable) was called, but this client does not have a function client.searchForFacetValues or client.initIndex(index).searchForFacetValues'
);
}
var state = this.state.setQueryParameters(userState || {});
var isDisjunctive = state.isDisjunctiveFacet(facet);
var algoliaQuery = requestBuilder.getSearchForFacetQuery(
facet,
query,
maxFacetHits,
state
);
this._currentNbQueries++;
var self = this;
var searchForFacetValuesPromise;
if (clientHasSFFV) {
searchForFacetValuesPromise = this.client.searchForFacetValues([
{ indexName: state.index, params: algoliaQuery },
]);
} else if (clientHasInitIndex) {
searchForFacetValuesPromise = this.client
.initIndex(state.index)
.searchForFacetValues(algoliaQuery);
} else {
delete algoliaQuery.facetName;
searchForFacetValuesPromise = this.client
.search([
{
type: 'facet',
facet: facet,
indexName: state.index,
params: algoliaQuery,
},
])
.then(function processResponse(response) {
return response.results[0];
});
}
this.emit('searchForFacetValues', {
state: state,
facet: facet,
query: query,
});
return searchForFacetValuesPromise.then(
function addIsRefined(content) {
self._currentNbQueries--;
if (self._currentNbQueries === 0) self.emit('searchQueueEmpty');
content = Array.isArray(content) ? content[0] : content;
content.facetHits.forEach(function (f) {
f.escapedValue = escapeFacetValue(f.value);
f.isRefined = isDisjunctive
? state.isDisjunctiveFacetRefined(facet, f.escapedValue)
: state.isFacetRefined(facet, f.escapedValue);
});
return content;
},
function (e) {
self._currentNbQueries--;
if (self._currentNbQueries === 0) self.emit('searchQueueEmpty');
throw e;
}
);
};
AlgoliaSearchHelper.prototype.setQuery = function (q) {
this._change({
state: this.state.resetPage().setQuery(q),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.clearRefinements = function (name) {
this._change({
state: this.state.resetPage().clearRefinements(name),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.clearTags = function () {
this._change({
state: this.state.resetPage().clearTags(),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.addDisjunctiveFacetRefinement = function (
facet,
value
) {
this._change({
state: this.state.resetPage().addDisjunctiveFacetRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.addDisjunctiveRefine = function () {
return this.addDisjunctiveFacetRefinement.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.addHierarchicalFacetRefinement = function (
facet,
path
) {
this._change({
state: this.state.resetPage().addHierarchicalFacetRefinement(facet, path),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.addNumericRefinement = function (
attribute,
operator,
value
) {
this._change({
state: this.state
.resetPage()
.addNumericRefinement(attribute, operator, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.addFacetRefinement = function (facet, value) {
this._change({
state: this.state.resetPage().addFacetRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.addRefine = function () {
return this.addFacetRefinement.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.addFacetExclusion = function (facet, value) {
this._change({
state: this.state.resetPage().addExcludeRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.addExclude = function () {
return this.addFacetExclusion.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.addTag = function (tag) {
this._change({
state: this.state.resetPage().addTagRefinement(tag),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.addFrequentlyBoughtTogether = function (params) {
this._recommendChange({
state: this.recommendState.addFrequentlyBoughtTogether(params),
});
return this;
};
AlgoliaSearchHelper.prototype.addRelatedProducts = function (params) {
this._recommendChange({
state: this.recommendState.addRelatedProducts(params),
});
return this;
};
AlgoliaSearchHelper.prototype.addTrendingItems = function (params) {
this._recommendChange({
state: this.recommendState.addTrendingItems(params),
});
return this;
};
AlgoliaSearchHelper.prototype.addTrendingFacets = function (params) {
this._recommendChange({
state: this.recommendState.addTrendingFacets(params),
});
return this;
};
AlgoliaSearchHelper.prototype.addLookingSimilar = function (params) {
this._recommendChange({
state: this.recommendState.addLookingSimilar(params),
});
return this;
};
AlgoliaSearchHelper.prototype.removeNumericRefinement = function (
attribute,
operator,
value
) {
this._change({
state: this.state
.resetPage()
.removeNumericRefinement(attribute, operator, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.removeDisjunctiveFacetRefinement = function (
facet,
value
) {
this._change({
state: this.state
.resetPage()
.removeDisjunctiveFacetRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.removeDisjunctiveRefine = function () {
return this.removeDisjunctiveFacetRefinement.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.removeHierarchicalFacetRefinement = function (
facet
) {
this._change({
state: this.state.resetPage().removeHierarchicalFacetRefinement(facet),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.removeFacetRefinement = function (facet, value) {
this._change({
state: this.state.resetPage().removeFacetRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.removeRefine = function () {
return this.removeFacetRefinement.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.removeFacetExclusion = function (facet, value) {
this._change({
state: this.state.resetPage().removeExcludeRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.removeExclude = function () {
return this.removeFacetExclusion.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.removeTag = function (tag) {
this._change({
state: this.state.resetPage().removeTagRefinement(tag),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.removeFrequentlyBoughtTogether = function (id) {
this._recommendChange({
state: this.recommendState.removeParams(id),
});
return this;
};
AlgoliaSearchHelper.prototype.removeRelatedProducts = function (id) {
this._recommendChange({
state: this.recommendState.removeParams(id),
});
return this;
};
AlgoliaSearchHelper.prototype.removeTrendingItems = function (id) {
this._recommendChange({
state: this.recommendState.removeParams(id),
});
return this;
};
AlgoliaSearchHelper.prototype.removeTrendingFacets = function (id) {
this._recommendChange({
state: this.recommendState.removeParams(id),
});
return this;
};
AlgoliaSearchHelper.prototype.removeLookingSimilar = function (id) {
this._recommendChange({
state: this.recommendState.removeParams(id),
});
return this;
};
AlgoliaSearchHelper.prototype.toggleFacetExclusion = function (facet, value) {
this._change({
state: this.state.resetPage().toggleExcludeFacetRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.toggleExclude = function () {
return this.toggleFacetExclusion.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.toggleRefinement = function (facet, value) {
return this.toggleFacetRefinement(facet, value);
};
AlgoliaSearchHelper.prototype.toggleFacetRefinement = function (facet, value) {
this._change({
state: this.state.resetPage().toggleFacetRefinement(facet, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.toggleRefine = function () {
return this.toggleFacetRefinement.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.toggleTag = function (tag) {
this._change({
state: this.state.resetPage().toggleTagRefinement(tag),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.nextPage = function () {
var page = this.state.page || 0;
return this.setPage(page + 1);
};
AlgoliaSearchHelper.prototype.previousPage = function () {
var page = this.state.page || 0;
return this.setPage(page - 1);
};
function setCurrentPage(page) {
if (page < 0) throw new Error('Page requested below 0.');
this._change({
state: this.state.setPage(page),
isPageReset: false,
});
return this;
}
AlgoliaSearchHelper.prototype.setCurrentPage = setCurrentPage;
AlgoliaSearchHelper.prototype.setPage = setCurrentPage;
AlgoliaSearchHelper.prototype.setIndex = function (name) {
this._change({
state: this.state.resetPage().setIndex(name),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.setQueryParameter = function (parameter, value) {
this._change({
state: this.state.resetPage().setQueryParameter(parameter, value),
isPageReset: true,
});
return this;
};
AlgoliaSearchHelper.prototype.setState = function (newState) {
this._change({
state: SearchParameters.make(newState),
isPageReset: false,
});
return this;
};
AlgoliaSearchHelper.prototype.overrideStateWithoutTriggeringChangeEvent =
function (newState) {
this.state = new SearchParameters(newState);
return this;
};
AlgoliaSearchHelper.prototype.hasRefinements = function (attribute) {
if (objectHasKeys(this.state.getNumericRefinements(attribute))) {
return true;
} else if (this.state.isConjunctiveFacet(attribute)) {
return this.state.isFacetRefined(attribute);
} else if (this.state.isDisjunctiveFacet(attribute)) {
return this.state.isDisjunctiveFacetRefined(attribute);
} else if (this.state.isHierarchicalFacet(attribute)) {
return this.state.isHierarchicalFacetRefined(attribute);
}
return false;
};
AlgoliaSearchHelper.prototype.isExcluded = function (facet, value) {
return this.state.isExcludeRefined(facet, value);
};
AlgoliaSearchHelper.prototype.isDisjunctiveRefined = function (facet, value) {
return this.state.isDisjunctiveFacetRefined(facet, value);
};
AlgoliaSearchHelper.prototype.hasTag = function (tag) {
return this.state.isTagRefined(tag);
};
AlgoliaSearchHelper.prototype.isTagRefined = function () {
return this.hasTagRefinements.apply(this, arguments);
};
AlgoliaSearchHelper.prototype.getIndex = function () {
return this.state.index;
};
function getCurrentPage() {
return this.state.page;
}
AlgoliaSearchHelper.prototype.getCurrentPage = getCurrentPage;
AlgoliaSearchHelper.prototype.getPage = getCurrentPage;
AlgoliaSearchHelper.prototype.getTags = function () {
return this.state.tagRefinements;
};
AlgoliaSearchHelper.prototype.getRefinements = function (facetName) {
var refinements = [];
if (this.state.isConjunctiveFacet(facetName)) {
var conjRefinements = this.state.getConjunctiveRefinements(facetName);
conjRefinements.forEach(function (r) {
refinements.push({
value: r,
type: 'conjunctive',
});
});
var excludeRefinements = this.state.getExcludeRefinements(facetName);
excludeRefinements.forEach(function (r) {
refinements.push({
value: r,
type: 'exclude',
});
});
} else if (this.state.isDisjunctiveFacet(facetName)) {
var disjunctiveRefinements =
this.state.getDisjunctiveRefinements(facetName);
disjunctiveRefinements.forEach(function (r) {
refinements.push({
value: r,
type: 'disjunctive',
});
});
}
var numericRefinements = this.state.getNumericRefinements(facetName);
Object.keys(numericRefinements).forEach(function (operator) {
var value = numericRefinements[operator];
refinements.push({
value: value,
operator: operator,
type: 'numeric',
});
});
return refinements;
};
AlgoliaSearchHelper.prototype.getNumericRefinement = function (
attribute,
operator
) {
return this.state.getNumericRefinement(attribute, operator);
};
AlgoliaSearchHelper.prototype.getHierarchicalFacetBreadcrumb = function (
facetName
) {
return this.state.getHierarchicalFacetBreadcrumb(facetName);
};
AlgoliaSearchHelper.prototype._search = function (options) {
var state = this.state;
var states = [];
var mainQueries = [];
if (!options.onlyWithDerivedHelpers) {
mainQueries = requestBuilder._getQueries(state.index, state);
states.push({
state: state,
queriesCount: mainQueries.length,
helper: this,
});
this.emit('search', {
state: state,
results: this.lastResults,
});
}
var derivedQueries = this.derivedHelpers.map(function (derivedHelper) {
var derivedState = derivedHelper.getModifiedState(state);
var derivedStateQueries = derivedState.index
? requestBuilder._getQueries(derivedState.index, derivedState)
: [];
states.push({
state: derivedState,
queriesCount: derivedStateQueries.length,
helper: derivedHelper,
});
derivedHelper.emit('search', {
state: derivedState,
results: derivedHelper.lastResults,
});
return derivedStateQueries;
});
var queries = Array.prototype.concat.apply(mainQueries, derivedQueries);
var queryId = this._queryId++;
this._currentNbQueries++;
if (!queries.length) {
return Promise.resolve({ results: [] }).then(
this._dispatchAlgoliaResponse.bind(this, states, queryId)
);
}
try {
this.client
.search(queries)
.then(this._dispatchAlgoliaResponse.bind(this, states, queryId))
.catch(this._dispatchAlgoliaError.bind(this, queryId));
} catch (error) {
this.emit('error', {
error: error,
});
}
return undefined;
};
AlgoliaSearchHelper.prototype._recommend = function () {
var searchState = this.state;
var recommendState = this.recommendState;
var index = this.getIndex();
var states = [{ state: recommendState, index: index, helper: this }];
this.emit('fetch', {
recommend: {
state: recommendState,
results: this.lastRecommendResults,
},
});
var derivedQueries = this.derivedHelpers.map(function (derivedHelper) {
var derivedIndex = derivedHelper.getModifiedState(searchState).index;
if (!derivedIndex) {
return [];
}
var derivedState = derivedHelper.getModifiedRecommendState(
new RecommendParameters()
);
states.push({
state: derivedState,
index: derivedIndex,
helper: derivedHelper,
});
derivedHelper.emit('fetch', {
recommend: {
state: derivedState,
results: derivedHelper.lastRecommendResults,
},
});
return derivedState._buildQueries(derivedIndex);
});
var queries = Array.prototype.concat.apply(
this.recommendState._buildQueries(index),
derivedQueries
);
if (queries.length === 0) {
return;
}
if (
queries.length > 0 &&
typeof this.client.getRecommendations === 'undefined'
) {
console.warn(
'Please update algoliasearch/lite to the latest version in order to use recommendations widgets.'
);
return;
}
var queryId = this._recommendQueryId++;
this._currentNbRecommendQueries++;
try {
this.client
.getRecommendations(queries)
.then(this._dispatchRecommendResponse.bind(this, queryId, states))
.catch(this._dispatchRecommendError.bind(this, queryId));
} catch (error) {
this.emit('error', {
error: error,
});
}
return;
};
AlgoliaSearchHelper.prototype._dispatchAlgoliaResponse = function (
states,
queryId,
content
) {
var self = this;
if (queryId < this._lastQueryIdReceived) {
return;
}
this._currentNbQueries -= queryId - this._lastQueryIdReceived;
this._lastQueryIdReceived = queryId;
if (this._currentNbQueries === 0) this.emit('searchQueueEmpty');
var results = content.results.slice();
states.forEach(function (s) {
var state = s.state;
var queriesCount = s.queriesCount;
var helper = s.helper;
var specificResults = results.splice(0, queriesCount);
if (!state.index) {
helper.emit('result', {
results: null,
state: state,
});
return;
}
helper.lastResults = new SearchResults(
state,
specificResults,
self._searchResultsOptions
);
helper.emit('result', {
results: helper.lastResults,
state: state,
});
});
};
AlgoliaSearchHelper.prototype._dispatchRecommendResponse = function (
queryId,
states,
content
) {
if (queryId < this._lastRecommendQueryIdReceived) {
return;
}
this._currentNbRecommendQueries -=
queryId - this._lastRecommendQueryIdReceived;
this._lastRecommendQueryIdReceived = queryId;
if (this._currentNbRecommendQueries === 0) this.emit('recommendQueueEmpty');
var results = content.results.slice();
states.forEach(function (s) {
var state = s.state;
var helper = s.helper;
if (!s.index) {
helper.emit('recommend:result', {
results: null,
state: state,
});
return;
}
helper.lastRecommendResults = results;
helper.emit('recommend:result', {
recommend: {
results: helper.lastRecommendResults,
state: state,
},
});
});
};
AlgoliaSearchHelper.prototype._dispatchAlgoliaError = function (
queryId,
error
) {
if (queryId < this._lastQueryIdReceived) {
return;
}
this._currentNbQueries -= queryId - this._lastQueryIdReceived;
this._lastQueryIdReceived = queryId;
this.emit('error', {
error: error,
});
if (this._currentNbQueries === 0) this.emit('searchQueueEmpty');
};
AlgoliaSearchHelper.prototype._dispatchRecommendError = function (
queryId,
error
) {
if (queryId < this._lastRecommendQueryIdReceived) {
return;
}
this._currentNbRecommendQueries -=
queryId - this._lastRecommendQueryIdReceived;
this._lastRecommendQueryIdReceived = queryId;
this.emit('error', {
error: error,
});
if (this._currentNbRecommendQueries === 0) this.emit('recommendQueueEmpty');
};
AlgoliaSearchHelper.prototype.containsRefinement = function (
query,
facetFilters,
numericFilters,
tagFilters
) {
return (
query ||
facetFilters.length !== 0 ||
numericFilters.length !== 0 ||
tagFilters.length !== 0
);
};
AlgoliaSearchHelper.prototype._hasDisjunctiveRefinements = function (facet) {
return (
this.state.disjunctiveRefinements[facet] &&
this.state.disjunctiveRefinements[facet].length > 0
);
};
AlgoliaSearchHelper.prototype._change = function (event) {
var state = event.state;
var isPageReset = event.isPageReset;
if (state !== this.state) {
this.state = state;
this.emit('change', {
state: this.state,
results: this.lastResults,
isPageReset: isPageReset,
});
}
};
AlgoliaSearchHelper.prototype._recommendChange = function (event) {
var state = event.state;
if (state !== this.recommendState) {
this.recommendState = state;
this.emit('recommend:change', {
search: {
results: this.lastResults,
state: this.state,
},
recommend: {
results: this.lastRecommendResults,
state: this.recommendState,
},
});
}
};
AlgoliaSearchHelper.prototype.clearCache = function () {
if (this.client.clearCache) this.client.clearCache();
return this;
};
AlgoliaSearchHelper.prototype.setClient = function (newClient) {
if (this.client === newClient) return this;
if (typeof newClient.addAlgoliaAgent === 'function') {
newClient.addAlgoliaAgent('JS Helper (' + version + ')');
}
this.client = newClient;
return this;
};
AlgoliaSearchHelper.prototype.getClient = function () {
return this.client;
};
AlgoliaSearchHelper.prototype.derive = function (fn, recommendFn) {
var derivedHelper = new DerivedHelper(this, fn, recommendFn);
this.derivedHelpers.push(derivedHelper);
return derivedHelper;
};
AlgoliaSearchHelper.prototype.detachDerivedHelper = function (derivedHelper) {
var pos = this.derivedHelpers.indexOf(derivedHelper);
if (pos === -1) throw new Error('Derived helper already detached');
this.derivedHelpers.splice(pos, 1);
};
AlgoliaSearchHelper.prototype.hasPendingRequests = function () {
return this._currentNbQueries > 0;
};
module.exports = AlgoliaSearchHelper;