RipGrep（rg）の Alpine Docker イメージを作って高速文字列検索

## 「`RipGrep`」（`rg`）を Alpine の Docker コンテナで動かす

RipGrep 入りの Alpine コンテナを作る Dockerfile とサンプルです。Intel アーキテクチャ[^arc]（Core i5、MacBook Pro）で動作確認しました。（~~残念ながらラズパイなどの ARM アーキテクチャには対応していません。~~[ラズパイ（ARM）対応の実験がはじまった](https://github.com/emk/rust-musl-builder#arm-support-experimental)ようです。）

[^arc]: 【Intel アーキテクチャとは】 Intel チップを前提に設計されたプラットホームのことです。「Intel 入ってる」とほぼ同義です。Intel 以外にも AMD64、ARMv6、ARMv7、IBM POWER、PowerPC64 など、たくさん種類があります。<br>バイナリ（プログラムがコンパイルされたファイル）はアーキテクチャに合わせて作成されるため、**アーキテクチャと[カーネル](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB)の種類が異なるプラットホームではバイナリは動きません**。Intel と AMD は互換が多いチップであるため、どちらでも動く場合は x86_64 アーキテクチャと呼ばれますが、全てのバイナリが動くという保証ではありません。特にチップの性能をフルに活用するライブラリを利用したプログラムの場合には注意が必要です。（機械学習系のプログラムなど）<br>このことは Docker でも重要で、[Docker Hub](https://hub.docker.com/) などから `docker pull` コマンドでダウンロードされるビルド済みイメージは、ビルド時と同じアーキテクチャでないと使えません。<br>DockerHub のイメージは、カーネルのほとんどが Linux であるものの、例えば `RaspberryPi Zero`（ARMv6l）用の Docker イメージが提供されていない場合は、`Dockerfile` および関連するソースファイルをダウンロードして `docker build` する必要があります。この時、注意しないといけないのが**ベースとなるイメージ（`Dockerfile` の `FROM` の値）も同じアーキテクチャのものにする必要がある**ということです。Dockerfile を利用する場合は、ベース・イメージが対応するアーキテクチャを提供しているか [Docker Hub で確認](https://hub.docker.com/_/alpine?tab=tags)しましょう。


```dockerfile:Dockerfile
#  Dockerfile to build RipGrep (rg) for Alpine linux image
#  via multi stage build
# =========================================================
# Note: As of 2019/02/06, rust-musl-builder is not compa-
#       tible to ARM architecture such as RaspberryPi.

# First stage (build)
# -------------------
FROM ekidd/rust-musl-builder:stable AS rg-build

WORKDIR /home/rust/src

RUN set -x &&\
    : "Cloning git from ripgrep repo" && \
    git clone https://github.com/BurntSushi/ripgrep && \
    : "Buildng ripgrep" && \
    cd ripgrep && \
    cargo build --release && \
    : "Show version" && \
    cd ./target/x86_64-unknown-linux-musl/release && \
    ./rg --version

# Second stage (copy)
# -------------------
FROM alpine AS ripgrep

COPY --from=rg-build \
     /home/rust/src/ripgrep/target/x86_64-unknown-linux-musl/release/rg \
     /usr/local/bin
```

#### Dockerイメージの作成例

```shell:Dockerイメージ構築例
$ ls
Dockerfile
$ # ビルド開始
$ docker build -t myripgrep .
（長いので省略）
Successfully built d2597650d47b
Successfully tagged ripgrep:latest
```

<details><summary>ラズパイで「standard_init_linux.go:190: exec user process caused "exec format error"」</summary><div>

##### RaspberryPi でビルドすると「standard_init_linux.go:190: exec user process caused "exec format error"」

RaspberryPi3 などの ARM アーキテクチャの場合、Docker のビルド時に失敗してしまいます。これは、`ekidd/rust-musl-builder` のコンテナ自体が ARM 対応していないためです。コンテナ単体でも起動しません。

```shell-session
$ uname -a
Linux UTouchMyPi 4.14.41-v7+ #1113 SMP Thu May 17 16:29:48 BST 2018 armv7l GNU/Linux
$ 
$ docker pull ekidd/rust-musl-builder:stable
$ docker run --rm -it ekidd/rust-musl-builder:stable /bin/bash
standard_init_linux.go:190: exec user process caused "exec format error"
```

[Issue にも上がっている](https://github.com/emk/rust-musl-builder/issues/63)のですが、「当面対応予定はない」とのこと。とほほ。

</div></details>

#### ripgrepイメージのサイズ

約 33MB になりました。

```shell-session:構築サイズ
$ docker image ls | grep myripgrep
myripgrep           latest          d2597650d47b        2 hours ago         32.9MB
```

最初のステージで利用した Rust のバイナリ・ビルド環境の `rust-musl-builder` のサイズが大きいので、バイナリが出来上がったら `rust-musl-builder` のコンテナとイメージは削除してもいいかもしれません。

## rgの基本構文

```php:rgの基本構文（--helpオプションで詳細確認可能）
USAGE:
    rg [OPTIONS] PATTERN [PATH ...]
    rg [OPTIONS] [-e PATTERN ...] [-f PATTERNFILE ...] [PATH ...]
    rg [OPTIONS] --files [PATH ...]
    rg [OPTIONS] --type-list
    command | rg [OPTIONS] PATTERN
```

`$ rg メキシコ ~/Documents` のように `$ rg 検索パターン 検索パス` で検索できます。検索パターンは正規表現が利用可能です。検索パスを省略した場合は、カレントディレクトリ以下を検索します。

#### ホストのボリュームをマウントして検索する例

```shell-session:ホストの/path/to/you/local/dirをゲストの/dataにマウントして利用する例
$ docker run --rm -it -v /path/to/you/local/dir:/data myripgrep /bin/sh
/ # # OS情報
/ # cat /etc/os-*
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.8.2
PRETTY_NAME="Alpine Linux v3.8"
HOME_URL="http://alpinelinux.org"
BUG_REPORT_URL="http://bugs.alpinelinux.org"
/ #
/ # # rg のバージョン確認
/ # rg --version
ripgrep 0.10.0 (rev 147e96914c)
-SIMD -AVX (compiled)
+SIMD +AVX (runtime)

/ # # rg の場所
/ # which rg
/usr/local/bin/rg
/ #
/ # #　マウントしたディレクトリの確認
/ # ls /data
barge_test           gimp_test            php_test             sed_test
bash_test            git-sign_test        python_test          wav-for-test-sound
docker_test          git_test             reboot_test          word2vec_test
flutter_test         github-encrypt_test  ruby_test
gcc_test             go_test              scp-spams_test
/ #
/ # # ヘルプ表示
/ # rg --help
（長いので省略）
/ #
/ # # /data 内の「stdio.h」を含むファイルを検索
/ # rg stdio.h /data
/data/gcc_test/test.c
1:#include <stdio.h>
/ #
/ # # /data 内の「メキシコ」を含むファイルを検索
/ # rg メキシコ /data
/data/word2vec_test/DIC_KANA.original.csv
2654:あめりかめきしこせんそう Mexican-American War 名詞 アメリカメキシコ戦争
27414:ニューメキシコ New Mexico 名詞
40744:めきしこ Mexico 名詞 メキシコ
40745:めきしこじん Mexican (person) 名詞 メキシコ人
40746:めきしこわん Gulf of Mexico 名詞 メキシコ湾
40747:めきしこわんりゅう Gulf Stream 名詞 メキシコ湾流

/data/word2vec_test/DIC_KANA.txt
2654:あめりかめきしこせんそう Mexican-American War 名詞 アメリカメキシコ戦争
27414:ニューメキシコ New Mexico 名詞
40744:めきしこ Mexico 名詞 メキシコ
40745:めきしこじん Mexican (person) 名詞 メキシコ人
40746:めきしこわん Gulf of Mexico 名詞 メキシコ湾
40747:めきしこわんりゅう Gulf Stream 名詞 メキシコ湾流
/ #
/ # # /data 内の「studio.h」を含むファイルを検索し、結果を JSON で表示
# rg --json stdio.h /data
{"type":"begin","data":{"path":{"text":"/data/gcc_test/test.c"}}}
{"type":"match","data":{"path":{"text":"/data/gcc_test/test.c"},"lines":{"text":"#include
 <stdio.h>\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":
{"text":"stdio.h"},"start":10,"end":17}]}}
{"type":"end","data":{"path":{"text":"/data/gcc_test/test.c"},"binary_offset":null,"stats":
{"elapsed":
{"secs":0,"nanos":1433800,"human":"0.001434s"},"searches":1,"searches_with_match":1,
"bytes_searched":80,"bytes_printed":272,"matched_lines":1,"matches":1}}}
{"data":{"elapsed_total":{"human":"6.577406s","nanos":577405900,"secs":6},"stats":
{"bytes_printed":272,"bytes_searched":80,"elapsed":
{"human":"0.001434s","nanos":1433800,"secs":0},"matched_lines":1,"matches":1,"searches":1,
"searches_with_match":1}},"type":"summary"}
```

## 動作確認環境

- macOS Mojave（OSX 10.14.3）
- `uname -a` : `Darwin MacBook-Pro.local 18.2.0 Darwin Kernel Version 18.2.0: Thu Dec 20 20:46:53 PST 2018; root:xnu-4903.241.1~1/RELEASE_X86_64 x86_64`
- docker: `brew cask docker` でインストール
- <details><summary>Docker version:</summary><div>

```shell-session
$ docker version
Client: Docker Engine - Community
 Version:           18.09.0
 API version:       1.39
 Go version:        go1.10.4
 Git commit:        4d60db4
 Built:             Wed Nov  7 00:47:43 2018
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.0
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.4
  Git commit:       4d60db4
  Built:            Wed Nov  7 00:55:00 2018
  OS/Arch:          linux/amd64
  Experimental:     false
```

</div></details>

## 所感

### `RipGrep` on Alpine Base-image

Rust 版 `grep` とも言える、爆速の文字列検索コマンド「`rg`」（`RipGrep`）が好きです。

<details><summary>RigGrep（rg）とは</summary><div>

> **RipGrep (rg)**
> **ripgrep is a line-oriented search tool that recursively searches your current directory for a regex pattern** while respecting your gitignore rules. ripgrep has first class support on Windows, macOS and Linux, with binary downloads available for every release. ripgrep is similar to other popular search tools like The Silver Searcher, ack and grep.
> （[BurntSushi/ripgrep](https://github.com/BurntSushi/ripgrep) @ GitHub より）

> **RipGrep（rg）とは**
> RipGrep（`rg`）は、`grep` に似たコマンドライン・ベースの UTF-8 対応テキスト検索ツールです。[BurntSushi](http://blog.burntsushi.net/ripgrep/)氏によって [`Rust`](https://ja.wikipedia.org/wiki/Rust_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E)) で書かれており、[`Silver Searcher`](http://qiita.com/tags/ag)（`ag`） の使いやすさと `GNU grep` の本来のスピードを兼ね備えています。 Windows, Mac, Linux などの OS で動作します。
> （「[爆速 grep「ripgrep」をラズパイにインストールする【実測値あり】](https://qiita.com/KEINOS/items/9267e6f4e3806f508db0)」 @ Qiita より）


- [`ripgrep` の Qiita 記事](https://www.google.com/search?q=site:qiita.com+ripgrep&tbs=qdr:y) @ Google


---
</div></details>

そこで、最近いまさらながら学習を始めた Docker で `ripgrep` を良い感じで使うにはどうすれば良いか考えてみました。

まずは「（Docker コンテナに）**マウントされたボリューム内にあるファイルを `rg` で文字列検索するだけのコンテナ**」を作ってみることにしました。動いてしまえば、あとはコンテナを API 化してコンテナ間で利用できそうです。

文字列検索に特化したコンテナであるため、できればイメージを軽くしたいので軽量 OS で有名な [Alpine Linux](https://ja.wikipedia.org/wiki/Alpine_Linux) をベース・イメージにしたいと思いました。

しかし、`rg` が対応しているパッケージマネージャーはたくさんある[^rgavailable]のですが、残念なことに 2019/02/05 現在 Alpine のパッケージマネージャー「apk」には対応していません[^rgnotinalpine]。

そこで、Rust の静的リンクなバイナリを構築[^rustonalpine]してから、そのバイナリをプレーンな `Alpine` イメージにコピーするという「Docker の[マルチステージビルド](https://www.google.com/search?q=site:qiita.com+docker+%E3%83%9E%E3%83%AB%E3%83%81%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B8%E3%83%93%E3%83%AB%E3%83%89)」の手法を使うことにしました。

便利なことに、サイズはいささか大きいのですが「[rust-musl-builder](https://github.com/emk/rust-musl-builder)」という **Rust の開発に必要なものが揃っている Docker のイメージ**を DockerHub に公開されている方がいらっしゃったので、そちらを使わせていただくことにしました。

> **rust-musl-builder**
> **Docker images for compiling static Rust binaries** using musl-libc and musl-gcc, with static versions of useful C libraries. Supports openssl and diesel crates.
>
> - GitHub: [emk/rust-musl-builder](https://github.com/emk/rust-musl-builder)
> - DockerHub: ekidd/rust-musl-builder

[^rgavailable]: 【`ripgrep` 対応のパッケージマネージャ一覧】 https://repology.org/metapackage/ripgrep/versions
[^rgnotinalpine]: 2019/02/０５ 現在 [PullRequest #1250](https://github.com/alpinelinux/aports/pull/1250) @ GitHub
[^rustonalpine]: 「[AlpineでRustのバイナリを動かす](http://etogen.hatenablog.com/entry/2018/03/31/155140)」@ etogenのブログ

## 参考文献

- 「[AlpineでRustのバイナリを動かす](http://etogen.hatenablog.com/entry/2018/03/31/155140)」@ etogenのブログ
- 「[glibc-と-musl](https://qiita.com/legokichi/items/b78de0df844e63e77a12#glibc-%E3%81%A8-musl) | Rust でクロスコンパイルして Raspberry Pi Zero W で動かす」 @ Qiita
- 「[Multi-stage buildでDockerイメージのサイズを最適化](https://qiita.com/hitochan777/items/39ef463800419dd3a2ef)」@ Qiita