Skip to content

Commit

Permalink
perf: optimized search
Browse files Browse the repository at this point in the history
  • Loading branch information
christoph-heinrich committed Sep 26, 2023
1 parent 9b4f6e5 commit af01ec0
Showing 1 changed file with 60 additions and 28 deletions.
88 changes: 60 additions & 28 deletions scripts/uosc/elements/Menu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -653,35 +653,34 @@ 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
return matrix[rows][cols]
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)
Expand All @@ -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 }
Expand All @@ -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

Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit af01ec0

Please sign in to comment.