Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(headless): Support the automatic query correction feature for the insight use case #4598

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EnableDidYouMeanParam,
FacetsParam,
FieldsToIncludeParam,
QueryCorrectionParam,
FirstResultParam,
PipelineRuleParams,
QueryParam,
Expand All @@ -33,6 +34,7 @@ export type InsightQueryRequest = InsightParam &
SortCriteriaParam &
FieldsToIncludeParam &
EnableDidYouMeanParam &
QueryCorrectionParam &
ConstantQueryParam &
TabParam &
FoldingParam &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,39 @@ import {
buildCoreDidYouMean,
DidYouMean,
DidYouMeanState,
DidYouMeanProps,
DidYouMeanOptions,
} from '../../core/did-you-mean/headless-core-did-you-mean.js';

export type {QueryCorrection, WordCorrection, DidYouMean, DidYouMeanState};
export type {
QueryCorrection,
WordCorrection,
DidYouMean,
DidYouMeanState,
DidYouMeanProps,
DidYouMeanOptions,
};

/**
* The insight DidYouMean controller is responsible for handling query corrections.
* When a query returns no result but finds a possible query correction, the controller either suggests the correction or
* automatically triggers a new query with the suggested term.
*
* @param engine - The insight engine.
* @param engine - The headless engine.
* @param props - The configurable `DidYouMean` properties.
*
* @group Controllers
* @category DidYouMean
*/
export function buildDidYouMean(engine: InsightEngine): DidYouMean {
const controller = buildCoreDidYouMean(engine, {
options: {queryCorrectionMode: 'legacy'},
});
export function buildDidYouMean(
engine: InsightEngine,
props: DidYouMeanProps = {
options: {
queryCorrectionMode: 'legacy',
},
}
): DidYouMean {
const controller = buildCoreDidYouMean(engine, props);
const {dispatch} = engine;

return {
Expand Down
SimonMilord marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -115,53 +115,161 @@ describe('AsyncInsightSearchThunkProcessor', () => {
expect(logQueryError).toHaveBeenCalledWith(theError);
});

it('process properly when there are no results returned and there is a did you mean correction', async () => {
const processor = new AsyncInsightSearchThunkProcessor<{}>(config);
const mappedRequest: MappedSearchRequest<InsightQueryRequest> = {
request: buildMockInsightQueryRequest(),
mappings: initialSearchMappings(),
};
describe('query correction processing', () => {
SimonMilord marked this conversation as resolved.
Show resolved Hide resolved
it('process properly when there are no results returned and there is a did you mean correction', async () => {
const processor = new AsyncInsightSearchThunkProcessor<{}>(config);
const mappedRequest: MappedSearchRequest<InsightQueryRequest> = {
request: buildMockInsightQueryRequest(),
mappings: initialSearchMappings(),
};

const originalResponseWithNoResultsAndCorrection = buildMockSearchResponse({
results: [],
queryCorrections: [
{
correctedQuery: 'bar',
wordCorrections: [
{correctedWord: 'foo', length: 3, offset: 0, originalWord: 'foo'},
const originalResponseWithNoResultsAndCorrection =
buildMockSearchResponse({
results: [],
queryCorrections: [
{
correctedQuery: 'bar',
wordCorrections: [
{
correctedWord: 'foo',
length: 3,
offset: 0,
originalWord: 'foo',
},
],
},
],
});

const responseAfterCorrection = buildMockSearchResponse({
results: [buildMockResult({uniqueId: '123'})],
});

(config.extra.apiClient.query as Mock).mockReturnValue(
Promise.resolve({success: responseAfterCorrection})
);

const fetched = {
response: {
success: originalResponseWithNoResultsAndCorrection,
},
],
duration: 123,
queryExecuted: 'foo',
requestExecuted: mappedRequest.request,
};

const processed = (await processor.process(
fetched
)) as ExecuteSearchThunkReturn;

expect(config.dispatch).toHaveBeenCalledWith(updateQuery({q: 'bar'}));
expect(config.extra.apiClient.query).toHaveBeenCalled();
expect(processed.response).toEqual({
...responseAfterCorrection,
queryCorrections:
originalResponseWithNoResultsAndCorrection.queryCorrections,
});
expect(processed.automaticallyCorrected).toBe(true);
});

const responseAfterCorrection = buildMockSearchResponse({
results: [buildMockResult({uniqueId: '123'})],
describe('legacy query correction processing', () => {
it('should automatically correct the query by triggering a second search request', async () => {
const processor = new AsyncInsightSearchThunkProcessor<{}>(config);
const mappedRequest: MappedSearchRequest<InsightQueryRequest> = {
request: buildMockInsightQueryRequest(),
mappings: initialSearchMappings(),
};

const originalResponseWithNoResultsAndCorrection =
buildMockSearchResponse({
results: [],
queryCorrections: [
{
correctedQuery: 'bar',
wordCorrections: [
{
correctedWord: 'foo',
length: 3,
offset: 0,
originalWord: 'foo',
},
],
},
],
});

const responseAfterCorrection = buildMockSearchResponse({
results: [buildMockResult({uniqueId: '123'})],
});

(config.extra.apiClient.query as Mock).mockReturnValue(
Promise.resolve({success: responseAfterCorrection})
);

const fetched = {
response: {
success: originalResponseWithNoResultsAndCorrection,
},
duration: 123,
queryExecuted: 'foo',
requestExecuted: mappedRequest.request,
};

const processed = (await processor.process(
fetched
)) as ExecuteSearchThunkReturn;

expect(config.dispatch).toHaveBeenCalledWith(updateQuery({q: 'bar'}));
expect(config.extra.apiClient.query).toHaveBeenCalled();
expect(processed.response).toEqual({
...responseAfterCorrection,
queryCorrections:
originalResponseWithNoResultsAndCorrection.queryCorrections,
});
expect(processed.automaticallyCorrected).toBe(true);
});
});

(config.extra.apiClient.query as Mock).mockReturnValue(
Promise.resolve({success: responseAfterCorrection})
);
describe('next query correction processing', () => {
it('should automatically correct the query without triggering a second search request', async () => {
const processor = new AsyncInsightSearchThunkProcessor<{}>(config);
const mappedRequest: MappedSearchRequest<InsightQueryRequest> = {
request: buildMockInsightQueryRequest(),
mappings: initialSearchMappings(),
};

const fetched = {
response: {
success: originalResponseWithNoResultsAndCorrection,
},
duration: 123,
queryExecuted: 'foo',
requestExecuted: mappedRequest.request,
};
const originalResponseWithResultsAndChangedQuery =
buildMockSearchResponse({
results: [buildMockResult()],
queryCorrection: {
correctedQuery: 'bar',
originalQuery: 'foo',
corrections: [],
},
});

const processed = (await processor.process(
fetched
)) as ExecuteSearchThunkReturn;
const fetched = {
response: {
success: originalResponseWithResultsAndChangedQuery,
},
duration: 123,
queryExecuted: 'foo',
requestExecuted: mappedRequest.request,
};

const processed = (await processor.process(
fetched
)) as ExecuteSearchThunkReturn;

expect(config.dispatch).toHaveBeenCalledWith(updateQuery({q: 'bar'}));
expect(config.extra.apiClient.query).toHaveBeenCalled();
expect(processed.response).toEqual({
...responseAfterCorrection,
queryCorrections:
originalResponseWithNoResultsAndCorrection.queryCorrections,
expect(config.dispatch).toHaveBeenCalledWith(updateQuery({q: 'bar'}));
expect(config.extra.apiClient.query).not.toHaveBeenCalled();
expect(processed.response).toMatchObject(
originalResponseWithResultsAndChangedQuery
);
expect(processed.automaticallyCorrected).toBe(true);
expect(processed.originalQuery).toBe('foo');
expect(processed.queryExecuted).toBe('bar');
});
});
expect(processed.automaticallyCorrected).toBe(true);
});
});
Loading
Loading