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

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


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

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

画像の一括リサイズしたいときありますよね。
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 を選択することになりそうです。