diff --git a/package.json b/package.json index 642790563..9a2e8c3de 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "bundlesize": [ { "path": "packages/algoliasearch/dist/algoliasearch.umd.js", - "maxSize": "7.57KB" + "maxSize": "7.65KB" }, { "path": "packages/algoliasearch/dist/algoliasearch-lite.umd.js", diff --git a/packages/algoliasearch/src/builds/browser.ts b/packages/algoliasearch/src/builds/browser.ts index 7444ddee7..9dcc2d791 100644 --- a/packages/algoliasearch/src/builds/browser.ts +++ b/packages/algoliasearch/src/builds/browser.ts @@ -68,6 +68,9 @@ import { deleteSynonym, DeleteSynonymOptions, exists, + findAnswers, + FindAnswersOptions, + FindAnswersResponse, findObject, FindObjectOptions, FindObjectResponse, @@ -237,6 +240,7 @@ export default function algoliasearch( methods: { batch, delete: deleteIndex, + findAnswers, getObject, getObjects, saveObject, @@ -345,6 +349,11 @@ export type SearchIndex = BaseSearchIndex & { query: string, requestOptions?: RequestOptions & SearchOptions ) => Readonly>>; + readonly findAnswers: ( + query: string, + queryLanguages: readonly string[], + requestOptions?: RequestOptions & FindAnswersOptions + ) => Readonly>; readonly searchForFacetValues: ( facetName: string, facetQuery: string, diff --git a/packages/algoliasearch/src/builds/node.ts b/packages/algoliasearch/src/builds/node.ts index a214648a4..20414795d 100644 --- a/packages/algoliasearch/src/builds/node.ts +++ b/packages/algoliasearch/src/builds/node.ts @@ -67,6 +67,9 @@ import { deleteSynonym, DeleteSynonymOptions, exists, + findAnswers, + FindAnswersOptions, + FindAnswersResponse, findObject, FindObjectOptions, FindObjectResponse, @@ -240,6 +243,7 @@ export default function algoliasearch( methods: { batch, delete: deleteIndex, + findAnswers, getObject, getObjects, saveObject, @@ -353,6 +357,11 @@ export type SearchIndex = BaseSearchIndex & { facetQuery: string, requestOptions?: RequestOptions & SearchOptions ) => Readonly>; + readonly findAnswers: ( + query: string, + queryLanguages: readonly string[], + requestOptions?: RequestOptions & FindAnswersOptions + ) => Readonly>; readonly batch: ( requests: readonly BatchRequest[], requestOptions?: RequestOptions diff --git a/packages/client-search/src/methods/index/findAnswers.ts b/packages/client-search/src/methods/index/findAnswers.ts new file mode 100644 index 000000000..6041dbe66 --- /dev/null +++ b/packages/client-search/src/methods/index/findAnswers.ts @@ -0,0 +1,26 @@ +import { encode } from '@algolia/client-common'; +import { MethodEnum } from '@algolia/requester-common'; +import { RequestOptions } from '@algolia/transporter'; + +import { FindAnswersOptions, FindAnswersResponse, SearchIndex } from '../..'; + +export const findAnswers = (base: SearchIndex) => { + return ( + query: string, + queryLanguages: readonly string[], + requestOptions?: RequestOptions & FindAnswersOptions + ): Readonly>> => { + return base.transporter.read( + { + method: MethodEnum.Post, + path: encode('1/answers/%s/prediction', base.indexName), + data: { + query, + queryLanguages, + }, + cacheable: true, + }, + requestOptions + ); + }; +}; diff --git a/packages/client-search/src/methods/index/index.ts b/packages/client-search/src/methods/index/index.ts index 376ed9c9a..6ff412547 100644 --- a/packages/client-search/src/methods/index/index.ts +++ b/packages/client-search/src/methods/index/index.ts @@ -17,6 +17,7 @@ export * from './deleteObjects'; export * from './deleteRule'; export * from './deleteSynonym'; export * from './exists'; +export * from './findAnswers'; export * from './findObject'; export * from './getObject'; export * from './getObjectPosition'; diff --git a/packages/client-search/src/types/FindAnswersOptions.ts b/packages/client-search/src/types/FindAnswersOptions.ts new file mode 100644 index 000000000..9dc0a6ac4 --- /dev/null +++ b/packages/client-search/src/types/FindAnswersOptions.ts @@ -0,0 +1,8 @@ +import { SearchOptions } from './SearchOptions'; + +export type FindAnswersOptions = { + readonly attributesForPrediction?: readonly string[]; + readonly nbHits?: number; + readonly threshold?: number; + readonly params?: SearchOptions; +}; diff --git a/packages/client-search/src/types/FindAnswersResponse.ts b/packages/client-search/src/types/FindAnswersResponse.ts new file mode 100644 index 000000000..7f8f12925 --- /dev/null +++ b/packages/client-search/src/types/FindAnswersResponse.ts @@ -0,0 +1,21 @@ +import { Hit } from './Hit'; +import { SearchResponse } from './SearchResponse'; + +export type FindAnswersResponse = SearchResponse & { + /** + * The hits returned by the search. + * + * Hits are ordered according to the ranking or sorting of the index being queried. + */ + hits: Array< + Hit< + TObject & { + _answer?: { + extract: string; + score: number; + extractAttribute: string; + }; + } + > + >; +}; diff --git a/packages/client-search/src/types/SearchOptions.ts b/packages/client-search/src/types/SearchOptions.ts index fc40265a6..b825431e3 100644 --- a/packages/client-search/src/types/SearchOptions.ts +++ b/packages/client-search/src/types/SearchOptions.ts @@ -312,7 +312,7 @@ export type SearchOptions = { /** * List of supported languages with their associated language ISO code. * - * Apply a set of natural language best practices such asignorePlurals, + * Apply a set of natural language best practices such as ignorePlurals, * removeStopWords, removeWordsIfNoResults, analyticsTags and ruleContexts. */ readonly naturalLanguages?: readonly string[]; diff --git a/packages/client-search/src/types/index.ts b/packages/client-search/src/types/index.ts index 32091b464..523dfd08e 100644 --- a/packages/client-search/src/types/index.ts +++ b/packages/client-search/src/types/index.ts @@ -25,6 +25,8 @@ export * from './DeleteByFiltersOptions'; export * from './DeleteResponse'; export * from './DeleteSynonymOptions'; export * from './FacetHit'; +export * from './FindAnswersOptions'; +export * from './FindAnswersResponse'; export * from './FindObjectOptions'; export * from './FindObjectResponse'; export * from './GetApiKeyResponse'; diff --git a/specs/.prettierrc b/specs/.prettierrc new file mode 100644 index 000000000..1ca87ab7d --- /dev/null +++ b/specs/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": false +} diff --git a/specs/answers.spec.ts b/specs/answers.spec.ts new file mode 100644 index 000000000..bb52c82bd --- /dev/null +++ b/specs/answers.spec.ts @@ -0,0 +1,70 @@ +import algolia from "../packages/algoliasearch/dist/algoliasearch"; +declare const algoliasearch: typeof algolia; + +declare const browser: { + executeAsync( + cb: (arg: TArg, done: (res: TResult) => TResult) => void, + arg: TArg + ): TResult; + + url(to: string): void; +}; + +const credentials = { + appId: "CKOEQ4XGMU", + apiKey: "6560d3886292a5aec86d63b9a2cba447" + // TODO: change these credentials to the main ones once enabled on our app + // appId: `${process.env.ALGOLIA_APPLICATION_ID_1}`, + // apiKey: `${process.env.ALGOLIA_SEARCH_KEY_1}` +}; + +describe("answers features - algoliasearch.com", () => { + beforeEach(async () => browser.url("algoliasearch.com")); + + it("searchIndex::findAnswers", async () => { + const results: any = await browser.executeAsync(function( + credentials, + done + ) { + const client = algoliasearch(credentials.appId, credentials.apiKey); + + // TODO: remove this customization once the engine accepts url encoded query params + client.transporter.userAgent.value = "answers-test"; + + const index = client.initIndex("ted"); + + Promise.all([ + index.findAnswers("sir ken robinson", ["en"]), + index.findAnswers("what", ["en"]), + index.findAnswers("sarah jones", ["en"], { + nbHits: 2, + attributesForPrediction: ["main_speaker"], + params: { + highlightPreTag: "_pre_", + highlightPostTag: "_post_" + } + }) + ]).then(function(responses) { + done({ + kenRobinson: responses[0], + what: responses[1], + sarah: responses[2] + }); + }); + }, + credentials); + + expect(results.kenRobinson.nbHits).toBe(10); + + expect(results.what.nbHits).toBe(0); + + expect(results.sarah.nbHits).toBe(1); + expect(results.sarah.hits[0]._highlightResult.main_speaker.value).toBe( + "_pre_Sarah_post_ _pre_Jones_post_" + ); + + expect(results.sarah.hits[0]._answer.extract).toBe( + "_pre_Sarah_post_ _pre_Jones_post_" + ); + }); +}); diff --git a/specs/search.spec.ts b/specs/search.spec.ts index 5f6ad5846..84ff13acf 100644 --- a/specs/search.spec.ts +++ b/specs/search.spec.ts @@ -1,3 +1,16 @@ +import algolia from "../packages/algoliasearch/dist/algoliasearch"; +declare const algoliasearch: typeof algolia; + +declare const browser: { + executeAsync( + cb: (arg: TArg, done: (res: TResult) => TResult) => void, + arg: TArg + ): TResult; + executeAsync(cb: (done: (res: TResult) => void) => void): TResult; + + url(to: string): void; +}; + const objects = [ { color: "red", @@ -29,7 +42,6 @@ const objects = [ } ]; -// @ts-ignore const credentials = { appId: `${process.env.ALGOLIA_APPLICATION_ID_1}`, apiKey: `${process.env.ALGOLIA_SEARCH_KEY_1}` @@ -38,7 +50,6 @@ const credentials = { // @ts-ignore const version = require("../lerna.json").version; - ["algoliasearch-lite.com", "algoliasearch.com"].forEach(preset => { describe(`search features - ${preset}`, () => { beforeEach(async () => browser.url(preset)); @@ -66,7 +77,10 @@ const version = require("../lerna.json").version; }); it("searchClient::searchForFacetValues and searchIndex::searchForFacetValues", async () => { - const results = await browser.executeAsync(function(credentials, done) { + const results: any = await browser.executeAsync(function( + credentials, + done + ) { const client = algoliasearch(credentials.appId, credentials.apiKey); const index = client.initIndex("javascript-browser-testing-lite"); @@ -80,19 +94,23 @@ const version = require("../lerna.json").version; green: responses[1] }); }); - }, credentials); + }, + credentials); expect(results.red.facetHits.pop().value).toEqual("red"); expect(results.green.facetHits.pop().value).toEqual("green"); }); it("cache requests", async () => { - const responses = await browser.executeAsync(function(credentials, done) { + const responses: any = await browser.executeAsync(function( + credentials, + done + ) { const client = algoliasearch(credentials.appId, credentials.apiKey); const params = [ { indexName: "javascript-browser-testing-lite", - params: { clickAnalytics: "true" } + params: { clickAnalytics: true } } ]; const promise = client.search(params); @@ -100,7 +118,8 @@ const version = require("../lerna.json").version; const promise3 = client.search(params, { cacheable: false }); return Promise.all([promise, promise2, promise3]).then(done); - }, credentials); + }, + credentials); expect(responses.length).toBe(3); const queryID = responses[0].results[0].queryID; @@ -113,12 +132,15 @@ const version = require("../lerna.json").version; }); it("cache responses", async () => { - const responses = await browser.executeAsync(function(credentials, done) { + const responses: any = await browser.executeAsync(function( + credentials, + done + ) { const client = algoliasearch(credentials.appId, credentials.apiKey); const params = [ { indexName: "javascript-browser-testing-lite", - params: { clickAnalytics: "true" } + params: { clickAnalytics: true } } ]; return client @@ -127,7 +149,8 @@ const version = require("../lerna.json").version; return Promise.all([response, client.search(params)]); }) .then(done); - }, credentials); + }, + credentials); expect(responses.length).toBe(2); const queryID = responses[0].results[0].queryID; @@ -135,14 +158,13 @@ const version = require("../lerna.json").version; expect(queryID).toBe(queryID2); }); - it("contains version", async () => { const browserVersion: string = await browser.executeAsync(function(done) { done(algoliasearch.version); }); - + expect(browserVersion).toBe(version); - expect(browserVersion.startsWith('4.')).toBe(true); + expect(browserVersion.startsWith("4.")).toBe(true); }); }); });