タグ: Solr

Solr 8.8で入れ子構造の文書をインデックスする

はじめに

以前書いた「Solrで入れ子構造の文書をインデックスする」という記事で対象としていたのはSolr 7.5でしたが、現時点での最新版のSolr 8.8ではいろいろと変わった部分があるので改めて採り上げてみます。

Nested Documents 機能について Solr 7.5 から 8.8 で変わった点

Solr 7.5 の頃と変わったのは以下の点です。

  • 3層以上の階層を持てるようになった
  • どの階層のドキュメントかを示すフィールドを明示的に定義しなくても良くなった。階層構造を保持する _nest_path_ フィールドを Solr が自動的に管理するようになったため
  • 入れ子構造内の特定のドキュメントだけを部分的に更新できるようになった

スキーマ設定

Nested Documents を扱うためのスキーマ設定は以下の通りです。

_root_ フィールド (必須)

<field name="_root_" type="string" indexed="true" stored="false" docValues="false" />

インデックス作成時にSolrが自動的に作成するフィールドです。入れ子になったドキュメントはルートドキュメント(一番の先祖にあたるドキュメント)のidフィールドの値を _root_ フィールドの値として持ちます。

_nest_path_ フィールド (オプション)

<fieldType name="_nest_path_" class="solr.NestPathField" />
<field name="_nest_path_" type="_nest_path_" />

入れ子構造のルートドキュメント以外のドキュメントにSolrが自動的に付与するフィールドです。このフィールドは検索結果を取得するときに ChildDocTransformer で子ドキュメント以下の階層構造を扱うのに使われます。もしこのフィールドが無い場合はすべての子孫がフラットなリストとして扱われます。

_nest_path_ フィールドを使わない場合は、それぞれの階層を区別するための自前のフィールドを定義することが推奨されます。(手間が掛かるだけなので、素直に _nest_path_ フィールドを使った方が良さそうです)

_nest_parent_ フィールド (オプション)

<field name="_nest_parent_" type="string" indexed="true" stored="true" />

ルートドキュメント以外のドキュメントにSolrが自動的に付与するフィールドです。親ドキュメントのidフィールドの値を持ちます。

制限

  • スキーマ内で各フィールド名の定義は1回だけ。親と子で同じフィールド名を別のフィールドタイプで定義するようなことはできない
  • すべてのドキュメントタイプで共通するフィールドにだけ require を使うことができる
  • 階層を問わず、すべてのドキュメントは id フィールドにユニークな値を持たなければならない

例: プレイリスト

以下のようなプレイリスト情報を Solr に与えます。
(_default の configset でコレクションを作っていればスキーマレスでインデックス作成できます)

[{
    "id":"list_1",
    "title_t":"list1",
    "songs":[
	{
	    "id":"l1!song1",
	    "title_t":"title1",
	    "artist_t":"artist1",
	    "trackNum_i":1
	},
	{
	    "id":"l1!song2",
	    "title_t":"title2",
	    "artist_t":"artist2",
	    "trackNum_i":2
	}
    ]
},
{
    "id":"list_2",
    "title_t":"list2",
    "songs":[
	{
	    "id":"l2!song1",
	    "title_t":"title3",
	    "artist_t":"artist3",
	    "trackNum_i":1
	},
	{
	    "id":"l2!song2",
	    "title_t":"title1",
	    "artist_t":"artist1",
	    "trackNum_i":2
	}
    ]
},
{
    "id":"list_3",
    "title_t":"list3",
    "sublist":[
        {
            "id":"l3!sublist1",
            "title_t":"sublist1",
            "songs":[
	            {
	                "id":"l3!song1",
	                "title_t":"title4",
	                "artist_t":"artist4",
	                "trackNum_i":1
	            },
	            {
	                "id":"l3!song2",
	                "title_t":"title2",
	                "artist_t":"artist2",
	                "trackNum_i":2
	            }
            ]
        },
        {
            "id":"l3!sublist2",
            "title_t":"sublist2",
            "songs":[
	            {
	                "id":"l3!song3",
	                "title_t":"title1",
	                "artist_t":"artist1",
	                "trackNum_i":1
	            },
	            {
	                "id":"l3!song4",
	                "title_t":"title3",
	                "artist_t":"artist3",
	                "trackNum_i":2
	            }
            ]
        }
    ]
}]

プレイリスト名が list1 のプレイリストを曲も含めて検索

ヒットしたプレイリストの子ドキュメントを取得するために ChildDocTransformer を利用します。

$ curl 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="title_t:list1 -_nest_path_:*"}' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"list1",
        "_version_":1697935145902800896,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":1,
            "_version_":1697935145902800896},
          
          {
            "id":"l1!song2",
            "title_t":"title2",
            "artist_t":"artist2",
            "trackNum_i":2,
            "_version_":1697935145902800896}]}]
  }}

曲名が title1 の曲が含まれるすべてのプレイリストを検索

_nest_path_ フィールドでルート直下の songs の階層を指定しています。 {!parent} 内での -_nest_path_:* は「_nest_path_フィールドを持つもの以外」つまりルートドキュメントだけを対象とするという意味です。

$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:*"}(+_nest_path_:\/songs +title_t:title1)' -d 'fl=*,[child]'
{
  "response":{"numFound":2,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_1",
        "title_t":"list1",
        "_version_":1697935145902800896,
        "songs":[
          {
            "id":"l1!song1",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":1,
            "_version_":1697935145902800896},
          
          {
            "id":"l1!song2",
            "title_t":"title2",
            "artist_t":"artist2",
            "trackNum_i":2,
            "_version_":1697935145902800896}]},
      {
        "id":"list_2",
        "title_t":"list2",
        "_version_":1697935145926918144,
        "songs":[
          {
            "id":"l2!song1",
            "title_t":"title3",
            "artist_t":"artist3",
            "trackNum_i":1,
            "_version_":1697935145926918144},
          
          {
            "id":"l2!song2",
            "title_t":"title1",
            "artist_t":"artist1",
            "trackNum_i":2,
            "_version_":1697935145926918144}]}]
  }}

サブリストに曲名が title1 の曲を含まれるプレイリストを検索

_nest_path_ フィールドで sublist 階層の下の songs の階層を指定しています。

$ curl -s 'http://localhost:8983/solr/nestedDocuments/select' -d 'omitHeader=true' --data-urlencode 'q={!parent which="*:* -_nest_path_:*"}(+_nest_path_:\/sublist\/songs +title_t:title1)' -d 'fl=*,[child]'
{
  "response":{"numFound":1,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"list_3",
        "title_t":"list3",
        "_version_":1697935153103372288,
        "sublist":[
          {
            "id":"l3!sublist1",
            "title_t":"sublist1",
            "_version_":1697935153103372288,
            "songs":[
              {
                "id":"l3!song1",
                "title_t":"title4",
                "artist_t":"artist4",
                "trackNum_i":1,
                "_version_":1697935153103372288},
              
              {
                "id":"l3!song2",
                "title_t":"title2",
                "artist_t":"artist2",
                "trackNum_i":2,
                "_version_":1697935153103372288}]},
          
          {
            "id":"l3!sublist2",
            "title_t":"sublist2",
            "_version_":1697935153103372288,
            "songs":[
              {
                "id":"l3!song3",
                "title_t":"title1",
                "artist_t":"artist1",
                "trackNum_i":1,
                "_version_":1697935153103372288},
              
              {
                "id":"l3!song4",
                "title_t":"title3",
                "artist_t":"artist3",
                "trackNum_i":2,
                "_version_":1697935153103372288}]}]}]
  }}

【Solr】Result Grouping と Facet との組み合わせ

前回取り上げた Result Grouping は Facet と組み合わせることができます。大阪の施設情報の例で言うと、type フィールドでファセットを作り、それぞれに含まれる area フィールドのグループの件数を求め る、といったことができます。

ただし、分散検索においては同じグループに属するドキュメントは同じシャードに置かれなければならない、という制限があります。

特定の条件に当てはまるドキュメントを同じシャードに配置するためにはドキュメントIDに複合キーを指定します。具体的には以下のような形になります。

通常
{"id":"10","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62","address_p":"34.6164938333333,135.438210722222"}
areaフィールドの値が同じドキュメントを同じシャードに配置
{"id":"住之江区!10","type":"官公庁","area":"住之江区","name":"軽自動車検査協会大阪主管事務所","address":"住之江区南港東3-4-62","address_p":"34.6164938333333,135.438210722222"}

idの値を “areaの値!本来のid” とすることでareaの値が同じドキュメントが同じシャードでインデックスされます。

組み合わせを試す前に、 まずは type フィールドを使った通常の Facet 検索の結果です。

$ curl -s 'http://localhost:8983/solr/osaka_shisetsu2/select?q=*:*&rows=0&facet=on&facet.field=type_str'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":2,
    "params":{
      "q":"*:*",
      "facet.field":"type_str",
      "rows":"0",
      "facet":"on"}},
  "response":{"numFound":9238,"start":0,"maxScore":1.0,"numFoundExact":true,"docs":[]
  },
  "facet_counts":{
    "facet_queries":{},
    "facet_fields":{
      "type_str":[
        "駅・バス停",2574,
        "公園・スポーツ",1090,
        "駐車場・駐輪場",1049,
        "学校・保育所",1045,
        "医療・福祉",840,
        "会館・ホール",642,
        "名所・旧跡",561,
        "警察・消防",330,
        "公衆トイレ",322,
        "官公庁",301,
        "文化・観光",290,
        "その他",141,
        "環境・リサイクル",53]},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}

それぞれの件数は Facet 内のドキュメントの件数を示しています。

次にグループ検索と Facet 検索の組み合わせです。

$ curl -s 'http://localhost:8983/solr/osaka_shisetsu2/select?q=*:*&group=true&group.field=area_str&rows=0&group.facet=true&facet=on&facet.field=type_str'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":4,
    "params":{
      "q":"*:*",
      "facet.field":"type_str",
      "group.facet":"true",
      "rows":"0",
      "facet":"on",
      "group.field":"area_str",
      "group":"true"}},
  "grouped":{
    "area_str":{
      "matches":9238,
      "groups":[]}},
  "facet_counts":{
    "facet_queries":{},
    "facet_fields":{
      "type_str":[
        "公園・スポーツ",25,
        "公衆トイレ",25,
        "医療・福祉",25,
        "名所・旧跡",25,
        "学校・保育所",25,
        "官公庁",25,
        "文化・観光",25,
        "駅・バス停",25,
        "その他",24,
        "会館・ホール",24,
        "警察・消防",24,
        "駐車場・駐輪場",24,
        "環境・リサイクル",20]},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}

それぞれの件数はグループ数(Facet に含まれるエリアの種類)です。公共の施設なのでほとんどのタイプはほとんどの区に存在している(大阪24区+「大阪市以外」というタイプがあるためほとんどのグループ数は25になっている)ためあまりおもしろい結果ではありませんが、通常の Facet 検索との違いは良く分かると思います。

ちなみに、ドキュメントルーティングの設定をせずに作ったインデックスでの同じクエリの結果は以下の通りです。

$ curl -s 'http://localhost:8983/solr/osaka_shisetsu2/select?q=*:*&group=true&group.field=area_str&rows=0&group.facet=true&facet=on&facet.field=type_str'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":7,
    "params":{
      "q":"*:*",
      "facet.field":"type_str",
      "group.limit":"3",
      "group.facet":"true",
      "rows":"0",
      "facet":"on",
      "group.field":"area_str",
      "group":"true"}},
  "grouped":{
    "area_str":{
      "matches":9238,
      "groups":[]}},
  "facet_counts":{
    "facet_queries":{},
    "facet_fields":{
      "type_str":[
        "医療・福祉",50,
        "学校・保育所",50,
        "官公庁",50,
        "駅・バス停",50,
        "公衆トイレ",49,
        "名所・旧跡",49,
        "会館・ホール",48,
        "公園・スポーツ",48,
        "駐車場・駐輪場",48,
        "警察・消防",47,
        "文化・観光",46,
        "その他",40,
        "環境・リサイクル",29]},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}

2つのシャードそれぞれでグループ化が実行されるため、重複が発生してグループの数が倍近くになってしまいます。これを防ぐために複合キーを用いたドキュメントルーティングが必要になる訳です。


【Solr】検索結果のグループ化(Result Grouping)

はじめに

Solr には Result Grouping という機能があり、検索結果を何らかの条件でグループ化できます。Solr リファレンスガイドで挙げられている例が Result Grouping の動作を知るにはいまいちな感じだったので、別のドキュメントを使って試してみました。

準備

サンプルとして大阪の施設情報を利用します。

デフォルトの configset でコレクション osaka_shisetsu を作成し、施設情報のデータを投入します。

$ cd server/solr/configsets
$ cp -r _default osaka_shisetsu
$ ../../scripts/cloud-scripts/zkcli.sh -zkhost localhost:9983 -cmd upconfig -confdir osaka_shisetsu/conf -confname osaka_shisetsu
$ curl -s 'http://localhost:8983/solr/admin/collections?action=CREATE&name=osaka_shisetsu&numShards=2&maxShardsPerNode=2&replicationFactor=2&collection.configName=osaka_shisetsu'
$ curl 'http://localhost:8983/solr/osaka_shisetsu/update?commit=true&indent=true' --data-binary @/tmp/data.json -H 'Content-Type: application/json'
$ curl -s 'http://localhost:8983/solr/osaka_shisetsu/select?q=*%3A*&rows=2'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":15,
    "params":{
      "q":"*:*",
      "rows":"2"}},
  "response":{"numFound":9238,"start":0,"maxScore":1.0,"numFoundExact":true,"docs":[
      {
        "id":"10",
        "type":["官公庁"],
        "area":["住之江区"],
	"name":["軽自動車検査協会大阪主管事務所"],
        "address":["住之江区南港東3-4-62"],
        "address_p":"34.6164938333333,135.438210722222",
	"_version_":1692768899944153088},
      {
        "id":"11",
        "type":["官公庁"],
	"area":["住之江区"],
        "name":["大阪陸運支局なにわ自動車検査登録事務所"],
        "address":["住之江区南港東3-1-14"],
        "address_p":"34.6190439722222,135.442191833333",
	"_version_":1692768899950444544}]
  }}

グルーピング検索

施設名に「事務所」を含むものをエリアでグループ化する検索を実行します。

$ curl -s 'http://localhost:8983/solr/osaka_shisetsu/select?group.field=area_str&group=true&q=name%3A%E4%BA%8B%E5%8B%99%E6%89%80'
{
  "responseHeader":{
    "zkConnected":true,
    "status":500,
    "QTime":12,
    "params":{
      "q":"name:事務所",
      "group.field":"area_str",
      "group":"true"}},
  "error":{
    "metadata":[
      "error-class","org.apache.solr.common.SolrException",
      "root-error-class","org.apache.solr.client.solrj.impl.BaseHttpSolrClient$RemoteSolrException"],
    "msg":"org.apache.solr.client.solrj.SolrServerException: No live SolrServers available to handle this request:[http://127.0.1.1:8983/solr/osaka_shisetsu_shard2_replica_n6, http://127.0.1.1:7574/solr/osaka_shisetsu_shard2_replica_n4, http://127.0.1.1:8983/solr/osaka_shisetsu_shard1_replica_n2]",
(略)
    "code":500}}

500エラーになってしまいました。

solr.log のエラーメッセージから、グループ化の対象のフィールドが multi-valued となっているのが原因であることが分かりました。

2021-02-26 13:59:29.828 ERROR (qtp691691381-15) [c:osaka_shisetsu s:shard2 r:core_node7 x:osaka_shisetsu_shard2_replica_n4] o.a.s.h.RequestHandlerBase java.lang.IllegalStateException: unexpected docvalues type SORTED_SET for field 'area_str' (expected=SORTED). Re-index with correct docvalues type.
    at org.apache.lucene.index.DocValues.checkField(DocValues.java:317)
    at org.apache.lucene.index.DocValues.getSorted(DocValues.java:369)
    at org.apache.lucene.search.grouping.TermGroupSelector.setNextReader(TermGroupSelector.java:57)
    at org.apache.lucene.search.grouping.FirstPassGroupingCollector.doSetNextReader(FirstPassGroupingCollector.java:349)

リファレンスにも確かに single-valued で indexed なフィールドでないといけないと書かれていました。

group.field
The name of the field by which to group results. The field must be single-valued, and either be indexed or a field type that has a value source and works in a function query, such as ExternalFileField. It must also be a string-based field, such as StrField or TextField

そこで、ダイナミックフィールド *_str の定義を multi-valued な strings 型から single-valued な string 型に変更してインデックスを作り直しました。

<fieldType name="string" class="solr.StrField" sortMissingLast="true" docValues="true" />
<fieldType name="strings" class="solr.StrField" sortMissingLast="true" multiValued="true" docValues="true" />

<dynamicField name="*_str" type="strings" stored="false" docValues="true" indexed="false" useDocValuesAsStored="false"/>

改めて検索を実行します。

$ curl -s 'http://localhost:8983/solr/osaka_shisetsu/select?group.field=area_str&group=true&rows=3&q=name%3A%E4%BA%8B%E5%8B%99%E6%89%80'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":27,
    "params":{
      "q":"name:事務所",
      "rows":"3",
      "group.field":"area_str",
      "group":"true"}},
  "grouped":{
    "area_str":{
      "matches":685,
      "groups":[{
          "groupValue":"浪速区",
          "doclist":{"numFound":6,"start":0,"maxScore":5.533971,"numFoundExact":true,"docs":[
              {
                "id":"23",
                "type":["官公庁"],
                "area":["浪速区"],
                "name":["難波年金事務所"],
                "address":["浪速区敷津東1-6-16"],
                "address_p":"34.6588191388888,135.49922225",
                "_version_":1692817725524541443}]
          }},
        {
          "groupValue":"西区",
          "doclist":{"numFound":15,"start":0,"maxScore":5.533971,"numFoundExact":true,"docs":[
              {
                "id":"37",
                "type":["官公庁"],
                "area":["西区"],
                "name":["堀江年金事務所"],
                "address":["西区北堀江3-10-1"],
                "address_p":"34.6750045555555,135.488194555555",
                "_version_":1692817725525590019}]
          }},
        {
          "groupValue":"東成区",
          "doclist":{"numFound":13,"start":0,"maxScore":5.533971,"numFoundExact":true,"docs":[
              {
                "id":"78",
                "type":["官公庁"],
                "area":["東成区"],
                "name":["今里年金事務所"],
                "address":["東成区大今里西2丁目1番8号"],
                "address_p":"34.6714081388888,135.539616805555",
                "_version_":1692817725528735750}]
          }}]}}}

名前に「事務所」を含む施設がエリア名でグループ化されて、それぞれのグループの検索結果が6件、15件、13件であることが分かります。

rows を指定することでグループ数を、group.limit を指定することでグループ毎のドキュメントの件数を変更できます。

$ curl -s 'http://localhost:8983/solr/osaka_shisetsu/select?group.field=area_str&group=true&rows=2&group.limit=3&q=name%3A%E4%BA%8B%E5%8B%99%E6%89%80'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":30,
    "params":{
      "q":"name:事務所",
      "group.limit":"3",
      "rows":"2",
      "group.field":"area_str",
      "group":"true"}},
  "grouped":{
    "area_str":{
      "matches":685,
      "groups":[{
          "groupValue":"浪速区",
          "doclist":{"numFound":6,"start":0,"maxScore":5.533971,"numFoundExact":true,"docs":[
              {
                "id":"23",
                "type":["官公庁"],
                "area":["浪速区"],
                "name":["難波年金事務所"],
                "address":["浪速区敷津東1-6-16"],
                "address_p":"34.6588191388888,135.49922225",
                "_version_":1692817725524541443},
              {
                "id":"552",
                "type":["学校・保育所"],
                "area":["浪速区"],
                "name":["大阪市立広田保育所"],
                "address":["浪速区日本橋西2-8-11"],
                "address_p":"34.6561468333333,135.50211125",
                "_version_":1692817725561241605},
              {
                "id":"3653",
                "type":["警察・消防"],
                "area":["浪速区"],
                "name":["浪速消防署立葉出張所"],
                "address":["浪速区桜川2-14-12"],
                "address_p":"34.6636563055555,135.489044111111",
                "_version_":1692817725818142729}]
          }},
        {
          "groupValue":"西区",
          "doclist":{"numFound":15,"start":0,"maxScore":5.533971,"numFoundExact":true,"docs":[
              {
                "id":"37",
                "type":["官公庁"],
                "area":["西区"],
                "name":["堀江年金事務所"],
                "address":["西区北堀江3-10-1"],
                "address_p":"34.6750045555555,135.488194555555",
                "_version_":1692817725525590019},
              {
                "id":"200",
                "type":["官公庁"],
                "area":["西区"],
                "name":["環境局河川事務所"],
                "address":["西区新町4-20-3"],
                "address_p":"34.6778531944444,135.483258083333",
                "_version_":1692817725531881486},
              {
                "id":"95",
                "type":["官公庁"],
                "area":["西区"],
                "name":["なにわ西府税事務所"],
                "address":["西区本田1-6-16"],
                "address_p":"34.6784530277777,135.479935888888",
                "_version_":1692817725529784322}]
          }}]}}}

SolrがApacheのトップレベルプロジェクトになりました

2021年2月17日にSolrがApacheのトップレベルプロジェクトになりました
去年の6月の投票で決定されていた提案が実施されたものです。
SolrはLuceneと開発の歩調を合わせる目的で2010年にLuceneプロジェクトにマージされました。それから約10年の時を経て独立したことになります。

Solr独立の提案とそれに先立つ議論では以下の理由が挙げられました。

  • プロジェクトを分離すればコミット・テストの作業負荷が軽減され時間短縮できる
  • 今や共通点の少なくなったLuceneとSolrのコードベースを統合して扱わなければならないため、ソースリリースのパッケージングやビルドが複雑になっている
  • Solrから見れば依存関係も含めてLuceneをまるごと取り込んでいるので、別コンポーネントとしてリファクタリングやメンテナンスの対象とできる
  • SolrとLuceneが分かれている方が、それぞれのプロジェクトに参加しようとしている開発者の学習も容易になる
  • ユーザ向けのメーリングリストはすでに分離されており、SolrとLuceneは独立した存在として認識されている
  • 10年前にプロジェクトがマージされたときの課題(コードの重複や相互作用の整理、再利用可能なコンポーネント化)はおおむね解決された

現時点では既に GitHub のリポジトリもそれぞれ別のものに移行済となっています。
また、今回のトップレベルプロジェクト昇格に伴い、Solrのウェブサイトは従来の
https://lucene.apache.org/solr/
から
https://solr.apache.org/
へと変更されています。


Solrのリファレンスガイドをビルドする

Solr のリファレンスガイドは、たとえばバージョン8.7のものは
https://lucene.apache.org/solr/8_7_0/index.html
というURLで閲覧できますが、リリース直後などにおいては最新のバージョンに対応したリファレンスガイドが公開されていないこともあります。
そういった場合を踏まえて、Solr のソースからリファレンスガイドを自前でビルドしてみました。

リファレンスガイドのソースは Solr のソースコードの solr/solr-ref-guide 以下にあります。ビルドに必要な手順が Readme.adoc に書かれています。

リファレンスガイドのソースは Asciidoc 形式で記述されているので、Asciidoc を扱える ruby のツール群をインストールします。

gem install jekyll --force --version 3.5.0
gem install jekyll-asciidoc
gem install slim
gem install tilt
gem install concurrent-ruby

ビルドには ivy が必要なので、まだ使ったことが無ければ準備しておきます。

ant ivy-bootstrap

ビルドします。

cd solr/solr-ref-guide
ant default

生成されたHTMLは solr/build/solr-ref-guide/html-site 以下にあります。

各所に DRAFT の文字列が見られますが、オフィシャルのリファレンスガイドとほぼ同じものをローカルに閲覧できるようになります。