## 背景 [[🦉Another Quick Switcher]]のファイル名に関する検索は一部不具合があるため。それを回収したい。 ## 不具合の事象 以下の[[Sort priorities]]を持つコマンドがあるとする。 ```txt Prefix name match Name match Length ``` 入力途中は違和感がないが... ![[Pasted image 20220904212141.png|frame]] `obsidian plugin`まで入力しきると、本来は一番上に出てほしいはずの`Obsidianプラグイン`が3番目になってしまう。 ![[Pasted image 20220904212159.png|frame]] [[Perfect word match]]を指定していないにも関わらず、`obsidian`と`plugin`の2つが完全単語一致してしまった上位2つの項目が優先されてしまった。(`Obsidianプラグイン`はエイリアスが`Obsidian plugins`であり`s`が余計) ## 原因 [[🦉Another Quick Switcher]]は`matcher.ts`で1つの`SuggestionItem`に対し、複数の`MatchResult`を返す。`MatchResult`には`MatchType`が含まれる。 ```ts type MatchType = | "not found" | "name" | "prefix-name" | "word-perfect" | "directory" | "header" | "link" | "tag"; ``` だが`MatchResult`は**スペースで分割された1つのクエリに対して1つ**である。つまり、==1つのクエリが複数のMatchResultを持つことはできない==。 また、`MatchResult`には実装上、暗黙の優先度がある。 > [!code]- > > ```ts > function matchQuery( > item: SuggestionItem, > query: string, > searchByTags: boolean, > searchByHeaders: boolean, > searchByLinks: boolean, > isNormalizeAccentsDiacritics: boolean > ): MatchQueryResult { > // tag > if (searchByTags && query.startsWith("#")) { > const tags = item.tags.filter((tag) => > smartIncludes(tag.slice(1), query.slice(1), isNormalizeAccentsDiacritics) > ); > return { > type: tags.length > 0 ? "tag" : "not found", > meta: tags, > }; > } > > const qs = query.split("/"); > const file = qs.pop()!; > const includeDir = qs.every((dir) => > smartIncludes(item.file.parent.path, dir, isNormalizeAccentsDiacritics) > ); > if (!includeDir) { > return { type: "not found" }; > } > > if ( > item.tokens.some((t) => smartEquals(t, file, isNormalizeAccentsDiacritics)) > ) { > return { type: "word-perfect", meta: [item.file.name] }; > } > > if (smartStartsWith(item.file.name, file, isNormalizeAccentsDiacritics)) { > return { type: "prefix-name", meta: [item.file.name] }; > } > const prefixNameMatchedAliases = item.aliases.filter((x) => > smartStartsWith(x, file, isNormalizeAccentsDiacritics) > ); > if (prefixNameMatchedAliases.length > 0) { > return { > type: "prefix-name", > meta: prefixNameMatchedAliases, > alias: minBy(prefixNameMatchedAliases, (x) => x.length), > }; > } > > if (smartIncludes(item.file.name, file, isNormalizeAccentsDiacritics)) { > return { type: "name", meta: [item.file.name] }; > } > const nameMatchedAliases = item.aliases.filter((x) => > smartIncludes(x, file, isNormalizeAccentsDiacritics) > ); > if (nameMatchedAliases.length > 0) { > return { > type: "name", > meta: nameMatchedAliases, > alias: minBy(nameMatchedAliases, (x) => x.length), > }; > } > > if (smartIncludes(item.file.path, file, isNormalizeAccentsDiacritics)) { > return { type: "directory", meta: [item.file.path] }; > } > > if (searchByHeaders) { > const headers = item.headers.filter((header) => > smartIncludes(header, query, isNormalizeAccentsDiacritics) > ); > if (headers.length > 0) { > return { > type: "header", > meta: headers, > }; > } > } > > if (searchByLinks) { > const links = item.links.filter((link) => > smartIncludes(link, query, isNormalizeAccentsDiacritics) > ); > if (links.length > 0) { > return { > type: "link", > meta: links, > }; > } > } > > if (searchByTags) { > const tags = item.tags.filter((tag) => > smartIncludes(tag.slice(1), query, isNormalizeAccentsDiacritics) > ); > if (tags.length > 0) { > return { > type: "tag", > meta: tags, > }; > } > } > > return { type: "not found" }; > } > ``` たとえば、`word-perfect`は`prefix-name`よりも優先されるため、クエリとファイルに含まれる単語が完全一致する場合は、`word-perfect`のみが返却される。 ところが今回の[[Sort priorities]]には[[Perfect word match]]は含まれない。そのため、通常であれば、`word-prefect`の有無は考慮されない。しかし、そのままでは`prefix-name`が含まれないため[[Prefix name match]]はヒットしない。 そのような矛盾を避けるため、今の実装では==`word-perfect`が含まれれば[[Prefix name match]]としてもヒットするようにしている==。 ```ts function priorityToPrefixName( a: SuggestionItem, b: SuggestionItem ): 0 | -1 | 1 { return compare( a, b, (x) => x.matchResults.filter((x) => ["prefix-name", "word-perfect"].includes(x.type) ).length, "desc" ); } ``` 先の結果をもう一度見てみよう。 ![[Pasted image 20220904212159.png|frame]] 上の2つは`obsidian`と`plugin`の2つが`word-perfect`になっている。そのためスコアは2だ。他は1が関の山なのでスコアリングが逆転する。 ## 対策 > [[🦉Another Quick Switcher]]は`matcher.ts`で1つの`SuggestionItem`に対し、複数の`MatchResult`を返す。`MatchResult`には`MatchType`が含まれる。 > だが`MatchResult`は**スペースで分割された1つのクエリに対して1つ**である。つまり、==1つのクエリが複数のMatchResultを持つことはできない==。 1つの`MatchResult`に対して複数の`MatchType`を含められるようにすればよい。それによって以下の歪な設計も回避できる。 > そのような矛盾を避けるため、今の実装では==`word-perfect`が含まれれば[[Prefix name match]]としてもヒットするようにしている==。 ### パフォーマンスの懸念 実装難易度は大したことはない。 1. `sorters.ts`は`includes`を`==`にして単一パラメータのみを見るようにする 2. `MatchQueryResult.type`を`MatchType`から`MatchType[]`に変更する 3. `matcher.ts#matchQuery`は`MatchType`ではなく`MatchType[]`を返すようにする ただ、これによって3のパフォーマンスが悪化する。 - 今までアーリーリターンしていた箇所が最後まで判定される - 毎回if文が10回走る - 今まではヒットすれば10回未満で済んだ - `sorter`の判定処理をMatchTypeの数だけ回す必要がある 1つ目は厳しいが、2つ目はデータ形式を変更すれば解消できる。具体的には`MatchType[]`ではなく、`{[key: MatchType]: boolean}`にする。(この型表現はイメージ) そうすると、`sorter`での判定ロジックが ``` ["aa","bb","cc"].includes("bb") ``` ではなく ``` {aa: true, bb: true, cc: true}["bb"] ``` となるので、hashアクセスのみに抑えられ、ほぼ劣化しないと思われる。 と思ったが。。==`type`だけでなく`meta`や`alias`にも影響があることを失念していた。。まずいかも。。== ## 対応前の速度 パフォーマンス劣化のリスクを減らすため、事前に測定しておく。 大体25~50msくらい。 ``` Get suggestions: (Recommended search): 3[ms] Get suggestions: o (Recommended search): 34[ms] Get suggestions: ob (Recommended search): 34[ms] Get suggestions: obs (Recommended search): 24[ms] Get suggestions: obsi (Recommended search): 23[ms] Get suggestions: obsid (Recommended search): 23[ms] Get suggestions: obsidi (Recommended search): 25[ms] Get suggestions: obsidia (Recommended search): 35[ms] Get suggestions: obsidian (Recommended search): 25[ms] Get suggestions: obsidian (Recommended search): 24[ms] Get suggestions: obsidian p (Recommended search): 25[ms] Get suggestions: obsidian pl (Recommended search): 46[ms] Get suggestions: obsidian plu (Recommended search): 35[ms] Get suggestions: obsidian plug (Recommended search): 36[ms] Get suggestions: obsidian plugi (Recommended search): 30[ms] Get suggestions: obsidian plugin (Recommended search): 27[ms] Get suggestions: obsidian plugins (Recommended search): 36[ms] ``` [[Search by]] ``` Tag Header Link ``` [[Sort priorities]] ```txt Perfect word match Prefix name match Name match Length Tag match Header match Link match Star Last opened Last modified ``` [[Exclude prefix path patterns]] ```txt _Privates/Daily Notes/ 📰Weekly Report/ ``` ## 最終的に 方向性をいくつか変えて対応した。その結果以下の課題が - [[内部リンク]]やheadersがうるさくなった [[Custom searches]]のデフォルト設定を変更してみる。 ## 新しいデフォルト設定 ### Recommended search - [[Search by]] - Tag - [[Sort priorities]] - Name match - Star - Last opened - Last modified ### Landmark search - [[Search by]] - Tag - Link - Header - [[Sort priorities]] -