タグ: Solr

jpsコマンドでSolrプロセスが表示されない

はじめに

あるとき、最近の Solr は jsp でプロセスが表示されないことに気づきました。Solr 5 の頃は表示されていた記憶があります。気になったので原因を調べてみました。

過去のSolrでは表示される

手元にある過去のバージョンの Solr を起動して試してみました。start.jar が Solr のプロセスです。

Solr 5.5.5

$ jps -l
12315 sun.tools.jps.Jps
12077 start.jar

Solr 7.5.0

$ jps -l
12550 start.jar
12668 sun.tools.jps.Jps

Solr 8.1.0

$ jps -l
13052 sun.tools.jps.Jps

Solr 8 あたりでこの変化が起こったようです。

jps で Java プロセスが表示される仕組み

jpsはJavaプロセスが起動するときに作成される /tmp/hsperfdata_USERNAME/PID というファイルの情報を利用して表示されます。たとえば java1 ユーザが起動した PID 13542 のプロセスがあれば /tmp/hsperfdata_java1/13542 というファイルになります。

hsperfdata ファイルは jps に限らず、jstat など Java のツール群で共通して使われるものです。上では /tmp と書きましたが、 システムプロパティ java.io.tmpdir で指定される作業ディレクトリ上に作られます。

jps が Java プロセスを見失うのはどういう場合か

java.io.tmpdir が変更された場合

この場合、java プロセスが作る hsperfdata ファイルの場所が jps の想定する場所と異なるために jps からは見つけられなくなります。

hsperfdata ファイルが作成されなくなる起動オプションが指定された場合

そもそも hsperfdata が作られなければ jps からは見つけられません。 hsperfdata ファイル生成を抑制するオプションとして、 -XX:-UsePerfData や -XX:+PerfDisableSharedMem があります。

Solr 起動時の java コマンドの起動オプション

-XX:+PerfDisableSharedMem が指定されていました。

$ ./solr start -V -p 8984
Using Solr root directory: /home/java1/fsw/solr-8.9.0
Using Java: /home/java1/.sdkman/candidates/java/current/bin/java
openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment 18.9 (build 11.0.6+10)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.6+10, mixed mode)

Starting Solr using the following settings:
    JAVA            = /home/java1/.sdkman/candidates/java/current/bin/java
    SOLR_SERVER_DIR = /home/java1/fsw/solr-8.9.0/server
    SOLR_HOME       = /home/java1/fsw/solr-8.9.0/server/solr
    SOLR_HOST       = 
    SOLR_PORT       = 8984
    STOP_PORT       = 7984
    JAVA_MEM_OPTS   = -Xms512m -Xmx512m
    GC_TUNE         = -XX:+UseG1GC -XX:+PerfDisableSharedMem -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=250 -XX:+UseLargePages -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent
    GC_LOG_OPTS     = -Xlog:gc*:file=/home/java1/fsw/solr-8.9.0/server/logs/solr_gc.log:time,uptime:filecount=9,filesize=20M
    SOLR_TIMEZONE   = UTC
    SOLR_OPTS       = -Xss256k


Waiting up to 180 seconds to see Solr running on port 8984 [|]  
Started Solr server on port 8984 (pid=15099). Happy searching!

試しに solr 起動スクリプトで -XX:+PerfDisableSharedMem を指定している箇所をコメントアウトしてみると、起動後に jps で Solr 8.9.0 のプロセスが表示されるようになりました。

SolrとNutchを組み合わせてウェブサイトのインデックスを作成する

はじめに

Apache Nutch はオープンソースのウェブクローラです。Nutch でクロールした結果を Solr でインデックスするという連携が簡単にできるようになっています。

Nutch のインストール

ダウンロード

https://www.apache.org/dyn/closer.cgi/nutch/ から apache-nutch-1.18-bin.tar.gz をダウンロードします。

展開

$ tar zxf apache-nutch-1.18-bin.tar.gz
$ cd apache-nutch-1.18

設定ファイル

設定ファイルは conf/nutch-default.xml と conf/nutch-site.xml です。 nutch-default.xml にはコメントも詳しく書かれているので、読めば設定の意味を理解できます。変更したい箇所だけ nutch-site.xml に記述します。

<configuration>
  <property>
    <name>http.agent.name</name>
    <value>Splout Nutch Spider</value>
  </property>
</configuration>

同一サイトへの連続アクセスの際のディレイは fetcher.server.delay で5秒と設定されているので、今回はこのまま使います。robot.txt の有るサイトではその指示に従うようです。

クローリング対象の設定

urls/seed.txt というファイルに1行1URLでクローリングの起点となるURLを記述します。

mkdir urls
echo https://blog.splout.co.jp/ > urls/seed.txt

ドキュメントに含まれるリンクを次々と辿っていくわけですが、特定のサイト以外のURLはクローリングしないように指示することができます。そのためのファイルが conf/regex-urlfilter.txt です。

今回は blog.splout.co.jp だけを対象としますので、regex-urlfilter.txt の最後の行

+.

+^https?://blog\.splout\.co\.jp/

に変更します。

クローリング実行

Nutch にはクローリング実行用のスクリプトも含まれていますが、ここではチュートリアルに従って1ステップずつ実行してみます。

$ bin/nutch inject crawl/crawldb urls
$ bin/nutch generate crawl/crawldb crawl/segments
$ s1=`ls -d crawl/segments/2* | tail -1`
$ echo $s1
crawl/segments/20210925115243
$ bin/nutch fetch $s1
$ bin/nutch updatedb crawl/crawldb $s1

ここまでで1ターン終わりです。

inject (URLの起点をDBに入れる) → generate (セグメント(1回のfetch処理でアクセスされるURLの集合)を作成) → fetch (ウェブコンテンツの取得) → コンテンツデータベースの更新

という流れです。

ここまでで集まったURLを元にして次のターンを実行します。

$ bin/nutch generate crawl/crawldb crawl/segments -topN 100
$ s2=`ls -d crawl/segments/2* | tail -1`
$ bin/nutch fetch $s2
$ bin/nutch parse $s2
$ bin/nutch updatedb crawl/crawldb $s2

スクリプトを使えばこれを自動で回してくれるわけです。

Invertlinks作成

どのページがどこからリンクされているかの情報を作ります。

$ bin/nutch invertlinks crawl/linkdb -dir crawl/segments

Solrセットアップ

configsets/_default をベースにして schema.xml だけ Nutch で用意されているものを使います。

$ tar zxf solr-8.5.1.tgz
$ cd solr-8.5.1
$ mkdir server/solr/configsets/nutch
$ cp -r server/solr/configsets/_default/* server/solr/configsets/nutch/
$ cp ../apache-nutch-1.18/plugins/indexer-solr/schema.xml server/solr/configsets/nutch/conf/
$ rm server/solr/configsets/nutch/conf/managed-schema 
$ bin/solr start
$ bin/solr create -c nutch -d server/solr/configsets/nutch/conf/

Solrインデックス作成

インデックス作成に関する設定は conf/index-writers.xml にあります。Solrに関する設定は以下のとおりです。

  <writer id="indexer_solr_1" class="org.apache.nutch.indexwriter.solr.SolrIndexWriter">
    <parameters>
      <param name="type" value="http"/>
      <param name="url" value="http://localhost:8983/solr/nutch"/>
      <param name="collection" value=""/>
      <param name="weight.field" value=""/>
      <param name="commitSize" value="1000"/>
      <param name="auth" value="false"/>
      <param name="username" value="username"/>
      <param name="password" value="password"/>
    </parameters>
    <mapping>
      <copy>
        <!-- <field source="content" dest="search"/> -->
        <!-- <field source="title" dest="title,search"/> -->
      </copy>
      <rename>
        <field source="metatag.description" dest="description"/>
        <field source="metatag.keywords" dest="keywords"/>
      </rename>
      <remove>
        <field source="segment"/>
      </remove>
    </mapping>
  </writer>

インデックス作成を実行します。

$ bin/nutch index crawl/crawldb/ $s2 -filter -normalize -deleteGone
(略)
Indexing 67/67 documents
Deleting 0 documents
Indexer: number of documents indexed, deleted, or skipped:
Indexer:     67  indexed (add/update)
Indexer: finished at 2021-09-25 15:35:30, elapsed: 00:00:01

オプションの意味は以下の通りです。

  • filter: 設定済みのURLフィルタを使って不要なURLを弾く
  • normalize: インデックス前にURLを正規化する
  • deleteGone: 404になったページや内容が重複するページなどについてはインデックスからの削除のリクエストを出す

検索してみる

“solr”で検索してみます。

$ curl -s 'http://localhost:8983/solr/nutch/select?omitHeader=true&rows=1&q=content:solr'
{
  "response":{"numFound":26,"start":0,"docs":[
      {
        "tstamp":"2021-09-25T05:42:51.082Z",
        "digest":"7268d16a01450b9d925824316fa1a7e4",
        "boost":0.009592353,
        "id":"https://blog.splout.co.jp/12174/",
        "title":"Solrの記事リスト(〜2020年12月) | SPLOUT BLOG",
        "url":"https://blog.splout.co.jp/12174/",
        "content":"Solrの記事リスト(〜2020年12月) | SPLOUT BLOG\nWORKS\nCOMPANY\nRECRUIT\nBLOG\nCONTACT\ntoggle navigation\nWORKS\nCOMPANY\nRECRUIT\nBLOG\nPRIVACY POLICY\nSECURITY POLICY\nCONTACT\nSolrの記事リスト(略)",
        "_version_":1711854563263250432}]
  }}

記事IDとしてURLが使われていることが分かります。管理用のいくつかのフィールドを除くと、記事に関するフィールドは以下の通りです。

  • title: 記事タイトル
  • url: 記事URL
  • content: 記事本文

これらを変更する場合は、Nutch 側の conf/index-writer.xml と Solr 側の schema.xml を変更することになります。

[Solr] Atomic Updates による部分的な更新

Solrの更新は基本的に上書きです。更新する必要の無いフィールドの値もすべて指定して更新処理を呼び出します。たとえば以下のようなドキュメントがインデックスされているとします。

{"id":"1", "title":"タイトル1", "body":"本文1", "remarks":"備考1"}

remaksがrequired=”false”なフィールドだとすると、

{"id":"1", "title":"タイトル2", "body":"本文2"}

で更新すると

{"id":"1", "title":"タイトル2", "body":"本文2", "remarks":"備考1"}

ではなく

{"id":"1", "title":"タイトル2", "body":"本文2"}

になります。

ただし、更新対象となるドキュメントのすべてのフィールドが stored=”true” または docValues=”true” である場合には特定のフィールドだけを部分的に更新することができます。これを Atomic Updates と呼びます。

以下のスキーマ定義で

    <field name="type"     type="string"  indexed="true" stored="false" multiValued="false" docValues="true" useDocValuesAsStored="true"/>
    <field name="area"     type="string"  indexed="true" stored="valse" multiValued="false" docValues="true" useDocValuesAsStored="true"/>
    <field name="name"     type="text_ja"  indexed="true" stored="true" multiValued="false"/>
    <field name="address"  type="string"  indexed="true" stored="valse" multiValued="true" docValues="true" useDocValuesAsStored="true"/>
    <field name="address_p"  type="location"  indexed="true" stored="false" multiValued="false" docValues="true" useDocValuesAsStored="true"/>
    <field name="score"     type="pint"  indexed="true" stored="false" multiValued="false" docValues="true" useDocValuesAsStored="true"/>

以下のデータが入っているとします。

$ curl -s 'http://localhost:8983/solr/osaka_shisetsu4/select?omitHeader=true&q=*%3A*'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"10",
        "name":"軽自動車検査協会大阪主管事務所",
        "area":"住之江区",
        "score":10,
        "address":["住之江区南港東3-4-62"],
        "address_p":"34.6164939,135.4382107",
        "_version_":1709611640162353152,
        "type":"官公庁"}]
  }}

Atomic Update で部分的に更新します。

[{
    "id":"10",
    "type":{"set":"官公庁1"},
    "address":{"add":"すみのえくなんこうひがし3-4-62"},
    "score":{"inc":3},
}]
  • type の「官公庁」を「官公庁1」で置き換え
  • address (multiValued)に「すみのえくなんこうひがし3-4-62」を追加
  • score の 10 に 3 を加算
$ curl -s 'http://localhost:8983/solr/osaka_shisetsu4/select?omitHeader=true&q=*%3A*'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"10",
        "name":"軽自動車検査協会大阪主管事務所",
        "area":"住之江区",
        "score":13,
        "address":["すみのえくなんこうひがし3-4-62",
          "住之江区南港東3-4-62"],
        "address_p":"34.6164939,135.4382107",
        "_version_":1709615534472953856,
        "type":"官公庁1"}]
  }}

add の代わりに add-distinct を指定すると、その値が含まれていないときだけ追加されます。

multiValued のデータから値を削除することもできます。

[{
    "id":"10",
    "address":{"remove":"すみのえくなんこうひがし3-4-62"}
}]

正規表現を使って複数削除することもできます。

[{
    "id":"10",
    "address":{"removeregex":"すみのえ.*"}
}]
$ curl -s 'http://localhost:8983/solr/osaka_shisetsu4/select?omitHeader=true&q=*%3A*'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"10",
        "name":"軽自動車検査協会大阪主管事務所",
        "area":"住之江区",
        "score":13,
        "address":["住之江区南港東3-4-62"],
        "address_p":"34.6164939,135.4382107",
        "_version_":1709616620606849024,
        "type":"官公庁1"}]
  }}

[Solr] Stored フィールドと DocValued フィールド

Solr の検索処理の要は転置インデックスです。転置インデックスは概念的には以下のようなデータ構造になっています。

キーワード文書ID
製品文書1,文書3,文書4
部品文書2,文書5,文書6

転置インデックスを使うことで、「部品」を含む文書の文書IDが2と5と6である、といったことを効率良く調べることができます。ただし、利用する機能(ソート、ファセット、ハイライティング)によっては特定のドキュメントに含まれるキーワードを調べるためのデータ構造があると効率が良いことがあります。それが DocValues です。

DocValues は以下のようなデータ構造です。

フィールド名文書ID→フィールド値のマップ
name{“文書1″:”製品1”, “文書2″:”部品1”, “文書3″:”製品2”, “文書4″:”製品3”, “文書5″:”部品2”, “文書6″:”部品3”}
price{“文書1”:1000, “文書2”:100, “文書3”:2000, “文書4”:3000, “文書5”:200, “文書6”:300}

すべての文書(あるいは特定の文書セット)における特定のフィールド値をまとめて取得できるデータ構造となっています。

たとえば name:部品 という検索で price フィールドを対象にファセットを作るとします。
この場合、転置インデックスによって「部品」を含む文書IDが2,5,6であることが分かり、それぞれの price フィールドの値が 100,200,300 であることが効率よく調べられます。
DocValues を使うには、対象としたいフィールドの定義で docValues=”true” を指定します。

文書IDからフィールド値を調べるためのデータとしては、フィールド定義で stored=”true” を指定することで格納されたフィールド値も利用できます。stored のデータ構造は以下のようなものです。

文書IDフィールド名→フィールド値のマップ
文書1{“name”:”製品1″, “price”:1000}
文書2{“name”:”部品1″, “price”:100}
文書3{“name”:”製品2″, “price”:2000}
文書4{“name”:”製品3″, “price”:3000}
文書5{“name”:”部品2″, “price”:200}
文書6{“name”:”部品3″, “price”:300}

特定の文書IDに紐づくすべてのフィールド値を一括して取得できるデータ構造となっています。

stored=”true” かつ docValues=”false” なフィールドに対してソート、ファセット、ハイライティングといった処理をするために、DocValues に近いデータ構造が FieldCache として Java ヒープ上に作られます。FieldCache は実行時に作られるもので、キャッシュデータが揃うまでに時間が掛かるとかJavaのヒープ使用量が大きくなるとかいった欠点があります。

それに対して DocValues はインデックス作成時に同時に作られます。OSのファイルキャッシュに乗るため、実行時のコストを小さくすることができます。

[Solr] Cross Collection Join と Range Hash Query

Solr リファレンスの Cross Collection Join の項に以下の説明があります。

If the local index is sharded according to the join key field, the cross collection join can leverage a secondary query parser called the “hash_range” query parser. The hash_range query parser is responsible for returning only the documents that hash to a given range of values. This allows the CrossCollectionQuery to query the remote Solr collection and return only the join keys that would match a specific shard in the local Solr collection. This has the benefit of making sure that network traffic doesn’t increase as the number of shards increases and allows for much greater scalability.

https://solr.apache.org/guide/8_9/other-parsers.html#cross-collection-join

意訳

ローカルインデックスが join key フィールドでシャードに分けられている場合、Cross Collection Join はHash Range クエリパーザという補助的なクエリパーザを利用できます。Hash Range クエリパーザは、そのハッシュが指定された範囲に収まるドキュメントだけを返す役目を持っています。これにより、CrossCollectionQuery はリモートの Solr コレクションにクエリを投げたときにローカル側のコレクションの特定のシャードにだけマッチする join key を受け取ることができます。この機能にはシャード数が増加してもネットワークトラフィックが増えないという利点があり、非常に優れたスケーラビリティを実現します。

この動作を確認してみます。前回同様

ローカルインデックス: http://localhost:8983/solr/collection1
リモートインデックス: http://localhost:7574/solr/collection2

です。それぞれ以下のようなデータが入っています。

$ curl -s 'http://localhost:8983/solr/collection1/select?q.op=AND&q=*%3A*&rows=5'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":4,
    "params":{
      "q":"*:*",
      "q.op":"AND",
      "rows":"5"}},
  "response":{"numFound":9238,"start":0,"maxScore":1.0,"numFoundExact":true,"docs":[
      {
        "id":"名所・旧跡!4548",
        "type_str":"名所・旧跡",
        "name_str":"高砂神社",
        "_version_":1706788558129332224},
      {
        "id":"名所・旧跡!4549",
        "type_str":"名所・旧跡",
        "name_str":"高崎神社",
        "_version_":1706788558131429376},
      {
        "id":"名所・旧跡!4550",
        "type_str":"名所・旧跡",
        "name_str":"大阪護国神社",
        "_version_":1706788558131429377},
      {
        "id":"名所・旧跡!4551",
        "type_str":"名所・旧跡",
        "name_str":"極楽寺",
        "_version_":1706788558131429378},
      {
        "id":"名所・旧跡!4552",
        "type_str":"名所・旧跡",
        "name_str":"あびこ観音",
        "_version_":1706788558131429379}]
  }}
$ curl -s 'http://localhost:7574/solr/collection2/select?q.op=AND&q=*%3A*&rows=5'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":5,
    "params":{
      "q":"*:*",
      "q.op":"AND",
      "rows":"5"}},
  "response":{"numFound":9238,"start":0,"maxScore":1.0,"numFoundExact":true,"docs":[
      {
        "id":"名所・旧跡!4548",
        "area_str":"住之江区",
        "address_str":"住之江区北島3-14-12",
        "address_p":"34.6003421111111,135.477833138888",
        "_version_":1706808733657464832},
      {
        "id":"名所・旧跡!4549",
        "area_str":"住之江区",
        "address_str":"住之江区南加賀屋4-15-3",
        "address_p":"34.6014560555555,135.474182833333",
        "_version_":1706808733658513408},
      {
        "id":"名所・旧跡!4550",
        "area_str":"住之江区",
        "address_str":"住之江区南加賀屋1-1-77",
        "address_p":"34.6102029722222,135.473905944444",
        "_version_":1706808733659561984},
      {
        "id":"名所・旧跡!4551",
        "area_str":"住吉区",
        "address_str":"住吉区遠里小野5丁目",
        "address_p":"34.6003890555555,135.495708055555",
        "_version_":1706808733659561985},
      {
        "id":"名所・旧跡!4552",
        "area_str":"住吉区",
        "address_str":"住吉区我孫子4丁目",
        "address_p":"34.5989438333333,135.508442333333",
        "_version_":1706808733659561986}]
  }}

idフィールドが CompositId になっており、type_str の値でシャーディングされます。
シャードが4つの場合、以下のような配置となっています。

$ curl -s 'http://localhost:8983/solr/collection1/select?facet.field=type_str&facet=on&q=*:*&rows=0&shards=shard1'|jq .facet_counts.facet_fields.type_str
[
  "名所・旧跡",
  561,
  "環境・リサイクル",
  53
]
$ curl -s 'http://localhost:8983/solr/collection1/select?facet.field=type_str&facet=on&q=*:*&rows=0&shards=shard2'|jq .facet_counts.facet_fields.type_str
[
  "駐車場・駐輪場",
  1049,
  "学校・保育所",
  1045,
  "医療・福祉",
  840,
  "会館・ホール",
  642,
  "官公庁",
  301,
  "その他",
  141
]
$ curl -s 'http://localhost:8983/solr/collection1/select?facet.field=type_str&facet=on&q=*:*&rows=0&shards=shard3'|jq .facet_counts.facet_fields.type_str
[
  "駅・バス停",
  2574,
  "警察・消防",
  330,
  "文化・観光",
  290
]
$ curl -s 'http://localhost:8983/solr/collection1/select?facet.field=type_str&facet=on&q=*:*&rows=0&shards=shard4'|jq .facet_counts.facet_fields.type_str
[
  "公園・スポーツ",
  1090,
  "公衆トイレ",
  322
]

それぞれのシャードのハッシュの値の範囲は以下の通りです。

$ curl -s 'http://localhost:8983/solr/admin/collections?action=COLSTATUS&collection=collection1&coreInfo=true' |jq '.collection1.shards | {(keys[0]):[.[].range][0],(keys[1]):[.[].range][1],(keys[2]):[.[].range][2],(keys[3]):[.[].range][3]}'
{
  "shard1": "80000000-bfffffff",
  "shard2": "c0000000-ffffffff",
  "shard3": "0-3fffffff",
  "shard4": "40000000-7fffffff"
}

ここで type_str:文化・観光 で area_type:中央区 のデータを Cross Collection Join で検索してみます。

type_str:文化・観光 {!join ttl=1 routed=\"true\" method=\"crossCollection\" fromIndex=\"collection2\" from=\"id\" to=\"id\" solrUrl=\"http://localhost:7574/solr\" v=\"area_str:中央区\"}

routed=true を指定して、Range Hash クエリを有効にしています。
この検索におけるリモートインデックス側(7574のSolr)のログを確認すると、以下のような検索クエリが来ていることが分かります。

2021-07-31 16:52:56.699 INFO  (qtp1620529408-898) [c:collection2 s:shard1 r:core_node3 x:collection2_shard1_replica_n1] o.a.s.c.S.Request [collection2_shard1_replica_n1]  webapp=/solr path=/stream params={indent=off&expr=unique(search(collection2,q%3D"area_str:中央区",fq%3D"{!hash_range+f%3Did+l%3D0+u%3D1073741823}",fl%3Did,sort%3D"id+asc",qt%3D"/export",method%3DcrossCollection),over%3Did)&wt=javabin&version=2.2} status=0 QTime=0

Hash Range を指定しているのは以下の部分です。

{!hash_range f:id l:0 u:1073741823}

idフィールドのハッシュ値の下限が0,上限が1073741823と指定されています。1073741823を16進数に直すと3FFFFFFFです。type_str:文化・観光 が属する shard3 にマッチするものだけが要求されていることが分かります。