[App Search] New Precision Slider for Relevance Tuning (#103015)

* Precision is now a required param for search settings

* Made RelevanceTuningLogic aware of precision param

* New PrecisionSlider component

* Add PrecisionSlider to RelevanceTuning

* Fix types in test

* Fix imports for PrecisionSlider

* Slight panel and text adjustments.

* Comment out docs link

* Add commented out test for docs

* Can we just all agree not to talk about this commit

* Restore docs link

* Fix docs link again

* Clean-up step description logic

* Test for documentation link

* Moving the spacer to align titles.

* Missing test for updatePrecision

* Fix CSS for step description

* Remove containing EuiPanel

* Improve screen reader experience

Co-authored-by: Davey Holler <daveyholler@hey.com>
This commit is contained in:
Byron Hulcher 2021-06-28 14:50:32 -04:00 committed by GitHub
parent cfd836014d
commit 043a2e2fe8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 354 additions and 4 deletions

View file

@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
const STEP_01_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step01.description',
{
defaultMessage: 'Lowest precision and highest recall setting.',
}
);
const STEP_02_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step02.description',
{
defaultMessage: 'Default. High recall, low precision.',
}
);
const STEP_03_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step03.description',
{
defaultMessage: 'Increasing phrase matching: half the terms.',
}
);
const STEP_04_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step04.description',
{
defaultMessage: 'Increasing phrase matching: three-quarters of the terms.',
}
);
const STEP_05_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step05.description',
{
defaultMessage: 'Increasing phrase matching requirements: all but one of the terms.',
}
);
const STEP_06_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step06.description',
{
defaultMessage: 'All terms must match.',
}
);
const STEP_07_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step07.description',
{
defaultMessage:
'The strictest phrase matching requirement: all terms must match, and in the same field.',
}
);
const STEP_08_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step08.description',
{
defaultMessage: 'Decreasing typo tolerance: advanced typo tolerance is disabled.',
}
);
const STEP_09_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step09.description',
{
defaultMessage: 'Decreasing term matching: prefixing is disabled.',
}
);
const STEP_10_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step10.description',
{
defaultMessage: 'Decreasing typo-tolerance: no compound-word correction.',
}
);
const STEP_11_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.step11.description',
{
defaultMessage: 'Exact spelling matches only.',
}
);
export const STEP_DESCRIPTIONS = [
undefined, // The precision number we get from the API starts with 1 instead of 0, so we leave this blank
STEP_01_DESCRIPTION,
STEP_02_DESCRIPTION,
STEP_03_DESCRIPTION,
STEP_04_DESCRIPTION,
STEP_05_DESCRIPTION,
STEP_06_DESCRIPTION,
STEP_07_DESCRIPTION,
STEP_08_DESCRIPTION,
STEP_09_DESCRIPTION,
STEP_10_DESCRIPTION,
STEP_11_DESCRIPTION,
];

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { PrecisionSlider } from './precision_slider';

View file

@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic';
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { rerender } from '../../../../../test_helpers';
import { PrecisionSlider } from './precision_slider';
const MOCK_VALUES = {
// RelevanceTuningLogic
searchSettings: {
precision: 2,
},
};
const MOCK_ACTIONS = {
// RelevanceTuningLogic
updatePrecision: jest.fn(),
};
describe('PrecisionSlider', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
jest.clearAllMocks();
setMockValues(MOCK_VALUES);
setMockActions(MOCK_ACTIONS);
wrapper = shallow(<PrecisionSlider />);
});
describe('Range Slider', () => {
it('has the correct min and max', () => {
expect(wrapper.find('[data-test-subj="PrecisionRange"]').prop('min')).toEqual(1);
expect(wrapper.find('[data-test-subj="PrecisionRange"]').prop('max')).toEqual(11);
});
it('displays the correct value', () => {
expect(wrapper.find('[data-test-subj="PrecisionRange"]').prop('value')).toEqual(2);
});
it('calls updatePrecision on change', () => {
wrapper
.find('[data-test-subj="PrecisionRange"]')
.simulate('change', { target: { value: 10 } });
expect(MOCK_ACTIONS.updatePrecision).toHaveBeenCalledWith(10);
});
});
describe('Step Description', () => {
it('is visible when there is a step description', () => {
setMockValues({ ...MOCK_VALUES, precision: 10 });
rerender(wrapper);
expect(wrapper.find('[data-test-subj="StepDescription"]').render().text()).toEqual(
'Default. High recall, low precision.'
);
});
it('is hidden when there is no step description', () => {
setMockValues({ ...MOCK_VALUES, precision: 14 });
rerender(wrapper);
expect(wrapper.contains('[data-test-subj="StepDescription"]')).toBe(false);
});
});
it('contains a documentation link', () => {
const documentationLink = wrapper.find('[data-test-subj="documentationLink"]');
expect(documentationLink.prop('href')).toContain('/precision-tuning.html');
expect(documentationLink.prop('target')).toEqual('_blank');
});
});

View file

@ -0,0 +1,124 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useActions, useValues } from 'kea';
import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiPanel,
EuiRange,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DOCS_PREFIX } from '../../../../routes';
import { RelevanceTuningLogic } from '../../relevance_tuning_logic';
import { STEP_DESCRIPTIONS } from './constants';
import './precision_slider.scss';
export const PrecisionSlider: React.FC = () => {
const {
searchSettings: { precision },
} = useValues(RelevanceTuningLogic);
const { updatePrecision } = useActions(RelevanceTuningLogic);
const stepDescription = STEP_DESCRIPTIONS[precision];
return (
<>
<EuiTitle size="m">
<h2>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.title',
{
defaultMessage: 'Precision tuning',
}
)}
</h2>
</EuiTitle>
<EuiSpacer />
<EuiText color="subdued">
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.description',
{
defaultMessage: 'Fine tune the precision vs. recall settings on your engine.',
}
)}{' '}
<EuiLink
data-test-subj="documentationLink"
href={`${DOCS_PREFIX}/precision-tuning.html`}
target="_blank"
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.learnMore.link',
{
defaultMessage: 'Learn more.',
}
)}
</EuiLink>
</EuiText>
<EuiSpacer />
<EuiFlexGroup justifyContent="spaceBetween" aria-hidden>
<EuiFlexItem grow={false}>
<strong>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.recall.label',
{
defaultMessage: 'Recall',
}
)}
</strong>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<strong>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.precision.label',
{
defaultMessage: 'Precision',
}
)}
</strong>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiRange
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.precisionSlider.ariaLabel',
{ defaultMessage: 'Recall vs. precision' }
)}
data-test-subj="PrecisionRange"
value={precision}
onChange={(e) => {
updatePrecision(parseInt((e.target as HTMLInputElement).value, 10));
}}
min={1}
max={11}
step={1}
showRange
showTicks
fullWidth
/>
{stepDescription && (
<>
<EuiSpacer size="s" />
<EuiPanel className="stepDescription" color="subdued" data-test-subj="StepDescription">
{stepDescription}
</EuiPanel>
</>
)}
</>
);
};

View file

@ -16,6 +16,7 @@ import { shallow } from 'enzyme';
import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt';
import { getPageHeaderActions } from '../../../test_helpers';
import { PrecisionSlider } from './components/precision_slider';
import { RelevanceTuning } from './relevance_tuning';
import { RelevanceTuningCallouts } from './relevance_tuning_callouts';
@ -51,6 +52,7 @@ describe('RelevanceTuning', () => {
it('renders', () => {
const wrapper = subject();
expect(wrapper.find(RelevanceTuningCallouts).exists()).toBe(true);
expect(wrapper.find(PrecisionSlider).exists()).toBe(true);
expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true);
expect(wrapper.find(RelevanceTuningPreview).exists()).toBe(true);
});

View file

@ -9,7 +9,7 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SAVE_BUTTON_LABEL } from '../../../shared/constants';
@ -19,6 +19,7 @@ import { getEngineBreadcrumbs } from '../engine';
import { AppSearchPageTemplate } from '../layout';
import { EmptyState } from './components';
import { PrecisionSlider } from './components/precision_slider';
import { RELEVANCE_TUNING_TITLE } from './constants';
import { RelevanceTuningCallouts } from './relevance_tuning_callouts';
import { RelevanceTuningForm } from './relevance_tuning_form';
@ -43,7 +44,7 @@ export const RelevanceTuning: React.FC = () => {
pageTitle: RELEVANCE_TUNING_TITLE,
description: i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description',
{ defaultMessage: 'Set field weights and boosts.' }
{ defaultMessage: 'Manage precision and relevance settings for your engine' }
),
rightSideItems: engineHasSchemaFields
? [
@ -74,6 +75,9 @@ export const RelevanceTuning: React.FC = () => {
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={3}>
<EuiSpacer size="m" />
<PrecisionSlider />
<EuiSpacer />
<RelevanceTuningForm />
</EuiFlexItem>
<EuiFlexItem grow={4}>

View file

@ -42,7 +42,6 @@ export const RelevanceTuningForm: React.FC = () => {
return (
<section className="relevanceTuningForm">
<form>
<EuiSpacer size="m" />
<EuiTitle size="m">
<h2>
{i18n.translate(

View file

@ -32,6 +32,7 @@ describe('RelevanceTuningLogic', () => {
],
},
search_fields: {},
precision: 10,
};
const schema = {};
const schemaConflicts = {};
@ -60,6 +61,7 @@ describe('RelevanceTuningLogic', () => {
searchSettings: {
boosts: {},
search_fields: {},
precision: 2,
},
unsavedChanges: false,
filterInputValue: '',
@ -225,6 +227,21 @@ describe('RelevanceTuningLogic', () => {
});
});
});
describe('updatePrecision', () => {
it('should set precision inside search settings', () => {
mount();
RelevanceTuningLogic.actions.updatePrecision(9);
expect(RelevanceTuningLogic.values).toEqual({
...DEFAULT_VALUES,
searchSettings: {
...DEFAULT_VALUES.searchSettings,
precision: 9,
},
});
});
});
});
describe('listeners', () => {

View file

@ -88,6 +88,7 @@ interface RelevanceTuningActions {
optionType: keyof Pick<Boost, 'operation' | 'function'>;
value: string;
};
updatePrecision(precision: number): { precision: number };
updateSearchValue(query: string): string;
}
@ -143,6 +144,7 @@ export const RelevanceTuningLogic = kea<
optionType,
value,
}),
updatePrecision: (precision) => ({ precision }),
updateSearchValue: (query) => query,
}),
reducers: () => ({
@ -150,11 +152,16 @@ export const RelevanceTuningLogic = kea<
{
search_fields: {},
boosts: {},
precision: 2,
},
{
onInitializeRelevanceTuning: (_, { searchSettings }) => searchSettings,
setSearchSettings: (_, { searchSettings }) => searchSettings,
setSearchSettingsResponse: (_, { searchSettings }) => searchSettings,
updatePrecision: (currentSearchSettings, { precision }) => ({
...currentSearchSettings,
precision,
}),
},
],
schema: [

View file

@ -69,5 +69,5 @@ export interface SearchSettings {
boosts: Record<string, Boost[]>;
search_fields: Record<string, SearchField>;
result_fields?: object;
precision?: number;
precision: number;
}

View file

@ -55,6 +55,7 @@ describe('removeBoostStateProps', () => {
weight: 1,
},
},
precision: 10,
};
expect(removeBoostStateProps(searchSettings)).toEqual({
...searchSettings,