タグ: Solr

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 にマッチするものだけが要求されていることが分かります。


[Solr] Cross Collection Join

はじめに

Solr 8.6 以降ではコレクションをまたがった Join クエリを実行することができます。

Cross Collection Join

メインの検索対象を collection1、Join 対象を collection2 とします。Cross Collection Join は、collection2 に対する検索条件にヒットするデータの join key を持つ collection1 側のデータを取得する一種のフィルタクエリとして機能します。注意が必要なのは collection2 側のデータのフィールドは取得できないということです。collection2 は別のサーバで動く Solr 上にあっても構いません。

動作確認

Cross Collection Join の動作を確認するため、2つの Solr プロセスを起動します。

bin/solr start -cloud -p 8983
bin/solr start -cloud -p 7574

8983 の方は solrconfig.xml に以下を記載します。

<queryParser name="join" class="org.apache.solr.search.JoinQParserPlugin">
    <arr name="allowSolrUrls">
      <str>http://localhost:7574/solr</str>
    </arr>
</queryParser>

8983 の collection1 には以下のようなデータが入っています。

$ curl -s 'http://localhost:8983/solr/collection1/select?q.op=OR&q=*:*&rows=5'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "q.op":"OR",
      "rows":"5",
      "_":"1625974694981"}},
  "response":{"numFound":9238,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"10",
        "type_str":"官公庁",
        "name_str":"軽自動車検査協会大阪主管事務所",
        "_version_":1704977365000519680},
      {
        "id":"11",
        "type_str":"官公庁",
        "name_str":"大阪陸運支局なにわ自動車検査登録事務所",
        "_version_":1704977365035122688},
      {
        "id":"12",
        "type_str":"官公庁",
        "name_str":"住吉税務署",
        "_version_":1704977365036171264},
      {
        "id":"13",
        "type_str":"官公庁",
        "name_str":"玉出年金事務所",
        "_version_":1704977365036171265},
      {
        "id":"14",
        "type_str":"官公庁",
        "name_str":"大阪南労働基準監督署",
        "_version_":1704977365037219840}]
  }}

7574 の collection2 には以下のようなデータが入っています。

$ curl -s 'http://localhost:7574/solr/collection2/select?q.op=OR&q=*:*&rows=5'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "q.op":"OR",
      "rows":"5",
      "_":"1625993148833"}},
  "response":{"numFound":9238,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"10",
        "area_str":"住之江区",
        "address_str":"住之江区南港東3-4-62",
        "address_p":"34.6164938333333,135.438210722222",
        "_version_":1704977387371888640},
      {
        "id":"11",
        "area_str":"住之江区",
        "address_str":"住之江区南港東3-1-14",
        "address_p":"34.6190439722222,135.442191833333",
        "_version_":1704977387419074560},
      {
        "id":"12",
        "area_str":"住吉区",
        "address_str":"住吉区住吉2丁目17番37号",
        "address_p":"34.6109641111111,135.491388722222",
        "_version_":1704977387420123136},
      {
        "id":"13",
        "area_str":"住之江区",
        "address_str":"住之江区北加賀屋2-3-6",
        "address_p":"34.6231918888888,135.477992138888",
        "_version_":1704977387420123137},
      {
        "id":"14",
        "area_str":"西成区",
        "address_str":"西成区玉出中2丁目13番27号",
        "address_p":"34.6237640555555,135.4924",
        "_version_":1704977387421171712}]
  }}

中央区にある「文化・観光」の施設を検索してみます。施設タイプは collection1、エリア情報は collection2 にあるので Cross Collection Join の出番です。

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

solrUrl で指定した Solr のコレクション collection2 で area_str:中央区 を検索して collection1 で type_str:文化・観光 を検索した結果と JOIN する、join key はそれぞれの id フィールド、というクエリです。

$ curl -s 'http://localhost:8983/solr/collection1/select?q=type_str%3A%E6%96%87%E5%8C%96%E3%83%BB%E8%A6%B3%E5%85%89%20%7B!join%20method%3D%22crossCollection%22%20fromIndex%3D%22collection2%22%20from%3D%22id%22%20to%3D%22id%22%20solrUrl%3D%22http%3A%2F%2Flocalhost%3A7574%2Fsolr%22%20v%3D%22area_str%3A%E4%B8%AD%E5%A4%AE%E5%8C%BA%22%7D'
{
  "responseHeader":{
    "zkConnected":true,
    "status":0,
    "QTime":0,
    "params":{
      "q":"type_str:文化・観光 {!join method=\"crossCollection\" fromIndex=\"collection2\" from=\"id\" to=\"id\" solrUrl=\"http://localhost:7574/solr\" v=\"area_str:中央区\"}",
      "q.op":"AND",
      "debugQuery":"false",
      "_":"1625975185887"}},
  "response":{"numFound":56,"start":0,"numFoundExact":true,"docs":[
      {
        "id":"3101",
        "type_str":"文化・観光",
        "name_str":"大阪市立島之内図書館",
        "_version_":1704977365352841232},
      {
        "id":"3136",
        "type_str":"文化・観光",
        "name_str":"上方浮世絵館",
        "_version_":1704977365354938370},
      {
        "id":"3137",
        "type_str":"文化・観光",
        "name_str":"府立上方演芸資料館(ワッハ上方)",
        "_version_":1704977365354938371},
      {
        "id":"3138",
        "type_str":"文化・観光",
        "name_str":"国立文楽劇場",
        "_version_":1704977365354938372},
      {
        "id":"3140",
        "type_str":"文化・観光",
        "name_str":"湯木美術館",
        "_version_":1704977365354938374},
      {
        "id":"3143",
        "type_str":"文化・観光",
        "name_str":"適塾",
        "_version_":1704977365354938377},
      {
        "id":"3144",
        "type_str":"文化・観光",
        "name_str":"府立現代美術センター",
        "_version_":1704977365354938378},
      {
        "id":"3145",
        "type_str":"文化・観光",
        "name_str":"大阪歴史博物館",
        "_version_":1704977365354938379},
      {
        "id":"3146",
        "type_str":"文化・観光",
        "name_str":"ピースおおさか:大阪国際平和センター",
        "_version_":1704977365354938380},
      {
        "id":"3147",
        "type_str":"文化・観光",
        "name_str":"大阪城天守閣",
        "_version_":1704977365354938381}]
  }}