Redisベースの検索エンジンRediSearchを使ってみる

## RediSearchとは？

[RediSearch](https://oss.redislabs.com/redisearch/)とはRedisをベースに作られたオープンソースの全文検索・セカンダリインデックスエンジン。開発元はRedisの作者であるSalvatore Sanfilippo氏も在籍しているRedis Labs, inc. Redisをベースにしているだけあって速度を売りにしている。

Redis Labsの[ホワイトペーパー](https://redislabs.com/blog/search-benchmarking-redisearch-vs-elasticsearch/)によると、Elasticsearchと比べてスループットで4倍ほどの差があるようだ。なぜこれだけのパフォーマンスが出せるのかというと、もちろんRedisをベースにしているのもあるが、RediSearch自体が[RedisModule](https://redis.io/topics/modules-intro)というRedisを拡張するAPIを使ってC言語で書かれているためだ。

![](./querying-results.png)![querying-results.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44173/5fa55427-20bc-e363-414e-ec211e4ac364.png)


余談だが、RedisModuleを使った他のOSSとして[rediSQL](https://github.com/RedBeardLab/rediSQL)というRedisでSQLを実行するなかなか野心的なOSS(しかもRustで書かれている！)もあるので興味がある人はみてほしい。

ちなみに本記事はv1.6.14時点の情報になります。**`v1.6.14時点では日本語の全文検索に対応していない`**ので期待された方はそっ閉じしてください。

## Installation

```sh
docker run -p 6379:6379 redislabs/redisearch:latest
```

ソースからインストールしたい人は[こちら](https://oss.redislabs.com/redisearch/Quick_Start.html)を参照。 

## スキーマ作成(`FT.CREATE`)

RediSearchは他のRedisのデータ構造と違ってドキュメントを追加する前にスキーマを定義しないといけない。  　

```sh
FT.CREATE <INDEX名> <INDEXオプション> SCHEMA <FIELD名> <FIELDの型> <FIELDオプション>...
```

* フィールドの数には1024の制限がある。そのうちTEXTフィールドの最大個数は128個
* フィールドの型
  * `TEXT`: テキスト型
  * `NUMERIC`: 数値型
  * `GEO`: 緯度経度座標
* よく使いそうなオプション
  * `NOHL`: HighlightをOffにすることでメモリを節約できる
  * `NOFIELDS`: 各Fieldを保存しないことによってメモリを節約できる
  * `NOFREQS`: Term frequency(頻出度)を保存しないことによってメモリを節約できる
* よく使いそうなフィールドオプション
  * `SORTABLE`: FT.SEARCHコマンドでSORTABLE指定できるようになる

例：ポケモンのデータのスキーマを作ってみる

```sh
FT.CREATE pokemon SCHEMA id NUMERIC name TEXT name_ja TEXT hp NUMERIC attack NUMERIC defence NUMERIC sp_attack NUMERIC sp_defence NUMERIC speed NUMERIC
```

## ドキュメントを追加する(`FT.ADD`)

```sh
FD.ADD <INDEX名> <DocID> <Score> <OPTIONS> FIELDS <FIELD名> <FIELDの値>...
```

* `DocID`: ドキュメントの識別子
* よく使いそうなオプション
  * `REPLACE`: SQLのUpseart的な動きをする。新規作成or既存を削除して再作成
  * `PARTIAL`: `REPLACE`指定の時だけ有効。いくつかのフィールドを省略すると既存の値がそのまま残るようになる

例：フシギダネのドキュメントをpokemonインデックスに追加する

```sh
FT.ADD pokemon Bulbasaur 1.0 FIELDS id 1 name Bulbasaur hp 45 attack 49 defence 49 sp_attack 65 sp_defence 65 speed 45
```

するとRedisには以下のようなキーが作成されます。

```sh
127.0.0.1:6379> keys *
 1) "ft:pokemon/bulbasaur"
 2) "nm:pokemon/defence"
 3) "nm:pokemon/sp_attack"
 4) "nm:pokemon/hp"
 5) "idx:pokemon"
 6) "nm:pokemon/attack"
 7) "Bulbasaur"
 8) "nm:pokemon/id"
 9) "nm:pokemon/sp_defence"
10) "nm:pokemon/speed"
```

このうち`"Bulbasaur"`は[Hash](https://redis.io/commands#hash)でできていて、中を覗くとこんな感じになってます。

```sh
127.0.0.1:6379> hgetall "Bulbasaur"
 1) "id"
 2) "1"
 3) "name"
 4) "Bulbasaur"
 5) "hp"
 6) "45"
 7) "attack"
 8) "49"
 9) "defence"
10) "49"
11) "sp_attack"
12) "65"
13) "sp_defence"
14) "65"
15) "speed"
16) "45"
```

他のキーはRediSearchが管理するインデックスデータで、普通のコマンドでは中を見ることはできません。

```sh
127.0.0.1:6379> type "ft:pokemon/bulbasaur"
ft_invidx
127.0.0.1:6379> type "nm:pokemon/defence"
numericdx
```

ちなみに、スキーマ定義にないフィールドを含めるとインデックスは作成されませんが、Hashにはちゃんと登録されます。なので、Redisをアプリケーションのデータベースとして使いながら、ドキュメントの特定のフィールドのみインデックス化というような使い方もOK。

## 検索する(`FT.SEARCH`)

```sh
FT.SEARCH <インデックス名> <クエリ> <オプション>
```

* よく使いそうなオプション
  * `NOCONTENT`: docidのみ検索結果に表示する
  * `LIMIT first num`: 表示件数。デフォルトは0 10。なぜデフォルト上限が10なのか...無限であるべきなのでは...
  * `SORTBY field [ASC|DESC]`: `SORTABLE`がついているフィールドでソートする
  * `WITHSORTKEYS`: `SORTABLE`がついているフィールドでソートする

---



* 名前でテキスト検索する

    ```sh
    FT.SEARCH pokemon Bulbasaur
    ```

* フィールド指定でテキスト検索する

    ```sh
    FT.SEARCH pokemon "@name:Bulbasaur"
    ```

* ワイルドカードを使う	　

    ```sh
    FT.SEARCH pokemon "@name:Bul*"
    ```

* NUMERICフィールドの範囲指定で`id`が1のポケモンを検索する

    ```sh
    FT.SEARCH pokemon "@id:[1 1]"
    ```

* `hp`が100以上150以下のポケモンを検索する

    ```sh
    FT.SEARCH pokemon "@hp:[50 100]"
    ```

* `hp`が100以上150以下かつ`attach`が100以上150以下のポケモンを検索する

    ```sh
    FT.SEARCH pokemon "@hp:[50 100] @attack:[50 100]"
    ```

## 性能

やはり性能は気になるところ。Redisに作られたHashから直接HGETALLするのとFT.SEARCHで検索した場合の速度を比較してみる。

### 条件

* CentOS 7.4 / i7-7700 8cores 3.6GHz / DDR4 16GB RAM

* RediSearchは以下のコマンドで実行
    * `docker run --network host -t redislabs/redisearch:1.6.14`
* 同じホスト上のredisearchコンテナに対して`redis-benchmark`を使ってスループットを検証する
    * オプションは`-c 64 -P 1`と`-c 64 -P 128`の二通り

### 結果

* GETでデータを取得する

    ```sh
    $ redis-benchmark -c 64 -P 1 -n 1000000 get foo
    151492.20 requests per second

    $ redis-benchmark -c 64 -P 128 -n 5000000 get foo
    3259452.25 requests per second
    ```


* HGETALLでデータを取得する

    ```sh
    $ redis-benchmark -c 64 -P 1 -n 1000000 hgetall Bulbasaur
    129617.62 requests per second

    $ redis-benchmark -c 64 -P 128 -n 5000000 hgetall Bulbasaur
    838363.50 requests per second
    ```

* 完全一致検索

    ```sh
    $ redis-benchmark -c 64 -P 1 -n 1000000 FT.SEARCH pokemon Bulbasaur
    47714.48 requests per second

    $ redis-benchmark -c 64 -P 128 -n 1000000 FT.SEARCH pokemon Bulbasaur
    58840.83 requests per second
    ```

* ワイルドカードによる前方一致検索

    ```sh
    $ redis-benchmark -c 64 -P 1  -n 1000000 FT.SEARCH pokemon "@name:Bul*"
    36892.20 requests per second

    $ redis-benchmark -c 64 -P 128  -n 1000000 FT.SEARCH pokemon "@name:Bul*"
    39684.12 requests per second
    ```

* NUMERICフィールドの範囲検索する

    ```
    $ redis-benchmark -c 64 -P 1 -n 1000000 FT.SEARCH pokemon "@id:[1 1]"
    57773.41 requests per second

    $ redis-benchmark -c 64 -P 128 -n 1000000 FT.SEARCH pokemon "@id:[1 1]"
    62410.28 requests per second
    ```

## RediSearch 2ではどうなる？

RediSearch 2は2020/9/18にリリースされた最新バージョンで1系とは非互換です。一番の変更点としては、従来インデックスへの追加を`FT.ADD`を使わなくて`HSET`でできるようになったことがあります。一方、データ構造がHASHに限定されてしまう(今後HASH以外もサポートしていくとのこと)デメリットもあります。

ここでは、RediSearch2は詳しく紹介しませんが、同じくベンチマークを取ったので見ていきます。

* インデックス作成

    ```sh
    $ FT.CREATE idx:pokemon on hash prefix 1 pokemon: SCHEMA id NUMERIC name TEXT name_ja TEXT hp NUMERIC attack NUMERIC defence NUMERIC sp_attack NUMERIC sp_defence NUMERIC speed NUMERIC
    ```

* 完全一致検索

    ```sh
    $ redis-benchmark -c 64 -P 1 -n 1000000 FT.SEARCH pokemon Bulbasaur
    43965.71 requests per second

    $ redis-benchmark -c 64 -P 128 -n 5000000 FT.SEARCH pokemon Bulbasaur
    50563.79 requests per second
    ```

* ワイルドカードによる前方一致検索

    ```sh
    $ redis-benchmark -c 64 -P 1  -n 1000000 FT.SEARCH pokemon "@name:Bul*"
    28401.02 requests per second

    $ redis-benchmark -c 64 -P 128  -n 1000000 FT.SEARCH pokemon "@name:Bul*"
    31661.60 requests per second
    ```

* NUMERICフィールドの範囲検索する

    ```
    $ redis-benchmark -c 64 -P 1 -n 1000000 FT.SEARCH pokemon "@id:[1 1]"
    43620.50 requests per second

    $ redis-benchmark -c 64 -P 128 -n 1000000 FT.SEARCH pokemon "@id:[1 1]"
    54071.59 requests per second
    ```

## RediSearchをつかうべきか

* 性能を気にしない単純なキーの検索=> `KEYS`や`SCAN`でキー文字列を検索すればよい
* 複数の条件で検索したい=> `RediSearch`が良いかも
* SQLで検索したい=> `rediSQL`が使えるかも。が、そもそも本当にRedisでやるべきかも検討したほうがいい

## まとめ

RediSearchを業務で使ってみたので紹介してみました。筆者的にはRediSearchはすでにProduction readyでRedisをヘビーにつかっていて検索等が多いプロジェクトなら導入の検討の価値ありだと思います。

## 参考文献

* [RediSearch Github](https://github.com/RediSearch/RediSearch)
* [RediSearch Documentation](https://oss.redislabs.com/redisearch/)
* [Search Benchmarking: RediSearch vs. Elasticsearch](https://redislabs.com/blog/search-benchmarking-redisearch-vs-elasticsearch/)