タグ: Solr

SolrのJSON Facet API

検索条件が複雑になりがちなファセット検索ではJSON APIが威力を発揮します。

以下は、大阪市の施設情報のデータを使った区のフィールドによるファセット検索の例です。

$ curl -s http://localhost:8983/solr/test/query?omitHeader=yes -d '{
    "query" : "*:*",
    "offset" : 0,
    "limit" : 0,
    "facet" : {
        "areas" : {
            "type" : "terms",
            "field" : "area",
            "limit" : 3
        }
    }
}' |jq .
{
  "response": {
    "numFound": 9238,
    "start": 0,
    "docs": []
  },
  "facets": {
    "count": 9238,
    "areas": {
      "buckets": [
        {
          "val": "平野区",
          "count": 610
        },
        {
          "val": "北区",
          "count": 566
        },
        {
          "val": "中央区",
          "count": 509
        }
      ]
    }
  }
}

複数のクエリによるファセット検索もすっきりと書けます。
以下は、大阪市役所の直線距離(0m~300m, 300m~500m, 500m~1km)で作成したファセットです。

$ cat request.json
{
    query: "*:*",
    offset: 0,
    limit: 0,
    facet: {
        "300m": {
            type: "query",
            q: "{!frange l=0 u=0.3}geodist()",
        },
        "500m": {
            type: "query",
            q: "{!frange l=0.3 u=0.5}geodist()"
        },
        "1km": {
            type: "query",
            q: "{!frange l=0.5 u=1}geodist()"
        }
    }
    params: {
	pt: "34.6938,135.502002777777", /* 大阪市役所 */
        sfield: "address_p"
    }
}

$ curl -s http://localhost:8983/solr/test1/query?omitHeader=yes -d @request.json
{
  "response":{"numFound":9238,"start":0,"docs":[]
  },
  "facets":{
    "count":9238,
    "300m":{
      "count":28},
    "500m":{
      "count":27},
    "1km":{
      "count":185}}}

ファセットのネストも書けます。
以下の例では、上の例で作ったファセットのサブファセットとして施設タイプを指定しています。

$ cat request.json
{
    query: "*:*",
    offset: 0,
    limit: 0,
    facet: {
	    "300m": {
	        type: "query",
	        q: "{!frange l=0 u=0.3}geodist()",
	        facet: {
		    average_distance: "avg(geodist())",
		    types: {
		        type: "terms",
		        field: "type",
                        limit: 3
		    }
                }
	    },
	    "500m": {
	        type: "query",
	        q: "{!frange l=0.3 u=0.5}geodist()",
	        facet: {
		    average_distance: "avg(geodist())",
		    types: {
		        type: "terms",
		        field: "type",
                        limit: 3
		    }
                }
	    },
	    "1km": {
	        type: "query",
	        q: "{!frange l=0.5 u=1}geodist()",
	        facet: {
		    average_distance: "avg(geodist())",
		    types: {
		        type: "terms",
		        field: "type",
                        limit: 3
		    }
                }
        }
    }
    params: {
	    pt: "34.6938,135.502002777777", /* 大阪市役所 */
	    sfield: "address_p"
    }
}
$ curl -s http://localhost:8983/solr/test1/query?omitHeader=yes -d @request.json
{
  "response":{"numFound":9238,"start":0,"docs":[]
  },
  "facets":{
    "count":9238,
    "300m":{
      "count":28,
      "average_distance":0.18667141635662304,
      "types":{
        "buckets":[{
            "val":"駅・バス停",
            "count":8},
          {
            "val":"文化・観光",
            "count":5},
          {
            "val":"官公庁",
            "count":4}]}},
    "500m":{
      "count":27,
      "average_distance":0.39623014517757793,
      "types":{
        "buckets":[{
            "val":"駐車場・駐輪場",
            "count":12},
          {
            "val":"文化・観光",
            "count":6},
          {
            "val":"官公庁",
            "count":2}]}},
    "1km":{
      "count":185,
      "average_distance":0.7759130600495204,
      "types":{
        "buckets":[{
            "val":"駐車場・駐輪場",
            "count":62},
          {
            "val":"駅・バス停",
            "count":46},
          {
            "val":"文化・観光",
            "count":24}]}}}}

JSON API は複雑な検索リクエストをプログラム的に生成する場合に非常に有効なので、活用していきたいところです。


SolrのJSON Request API

Solrに検索リクエストを送る場合、検索用のパラメータはリクエストバラメータで指定するのが通常の方法です。

curl -s 'http://localhost:8983/solr/test/select?fl=id,type,name&q=area:中央区&fq=type:官公庁'

これとは別に、検索パラメータをJSON形式で指定する方式も用意されています。
上記の例はJSON方式だと以下のようになります。

curl -s http://localhost:8983/solr/test/query -d '
{
  "query" : "area:中央区",
  "filter" : "type:官公庁",
  "fields" : "id,type,name"
}'

ファイルで指定することもできます。

$ cat request.json
{
  "query" : "area:中央区",
  "filter" : "type:官公庁",
  "fields" : "id,type,name"
}
$ curl http://localhost:8983/solr/test/query -d @request.json

リクエストパラメータによる検索条件の指定とJSONによる指定は併用できます。同じパラメータに対してそれぞれで異なる値を指定した場合、基本的にはリクエストパラメータの方が優先されますが、複数の値を許すパラメータについては両方が使われます。

つまり、以下の2つは同じ内容のリクエストです。

curl 'http://localhost:8983/solr/test/query?json.limit=5&json.filter="area:中央区"' -d '
{
  "query" : "name:自動車",
  "limit" : 3
  "filter" : "type:官公庁",
  "fields" : "id,type,area,name"
}'
curl 'http://localhost:8983/solr/test/query' -d '
{
  "query" : "name:自動車",
  "limit" : 5
  "filter" : ["type:官公庁","area:中央区"],
  "fields" : "id,type,area,name"
}'

通常の検索方法と比べると、JSON方式には可読性や柔軟性の高さという利点があります。記号文字のエスケープやURLエンコーディングにもあまり気を使わなくて済みます。

また、Solr で使われている JSON 用の Noggit パーザの拡張機能により、JSONの標準から外れた便利な記法を利用できます。たとえば、シングルクォートの文字列を使えるのでフレーズ検索のときに便利です。

curl http://localhost:8983/solr/test/query -d '
{
  "query" : 'address:"中央区大手前"',
  "filter" : "type:官公庁",
  "fields" : "id,type,name"
}'

JSON Request API の仕様についてはリファレンスに詳しい記述があります。


SolrCloudをDockerで動かす

前回に引き続き、SolrをDockerで動かしてみます。今回はSolrとZooKeeperを別コンテナに分けてSolrCloudを構成します。

docker-compose.yml作成

まず、以下の内容の docker-compose.yml を作成します。

ZooKeeperの公式コンテナイメージの情報はこちらです。

正しく動作させるためのポイントがいくつかあります。

Solrの起動オプションを使ってコンテナのZooKeeperを指定

そのまま起動するとSolrは同梱のZooKeeperを使ってしまうので、docker-compose でコマンドラインオプションを指定してコンテナのZooKeeperを参照するように指定しています。

ZooKeeperの4lw.commands.whitlistの指定

“ZOO_4LW_COMMANDS_WHITELIST: mntr,conf,ruok”を指定します。これをしないと、管理GUIの”Cloud” → “ZK Status”の画面で以下のエラーになりZooKeeperの状態を確認できません。

Could not execute ruok towards ZK host zoo1:2181. Add this line to the 'zoo.cfg' configuration file on each zookeeper node: '4lw.commands.whitelist=mntr,conf,ruok'. See also chapter 'Setting Up an External ZooKeeper Ensemble' in the Solr Reference Guide.

ZooKeeperのバージョンを3.5.7で指定する

ZooKeeperの最新イメージは3.6系になっていますが、これを使うと、先程の”Cloud”→”ZK Status”の画面で4lw.commands.whitelistの指定をしていても以下のエラーになります。

For input string: "null"

いろいろ調べてみると Solr 8.5 と ZooKeeper 3.6 の組み合わせで発生するという情報が見つかりました。この問題を回避するために ZooKeeper のバージョン 3.5.7 を指定しています。

起動

$ docker-compose up -d

http://localhost:8983/solr/ で管理GUIにアクセスできます。

起動後

いつもの動作確認をしてみます。

設定ファイルアップロード

$ (cd configsets/_default/conf && zip -r - *) | curl -X POST --header "Content-Type:application/octet-stream" --data-binary @- "http://localhost:8983/solr/admin/configs?action=UPLOAD&name=test"

コレクション作成

$ curl -s "http://localhost:8983/solr/admin/collections?action=CREATE&name=test&maxShardsPerNode=2&numShards=2&replicationFactor=3&collection.configName=test"

ドキュメント投入

$ bin/post -c test example/exampledocs/manufacturers.xml

検索

$ curl -s "http://localhost:8983/solr/test/select?q=*%3A*&rows=1"
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":64,
    "params":{
      "q":"*:*",
      "rows":"1"}},
  "response":{"numFound":11,"start":0,"maxScore":1.0,"docs":[
      {
        "id":"apple",
        "compName_s":"Apple",
        "address_s":"1 Infinite Way, Cupertino CA",
        "_version_":1670655179588894720}]
  }}

SolrをDockerで動かす

SolrをDockerで稼働させるにあたっては、公式に提供されているコンテナイメージを利用することができます。ただし、このページの説明は非常に簡素なので、利用法については GitHub の README を参照した方が分かりやすいと思います。

起動

docker run -d -p 8983:8983 --name my_solr solr solr-precreate gettingstarted

サンプルデータの投入

docker exec -it my_solr post -c gettingstarted example/exampledocs/manufacturers.xml

データの確認

$ curl 'http://localhost:8983/solr/gettingstarted/select?q=*%3A*&rows=1'
{
  "responseHeader":{
    "status":0,
    "QTime":1,
    "params":{
      "q":"*:*",
      "rows":"1"}},
  "response":{"numFound":11,"start":0,"docs":[
      {
        "id":"adata",
        "compName_s":"A-Data Technology",
        "address_s":"46221 Landing Parkway Fremont, CA 94538",
        "_version_":1670098040008998912}]
  }}

コンテナの中はどうなっているか

コンテナに入って中を確認してみます。

$ sudo docker exec -it my_solr /bin/bash
solr@ef3c76ff156a:/opt/solr-8.5.2$ pwd
/opt/solr-8.5.2
solr@ef3c76ff156a:/opt/solr-8.5.2$ ls
CHANGES.txt  LICENSE.txt  LUCENE_CHANGES.txt  NOTICE.txt  README.txt  bin  contrib  dist  example  licenses  server

Solr は /opt/solr-8.5.2 にインストールされていて、solr ユーザのホームディレクトリになっていることが分かります。

solr@ef3c76ff156a:/opt/solr-8.5.2$ cat /etc/os-release 
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
solr@ef3c76ff156a:/opt/solr-8.5.2$ dpkg --list|grep solr

OS は Debian buster です。Solr パッケージを使っている訳ではないです。

solr@ef3c76ff156a:/opt/solr-8.5.2$ find /var/solr -type d
/var/solr
/var/solr/data
/var/solr/data/gettingstarted
/var/solr/data/gettingstarted/data
/var/solr/data/gettingstarted/data/snapshot_metadata
/var/solr/data/gettingstarted/data/tlog
/var/solr/data/gettingstarted/data/index
/var/solr/data/gettingstarted/conf
/var/solr/data/gettingstarted/conf/lang
/var/solr/data/userfiles
/var/solr/data/filestore
/var/solr/logs

データは /var/solr 以下に格納されます。

solr@ef3c76ff156a:/opt/solr-8.5.2$ ls /opt/docker-solr/scripts/
docker-entrypoint.sh  init-var-solr  oom_solr.sh  precreate-core  run-initdb  solr-create  solr-demo  solr-fg  solr-foreground	solr-precreate	start-local-solr  stop-local-solr  wait-for-solr.sh  wait-for-zookeeper.sh

コンテナ外からの操作で使われるスクリプトが /opt/docker-solr/scripts 以下に置かれています。起動時に使った solr-precreate はここに置かれているものです。

solr-precreate の中身は、/opt/solr/server/solr/configsets/_defaults の設定ファイルを指定されたコアのディレクトリ(上の例なら /var/solr/data/gettingstarted/conf)にコピーしてコアを作成してから Solr を起動するというものになっています。

SolrのデータをホストOS側に置く

データサイズが気になる場合や、インデックスの使い回しをしたい場合などには、SolrのデータをホストOS側に置くこともできます。

$ mkdir solrdata
$ sudo chown 8983:8983 solrdata/
$ sudo docker run -d -v "$PWD/solrdata:/var/solr" -p 8983:8983 --name my_solr2 solr solr-precreate gettingstarted

起動前に、ホスト側に作ったデータディレクトリの所有者をコンテナのsolrユーザ(8983:8983)に合わせておくことが必要です。

起動後

起動後はホストOS側から localhost:8983 でアクセスできるので、管理GUIなりウェブAPIなりで管理することができます。


SolrCloudのリーダー再選出の動作を確認する

はじめに

前回の記事ではシャードを構成する複数のレプリカの中からリーダーが選出される仕組みを解説しました。この記事では、実際に動いているSolrCloudを使って、特定のノードがダウンしたときにリーダーが切り替わる動作を確認してみます。

SolrCloudの構成

  • サーバ3台、サーバ毎に Solr 1プロセス
  • コレクション名 test のコレクションを作成
  • test コレクションは shard1 と shard2 の2つのシャードを含む
  • 各シャードはそれぞれ3つのレプリカ(それぞれ別のSolrノードで動く)を含む
コア名ノードシリアル番号備考
core_node3172.19.0.7:898512リーダー
core_node5172.19.0.6:898314
core_node7172.19.0.5:898413
shard1
コア名ノードシリアル番号備考
core_node9172.19.0.7:898512リーダー
core_node11172.19.0.6:898314
core_node12172.19.0.5:898413
shard2

リーダーがダウンしたとき

ノード 172.19.0.7:8985 (core_node3, core_node9)を落としてみます。

それまでのリーダー(シリアル番号12)の次に若い番号(13)を持つcore_node7およびcore_node12が新しいリーダーに選ばれています。

コア名ノードシリアル番号備考
core_node3172.19.0.7:898512ダウン
core_node5172.19.0.6:898314
core_node7172.19.0.5:898413リーダー
shard1
コア名ノードシリアル番号備考
core_node9172.19.0.7:898512ダウン
core_node11172.19.0.6:898314
core_node12172.19.0.5:898413リーダー
shard2

ダウンしたノードが復帰したとき

ダウンさせていた 172.19.0.7:8985 (core_node3, core_node9)を復帰させます。

リーダーに変更は無く、core_node3とcore_node9は新しい番号15をそれぞれ割り当てられます。

コア名ノードシリアル番号備考
core_node3172.19.0.7:898515復帰
core_node5172.19.0.6:898314
core_node7172.19.0.5:898413リーダー
shard1
コア名ノードシリアル番号備考
core_node9172.19.0.7:898515復帰
core_node11172.19.0.6:898314
core_node12172.19.0.5:898413リーダー
shard2

リーダーを意図的に変更する

まずcore_node3をpreferredLeaderに指定します。

curl 'http://172.19.0.6:8983/solr/admin/collections?action=ADDREPLICAPROP&shard=shard1&collection=test&replica=core_node3&property=preferredLeader&property.value=true'

REBALANCELEADERSを実行。

curl 'http://172.19.0.6:8983/solr/admin/collections?action=REBALANCELEADERS&collection=test'
{
  "responseHeader":{
    "status":0,
    "QTime":3053},
  "Summary":{
    "Success":"All active replicas with the preferredLeader property set are leaders"},
  "successes":{
    "shard1":{
      "status":"success",
      "msg":"Successfully changed leader of slice shard1 to core_node3"}}}

shard1のリーダーがcore_node3に変更されました。

FORCELEADER

FORCELEADERは障害等何らかの理由でリーダー不在の状態ができてしまった場合に強制的にリーダーを割り当てるためのコマンドです。リーダーが居ない状態を意図的に作るのは難しいので、正常な状態のシャードに対して FORCELEADER を実行するとどうなるか試してみました。

curl 'http://172.19.0.6:8983/solr/admin/collections?action=FORCELEADER&collection=test&shard=shard2'
{
  "responseHeader":{
    "status":500,
    "QTime":35},
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.common.SolrException"],
    "msg":"The shard already has an active leader. Force leader is not applicable. State: shard2:{
(略)
    "code":500}}

指定されたシャードにはリーダーが居るのでFORCELEADERの実行はできませんと怒られてしまいました。

おわりに

SolrCloudのクラスタはZooKeeperと連携していて、リーダーがダウンしたら自動的に新しいリーダーが選ばれてなるべくダウンタイムが小さくなるように工夫されている、という漠然とした理解から一歩進むために、具体的なリーダー選出のロジックを調べました。ZooKeeperの分散アプリケーションのコーディネート機能を使って案外シンプルなロジックで実装されていることが分かりました。この理解を持った上でリーダー調整用のAPIを上手に使えば、稀に発生するクラスタ異常にもうまく対処することができそうです。