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

ククログ


第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

Groongaで遅いクエリーを手軽に特定する方法

Groongaを使っていると、時々他の検索と比べて応答が遅い検索があることがあります。 そういった時は、ボトルネックを改善するために、どのクエリーの応答がどのくらい遅いのかを測定したくなります。

Groongaには、groonga-query-logという便利なツールがあります。 このgroonga-query-logには、クエリーログを解析して、どのクエリーの応答がどのくらい遅いのかを出力してくれるスクリプトが含まれています。 したがって、このツールを使えば、クエリーログを取得するだけでボトルネックの測定ができます。

ちなみに、groonga-query-logには、今回紹介する遅いクエリーを特定するスクリプトだけではなく、先日このブログで紹介されていた、クラッシュ時のログを解析するスクリプトクエリーログを手軽に再生するスクリプト、回帰テストを実行するスクリプト等、様々なスクリプトがありますので、興味が湧いたら他の機能についても使ってみて下さい。

遅いクエリーを特定するスクリプトの使い方は以下の通りです。

事前にRubyをインストールします。Rubyをインストールしたら以下のコマンドでgroonga-query-logをインストールします。

% gem install groonga-query-log

その後は、ボトルネックを測定するためにクエリーログを取得します。 クエリーログは以下のように--query-log-pathを指定してGroongaを起動することで取得できます。 例えば、サーバーモードでGroongaを起動して、クエリーログを取得する場合は、以下のように実行します。

% groonga -s --protocol http --query-log-path ~/benchmark/query.log ~/testdb/db

クエリーログを取得したら、取得したクエリーログを解析します。 クエリーログの解析は以下のコマンドで実行できます。

% groonga-query-log-analyze ~/benchmark/query.log

上記のコマンドを実行すると、標準出力に以下のような結果が出力されます。標準出力ではなく、ファイルに結果を出力したい場合は、--outputオプションで保存先を指定することができます。 情報が多いですが、1つずつ解説していきます。

Summary:
  Threshold:
    slow response     : 0.2
    slow operation    : 0.1
  # of responses      : 20
  # of slow responses : 1
  responses/sec       : 0.1351168407649807
  start time          : 2018-06-26T13:26:01.958965+09:00
  last time           : 2018-06-26T13:28:29.979003+09:00
  period(sec)         : 148.020038707
  slow response ratio : 5.000%
  total response time : 0.9940333010000002
  Slow Operations:

Slow Queries:
 1) [2018-06-26T13:26:42.318254+09:00-2018-06-26T13:26:42.853803+09:00 (0.53554963)](0): load --table Site
  name: <load>
  parameters:
    <table>: <Site>
  1) 0.53550895:       load(     9)

 2) [2018-06-26T13:27:20.700551+09:00-2018-06-26T13:27:20.838092+09:00 (0.13754151)](0): column_create --table Terms --name blog_title --flags COLUMN_INDEX|WITH_POSITION --type Site --source title
  name: <column_create>
  parameters:
    <table>: <Terms>
    <name>: <blog_title>
    <flags>: <COLUMN_INDEX|WITH_POSITION>
    <type>: <Site>
    <source>: <title>

 3) [2018-06-26T13:26:30.927046+09:00-2018-06-26T13:26:31.062374+09:00 (0.13532895)](0): column_create --table Site --name title --type ShortText
  name: <column_create>
  parameters:
    <table>: <Site>
    <name>: <title>
    <type>: <ShortText>

 4) [2018-06-26T13:26:13.510750+09:00-2018-06-26T13:26:13.599616+09:00 (0.08886603)](0): table_create --name Site --flags TABLE_HASH_KEY --key_type ShortText
  name: <table_create>
  parameters:
    <name>: <Site>
    <flags>: <TABLE_HASH_KEY>
    <key_type>: <ShortText>

 5) [2018-06-26T13:27:12.821842+09:00-2018-06-26T13:27:12.909721+09:00 (0.08787940)](0): table_create --name Terms --flags TABLE_PAT_KEY --key_type ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto
  name: <table_create>
  parameters:
    <name>: <Terms>
    <flags>: <TABLE_PAT_KEY>
    <key_type>: <ShortText>
    <default_tokenizer>: <TokenBigram>
    <normalizer>: <NormalizerAuto>

 6) [2018-06-26T13:28:10.403229+09:00-2018-06-26T13:28:10.407243+09:00 (0.00401451)](0): select --table Site --offset 7 --limit 3
  name: <select>
  parameters:
    <table>: <Site>
    <offset>: <7>
    <limit>: <3>
  1) 0.00010690:     select(     9)
  2) 0.00387348:     output(     2)

 7) [2018-06-26T13:26:56.509382+09:00-2018-06-26T13:26:56.509947+09:00 (0.00056562)](0): select --table Site --query _id:1
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <_id:1>
  1) 0.00037286:     filter(     1) query: _id:1
  2) 0.00001580:     select(     1)
  3) 0.00010979:     output(     1)

 8) [2018-06-26T13:27:45.307960+09:00-2018-06-26T13:27:45.308409+09:00 (0.00044912)](0): select --table Site --output_columns _key,title,_score --query title:@test
  name: <select>
  parameters:
    <table>: <Site>
    <output_columns>: <_key,title,_score>
    <query>: <title:@test>
  1) 0.00029620:     filter(     9) query: title:@test
  2) 0.00001499:     select(     9)
  3) 0.00008281:     output(     9) _key,title,_score

 9) [2018-06-26T13:28:24.234937+09:00-2018-06-26T13:28:24.235383+09:00 (0.00044695)](0): select --table Site --query title:@test --output_columns _id,_score,title --sort_keys -_score
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <title:@test>
    <output_columns>: <_id,_score,title>
    <sort_keys>: <-_score>
  1) 0.00027925:     filter(     9) query: title:@test
  2) 0.00001296:     select(     9)
  3) 0.00004034:       sort(     9)
  4) 0.00005845:     output(     9) _id,_score,title

10) [2018-06-26T13:28:29.978590+09:00-2018-06-26T13:28:29.979003+09:00 (0.00041371)](0): select --table Site --query title:@test --output_columns _id,_score,title --sort_keys -_score,_id
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <title:@test>
    <output_columns>: <_id,_score,title>
    <sort_keys>: <-_score,_id>
  1) 0.00023013:     filter(     9) query: title:@test
  2) 0.00000878:     select(     9)
  3) 0.00004086:       sort(     9)
  4) 0.00005566:     output(     9) _id,_score,title

Summary

まず、Summaryの内容について解説します。 Summaryには、以下の項目があります。

  • Threshold:実行に何秒かかったら、遅いクエリー、遅いオペレーションとするかのしきい値を表示しています。
    • slow responseに表示されている値より時間のかかったクエリーを遅いクエリーと判定します。
      • 単位は秒で、デフォルト値は、0.2秒ですが、groonga-query-log-analyze実行時に--slow-response-thresholdオプションを使って、しきい値を変更できます。
    • slow operationに表示されている値より時間のかかったオペレーションを、遅いオペレーションと判定します。
      • 単位は秒で、デフォルト値は、0.1秒ですが、groonga-query-log-analyze実行時に--slow-operation-thresholdオプションを使って、しきい値を変更できます。
    • 例えば、slow responseのしきい値を1.0秒、slow operationのしきい値を1.0秒にしたい場合は、以下のようにgroonga-query-log-analyzeを実行します。
% groonga-query-log-analyze --slow-response-threshold=1.0 --slow-operation-threshold=1.0 query.log
  • of responses:解析に使用したクエリーログに含まれる全てのクエリーの数を表します。
    • 例えば、of responsesが10だったとすると、query.logというクエリーログには、全部で10個のクエリーが記録されている事になります。
  • of slow responsesslow responseに設定したしきい値を超えたクエリーがいくつあったかを表します。
    • 例えば、of slow responsesが2だったとすると、slow responseに指定したしきい値を超えたクエリーが2つあった事になります。
  • responses/sec:平均応答時間を表します。解析したクエリーが平均してどのくらいの応答時間なのかを表示します。単位は秒です。
  • start timelast time:クエリーの実行開始時間と終了時間を表します。
  • period(sec):クエリーを流していた時間を表します。start timeend timeの差を表します。
  • slow response ratio:全体の中で遅いクエリーが占める割合を表します。例えば、slow response ratioの値が5.000%だった場合は、実行したクエリーのうち5%が遅いクエリーということになります。
  • total response time:クエリーの総実行時間を表します。単位は秒です。例えば、total response timeが0.9940333010000002だった場合は、全てのクエリーを実行するのに、約0.99秒かかったことになります。

Slow Operations

Slow Operationsは、具体的に遅いオペレーションを表示します。 例えば以下のような表示になります。

  Slow Operations:
    [56.021284]( 3.77%) [168](70.59%)    filter: title == "test"

この例の場合は、filter条件のtitle == "test"が遅いオペレーションと出ています。

  • 一番左の[56.021284]は、このオペレーションを実行した総実行時間を表します。単位は秒です。したがって、この場合は、filter == "test"というオペレーションを全部で約56秒実行したことになります。
  • 左から二番目の( 3.77%)は、他のオペレーションも含めた全実行時間のうちこのオペレーションが占める割合を表しています。したがって、この例の場合は、title == "test"というオペレーションが、全体のうち3.77%を占める事を表しています。
  • 左から3番目の[168]はこのオペレーションを実行した回数を表しています。この例の場合は、[168]となっているので、title == "test"というオペレーションが168回実行されています。
  • 左から4番目の(70.59%)は、他のオペレーションも含めた全実行回数のうちこのオペレーションが占める割合を表しています。この例の場合は、(70.59%)となっているので、全体のオペレーションのうち約70%は、title == testを実行していることになります。

Slow Queries

Slow Queriesは、具体的に遅いクエリーを遅い順に表示します。表示件数は、--n-entriesまたは-nオプションで変更でき、デフォルトでは10件表示されます。 例えば、5件表示させたい場合は、以下のように実行します。

% groonga-query-log-analyze --n-entries=5 ~/benchmark/query.log

or

% groonga-query-log-analyze -n 5 ~/benchmark/query.log

クエリーに複数の条件が含まれている場合は、それぞれの条件にどのくらい時間がかかったかも表示されます。 例えば以下のような表示の場合は、filterselectoutputの実行にそれぞれ0.00037286秒0.00001580秒0.00010979秒かかったことになります。

クエリー全体の実行時間は、最初の行に出力されます。以下の表示の場合は、7) [2018-06-26T13:26:56.509382+09:00-2018-06-26T13:26:56.509947+09:00 (0.00056562)](0): select --table Site --query _id:1(0.00056562)の部分がクエリー全体の実行時間を表しています。

 7) [2018-06-26T13:26:56.509382+09:00-2018-06-26T13:26:56.509947+09:00 (0.00056562)](0): select --table Site --query _id:1
  name: <select>
  parameters:
    <table>: <Site>
    <query>: <_id:1>
  1) 0.00037286:     filter(     1) query: _id:1
  2) 0.00001580:     select(     1)
  3) 0.00010979:     output(     1)

まとめ

groonga-query-log-analyzeを使うことで、このようにして遅いクエリーや、オペレーションを特定することができます。 どのくらい遅いのかの他に、全体に占める割合も出力することができ、また、どの条件に時間がかかっているかも出力できるので、チューニングのポイントを探すのに役に立ちます。

例えば、とても遅いクエリーだが、全体に占める割合が少ないクエリーをチューニングするよりも、そこそこ遅いクエリーで全体に占める割合が多いクエリーをチューニングしたほうが、性能向上に寄与します。 どのクエリー、オペレーションが遅いかだけの情報だと、上記のような判断はできませんが、groonga-query-log-analyzeを使った解析なら、上記のような判断もでき、より効果的なチューニングが実施できます。

タグ: Groonga
2018-06-26

Groongaのクエリーログを手軽に再生する方法

Groongaを使っていて問題が起きた時に、問題を再現させるために、問題が起きた時に実行していたクエリーを手元の環境でも実行したくなります。 実行するクエリーが少ない場合(1つか2つくらい)であれば、手作業で実行してもそんなに苦ではありませんが、実行するクエリーが大量にある場合は、手作業では限界があります。

Groongaには、groonga-query-log という便利なツールがあります。 この groonga-query-log には、クエリーログを手軽に再生するスクリプトが含まれています。 このスクリプトを使うことで、クエリーログに記載されているクエリーをログに記録されている順番で再生することができます。つまり、クエリーログさえあれば、簡単にログに記録されているクエリーを実行できます。

Groongaのクエリーログは、以下のようにGroongaを起動する際に--query-log-pathを指定することで、指定されたパスにクエリーログを作成できます。 (以下の例ですと、ホームディレクトリの直下にgroonga.query.logという名前でクエリーログができます。)

% groonga --query-log-path ~/groonga.query.log ~/db/db

ちなみに、groonga-query-log には、今回紹介するクエリーログを手軽に再生するスクリプトだけではなく、先日このブログで紹介されていた、クラッシュ時のログを解析するスクリプトや スロークエリーを特定するスクリプト、回帰テストを実行するスクリプト等、様々なスクリプトがありますので、興味が湧いたら他の機能についても使ってみて下さい。

クエリーログを再生するスクリプトの使い方は以下の通りです。

事前にRubyをインストールします。Rubyをインストールしたら以下のコマンドで groonga-query-log をインストールします。

% gem install groonga-query-log

次は、再生するクエリーを実行するためにデータベースを準備します。

ログを取得したマシンのデータベースが使える場合は、そちらを使うと新たにデータベースを準備しなくてすみますが、問題が起きたときには問題が発生したデータベースは使えないこともあるので、問題が発生したデータベースのダンプ等から、新しく手元の環境でデータベースを生成するほうが多いかもしれません。

今回の例では、以下のコマンドで、データベースのダンプから、新しくデータベースを作成します。

% groonga --file dump.grn -n ~/testdb/db 

次は再生するクエリーを実行するGroongaを起動します。Groongaはサーバーモードで起動します。プロトコルは、gqtp, httpのどちらでも問題ありません。 例えば、httpプロトコルを使用する場合は、以下のように起動します。

% groonga -s --protocol http ~/testdb/db

Groongaをサーバーモードで起動したら、準備完了です。 以下のように、 groonga-query-log-replay コマンドを実行して、クエリーログを再生できます。

% groonga-query-log-replay  --n-clients=1 --output-responses=./response.log replay-query.log

--n-clients はクエリーを実行するクライアント数を設定できます。 --output-responces は実行したクエリーの結果を保存するファイルを指定します。最後に、再生するクエリーログ(上の例では、replay-query.log)を指定して実行します。

これで、指定したクエリーログに記録されているクエリーが実行され、実行結果が、--output-responsesで指定したファイルに記録されます。

デフォルトでは、localhostの10041ポートに接続しますが、--host オプションと --port オプションを使うことで、それぞれ変更することができます。

タグ: Groonga
2018-06-19

Groongaクラッシュ時のログの解析方法

Groonga周辺を便利にするツールもいろいろと作っている須藤です。

Groongaはクラッシュセーフではないので、クラッシュするタイミングによってはデータベースが壊れてしまうことがあります。データベースが壊れていそうかどうかはログを解析することで判断できます。この記事ではクラッシュ時のログを解析するためのスクリプトを紹介します。

インストール

クラッシュ時のログを解析するスクリプトはgroonga-query-log gemの中に入っています。このスクリプトを使うと以下のことがわかります。

  • いつクラッシュしたか
  • クラッシュしたときに実行していたクエリーはなにか
  • flushの変更コマンド(loadtable_createなど)(これらがあるとデータベースが壊れている可能性が高い)
  • 解析対象のログ中にある重要度が高いメッセージ
  • (クラッシュ時ではなく)正常終了時にメモリーリークしていたか

事前にRubyをインストールします。Rubyをインストールしたら以下のコマンドでインストールできます。

% gem install groonga-query-log

使い方

インストールするとgroonga-query-log-check-crashというコマンドが使えるようになります。これでクラッシュ時のログを解析すると前述のこと(いつクラッシュしたかなど)がわかります。

使う時は次のように通常のログ(--log-path ...を指定すると出力されるログ)とクエリーログ(--query-log-path ...を指定すると出力されるログ)を「すべて」指定します。結果が多くなることがあるので、標準出力をリダイレクトして結果をファイルに保存しておくと便利です。

% groonga-query-log-check-crash groonga.log* groonga-query-log* > analyze.log

指定する順番は気にしなくてよいです。ファイル名に含まれているタイムスタンプ情報を元にコマンド内部で自動で並び替えるからです。なお、タイムスタンプ情報のフォーマットはstrftime(3)で言うと%Y-%m-%d-%H-%M-%S-%6N%6Nstrftime(3)にはないけど、6桁のミリ秒のつもり)です。このフォーマットは--log-rotate-threshold-size--query-log-rotate-threshold-sizeを使ったときに使われているフォーマットなので、これらのオプションを使ってログローテーションしている場合はとくに気にする必要はありません。

(違うタイムスタンプ情報のフォーマットもサポートして欲しい場合は https://github.com/groonga/groonga-query-log/issues で相談してください。)

また、gzip・ZIPで圧縮されていても構いません。自動で伸張して解析します。

改めてまとめると、持っている通常のログ・クエリーログをすべて渡してください、ということです。

結果

コマンドを実行するとログを解析し、クラッシュを検出すると随時解析結果を出力します。ログが大きいほど時間がかかります。ログはストリームで解析しているので大量のログを解析してもメモリー使用量が比例して大きくなることはありません。

出力結果は特に決まったフォーマットになっているわけではなく(たとえばMarkdownでマークアップされているわけではない)、解析結果が1つずつ表示されるだけです。フォーマットは今後も変わっていきます。(改良していきます。)そのため、ここで現時点のフォーマットの説明はしません。

以下の情報がわかるような出力になっています。

  • いつクラッシュしたか
  • クラッシュしたときに実行していたクエリーはなにか
  • flushの変更コマンド(loadtable_createなど)(これらがあるとデータベースが壊れている可能性が高い)
  • 解析対象のログ中にある重要度が高いメッセージ
  • (クラッシュ時ではなく)正常終了時にメモリーリークしていたか

また、より詳しく知りたい場合はどのログファイルを確認すればよいかもわかるようになっています。

まとめ

Groongaがクラッシュしたときのログ解析を支援するツールがあることを紹介しました。Groongaを運用している人はぜひ活用してください。

ログの解析ロジックは私が手動で解析するときのロジックです。そのため、未対応なパターンも十分にありえます。未対応のパターンがあった場合は https://github.com/groonga/groonga-query-log/issues で教えてください。

タグ: Groonga
2018-06-11

タグ:
年・日ごとに見る
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|