カテゴリー: テクノロジー

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 を変更することになります。


MacBookを新しい端末に移行してみた

もうIntel Macには戻れない。
マエダです。

先日、M2チップ搭載のMacBook Airに開発環境を移行してみました。

M2というからには現状一番スペックが高いのかと思ってましたが、M1 < M2 < M1 Pro < M1 Max < M1 Ultraというポジションのようです。
僕はグラフィック編集や動画編集ができるスキルは持ってないためM2で十分です。

これまで仮想環境をローカルで動作させたりしているとものすごい音がしていたこともありましたが、M2になってからは全く音がなくなり快適に利用できています。

利用に際してポイントは以下。

■ CocoaPodsはgemでなく、Homebrewでインストールすべし

こういった動作確認できるかできないかの問題は時間が解決してくれますが、現状brewでインストールしておけば正常に利用可能です。

sudo gem install cocoapods

brew install cocoapods

https://cocoapods.org/

■ Intel Mac用アプリもRosettaで互換できるかもしれない

すべてのアプリの検証は不可なため「かもしれない」としてますが、移行に不安な方もこういったものがあるんだなくらい知っておくと新しい端末に移行してみようと思えるかもです。

https://support.apple.com/ja-jp/HT211861

■ ローカルVMはMultipassが便利

vagrantを使ってきましたが現状正常動作できておらずこちらも時間が解決してくれると思いますが、UbuntuユーザーならMultipassというVMが非常にシンプルで便利です。

https://multipass.run/

移行するときにいろいろと作業に便利なシェル関連をコピペすることがあるかと思いますが、古い端末から新しい端末へのコピーは共有ドライブを利用したりしてましたがイマドキはドラッグ&ドロップでできてしまいます。

※ Apple公式からお借りしました。

https://support.apple.com/ja-jp/HT212757

こんなことができるなんて知らなかったし調べたこともなかったのでひとつの端末でどちらも操作できたときは失礼ながらバグったのかと思いました。。。

どんどん便利になってとても快適。

僕たちもワクワクするようなプロダクトを提供できるようがんばります!


[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のファイルキャッシュに乗るため、実行時のコストを小さくすることができます。