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

梅酒を作る

今年も梅酒の季節が来ました。
多分出来合いのものを買ったほうが安いしおいしいと思うのですが、自己満足で作るようにしてます。

材料

  • 南高梅1kg
  • 氷砂糖700g前後
  • ホワイトリカー1.8リットル
  • 容器(4リットル)

材料は毎年だいたい5月末から6月にスーパーで果実酒コーナーが作られて全部まとめて用意されています。

ちなみになぜ氷砂糖なのか調べてみたところ、浸透圧を利用するためだそうです。

  1. 作りたては梅の実が一番濃い状態
  2. 浸透圧により梅の実に水分が移動する
  3. 時間とともに氷砂糖がゆっくりと溶け出し、梅の実よりも外のほうが濃い状態になる
  4. 浸透圧により梅の実から水分が外に移動する。この時に梅エキスも一緒に出ていく

作り方

  1. 容器の洗浄と消毒
    容器によっては熱湯消毒をすると割れてしまうので気をつけてください。
  2. 水洗い
  3. 水分を拭き取り乾かす
  4. ヘタをとる
  5. 容器へ入れる
    なるべく梅と氷砂糖が交互になるように
  6. ホワイトリカーを入れる
  7. しっかりとフタを締めて冷暗所へ

だいたい半年くらいから飲めるようです。
2年目が一番好まれているとかなんとか
手軽にできる割に満足感が高いのでぜひお試しください。

※飲酒は20歳になってから
※飲酒運転は法律で禁止されています。
※アルコール度数が低いものを使ったり自分が楽しむ目的でない場合、酒税法違反となる場合があります。


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


画像ファイルの一括サイズ変更

いやーオリンピック盛り上がりましたね。(まだはじまったばかりのときに書いてます。)
マエダです。

画像の一括リサイズしたいときありますよね。
Macを利用していると標準のプレビューアプリでサイズ変更できたりします。
※ 是非Google先生で検索してみてください。

今回はImageMagickを利用する方法をご紹介します。

1. MacにImageMagickをインストールする

※ homebrewのインストール方法は割愛です。

brew install imagemagick

2. mogrifyコマンドを実行する

以下は縦横比(アスペクト比)を維持して一括リサイズする例です。

mogrify -resize 640x480 *.jpg
mogrify -resize 640x480 *.png

以下は縦横比(アスペクト比)を維持せず一括リサイズする例です。

mogrify -resize 640x480! *.jpg
mogrify -resize 640x480! *.png

1ファイルずつリサイズしていたときがなんだったんだろうかという気分になるくらいかんたんです。

さらに背景を設定してアスペクト比の異なる画像を同じサイズ(キャンバスサイズ?)にするためには以下のように一括で設定できます。

番外編 背景画像を設定

例としてwhite_background.jpgという画像を設定します。

find . -type f -print0 | xargs -0 -I% composite -gravity center -compose over % ../white_background.jpg %

いかがだったでしょうか。
非常に楽ちんですね。

ツールを使用した画像最適化について弊社デザイナの記事ありますのでこちらもチェックしてみてください!

画像最適化、してますか?


Raspberry Pi に Chromium OS をインストールしてみた

はじめに

Chromebook のことを調べているときに、ラズパイに Chromium OS ってインストールできるのかなとふと思いついて調べてみたらできることが判ったので試しにやってみました。

インストールイメージのダウンロード

FydeOSのRaspberry Pi用Chromium OSのページからインストールイメージをダウンロードします。この記事を書いたときのインストールイメージは chromiumos_image_r86r2-rpi4b.img.xz でした。

インストールイメージをSDカードに書き込む

Raspberry Pi Imagerを使いました。Raspberry Pi Imagerは対象のSDカードに以前別のOSをインストールしていたりしてパーティションが分けられているような場合でも適切に作り直してくれるので、圧倒的に楽になりましたね。

起動・初期設定

書き込みが終わったSDカードをRaspberry Piに挿して起動するとすんなりChromium OSが立ち上がりました。初期設定として、言語・キーボードを選択して無線LANの設定をしたらすぐに使えるようになりました。

Linuxを使えるようにする

せっかくなので Linux を使えるようにします。Chrome OSはLinuxカーネルを使ったディストリビューションの1つという説明をさせることがありますが、ここで言うLinuxはChrome OSの素の部分のことではなく、裏で起動される仮想マシン上のもののようです。

7.5GBのディスク領域が推奨されます。

64GBのSDカードを使っている場合は、余裕でLinux用の空き容量がありました。

インストールは簡単です。ユーザ名とディスクサイズを指定して数分待つとターミナルが起動します。仮想マシン上で動いているのは Debian なので、 apt でパッケージをインストールできます。

Emacs もこの通り動きます。

日本語入力

少し前までのバージョンだと日本語入力にはいろいろと不具合があったようですが、今回インストールしたr86r2ではあまり不都合を感じませんでした。

1つ問題だったのは、Linux ターミナルから開いたウィンドウ上では日本語入力できなかったことです。例えば、ターミナル上で開いた Emacs では日本語入力できるが、別ウィンドウとして開いた Emacs では日本語入力できない、といった具合です。

おわりに

インストール自体は簡単でした。

Chromium OS では Google Play から Android アプリをインストールすることはできないので、アプリケーションの追加は Chrome Store からか追加インストールした Linux システムからということになります。

Chromium OS の軽さは魅力ではありますが、実用ということであれば、やはり Raspberry Pi OS を選択することになりそうです。