From af01ec00c88449f28ec7858d84ab109f200c73a7 Mon Sep 17 00:00:00 2001 From: christoph-heinrich Date: Mon, 25 Sep 2023 08:21:05 +0200 Subject: [PATCH] perf: optimized search --- scripts/uosc/elements/Menu.lua | 88 +++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/scripts/uosc/elements/Menu.lua b/scripts/uosc/elements/Menu.lua index 012502be..c9259aa7 100644 --- a/scripts/uosc/elements/Menu.lua +++ b/scripts/uosc/elements/Menu.lua @@ -653,21 +653,19 @@ end ---@param matrix integer[][] ---@return integer local function levenshtein_distance(text_chars, from, to, pattern_chars, pattern_char_count, matrix) - local cols = to - from + 2 - local rows = pattern_char_count + 1 - if cols == 1 then return pattern_char_count end + local cols = pattern_char_count + 1 + local rows = to - from + 2 + if cols == 1 then return rows - 1 end if rows == 1 then return cols - 1 end - for i = 1, rows do if not matrix[i] then matrix[i] = { i - 1 } else matrix[i][1] = i - 1 end end - for j = 1, cols do matrix[1][j] = j - 1 end - for i = 2, rows do - for j = 2, cols do - if pattern_chars[i - 1] == text_chars[from + j - 2] then - matrix[i][j] = matrix[i - 1][j - 1] + for c = 2, cols do + for r = 2, rows do + if pattern_chars[r - 1] == text_chars[from + c - 2] then + matrix[r][c] = matrix[r - 1][c - 1] else - matrix[i][j] = 1 + math.min( - matrix[i - 1][j - 1], -- substitution - matrix[i][j - 1], -- insertion - matrix[i - 1][j]) -- deletion + matrix[r][c] = 1 + math.min( + matrix[r - 1][c - 1], -- substitution + matrix[r][c - 1], -- insertion + matrix[r - 1][c]) -- deletion end end end @@ -675,13 +673,14 @@ local function levenshtein_distance(text_chars, from, to, pattern_chars, pattern end ---Calculates the lowest levenshtein distance of any substring of length pattern_char_count in text. ----@param text string +---@param text_chars string[] +---@param text_char_count integer ---@param pattern_chars string[] ---@param pattern_char_count integer ---@param matrix integer[][] ---@return integer -local function substring_levenshtein_distance(text, pattern_chars, pattern_char_count, matrix) - local best_score, text_chars, text_char_count = pattern_char_count, utf8_chars(text) +local function substring_levenshtein_distance(text_chars, text_char_count, pattern_chars, pattern_char_count, matrix) + local best_score = pattern_char_count for i = -pattern_char_count + 2, text_char_count do local from, to = math.max(1, i), math.min(i + pattern_char_count - 1, text_char_count) local score = levenshtein_distance(text_chars, from, to, pattern_chars, pattern_char_count, matrix) @@ -694,18 +693,19 @@ end ---Sort and filter items based on the hamming distance ----@param items MenuStackItem[] +---@param source {scroll_y: number; selected_index?: integer; items?: MenuDataItem[], prepared: { matrix: integer [][]; text: { tc: string[]; tcn: integer; hc: string[]; hcn: integer; twc: string[]; twcn: integer; hwc: string[]; hwcn: integer;}[] }} ---@param query string ---@return MenuStackItem[] -local function fuzzy_search(items, query) - local t, n, matrix, query_chars, query_char_count = {}, 1, {}, utf8_chars(query) - for _, item in ipairs(items) do - local title = item.title and item.title:lower() - local hint = item.hint and item.hint:lower() - local td = title and substring_levenshtein_distance(title, query_chars, query_char_count, matrix) or INFINITY - local hd = hint and substring_levenshtein_distance(hint, query_chars, query_char_count, matrix) or INFINITY - local tcd = title and substring_levenshtein_distance(table.concat(first_word_chars(title)), query_chars, query_char_count, matrix) or INFINITY - local hcd = hint and substring_levenshtein_distance(table.concat(first_word_chars(hint)), query_chars, query_char_count, matrix) or INFINITY +local function fuzzy_search(source, query) + local t, n, query_chars, query_char_count = {}, 1, utf8_chars(query) + local matrix, text = source.prepared.matrix, source.prepared.text + for i = 1, query_char_count + 1 do matrix[1][i] = i - 1 end + for i, item in ipairs(source.items) do + local p = text[i] + local td = p.tc and substring_levenshtein_distance(p.tc, p.tcn, query_chars, query_char_count, matrix) or INFINITY + local hd = p.hc and substring_levenshtein_distance(p.hc, p.hcn, query_chars, query_char_count, matrix) or INFINITY + local tcd = p.twc and substring_levenshtein_distance(p.twc, p.twcn, query_chars, query_char_count, matrix) or INFINITY + local hcd = p.hwc and substring_levenshtein_distance(p.hwc, p.hwcn, query_chars, query_char_count, matrix) or INFINITY local distance = math.min(td, hd, tcd, hcd) if distance ~= INFINITY then t[n] = { distance, n, item } @@ -717,10 +717,41 @@ local function fuzzy_search(items, query) return t end +---@param items MenuStackItem[] +---@return { matrix: integer [][]; text: { tc: string[]; tcn: integer; hc: string[]; hcn: integer; twc: string[]; twcn: integer; hwc: string[]; hwcn: integer;}[] } +function search_prepare_search(items) + local t, n_max = {}, 0 + for i, item in ipairs(items) do + local title = item.title and item.title:lower() + local hint = item.hint and item.hint:lower() + local tc, tcn, hc, hcn, twc, twcn, hwc, hwcn = nil, nil, nil, nil, nil, nil, nil, nil + if title then + local title_initials = table.concat(first_word_chars(title)) + tc, tcn = utf8_chars(title) + twc, twcn = utf8_chars(title_initials) + end + if hint then + local hint_initials = table.concat(first_word_chars(hint)) + hc, hcn = utf8_chars(hint) + hwc, hwcn = utf8_chars(hint_initials) + end + local n = math.max(tcn or 0, hcn or 0) + if n > n_max then + n_max = n + end + t[i] = { + tc = tc, tcn = tcn, hc = hc, hcn = hcn, twc = twc, twcn = twcn, hwc = hwc, hwcn = hwcn, + } + end + local matrix = {} + for i = 1, n_max + 1 do matrix[i] = { i - 1 } end + return { text = t, matrix = matrix } +end + ---@param menu MenuStack function Menu:search_internal(menu) local query = menu.search.query:lower() - menu.items = query ~= '' and fuzzy_search(menu.search.source.items, query) or menu.search.source.items + menu.items = query ~= '' and fuzzy_search(menu.search.source, query) or menu.search.source.items self:search_update_items() end @@ -819,7 +850,8 @@ function Menu:search_start() query = '', timeout = timeout, width = menu.width, top = menu.top, source = { scroll_y = menu.scroll_y, selected_index = menu.selected_index, - items = not menu.on_search and menu.items or nil + items = not menu.on_search and menu.items or nil, + prepared = not menu.on_search and search_prepare_search(menu.items) or nil, }, } self:search_ensure_key_bindings()