Apache Solrの基本

この記事は [ただの集団 Advent Calendar 2020](https://adventar.org/calendars/5581) の19日目の記事です。

# はじめに
全文検索エンジンとして最もポピュラーなのはElasticSearchだと思いますが、数年前に同種のプロダクトである [Apache Solr](https://lucene.apache.org/solr/) を使ったお仕事を経験した事があります。
最近、復習がてらSolrの本を読み直したので、アウトプットとして軽く纏めます。

両者の主な共通点、長所、短所と、細かい話は抜きにしてとりあえず動かしてみる、という感じで。

# Apache SolrとElasticSearchの共通点、長所、短所
### 共通点
- 土台となる全文検索ライブラリとして、[Apache Lucene](https://lucene.apache.org) を使用している
- Javaで実装されている

### Apache Solrの長所、短所
- 長所
 - 検索パラメータはクエリパラメータとして指定するので、クエリDSLを使うElasticSearchと比較して直感的に記述できる
 - １つ目と被るが、ハイライト検索やファセット検索など、高度な検索機能でも比較的簡易なクエリパラメータで記述できる
 - 実績が十分（ElasticSearchよりも先にリリースされている）
 - 機能が豊富（ElasticSearchも充実してきているので、比較した上で長所、とは言えないかも）
- 短所
 - クラスタの構築は [Apache Zookeeper](https://zookeeper.apache.org) が必要なため、設定が若干面倒
 - クエリパラメータで複雑な指定をすると見通しが悪くなる

### ElasticSearchの長所、短所
- 長所
 - 構造的なクエリを作成できる「クエリDSL」は学習コストが高いものの、慣れれば複雑な検索を効率的に実施できる
 - クラスタの構築が簡単
 - 便利な関連プロダクトが豊富（KibanaやLogstashなど）
- 短所
 - 長所の裏返しで、クエリDSLの書き方が複雑で学習コストが高い（未だに書き方でいつも迷う・・・）

# とりあえず動かしてみる

時流（？）に沿ってDockerで起動します。

```docker-compose.yml
version: "3"

services:
  solr1:
    image: solr:8.7
    container_name: solr1
    ports:
      - 8983:8983
    volumes:
      - ./solr:/var/solr
    command:
      - solr-precreate
      - solrbook
    networks:
      - solr

networks:
  solr:
```

SolrではWebUIが提供されており、細かい設定の確認や検索の実行、形態素解析がどのように実行されるか、といった一通りの動作確認ができます。
![solr管理画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/760afcf7-199b-85a0-5ed3-5a1fb94155a9.png)
docker-compose.ymlのcommand指定で「solrbook」という名前でコアを作成しているので、そちらを選択します。

:::note info
Solrには「コア」という概念があり、スキーマ、インデックスデータ、インデックスデータを操作するJavaオブジェクトをひとまとめにしたものです。コアを作成する事で、データの登録、検索が可能になります。
:::

![Solr検索.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/b2f7e3e9-e1be-0c5a-1f60-4a06b0104bc8.png)
まだデータを登録していないので、もちろん検索結果は０件です。
では、データを登録してみましょう。登録データは [こちら](https://github.com/solrbook3/examples/blob/master/solrbook/sample-books.json) を拝借しました。

```
curl http://localhost:8983/solr/solrbook/update?commit=true --data-binary @sample-data/sample-books.json -H 'Content-type:application/json'
```

![Solr検索-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/9e8b8b46-42e6-6b68-46fa-49d4f276bed9.png)
登録後、データを検索できました。

# 高度な検索
少し高度な検索もしてみます。

### ハイライト検索
検索結果として表示されているドキュメントのどの部分が検索クエリにヒットしたのか、を提供する機能です。
![ハイライト検索.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/ef079b38-0108-a673-468d-42ea0b5540e9.png)
検索結果の下部に "highlighting" と表示され、検索クエリとして「title:オンラインゲーム」と指定しているのでオンラインゲームがemタグで囲まれています。用途の一例としては、検索元のアプリケーションで結果を表示する時に、クエリに一致した箇所を強調表示する、などが挙げられます。また、設定次第でemタグから変更する事もできます。

クエリパラメータ指定だとこんな感じで、管理画面と同じ結果が表示されます。

```
curl 'http://localhost:8983/solr/solrbook/select?hl.fl=title&hl=on&q=title:%E3%82%AA%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E3%82%B2%E3%83%BC%E3%83%A0'
```

### ファセット検索
検索結果をフィールドごとにグルーピングできます。

```
curl 'http://localhost:8983/solr/solrbook/select?facet.field=price&facet=on&q=*:*'
```

「facet.field=price」と指定する事で、検索結果をpriceでグルーピングしています。

```
"facet_counts":{
    "facet_queries":{},
    "facet_fields":{
      "price":[
        "1980",114,
        "1480",59,
        "1780",56,
        "1880",54,
        "1680",50,
        "2480",49,
        "1380",48,
        "980",42,
        "1580",42,
        "2580",41,
        "2980",41,
        "2380",38,
        "2180",36,
        "2680",34,
        "1280",31,
        "2280",29,
        "857",27,
        "2880",25,
        "1000",24,
        "2780",24,
        "3200",24,
        "880",22,
        "1080",16,
        "1180",13,
        "840",12,
        "3500",12,
        "3600",9,
        "780",8,
        "950",8,
        "1143",8,
        "1200",8,
        "3300",8,
        "680",7,
        "1429",6,
        "3280",6,
        "690",5,
        "762",5,
        "900",5,
        "2080",5,
        "790",4,
        "1850",4,
        "3800",4,
        "3400",3,
        "3480",3,
        "740",2,
        "743",2,
        "2570",2,
        "2770",2,
        "2800",2,
        "3180",2,
        "3700",2,
        "3900",2,
        "4800",2,
        "933",1,
        "1333",1,
        "1714",1,
        "2560",1,
        "2900",1,
        "3380",1,
        "3580",1,
        "3880",1,
        "4200",1,
        "4300",1,
        "5200",1]
    },
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}
}
```

多少高度な検索でも、直感的なパラメータ指定で可能な事が分かると思います。

# クラスタリング

SolrCloudという仕組みでクラスタリングを実現する事が出来ます。

Solrの設定ファイルや分散クラスタ情報をzookeeperで中央管理する事で、分散インデックス、分散検索、レプリケーション、自動フェールオーバーの仕組みを提供しています。

SolrCloudにおいてはスキーマ情報やインデックス、インデックスデータ操作の仕組みを「コレクション」という概念で取り扱っています。詳細は省きますが、単一でSolrを起動する場合の「コア」のようなものと考えてください。

### とりあえず動かしてみる
```docker-compose.yml
services:
  solr1:
    image: solr:8.7
    container_name: solr1
    ports:
      - "8981:8983"
    volumes:
      - ./solr1:/var/solr
    environment:
      - ZK_HOST=zk1:2181,zk2:2181,zk3:2181
    networks:
      - solr
    depends_on:
      - zk1
      - zk2
      - zk3

  solr2:
    image: solr:8.7
    container_name: solr2
    ports:
      - "8982:8983"
    volumes:
      - ./solr2:/var/solr
    environment:
      - ZK_HOST=zk1:2181,zk2:2181,zk3:2181
    networks:
      - solr
    depends_on:
      - zk1
      - zk2
      - zk3

  solr3:
    image: solr:8.7
    container_name: solr3
    ports:
      - "8983:8983"
    volumes:
      - ./solr3:/var/solr
    environment:
      - ZK_HOST=zk1:2181,zk2:2181,zk3:2181
    networks:
      - solr
    depends_on:
      - zk1
      - zk2
      - zk3

  zk1:
    image: zookeeper:3.5
    container_name: zk1
    restart: always
    hostname: zk1
    ports:
      - 2181:2181
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
      ZOO_4LW_COMMANDS_WHITELIST: mntr,conf,ruok
    networks:
      - solr

  zk2:
    image: zookeeper:3.5
    container_name: zk2
    restart: always
    hostname: zk2
    ports:
      - 2182:2181
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
      ZOO_4LW_COMMANDS_WHITELIST: mntr,conf,ruok
    networks:
      - solr

  zk3:
    image: zookeeper:3.5
    container_name: zk3
    restart: always
    hostname: zk3
    ports:
      - 2183:2181
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zk1:2888:3888;2181 server.2=zk2:2888:3888;2181 server.3=zk3:2888:3888;2181
      ZOO_4LW_COMMANDS_WHITELIST: mntr,conf,ruok
    networks:
      - solr

networks:
  solr:
```

:::note info
zookeeperも複数台存在する理由としては、設定を中央管理するという性質上、一台だけだと単一障害点となるためです。そのため、zookeeper自身もクラスタリング機能を提供しており、実運用では複数台構成にするのが通常です。
:::

![SolrCloud.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/d2a2db8f-5e61-2043-404a-6f2f12f4f02d.png)
単一モードで起動した時と比較して、メニューの項目が若干変わっています。「Cloud」のメニューを開くと、何やら３台のSolrノードが１つのグループになっていそうな雰囲気です。
結論を言うとその通りで、zookeeperによって３台のSolrノードが管理されている、という事になります。

さて、現状だとデータはおろかコレクションも無い状態なので、さっそくコレクションを作成してデータ登録をおこないます。

```
# コレクション「solrbook」を作成
curl "http://localhost:8981/solr/admin/collections?action=CREATE&name=solrbook&maxShardsPerNode=2&numShards=2&replicationFactor=3&collection.configName=_default"
# データを登録
curl http://localhost:8981/solr/solrbook/update?commit=true --data-binary @sample-data/sample-books.json -H 'Content-type:application/json'
```

:::note info
上記のコレクション作成コマンドの意味としては、「solrbook」という名前のコレクションを作成、インデックスを２つのシャードに分割、１つのノードが管理できるシャード数の上限は２、１つのシャードを３つのSolrノードでレプリケートしなさい、という意味になります。
:::

３つのSolrノードでクラスタリングを構成しており、
![Screen Shot 2020-12-12 at 23.54.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/91e00ada-d5f7-6121-fb12-5c24f341f736.png)
当然、検索も出来ます。
![Screen Shot 2020-12-12 at 23.56.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/6ad302f1-5d11-f97c-6e01-87d5c6589748.png)
どのSolrノードに対して検索を実行しても同じ結果が返り、シャードがレプリケートされているので、いずれかのノードがダウンしても生存しているノードでデータ登録、検索が可能です。復旧後のデータ同期も自動で実施してくれます。

:::note info
ただし、シャードを管理しているノードが全台ダウンしてしまうと、該当シャードに含まれるデータが検索結果から欠損します。上記の例では全台ダウンしない限り欠損する事は無いですが、一部のノードのみでシャードを管理している場合はその可能性があります。
:::

色々と自動でやってくれるので、煩雑な作業をする事なくクラスタを管理できます。

クラスタにノードを追加するのも簡単です。一台新たにSolrノードを起動して、クラスタに追加します。ここでは敢えてシャード１のみ担当させてみます。

```
curl "http://localhost:8981/solr/admin/collections?action=ADDREPLICA&collection=solrbook&shard=shard1&node=192.168.208.8:8983_solr"
```
新しく追加したSolrノード（192.168.208.8）がシャード１に紐付きました。上記コマンドのシャード指定をshard2に変更してコマンドを実行すればシャード２も担当させる事が出来ます。
![Screen Shot 2020-12-13 at 0.12.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942040/9aa8717e-2eb3-3b91-537a-79b4b61d3951.png)

# まとめ
Apache SolrとElasticSearchの簡単な比較、Solrの起動、基本的な検索、少し高度な検索（ハイライト検索、ファセット検索）、クラスタリング機能（SolrCloud）など、基本の部分を纏めてみました。

細かい部分はかなり端折っているのと、ここでは紹介していない様々な機能がありますので、Solrを扱った事の無い人は、この記事をきっかけに興味を持って色々と調べてもらえると幸いです。
公式ドキュメントもかなり充実しているので、書籍等で基本的な概念を押さえて、必要な情報は公式ドキュメントを参照すれば大抵の事は分かるかと思います。

# 参考文献
[［改訂第3版］Apache Solr入門 ―オープンソース全文検索エンジン](https://gihyo.jp/book/2017/978-4-7741-8930-7)
[solrbook3](https://github.com/solrbook3)
[docker-solr](https://github.com/docker-solr/docker-solr)