1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

fzfで日本語を検索したかったので、CJK対応のfuzzy finder「Yuru」をRustで作った

1
Last updated at Posted at 2026-05-07

リンク

# とりあえず試したい人向け (詳細インストールはhttps://github.com/Ameyanagi/yuruへ)
curl -fsSL https://raw.githubusercontent.com/Ameyanagi/yuru/v0.1.8/install | sh -s -- --all --version v0.1.8

image.png

はじめに

ターミナルで日々作業している方なら、fzf はもう体の一部になっている人も多いと思います。私もfzfを使わない日はなく、おそらく10分に1回は叩いている気がします。

「候補一覧から1つをサッと選ぶ」というワークフローを担う、ほぼ唯一の道具です。

ただ、日本語のファイル名をfzfで検索しようとすると、毎回ちょっとした違和感がありました。

  • 日本人.txtnihonjin で引きたいのに引けない
  • 仕方なくIMEをONにして「にほんじん」と打ち直す
  • ターミナルを高速に使いたいのに、IMEの切り替えで一瞬止まる

この「IMEを起動するために手が止まる」問題を解消したくて、CJK(中日韓)テキストを音声的にfuzzy検索できるツール Yuru をRustで作りました。

名前は「ゆるい」から取っています。クエリは少しゆるくていい、それでも目的のCJKテキストにたどり着く、というイメージです。

# こういう検索が通ります
printf "カメラ.txt\n" | yuru --lang ja --filter kamera
printf "日本人.txt\n" | yuru --lang ja --filter nihonjin
printf "北京大学.txt\n" | yuru --lang zh --filter bjdx
printf "한글.txt\n"   | yuru --lang ko --filter hangeul

fzfにおけるCJKのギャップ

fzfは日本語・中国語・韓国語のUnicode文字をテキストとして扱ってくれます。表示文字列とクエリが同じ文字列であれば動きます。

問題は、CJK検索では「人間の感覚としては関連しているが、バイト列としては別物」というケースが頻繁にあることです。

たとえば日本語の「ア」音1つとっても、

  • 半角カタカナ
  • 全角カタカナ
  • ひらがな
  • ASCII a / A
  • 全角ASCII /

があり、漢字になればさらに、

  • 漢字: 日本人
  • かな読み: にほんじん
  • ローマ字読み: nihonjin

と複数の表記が「人間の中では同じもの」として結びついています。fzfではこれらは別の文字列です。nihonjin日本人.txt は引けません。

これは中国語・韓国語でも同じです。

  • 中国人ユーザーは漢字ではなくpinyinや頭文字で検索したいことが多い
  • 韓国人ユーザーはローマ字化、ハングル初声(초성)、2-setキーボード入力など複数の入力方法を切り替える

これらは頭の中では繋がっていますが、文字列マッチには見えません。

Yuruのコアアイデア: 1候補に複数の検索キー

fzfは基本的に「1行の入力 = 1つの検索可能な文字列」というモデルです。Yuruはここを変えています。

1つの可視候補に対して、複数の検索キーを持つ

たとえば 資料/東京駅.pdf というパスに対して、Yuruは内部的に以下のキーを生成します。

  • 元のテキスト
  • 幅正規化テキスト(半角/全角統一)
  • かな読み
  • ローマ字読み
  • 各言語固有のキー
  • ソーススパンマップ(生成キー → 元テキストの位置対応)

これにより tokyoeki東京駅 がマッチします。重要なのは、ヒットしたあともハイライトは元の 東京駅 の部分に正しく当たることです。CJKの塊を「不透明な1ヒット」として扱わず、可視テキスト上の正しい位置を強調表示できます。

日本語マッチング

--lang ja を指定すると、

  • 幅正規化(半角↔全角)
  • ひらがな/カタカナ正規化
  • ローマ字キー生成
  • LinderaのIPADIC辞書による漢字読みの推定

が走ります。

IMEを使う日本人なら馴染みのある、ローマ字の入力ゆれにも対応しています。

入力 マッチ
zyu じゅ
nn / xn
ltsu / xtsu
lyu / xyu
printf "重要事項\n" | yuru --lang ja --filter zyu

日本語のファイル名には日付が混じることが多いので、数字まわりも少し賢く扱います。

printf "2025年8月.pdf\n" | yuru --lang ja --filter 20258gatsu

ゴールは言語学的に完璧な読み推定ではありません。ターミナルでとにかく速く、人間が普段の指の動きで打ったクエリで引けること、これが目的です。

中国語マッチング

--lang zh ではpinyin系のキーが追加されます。

  • スペース区切りのfull pinyin
  • 連結pinyin
  • 頭文字(initials)
printf "北京大学.txt\nnotes.txt\n" | yuru --lang zh --filter beijing
printf "北京大学.txt\nnotes.txt\n" | yuru --lang zh --filter bjdx

多音字については zh.polyphone = "common" を指定すると、上限を設けたうえで一般的な別読みを追加します。全パターンの直積は意図的に作りません。候補キーが爆発するからです。

韓国語マッチング

--lang ko はハングル音節を分解し、

  • ローマ字化キー: 한글han geul / hangeul
  • 초성(チョソン、初声)キー: 한글ㅎㄱ
  • 2-setキーボード入力キー: 한글gksrmf

を生成します。

printf "한글.txt\nnotes.txt\n" | yuru --lang ko --filter hangeul
printf "한글.txt\nnotes.txt\n" | yuru --lang ko --filter ㅎㄱ
printf "한글.txt\nnotes.txt\n" | yuru --lang ko --filter gksrmf

現状のローマ字化は決定論的で、fuzzy finderの再現率とソーススパン強調表示に最適化しています。같이 → gachi신라 → silla のような発音同化はまだ未実装です。

自動言語判定

--lang auto はロケール、クエリ文字、候補のサンプルから言語バックエンドを1つ選びます。

printf "北京大学.txt\n" | LANG=zh_CN.UTF-8 yuru --lang auto --filter bjdx

--explainでマッチを説明する

CJKマッチングではしばしば「生成された検索キー」がスコアの根拠になります。なぜ勝ったのかが見えにくいので、--explain を用意しました。

printf "北京大学.txt\n" | yuru --lang zh --filter bjdx --explain

インデックス・スコアリングの挙動をデバッグするときに重宝します。

パフォーマンスの考え方

Yuruでは多言語処理という重い仕事は候補側のインデックス時に寄せています。クエリの変更時には既に構築済みのキーに対して検索するだけです。

無制限に膨らまないよう以下を上限で抑えています。

  • max_query_variants
  • max_search_keys_per_candidate
  • max_total_key_bytes_per_candidate

インタラクティブモードでは、stdinやデフォルトコマンドが候補を流し込んでいる最中にもUIが開きます。ソースワーカーがレコードを候補集合に流し込み、サーチワーカーが利用可能になったデータに対して再検索を走らせる構造です。fzfのように全入力を待ってから起動したい場合は --sync を指定してください。

ベンチマーク(macOS / Apple Silicon)の現状値:

  • 10万候補: 多くのケースで数msオーダー
  • 100万候補(プレーン検索): 数十msオーダー

漢字読みの生成が走る場面はインデックスが重くなりますが、ホットな検索パスは候補数とキー長に対してほぼ線形を保っています。

fzf互換

Yuruはfzfの完全クローンを目指してはいません。fzfの隣に置いて、fzfで身についた習慣を壊さないことを目標にしています。

実装済みの主なfzfオプション:

  • --query / --filter
  • --select-1 / --exit-0
  • --print-query / --read0 / --print0
  • --nth / --with-nth / --accept-nth
  • --scheme / --walker / --expect
  • --header / --header-lines
  • --preview / --multi

--bind は部分対応です。未対応のbindアクションをどう扱うかは互換モードで制御できます。

yuru --fzf-compat warn    # デフォルト: 警告
yuru --fzf-compat strict  # 失敗させる
yuru --fzf-compat ignore  # 黙ってスキップ

FZF_DEFAULT_OPTS はデフォルトでsafeモードで読み込みます。検索/スクリプティング系のオプションは反映され、UI重視やシェル実行系のものは明示的に広げない限り無視されます。

yuru --load-fzf-default-opts never
yuru --load-fzf-default-opts safe
yuru --load-fzf-default-opts all

シェル統合は CTRL-TCTRL-RALT-C**<TAB> をbash / zsh / fish / PowerShellに対して提供します。fzfの挙動を踏襲しているので、fzfから乗り換えても指の動きはそのままです。

ビルトインプレビュー

[preview] command = "auto" または --preview-auto でビルトインプレビューが有効になります。

  • テキストファイル: bat があれば使用、なければプレーン出力にフォールバック
  • 画像ファイル: ratatui-image でターミナル内に描画。ラスタ画像とSVGに対応
  • Ghostty: tmuxのpassthroughが有効ならtmux内でもKitty graphics protocolを利用

自動判定が外れる場合はプロトコルを強制できます。

export YURU_PREVIEW_IMAGE_PROTOCOL=kitty
export YURU_PREVIEW_IMAGE_PROTOCOL=sixel
export YURU_PREVIEW_IMAGE_PROTOCOL=iterm2
export YURU_PREVIEW_IMAGE_PROTOCOL=halfblocks

プレビュー処理はメインUIループの外で動きます。選択変更、プレビューコマンド、デコード済み画像、ターミナルエンコーディングがキャッシュやワーカー越しになっているので、クエリ入力やカーソル移動のレスポンスは落ちません。

インストール

ユーザー空間にインストールされ、sudo は不要です。

macOS / Linux

curl -fsSL https://raw.githubusercontent.com/Ameyanagi/yuru/v0.1.6/install \
  | sh -s -- --all --version v0.1.6

ガイド付きインストールのデフォルト値を事前指定する場合:

curl -fsSL https://raw.githubusercontent.com/Ameyanagi/yuru/v0.1.6/install \
  | sh -s -- --all --version v0.1.6 \
    --default-lang ja \
    --preview-command auto \
    --preview-image-protocol none \
    --path-backend auto \
    --bindings all

Windows PowerShell

$script = Invoke-RestMethod https://raw.githubusercontent.com/Ameyanagi/yuru/v0.1.6/install.ps1
Invoke-Expression "& { $script } -All -Version v0.1.6"

crates.io

cargo install yuru

ソースビルドは日本語読み推定にLinderaのIPADIC辞書を埋め込むため、Cコンパイラが必要です。リリースバイナリ経由なら不要です。

シェル統合だけ手動で入れる

eval "$(yuru --bash)"
source <(yuru --zsh)
yuru --fish | source
yuru --powershell | Invoke-Expression

設定

~/.config/yuru/config.toml を読み込みます(CLI引数が最優先)。

[defaults]
lang = "auto"          # plain | ja | ko | zh | auto
scheme = "path"        # default | path | history
case = "smart"         # smart | ignore | respect
limit = 200
load_fzf_defaults = "safe"
fzf_compat = "warn"

[preview]
command = "auto"        # auto | none | shell command
image_protocol = "none" # none | halfblocks | sixel | kitty | iterm2

[matching]
algo = "greedy"        # greedy | fzf-v1 | fzf-v2 | nucleo
max_query_variants = 8
max_search_keys_per_candidate = 8
max_total_key_bytes_per_candidate = 1024

[ja]
reading = "lindera"    # none | lindera

[ko]
romanization = true
initials = true
keyboard = true

[zh]
pinyin = true
initials = true
polyphone = "common"   # none | common

[shell]
bindings = "all"       # all | none | ctrl-t,ctrl-r,alt-c,completion
path_backend = "auto"  # auto | fd | fdfind | find

fzf-v1 fzf-v2 はfzfの正確な移植ではなく「互換志向の名前」です。fzf-v1 はYuruのgreedyスコアラ、fzf-v2nucleo はnucleoバックエンドのqualityスコアラを使います。

セットアップ確認には:

yuru doctor

試してみてください

ターミナルで日本語ファイル名を扱うことが多い方なら、「IMEを切り替えるためだけに指が止まる」という小さな摩擦をYuruは消してくれるはずです。

バグ報告・機能要望はIssuesでお待ちしています。CJK周りはまだ「ここはこう拾ってほしい」というケースが多くあると思うので、フィードバックは特にありがたいです。気に入ったらgithubにスターいただけると幸いです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?