エムスリーエンジニアリンググループ AI・機械学習チームの中村(@po3rin) です。 好きな言語はGo。仕事では主に検索周りを担当しています。今回はGoでfuzzy-findingツールを作ったのでその方法と、Chromeブックマークのような無限ネストJSONを Visitor Pattern でスライスにパースする方法を紹介します。
fuzzy-findingとは
いわゆる あいまい検索 で、文字列の部分一致でインタラクティブに絞り込みながら検索することです。有名どこだと fzf などが有名です。ファイル名などをガッとあいまい検索できます。
bmfzf
私は気になる記事はGoogle Chromeのブックマークで管理しています。スマホのChromeアプリとも同期できるので個人的には重宝しています。そこで趣味でGoogle Chromeのブックマークをfuzzy-findingできるツール bmfzf を作りました。
作ったツールはこちら。Google Chromeのブックマークをfuzzy-findingするツールです。
下記のようにChromeのブックマークをfuzzy-findingできます。ディレクトリ構造も含めてfuzzy-findingできます。
bmfzfは下記のようにopenコマンドで選択したブックマークをChromeですぐに開けるようにしておくと便利です。
# MacOS example $ open -a '/Applications/Google Chrome.app' $(bmfzf)
openまでbmfzfが行わないのはUNIX哲学の 「1つのプログラムには1つのことをうまくやらせる」 を守るためです。こうすることで他のツールと合わせて使いやすくなります。「UNIXという考え方」という本は僕がツールを作る際の指針となっている本なのでまだ読んだことがない人は是非読んでみてください。
- 作者:Mike Gancarz
- 発売日: 2001/02/01
- メディア: 単行本
なぜAlfredじゃないのか
AlfredはMac全体に対してfuzzy searchできますが、ブックマーク階層を無視して、ブックマーク名だけで検索がかかります。私のようにブックマークをフォルダで階層的に管理している場合はfolderも含めてfuzzy-findingができると便利です。
fuzzy-finding
Goでfuzzy-findingツールを作るにはktr0731/go-fuzzyfinder
パッケージを使うのがオススメです。下記のようにfuzzy-findingしたい対象をスライスとして用意すればすぐに実装ができます。
package main import ( // ... "github.com/ktr0731/go-fuzzyfinder" ) type Bookmark struct { Name string } var bs = []Bookmark{ {"Twitter"}, {"Netflix"}, {"AWS"}, } func main() { id, _ := fuzzyfinder.Find( bs, func(i int) string { return bs[i].Name }, fuzzyfinder.WithPreviewWindow( func(i, w, h int) string { if i == -1 { return "" } return fmt.Sprintf("Name: %s", bs[i].Name) }, ), ) fmt.Printf("selected: %v\n", bs[id]) }
fuzzyfinder.WithPreviewWindow
はOptionであり、fuzzy-finding中にwindowを表示できます。ここで重要なのはfuzzyfinder.Find
メソッドはスライスで受ける必要があることです。ゆえにfuzzy-finding対象がネスト構造だとそれを平らにする必要があります。
複数type無限ネストJSONのパース
fuzzy-findingは紹介したktr0731/go-fuzzyfinder
で簡単に実装できるので、後はChromeブックマークを構造体のスライスに詰めるだけです。ブックマークはJSONファイルでローカルに保存されているので、このファイルを読み込めば良さそうです。調べたところ、OSによってデフォルトのファイル位置が異なるのでこのように設定しています。
func bookmarkFileLocation() (string, error) { os := runtime.GOOS switch os { case "windows": return `AppData\Local\Google\Chrome\User Data\Default\bookmarks`, nil case "darwin": return `Library/Application Support/Google/Chrome/Default/Bookmarks`, nil case "linux": return `.config/google-chrome/Default`, nil default: return "", fmt.Errorf("sorry... your OS %v is not supported. please specify your bookmark file using -f flag.", os) } }
さて、JSONファイルを見てみるとちょっと面倒な無限ネストです。
{ "checksum": "b7f8b4409e53e46b25a6a4a6a29e85b7", "roots": { "bookmark_bar": { // ... "name": "ブックマークバー", "type": "folder", "children": [ { // .. "type": "url", "name": "Google" }, { // ... "name": "private", "type": "folder" "children": [ { // ... "type": "url", "name": "Facebook" }, { // ... "type": "url", "name": "Twitter" } ] } ] } } }
上のようにurl
タイプとfolder
タイプがあり、children
フィールドで無限ネストしてきます。ktr0731/go-fuzzyfinder
はネストしている構造を平らにしてsliceとして渡す必要があります。
Nodeのタイプを判断して構造体に詰めていくときは少し工夫が必要です。今回は下記のように Visitor pattern で実装しています。
// Bookmark contains bookmark data that have url type type Bookmark struct { Name string Path string URL string } // Node has Bookmark info. // folder type Node has child Nodes. type Node struct { Name string `json:"name"` Type string `json:"type"` URL string `json:"url"` Children []Node `json:"children"` } // Visitor visits Nodes. type Visitor interface { Visit(n Node, path string) error }
ここではブックマークのNodeを構造体で宣言します。Node
をVisitor
が1つずつ見ていき、[]Bookmark
に詰めていきます。Visotorの実装bookmarkRecoder
は下記のようになります。
type bookmarkRecoder struct { data []Bookmark } func newBookmarkRecoder() *bookmarkRecoder { return &bookmarkRecoder{ data: make([]Bookmark, 0, 100), } } func (b *bookmarkRecoder) Visit(n Node, path string) error { bm := Bookmark{ Name: n.Name, Path: path, URL: n.URL, } b.data = append(b.data, bm) return nil }
後はwalkEdge
関数を作り、NodeとVisitorを受けるようにすれば全てのノードを1つずつ訪問しながら[]Bookmark
に追加していきます。
func walkEdge(n Node, path string, v Visitor) error { switch n.Type { case "folder": for _, c := range n.Children { p := filepath.Join(path, c.Name) err := walkEdge(c, p, v) if err != nil { return err } } case "url": v.Visit(n, path) case "": // empty type return nil default: return fmt.Errorf("unsupported type: %+v", n.Type) } return nil }
複数タイプ無限ネストJSONの処理は一手間かかりましたが、この関数を使えばktr0731/go-fuzzyfinder
に渡すsliceが手に入ります。これで Chrome Bookmark Fuzzy-finder が完成しました。より詳しいコードはGitHubリポジトリでご確認ください。
まとめ
fuzzy-finderツールを作るにはktr0731/go-fuzzyfinder
が便利です。一方でネストしたデータ構造をfuzzy-findingするには平らなデータ構造にする必要があるので、Visitor pattern などをうまく使って実装する必要があります。Terminalから一撃でChrome Bookmarkを検索したい人は是非bmfzf
使ってみてください。
We're hiring !!!
エムスリーではGo大好きエンジニアを募集しています! 「ちょっと話聞いてみたいかも」という人はこちらから!