diff options
| author | Terry Truong <terry06890@gmail.com> | 2022-08-31 12:18:39 +1000 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2022-08-31 12:45:03 +1000 |
| commit | 8f4b899effcca7316a760b16773ebdc781215591 (patch) | |
| tree | 6417e3424c3cdcccfa79224e30a05063fb7d4fcd | |
| parent | 0cd58b3c1a8c5297579ea7a24a14d82ae8fed169 (diff) | |
Improve search suggestions
Don't show multiple suggestions for the same node
Prioritise common-names
Place suggestions from prefix-search before substring-search
Add coloring to search-string matched-part and canonical-name-part
| -rwxr-xr-x | backend/tilo.py | 82 | ||||
| -rw-r--r-- | src/components/SearchModal.vue | 15 | ||||
| -rw-r--r-- | src/lib.ts | 2 |
3 files changed, 53 insertions, 46 deletions
diff --git a/backend/tilo.py b/backend/tilo.py index f08da80..00b3fc9 100755 --- a/backend/tilo.py +++ b/backend/tilo.py @@ -48,9 +48,9 @@ class TolNode: class SearchSugg: " Represents a search suggestion " def __init__(self, name, canonicalName=None, pop=0): - self.name = name # string - self.canonicalName = canonicalName # string | null - self.pop = pop # number + self.name = name # string + self.canonicalName = canonicalName # string | null + self.pop = pop if pop != None else 0 # number class SearchSuggResponse: " Sent as responses to 'sugg' requests " def __init__(self, searchSuggs, hasMore): @@ -146,48 +146,54 @@ def lookupSuggs(searchStr, suggLimit, tree, dbCur): results = [] hasMore = False # Get node names and alt-names, ordering by popularity - query1, query2 = (None, None) nodesTable = f"nodes_{getTableSuffix(tree)}" - query1 = f"SELECT DISTINCT {nodesTable}.name, node_pop.pop FROM {nodesTable}" \ + nameQuery = f"SELECT {nodesTable}.name, node_pop.pop FROM {nodesTable}" \ f" LEFT JOIN node_pop ON {nodesTable}.name = node_pop.name" \ f" WHERE node_pop.name LIKE ? AND node_pop.name NOT LIKE '[%'" \ - f" ORDER BY node_pop.pop DESC LIMIT ?" - query2 = f"SELECT DISTINCT alt_name, names.name, node_pop.pop FROM" \ + f" ORDER BY node_pop.pop DESC" + altNameQuery = f"SELECT alt_name, names.name, pref_alt, node_pop.pop FROM" \ f" names INNER JOIN {nodesTable} ON names.name = {nodesTable}.name" \ f" LEFT JOIN node_pop ON {nodesTable}.name = node_pop.name" \ - f" WHERE alt_name LIKE ? ORDER BY node_pop.pop DESC LIMIT ?" - suggs = [] - for nodeName, pop in dbCur.execute(query1, (searchStr + "%", suggLimit + 1)): - suggs.append(SearchSugg(nodeName, pop=pop)) - for altName, nodeName, pop in dbCur.execute(query2, (searchStr + "%", suggLimit + 1)): - suggs.append(SearchSugg(altName, nodeName, pop)) + f" WHERE alt_name LIKE ? ORDER BY node_pop.pop DESC" + suggs = {} + tempLimit = suggLimit + 1 # For determining if 'more suggestions exist' + # Prefix search + for altName, nodeName, prefAlt, pop in dbCur.execute(altNameQuery, (searchStr + "%",)): + if nodeName not in suggs or prefAlt == 1 and suggs[nodeName].canonicalName != None: + suggs[nodeName] = SearchSugg(altName, nodeName, pop) + if len(suggs) == tempLimit: + break + if len(suggs) < tempLimit: + # Prefix search of canonical names + for nodeName, pop in dbCur.execute(nameQuery, (searchStr + "%",)): + if nodeName not in suggs: + suggs[nodeName] = SearchSugg(nodeName, pop=pop) + if len(suggs) == tempLimit: + break + suggList = sorted(suggs.values(), key=lambda x: x.pop, reverse=True) # If insufficient results, try substring-search - foundNames = {n.name for n in suggs} - suggs2 = [] - if len(suggs) < suggLimit: - newLim = suggLimit + 1 - len(suggs) - for nodeName, pop in dbCur.execute(query1, ("%" + searchStr + "%", newLim)): - if nodeName not in foundNames: - suggs2.append(SearchSugg(nodeName, pop=pop)) - foundNames.add(nodeName) - if len(suggs) + len(suggs2) < suggLimit: - newLim = suggLimit + 1 - len(suggs) - len(suggs2) - for altName, nodeName, pop in dbCur.execute(query2, ("%" + searchStr + "%", suggLimit + 1)): - if altName not in foundNames: - suggs2.append(SearchSugg(altName, nodeName, pop)) - foundNames.add(altName) - # Sort results - suggs.sort(key=lambda x: x.name) - suggs.sort(key=lambda x: x.pop, reverse=True) - suggs2.sort(key=lambda x: x.name) - suggs2.sort(key=lambda x: x.pop, reverse=True) - suggs.extend(suggs2) - # Apply suggestion-quantity limit - results = suggs[:suggLimit] - if len(suggs) > suggLimit: - hasMore = True + if len(suggs) < tempLimit: + newNames = set() + oldNames = suggs.keys() + for altName, nodeName, prefAlt, pop in dbCur.execute(altNameQuery, ("%" + searchStr + "%",)): + if nodeName not in suggs or \ + nodeName not in oldNames and prefAlt == 1 and suggs[nodeName].canonicalName != None: + suggs[nodeName] = SearchSugg(altName, nodeName, pop) + newNames.add(nodeName) + if len(suggs) == tempLimit: + break + if len(suggs) < tempLimit: + for nodeName, pop in dbCur.execute(nameQuery, ("%" + searchStr + "%",)): + if nodeName not in suggs: + suggs[nodeName] = SearchSugg(nodeName, pop=pop) + newNames.add(nodeName) + if len(suggs) == tempLimit: + break + suggList.extend(sorted([suggs[n] for n in newNames], key=lambda x: x.pop, reverse=True)) # - return SearchSuggResponse(results, hasMore) + if len(suggList) > suggLimit: + hasMore = True + return SearchSuggResponse(suggList[:suggLimit], hasMore) def lookupInfo(name, tree, dbCur): " For a node name, returns an InfoResponse, or None " # Get node info diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue index 5d6345e..01db1ab 100644 --- a/src/components/SearchModal.vue +++ b/src/components/SearchModal.vue @@ -15,8 +15,9 @@ @click="resolveSearch(sugg.canonicalName || sugg.name)"> <div class="grow overflow-hidden whitespace-nowrap text-ellipsis"> <span>{{suggDisplayStrings[idx][0]}}</span> - <span class="font-bold">{{suggDisplayStrings[idx][1]}}</span> + <span class="font-bold text-lime-600">{{suggDisplayStrings[idx][1]}}</span> <span>{{suggDisplayStrings[idx][2]}}</span> + <span class="text-stone-500">{{suggDisplayStrings[idx][3]}}</span> </div> <info-icon class="hover:cursor-pointer my-auto w-5 h-5" @click.stop="onInfoIconClick(sugg.canonicalName || sugg.name)"/> @@ -90,22 +91,22 @@ export default defineComponent({ color: this.uiOpts.textColor, }; }, - suggDisplayStrings(): [string, string, string][] { - let result: [string, string, string][] = []; + suggDisplayStrings(): [string, string, string, string][] { + let result: [string, string, string, string][] = []; let input = this.suggsInput.toLowerCase(); // For each SearchSugg for (let sugg of this.searchSuggs){ let idx = sugg.name.indexOf(input); // Split suggestion text into parts before/within/after an input match - let strings: [string, string, string]; + let strings: [string, string, string, string]; if (idx != -1){ - strings = [sugg.name.substring(0, idx), input, sugg.name.substring(idx + input.length)]; + strings = [sugg.name.substring(0, idx), input, sugg.name.substring(idx + input.length), '']; } else { - strings = [input, '', '']; + strings = [input, '', '', '']; } // Indicate any distinct canonical-name if (sugg.canonicalName != null){ - strings[2] += ` (aka ${sugg.canonicalName})`; + strings[3] = ` (aka ${sugg.canonicalName})`; } // result.push(strings); @@ -140,7 +140,7 @@ export function getDefaultUiOpts(lytOpts: LayoutOptions): UiOptions { let bgColor = '#292524', bgColorLight = '#44403c', bgColorDark = '#1c1917', bgColorLight2 = '#57534e', bgColorDark2 = '#0e0c0b', - bgColorAlt = '#f5f5f4', bgColorAltDark = '#a8a29e'; + bgColorAlt = '#f5f5f4', bgColorAltDark = '#d6d3d1'; let altColor = '#a3e623', altColorDark = '#65a30d'; let accentColor = '#f59e0b'; let scrollGap = getScrollBarWidth(); |
