## 背景
[[🦉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]]
-