株式会社クリアコード > ククログ

ククログ


おすすめzsh設定

他の人がzshを使っているのを見ていると、「もっと便利に使えるのに」と、もやっとしたり、「え、その便利な機能ってなに?」と、発見があったりします。だれかに「この設定をすると便利ですよ」と話しやすくするために、今のzshのおすすめ設定をここに記しておきます。

もし、Emacsも使っている場合はおすすめEmacs設定もどうぞ。

ディレクトリ構成

長年漬け込んできたzshの設定がそこそこの量になっているので、以下のようなディレクトリ構成にして分類しています。主に、zsh標準機能の設定と追加パッケージの設定を分けるためにこうしています。

~
├── .zshrc                    # シェルを起動する毎に読み込まれる。
│                               # ~/.zsh.d/zshrcを読み込んで
│                               # 標準機能の追加設定を行う。
├── .zshenv                   # ログイン時に一度だけ読み込まれる。
│                               # ~/.zsh.d/zshenvを読み込んで
│                               # 追加設定を行う。
└── .zsh.d                    # zsh関連のファイル置き場。
       ├── config             # 標準機能以外の設定を置くディレクトリ。
       │    └── packages.zsh # 追加パッケージの設定をするファイル。
       ├── zshrc              # おすすめ~/.zshrc設定。
       ├── zshenv             # おすすめ~/.zshenv設定。
       ├── package.zsh        # パッケージ管理システム。
       └── packages           # パッケージをインストールするディレクトリ。

おすすめ設定の使い方

このうち、~/.zsh.d/以下にあるzshrczshenvは一般的なおすすめ設定となっていて、GitHubに置いてあります。

GitHubにあるおすすめ設定は以下のようにして使えます。(以下のコマンドをそのまま実行すると既存の~/.zshrc~/.zshenvを上書きするので注意してください。)

% git clone https://github.com/clear-code/zsh.d.git ~/.zsh.d
% echo "source ~/.zsh.d/zshrc" > ~/.zshrc
% echo "source ~/.zsh.d/zshenv" > ~/.zshenv

これらのおすすめ設定は~/.zshrc~/.zshenvの先頭 で読み込んで、その後でカスタマイズすることを想定しています。 例えばこんな感じです。

~/.zshrc:

### ~/.zshrc

# おすすめ設定を読み込む。
source ~/.zsh.d/zshrc

# これより↓に自分用の設定を書く。
#   setopt ...
#   alias ...
# など

~/.zshenv:

### ~/.zshenv

# おすすめ設定を読み込む。
source ~/.zsh.d/zshenv

# これより↓に自分用の設定を書く。
#   export EMAIL="..."
# など

~/.zshrc~/.zshenvの使い分け

~/.zshrc~/.zshenvは以下のように使い分けます。

  • 対話的に使うときだけ必要なものは~/.zshrc
  • 常に使うものは~/.zshenv

対話的以外にどういう場面で使うかというと、例えば、Emacs上からM-!でコマンドを実行するときです。このときは~/.zshenvのみが読み込まれて~/.zshrcは読み込まれません。

~/.zshenvのおすすめ設定

パスやEmacsなどからも使うページャやgrepなどのコマンドの設定、環境変数の設定などをします。設定の説明はコメントを見てください。

パスの設定

(N-/)は便利なので覚えておいて損はありません。

## 重複したパスを登録しない。
typeset -U path
## (N-/): 存在しないディレクトリは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            /: ディレクトリのみ残す。
path=(# システム用
      /bin(N-/)
      # 自分用
      $HOME/local/bin(N-/)
      # Debian GNU/Linux用
      /var/lib/gems/*/bin(N-/)
      # MacPorts用
      /opt/local/bin(N-/)
      # Solaris用
      /opt/csw/bin(N-/)
      /usr/sfw/bin(N-/)
      # Cygwin用
      /cygdrive/c/meadow/bin(N-/)
      # システム用
      /usr/local/bin(N-/)
      /usr/bin(N-/)
      /usr/games(N-/))
sudo時のパスの設定

sudoを使うときはsbinもパスに入れたいです。ただ、ここで設定したからといって実際にsudoで使われるわけではありません。後で~/.zshrcで設定するsudo時の補完用に使います。そのため、~/.zshenvではなく~/.zshrcで設定したほうがよい気もしますが、パスなので~/.zshenvに入れました。

typeset -Tは便利なので覚えておいて損はないと思います。

## -x: export SUDO_PATHも一緒に行う。
## -T: SUDO_PATHとsudo_pathを連動する。
typeset -xT SUDO_PATH sudo_path
## 重複したパスを登録しない。
typeset -U sudo_path
## (N-/): 存在しないディレクトリは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            /: ディレクトリのみ残す。
sudo_path=({,/usr/pkg,/usr/local,/usr}/sbin(N-/))
man時のパスの設定

(ほとんどありませんが)Emacs上でmanを読むこともあるためman用のパスも~/.zshenvで設定します。

## 重複したパスを登録しない。
typeset -U manpath
## (N-/) 存在しないディレクトリは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            /: ディレクトリのみ残す。
manpath=(# 自分用
         $HOME/local/share/man(N-/)
         # MacPorts用
         /opt/local/share/man(N-/)
         # Solaris用
         /opt/csw/share/man(N-/)
         /usr/sfw/share/man(N-/)
         # システム用
         /usr/local/share/man(N-/)
         /usr/share/man(N-/))
Rubyの設定

Rubyのライブラリを開発するときは./lib$LOAD_PATHに入っていると便利なので、その設定をしています。

## ライブラリのロードパス
### -x: export RUBYLIBも一緒に行う。
### -T: RUBYLIBとruby_pathを連動する。
typeset -xT RUBYLIB ruby_path
### 重複したパスを登録しない。
typeset -U ruby_path
### パスを設定
ruby_path=(# カレントディレクトリのライブラリを優先する
           ./lib)
Pythonの設定

Pythonのライブラリを開発するときは./libsys.pathに入っていると便利なので、その設定をしています。

## ライブラリのロードパス
### -x: export PYTHONPATHも一緒に行う。
### -T: PYTHONPATHとpython_pathを連動する。
typeset -xT PYTHONPATH pyhon_path
### 重複したパスを登録しない。
typeset -U python_path
### パスを設定。
python_path=(# カレントディレクトリのライブラリを優先する
             ./lib)
pkg-configの設定

pkg-configをサポートしたライブラリを使った開発するときの設定です。

## .pcのロードパス
### -x: export PKG_CONFIG_PATHも一緒に行う。
### -T: PKG_CONFIG_PATHとpkg_config_pathを連動する。
typeset -xT PKG_CONFIG_PATH pkg_config_path
### 重複したパスを登録しない。
typeset -U pkg_config_path
### パスを設定。
### (N-/) 存在しないディレクトリは登録しない。
###    パス(...): ...という条件にマッチするパスのみ残す。
###            N: NULL_GLOBオプションを設定。
###               globがマッチしなかったり存在しないパスを無視する。
###            -: シンボリックリンク先のパスを評価。
###            /: ディレクトリのみ残す。
pkg_config_path=(# 自分用
                 $HOME/local/lib/pkgconfig(N-/)
                 # MacPorts用
                 /opt/local/lib/pkgconfig(N-/))
ページャの設定

エンコーディングを変換できることが便利なのでlvを優先して使います。

if type lv > /dev/null 2>&1; then
    ## lvを優先する。
    export PAGER="lv"
else
    ## lvがなかったらlessを使う。
    export PAGER="less"
fi
lvの設定

単に「lv」としただけでもいい感じに動くようにします。

if [ "$PAGER" = "lv" ]; then
    ## -c: ANSIエスケープシーケンスの色付けなどを有効にする。
    ## -l: 1行が長くと折り返されていても1行として扱う。
    ##     (コピーしたときに余計な改行を入れない。)
    export LV="-c -l"
else
    ## lvがなくてもlvでページャーを起動する。
    alias lv="$PAGER"
fi
grepの設定

grepはGNU grepを使い、便利な機能は単に「grep」としただけで使えるようにします。一番便利なのは--color=autoです。

## GNU grepがあったら優先して使う。
if type ggrep > /dev/null 2>&1; then
    alias grep=ggrep
fi
## デフォルトオプションの設定
export GREP_OPTIONS
### バイナリファイルにはマッチさせない。
GREP_OPTIONS="--binary-files=without-match"
### grep対象としてディレクトリを指定したらディレクトリ内を再帰的にgrepする。
GREP_OPTIONS="--directories=recurse $GREP_OPTIONS"
### 拡張子が.tmpのファイルは無視する。
GREP_OPTIONS="--exclude=\*.tmp $GREP_OPTIONS"
## 管理用ディレクトリを無視する。
if grep --help | grep -q -- --exclude-dir; then
    GREP_OPTIONS="--exclude-dir=.svn $GREP_OPTIONS"
    GREP_OPTIONS="--exclude-dir=.git $GREP_OPTIONS"
    GREP_OPTIONS="--exclude-dir=.deps $GREP_OPTIONS"
    GREP_OPTIONS="--exclude-dir=.libs $GREP_OPTIONS"
fi
### 可能なら色を付ける。
if grep --help | grep -q -- --color; then
    GREP_OPTIONS="--color=auto $GREP_OPTIONS"
fi
エディタの設定

visudoなど、エディタが必要になったときはvimを使うようにします。

## vimを使う。
export EDITOR=vim
## vimがなくてもvimでviを起動する。
if ! type vim > /dev/null 2>&1; then
    alias vim=vi
fi
メールアドレスの設定

~/.zsh.d/emailまたは~/.emailにメールアドレスを書いておくと、そのファイルの中身を環境変数EMAILに設定します。メールアドレスはそれぞれ違うためこのように設定するようにしました。

## ~/.zsh.d/email → ~/.emailの順に探して最初に見つかったファイルから読み込む。
## (N-.): 存在しないファイルは登録しない。
##    パス(...): ...という条件にマッチするパスのみ残す。
##            N: NULL_GLOBオプションを設定。
##               globがマッチしなかったり存在しないパスを無視する。
##            -: シンボリックリンク先のパスを評価。
##            .: 通常のファイルのみ残す。
email_files=(~/.zsh.d/email(N-.)
             ~/.email(N-.))
for email_file in ${email_files}; do
    export EMAIL=$(cat "$email_file")
    break
done

~/.zshrcのおすすめ設定

補完やaliasなど対話的に使ったときに便利な機能の設定をします。こちらも設定の説明はコメントを見てください。

キーバインド

環境変数EDITORにvimと設定しているとviキーバインドになってしまうので、Emacsキーバインドを使うように明示的に指定しています。

## Emacsキーバインドを使う。
bindkey -e
ディレクトリ移動

cdの機能を拡張します。chpwd_functionsは覚えておいて損はないと思います。直接chpwdを定義するよりも追加の設定が簡単になります。

## ディレクトリ名だけでcdする。
setopt auto_cd
## cdで移動してもpushdと同じようにディレクトリスタックに追加する。
setopt auto_pushd
## カレントディレクトリ中に指定されたディレクトリが見つからなかった場合に
## 移動先を検索するリスト。
cdpath=(~)
## ディレクトリが変わったらディレクトリスタックを表示。
chpwd_functions=($chpwd_functions dirs)
ヒストリ

ヒストリはとてもよく使うため基本的に過去のコマンドラインは消えないようにします。

## ヒストリを保存するファイル
HISTFILE=~/.zsh_history
## メモリ上のヒストリ数。
## 大きな数を指定してすべてのヒストリを保存するようにしている。
HISTSIZE=10000000
## 保存するヒストリ数
SAVEHIST=$HISTSIZE
## ヒストリファイルにコマンドラインだけではなく実行時刻と実行時間も保存する。
setopt extended_history
## 同じコマンドラインを連続で実行した場合はヒストリに登録しない。
setopt hist_ignore_dups
## スペースで始まるコマンドラインはヒストリに追加しない。
setopt hist_ignore_space
## すぐにヒストリファイルに追記する。
setopt inc_append_history
## zshプロセス間でヒストリを共有する。
setopt share_history
## C-sでのヒストリ検索が潰されてしまうため、出力停止・開始用にC-s/C-qを使わない。
setopt no_flow_control
プロンプト

以下のような2段プロンプトにします。1段目にはできるだけ現在情報を多めに表示して、後から実行履歴をさかのぼって見たときに有意義な情報が残るようにします。プロンプト内にコマンドの実行ステータスを入れるようにしているのでprint_exit_statusオプションは設定していません。

-(user@debian)-(0)-<2011/09/01 00:54>--------------------[/home/user]-
-[84](1)%                                                         [~]

プロンプトで多くの機能を使えるようにします。

## PROMPT内で変数展開・コマンド置換・算術演算を実行する。
setopt prompt_subst
## PROMPT内で「%」文字から始まる置換機能を有効にする。
setopt prompt_percent
## コピペしやすいようにコマンド実行後は右プロンプトを消す。
setopt transient_rprompt

使っていませんが、256色用の色を生成する機能です。

## 256色生成用便利関数
### red: 0-5
### green: 0-5
### blue: 0-5
color256()
{
    local red=$1; shift
    local green=$2; shift
    local blue=$3; shift

    echo -n $[$red * 36 + $green * 6 + $blue + 16]
}

fg256()
{
    echo -n $'\e[38;5;'$(color256 "$@")"m"
}

bg256()
{
    echo -n $'\e[48;5;'$(color256 "$@")"m"
}

プロンプトにたくさんの情報を盛り込もうと頑張っています。「%」が多くてとても読みづらいですね。

## プロンプトの作成
### ↓のようにする。
###   -(user@debian)-(0)-<2011/09/01 00:54>--------------------[/home/user]-
###   -[84](0)%                                                         [~]

## バージョン管理システムの情報も表示する
autoload -Uz vcs_info
zstyle ':vcs_info:*' formats \
    '(%{%F{white}%K{green}%}%s%{%f%k%})-[%{%F{white}%K{blue}%}%b%{%f%k%}]'
zstyle ':vcs_info:*' actionformats \
    '(%{%F{white}%K{green}%}%s%{%f%k%})-[%{%F{white}%K{blue}%}%b%{%f%k%}|%{%F{white}%K{red}%}%a%{%f%k%}]'

### プロンプトバーの左側
###   %{%B%}...%{%b%}: 「...」を太字にする。
###   %{%F{cyan}%}...%{%f%}: 「...」をシアン色の文字にする。
###   %n: ユーザ名
###   %m: ホスト名(完全なホスト名ではなくて短いホスト名)
###   %{%B%F{white}%(?.%K{green}.%K{red})%}%?%{%f%k%b%}:
###                           最後に実行したコマンドが正常終了していれば
###                           太字で白文字で緑背景にして異常終了していれば
###                           太字で白文字で赤背景にする。
###   %{%F{white}%}: 白文字にする。
###     %(x.true-text.false-text): xが真のときはtrue-textになり
###                                偽のときはfalse-textになる。
###       ?: 最後に実行したコマンドの終了ステータスが0のときに真になる。
###       %K{green}: 緑景色にする。
###       %K{red}: 赤景色を赤にする。
###   %?: 最後に実行したコマンドの終了ステータス
###   %{%k%}: 背景色を元に戻す。
###   %{%f%}: 文字の色を元に戻す。
###   %{%b%}: 太字を元に戻す。
###   %D{%Y/%m/%d %H:%M}: 日付。「年/月/日 時:分」というフォーマット。
prompt_bar_left_self="(%{%B%}%n%{%b%}%{%F{cyan}%}@%{%f%}%{%B%}%m%{%b%})"
prompt_bar_left_status="(%{%B%F{white}%(?.%K{green}.%K{red})%}%?%{%k%f%b%})"
prompt_bar_left_date="<%{%B%}%D{%Y/%m/%d %H:%M}%{%b%}>"
prompt_bar_left="-${prompt_bar_left_self}-${prompt_bar_left_status}-${prompt_bar_left_date}-"
### プロンプトバーの右側
###   %{%B%K{magenta}%F{white}%}...%{%f%k%b%}:
###       「...」を太字のマジェンタ背景の白文字にする。
###   %d: カレントディレクトリのフルパス(省略しない)
prompt_bar_right="-[%{%B%K{magenta}%F{white}%}%d%{%f%k%b%}]-"

### 2行目左にでるプロンプト。
###   %h: ヒストリ数。
###   %(1j,(%j),): 実行中のジョブ数が1つ以上ある場合だけ「(%j)」を表示。
###     %j: 実行中のジョブ数。
###   %{%B%}...%{%b%}: 「...」を太字にする。
###   %#: 一般ユーザなら「%」、rootユーザなら「#」になる。
prompt_left="-[%h]%(1j,(%j),)%{%B%}%#%{%b%} "

## プロンプトフォーマットを展開した後の文字数を返す。
## 日本語未対応。
count_prompt_characters()
{
    # print:
    #   -P: プロンプトフォーマットを展開する。
    #   -n: 改行をつけない。
    # sed:
    #   -e $'s/\e\[[0-9;]*m//g': ANSIエスケープシーケンスを削除。
    # sed:
    #   -e 's/ //g': *BSDやMac OS Xのwcは数字の前に空白を出力するので削除する。
    print -n -P -- "$1" | sed -e $'s/\e\[[0-9;]*m//g' | wc -m | sed -e 's/ //g'
}

## プロンプトを更新する。
update_prompt()
{
    # プロンプトバーの左側の文字数を数える。
    # 左側では最後に実行したコマンドの終了ステータスを使って
    # いるのでこれは一番最初に実行しなければいけない。そうし
    # ないと、最後に実行したコマンドの終了ステータスが消えて
    # しまう。
    local bar_left_length=$(count_prompt_characters "$prompt_bar_left")
    # プロンプトバーに使える残り文字を計算する。
    # $COLUMNSにはターミナルの横幅が入っている。
    local bar_rest_length=$[COLUMNS - bar_left_length]

    local bar_left="$prompt_bar_left"
    # パスに展開される「%d」を削除。
    local bar_right_without_path="${prompt_bar_right:s/%d//}"
    # 「%d」を抜いた文字数を計算する。
    local bar_right_without_path_length=$(count_prompt_characters "$bar_right_without_path")
    # パスの最大長を計算する。
    #   $[...]: 「...」を算術演算した結果で展開する。
    local max_path_length=$[bar_rest_length - bar_right_without_path_length]
    # パスに展開される「%d」に最大文字数制限をつける。
    #   %d -> %(C,%${max_path_length}<...<%d%<<,)
    #     %(x,true-text,false-text):
    #         xが真のときはtrue-textになり偽のときはfalse-textになる。
    #         ここでは、「%N<...<%d%<<」の効果をこの範囲だけに限定させる
    #         ために用いているだけなので、xは必ず真になる条件を指定している。
    #       C: 現在の絶対パスが/以下にあると真。なので必ず真になる。
    #       %${max_path_length}<...<%d%<<:
    #          「%d」が「${max_path_length}」カラムより長かったら、
    #          長い分を削除して「...」にする。最終的に「...」も含めて
    #          「${max_path_length}」カラムより長くなることはない。
    bar_right=${prompt_bar_right:s/%d/%(C,%${max_path_length}<...<%d%<<,)/}
    # 「${bar_rest_length}」文字分の「-」を作っている。
    # どうせ後で切り詰めるので十分に長い文字列を作っているだけ。
    # 文字数はざっくり。
    local separator="${(l:${bar_rest_length}::-:)}"
    # プロンプトバー全体を「${bar_rest_length}」カラム分にする。
    #   %${bar_rest_length}<<...%<<:
    #     「...」を最大で「${bar_rest_length}」カラムにする。
    bar_right="%${bar_rest_length}<<${separator}${bar_right}%<<"

    # プロンプトバーと左プロンプトを設定
    #   "${bar_left}${bar_right}": プロンプトバー
    #   $'\n': 改行
    #   "${prompt_left}": 2行目左のプロンプト
    PROMPT="${bar_left}${bar_right}"$'\n'"${prompt_left}"
    # 右プロンプト
    #   %{%B%F{white}%K{green}}...%{%k%f%b%}:
    #       「...」を太字で緑背景の白文字にする。
    #   %~: カレントディレクトリのフルパス(可能なら「~」で省略する)
    RPROMPT="[%{%B%F{white}%K{magenta}%}%~%{%k%f%b%}]"

    # バージョン管理システムの情報を取得する。
    LANG=C vcs_info >&/dev/null
    # バージョン管理システムの情報があったら右プロンプトに表示する。
    if [ -n "$vcs_info_msg_0_" ]; then
        RPROMPT="${vcs_info_msg_0_}-${RPROMPT}"
    fi
}

## コマンド実行前に呼び出されるフック。
precmd_functions=($precmd_functions update_prompt)
補完

zshのとても大事な機能です。

## 初期化
autoload -U compinit
compinit

補完候補をグループ化するとぐっと見やすくなります。

## 補完方法毎にグループ化する。
### 補完方法の表示方法
###   %B...%b: 「...」を太字にする。
###   %d: 補完方法のラベル
zstyle ':completion:*' format '%B%d%b'
zstyle ':completion:*' group-name ''

補完候補が多いときや日本語のファイル名のときはメニューから選択できると便利です。selectの他にtrueも設定できるのですが、それを設定するとすぐに補完された状態になり使い勝手が悪いため設定していません。

## 補完侯補をメニューから選択する。
### select=2: 補完候補を一覧から選択する。
###           ただし、補完候補が2つ以上なければすぐに補完する。
zstyle ':completion:*:default' menu select=2

パスを補完しているときにディレクトリやシンボリックリンクがパッと見てわかるので便利です。

## 補完候補に色を付ける。
### "": 空文字列はデフォルト値を使うという意味。
zstyle ':completion:*:default' list-colors ""

大文字を入力しなくても大文字の補完候補が出てきて便利です。また、Emacsのpartial-completion-modeのようにも補完できて、public_htmlをp_で補完できて便利です。

## 補完候補がなければより曖昧に候補を探す。
### m:{a-z}={A-Z}: 小文字を大文字に変えたものでも補完する。
### r:|[._-]=*: 「.」「_」「-」の前にワイルドカード「*」があるものとして補完する。
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z} r:|[._-]=*'

多めに補完方法を使っています。

## 補完方法の設定。指定した順番に実行する。
### _oldlist 前回の補完結果を再利用する。
### _complete: 補完する。
### _match: globを展開しないで候補の一覧から補完する。
### _history: ヒストリのコマンドも補完候補とする。
### _ignored: 補完候補にださないと指定したものも補完候補とする。
### _approximate: 似ている補完候補も補完候補とする。
### _prefix: カーソル以降を無視してカーソル位置までで補完する。
zstyle ':completion:*' completer \
    _oldlist _complete _match _history _ignored _approximate _prefix

細々と便利な設定です。

## 補完候補をキャッシュする。
zstyle ':completion:*' use-cache yes
## 詳細な情報を使う。
zstyle ':completion:*' verbose yes
## sudo時にはsudo用のパスも使う。
zstyle ':completion:sudo:*' environ PATH="$SUDO_PATH:$PATH"

## カーソル位置で補完する。
setopt complete_in_word
## globを展開しないで候補の一覧から補完する。
setopt glob_complete
## 補完時にヒストリを自動的に展開する。
setopt hist_expand
## 補完候補がないときなどにビープ音を鳴らさない。
setopt no_beep
## 辞書順ではなく数字順に並べる。
setopt numeric_glob_sort
展開

configureを使うときに便利です。

## --prefix=~/localというように「=」の後でも
## 「~」や「=コマンド」などのファイル名展開を行う。
setopt magic_equal_subst

あまり必要な機会はありませんが、設定しています。

## 拡張globを有効にする。
## glob中で「(#...)」という書式で指定する。
setopt extended_glob
## globでパスを生成したときに、パスがディレクトリだったら最後に「/」をつける。
setopt mark_dirs
ジョブ

別の端末からgdbでアタッチするときにプロセスIDがわかると便利です。

## jobsでプロセスIDも出力する。
setopt long_list_jobs
実行時間

明示的にtimeを使わずに済んで便利です。

## 実行したプロセスの消費時間が3秒以上かかったら
## 自動的に消費時間の統計情報を表示する。
REPORTTIME=3
ログイン・ログアウト

クラックされたときにすぐに気づけ(ることがあり)ます。

## 全てのユーザのログイン・ログアウトを監視する。
watch="all"
## ログイン時にはすぐに表示する。
log

## ^Dでログアウトしないようにする。
setopt ignore_eof
単語

C-wで単語単位で削除するときにパスの1コンポーネントだけ削除できて便利です。M-f/M-bで単語移動をする人も便利でしょう。

## 「/」も単語区切りとみなす。
WORDCHARS=${WORDCHARS:s,/,,}
alias

LGが特に便利です。

## ページャーを使いやすくする。
### grep -r def *.rb L -> grep -r def *.rb |& lv
alias -g L="|& $PAGER"
## grepを使いやすくする。
alias -g G='| grep'
## 後はおまけ。
alias -g H='| head'
alias -g T='| tail'
alias -g S='| sed'

rrをよく使います。

## 完全に削除。
alias rr="command rm -rf"
## ファイル操作を確認する。
alias rm="rm -i"
alias cp="cp -i"
alias mv="mv -i"

poをよく使います。pdauto_pushdを設定しているため使いません。

## pushd/popdのショートカット。
alias pd="pushd"
alias po="popd"

素のlsを使うことはほとんどなく、いつもlaを使っています。

## lsとpsの設定
### ls: できるだけGNU lsを使う。
### ps: 自分関連のプロセスのみ表示。
case $(uname) in
    *BSD|Darwin)
        if [ -x "$(which gnuls)" ]; then
            alias ls="gnuls"
            alias la="ls -lhAF --color=auto"
        else
            alias la="ls -lhAFG"
        fi
        alias ps="ps -fU$(whoami)"
        ;;
    SunOS)
        if [ -x "`which gls`" ]; then
            alias ls="gls"
            alias la="ls -lhAF --color=auto"
        else
            alias la="ls -lhAF"
        fi
        alias ps="ps -fl -u$(/usr/xpg4/bin/id -un)"
        ;;
    *)
        alias la="ls -lhAF --color=auto"
        alias ps="ps -fU$(whoami) --forest"
        ;;
esac
ウィンドウタイトル

タブを切り替えているときに便利です。

## 実行中のコマンドとユーザ名とホスト名とカレントディレクトリを表示。
update_title() {
    local command_line=
    typeset -a command_line
    command_line=${(z)2}
    local command=
    if [ ${(t)command_line} = "array-local" ]; then
        command="$command_line[1]"
    else
        command="$2"
    fi
    print -n -P "\e]2;"
    echo -n "(${command})"
    print -n -P " %n@%m:%~\a"
}
## X環境上でだけウィンドウタイトルを変える。
if [ -n "$DISPLAY" ]; then
    preexec_functions=($preexec_functions update_title)
fi

おまけ: package.zshの使い方

GitHubにあるおすすめ設定の中にpackage.zshという簡単なパッケージ管理*1システムも入っています。これを使えばGitHub上にあるzshを拡張するパッケージを簡単にインストールすることができます。例えば、auto-fuを使う場合は以下のようにします。auto-fuの設定をするファイルが~/.zsh.d/config/packages.zshなのはauto-fuがzshの標準機能ではなく追加パッケージだからです。このポリシーについてはこのページの先頭にある「ディレクトリ構成」のところを見直してください。

~/.zsh.d/config/packages.zsh:

# -*- sh -*-

# パッケージ管理システムを読み込む。
source ~/.zsh.d/package.zsh

# パッケージがインストールされていなければGitHubからcloneしてくる。
package-install github hchbaw/auto-fu.zsh
# パッケージを読み込む。
source $(package-directory hchbaw/auto-fu.zsh)/auto-fu.zsh
# auto-fuを初期化する。
zle-line-init() {
    auto-fu-init
}
zle -N zle-line-init
zle -N zle-keymap-select auto-fu-zle-keymap-select

# auto-fuをカスタマイズする。
## Enterを押したときは自動補完された部分を利用しない。
afu+cancel-and-accept-line() {
    ((afu_in_p == 1)) && { afu_in_p=0; BUFFER="$buffer_cur" }
    zle afu+accept-line
}
zle -N afu+cancel-and-accept-line
bindkey -M afu "^M" afu+cancel-and-accept-line

~/.zshrc:

### ~/.zshrc

# おすすめ設定を読み込む。
source ~/.zsh.d/zshrc

# 追加パッケージの設定を読み込む。
source ~/.zsh.d/config/packages.zsh

これで次にzshを起動するとauto-fuが使えるようになっています。

まとめ

長年漬け込んできたzshの設定を紹介しました。

ここで紹介した内容はGitHubに置いておいたので、興味がある人は試してみてください。

おすすめEmacs設定もありますので、Emacsも使っているひとはそちらも試してみてください。

*1 「管理」というより「インストール」というか「ダウンロード」。

2011-09-05

groongaで高速な位置情報検索

groongaのドキュメントにも位置情報検索について書かれているのですが、情報の更新が追いついていないため情報が不足しています。そこで、ここに現状に合わせたgroongaの位置情報検索についての情報をまとめておきます。なお、ここにまとめた内容もドキュメントに反映させる予定です。

できること

groongaには位置情報を用いた検索機能がついています。位置情報を用いた検索では索引を利用するため、全文検索と同じように高速に検索することができます。ただし、PostGISMySQLのように*1線や面などもデータとして保持できるというわけではなく、点のみをデータとして保持できます。よって、groongaにできることは以下の通りです。

  1. 指定した四角の中に含まれている座標を持つレコードを検索する。
  2. 指定した円の中に含まれている座標を持つレコードを検索する。
  3. 座標間の距離を計算する。
  4. ある座標からの距離が近い順にレコードをソートする。

つまり、以下のようなユースケースにはgroongaの位置情報検索機能を使うことができます。

  • 東京ドーム周辺のコンビニを検索する。
  • 新宿駅東口から近い順に和菓子屋をソートして表示する。
  • 今いる場所の近くにあるラーメン屋をリストアップして、近い順にソートして、今いる位置からの距離も計算する。

一方、以下のようなユースケースでは使えません。

  • 杉並区内にある駅を検索する。(1つの四角または1つの円で表現できない領域では検索できない。)
  • 湖を点ではなく領域で表現する。(レコードは点としてしか位置情報を持てない。)

文章だけだとピンとこないはずなので、図も用意しました。

まず、以下の図を見てください。黒い点がレコードを表しています。それぞれの操作でレコードがどのように扱われるかを示します。

レコードのみ

以下の図は「指定した四角の中に含まれている座標を持つレコードを検索」したところです。赤い四角が「指定した四角」で、赤い点が検索されたレコードです。

指定した四角の中に含まれている座標を持つレコードを検索

以下の図は「指定した円の中に含まれている座標を持つレコードを検索」したところです。赤い円が「指定した円」で、赤い点が検索されたレコードです。

指定した円の中に含まれている座標を持つレコードを検索

以下の図は「座標間の距離を計算」したところです。赤い点が基準点で、基準点とレコードの座標の間の距離を計算しています。

座標間の距離を計算

以下の図は「ある座標からの距離が近い順にレコードをソート」したところです。赤い点が基準点で、基準点からの距離が近い順にレコードを順番に選んでいます。赤い数字が選ばれた順番です。

ある座標からの距離が近い順にレコードをソート

表現方法

前述の通り、groongaで保持できる位置情報は点だけです。点を格納するカラムは以下のどちらかの型にしなければいけません。

  • TokyoGeoPoint: 日本測地系での座標のときに利用する。
  • WGS84GeoPoint: 世界測地系(WGS 84のWGSはWorld Geodetic Systemの略)での座標のときに利用する。

どちらの型を用いた場合でも、緯度と経度を格納するという点は変わりません。そのため、どちらの型の値も同じ表現方法を用います。サポートしている表現方法は以下のフォーマットの文字列です。

  • "#{緯度}x#{経度}"
  • "#{緯度},#{経度}"

緯度・経度は「ミリ秒」または「度」で表現します。ミリ秒表記はあまりなじみがないかもしれませんが、度表記はGoogle Mapsでも使われている表記なので見たことがあるかもしれません。たとえば、東京駅は緯度が35度40分52.975秒、経度が139度45分57.902秒ですが、これは以下のように表現します。

ミリ秒表記:

  • 35度40分52.975秒 → ((35 * 60 * 60) + (40 * 60) + 52.975) * 1000 → 128452975
  • 139度45分57.902秒 → ((139 * 60 * 60) + (45 * 60) + 57.902) * 1000 → 503157902
  • 座標: "128452975x503157902" または "128452975,503157902"

度表記:

  • 35度40分52.975秒 → 35 + ((40 + (52.975 / 60.0)) / 60.0) → 35.6813819444444
  • 139度45分57.902秒 → 139 + ((45 + (57.902 / 60.0)) / 60.0) → 139.766083888889
  • 座標: "35.6813819444444x139.766083888889"または"35.6813819444444,139.766083888889"(これはGoogle Mapsと同じ表記。つまり、Google Mapsで表示した座標情報をそのまま利用できる。)

以下は使用例です。まず、テーブルとカラムを定義します。

% groonga -n /tmp/geo-point
> table_create Stations TABLE_HASH_KEY ShortText
[[0,1315881737.57395,0.055109867],true]
> column_create Stations location COLUMN_SCALAR WGS84GeoPoint
[[0,1315881759.65377,0.081054688],true]

データを取り込みます。上記の4パターンすべてを取り込んでいます。

> load --table Stations
> [
> ["_key", "location"],
> ["東京駅(ミリ秒 + x表記)", "128452975x503157902"],
> ["東京駅(ミリ秒 + ,表記)", "128452975,503157902"],
> ["東京駅(度 + x表記)", "35.6813819444444x139.766083888889"],
> ["東京駅(度 + ,表記)", "35.6813819444444,139.766083888889"]
> ]
[[0,1315881767.69242,127.240681505],4]

格納されているデータを確認します。groonga内部では緯度・経度をミリ秒として保持しているため、ミリ秒表記で出力されます。groonga内部でミリ秒として保持しているのは浮動小数点数ではなく整数として処理したいからです。

> select Stations --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="4" NHITS="4">
<HIT NO="1">
<FIELD NAME="_id">1</FIELD>
<FIELD NAME="_key">東京駅(ミリ秒 + x表記)</FIELD>
<FIELD NAME="location">128452975x503157902</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_id">2</FIELD>
<FIELD NAME="_key">東京駅(ミリ秒 + ,表記)</FIELD>
<FIELD NAME="location">128452975x503157902</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_id">3</FIELD>
<FIELD NAME="_key">東京駅(度 + x表記)</FIELD>
<FIELD NAME="location">128452974x503157901</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_id">4</FIELD>
<FIELD NAME="_key">東京駅(度 + ,表記)</FIELD>
<FIELD NAME="location">128452974x503157901</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

目視でデータを確認する場合は「--output_type xml」を指定して、XMLとして出力した方が確認しやすいです。

使い方

groongaの位置情報検索機能で利用できる以下のことについてその実現方法を説明します。

  1. 指定した四角の中に含まれている座標を持つレコードを検索する。
  2. 指定した円の中に含まれている座標を持つレコードを検索する。
  3. 座標間の距離を計算する。
  4. ある座標からの距離が近い順にレコードをソートする。

説明にあたって、店舗を検索するアプリケーションを考えます。各店舗がそれぞれ1レコードに対応します。

スキーマ定義

まず、店舗を格納する「Shops」テーブルを定義します。今回は説明用なので、各店舗には必要最小限の情報として店舗名と位置情報のみを格納することとします。

table_create Shops TABLE_HASH_KEY ShortText
column_create Shops location COLUMN_SCALAR WGS84GeoPoint

位置情報で高速に検索できるようにインデックスを張ります。

table_create Locations TABLE_PAT_KEY WGS84GeoPoint
column_create Locations shop COLUMN_INDEX Shops location

インデックス用のテーブル「Locations」はパトリシアトライ(TABLE_PAT_KEY)にします。キーの型はインデックス対象のカラム(Shops.location)と同じ型(WGS84GeoPoint)にすることがポイントです。

以下は実際に実行した結果です。

ddl.grn:

table_create Shops TABLE_HASH_KEY ShortText
column_create Shops location COLUMN_SCALAR WGS84GeoPoint

table_create Locations TABLE_PAT_KEY WGS84GeoPoint
column_create Locations shop COLUMN_INDEX Shops location

データベースの作成:

% rm -rf /tmp/shops
% mkdir -p /tmp/shops/
% groonga -n /tmp/shops/db < ddl.grn
[[0,1315883158.10711,0.05206624],true]
[[0,1315883158.1593,0.067047364],true]
[[0,1315883158.22642,0.056288895],true]
[[0,1315883158.28277,0.11994776],true]
サンプルデータ

たまたまたいやき屋(+α)のデータがあった*2のでそのデータを使います。

shops.grn:

load --table Shops
[
["_key", "location"],
["根津のたいやき", "35.720253,139.762573"],
["たい焼 カタオカ", "35.712521,139.715591"],
["そばたいやき空", "35.683712,139.659088"],
["車", "35.721516,139.706207"],
["広瀬屋", "35.714844,139.685608"],
["さざれ", "35.714653,139.685043"],
["おめで鯛焼き本舗錦糸町東急店", "35.700516,139.817154"],
["尾長屋 錦糸町店", "35.698254,139.81105"],
["たいやき工房白家 阿佐ヶ谷店", "35.705517,139.638611"],
["たいやき本舗 藤家 阿佐ヶ谷店", "35.703938,139.637115"],
["みよし", "35.644539,139.537323"],
["寿々屋 菓子", "35.628922,139.695755"],
["たい焼き / たつみや", "35.665501,139.638657"],
["たい焼き鉄次 大丸東京店", "35.680912,139.76857"],
["吾妻屋", "35.700817,139.647598"],
["ほんま門", "35.722736,139.652573"],
["浪花家", "35.730061,139.796234"],
["代官山たい焼き黒鯛", "35.650345,139.704834"],
["たいやき神田達磨 八重洲店", "35.681461,139.770599"],
["柳屋 たい焼き", "35.685341,139.783981"],
["たい焼き写楽", "35.716969,139.794846"],
["たかね 和菓子", "35.698601,139.560913"],
["たい焼き ちよだ", "35.642601,139.652817"],
["ダ・カーポ", "35.627346,139.727356"],
["松島屋", "35.640556,139.737381"],
["銀座 かずや", "35.673508,139.760895"],
["ふるや古賀音庵 和菓子", "35.680603,139.676071"],
["蜂の家 自由が丘本店", "35.608021,139.668106"],
["薄皮たい焼き あづきちゃん", "35.64151,139.673203"],
["横浜 くりこ庵 浅草店", "35.712013,139.796829"],
["夢ある街のたいやき屋さん戸越銀座店", "35.616199,139.712524"],
["何故屋", "35.609039,139.665833"],
["築地 さのきや", "35.66592,139.770721"],
["しげ田", "35.672626,139.780273"],
["にしみや 甘味処", "35.671825,139.774628"],
["たいやきひいらぎ", "35.647701,139.711517"]
]

データのロード:

% groonga /tmp/shops/db < shops.grn
[[0,1315883204.86313,0.005284274],36]

それでは、サンプルデータが用意できたので実際に使ってみましょう。

指定した四角の中に含まれている座標を持つレコードを検索

明日、あなたは初めて浅草に行くことになりました。初めて行く土地にたいやき屋があるかどうか、気になりますよね。そこで、浅草周辺にあるたいやき屋をgroongaで検索することにしました。

地図を見ると、左上が「35.7185,139.7912」で右下が「35.7065,139.8069」となる四角い範囲の中にたいやき屋があるかどうかを調べれば浅草周辺のたいやき屋を見つけられそうです。

浅草周辺のたいやき屋をgeo_in_rectangle()で検索

groongaにはgeo_in_rectangle(カラム名, 四角い範囲の左上の座標, 四角い範囲の右下の座標)という関数があり、この関数を--filterオプションに指定すると指定した四角い範囲内にあるレコードをインデックスを使って高速に検索することができます。

% groonga /tmp/shops/db
> select Shops --filter 'geo_in_rectangle(location, "35.7185,139.7912", "35.7065,139.8069")' --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="2" NHITS="2">
<HIT NO="1">
<FIELD NAME="_id">30</FIELD>
<FIELD NAME="_key">横浜 くりこ庵 浅草店</FIELD>
<FIELD NAME="location">128563246x503268584</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_id">21</FIELD>
<FIELD NAME="_key">たい焼き写楽</FIELD>
<FIELD NAME="location">128581088x503261445</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

浅草周辺には、くりこ庵と写楽があるんですね。それでは、写楽の方に行くことにしましょう。

補足: 実際はブラウザ内の地図表示エリアに表示している範囲にあるレコードを検索するためにgeo_in_rectangle()を使うことが多いでしょう。なぜなら、ほとんどの地図表示エリアは四角だからです。

指定した円の中に含まれている座標を持つレコードを検索

浅草周辺を検索するために四角を指定するのは少し面倒ですね。それよりも、「浅草駅から500m以内にあるたいやき屋」の方がわかりやすいです。

地図を見ると、浅草駅は「35.7119,139.7983」にあります。

浅草周辺のたいやき屋をgeo_in_circle()で検索

groongaにはgeo_in_circle(カラム名, 円の中心の座標, 円の半径)という関数があり、この関数を--filterオプションに指定すると指定した円の範囲内にあるレコードをインデックスを使って高速に検索することができます。

% groonga /tmp/shops/db
> select Shops --filter 'geo_in_circle(location, "35.7119,139.7983", 500)' --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="1" NHITS="1">
<HIT NO="1">
<FIELD NAME="_id">30</FIELD>
<FIELD NAME="_key">横浜 くりこ庵 浅草店</FIELD>
<FIELD NAME="location">128563246x503268584</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

> 

浅草駅の近くには、くりこ庵しかないんですね。それでは、くりこ庵に行くことにしましょう。

座標間の距離を計算

東京駅から2km以内にあるたいやき屋を検索し、それぞれのたいやき屋までの距離も取得しましょう。距離を取得するには_scoreカラムとgeo_distance(カラム名, 基準点の座標)関数を使います。

_scoreカラムは擬似カラムの一種で、検索結果レコードに自動的に追加されているカラムです。通常は検索のヒットスコアを入れるのですが、それ以外の値でも任意の値を入れることができるので、東京駅からの距離を入れることにします。

--scorerオプションを指定することにより_scoreカラムに任意の値を設定できます。ここでgeo_distance()を使い、東京駅からの距離を_scoreカラムに入れます。

実行例(読みやすくするため改行が入っていますが実際は改行を入れてはいけません):

> select Shops
    --filter 'geo_in_circle(location, "35.68138194,139.766083888889", 2000)'
    --scorer '_score = geo_distance(location, "35.68138194,139.766083888889")'
    --output_columns '_key,_score,*'
    --sortby _score
    --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="7" NHITS="7">
<HIT NO="1">
<FIELD NAME="_key">たい焼き鉄次 大丸東京店</FIELD>
<FIELD NAME="_score">230</FIELD>
<FIELD NAME="location">128451283x503166852</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_key">たいやき神田達磨 八重洲店</FIELD>
<FIELD NAME="_score">407</FIELD>
<FIELD NAME="location">128453259x503174156</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_key">銀座 かずや</FIELD>
<FIELD NAME="_score">990</FIELD>
<FIELD NAME="location">128424628x503139222</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_key">にしみや 甘味処</FIELD>
<FIELD NAME="_score">1310</FIELD>
<FIELD NAME="location">128418570x503188660</FIELD>
</HIT>
<HIT NO="5">
<FIELD NAME="_key">しげ田</FIELD>
<FIELD NAME="_score">1606</FIELD>
<FIELD NAME="location">128421453x503208982</FIELD>
</HIT>
<HIT NO="6">
<FIELD NAME="_key">柳屋 たい焼き</FIELD>
<FIELD NAME="_score">1671</FIELD>
<FIELD NAME="location">128467227x503222331</FIELD>
</HIT>
<HIT NO="7">
<FIELD NAME="_key">築地 さのきや</FIELD>
<FIELD NAME="_score">1765</FIELD>
<FIELD NAME="location">128397312x503174595</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

鉄次が一番近いですね。

ある座標からの距離が近い順にレコードをソート

前の例では_scoreに入れた東京駅からの距離でソートしていましたが、このときはインデックスを使いません。インデックスを使ってソートする場合は--sortbyにgeo_distance()関数を指定します。ただし、--sortby内では緯度・経度の区切りに「,」は使えないことに注意してください。--sortbyでgeo_distance()を使うときは、「"#{緯度},#{経度}"」ではなく「"#{緯度}x#{経度}"」というように緯度・経度の区切りには「x」を使ってください。

  • ×: --filter 'geo_distance(location, "35.68138194,139.766083888889")'
  • ○: --filter 'geo_distance(location, "35.68138194x139.766083888889")'

実行例(読みやすくするため改行が入っていますが実際は改行を入れてはいけません):

> select Shops
    --filter 'geo_in_circle(location, "35.68138194,139.766083888889", 2000)'
    --sortby 'geo_distance(location, "35.68138194x139.766083888889")'
    --output_type xml
<?xml version="1.0" encoding="utf-8"?>
<SEGMENTS>
<SEGMENT>
<RESULTPAGE>
<RESULTSET OFFSET="0" LIMIT="7" NHITS="7">
<HIT NO="1">
<FIELD NAME="_key">たい焼き鉄次 大丸東京店</FIELD>
<FIELD NAME="location">128451283x503166852</FIELD>
</HIT>
<HIT NO="2">
<FIELD NAME="_key">たいやき神田達磨 八重洲店</FIELD>
<FIELD NAME="location">128453259x503174156</FIELD>
</HIT>
<HIT NO="3">
<FIELD NAME="_key">銀座 かずや</FIELD>
<FIELD NAME="location">128424628x503139222</FIELD>
</HIT>
<HIT NO="4">
<FIELD NAME="_key">にしみや 甘味処</FIELD>
<FIELD NAME="location">128418570x503188660</FIELD>
</HIT>
<HIT NO="5">
<FIELD NAME="_key">しげ田</FIELD>
<FIELD NAME="location">128421453x503208982</FIELD>
</HIT>
<HIT NO="6">
<FIELD NAME="_key">柳屋 たい焼き</FIELD>
<FIELD NAME="location">128467227x503222331</FIELD>
</HIT>
<HIT NO="7">
<FIELD NAME="_key">築地 さのきや</FIELD>
<FIELD NAME="location">128397312x503174595</FIELD>
</HIT>
</RESULTSET>
</RESULTPAGE>
</SEGMENT>
</SEGMENTS>

>

インデックスを使わない場合と同じ結果になっていますね。

データ構造

ここまではユーザ向けの説明でしたが、ここからは実装の説明になります。

groongaでは位置情報を高速に検索するために、GeoHashと同じ考え方で緯度経度をエンコードしてパトリシアトライに格納しています。同じ考え方というのは、緯度と経度の情報を交互に含んだバイト列としてエンコードするという点です。

例えば、東京駅はミリ秒表記では「128452975x503157902」になります。groongaは内部では緯度・経度をミリ秒としてデータを持っていて、それぞれ32bit整数として保持しています。東京駅のデータは2進数で表すと以下のようになります。

何番目のビットか 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
緯度(128452975) 0 0 0 0 0 1 1 1 1 0 1 0 1 0 0 0
経度(503157902) 0 0 0 1 1 1 0 1 1 1 1 1 1 1 0 1
何番目のビットか 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
緯度(128452975) 0 0 0 0 1 0 0 1 0 1 1 0 1 1 1 1
経度(503157902) 1 0 0 1 0 1 0 0 1 0 0 0 1 1 1 0

この緯度・経度データをエンコードして1つのビット列にし、それをパトリシアトライのキーとします。このとき、緯度のビットと経度のビットを交互に使います。

何番目のビットか 0 1 2 3 4 5 6 7 8 9 10 11 12 13 ...
エンコードされた緯度・経度データ 0 0 0 0 0 0 0 1 0 1 1 1 1 0 ...
緯度の何番目のビットか 0 1 2 3 4 5 6 ...
緯度(128452975) 0 0 0 0 0 1 1 ...
経度の何番目のビットか 0 1 2 3 4 5 6 ...
経度(503157902) 0 0 0 1 1 1 0 ...

こうすることにより、先頭の方から2ビットずつデータを読んでいけば緯度情報と経度情報を両方読み込むことができるデータ構造になります。さらに、先頭の方がより粗い位置情報(より広い範囲を表す情報)となっているので、先頭からデータを読み込むことにより徐々に範囲を絞り込んでいけます。

検索方法

では、このデータ構造を使ってどのように効率よく検索するかを説明します。

以下の図は先頭の2ビットだけを読んだ状態の図です。

先頭2ビットだけを読んだ状態

先頭の2ビットが「00」の場合は右上の赤い範囲を表します。つまり、赤い範囲にあるレコードを検索したい場合は先頭の2ビットが「00」のレコードを検索すればよいことになります。

さらに2ビット読んで先頭の4ビットまで使うことにしたのが以下の図です。

先頭4ビットまで読んだ状態

先頭の4ビットが「0000」であれば、右上の範囲の中のさらに左下の赤い範囲にあることがわかります。

このようにして先頭から2ビット単位でデータを読み込むことによりレコードを絞り込むことができます。先頭ビットでレコードを絞り込んでいく部分はパトリシアトライの前方一致検索機能を使います。そのため、位置情報のインデックス用のテーブルはパトリシアトライである必要があります。

geo_in_rectangle()を使った検索

geo_in_rectangle()がどのように検索しているかを説明します。

geo_in_rectangle()は、まず,指定された四角よりも少しだけ大きい範囲を選びます。例えば、黒塗りの四角が指定された場合は赤い縁になっている2つの範囲を選びます。このとき、できるだけ小さい範囲を選ぶようにがんばります。

geo_in_rectangle()で使う範囲

次に、範囲の中にあるレコードを取り出し、指定された四角の中に本当にレコードが含まれているかを確認します。範囲を少し大きめにとっているため、このチェックをしないと、指定された四角に入っていない(けど近くにある)レコードも検索結果に含めてしまう可能性があるためです。

geo_in_circle()を使った検索

geo_in_circle()も、形が四角ではなく円であるというだけでやっていることはgeo_in_rectangle()とほとんど同じです。違うのはどうやって検索対象とする範囲を選ぶかという部分だけです。

まず、指定された円よりも少しだけ大きい範囲を選びます。例なので、実際の処理よりも大雑把に説明します。以下の図のように黒塗りの円が指定された場合は赤い縁になっている9つの範囲を選びます*3。このとき、できるだけ小さい範囲を選ぶようにがんばります。

geo_in_circle()で使う範囲

次に、範囲の中にあるレコードを取り出し、指定された円の中に本当にレコードが含まれているかを確認します。これはgeo_in_rectangle()でもやっている処理と同じです。

まとめ

groongaのドキュメントの位置情報検索についての情報が不足しているため、現状に合わせた内容をまとめました。ここにまとめた内容は後でgroongaのドキュメントに反映させる予定です。

*1 このあたりに興味のある人はOpen Geospatial Consortiumのサイトもみるとよいでしょう。

*2 元々はgroonga本体のテスト用に用意したデータです。

*3 実際はもっと細かく範囲を分割して、検索範囲をもっと小さくします

タグ: Groonga
2011-09-13

全文検索エンジンgroongaを囲む昼下がりと夕べのお知らせ

今月も全文検索エンジンgroongaと、groongaをMySQLから使うためのモジュールであるgroongaストレージエンジンがリリースされました。

そして、groonga勉強会の開催が決まりました。

昼下がりの方は来週の土曜日に札幌で開催されます。夕べの方は2ヶ月後の29日に東京で開催されます。夕べは昨年同じ日にちに開催したgroonga勉強会の第2回目という位置づけで、前回と同様に開発している側からのgroongaと関連プロダクトの説明が主になります。一方、昼下がりの方は時間的なゆとりがあることもあり、単に説明を聞くだけではなく、質疑応答にも十分な時間をとれそうです。

groongaに興味のある方はぜひご参加ください。

タグ: Groonga
2011-09-29

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|