Skip to content

Commit

Permalink
feat(headless,atomic): support new query correction system (#3530)
Browse files Browse the repository at this point in the history
  • Loading branch information
olamothe authored Jan 17, 2024
1 parent 59c3ac7 commit 1d81780
Show file tree
Hide file tree
Showing 24 changed files with 492 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,14 @@ export declare interface AtomicComponentError extends Components.AtomicComponent


@ProxyCmp({
inputs: ['automaticallyCorrectQuery', 'queryCorrectionMode']
})
@Component({
selector: 'atomic-did-you-mean',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: [],
inputs: ['automaticallyCorrectQuery', 'queryCorrectionMode'],
})
export class AtomicDidYouMean {
protected el: HTMLElement;
Expand Down
16 changes: 16 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ export namespace Components {
* The `atomic-did-you-mean` component is responsible for handling query corrections. When a query returns no result but finds a possible query correction, the component either suggests the correction or automatically triggers a new query with the suggested term.
*/
interface AtomicDidYouMean {
/**
* Whether to automatically apply corrections for queries that would otherwise return no results. When `automaticallyCorrectQuery` is `true`, the component automatically triggers a new query using the suggested term. When `automaticallyCorrectQuery` is `false`, the component returns the suggested term without triggering a new query. The default value is `true`.
*/
"automaticallyCorrectQuery": boolean;
/**
* Define which query correction system to use `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. Default value is `legacy`. In the next major version of Atomic, the default value will be `next`.
*/
"queryCorrectionMode": 'legacy' | 'next';
}
/**
* The `atomic-external` component allows components defined outside of the `atomic-search-interface` to initialize.
Expand Down Expand Up @@ -4465,6 +4473,14 @@ declare namespace LocalJSX {
* The `atomic-did-you-mean` component is responsible for handling query corrections. When a query returns no result but finds a possible query correction, the component either suggests the correction or automatically triggers a new query with the suggested term.
*/
interface AtomicDidYouMean {
/**
* Whether to automatically apply corrections for queries that would otherwise return no results. When `automaticallyCorrectQuery` is `true`, the component automatically triggers a new query using the suggested term. When `automaticallyCorrectQuery` is `false`, the component returns the suggested term without triggering a new query. The default value is `true`.
*/
"automaticallyCorrectQuery"?: boolean;
/**
* Define which query correction system to use `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms. `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms. Default value is `legacy`. In the next major version of Atomic, the default value will be `next`.
*/
"queryCorrectionMode"?: 'legacy' | 'next';
}
/**
* The `atomic-external` component allows components defined outside of the `atomic-search-interface` to initialize.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
buildQueryTrigger,
QueryTriggerState,
} from '@coveo/headless';
import {Component, Fragment, h, State} from '@stencil/core';
import {Component, Fragment, h, Prop, State} from '@stencil/core';
import {
BindStateToController,
InitializableComponent,
Expand Down Expand Up @@ -46,8 +46,34 @@ export class AtomicDidYouMean implements InitializableComponent {
private queryTriggerState?: QueryTriggerState;
@State() public error!: Error;

/**
* Whether to automatically apply corrections for queries that would otherwise return no results.
* When `automaticallyCorrectQuery` is `true`, the component automatically triggers a new query using the suggested term.
* When `automaticallyCorrectQuery` is `false`, the component returns the suggested term without triggering a new query.
*
* The default value is `true`.
*/
@Prop({reflect: true}) public automaticallyCorrectQuery = true;

// TODO: V3: Default to `next`
/**
* Define which query correction system to use
*
* `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms.
* `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms.
*
* Default value is `legacy`. In the next major version of Atomic, the default value will be `next`.
*/
@Prop({reflect: true})
public queryCorrectionMode: 'legacy' | 'next' = 'legacy';

public initialize() {
this.didYouMean = buildDidYouMean(this.bindings.engine);
this.didYouMean = buildDidYouMean(this.bindings.engine, {
options: {
automaticallyCorrectQuery: this.automaticallyCorrectQuery,
queryCorrectionMode: this.queryCorrectionMode,
},
});
this.queryTrigger = buildQueryTrigger(this.bindings.engine);
}

Expand Down
7 changes: 7 additions & 0 deletions packages/headless/src/api/search/search-api-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ export interface EnableDidYouMeanParam {
enableDidYouMean?: boolean;
}

export interface QueryCorrectionParam {
queryCorrection?: {
enabled?: boolean;
options?: {automaticallyCorrect?: 'never' | 'whenNoResults'};
};
}

export interface EnableQuerySyntaxParam {
enableQuerySyntax?: boolean;
}
Expand Down
22 changes: 20 additions & 2 deletions packages/headless/src/api/search/search/query-corrections.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Describe correction for a query
* Describe correction for a query, using the older index based system.
*/
export interface QueryCorrection {
/**
Expand All @@ -9,7 +9,7 @@ export interface QueryCorrection {
/**
* Array of correction for each word in the query
*/
wordCorrections: WordCorrection[];
wordCorrections?: WordCorrection[];
}

export interface WordCorrection {
Expand All @@ -30,3 +30,21 @@ export interface WordCorrection {
*/
correctedWord: string;
}

/**
* Describe correction for a query, using the advanced machine learning based system.
*/
export interface Correction {
/**
* The original query that was performed, without any automatic correction applied.
*/
originalQuery: string;
/**
* The correction that was applied to the query.
*/
correctedQuery: string;
/**
* Array of correction for each word in the query
*/
corrections: QueryCorrection[];
}
2 changes: 2 additions & 0 deletions packages/headless/src/api/search/search/search-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
SortCriteriaParam,
TabParam,
TimezoneParam,
QueryCorrectionParam,
PipelineRuleParams,
} from '../search-api-params';

Expand All @@ -47,6 +48,7 @@ export type SearchRequest = BaseParam &
ContextParam &
DictionaryFieldContextParam &
EnableDidYouMeanParam &
QueryCorrectionParam &
EnableQuerySyntaxParam &
FieldsToIncludeParam &
PipelineParam &
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/api/search/search/search-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {Trigger} from './../trigger';
import {AutomaticFacets} from './automatic-facets';
import {ExecutionReport} from './execution-report';
import {ExtendedResults} from './extended-results';
import {QueryCorrection} from './query-corrections';
import {Correction, QueryCorrection} from './query-corrections';
import {QueryRankingExpression} from './query-ranking-expression';
import {QuestionsAnswers} from './question-answering';
import {Result} from './result';
Expand All @@ -22,7 +22,8 @@ export interface SearchResponseSuccess {
searchUid: string;
totalCountFiltered: number;
facets: AnyFacetResponse[];
queryCorrections: QueryCorrection[];
queryCorrections?: QueryCorrection[];
queryCorrection?: Correction;
triggers: Trigger[];
questionAnswer: QuestionsAnswers;
pipeline: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ import {
DidYouMeanProps,
} from './headless-core-did-you-mean';

jest.mock('pino', () => ({
...jest.requireActual('pino'),
__esModule: true,
default: () => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
}),
}));

describe('did you mean', () => {
let dym: DidYouMean;
let engine: MockSearchEngine;
Expand Down Expand Up @@ -48,24 +59,22 @@ describe('did you mean', () => {
});

it('should dispatch disableAutomaticQueryCorrection at initialization when specified', () => {
initDidYouMean({automaticallyCorrectQuery: false});
initDidYouMean({options: {automaticallyCorrectQuery: false}});

dym.applyCorrection();
expect(engine.actions).toContainEqual(disableAutomaticQueryCorrection());
});

it('should not dispatch disableAutomaticQueryCorrection at initialization when specified', () => {
initDidYouMean({automaticallyCorrectQuery: true});
initDidYouMean({options: {automaticallyCorrectQuery: true}});

dym.applyCorrection();
expect(engine.actions).not.toContainEqual(
disableAutomaticQueryCorrection()
);
});

it('should allow to update query correction when automatic correction is disabled', () => {
engine.state.didYouMean.queryCorrection.correctedQuery = 'bar';
initDidYouMean({automaticallyCorrectQuery: false});
initDidYouMean({options: {automaticallyCorrectQuery: false}});

dym.applyCorrection();
expect(engine.actions).toContainEqual(applyDidYouMeanCorrection('bar'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
applyDidYouMeanCorrection,
disableAutomaticQueryCorrection,
enableDidYouMean,
setCorrectionMode,
} from '../../../features/did-you-mean/did-you-mean-actions';
import {didYouMeanReducer as didYouMean} from '../../../features/did-you-mean/did-you-mean-slice';
import {
Expand All @@ -23,6 +24,10 @@ import {
export type {QueryCorrection, WordCorrection};

export interface DidYouMeanProps {
options?: DidYouMeanOptions;
}

export interface DidYouMeanOptions {
/**
* Whether to automatically apply corrections for queries that would otherwise return no results.
* When `automaticallyCorrectQuery` is `true`, the controller automatically triggers a new query using the suggested term.
Expand All @@ -31,6 +36,17 @@ export interface DidYouMeanProps {
* The default value is `true`.
*/
automaticallyCorrectQuery?: boolean;

// TODO: V3: Change the default value to `next`.
/**
* Define which query correction system to use
*
* `legacy`: Query correction is powered by the legacy index system. This system relies on an algorithm using solely the index content to compute the suggested terms.
* `next`: Query correction is powered by a machine learning system, requiring a valid query suggestion model configured in your Coveo environment to function properly. This system relies on machine learning algorithms to compute the suggested terms.
*
* Default value is `legacy`. In the next major version of Headless, the default value will be `next`.
*/
queryCorrectionMode?: 'legacy' | 'next';
}
export interface DidYouMean extends Controller {
/**
Expand Down Expand Up @@ -59,7 +75,6 @@ export interface DidYouMeanState {
* This happens when there is no result returned by the API for a particular misspelling.
*/
wasAutomaticallyCorrected: boolean;

/**
* The query correction that is currently applied by the "did you mean" module.
*/
Expand Down Expand Up @@ -93,10 +108,12 @@ export function buildCoreDidYouMean(

dispatch(enableDidYouMean());

if (props.automaticallyCorrectQuery === false) {
if (props.options?.automaticallyCorrectQuery === false) {
dispatch(disableAutomaticQueryCorrection());
}

dispatch(setCorrectionMode(props.options?.queryCorrectionMode || 'legacy'));

const getState = () => engine.state;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,33 @@ import {executeSearch} from '../../features/search/search-actions';
import {
buildCoreDidYouMean,
DidYouMean,
DidYouMeanProps,
DidYouMeanState,
DidYouMeanOptions,
} from '../core/did-you-mean/headless-core-did-you-mean';

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

/**
* The 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 headless engine.
* @param props - The configurable `DidYouMean` properties.
*/
export function buildDidYouMean(engine: SearchEngine): DidYouMean {
const controller = buildCoreDidYouMean(engine);
export function buildDidYouMean(
engine: SearchEngine,
props: DidYouMeanProps = {}
): DidYouMean {
const controller = buildCoreDidYouMean(engine, props);
const {dispatch} = engine;

return {
Expand Down
2 changes: 2 additions & 0 deletions packages/headless/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export type {
DidYouMeanState,
QueryCorrection,
WordCorrection,
DidYouMeanProps,
DidYouMeanOptions,
} from './did-you-mean/headless-did-you-mean';
export {buildDidYouMean} from './did-you-mean/headless-did-you-mean';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {StringValue} from '@coveo/bueno';
import {createAction} from '@reduxjs/toolkit';
import {
validatePayload,
requiredNonEmptyString,
} from '../../utils/validate-payload';
import {CorrectionMode} from './did-you-mean-state';

export const enableDidYouMean = createAction('didYouMean/enable');

Expand All @@ -20,3 +22,16 @@ export const applyDidYouMeanCorrection = createAction(
'didYouMean/correction',
(payload: string) => validatePayload(payload, requiredNonEmptyString)
);

export const setCorrectionMode = createAction(
'didYouMean/automaticCorrections/mode',
(payload: CorrectionMode) =>
validatePayload(
payload,
new StringValue({
constrainTo: ['next', 'legacy'],
emptyAllowed: false,
required: true,
})
)
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
disableDidYouMean,
disableAutomaticQueryCorrection,
enableAutomaticQueryCorrection,
setCorrectionMode,
} from './did-you-mean-actions';
import {didYouMeanReducer} from './did-you-mean-slice';
import {getDidYouMeanInitialState, DidYouMeanState} from './did-you-mean-state';
Expand Down Expand Up @@ -112,4 +113,32 @@ describe('did you mean slice', () => {
.automaticallyCorrectQuery
).toBe(false);
});

it('should handle #setCorrectionMode', () => {
state.queryCorrectionMode = 'legacy';
expect(
didYouMeanReducer(state, setCorrectionMode('next')).queryCorrectionMode
).toBe('next');
});

it('should set corrected query if mode is next', () => {
state.queryCorrectionMode = 'next';
const searchAction = executeSearch.fulfilled(
buildMockSearch({
response: buildMockSearchResponse({
queryCorrection: {
originalQuery: 'foo',
correctedQuery: 'bar',
corrections: [],
},
}),
}),
'',
{legacy: logSearchEvent({evt: 'foo'})}
);
const resultingState = didYouMeanReducer(state, searchAction);

expect(resultingState.queryCorrection.correctedQuery).toBe('bar');
expect(resultingState.wasCorrectedTo).toBe('bar');
});
});
Loading

0 comments on commit 1d81780

Please sign in to comment.