タグ: Solr

【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 の文字列が見られますが、オフィシャルのリファレンスガイドとほぼ同じものをローカルに閲覧できるようになります。


Solrの記事リスト(〜2020年12月)

はじめに

ふと気付けばこのブログの Solr の記事が40本を超えていました。Solr タグで検索していただくこともできますが、もう少し見やすくなるようにトピックで分類して記事タイトルとリンクの形でリスト化してみました。

Prometheus連携

パッケージ管理

JSON Request API

Docker

日付表現

SolrCloudのリーダー選出

SolrCloud

インデックス作成

distinct

その他


SolrのCirtcuit Breakerは具体的に何を見ているのか

はじめに

前回紹介した Circuit Breaker には MemoryCircuitBreaker と CPUCircuitBreaker の2種類があります。それぞれメモリとCPUの使い過ぎに対応しているのだなと何となくは想像が付きますが、具体的にはシステムの何を参照しているのかを調べてみました。

MemoryCircuitBreaker

リファレンスによると、JVMのヒープ使用率が設定値を超えると503を返すと書かれています。MemoryCircuitBreaker の実装をみると、取得しているのは ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() でした。これは現在のヒープ使用量をByteで返すものです。

一方、設定ではヒープの使用率を記述するので、比較するには使用率か使用量かのどちらかに変換して合わせる必要があります。
ソースを確認すると、MemoryCircuitBreaker のコンストラクタで memThreashold を読み込んだときに、ヒープの最大値 * memThreshold / 100 で閾値となるメモリ使用量を計算しておき、MemoryMXBean から取得したメモリ使用量がその値を超えたらリクエストを拒否するという実装になっていました。

CPUCircuitBreaker

CPUCircuitBreaker で監視しているのは ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverate() なので OS から取得するロードアベレージです。設定するのはCPU使用率なので、こちらも何らかの変換をしているのかと思いきや、何とソースコードではCPU使用率の閾値(%)とロードアベレージ(0以上の数値、CPUの数にもよるが通常はせいぜい10未満)を直接比較していました。

これだと使用率のつもりで75などと設定しても全く引っ掛からないのはずで不思議に思って調べてみたところ、どうやらバグのようで JIRA に Issue が作られていました。

CPU circuit breaker needs to use CPU utilization, not Unix load average

タイトルそのまんまですね。
ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverate() の代わりに ManagementFactory.getOperatingSystemMXBean().getSystemCPULoad() (0.0-0.1の間のCPU使用率)を使うことが提案されており、近いうちに修正されることと思います。