Solr 9 でも DataImportHandler を使いたい

# はじめに

[情報検索・検索技術 Advent Calendar 2022](https://adventar.org/calendars/7389) の 10 日目の記事です。前回は Jun Ohtani さんの「ElasticsearchのアーキテクチャとStateless / Serverless」でした。
ついに検索エンジンもサーバレスでオートスケールしてくれる時代になってきましたね。

今回はタイトル通り Solr 9 から独立プロジェクトとなった Data Import Handler を Solr 9 で使う方法について調べてみました。

試してみたい方は、サンプルコードを書いてみましたので参考にしてください。

https://github.com/Sashimimochi/solr-tutorial

本記事、サンプルコード共に私の経験が浅く、誤りや冗長な情報がところどころあるかと思います。ご容赦ください。

:::note info
2023/4/21に公式のDIHリポジトリが9.0には対応されました。
https://github.com/SearchScale/dataimporthandler/pull/33
:::

# 衝撃の事件

2022年5月12日に Apache Solr 9.0.0 が 公開されました。
2019年3月14日以来、3年ぶりとなるメジャーバージョンアップです。

また、つい先日の2022年11月21日には Solr 9.1.0 が公開されました。

Solr 9 では、個人的に切望していた DenseVectorField フィールド型と K近傍法（KNN）クエリーパーサーによる密ベクトルニューラル検索が追加されています。
前々回の本アドベントカレンダーでも取り上げられている通り、今注目を集めている検索手法といえるでしょう。

これまで [faiss](https://github.com/facebookresearch/faiss) や [annoy](https://github.com/spotify/annoy) といった Python 製の近似近傍探索ライブラリを使ってベクトル検索エンジンを組む必要があったものが、運用知見のある Solr でできるようになると思うと期待が高まります。

そのほかにもパフォーマンス面の向上などさまざまなアップデートがされています。

https://solr.apache.org/guide/solr/9_0/upgrade-notes/major-changes-in-solr-9.html

https://solr.apache.org/docs/9_1_0/changes/Changes.html

https://www.rondhuit.com/apache-solr-9-0-0.html

https://www.rondhuit.com/apache-solr-9-1-0.html

一見、良いこと尽くめのアップデートに見えたのですが、一つ衝撃的なアップデートが混じっていました。

> The Data Import Handler (DIH) is an independent project now; it is no longer a part of Solr.

> Data Import Handler (DIH) は Solr の管理下から離れ、独立したプロジェクトとなった。

そう、Solr 単体の機能ではインデックスの更新ができなくなってしまったのです！

これまでは当たり前のようにあったこの画面は、もう Solr 9 にはありません。
![スクリーンショット 2022-12-05 154717.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199063/00fa13d1-b5a8-a3f7-146a-d19d411169d7.png)
独立プロジェクトとなった DIH は、3rd party パッケージとしてリポジトリも分離されました。

https://github.com/rohitbemax/dataimporthandler

これまで DIH に頼りきりだった身としては、早急に分離後の DIH の使い方を調べるか、従来の DIH を捨てて自作するかの選択を迫られました。

過去に1度だけ自作したことはありますが、結果としてがっつりサービスに寄せた作りになりました。
また、パフォーマンス面でのチューニングも必要な上、bug fix や feature 機能の追加など保守・追従もしんどいです。

これまでどっぷり依存していた身としては、サービスごとに importhandler を自作するのは気が重く、そんな簡単に自作に踏み切れません。

ということで、分離後の DIH の使い方調査と相成りました。

:::note info
後述しますが、実際問題は自作を余儀なくされる可能性が高いかと思っています。
:::

# 導入手順

基本的には DIH の [README](https://github.com/rohitbemax/dataimporthandler/blob/master/README.md) の Installing and running に沿ってそのまま行えばできます。

ただ、致命的な問題なのが、本家のリポジトリが Solr 9 に追従できていません。

https://github.com/rohitbemax/dataimporthandler/issues/32

PRは出されているようですが、2022年8月以降、更新の兆しがないまま止まっています。

https://github.com/rohitbemax/dataimporthandler/pull/33

PRを出された方は「仕事ではSolrを使わなくなったので、このPRは良しなにしてください」と言っていることから、今のところ進展の望み薄です。

仕方がないので、[PR](https://github.com/rohitbemax/dataimporthandler/pull/33)のブランチに一番近そうで、怪しい変更も入っていなさそうな有志の方のブランチを使わせてもらうことにします。

https://github.com/nightvixen/dataimporthandler/tree/branch_9x

:::note warn
非公式の fork リポジトリを使用しています。利用の際はご自身の責任で実施してください。
:::

インストール手順は本家のリポジトリから変わっていないので、本家の README に従って行います。
[README](https://github.com/rohitbemax/dataimporthandler/blob/master/README.md) を見るに、**Solr のパッケージ管理機能**を使ったインストール手順が記載されています。

パッケージ管理機能は Solr 8.4 から追加された機能で、Solr 起動後に動的にパッケージをインストールできるようにしたもののようです。

https://solr.apache.org/guide/8_4/package-manager.html

パッケージ管理機能を使えば、従来の

1. `.jar` ファイルを特定の場所に置く
1. `solrconfig.xml` で読み込む `.jar` ファイルの場所を指定する
1. 複数ノードあるなら各ノード分やる

といった煩雑な作業いらずで、コマンド操作だけで複数ノードへのプラグインのインストールやアップデートを一括で操作できるようになります。

## インストール作業開始

まずは、パッケージ管理機能を有効化します。
デフォルトでは無効になっているので、起動オプションで指定して有効化します。

```bash
$ bin/solr -c -Denable.packages=true
```

docker-compose ではこのように定義します。

```diff_yaml:docker-compose.yml
services:
  solr_node1:
    container_name: solr_node1
    image: solr:9.0.0
+   command:
+     - "-Denable.packages=true" # ←ココ
```

次にインストールするパッケージのリポジトリを追加します。
Solr のパッケージには [npm](https://www.npmjs.com/) や [PyPI](https://pypi.org/) のような特定のパッケージレジストリは存在しないので、適当なリポジトリのパスを直接指定します。

今回は https://github.com/nightvixen/dataimporthandler/tree/branch_9x/repo 配下にあるパッケージを利用するので、これを指定します。

```bash
# コンテナで環境構築したならコンテナ内に入っておく
$ docker-compose exec solr_node1 /bin/bash
# add repository
$ bin/solr package add-repo data-import-handler \
"https://raw.githubusercontent.com/nightvixen/dataimporthandler/branch_9x/repo"
Added repository: data-import-handler
```

パッケージが取得できれば良いので、取得元は GitHub でなくても適当なローカルサーバでも良いです。
`repository.json` ファイルがあれば、ウェブサーバ上の特定のディレクトリを Solr はパッケージレジストリとして認識するようです。

パッケージの取得元が登録出来たら、利用可能なパッケージを確認してみます。

```bash
$ bin/solr package list-available
Available packages:
-----
data-import-handler             A utility to import documents from a database system using Solr
        Version: 9.0.0
```

ここで表示されるバージョンは `repository.json` に書かれている `"version"` になります。

インストール対象のパッケージが確認できたら、インストールしてみましょう。

```bash
$ bin/solr package install data-import-handler
Posting manifest...
Posting artifacts...
Executing Package API to register this package...
Response: {"responseHeader":{
    "status":0,
    "QTime":99}}
data-import-handler installed.
```

バージョンを指定してインストールすることもできます。

```bash
$ bin/solr package install data-import-handler:9.0.0
```

このとき、Solrに対応するバージョンのパッケージがない場合は、インストールはできてもデプロイ時にエラーになります。

無事インストールができたら、従来通り `data-config.xml` を書いていきます。
[README](https://github.com/rohitbemax/dataimporthandler/blob/master/README.md) だと、後で作ってリロードになっていますが、ここでは、config のデプロイ前の想定なので、先に作っておきます。

```xml:data-config.xml
<dataConfig>
    <dataSource
        name="ldgourmet"
        type="JdbcDataSource"
        driver="org.mariadb.jdbc.Driver"
        url="jdbc:mysql://mysql:3306/ldgourmet?useUnicode=true&amp;characterEncoding=UTF8&amp;useOldAliasMetaBehavior=true&amp;zeroDateTimeBehavior=convertToNull&amp;tinyInt1isBit=false"
        user="solrtutorial"
        password="solrtutorial"
        batchSize="-1" />
    <document>
        <entity
            name="tutorial" dataSource="ldgourmet"
            transformer="RegexTransformer"
            pk="id"
            
            query="
                SELECT *
                FROM restaurants" >
        </entity>
    </document>
</dataConfig>
```

このとき、取り込み元のDBがMySQLであっても、`driver` は `org.mariadb.jdbc.Driver` にすることに注意してください。
これはパッケージ付属のドライバーが mariadb のものになっているためです。

```json:repository.json
        {
          "version": "9.0.0",
          "date": "2022-06-14",
          "artifacts": [
            {
              "url": "data-import-handler-9.0.0.jar",
              "sig": "hFnltTf+qvJipJxo0EDUeNYPq2EK6mlBgF6BQoBV93pV/EtyuJp5WKk2zXSzjDYApPhnsDLybjXaDSwI3B5RsA=="
            },
            {
              "url": "mariadb-java-client-2.6.0.jar",
              "sig": "LvJwQiZwqGQgeV0yXe2zsGcWFg8Wcbt5J8b0A6NJwIIDYfaLDbLOxFmvUQ8f1U038OUsZMmDAjWJySqofzUZfQ=="
            }
```

また、`solrconfig.xml` に `DataImportHandler` の項があると collection の作成に失敗するので忘れずに削除しておきましょう。

```diff_xml:solrconfig.xml
 <config>
-    <luceneMatchVersion>8.6.2</luceneMatchVersion>
+    <luceneMatchVersion>9.0.0</luceneMatchVersion>
 
@@ -40,20 +40,20 @@
 
-    <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
-        <lst name="defaults">
-            <str name="config">data-config.xml</str>
-        </lst>
-    </requestHandler>
-
     <requestHandler name="/select" class="solr.SearchHandler">
         <lst name="defaults">
             <str name="echoParams">${echoParams.default:all}</str>
```

`data-config.xml` が書けたら、config をデプロイして collection を作成します。
collection の作成は UI 上から行っても構いません。

```bash
# config のデプロイ
$ server/scripts/cloud-scripts/zkcli.sh -zkhost zookeeper1:2181 -cmd upconfig -confdir /opt/solr/server/solr/configsets/ldgourmet/conf -confname ldgourmet_conf
INFO  - 2022-12-07 07:56:30.703; org.apache.solr.common.cloud.ConnectionManager; Waiting up to 30000ms for client to connect to ZooKeeper
INFO  - 2022-12-07 07:56:30.720; org.apache.solr.common.cloud.ConnectionManager; zkClient has connected
INFO  - 2022-12-07 07:56:30.720; org.apache.solr.common.cloud.ConnectionManager; Client is connected to ZooKeeper
# collection の作成
$ curl "http://localhost:8983/solr/admin/collections?action=CREATE&name=ldgourmet&collection.configName=ldgourmet_conf&numShards=1&replicationFactor=1&maxShardsPerNode=1"
{
  "responseHeader":{
    "status":0,
    "QTime":1627},
  "success":{
    "172.22.0.4:8983_solr":{
      "responseHeader":{
        "status":0,
        "QTime":1132},
      "core":"ldgourmet_shard1_replica_n1"}}}
```

collection ができたら、インストールしたパッケージを collection にデプロイします。

```bash
$ bin/solr package deploy data-import-handler -y -collections ldgourmet
Executing {"add-requesthandler":{"name":"/dataimport","class":"data-import-handler:org.apache.solr.handler.dataimport.DataImportHandler","defaults":{"config":"data-config.xml"}}} for path:/api/collections/ldgourmet/config
Executing http://localhost:8983/api/collections/ldgourmet/config/requestHandler?componentName=/dataimport&meta=true for collection:ldgourmet
{
  "responseHeader":{
    "status":0,
    "QTime":1},
  "config":{"requestHandler":{"/dataimport":{
        "name":"/dataimport",
        "class":"data-import-handler:org.apache.solr.handler.dataimport.DataImportHandler",
        "defaults":{"config":"data-config.xml"},
        "_packageinfo_":{
          "package":"data-import-handler",
          "version":"9.0.0",
          "files":["/package/data-import-handler/9.0.0/data-import-handler-9.0.0.jar",
            "/package/data-import-handler/9.0.0/mariadb-java-client-2.6.0.jar"],
          "manifest":"/package/data-import-handler/9.0.0/manifest.json",
          "manifestSHA512":"bb464c5aa39bebbe63365b0a80ecd1f82636b12120a658292325e0cc2de896ce8b8444edaae1e5f87f331db3f8f59c352401c0fa440879706a0d9fec9aa8c528"}}}}}

Actual: 9.0.0, expected: 9.0.0
Deployed on [ldgourmet] and verified package: data-import-handler, version: 9.0.0
Deployment successful
```

もし、Solr に対応するバージョンのパッケージでない場合はエラーになります。
例えば、Solr 9.1.0 に Solr 9.0.0 用の DIH をデプロイしようとするとこのようになります。
```bash
$ bin/solr package deploy data-import-handler -y -collections ldgourmet
Version incompatible! Solr version: 9.1.0, package version constraint: 9.0
```

パッケージがデプロイ対象の Solr に対応しているかは、`repository.json` の `version-constraint` と突合しているようです。
```json:repository.json
          "manifest": {
              "version-constraint": "9.0",
```

パッケージがデプロイ出来たら全件取り込みを実施してみましょう。
UI 上からはできなくなっているので、CLI から実行してやります。

```bash
$ curl "http://localhost:8983/solr/ldgourmet/dataimport?command=full-import&entity=tutorial"
{
  "responseHeader":{
    "status":0,
    "QTime":78},
  "initArgs":[
    "defaults",[
      "config","data-config.xml"]],
  "command":"full-import",
  "status":"idle",
  "importResponse":"",
  "statusMessages":{}}
```

問題なくインデックスができたら、検索してみましょう。

```bash
$ curl "http://localhost:8983/solr/ldgourmet/select?indent=true&q=*:*&echoParams=none"
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":1},
  "response":{"numFound":214237,"start":0,"numFoundExact":true,"docs":[
      {
        "id":0},
      {
        "id":2},
      {
        "id":3},
      {
        "id":4},
      {
        "id":5},
      {
        "id":6},
      {
        "id":7},
      {
        "id":8},
      {
        "id":9},
      {
        "id":10},
      {
        "id":11},
      {
        "id":12},
      {
        "id":13},
      {
        "id":14},
      {
        "id":15},
      {
        "id":16},
      {
        "id":17},
      {
        "id":18},
      {
        "id":19},
      {
        "id":20}]
  },
  "facet_counts":{
    "facet_queries":{},
    "facet_fields":{},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}
```
ちゃんとドキュメントがヒットしましたね。

管理画面からもインデックス取り込みができていることが確認できます。
![スクリーンショット 2022-12-07 165816.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199063/462258fc-3bbd-a33b-65ed-909ef973779a.png)

無事、Solr 9 でも DIH を使ってインデックス更新ができました。よかったよかった。

# おわりに

Solr 9 から DIH が 3rd party パッケージに分離して焦りましたが、インストール手順はしっかり確立されているようです。
あとは DIH のリポジトリがちゃんと Solr に追従してくれれば、引き続き DIH を使い続けられそうです。
とはいえ、現状の腰の重さを見るに楽観はしない方がいいかもしれません。

機能面/性能面ともに、 Solr 9 でかなり改善されているようなので、私と同じように DIH マターで二の足を踏んでいる方のためにもアップデートが待ち遠しいです。(自分で PR 書ければベストなんでしょうけどね...)

それでは、みなさんの良き検索エンジンライフを願って。

## サンプルコード

https://github.com/Sashimimochi/solr-tutorial