株式会社クリアコード > ククログ

ククログ


Groongaである時間範囲のデータを集計する方法

月単位や日単位などある時間範囲で検索したい場面は多いです。
こんな時は月単位、日単位で日付情報を格納するカラムを作り、それを用いて検索したくなります。

しかし、検索要件が変わり、新たに年単位や週単位で検索する必要が出た場合、それらの情報を格納するカラムを増やさなければなりません。
日付情報は一つのカラムに格納し、その情報を年単位や週単位に丸めて検索できれば、要件の変更に追従してカラムを増やす必要がなくなります。

Groongaでは、time_classify関数を使って日付情報をある期間で丸めることができます。
time_classify関数には以下の関数が存在し、それぞれ、年、月、週、日、曜日、時間、分、秒単位で時間を丸めることができます。

  • time_classify_year
  • time_classify_month
  • time_classify_week
  • time_classify_day
  • time_classify_day_of_week
  • time_classify_hour
  • time_classify_minute
  • time_classify_second

例えば、time_classify_yearなら同じ年の時間を丸めることができます。
つまり、2020年1月13日と2020年5月21日が、2020年1月1日 00:00:00に丸められます。

これらの関数の値は以下のようにドリルダウンのキーとして使用できるので、ある範囲の時間のデータを集計できます。
例えば以下の例では、売上台帳から、ある製品が月単位でどのくらい売れたかを集計しています。

plugin_register functions/time

table_create Sales TABLE_NO_KEY
column_create Sales name COLUMN_SCALAR ShortText
column_create Sales price COLUMN_SCALAR UInt32
column_create Sales timestamp COLUMN_SCALAR Time

load --table Sales
[
{"name": "Apple" , "price": "256", "timestamp": "2020-01-30 11:50:11.000000"},
{"name": "Apple" , "price": "256", "timestamp": "2020-05-01 10:20:00.000000"},
{"name": "Orange", "price": "122", "timestamp": "2020-05-02 11:44:12.000001"},
{"name": "Apple" , "price": "256", "timestamp": "2020-01-07 19:50:23.000020"},
{"name": "banana", "price": "88" , "timestamp": "2020-05-08 11:00:02.000000"},
{"name": "banana", "price": "88" , "timestamp": "2020-05-08 21:34:12.000001"}
]

select Sales \
  --limit -1 \
  --columns[month].stage initial \
  --columns[month].type Time \
  --columns[month].value "time_classify_month(timestamp)" \
  --drilldowns[sales_per_month_name].keys "month, name" \
  --drilldowns[sales_per_month_name].output_columns "_value.name, _nsubrecs, time_format_iso8601(_value.month)" \
  --drilldowns[sales_per_month_name].limit -1
[
  [
    0,
    1590035787.669446,
    0.003034353256225586
  ],
  [
    [
      [
        6
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "month",
          "Time"
        ],
        [
          "name",
          "ShortText"
        ],
        [
          "price",
          "UInt32"
        ],
        [
          "timestamp",
          "Time"
        ]
      ],
      [
        1,
        1577804400.0,
        "Apple",
        256,
        1580352611.0
      ],
      [
        2,
        1588258800.0,
        "Apple",
        256,
        1588296000.0
      ],
      [
        3,
        1588258800.0,
        "Orange",
        122,
        1588387452.000001
      ],
      [
        4,
        1577804400.0,
        "Apple",
        256,
        1578394223.00002
      ],
      [
        5,
        1588258800.0,
        "banana",
        88,
        1588903202.0
      ],
      [
        6,
        1588258800.0,
        "banana",
        88,
        1588941252.000001
      ]
    ],
    {
      "sales_per_month_name":
      [
        [
          4
        ],
        [
          [
            "name",
            "ShortText"
          ],
          [
            "_nsubrecs",
            "Int32"
          ],
          [
            "time_format_iso8601",
            null
          ]
        ],
        [
          "Apple",
          2,
          "2020-01-01T00:00:00.000000+09:00"
        ],
        [
          "Apple",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "Orange",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "banana",
          2,
          "2020-05-01T00:00:00.000000+09:00"
        ]
      ]
    }
  ]
]

上記の例では、--columns[month]を使って動的にカラムを生成しています。
Groongaには動的カラムというクエリー実行時に一時的に作成できるカラムがあります。
この動的カラムを使って、time_classify_month(timestamp)の結果をmonthカラムに格納しています。

次に--drilldowns[sales_per_month_name]を使って、同じ期間に売れた製品をグループ化します。

ポイントは、--drilldowns[sales_per_month_name].keys "month, name" のところです。
ここでは、どのカラムの値を使ってグループ化するかを指定しています。

monthを指定することで、同じ月のレコードをグループ化しています。また、nameを指定することで、製品名でもグループ化しています。

このように、monthカラムの値と、nameカラムの値をグループ化すると以下の結果になります。

    {
      "sales_per_month_name":
      [
        [
          4
        ],
        [
          [
            "name",
            "ShortText"
          ],
          [
            "_nsubrecs",
            "Int32"
          ],
          [
            "time_format_iso8601",
            null
          ]
        ],
        [
          "Apple",
          2,
          "2020-01-01T00:00:00.000000+09:00"
        ],
        [
          "Apple",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "Orange",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "banana",
          2,
          "2020-05-01T00:00:00.000000+09:00"
        ]
      ]
    }

_nsubrecsの値がグループ化したレコードの数を表します。
したがって、例えば["Apple",2,"2020-01-01T00:00:00.000000+09:00"]という結果なら、nameの値がApplemonthカラムの値が
2020-01-01T00:00:00.000000+09:00というレコードが2件あると解釈します。
(Timeの値は通常だとUNIX時間で表示されますが、わかりにくいのでこの例では、人間に読みやすい形式に変換して出力しています。)
つまり、2020年1月にりんごは2個売れたと解釈できます。

もう少し高度な使い方として、以下のようにドリルダウンを使ってある期間のデータを計算して加工できます。
以下の例では月毎の売上の合計値を出力しています。

select Sales \
  --limit -1 \
  --columns[month].stage initial \
  --columns[month].type Time \
  --columns[month].value "time_classify_month(timestamp)" \
  --drilldowns[sales_per_month_name].keys "month, name" \
  --drilldowns[sales_per_month_name].output_columns "_value.name, _nsubrecs, time_format_iso8601(_value.month)" \
  --drilldowns[sales_per_month_name].limit -1 \
  --drilldowns[sum_sales_per_month].keys month \
  --drilldowns[sum_sales_per_month].calc_target price \
  --drilldowns[sum_sales_per_month].calc_types SUM \
  --drilldowns[sum_sales_per_month].output_columns "time_format_iso8601(_key), _sum"
[
  [
    0,
    1590116735.057712,
    0.0004277229309082031
  ],
  [
    [
      [
        6
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "month",
          "Time"
        ],
        [
          "name",
          "ShortText"
        ],
        [
          "price",
          "UInt32"
        ],
        [
          "timestamp",
          "Time"
        ]
      ],
      [
        1,
        1577804400.0,
        "Apple",
        256,1
        580352611.0
      ],
      [
        2,
        1588258800.0,
        "Apple",
        256,
        1588296000.0
      ],
      [
        3,
        1588258800.0,
        "Orange",
        122,
        1588387452.000001
      ],
      [
        4,
        1577804400.0,
        "Apple",
        256,
        1578394223.00002
      ],
      [
        5,
        1588258800.0,
        "banana",
        88,
        1588903202.0
      ],
      [
        6,
        1588258800.0,
        "banana",
        88,
        1588941252.000001
      ]
    ],
    {
      "sales_per_month_name":
      [
        [
          4
        ],
        [
          [
            "name",
            "ShortText"
          ],
          [
            "_nsubrecs",
            "Int32"
          ],
          [
            "time_format_iso8601",
            null
          ]
        ],
        [
          "Apple",
          2,
          "2020-01-01T00:00:00.000000+09:00"
        ],
        [
          "Apple",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "Orange",
          1,
          "2020-05-01T00:00:00.000000+09:00"
        ],
        [
          "banana",
          2,
          "2020-05-01T00:00:00.000000+09:00"
        ]
      ],
      "sum_sales_per_month":
      [
        [
          2
        ],
        [
          [
            "time_format_iso8601",
            null
          ],
          [
            "_sum",
            "Int64"
          ]
        ],
        [
          "2020-01-01T00:00:00.000000+09:00",
          512
        ],
        [
          "2020-05-01T00:00:00.000000+09:00",
          554
        ]
      ]
    }
  ]
]

ドリルダウンは、同一グループのカラムの値を計算できます。
具体的には、合計値と最大値、最小値、平均値を計算できます。

上記の例では、月ごとの売上を計算したいので、合計値を計算します。

どんな計算をするかは、--drilldowns[sum_sales_per_month].calc_typesで指定しています。
合計値を計算する場合はSUMを指定します。

最大値の場合はMAX、最小値の場合はMIN、平均値の場合はAVGを指定します。
計算した値は、それぞれ_sum_max_min_avgというカラムに格納されます。(これらのカラムは自動的に作成されるので、ユーザーが用意する必要はありません。)

計算対象のカラムは、--drilldowns[sum_sales_per_month].calc_targetで指定しています。
上記の例では売上を集計したいので、priceカラムを指定しています。

最後に--drilldowns[sum_sales_per_month].output_columnsで出力する情報を指定しています。
月毎の売上がわかれば良いので、月と売上の合計値が出力されれば良いことになります。

したがって、--drilldowns[sum_sales_per_month].output_columnsには、_key_sumを指定しています。
_keyはドリルダウンのキーを表すのでmonthカラムの値を出力します。
_sumはグループ内のpriceカラムの値の合計値を出力します。

結果は以下のようになり、この店舗では、2020年1月の売上は512円で2020年5月の売上は554円であることがわかります。

        [
          "2020-01-01T00:00:00.000000+09:00",
          512
        ],
        [
          "2020-05-01T00:00:00.000000+09:00",
          554
        ]

このようにして、月毎の売上の合計値を出力できました。

月単位や日単位などある時間範囲で集計したい場面に遭遇したら、time_classify関数の使用を検討してみてはいかがでしょうか?

タグ: Groonga
2020-05-22

GitHub Actionsを使ったGroongaのパッケージのインストール、テストの自動化

Groongaでは、これまでもGitHub Actionsを使ってパッケージの作成を自動化したり、テストの自動化を実施してきました。
いままで自動化してきたテストは、リポジトリーにpushされたソースコードに対してビルド、テストするものでした。
これらの自動化により、リリース前に初めて問題が発覚することが少なくなり、問題が発生した段階で対処を進めることができています。

ただ、リポジトリーにpushされたソースコードに対するテストだと、各OS向けに作成したパッケージがちゃんとインストールできるか、
パッケージからインストールした環境で動作するかは確認できていませんでした。
そのため、パッケージの作成に失敗していた場合には、リリース後、パッケージからGroongaをインストールする段階にならないと問題に気がつけない状態でした。

リリース後にパッケージに問題があるとわかった場合は、再リリースすることになり、余計な時間がかかってしまいます。
そこで、リポジトリーにソースコードがpushされた段階でパッケージのインストールとパッケージからインストールしたGroongaのテストを実行するようにしました。

この記事は、作成したパッケージをインストール、テストする方法を説明したものです。
Groongaに固有の部分もありますが、各OS向けにパッケージを提供しているプロジェクトにとって参考になる情報もあると思います。

パッケージのテスト環境の構築

パッケージをテストするためには、当然テスト対象のパッケージを作成する必要がありますが、Groongaでは、既に自動化されています。
パッケージは既にできているので、この記事では、作成されたパッケージを取得するところから説明します。

前述の通りGroongaでは、リポジトリーにソースコードがpushされるたびにパッケージの作成が実行されるので、パッケージ作成後にテストを実行します。

パッケージの作成とパッケージのテストのジョブを分けても良いのですが、そのようにすると、パッケージを作成するジョブでartifactsにパッケージを保存し、
パッケージをテストするジョブでは、artifactsから必要なパッケージをダウンロードする操作が必要になり煩雑です。
(GitHub Actionsでは、パッケージ等のワークフローの成果物をartifactsとして保持できます。)

そのため、Groongaではパッケージの作成とパッケージのテストは同一のジョブで実行しています。

具体的には、以下のようにしています。

  1. Docker上にホストのディレクトリをマウント
  2. テスト用のイメージ、テストスクリプトを指定
# Test
- name: Test
  run: |
    docker run \
      --rm \
      --tty \
      --volume ${PWD}:/groonga:ro \
      ${{ matrix.test-docker-image }} \
      /groonga/${{ matrix.test-script }}

上記設定は、 https://github.com/groonga/groonga/blob/v10.0.2/.github/workflows/package.yml#L158 に記載されています。

1. Docker上にホストのディレクトリをマウント

docker run--volumeオプションを使って、ホストのディレクトリをDocker上にマウントできます。
上記では、--volume ${PWD}:/groonga:roと指定されているので、現在居るディレクトリをDcoker上の/groongaにマウントしています。

2. テスト用のイメージ、テストスクリプトを指定

${{ matrix.test-docker-image }}で実行するイメージを指定し、/groonga/${{ matrix.test-script }}で実行するテストスクリプトを指定しています。

GitHub Actions上で作成しているパッケージは、CentOS向けとDebian向けのものなので、この2つのOSのイメージを使用します。
複数のバージョンがあるので、matrixを使用し、バージョンごとにイメージを変更してテストしています。

${{ matrix.test-docker-image }}は以下のように定義されているので、Debian stretch、Debian buster、CentOS6、CentOS7、CentOS8のイメージを使用するようになっています。
テストに使用するスクリプトもOSによって異なるので、${{ matrix.test-script }}としてそれぞれのパスを指定しています。

        include:
          - label: Debian GNU/Linux stretch amd64
            id: debian-stretch-amd64
            test-docker-image: debian:stretch
            test-script: packages/apt/test.sh
          - label: Debian GNU/Linux stretch i386
            id: debian-stretch-i386
            test-docker-image: i386/debian:stretch
            test-script: packages/apt/test.sh
          - label: Debian GNU/Linux buster amd64
            id: debian-buster-amd64
            test-docker-image: debian:buster
            test-script: packages/apt/test.sh
          - label: Debian GNU/Linux buster i386
            id: debian-buster-i386
            test-docker-image: i386/debian:buster
            test-script: packages/apt/test.sh
          - label: CentOS 6
            id: centos-6
            test-docker-image: centos:6
            test-script: packages/yum/test.sh
          - label: CentOS 7
            id: centos-7
            test-docker-image: centos:7
            test-script: packages/yum/test.sh
          - label: CentOS 8
            id: centos-8
            test-docker-image: centos:8
            test-script: packages/yum/test.sh

また、テストに使用するイメージは既存のイメージを再利用せず、新規に作るのが良いです。
新規の環境でテストしないと依存ライブラリーが足りない等の問題に気がつけない可能性があるためです。

ここまでで、それぞれのDockerイメージ上でテストを実行する準備が整いました。
次は、パッケージのインストールとテストを実施します。
パッケージのインストールとテストは、テストスクリプト内で実施しています。

パッケージのインストールとテストの実施

Debian向け、CentOS向けのパッケージのテストスクリプトは、以下の場所にあります。
ここからは、これらのスクリプトの内容を説明し、どのような流れでパッケージのインストールとテストを行っているかを説明します。

Debian向け:
https://github.com/groonga/groonga/blob/v10.0.2/packages/apt/test.sh

CentOS向け:
https://github.com/groonga/groonga/blob/v10.0.2/packages/yum/test.sh

基本的な流れはどちらのスクリプトも同じで、それぞれのOSのパッケージ管理システム(Debianならapt、CentOS6,7ならyum、CentOS8ならdnf)を使って
ホストからマウントしたディレクトリにある、パッケージをインストールし、その後grntest(Groonga用のテストツール)を使ってGroongaのテストを実行しています。

ただ、CentOS6,7はRubyのバージョンが古く、grntestを使ったテストができないので、パッケージのインストールのみを確認しています。
また、CentOS8は、CentOS8向けのMessagePackのパッケージが無いため、現状ではテストを実行せずにスクリプトを終了しています。
(MessagePackがないとすべてのテストを実行できないためです。)

Debian向けパッケージのインストール、テストの流れ

まず、テストスクリプトの以下の箇所でOSのコードネームとアーキテクチャを取得します。
これらは、インストールするパッケージのパスに使用します。

code_nameには、stretchbuster等のDebianの各バージョンのコードネームが入ります。
architectureには、amd64i386等のCPUアーキテクチャが入ります。

code_name=$(lsb_release --codename --short)
architecture=$(dpkg --print-architecture)

次にインストールするパッケージのパスを指定してaptコマンドでパッケージをインストールします。
aptコマンドは、APTリポジトリを用意しなくても、以下のようにローカルに保存されているパッケージを直接指定してインストールできます。

repositories_dir=/groonga/packages/apt/repositories
apt install -V -y \
  ${repositories_dir}/debian/pool/${code_name}/main/*/*/*_{${architecture},all}.deb

インストール後、インストールが成功しているかを、groongaコマンドを使って確認します。

groonga --version

次にテスト用のディレクトリを作成し、テストスクリプトを移動します。
この段階で、i386の環境では動作しないテストを削除します。

mkdir -p /test
cd /test
cp -a /groonga/test/command ./
if [ "${architecture}" = "i386" ]; then
  rm command/suite/ruby/eval/convert/string_to_time/over_int32.test
  # TODO: debug this
  rm command/suite/select/filter/geo_in_circle/no_index/north_east.test
fi

最後にgemgrntestをインストールし、テストを実行しています。

apt install -V -y \
  gcc \
  make \
  ruby-dev
gem install grntest

export TZ=Asia/Tokyo

grntest_options=()
grntest_options+=(--base-directory=command)
grntest_options+=(--n-retries=3)
grntest_options+=(--n-workers=$(nproc))
grntest_options+=(--reporter=mark)
grntest_options+=(command/suite)
grntest "${grntest_options[@]}"
grntest "${grntest_options[@]}" --interface http
grntest "${grntest_options[@]}" --interface http --testee groonga-httpd
CentOS向けパッケージのインストール、テストの流れ

まず、テストスクリプトの以下の箇所でOSのバージョンを取得します。
これは、パッケージのパスを指定するのとパッケージ管理システムのコマンドを指定するのに使います。
(CentOSのパッケージ管理は、CentOS6,7では、yumコマンドで実行しますが、CentOS8では、dnfコマンドで実行するためです。)

version=$(cut -d: -f5 /etc/system-release-cpe)

以下の箇所でバージョン毎にパッケージ管理システムのコマンドを指定します。

case ${version} in
  6|7)
    DNF=yum
    ;;
  *)
    DNF="dnf --enablerepo=PowerTools"
    ;;
esac

以下の箇所でGroongaの公開鍵のインポートを行い、RPMパッケージをインストールします。
yumdnfコマンドもaptコマンドと同様、YUMリポジトリを用意しなくても、以下のようにローカルに保存されているパッケージを直接指定してインストールできます。

${DNF} install -y \
  https://packages.groonga.org/centos/groonga-release-latest.noarch.rpm

repositories_dir=/groonga/packages/yum/repositories
${DNF} install -y \
  ${repositories_dir}/centos/${version}/x86_64/Packages/*.rpm

インストールが成功しているかを、groongaコマンドを使って確認します。

groonga --version

CentOS6,7はRubyのバージョンが古くgrntestが動作しないためここでスクリプトを終了します。

case ${version} in
  6|7)
    exit 0
    ;;
  *)
    ;;
esac

CentOS8は、以下の箇所でgrntestをインストール後テストを実行します。
ただ、前述の通り現在は、CentOS8向けのMessagePackのパッケージが無いため、テスト実行前にexit 0でスクリプトを終了しています。

# TODO: Require msgpack for testing normalizer options
exit 0

${DNF} install -y \
  gcc \
  make \
  redhat-rpm-config \
  ruby-devel
gem install grntest

export TZ=Asia/Tokyo

grntest_options=()
grntest_options+=(--base-directory=/groonga/test/command)
grntest_options+=(--n-retries=3)
grntest_options+=(--n-workers=$(nproc))
grntest_options+=(--reporter=mark)
grntest_options+=(/groonga/test/command/suite)

grntest "${grntest_options[@]}"
grntest "${grntest_options[@]}" --interface http
grntest "${grntest_options[@]}" --interface http --testee groonga-httpd

以上のようにして、Groongaではリポジトリへのpushをトリガーとして、パッケージの作成、インストール、テストまでを自動で実行しています。
こうすることで、作成したパッケージがインストールできないといった問題を未然に防げるようになり、より安定したものをリリースできます。

各OSに向けのパッケージを配布しているプロジェクトは、上記のようなやり方を参考にして、パッケージのテストも自動化してみてはいかがでしょうか?

タグ: Groonga
2020-05-05

Groongaの単語抽出演算子を使ったタグの付与

Groongaにはタグ検索という機能があります。

例えば、動画共有サイトであれば、登録されている動画に、その動画の特徴を表す短いキーワード(「スポーツ」、「カバディ」など)が含まれています。
これらの短いキーワードをタグといいます。
このタグで検索することで、動画のタイトルだけではなく、動画の特徴による検索ができ、より良い検索結果を提供できます。

上記の動画の例で言えば、動画のタイトルによる検索では自分が検索時に見たいと思った動画しか検索できませんが、タグ検索をすると、検索時には意識していなかった興味のある動画を見つけることができます。

Groongaの公式ドキュメントの例は小規模なためデータにタグを人力で付与してデータを登録することができますが、実際に使用するケースではデータはもっと巨大です。
巨大なデータに人力でタグを付与するのは、とても困難な作業です。

データ登録時にユーザーにタグを指定してもらうインターフェースにするというのも解決策の一つです。
しかし、既にあるデータに対してタグ検索をしたいという要件が後から追加された場合は、上記の案では解決できません。
既に登録されたデータにタグとなるキーワードが含まれているかどうかを確認し、含まれているキーワードをタグとして登録する必要があります。
このような作業は人力ではなくコンピュータに任せたいところです。

Groongaでは、単語抽出演算子という演算子があり、これを使うことであらかじめ登録しておいた単語を抽出できます。
あらかじめタグとして使用するキーワードを登録しておき、タグを付与する対象のデータにこれらのキーワードが含まれていれば、そのキーワードを抽出できます。
したがって、この単語抽出演算子を使って抽出したキーワードをそのレコードのタグとして登録することで、検索用のタグを付与できます。

具体的には以下のように使用します。

まず、以下のようにタグとして抽出したいキーワードを_keyとして登録したテーブルを用意します。
抽出したいキーワードはテーブルのキーとして登録しておく必要があります。
また、抽出したいキーワードを登録するテーブルは、TABLE_PAT_KEYTABLE_DAT_KEYである必要があります。

以下の例では、Groonga, Mroonga, PGroonga, RroongaRuby, PostgreSQL, MySQL, Fulltextをタグとして使用するキーワードとして登録します。

table_create Words TABLE_PAT_KEY ShortText --normalizer NormalizerNFKC121
load --table Words
[
  {"_key": "Groonga"},
  {"_key": "Mroonga"},
  {"_key": "PGroonga"},
  {"_key": "Rroonga"},
  {"_key": "Ruby"},
  {"_key": "PostgreSQL"},
  {"_key": "MySQL"},
  {"_key": "Fulltext"}
]

単語抽出演算子の構文は_key *T "document"です。
documentの部分には、抽出対象のデータそのものをいれます。例えば、"Groonga is the successor project to Senna."という文書から上記で登録したキーワードを抽出したい場合は以下のようなクエリーを実行します。

select \
  --table Words \
  --filter '_key *T "Groonga is the successor project to Senna."' \
  --output_columns _key
[
  [
    0,
    1587435831.615494,
    0.003186702728271484
  ],
  [
    [
      [
        1
      ],
      [
        [
          "_key",
          "ShortText"
        ]
      ],
      [
        "groonga"
      ]
    ]
  ]
]

この文書には、タグとして抽出したいキーワードのGroongaが含まれているので、上記のクエリーでGroongaが抽出されています。
抽出結果が"groonga"と小文字になっているのは、Wordsテーブルに指定しているNormalizerNFKC121がキーワードを正規化しているためです。
キーワードを正規化するのは、大文字/小文字の区別なくヒットさせるためです。
この抽出されたキーワードをタグとして登録します。

それではまず、説明のためタグを付与する前のデータを登録します。

table_create --name FullTextSearchEngines --flags TABLE_HASH_KEY --key_type UInt32
column_create --table FullTextSearchEngines --name name --flags COLUMN_SCALAR --type ShortText
column_create --table FullTextSearchEngines --name description --flags COLUMN_SCALAR --type ShortText

load --table FullTextSearchEngines
[
  {
    "_key":1,
    "name":"Groonga",
    "description":"Groonga is a fast and accurate full text search engine based on inverted index. One of the characteristics of Groonga is that a newly registered document instantly appears in search results. Also, Groonga allows updates without read locks. These characteristics result in superior performance on real-time applications."
  },
  {
    "_key":2,
    "name":"Mroonga",
    "description":"Mroonga is a storage engine for MySQL. It provides fast fulltext search feature for all languages including Chinese, Japanese and Korean to all MySQL users. Mroonga was called Groonga storage engine"
  },
  {
    "_key":3,
    "name":"PGroonga",
    "description":"PGroonga is a PostgreSQL extension to use Groonga as the index.PostgreSQL supports full text search against languages that use only alphabet and digit. It means that PostgreSQL does not support full text search against Japanese, Chinese and so on. You can use super fast full text search feature against all languages by installing PGroonga into your PostgreSQL!"
  },
  {
    "_key":4,
    "name":"Rroonga",
    "description":"Rroonga provides Groonga's DB-API layer features to Ruby. Features specialized to Web applications built on Rroonga are provided by ActiveGroonga. Convenience features for search Web applications are provided by racknga. All of them have the same merit that you can use Groonga features via Rubyish useful API. "
  },
  {
    "_key":5,
    "name":"Droonga",
    "description":"Droonga is a distributed full-text search engine, based on a stream oriented processing model. In many operations (searching, updating, grouping, and so on), Droonga processes various data by pipeline. As the result, Droonga has large potential around its flexibility and extensibility. Moreover, those features provide high availability for people who develop any data processing engine based on Droonga. You can process complex operations by mixing operations, and you can add custom operations to Droonga via plugins written as Ruby-scripts."
  }
]

上記の通り、全文検索エンジンの説明をデータとして登録しました。
nameが全文検索エンジンの名前、descriptionが全文検索エンジンの説明を表しています。
今回はこのdescriptionからキーワードを抽出して、タグとして書くレコードに付与します。

次は、単語抽出演算子を使ってタグとして使用するキーワードを抽出します。

select \
  --table Words \
  --filter '_key *T "Groonga is a fast and accurate full text search engine based on inverted index. One of the characteristics of Groonga is that a newly registered document instantly appears in search results. Also, Groonga allows updates without read locks. These characteristics result in superior performance on real-time applications."' \
  --output_columns _key
[
  [
    0,
    1587436031.572823,
    0.001593351364135742
  ],
  [
    [
      [
        1
      ],
      [
        [
          "_key",
          "ShortText"
        ]
      ],
      [
        "groonga"
      ]
    ]
  ]
]

select \
  --table Words \
  --filter '_key *T "Mroonga is a storage engine for MySQL. It provides fast fulltext search feature for all languages including Chinese, Japanese and Korean to all MySQL users. Mroonga was called Groonga storage engine"' \
  --output_columns _key
[
  [
    0,
    1587436162.267088,
    0.0009558200836181641
  ],
  [
    [
      [
        4
      ],
      [
        [
          "_key",
          "ShortText"
        ]
      ],
      [
        "mroonga"
      ],
      [
        "mysql"
      ],
      [
        "fulltext"
      ],
      [
        "groonga"
      ]
    ]
  ]
]

select \
  --table Words \
  --filter '_key *T "PGroonga is a PostgreSQL extension to use Groonga as the index.PostgreSQL supports full text search against languages that use only alphabet and digit. It means that PostgreSQL does not support full text search against Japanese, Chinese and so on. You can use super fast full text search feature against all languages by installing PGroonga into your PostgreSQL!"' \
  --output_columns _key
[
  [
    0,
    1587436237.72361,
    0.001041412353515625
  ],
  [
    [
      [
        3
      ],
      [
        [
          "_key",
          "ShortText"
        ]
      ],
      [
        "pgroonga"
      ],
      [
        "groonga"
      ],
      [
        "postgresql"
      ]
    ]
  ]
]

select \
  --table Words \
  --filter '_key *T "Rroonga provides Groonga\'s DB-API layer features to Ruby. Features specialized to Web applications built on Rroonga are provided by ActiveGroonga. Convenience features for search Web applications are provided by racknga. All of them have the same merit that you can use Groonga features via Rubyish useful API. "' \
  --output_columns _key
[
  [
    0,
    1587436322.47859,
    0.0007700920104980469
  ],
  [
    [
      [
        3
      ],
      [
        [
          "_key",
          "ShortText"
        ]
      ],
      [
        "rroonga"
      ],
      [
        "groonga"
      ],
      [
        "ruby"
      ]
    ]
  ]
]

select \
  --table Words \
  --filter '_key *T "Droonga is a distributed full-text search engine, based on a stream oriented processing model. In many operations (searching, updating, grouping, and so on), Droonga processes various data by pipeline. As the result, Droonga has large potential around its flexibility and extensibility. Moreover, those features provide high availability for people who develop any data processing engine based on Droonga. You can process complex operations by mixing operations, and you can add custom operations to Droonga via plugins written as Ruby-scripts."' \
  --output_columns _key
[
  [
    0,
    1587436396.106364,
    0.0008873939514160156
  ],
  [
    [
      [
        1
      ],
      [
        [
          "_key",
          "ShortText"
        ]
      ],
      [
        "ruby"
      ]
    ]
  ]
]

これで各レコードが持つキーワードが抽出できました。
次は、抽出したキーワードをタグとして登録します。

以下では、既に作成したFullTextSearchEnginesテーブルにtagsカラムを追加しています。
tagsカラムはTagsテーブルへの参照になっており、Tagsテーブルは、tagsカラムに登録した文字列をキーとするレコードが作られます。
また、Tagsテーブルはtagsカラムに対するインデックスをindex_tagsカラムに格納してます。これにより、タグの全文検索を高速に実行できます。

table_create --name Tags --flags TABLE_HASH_KEY --key_type ShortText --normalizer NormalizerNFKC121
column_create --table FullTextSearchEngines --name tags --flags COLUMN_VECTOR --type Tags
column_create --table Tags --name index_tags --flags COLUMN_INDEX --type FullTextSearchEngines --source tags

load --table FullTextSearchEngines
[
  {
   "_key":1,
   "name":"Groonga",
   "description":"Groonga is a fast and accurate full text search engine based on inverted index. One of the characteristics of Groonga is that a newly registered document instantly appears in search results. Also, Groonga allows updates without read locks. These characteristics result in superior performance on real-time applications.",
   "tags":["groonga"]
  },
  {
    "_key":2,
    "name":"Mroonga","description":"Mroonga is a storage engine for MySQL. It provides fast fulltext search feature for all languages including Chinese, Japanese and Korean to all MySQL users. Mroonga was called Groonga storage engine",
    "tags":["mroonga" ,"mysql" ,"fulltext", "groonga"]
  },
  {
    "_key":3,
    "name":"PGroonga",
    "description":"PGroonga is a PostgreSQL extension to use Groonga as the index.PostgreSQL supports full text search against languages that use only alphabet and digit. It means that PostgreSQL does not support full text search against Japanese, Chinese and so on. You can use super fast full text search feature against all languages by installing PGroonga into your PostgreSQL!",
    "tags":["pgroonga", "groonga", "postgresql"]
  },
  {
    "_key":4,
    "name":"Rroonga",
    "description":"Rroonga provides Groonga's DB-API layer features to Ruby. Features specialized to Web applications built on Rroonga are provided by ActiveGroonga. Convenience features for search Web applications are provided by racknga. All of them have the same merit that you can use Groonga features via Rubyish useful API."
    "tags":["rroonga","groonga", "ruby"]
  },
  {
    "_key":5,
    "name":"Droonga","description":"Droonga is a distributed full-text search engine, based on a stream oriented processing model. In many operations (searching, updating, grouping, and so on), Droonga processes various data by pipeline. As the result, Droonga has large potential around its flexibility and extensibility. Moreover, those features provide high availability for people who develop any data processing engine based on Droonga. You can process complex operations by mixing operations, and you can add custom operations to Droonga via plugins written as Ruby-scripts."
    "tags":["ruby"]
  }
]

これで、既存のデータにタグを付与し、タグ検索をする準備が整いました。
後は、以下のクエリーを実行すればタグによる検索が実行できます。

select --table FullTextSearchEngines --query tags:@Groonga --output_columns _key,name

上記では、Groongaというタグを持つレコードを検索しており、結果は以下のようになります。

[
  [
    0,
    1587436478.485265,
    0.002483844757080078
  ],
  [
    [
      [
        4
      ],
      [
        [
          "_key",
          "UInt32"
        ],
        [
          "name",
          "ShortText"
        ]
      ],
      [
        1,
        "Groonga"
      ],
      [
        2,
        "Mroonga"
      ],
      [
        3,
        "PGroonga"
      ],
      [
        4,
        "Rroonga"
      ]
    ]
  ]
]

無事にGroongaをタグに持つレコードが検索できました。

今回は、Groongaの単語抽出演算子を使って既存のデータにタグを付与してタグ検索をする方法を紹介しました。
既にあるデータにタグ検索機能を追加する場合には、この単語抽出機能の使用を検討してみてはいかがでしょうか。

タグ: Groonga
2020-04-21

第28回 中国地方DB勉強会 in 岡山:Amazon RDS + Amazon EC2 + ロジカルレプリケーションを使った低コスト高速全文検索 #ChugokuDB

2020年1月25日(土)に第28回 中国地方DB勉強会 in 岡山が開催されました。
私は、昨年の2019年11月に行われた PostgreSQL Conference Japan 2019 の内容(「Amazon RDS + Amazon EC2 + ロジカルレプリケーションを使った低コスト高速全文検索」)を再演させていただきました。

これは、既存の技術を組み合わせて、なるべく楽に高速、高機能な全文検索ができる仕組みを紹介したものです。

当日使用した資料は、以下に公開しています。
PostgreSQL Conference Japan 2019 の資料とほぼ同じですが、 PostgreSQL Conference Japan 2019 で頂いた質問を元に一部加筆しています。

関連リンク:

内容

内容については、PostgreSQL Conference Japan 2019 の再演となりますので、PostgreSQL Conference Japan 2019:Amazon RDS + Amazon EC2 + ロジカルレプリケーションを使った低コスト高速全文検索 #pgcon19jの内容とほぼ同じです。

ただ、PostgreSQL Conference Japan 2019 にて、この構成は同期レプリケーションと非同期レプリケーションのどちらを使っているのか?という質問をいただいていたので、この質問についての回答を追記しています。(スライドの85 - 90ページが追記した内容です。)

以下に追記した内容について記載します。

同期、非同期どちらのレプリケーションでもAmazon RDS + Amazon EC2 + ロジカルレプリケーション + PGroongaを使った低コスト高速全文検索の構成を使うことができます。

ただ、同期、非同期のレプリケーションを使った構成にはそれぞれ以下の特徴があるので、これらの特徴を踏まえた上でユースケースに合わせて選択することをおすすめします。

  • 同期レプリケーション
    • 同期レプリケーションでは、Subscriberの更新を待ってから応答を返すため、非同期レプリケーションに比べて更新性能は落ちてしまう。
    • Subscriberの更新を待つので、PublisherとSubscriberのデータは同一であることが保証される。
  • 非同期レプリケーション
    • 非同期レプリケーションでは、Subscriberの更新を待たずに応答を返すため、同期レプリケーションに比べて更新性能は高い。
    • Subscriberの更新を待たないので、PublisherとSubscriberのデータが同一でないことがある。(タイミングによっては、更新前のデータが見えてしまう。)

同期レプリケーションでは、PublisherとSubscriberのデータが同一であることが保証されるので、更新したデータを即時検索できることがとても重要な場合は、同期レプリケーションを選択することをおすすめします。

そうではない場合は、非同期レプリケーションを選択して更新性能を落とさず検索できるようにすることをおすすめします。

まとめ

今回の発表にあたって、追記した内容を中心に紹介いたしました。
中国地方DB勉強会の皆様、中国地方のコミュニティでの発表という貴重な機会をいただきありがとうございました。

最後に、PGroongaを使った全文検索について、興味、疑問などございましたら、是非、お問い合わせください。

タグ: Groonga
2020-01-28

GitHub Actionsを使ったパッケージ作成の自動化

2019年の11月にGitHub Actionsが正式にリリースされました。
GitHub ActionsはGitHubに組み込まれたCI/CD機能でpush等のGitHub上のイベントをトリガーに任意のアクションを実行できるものです。
GitHub ActionsではDockerが使用できるので、様々な環境上でテストの実行やビルドなどができます。

CIサービスは、他にもAppVeyorやTravis CIがありますが、AppVeyorは無料のプランだと、ワーカーがプロジェクトにつき1つなので、Groongaのように複数のパッケージを作成するためにジョブが多くなるプロジェクトだと、ビルドとテストの完了に時間がかかってしまい効率的ではありませんでした。
GitHub Actionsでは、リポジトリーにつき20まで並列で実行できます。

また、Travis CIでは、ビルドした成果物を保存する場所がデフォルトで用意されていないため、正常にビルドできるかの確認やテスト実行はできますが、リリース用のパッケージを作成して置いておくということがやりにくいです。
GitHub Actionsではアクションを実行した結果、生成されたファイルを置いておく場所がデフォルトで用意されているため、GitHub Actions上でパッケージを作成し、作成したパッケージを取得してリリース用のパッケージとしてアップロードするということがやりやすいです。

上記のようなメリットがあったため、Groongaプロジェクトでは、CI/CDをTravis CIやAppveyorからGitHub Actionsへ移行しています。

いままで、GroongaやMroonga、PGroongaのパッケージは開発者の手元でビルド、署名、アップロードを実施していました。
GroongaとMroongaは毎月末にリリースがあるため、毎月末に開発者はパッケージを作る作業を実施しなければなりません。
また、この際にパッケージの作成に失敗するような問題が発覚すると、問題の修正作業にも追われることになります。

GitHub Actionsはpushトリガーでアクションを実行できるので、変更がリポジトリにpushされた段階でパッケージ作成することで、問題の早期発見に繋がり、問題を発生させた変更もすぐに特定できます。
また、GitHub Actionsでパッケージを作成すると開発者はパッケージを署名、アップロードするだけでよくなるので、毎月発生したパッケージ作成時間をなくし、その時間を別の作業に充てることができます。

GitHub Actionsを活用すると、上記のような様々なメリットが発生するので、GroongaとMroonga、PGroongaのパッケージの作成をGitHub Actionsで行うようにしました。この記事は、その過程で得た知見を共有するために記載しています。

まずは、GitHub Actionsの使い方について記載します。

使い方

GitHub ActionsのアクションはYAML形式で記述します。
YAML形式なので、GitHub上でコードとして管理できます。

ここからは、Groongaのパッケージ作成のアクションを例に説明していきます。
Groongaのパッケージ作成のアクションは、以下の場所にあります。

https://github.com/groonga/groonga/blob/master/.github/workflows/package.yml

トリガー

何のイベントをトリガーにしてアクションを実行するかを決めるには、on:を使います。
Groongaのアクションではpush毎にアクションが実行されてほしいので、on:にはpushを指定します。

on:
  push:

GitHub Actionsでは、イベントが起こった対象を指定することができます。
上記のように定義すると、リポジトリーのどのブランチ、タグにpushされてもアクションが実行されます。

全てのブランチやタグが対象ではなく、特定のブランチ、タグにpushされたときだけアクションを実行したい場合は以下のようにします。

on:
  push:
    branches:
      - branch
    tags:
      - tag

push等のイベントトリガーではなく、定期的に実行したいアクションがあるケースもあります。
その場合には、schedule:を使用します。

schedule:の書式はcronの書式で指定できます。ここで指定した時刻はUTCなので、UTCと時差がある日本の場合は時差を考慮に入れて設定する必要があることに注意してください。

Groongaでは以下のように設定し、毎朝9時にアクションが実行されるようにしています。

  schedule:
    - cron: |
        0 0 * * *

onに指定できるイベントは他にもあり、以下のページにまとまっているので、必要に応じて参考にしてください。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows

アクションの定義

実行するアクションはjobs:という単位で定義できます。
複数のジョブを作成してそれぞれ並列に実行することもできますし、あるジョブの完了を待ってから実行するというようにジョブ同士の依存関係を定義することもできます。

Groongaでは、現状、パッケージを作成するというアクションのみを行っていますので、以下のようにbuildというジョブを1つだけ定義しています。

jobs:
  build:

ジョブの中ではいくつかのステップに分けてアクションを定義します。
GitHub Actionsのジョブはステップごとに成功、失敗を表示するので、ジョブをステップに分割しておくと、ジョブが失敗したときの原因特定が容易になります。

Groongaでは、パッケージ作成を以下のステップに分割して実行しています。

  • 依存パッケージの配置
    • aptを用いたインストール
    • pip3を用いたインストール(ドキュメントビルド用に使うSphinxをインストールしています)
    • パッケージのビルドに必要なソースコードをClone
  • configureの生成
  • ソースアーカイブ作成用にconfigure実行
  • ソースアーカイブをビルド
  • debian/changelogの更新
  • Docker上でパッケージをビルド
  • ビルドされた成果物をGitHubへアップロード

具体的には以下のように定義しています。
name:で分割したステップに名前をつけます。
run:で実行するアクションを定義します。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
      - name: Install dependencies
        run: |
          sudo apt update
          sudo apt -V install \
            autoconf-archive \
            bison \
            devscripts \
            python3-pip \
            ruby
      - name: Install Sphinx
        run: |
          sudo pip3 install -v sphinx
      - name: Clone dependencies
        run: |
          cd ..
          git clone --depth 1 https://github.com/groonga/groonga.org.git
          git clone --depth 1 https://github.com/clear-code/cutter.git
      - name: Generate configure
        run: |
          ./autogen.sh
      - name: Configure for archive
        run: |
          ./configure \
            --enable-document \
            --enable-mruby \
            --with-cutter-source-path=../cutter \
            --with-groonga-org-path=../groonga.org \
            --with-ruby
      - name: Build archive
        run: |
          make dist
      - name: Update version
        run: |
          OLD_RELEASE=$(grep -E -o '[0-9.]+' packages/debian/changelog | \
                          head -n1)
          OLD_RELEASE_DATE_FULL="$(grep '^ -- ' packages/debian/changelog | \
                                     head -n1 | \
                                     sed -E -e 's/ -- .+<[^>]+>  //')"
          OLD_RELEASE_DATE=$(date --date="${OLD_RELEASE_DATE_FULL}" +%Y-%m-%d)
          make update-latest-release \
            OLD_RELEASE=${OLD_RELEASE} \
            OLD_RELEASE_DATE=${OLD_RELEASE_DATE} \
            NEW_RELEASE_DATE=$(date +%Y-%m-%d)
      - name: Build with docker
        run: |
          cd packages
          rake ${{ matrix.rake_arguments }}
        env:
          APACHE_ARROW_REPOSITORY: ../../arrow
      - uses: actions/upload-artifact@master
        with:
          name: packages-${{ matrix.id }}
          path: ${{ matrix.repositories_path }}

どのOSでアクションを実行するかはruns-on:で指定します。Groongaではruns-on:ubuntu-latestと定義して、Ubuntuの最新版でアクションが実行されるようにしています。これ以外にも、windows-latest(Windows Serverの最新版)やmacos-latestなどがサポートされています。

実行するコマンドはruns-on:で定義したOSに応じて定義する必要があります。Ubuntuでアクションを実行する場合は、デフォルトでBashが使われるので、Bashで実行できるコマンドを使って定義しています

windows-latestを指定した場合は、デフォルトでアクションがPowerShellで実行されるので、アクションはPowerShellで実行できるコマンドで定義する必要があります。(コマンドプロンプトを使うように指定することもできます。)

アクションを実行するシェルの指定については、以下のページにも説明があるので、必要に応じて参照してください。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell

GitHub Actionsでは、よくある処理をまとめて公開共有することが可能で、それらの処理はsteps:の中で呼び出せます。
Groongaでは、リポジトリからソースコードをチェックアウトするのにactions/checkoutを、ビルドしたパッケージをアップロードするのにactions/upload-artifactを使用しています。

actions/checkout@以下にチェックアウトしたいタグやブランチを指定することができます。Groongaでは、masterのソースコードを使いたいので- uses: actions/checkout@masterと指定しています。

actions/checkoutactions/upload-artifact以外にも様々な処理がactionとして以下のURLで公開されています。
https://github.com/marketplace?type=actions

複数の環境でアクションを実行する

複数の環境(複数のOSであったり、ソフトウェアの複数のバージョン)に対して同じアクションを実行したいケースがあります。
もちろん、それぞれの環境用にワークフローを定義しても良いのですが、同じアクションが複数のワークフローにあると、修正漏れが発生しやすくなってしまい、メンテナンスが煩雑になります。

同じアクションを複数の環境で実行したい場合は、matrix:という定義が使えます。

Groongaも各OS毎にパッケージを作成するため、このmatrix:を使ってDebianの各バージョン(stretchの32bit, 64bit、busterの32bit, 64bit)、CentOSの各バージョン(CentOS 6,7,8)に対して同じアクションを実行しています。

具体的には以下のように定義しています。

    strategy:
      matrix:
        label:
          - Debian GNU/Linux stretch amd64
          - Debian GNU/Linux stretch i386
          - Debian GNU/Linux buster amd64
          - Debian GNU/Linux buster i386
          - CentOS 6
          - CentOS 7
          - CentOS 8
        include:
          - label: Debian GNU/Linux stretch amd64
            id: debian-stretch-amd64
            rake_arguments: apt:build APT_TARGETS=debian-stretch
            repositories_path: packages/apt/repositories/
          - label: Debian GNU/Linux stretch i386
            id: debian-stretch-i386
            rake_arguments: apt:build APT_TARGETS=debian-stretch-i386
            repositories_path: packages/apt/repositories/
          - label: Debian GNU/Linux buster amd64
            id: debian-buster-amd64
            rake_arguments: apt:build APT_TARGETS=debian-buster
            repositories_path: packages/apt/repositories/
          - label: Debian GNU/Linux buster i386
            id: debian-buster-i386
            rake_arguments: apt:build APT_TARGETS=debian-buster-i386
            repositories_path: packages/apt/repositories/
          - label: CentOS 6
            id: centos-6
            rake_arguments: yum:build YUM_TARGETS=centos-6
            repositories_path: packages/yum/repositories/
          - label: CentOS 7
            id: centos-7
            rake_arguments: yum:build YUM_TARGETS=centos-7
            repositories_path: packages/yum/repositories/
          - label: CentOS 8
            id: centos-8
            rake_arguments: yum:build YUM_TARGETS=centos-8
            repositories_path: packages/yum/repositories/

strategy:の中にmatrix:を定義します。matrix:の中に変数を定義してそれをsteps:の中で${{matrix.xx.xx}}の形で参照できます。

例えば、Groongaの場合各OS向けのパッケージでビルドしたパッケージの格納場所が異なるため、${{ matrix.repositories_path }}を参照して各OSごとのパッケージをアップロードできるようにしています。

${{ matrix.repositories_path }}とすると、matrix:の定義の中にあるrepositories_path:を参照するので、

      - uses: actions/upload-artifact@master
        with:
          name: packages-${{ matrix.id }}
          path: ${{ matrix.repositories_path }}

は、packages/apt/repositories/packages/yum/repositories/配下のファイルをアップロードしていることになります。

まとめ

Groongaのパッケージ作成に使っている内容を中心にGituHub Actionsを使ったパッケージ作成の自動化の方法について記載しました。
GitHub Actionsに興味はあるが、使っていないという方は、この記事に記載されている内容を参考にして自身のプロジェクトで使用してみてはいかがでしょうか。

タグ: Groonga
2020-01-23

PostgreSQL Conference Japan 2019:Amazon RDS + Amazon EC2 + ロジカルレプリケーションを使った低コスト高速全文検索 #pgcon19j

2019年11月15日(金)にPostgreSQL Conference Japan 2019が開催されました。
私は、「Amazon RDS + Amazon EC2 + ロジカルレプリケーションを使った低コスト高速全文検索」という題名で、既存の技術を組み合わせて、なるべく楽に高速、高機能な全文検索ができる仕組みを紹介しました。

当日、使用した資料は以下に公開しています。

関連リンク:

内容

RDBMSのマネージドサービスにAmazon RDSがあります。Amazon RDSはマネージドサービスなので、RDBMSの管理や運用などのコストを大幅に下げるような機能(自動レプリケーション、自動フェイルオーバー、自動バックアップ、設定の最適化などなど)がたくさんあり、これらを使うことで、DBの管理、運用はとても楽になります。もちろん、PostgreSQLにも対応しておりPostgreSQLの管理、運用も大幅に楽になります。

Amazon RDSはとても便利なのですが、Amazon RDS上のPostgreSQLには決められた拡張しかインストールできません。
したがって、Amazon RDS上のPostgreSQLで全文検索をしたい場合、pg_trgmを使った全文検索をすることになります。(pg_trgmはAmazon RDS上で使用できます)
しかしながら、pg_trgmはアルファベットと数字にしか対応していません。日本語や中国語などの全文検索には使用できません。

PostgreSQLで使用できる全言語対応の全文検索の拡張には、PGroonga(ぴーじーるんが)があります。
PGroongaは、全言語対応の超高速全文検索機能をPostgreSQLで使えるようにする拡張で、安定して高速で、かつ高機能(同義語、表記ゆれや異体字への対応、類似文書検索など)ですが、前述の通り、Amazon RDS上では使用できません。

Amazon RDS上のPostgreSQLには直接PGroongaをインストールできないので、PostgreSQL10から追加されたロジカルレプリケーションと、Amazon EC2を使用してAmazon RDSのメリットである、管理、運用コストの低減を享受しつつ、高速、高機能な全文検索を実現する構成を紹介しました。

PostgreSQLでは、ストリーミングレプリケーションというレプリケーションがありますが、これは、複製元と複製先のDBはまったく同じものになります。
一方、ロジカルレプリケーションでは、複製元と複製先でテーブルの構造を一致させなくても良いという特徴があります。
この特徴を利用すると、複製先のテーブルにのみインデックスを設定するということができます。

つまり、複製元をAmazon RDSとし、複製先にはAmazon EC2を用意します。Amazon EC2にはPostgreSQLとPGroongaをインストールしておき、更新はAmazon RDS、検索はAmanzon EC2で行うという構成を取ることができます。

この構成では、Amazon RDSにさえデータがあれば、検索用のEC2はいくらでも作れるという特徴があります。
そのため、Amazon RDSのデータは大事に保護しなければなりませんが、DBの管理、運用に便利な機能があるAmazon RDSを使っているので、データの保護は今までよりも大幅に楽にできます。

一方で、Amazon EC2では、データをAmazon RDSからロジカルレプリケーションで同期するため、PGroongaを使ってAmazon RDSにあるデータを高速に全文検索できます。

このように、ロジカルレプリケーションの複製元と複製先のテーブル構造を一致させなくても良いという特徴を使って、Amazon RDSのメリットを享受しつつ、高速で高機能な全文検索を実現できるのです。

まとめ

Amazon RDSのメリットを活かしつつ、PGroongaを使った高速で高機能な全文検索を実現する構成を紹介しました。
Amazon RDSを使いたいけど、PGroongaが使えないので、しかたなく自前でサーバーを用意している。という方や、PGroongaを使って、高速で高機能な全文検索をしたいけど、Amazon RDSを使っているのでPGroongaが使えないとお悩みの方は、ここで紹介した構成を検討してみてはいかがでしょうか?

PGroongaを使った全文検索について、興味、疑問などございましたら、是非、お問い合わせください。

タグ: Groonga
2019-11-18

redmine.tokyo第17回勉強会:Redmine検索の未来像 #redmineT

Redmineの全文検索プラグインを開発している須藤です。

2019年11月2日にredmine.tokyo第17回勉強会が開催されました。私は参加していないのですが、この半年一緒に全文検索プラグインを開発してきた島津製作所の赤羽根さんが全文検索プラグインの導入結果を報告しました。私はこれに合わせて技術面からの補足として次の資料を作成しました。

関連リンク:

内容

従来の検索システムのなにが課題でそれをどう解決したかについてまとめました。しかし、従来の課題を解決すればすごく明るい未来があるわけではありません。少し明るい未来があるだけです。すごく明るい未来にするためにはさらになにに取り組めばよいのかについてもまとめています。そして、その取り組みはすでに始まっています。

今はクリアコードと島津製作所さんで一緒に取り組んでいますが、できれば他の会社も巻き込んですごく明るい未来を目指したいと思っています。私たちの成果はRedmine本体同様ユーザーが自由に使えるソフトウェアとして公開しています。もし、他の会社も一緒に取り組んだ場合もその成果はユーザーが自由に使えるソフトウェアとして公開します。一緒に取り組まなくても使えるように公開しますが、一緒に取り組むことですごく明るい未来がより早く実現できます。

一緒にすごく明るい未来を目指したい方はお問い合わせください。

まとめ

クリアコードと島津製作所さんは一緒にRedmineの全文検索プラグインを開発していました。その成果を島津製作所の赤羽根さんがredmine.tokyo第17回勉強会で紹介しました。私も紹介する資料を用意しました。

Redmineの検索機能をよりよくして、Redmineに蓄積した知識をより活用するために一緒に取り組みたい企業を募集しています。興味が湧いてきた方はお問い合わせください。

タグ: Groonga
2019-11-05

Groongaの回帰テストで既知の差分を吸収するには

はじめに

オープンソースのカラムストア機能付き全文検索エンジンに、Groongaがあります。
Groongaを使うと全文検索機能付き高性能アプリケーションを開発することができます。

Groongaは毎月29日(肉の日)ごろに新しいバージョンをリリースしています。
そう頻繁なことではありませんが、非互換な変更が入ったりすることもあります。

バージョンアップによって、Groongaを利用するアプリケーションで検索結果が変わっていたりすると大変です。
そんなときに便利なのが、groonga-query-log です。
groonga-query-log には回帰テスト用のツールが含まれているので、古いGroongaと新しいGroongaに対して既存のクエリーログを使って回帰テストが実行できます。
ただ、そこそこ古いGroongaとの回帰テストの結果を比較すると、そのままでは大量に差分がでてしまって困ることがあります。

今回は、回帰テストを実行する際に既知の非互換な差分は吸収しつつ、本当に必要な差分のみを検出するやりかたを紹介します。

groonga-query-log とは

groonga-query-log はGroongaのクエリーログに関する便利ツールを集めたものです。
groonga-query-log できることについては、いくつか記事がありますのでそちらを参考にしてみてください。

回帰テストに関しては、groonga-query-log-run-regression-test というスクリプトが含まれているのでそれを使います。

回帰テストの実行のしかた

groonga-query-log-run-regression-testを使って回帰テストを実行できます。

groonga-query-log-run-regression-testはgroonga-query-logに含まれています。

% gem install groonga-query-log

回帰テストを実行するには、Groongaのダンプデータと回帰テストの対象となるクエリーログが必要です。

groonga-query-log-run-regression-test \
  --old-groonga=(比較元のGroongaのパス) \
  --new-groonga=(比較先のGroongaのパス) \
  --input-directory=(ダンプやクエリーログのあるディレクトリ)

詳細については、ドキュメントを参照してください。

差分を吸収するためのオプションの解説

回帰テストの差分を吸収するためのオプションがいくつかあります。

  • --ignore-drilldown-key
  • --vector-accessor
  • --rewrite-vector-equal
  • --rewrite-vector-not-equal-empty-string
  • --rewrite-nullable-reference-number
  • --nullable-reference-number-accessor
  • --rewrite-not-or-regular-expression

それぞれのオプションの使い方を説明します。

--ignore-drilldown-key

Groonga 7.1.0でベクターカラムのドリルダウンをサポートしたことによる影響を無視するためのオプションです。
7.0.9以前の古いバージョンのGroongaでは、正しい結果を返していませんでした。
7.0.9以前の古いバージョンとの7.1.0以降のGroongaを比較する際に、ドリルダウン対象となっているベクターカラムを指定します。

--vector-accessor

後述する --rewrite-vector-equal--rewrite-vector-not-equal-empty-string を適用する対象のベクターカラムを指定します。
対象が複数あるなら、このオプションを複数指定します。

--rewrite-vector-equal

ベクターカラムに対して vector == ... で検索しているクエリー(正しくない使い方)を vector @ ... に書き換えてテストを実行します。
クエリーで書き換える対象は --vector-accessor で指定します。

古いGroongaでは、vector == ... で検索するときは先頭の要素と同じならヒットするようになっていました。(ベクターカラムをスカラーカラムとして扱っていてたまたま動いていた)
新しいGroongaではヒットしなくなっているので差分が発生します。その差分を吸収するために指定します。

--rewrite-vector-not-equal-empty-string

ベクターカラムに対して vector != "" で検索しているクエリー(正しくない使い方)を vector_size(vector) > 0 に書き換えてテストを実行します。
クエリーで書き換える対象は --vector-accessor で指定します。
vector_sizeは Groonga 5.0.3以降で使えるものなのでそれより古いGroongaと新しいGroongaとの回帰テストには使えません。

--rewrite-nullable-reference-number--nullable-reference-number-accessor

古いバージョンでは参照先のレコードが存在しないとき、そのレコードのカラムの値のデフォルト(数値なら0とか)を使ってfilterを処理していました。
新しいバージョンでは必ず false になるため、同等の内容になるように以下のようにクエリーを書き換えます。

(参照型のカラム) <= 10000 というクエリーは (参照先の_key == null ? 0 : 参照型のカラム) <= 10000 に置き換えます。

--rewrite-not-or-regular-expression

正規表現で否定先読みを用いた検索で、古いGroongaと新しいGroongaで差分がでないようにします。
&! マッチしたレコードを除く演算子と @ 全文検索を組み合わせてGroonga 5.0.7とそれ以前で挙動が同じになるようにします。

例えば、column1 @ "keyword1" && column2 @~ "^(?!.*keyword2|keyword3|...).+$
'column1 @ "keyword1" &! column2 @ "keyword2" &! column2 @ "keyword3" &! ...' に置き換えてテストを実行します。

まとめ

今回は、回帰テストを実行する際に既知の非互換な差分は吸収しつつ、本当に必要な差分のみを検出するやりかたを紹介しました。

タグ: Groonga
2019-07-22

階層構造データ用のGroongaのスキーマ設計方法

Groongaを開発している須藤です。

GroongaにはRDBMS(MySQLやMariaDBやPostgreSQLなど)と同じようにテーブルとカラムがあり、それらに構造化したデータを保存します。しかし、用途が違う(Groongaは検索がメインでRDBMSは検索だけでなくデータの管理も大事)のでスキーマの設計方法はRDBMSでの設計方法と違う部分があります。そのため、RDBMSのスキーマの設計に慣れている人でもGroongaのスキーマをうまく設計できないことがあります。

この記事では少し複雑なデータをGroongaで検索するためのスキーマの設計方法を説明します。

なお、ここで使っている設計を実装したGroongaコマンドはgroonga/groonga-schema-design-exampleにあります。実際に試してみると理解が深まるはずなのでぜひ試してみてください。

データ

まず、検索対象のデータを説明します。

検索対象は次の2つです。

  • 論文
  • 書籍

それぞれのデータがもつ情報は同じものもあれば違うものもあります。

たとえば、「タイトル」は「論文」にも「書籍」にもあります。

しかし、「雑誌」は「論文」だけにあり、「書籍」にはありません。(論文が収録されている雑誌の情報です。)

また、次のような親子関係があります。「論文」はいくつも階層になった親があります。書籍は複数の親を持ちます。少し複雑なデータですね!

  • 出版元

    • 雑誌
        • 論文
  • 出版社

    • 書籍
  • 親カテゴリー

    • 子カテゴリー
      • 書籍
  • シリーズ

    • 書籍(シリーズに属さない書籍もある)

検索方法

この少し複雑なデータに対して次のような検索をするためのスキーマを設計します。

  • 「論文」と「書籍」を横断全文検索
  • 複数カラムで横断全文検索
    • たとえば「タイトル」と「著者」で横断全文検索
  • 「論文」だけを全文検索
  • 「書籍」だけを全文検索
  • 検索結果を任意の親でドリルダウン
    • たとえば「出版元」でドリルダウン
  • 指定した親に属する子レコードを検索
    • たとえば「出版元」が発行している「雑誌」を検索

設計の概要

Groongaでは検索対象を1つのテーブルに集めることが重要です。すごく重要です。本当に重要です。

今回のケースでは「論文」と「書籍」が検索対象なので、それらを同じテーブルに格納します。今回の設計では2つ合わせて「文献」と扱うことにし、Literatureテーブルを作成します。

Literatureテーブルの定義は次の通りです。

table_create Literature TABLE_HASH_KEY ShortText

主キーに設定する値は「論文」と「書籍」全体で一意にする必要があることに注意してください。「論文」ではISSNとなにかを使って、「書籍」ではISBNを使うと一意にできる気がします。ここでは、どうにかして一意にできる前提で設計を進めます。

LiteratureTABLE_HASH_KEYを使っているのは、今回のケースでは主キーで完全一致検索できれば十分だからです。

参考:TABLE_*の特徴の違い

「論文」と「書籍」を同じテーブルに入れるため、区別するための情報も格納します。この設計ではtypeカラムを追加し、そこに"paper"(「論文」)または"book"(「書籍」)を格納することにします。

typeの型はShortTextでもよいのですが、検索効率および空間効率を考慮してTypes型にします。Typesはこの後にすぐ定義しますが、ただのテーブルです。カラムの型にテーブルを指定すると実際のデータ("paper""book")はTypesテーブルの主キーに入ります。カラムにはTypesテーブルの該当レコードのID(12とか)が入ります。各カラムに入るデータは単なる数値なので比較も高速(検索効率がよい)ですし、サイズも小さい(空間効率がよい)です。

column_create Literature type COLUMN_SCALAR Types

Typesは次のように定義したテーブルです。主キーには"paper"または"book"を設定します。主キーは完全一致だけできれば十分なのでTABLE_HASH_KEYにしています。

# "paper"または"book"
table_create Types TABLE_HASH_KEY ShortText

Literatureテーブルにレコードを追加したときにこのテーブルにも自動でレコードが追加されるので明示的に管理する必要はありません。typeカラムに"paper"を格納しようとすれば自動でTypesテーブルに主キーが"paper"のレコードが追加されます。すでにレコードがあればそのレコードを使います。つまり、typeカラムの型にShortTextを使ったときと同じように使えます。

型にテーブルを指定する方法はGroongaではよく使う方法です。用途はいろいろありますが、この使い方はRDBMSでいうenum型のようなものを実現するための使い方です。enum型のように値を制限することはできませんが。。。他の用途は後ででてきます。

Literatureに「論文」と「書籍」の情報をすべて格納します。中には「論文」にしかない情報あるいは「書籍」にしかない情報も存在します。存在しない情報は該当カラムに値を設定しません。

たとえば、「子カテゴリー」情報は「書籍」にしか存在しないので「論文」用のレコードを格納するときは「子カテゴリー」情報のカラムに値を設定しません。

GroongaにはNULLはないので、値を設定しなかったカラムの値はその型の初期値になっています。たとえば、ShortTextなら空文字列ですし、Int32なら0です。

今回の設計ではLiteratureテーブルには次のカラムを用意します。

  • type (Types): 種類(「論文」("paper")か「書籍」("book"))
  • title (ShortText): タイトル
  • authors (Authors): 著者(複数)
  • volume (Volumes): 号(「論文」のみ)
  • book_publisher (BookPublishers): 出版社(「書籍」のみ)
  • child_category (ChildCategories): 子カテゴリー(「書籍」のみ)
  • series (Series): シリーズ(「書籍」のみ)

titleauthorsは全文検索のためのカラムです。

検索項目を増やす場合は単にカラムを増やしてインデックスを追加するだけです。追加方法はauthorsを例にして後述します。全文検索用のスキーマ設計の方法もあわせて説明します。

以下のカラムはドリルダウンのためのカラムです。

  • volume
  • book_publisher
  • child_category
  • series

これらの情報で親子関係を表現します。親の親がある場合でもGroongaでは各レコードは直接の親だけを格納していれば十分です。各レコードに親の情報だけでなく、親の親の情報も格納する必要はありません。これは正規化した状態のままでよいということです。正規化した状態のままで扱えるため情報の管理が楽です。たとえば、「雑誌」の名前を変更する時は雑誌テーブルの該当レコードを変更するだけでよく、「雑誌」情報を持っているすべてのレコードを変更する必要はないということです。

ドリルダウン用のスキーマ設計は後述します。

以上が設計の概要です。ポイントは次の通りです。

  • 横断検索対象の情報はすべて1つのテーブルにまとめる
  • 対象の種類を区別する必要がある場合はカラムにその情報を入れて区別する
  • 検索条件に使いたい情報を増やす場合はテーブルにカラムを追加する
  • 特定のレコードにしかない情報(「論文」にしかない情報や「書籍」にしかない情報)でもカラムを追加してよい
    • 情報が存在しないレコードでは単にカラムに値を設定しない
  • ドリルダウン用の情報は正規化したままでよい

検索項目の追加

著者情報を例に検索項目を追加する方法を示します。

著者は複数存在するので次のようにCOLUMN_VECTORで定義します。

column_create Literature authors COLUMN_VECTOR Authors

型はAuthorsテーブルにしていますがShortTextにしてもよいです。テーブルを使っている理由はtypeカラムのときと同じで検索効率および空間効率がよいからです。著者でドリルダウンするなら(今回は説明しません)テーブルにするべきです。計算効率が全然違います。

今回の設計では著者名を主キーにします。

table_create Authors TABLE_HASH_KEY ShortText

同姓同名の著者を別人として扱いたい場合は著者IDを振ってnameカラムを追加します。今回の説明ではそこは本質ではないので単に著者名を主キーにしています。

著者名で完全一致検索する場合は次のようにすれば効率よく検索できます。

select \
  --table Literature \
  --query 'authors:@山田太郎'

著者名で全文検索する場合は追加のインデックスが必要です。

まず、Authors._keyで全文検索するためのインデックスが必要です。

table_create Terms TABLE_PAT_KEY ShortText \
  --default_tokenizer TokenNgram \
  --normalizer NormalizerNFKC100

column_create Terms authors_key \
  COLUMN_INDEX|WITH_POSITION Authors _key

Termsテーブルは他の全文検索用インデックスでも共有可能です。共有するとトークン(全文検索用にテキストを分割したもの)の情報を共有でき、DB全体の空間効率がよくなります。

TermsテーブルではTokenNgramNormalizerNFKC100を使っています。他にも指定できるものはありますが、これらがバランスがよいので、まずはこれから始めるのがよいです。必要なら後で調整するとよいです。

Terms.authors_keyは全文検索用のインデックスなのでWITH_POSITIONを指定しています。

これで、著者名で全文検索して該当著者を検索できるようになります。しかし、その著者から該当「論文」を見つけることはまだできません。追加で次のインデックスが必要です。

column_create Authors literature_authors \
  COLUMN_INDEX Literature authors

このインデックスはどの著者がどの「論文」の著者かを高速に検索するためのインデックスです。このインデックスも作ることで「著者名で全文検索して著者を見つけ、さらに、その著者がどの論文の著者かを検索する」を実現できます。

検索クエリーは次のようになります。完全一致検索のときとの違いはauthors:@._keyが加わってauthors._key:@となっているところです。

select \
  --table Literature \
  --query 'authors._key:@山田'

各インデックスカラムの役割を図示すると次の通りです。

ネストした検索

authorsは複数の著者が存在するためCOLUMN_VECTORを使っています。また、重複した情報が多くなるため型にテーブルを利用しました。そのため、少し複雑になっています。

titleのように単純な情報の場合は次のようにするだけで十分です。

column_create Literature title COLUMN_SCALAR ShortText
column_create Terms literature_title \
  COLUMN_INDEX|WITH_POSITION Literature title

titleauthorsを両方検索対象にするには次のようにします。

select \
  --table Literature \
  --match_columns 'title || authors._key' \
  --query 'キーワード'

「論文」(type"paper")だけを検索する場合は次のように--filterで条件を追加します。selectでは--query--filterで条件を指定できますが、--queryはユーザーからの入力をそのまま入れる用のオプションで--filterはシステムでより詳細な条件を指定する用のオプションです。

select \
  --table Literature \
  --match_columns 'title || authors._key' \
  --query 'キーワード' \
  --filter 'type == "paper"'

参考:select

1段のドリルダウンの実現

検索対象のデータには2段以上の親子関係のドリルダウンがありますが、まずは1段の親子関係のドリルダウンの実現方法について説明します。

例として次の親子関係のドリルダウンの実現方法について説明します。

    • 論文

効率的なドリルダウンを実現するためにテーブルを型にしたカラムを作成します。(enum型っぽい使い方とは別の型にテーブルを使う使い方。)

今回の設計では「号」用にVolumesテーブルを作成します。

table_create Volumes TABLE_HASH_KEY ShortText

「論文」はLiteratureテーブルなので、Literatureテーブルにvolumeカラムを作成します。型はVolumesテーブルです。

column_create Literature volume COLUMN_SCALAR Volumes

これでvolumeカラムで効率的にドリルダウンできます。次のようにすれば、「号」でドリルダウンし、その「号」には何件の「論文」があるかを検索できます。

select \
  --table Literature \
  --drilldowns[volumes].keys 'volume' \
  --drilldowns[volumes].output_columns '_key,_nsubrecs'

図示すると次の通りです。

1段のドリルダウン

次の親子関係も同様に実現できます。

  • 出版社
    • 書籍
  • シリーズ
    • 書籍(シリーズに属さない書籍もある)

2段以上のドリルダウンの実現

続いて2段以上の親子関係のドリルダウンの実現方法について説明します。

まずは、次の2段のケースについて説明します。

  • 雑誌
      • 論文

その後、次の3段のケースについて説明します。

  • 出版元
    • 雑誌
        • 論文

2段の場合もテーブルを型にしたカラムを作成するのは同じです。

今回の設計では「雑誌」用にMagazinesテーブルを作成します。

table_create Magazines TABLE_HASH_KEY ShortText

「号」が所属する「雑誌」を格納するカラムをVolumesテーブルに追加します。

column_create Volumes magazine COLUMN_SCALAR Magazines

これで「号」から「雑誌」をたどることができます。

「号」と「雑誌」でドリルダウンするには次のようにします。ポイントは、.tablevolumesを指定しているところと、calc_targetcalc_typesです。

select \
  --table Literature \
  --drilldowns[volumes].keys 'volume' \
  --drilldowns[volumes].output_columns '_key,_nsubrecs' \
  --drilldowns[magazines].table 'volumes' \
  --drilldowns[magazines].keys 'magazine' \
  --drilldowns[magazines].calc_target '_nsubrecs' \
  --drilldowns[magazines].calc_types 'SUM' \
  --drilldowns[magazines].output_columns '_key,_sum'

--drilldowns[${LABEL}]は高度なドリルダウンのためのパラメーターです。

参考:高度なドリルダウン関連のパラメーター

このselectでは以下の2つのドリルダウンを実行します。

  • --drilldowns[volumes]: 「号」でドリルダウン
  • --drilldowns[magazines]: 「雑誌」でドリルダウン

--drilldowns[magazines].tableで他のドリルダウンの結果を指定できます。指定するとドリルダウン結果をさらにドリルダウンできます。今回のように親子関係がある場合は子のドリルダウン結果から親のドリルダウン結果を計算します。

ただ、普通にドリルダウンすると、カウントした件数は「論文」の件数ではなく、「号」の件数になります。孫(「論文」)でドリルダウンしているのではなく、子(「号」)でドリルダウンしているからです。孫(「論文」)の件数をカウントするには子(「号」)でカウントした件数をさらにカウントする。その設定が次のパラメーターです。

  • --drilldowns[magazines].calc_target '_nsubrecs'
  • --drilldowns[magazines].calc_types 'SUM'

_nsubrecsには子(「号」)でカウントした孫(「論文」)の件数が入っています。それのSUM(総計)を計算するので孫の件数になります。出力する時は_nsubrecsではなく_sumで参照します。

--drilldowns[magazines].output_columns '_key,_sum'

図示すると次の通りです。

2段のドリルダウン

3段になった次のケースも同様です。

  • 出版元
    • 雑誌
        • 論文

まず、出版元を効率よくドリルダウンするためにPaperPublishersテーブルを作ります。

table_create PaperPublishers TABLE_HASH_KEY ShortText

Magazinesテーブル(「雑誌」)に出版元を格納するカラムを追加します。

column_create Magazines publisher COLUMN_SCALAR PaperPublishers

これで「雑誌」から「出版元」をたどることができます。

「号」と「雑誌」と「出版元」でドリルダウンするには次のようにします。ポイントは、「出版元」のドリルダウンのcalc_target_nsubrecsではなく_sumを使っているところです。「出版元」のドリルダウンで「論文」の件数をカウントするには「雑誌」のドリルダウンでカウント済みの「論文」の件数の総計を計算します。そのカウント済みの「論文」の件数が_nsubrecsではなく_sumにあるので_sumを使います。

select \
  --table Literature \
  --drilldowns[volumes].keys 'volume' \
  --drilldowns[volumes].output_columns '_key,_nsubrecs' \
  --drilldowns[magazines].table 'volumes' \
  --drilldowns[magazines].keys 'magazine' \
  --drilldowns[magazines].calc_target '_nsubrecs' \
  --drilldowns[magazines].calc_types 'SUM' \
  --drilldowns[magazines].output_columns '_key,_sum' \
  --drilldowns[paper_publishers].table 'magazines' \
  --drilldowns[paper_publishers].keys 'publisher' \
  --drilldowns[paper_publishers].calc_target '_sum' \
  --drilldowns[paper_publishers].calc_types 'SUM' \
  --drilldowns[paper_publishers].output_columns '_key,_sum'

図示すると次の通りです。

3段のドリルダウン

次のケースも同様に実現できる。

  • 親カテゴリ
    • 子カテゴリ
      • 書籍

子の一覧

親子階層の情報を使って子のレコードを検索する方法を説明します。

ここでは、対象の「出版元」内の「雑誌」の一覧を返すケースを例にして説明します。

まず、対象の「出版元」を絞り込む必要があります。ここでは「出版元」の名前(主キーに入っています)を全文検索して絞り込むとします。

全文検索用のインデックスのテーブルはAuthors._key用に作ったTermsテーブルを流用します。

column_create Terms paper_publishers_key \
  COLUMN_INDEX|WITH_POSITION PaperPublishers _key

これで「出版元」の名前で全文検索できます。しかし、authorsのときと同じで、「出版元」は絞り込めますが、絞り込んだ「出版元」を元に「雑誌」を絞り込むことはできません。「雑誌」も絞り込めるようにするには追加で次のインデックスが必要です。

column_create PaperPublishers magazines_publisher \
  COLUMN_INDEX Magazines publisher

このインデックスは「出版元」をキーにどの「雑誌」がその「出版元」を参照しているかを高速に検索するためのインデックスです。このインデックスがあることで、絞り込んだ「出版元」を元に「雑誌」を絞り込めます。

次のようなクエリーで「出版元」の名前で全文検索し、絞り込んだ「出版元」が発行している「雑誌」を出力できます。

select \
  --table Magazines \
  --match_columns 'publisher._key' \
  --query 'おもしろ雑誌' \
  --output_columns '_key, publisher._key'

他の親子関係のケースも同様に実現できます。

まとめ

Groongaで以下の機能を効率的に実現するためのスキーマ設計方法について説明しました。

  • 「論文」と「書籍」を横断全文検索
  • 複数カラムで横断全文検索
    • たとえば「タイトル」と「著者」で横断全文検索
  • 「論文」だけを全文検索
  • 「書籍」だけを全文検索
  • 検索結果を任意の親でドリルダウン
    • たとえば「出版元」でドリルダウン
  • 指定した親に属する子レコードを検索
    • たとえば「出版元」が発行している「雑誌」を検索

今回の設計を実装したGroongaコマンドはgroonga/groonga-schema-design-exampleにあります。実際に試してみると理解が深まるはずなのでぜひ試してみてください。

クリアコードではGroongaのスキーマ設計もサポートしています。Groongaをもっと活用したい方はお問い合わせください。

タグ: Groonga
2019-01-16

db tech showcase Tokyo 2018 - MySQL・PostgreSQLだけで作る高速あいまい全文検索システム #dbts2018

db tech showcase Toyo 2018で話すことを事前に宣伝をしておきたかったけど間に合わなかった須藤です。

関連リンク:

内容

去年は「MySQL・PostgreSQLだけで作る高速でリッチな全文検索システム」というタイトルで話しました。去年はMySQL(Mroonga)・PostgreSQL(PGroonga)で次のことを実現するための具体的なSQLを紹介しました。

  • 全文検索
  • キーワードハイライト
  • 周辺テキスト表示
  • 入力補完
  • 同義語展開
  • 関連文書の表示
  • 構造化データ(オフィス文書・HTML・PDFなど)対応

今年は「MySQL・PostgreSQLだけで作る高速あいまい全文検索システム」というタイトルで話しました。今年も話の流れは同じにしました。あることを実現する具体的なSQLを紹介するというスタイルです。今年はMySQL(Mroonga)・PostgreSQL(PGroonga)で次のことを実現するための具体的なSQLを紹介しました。

  • ヨミガナ検索
  • 同義語展開
  • 電話番号検索
  • ワイン名検索
  • fuzzy検索

今年は「あいまい検索」の実現方法にフォーカスした機能を選びました。同義語展開は去年も紹介したのですが、「あいまい検索」というテーマでは入っていたほうがよさそうだと思ったので入れました。「近傍検索」と「quorumマッチ」は入れられませんでした。

「あいまい検索」というテーマにしたのは今年はこのあたりの機能を強化したからです。「ヨミガナ検索」は今回の発表のために間に合わせました。

まとめ

去年の内容と組み合わせると全文検索まわりのかなりのことをMySQL・PostgreSQLで実現できます。ぜひ、Mroonga・PGroongaを使ってMySQL・PostgreSQLをさらに活用してください。

Mroonga・PGroongaのサポートが必要な方は問い合わせフォームからご相談ください。

タグ: Groonga
2018-09-20

タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|