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

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}]}]}]
  }}

MDN の動作サンプルを改造して遊ぶ

先日、MDN Web Docs で調べものをしているときに、
たまたま面白そうなサンプルを見つけました。


今日は、それを使って遊んでみたいと思います。


その前に、MDN Web Docs について分からない人向けに簡単に説明すると、
Mozilla.org による、Web技術者向けのドキュメントが集まったウェブサイトです。

MDN Web Docs (以前は MDN — Mozilla Developer Network と呼ばれていました) は、ウェブ技術とウェブを支えるソフトウェア、 CSS、 HTML、 JavaScript などについて学ぶための進化し続ける学習プラットフォームです。

MDN Web Docs について – MDN プロジェクト | MDN (mozilla.org)


私の中で、辞書変わりに常に開いておきたいサイトランキング1位に輝いています。
人によっては、読んでるだけで楽しい! ワクワクする! という噂もあります。


今回取り上げるサンプルと、元になるコードは以下にあります。
KeyboardEvent.code – Web APIs | MDN (mozilla.org)


“Try it out” と書かれた文字の下に、
黒い正方形と、その中にオレンジの二等辺三角形が描画されています。


コードを見ると、
オレンジの二等辺三角形は spaceship という名前のようです。
黒い四角は world。


私がつけたわけではありません。
が、ここに世界と、宇宙船が誕生したわけです。


ここからコードを追加し、
スペースキーを押すとショットを撃てるようにしたいと思います。


一部抜粋ですが、こんな感じに。

const spaceship = document.getElementById("spaceship");
const shot      = document.getElementById("shot");
const world     = document.querySelector('.world');
let isFiring    = false

function refresh() {
	let x = position.x - (shipSize.width/2);
	let y = position.y - (shipSize.height/2);
	let transform = "translate(" + x + " " + y + ") rotate(" + angle + " 15 15)";
	spaceship.setAttribute("transform", transform);

	if (isFiring) {
		let rad = angle * (Math.PI / 180);
		shotPosition.x += (Math.sin(rad) * shotMoveRate);
		shotPosition.y -= (Math.cos(rad) * shotMoveRate);
		let sx = shotPosition.x + (shipSize.width / 2);
		let sy = shotPosition.y + (shipSize.height / 2);
		shot.setAttribute("transform", 'translate(' + sx + ' ' + sy +')');

		if (isOutOfArea(sx, sy)) {
			isFiring = false;
		}
	} else {
		shotPosition.x = x;
		shotPosition.y = y;
	}
}
function isOutOfArea(x, y) {
	return (x < -1 || x > world.clientWidth || y < -1 || y > world.clientHeight);
}
setInterval(() => {
	refresh();
}, 33);


MDN のコードは、あくまでも KeyboardEvent のサンプルなので、
キーボードを押した際にしか処理が行われないようになっています。


これはこれで省エネなのですが、
この世界の神がキーボードを押している間しか時間が進みませんので、
いろいろと不都合です。

定期的に画面を描画し直すように変更しました。

次に、スペースキーで shot を発射するための処理を入れるのですが、
この宇宙船は、2次元平面上で回転します。

宇宙船の向きに合わせて前方に発射しましょう。

宇宙船と角度を合わせて、
座標を移動させるだけです。

無事、撃てるようになったら、
世界の領域を越えたショットを消すようにします。

宇宙の外に迷惑をかけてはいけません。


ここまでの動作サンプルを置いておきます。
下の枠内をクリックすると読み込みます。

クリックで読み込み

推奨環境は Google Chrome、MS Edge です。


気付いた人もいらっしゃるかもしれませんが、
angle の値を使いまわしているため、
ショットを撃ったあとに回転するとショットがカーブします。
これはこれで面白いかと思い放置しています。


自機が動いてショットを撃つ。

これだけでも、
なんとなくゲームのような片鱗が見えてきたのではないでしょうか。

360度全方位の2Dシューティングゲーム (画面は3D) が
何年か前にありましたが、
ものすごく頑張ればそれに近いものが作れるかもしれません。


MDN にはいろいろな動作サンプルがあります。
あなたの求めるものが意外なところから見つかるかもしれません。

【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/
へと変更されています。