ちょっと話題の記事

AWSサポートとの問い合わせ履歴をDynamoDBに入れCloudSearchで日本語全文検索してみた

2014.09.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは植木和樹です。AWSで困ったときに頼りになるのがAWSサポートです。サポートでは分からないことを解決するまで親切丁寧に対応してくれるので大変助かっております。特にクラスメソッドでは日々AWSの多く機能を利用しているため、サポートとのやりとりはAWSノウハウの宝庫といっても過言ではありません。

せっかくのケースを埋もれさせておくのはもったいない。ということで、本日は過去のサポートとの問い合わせ履歴をCloudSearchで全文検索する仕組みを作ってみました。

CloudSearchといえば今年の4月に大幅なバージョンアップが行われ、これまで難だった日本語検索機能も追加されています。

環境

AWSサポートは一般的に本番環境用のアカウントからケースを作成することになります。今回はAWSサポート用アカウント(aws-support)とは別に、個人検証用のアカウント(default)にDynamoDBとCloudSearchを用意しています。

20140922_dynamo-cloudsearch_001

AWSサポート→DynamoDB

DynamoDBにテーブルを作成する

AWSサポートとやりとりした履歴データは問い合わせカテゴリや優先度を収めた「cases」と、実際の問い合わせ内容が収められた「communications」で構成されています。今回はそれぞれをDynamoDBの別テーブルに収めることにしましょう。テーブルは異なりますがプライマリキー(Hash+Range)は同じです。

また今回は検索はCloudSearchで行い、検索結果から得られたcase_idで各ケースデータを取得することを想定しているのでセカンダリーインデックスは設定していません。DynamoDBで直接検索する場合はdisplay_id(問い合わせ時に用いる数字8桁のケースID)をグローバルセカンダリーインデックスに指定しておくと良いでしょう。

aws-support-cases テーブル

項目
Primary Hash Key case_id (String)
Primary Range Key time_created (String)

aws-support-communications テーブル

項目
Primary Hash Key case_id (String)
Primary Range Key time_created (String)

AWSサポートのケース情報を抽出し、DynamoDBに格納する

awscliでケースを抽出しDynamoDBに入れることを試みたのですが、各ケースをDynamoDBにインポートするためのJSON形式に変換するのが手間だったのでAWS SDK for Rubyでインポート用のスクリプトを用意しました。またawscli(Python)だと時々海外の方が入力する右シングルクォーテーション(\u2019)が扱えなかったこともRubyを用いた理由です。

スクリプトの注意点は下記となります。

  • describe_cases時に言語設定(language)が"en"と"ja"が分かれて返ってくるため、それぞれの言語で取得する。
  • AWSサポートのエンドポイントはus-east-1のみなのでAWS::Supportインスタンス生成時の設定でRegionを固定する。
  • display_idString#to_iで数値に変換する。
  • ケースでの添付ファイルが空の場合attachmentSetが空の配列になるが、空の配列はDynamoDBに入れられないのでキーを削除する。
  • DynamoDBとCloudSearchは--profileオプションでAWSクレデンシャルを指定します。
  • AWSサポート用のクレデンシャルはaws-supportというプロファイル名で$HOME/.aws/configに設定しておきます。

cm-export-cases.rb

#!/usr/bin/env ruby

require 'aws-sdk-v1'
require 'optparse'
require 'time'
require 'pp'

begin
  require 'aws/profile_parser'
rescue LoadError; end

ARGV.options do |opt|
  begin
    aws_opts = {}

    opt.on('-h', '--help')  { puts opt.help; exit 0 }
    opt.on('-d', '--debug') { aws_opts[:log_level] = :debug }
    opt.on('-k', '--access-key ACCESS_KEY') { |v| aws_opts[:access_key_id]      = v }
    opt.on('-s', '--secret-key SECRET_KEY') { |v| aws_opts[:secret_access_key]  = v }
    opt.on('-r', '--region REGION')         { |v| aws_opts[:region]             = v }
    opt.on('--profile PROFILE')             { |v| parser = AWS::ProfileParser.new; aws_opts = parser.get(v) }
    opt.parse!

    if aws_opts.empty?
      puts opt.help
      exit 1
    end
    AWS.config(aws_opts)
  rescue => e
    $stderr.puts e
    exit 1
  end
end

# get an existing table by name and specify its hash key
dynamo_db = AWS::DynamoDB.new
case_table = dynamo_db.tables['aws-support-cases']
comm_table = dynamo_db.tables['aws-support-communications']
case_table.hash_key = [:case_id, :string]
comm_table.hash_key = [:case_id, :string]

# Gather support cases from an another profile
aws_opts = {}
begin
  parser = AWS::ProfileParser.new
  aws_opts = parser.get('aws-support')
rescue => e
  $stderr.puts e
  exit 1
end
lastyear = Time.now - 365 * 24 * 60 * 60
aws_opts[:region] = 'us-east-1'
support = AWS::Support.new(aws_opts)
%w(en ja).each do |lang|
  case_response = support.client.describe_cases(:language => lang,
                                                :after_time => lastyear.utc.iso8601,
                                                :include_resolved_cases => true,
                                                :include_communications => false)

  case_response.data[:cases].each do |case_data|
    case_data[:display_id] = case_data[:display_id].to_i
    pp case_data

    # add a case item
    case_item = case_table.items.create(case_data)

    # add related communication items
    comm_response = support.client.describe_communications(:case_id => case_data[:case_id])
    comm_response.data[:communications].each do |comm_data|
      comm_data.delete_if { |k,v| v.empty? }

      pp comm_data
      comm_item = comm_table.items.create(comm_data)
    end
  end
end

スクリプトを保存したら下記のコマンドを実行すると、ケースのDynamoへのエクスポートが行われます。

$ ruby cm-export-cases.rb --profile default

サポートケースの数が多い場合はmax_resultsで件数を制限しながら少しずつインポートしなければいけないと思いますが、ここでは省略しています。

DynamoDB→CloudSearch

CloudSearchにはDynamoDBからドキュメントをインポートする機能があります。またDynamoDBに格納されたレコード情報を基にインデックス設定をほぼ自動的に行うことができます。

CloudSearchドメインの作成インデックスの自動設定

CloudSearchマネージメントコンソールの画面から新しい検索ドメインとしてaws-supportを作成します。

20140922_dynamo-cloudsearch_002

20140922_dynamo-cloudsearch_003

awscliでも作成することができます。

$ aws cloudsearch create-domain --domain-name aws-support

ドメインを作成したら「Indexing Options」にある「configuration wizard」のリンクをクリックしてインデックス設定を行いましょう。DynamoDBに格納されたレコード情報を基にインデックス設定を設定してくれます。

インデックスの設定見直し

自動設定されたインデックスでは日本語検索などに少々不便なので見直します。

Name 変更箇所 備考
category_code Typeを"text"から"literal"に変更する。 「一般的なご質問」や「インスタンス関連」などカテゴリーコード
language Typeを"text"から"literal"に変更する。 "en"または"ja"
service_code Typeを"text"から"literal"に変更する。 「EC2」や「RDS」などサービスコード
severity_code Typeを"text"から"literal"に変更する。 緊急度のコード(low, normal, high, urgent など)
status Typeを"text"から"literal"に変更する。 ケースのステータス(解決済み = resolved など)
subject Analysis Schemeを"English"から"Japanese"に変更する。 日本語の形態素解析が有効になる

またDynamoDB上ではtime_createdはStringですが、日付時刻をISO8601形式で格納しているためCloudSearchではDateとして認識してくれています。賢い。

DynamoDBのaws-support-casesを用いたインデックス設定が終わったら、同様にaws-support-communicationsも読み込ませてf_bodyフィールド(DynamoDBではbody)を追加しておきましょう。ケースの「内容」にあたるフィールドなのでAnalysis Schemaは"Japanese"に変更しておきましょう。

ドキュメントのアップロード

インデックスを設定したら「Upload Documents」ボタンをクリックしてDynamoDBからドキュメントをアップロード(インポート)しましょう。これもaws-support-casesとaws-support-communications、それぞれのテーブルをアップロードすればケース情報とやりとりの本文すべてを対象に全文検索ができるようになります。

20140922_dynamo-cloudsearch_004

検索してみる

試しにいくつかの単語で検索してみました。

「リザーブ」で検索してみます。

20140922_dynamo-cloudsearch_005

ただ「リザーブド」では検索がマッチしません。Analysis SchemeのStemming(語幹処理)で細かな調整が必要なようですね。

20140922_dynamo-cloudsearch_006

課題

今回はAWSサポートの問い合わせ履歴をエクスポートして、DynamoDBにインポートし、CloudSearchで全文検索できるまでを行いました。しかし実際の運用に用いるためにはまだまだ課題があります。

  • 定期的なケースのエクスポートとインポート。
  • CloudSearchで検索した結果を基に、ケースを見やすく表示する。(フロントエンドとサーバーサイドアプリの開発)
  • 大量のケースの一括インポート
  • 日本語検索でマッチしない単語の調整
  • アクセスポリシーの見直し
  • ドキュメントに応じたCloudSearchのスペックやスケーリングの設定
  • ケースに添付されたファイルの扱いはどうする?

まとめ

微調整は必要ですが、ケースをCloudSearchで全文検索するまでなら、たった半日程度で準備ができてしまいました。

CloudSearchは今年の3月に大幅なバージョンアップが行われました。昨年の9月に初めてCloudSearchを使ったときよりも、ずっと賢く・使いやすくなっている印象があります。サーバーを用意せずお手軽に全文検索できるシステムを用意できますので、みなさんも是非試してみてください。