Skip to content
This repository has been archived by the owner on Jul 11, 2023. It is now read-only.

feat(answers): add findAnswers #804

Merged
merged 12 commits into from Dec 23, 2020
22 changes: 22 additions & 0 deletions index.d.ts
Expand Up @@ -171,6 +171,28 @@ declare namespace algoliasearchHelper {
) => void
): void;

/**
* Start the search for answers with the parameters set in the state.
* This method returns a promise.
* @param {Object} options - the options for answers API call
* @param {string[]} options.attributesForPrediction - Attributes to use for predictions. If empty, `searchableAttributes` is used instead.
* @param {string[]} options.queryLanguages - The languages in the query. Currently only supports ['en'].
* @param {number} options.nbHits - Maximum number of answers to retrieve from the Answers Engine. Cannot be greater than 1000.
*/
findAnswers(options: {
attributesForPrediction: string[];
queryLanguages: string[];
nbHits: number;
}): Promise<{
hits: Array<any & {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you not use the return type of findAnswers on the client?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method likely should be generic, like the client one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the dependency and now it's using the type from the client:
da8af13

_answer: {
extract: string;
extractAttribute: string;
score: number;
}
}>
}>;

/**
* Search for facet values based on an query and the name of a faceted attribute. This
* triggers a search and will return a promise. On top of using the query, it also sends
Expand Down
71 changes: 71 additions & 0 deletions src/algoliasearch.helper.js
Expand Up @@ -8,6 +8,8 @@ var requestBuilder = require('./requestBuilder');
var events = require('events');
var inherits = require('./functions/inherits');
var objectHasKeys = require('./functions/objectHasKeys');
var omit = require('./functions/omit');
var merge = require('./functions/merge');

var version = require('./version');

Expand Down Expand Up @@ -248,6 +250,75 @@ AlgoliaSearchHelper.prototype.searchOnce = function(options, cb) {
});
};

/**
* @typedef Answer
* @type {object}
* @property {string} extract the extracted value with highlights
* @property {string} extractAttribute the attribute used to extract the answer
* @property {number} score the score indicating how well it was matched
*/

/**
* @typedef AnswerHit
* @type {object}
* @property {Answer} _answer the object describing why the hit was chosen
*/

/**
* @typedef AnswersResult
* @type {object}
* @property {AnswerHit[]} hits the answer hits
*/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these docs aren't used as explanation while there's the definition file (only locally), so I don't think it's needed.

Are you allowed to omit the return type all together or does eslint not allow it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think? 5aa1206

/**
* Start the search for answers with the parameters set in the state.
* This method returns a promise.
* @param {Object} options - the options for answers API call
* @param {string[]} options.attributesForPrediction - Attributes to use for predictions. If empty, `searchableAttributes` is used instead.
* @param {string[]} options.queryLanguages - The languages in the query. Currently only supports ['en'].
* @param {number} options.nbHits - Maximum number of answers to retrieve from the Answers Engine. Cannot be greater than 1000.
*
* @return {AlgoliaSearchHelper}
eunjae-lee marked this conversation as resolved.
Show resolved Hide resolved
* @return {promise.<AnswersResult>} the answer results
*/
AlgoliaSearchHelper.prototype.findAnswers = function(options) {
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' // FIXME remove this line once the engine is fixed.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the doc, snippetEllipsisText is throwing an error. I've reported it to the team, but for now let's have it here.

])
}
);

if (
typeof this.client.initIndex !== 'function'
) {
throw new Error(
'search for answers was called, but this client does not have a function client.initIndex'
);
}
var index = this.client.initIndex(derivedState.index);
if (typeof index.findAnswers !== 'function') {
throw new Error(
'search for answers was called, but this client does not have a function client.initIndex(index).findAnswers'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is helpful, but the second error message gives pretty much the same information. Could it be consolidated to save space? How do we do it in searchForFacetValues?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike SFFV, the existence of findAnswers can be checked only after creating index here. So I couldn't merge them in a single conditional block.
2433fb1

);
}
return index.findAnswers(derivedState.query, options.queryLanguages, data);
};

/**
* Structure of each result when using
* [`searchForFacetValues()`](reference.html#AlgoliaSearchHelper#searchForFacetValues)
Expand Down
103 changes: 103 additions & 0 deletions test/spec/algoliasearch.helper/findAnswers.js
@@ -0,0 +1,103 @@
'use strict';

var algoliasearchHelper = require('../../../index');

function makeFakeFindAnswersResponse() {
return {
exhaustiveFacetsCount: true,
facetHits: [],
processingTimeMS: 3
};
}

function setupTestEnvironment(helperOptions) {
var findAnswers = jest.fn(function() {
return Promise.resolve([makeFakeFindAnswersResponse()]);
});

var fakeClient = {
initIndex: function() {
return {
findAnswers: findAnswers
};
}
};

var helper = algoliasearchHelper(fakeClient, 'index', helperOptions);

return {
findAnswers: findAnswers,
helper: helper
};
}

test('returns an empty array with no derived helper', function() {
var env = setupTestEnvironment();
var helper = env.helper;
var findAnswers = env.findAnswers;

return helper
.findAnswers({
attributesForPrediction: ['description'],
queryLanguages: ['en'],
nbHits: 1
})
.then(function(result) {
expect(findAnswers).toHaveBeenCalledTimes(0);
expect(result).toEqual([]);
});
});

test('returns a correct result with one derivation', function() {
var env = setupTestEnvironment();
var helper = env.helper;
var findAnswers = env.findAnswers;

helper.derive(function(state) {
return state;
});

return helper
.findAnswers({
attributesForPrediction: ['description'],
queryLanguages: ['en'],
nbHits: 1
})
.then(function(result) {
expect(findAnswers).toHaveBeenCalledTimes(1);
expect(result).toEqual([makeFakeFindAnswersResponse()]);
});
});

test('runs findAnswers with facets', function() {
var env = setupTestEnvironment({facets: ['facet1']});
var helper = env.helper;
var findAnswers = env.findAnswers;
helper.addFacetRefinement('facet1', 'facetValue');

helper.derive(function(state) {
return state;
});

helper.setQuery('hello');

return helper
.findAnswers({
attributesForPrediction: ['description'],
queryLanguages: ['en'],
nbHits: 1
})
.then(function() {
expect(findAnswers).toHaveBeenCalledTimes(1);
expect(findAnswers).toHaveBeenCalledWith('hello', ['en'], {
attributesForPrediction: ['description'],
nbHits: 1,
params: {
facetFilters: ['facet1:facetValue'],
facets: ['facet1'],
query: 'hello',
tagFilters: ''
}
});
});
});