mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
cfd836014d
commit
043a2e2fe8
12 changed files with 354 additions and 4 deletions
|
@ -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,
|
||||
];
|
|
@ -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';
|
|
@ -0,0 +1,3 @@
|
|||
.stepDescription {
|
||||
min-height: $euiSize * 4;
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -42,7 +42,6 @@ export const RelevanceTuningForm: React.FC = () => {
|
|||
return (
|
||||
<section className="relevanceTuningForm">
|
||||
<form>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -69,5 +69,5 @@ export interface SearchSettings {
|
|||
boosts: Record<string, Boost[]>;
|
||||
search_fields: Record<string, SearchField>;
|
||||
result_fields?: object;
|
||||
precision?: number;
|
||||
precision: number;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ describe('removeBoostStateProps', () => {
|
|||
weight: 1,
|
||||
},
|
||||
},
|
||||
precision: 10,
|
||||
};
|
||||
expect(removeBoostStateProps(searchSettings)).toEqual({
|
||||
...searchSettings,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue