mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
OptionsPicker: Use fuzzy search and improve ranking of matches (#79286)
This commit is contained in:
parent
19cda38f1b
commit
09cef892a5
@ -260,7 +260,7 @@
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"@kusto/monaco-kusto": "^7.4.0",
|
||||
"@leeoniya/ufuzzy": "1.0.8",
|
||||
"@leeoniya/ufuzzy": "1.0.13",
|
||||
"@lezer/common": "1.0.2",
|
||||
"@lezer/highlight": "1.1.3",
|
||||
"@lezer/lr": "1.3.3",
|
||||
|
@ -46,7 +46,7 @@
|
||||
"@emotion/css": "11.11.2",
|
||||
"@grafana/data": "10.3.0-pre",
|
||||
"@grafana/ui": "10.3.0-pre",
|
||||
"@leeoniya/ufuzzy": "1.0.8",
|
||||
"@leeoniya/ufuzzy": "1.0.13",
|
||||
"d3": "^7.8.5",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.2.0",
|
||||
|
@ -53,7 +53,7 @@
|
||||
"@grafana/e2e-selectors": "10.3.0-pre",
|
||||
"@grafana/faro-web-sdk": "1.2.1",
|
||||
"@grafana/schema": "10.3.0-pre",
|
||||
"@leeoniya/ufuzzy": "1.0.8",
|
||||
"@leeoniya/ufuzzy": "1.0.13",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@react-aria/button": "3.8.0",
|
||||
|
@ -210,9 +210,9 @@ function useInternalMatches(filtered: ActionImpl[], search: string): Match[] {
|
||||
} else {
|
||||
const termCount = ufuzzy.split(throttledSearch).length;
|
||||
const infoThresh = Infinity;
|
||||
const oooSearch = termCount < 5;
|
||||
const oooLimit = termCount < 5 ? 4 : 0;
|
||||
|
||||
const [, info, order] = ufuzzy.search(haystack, throttledSearch, oooSearch, infoThresh);
|
||||
const [, info, order] = ufuzzy.search(haystack, throttledSearch, oooLimit, infoThresh);
|
||||
|
||||
if (info && order) {
|
||||
for (let orderIndex = 0; orderIndex < order.length; orderIndex++) {
|
||||
|
@ -10,7 +10,7 @@ const uf = new uFuzzy({
|
||||
});
|
||||
|
||||
export function fuzzySearch(haystack: string[], query: string, dispatcher: (data: string[][]) => void) {
|
||||
const [idxs, info, order] = uf.search(haystack, query, false, 1e5);
|
||||
const [idxs, info, order] = uf.search(haystack, query, 0, 1e5);
|
||||
|
||||
let haystackOrder: string[] = [];
|
||||
let matchesSet: Set<string> = new Set();
|
||||
|
@ -116,7 +116,7 @@ class FullResultCache {
|
||||
// eslint-disable-next-line
|
||||
const values = allFields.map((v) => [] as any[]); // empty value for each field
|
||||
|
||||
let [idxs, info, order] = this.ufuzzy.search(haystack, query, true);
|
||||
let [idxs, info, order] = this.ufuzzy.search(haystack, query, 5);
|
||||
|
||||
for (let c = 0; c < allFields.length; c++) {
|
||||
let src = allFields[c].values;
|
||||
|
@ -774,6 +774,39 @@ describe('optionsPickerReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when similar data for updateOptionsAndFilter', () => {
|
||||
it('should properly rank by match quality', () => {
|
||||
const searchQuery = 'C';
|
||||
|
||||
const options: VariableOption[] = 'A AA AB AC BC C CD'.split(' ').map((v) => ({
|
||||
selected: false,
|
||||
text: v,
|
||||
value: v,
|
||||
}));
|
||||
|
||||
const expect: VariableOption[] = 'C CD AC BC'.split(' ').map((v) => ({
|
||||
selected: false,
|
||||
text: v,
|
||||
value: v,
|
||||
}));
|
||||
|
||||
const { initialState } = getVariableTestContext({
|
||||
queryValue: searchQuery,
|
||||
});
|
||||
|
||||
reducerTester<OptionsPickerState>()
|
||||
.givenReducer(optionsPickerReducer, cloneDeep(initialState))
|
||||
.whenActionIsDispatched(updateOptionsAndFilter(options))
|
||||
.thenStateShouldEqual({
|
||||
...cloneDeep(initialState),
|
||||
options: expect,
|
||||
selectedValues: [],
|
||||
queryValue: 'C',
|
||||
highlightIndex: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when large data for updateOptionsFromSearch is dispatched and variable has searchFilter', () => {
|
||||
it('then state should be correct', () => {
|
||||
const searchQuery = '__searchFilter';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { cloneDeep, isString, trimStart } from 'lodash';
|
||||
import { cloneDeep, isString } from 'lodash';
|
||||
|
||||
import { containsSearchFilter } from '@grafana/data';
|
||||
|
||||
@ -34,6 +35,14 @@ export const initialOptionPickerState: OptionsPickerState = {
|
||||
|
||||
export const OPTIONS_LIMIT = 1000;
|
||||
|
||||
const ufuzzy = new uFuzzy({
|
||||
intraMode: 1,
|
||||
intraIns: 1,
|
||||
intraSub: 1,
|
||||
intraTrn: 1,
|
||||
intraDel: 1,
|
||||
});
|
||||
|
||||
const optionsToRecord = (options: VariableOption[]): Record<string, VariableOption> => {
|
||||
if (!Array.isArray(options)) {
|
||||
return {};
|
||||
@ -237,14 +246,32 @@ const optionsPickerSlice = createSlice({
|
||||
return state;
|
||||
},
|
||||
updateOptionsAndFilter: (state, action: PayloadAction<VariableOption[]>): OptionsPickerState => {
|
||||
const searchQuery = trimStart((state.queryValue ?? '').toLowerCase());
|
||||
const needle = state.queryValue.trim();
|
||||
|
||||
state.options = action.payload.filter((option) => {
|
||||
const optionsText = option.text ?? '';
|
||||
const text = Array.isArray(optionsText) ? optionsText.toString() : optionsText;
|
||||
return text.toLowerCase().indexOf(searchQuery) !== -1;
|
||||
});
|
||||
let opts: VariableOption[] = [];
|
||||
|
||||
if (needle === '') {
|
||||
opts = action.payload;
|
||||
} else {
|
||||
// with current API, not seeing a way to cache this on state using action.payload's uniqueness
|
||||
// since it's recreated and includes selected state on each item :(
|
||||
const haystack = action.payload.map(({ text }) => (Array.isArray(text) ? text.toString() : text));
|
||||
|
||||
const [idxs, info, order] = ufuzzy.search(haystack, needle, 5);
|
||||
|
||||
if (idxs?.length) {
|
||||
if (info && order) {
|
||||
opts = order.map((idx) => action.payload[info.idx[idx]]);
|
||||
} else {
|
||||
opts = idxs!.map((idx) => action.payload[idx]);
|
||||
}
|
||||
|
||||
// always sort $__all to the top, even if exact match exists?
|
||||
opts.sort((a, b) => (a.value === '$__all' ? -1 : 0) - (b.value === '$__all' ? -1 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
state.options = opts;
|
||||
state.highlightIndex = 0;
|
||||
|
||||
return applyStateChanges(state, updateDefaultSelection, updateOptions);
|
||||
|
@ -10,7 +10,7 @@ const uf = new uFuzzy({
|
||||
});
|
||||
|
||||
export function fuzzySearch(haystack: string[], query: string, dispatcher: (data: string[][]) => void) {
|
||||
const [idxs, info, order] = uf.search(haystack, query, false, 1e5);
|
||||
const [idxs, info, order] = uf.search(haystack, query, 0, 1e5);
|
||||
|
||||
let haystackOrder: string[] = [];
|
||||
let matchesSet: Set<string> = new Set();
|
||||
|
14
yarn.lock
14
yarn.lock
@ -3169,7 +3169,7 @@ __metadata:
|
||||
"@grafana/data": "npm:10.3.0-pre"
|
||||
"@grafana/tsconfig": "npm:^1.2.0-rc1"
|
||||
"@grafana/ui": "npm:10.3.0-pre"
|
||||
"@leeoniya/ufuzzy": "npm:1.0.8"
|
||||
"@leeoniya/ufuzzy": "npm:1.0.13"
|
||||
"@rollup/plugin-node-resolve": "npm:15.2.3"
|
||||
"@testing-library/jest-dom": "npm:^6.1.2"
|
||||
"@testing-library/react": "npm:14.0.0"
|
||||
@ -3366,7 +3366,7 @@ __metadata:
|
||||
"@grafana/faro-web-sdk": "npm:1.2.1"
|
||||
"@grafana/schema": "npm:10.3.0-pre"
|
||||
"@grafana/tsconfig": "npm:^1.2.0-rc1"
|
||||
"@leeoniya/ufuzzy": "npm:1.0.8"
|
||||
"@leeoniya/ufuzzy": "npm:1.0.13"
|
||||
"@monaco-editor/react": "npm:4.6.0"
|
||||
"@popperjs/core": "npm:2.11.8"
|
||||
"@react-aria/button": "npm:3.8.0"
|
||||
@ -4177,10 +4177,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@leeoniya/ufuzzy@npm:1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "@leeoniya/ufuzzy@npm:1.0.8"
|
||||
checksum: b5068593022e322e395e7cb2c134b4b040375165d0953d3f40f8dcd4bb84fe56c39c08ee0dede056b4b8ed1060d39f5bf6c821dab4ae271e8e5befc81c33d7c6
|
||||
"@leeoniya/ufuzzy@npm:1.0.13":
|
||||
version: 1.0.13
|
||||
resolution: "@leeoniya/ufuzzy@npm:1.0.13"
|
||||
checksum: 2222357f136764674e84d1b85ae2b4436840a270f75598c902e528ccfbc3d91b638f9e3b6d16e115e98eaffa4bcd9c71db909b126cf4ac3102fa14339d16cf4b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -17337,7 +17337,7 @@ __metadata:
|
||||
"@grafana/tsconfig": "npm:^1.3.0-rc1"
|
||||
"@grafana/ui": "workspace:*"
|
||||
"@kusto/monaco-kusto": "npm:^7.4.0"
|
||||
"@leeoniya/ufuzzy": "npm:1.0.8"
|
||||
"@leeoniya/ufuzzy": "npm:1.0.13"
|
||||
"@lezer/common": "npm:1.0.2"
|
||||
"@lezer/highlight": "npm:1.1.3"
|
||||
"@lezer/lr": "npm:1.3.3"
|
||||
|
Loading…
Reference in New Issue
Block a user