Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Ruby+CloudSearchを用いた検索機能の実装をCloudSearch初心者が説明してみた

はじめまして、今年の5月に中途入社したサーバーサイドエンジニアの葉坂です。最近、弊社のサービスの検索改善を行ったのですが、その際にCloudSearchを初めて触りました(検索エンジンサービス自体触るのが初でした)。なので私の復習も兼ねてRuby+CloudSearchを用いたデータの検索機能の実装について説明していきたいと思います。

そもそもCloudSearchとは?

ご存知の方も多いかと思いますが、AWSが提供する検索機能を手軽に構築、実装できるクラウド型のサービスです。全文検索はもちろんのこと、ブール型検索(ANDやOR、NOTを用いた絞り込み)、プレフィックス検索(前方一致で該当する文字列の検索)、サジェスト検索などたくさんの機能があります。

Ruby + CloudSearchでデータの検索ができるようになるまでの流れ

  1. Amazon CloudSearch ドメインの作成
  2. Amazon CloudSearch 用にデータを準備
  3. Amazon CloudSearch ドメインにデータをアップロード
  4. 検索機能の実装

1~3に関してはすばらしい公式のドキュメントがあるので、リンクだけ貼らせていただきました。さて今回は公式のドキュメントはあるものの、個人的に苦戦した、4.検索機能の実装の部分をメインでお話しさせていただきます。

※1~3、AWS SDK for Rubyの設定に関してはもうすでに完了しているという前提で進めていきます。

検索機能の実装

これ以降は例として、Userテーブル(カラム:id, username, nickname, created_at)のデータをCloudSearchの検索ドメインにアップロードしてあるものとします。

gem

公式のgemがあるのでこちらを使用します。

まずはクライアントの生成から

検索機能の実装では主にClass: Aws::CloudSearchDomain::Clientを使用します。

endpointはCloudSearchのダッシュボードにあるsearch-endpointを使用します。

CloudSearchのダッシュボード

client = Aws::CloudSearchDomain::Client.new(endpoint: "http://<your endpoint>")

全文検索をしたい場合

指定された検索条件に一致するドキュメントのリストを取得するためAws::CloudSearchDomain::Client#serchを使用します。

response = client.search(
    query: 'hoge',
    query_parser: 'simple',
    return: '_no_fields',
    start: 0,
    size: 3
)

# 検索条件に一致したドキュメントのコレクションを取得
response.hits
=> #<struct Aws::CloudSearchDomain::Types::Hits found=15, start=0, cursor=nil, hit=[#<struct Aws::CloudSearchDomain::Types::Hit id="148", fields=nil, exprs=nil, highlights=nil>, #<struct Aws::CloudSearchDomain::Types::Hit id="144", fields=nil, exprs=nil, highlights=nil>, #<struct Aws::CloudSearchDomain::Types::Hit id="5109", fields=nil, exprs=nil, highlights=nil>]>

# 検索条件に一致したドキュメントを取得
response.hits.hit
=> 
[
    #<struct Aws::CloudSearchDomain::Types::Hit id="148", fields=nil, exprs=nil, highlights=nil>, 
    #<struct Aws::CloudSearchDomain::Types::Hit id="144", fields=nil, exprs=nil, highlights=nil>, 
    #<struct Aws::CloudSearchDomain::Types::Hit id="203", fields=nil, exprs=nil, highlights=nil>
]

# 検索条件に一致したドキュメントの総数を取得
response.hits.found
=> 15

# 取得したドキュメントのidをもとに下記のような使い方もできます
user_ids = response.hits.hit.map(&:id)
User.where(id: user_ids)

Aws::CloudSearchDomain::Client#serchに上記のような引数を指定すると、hogeという文字列を検索ドメインにあるすべてのフィールドで検索し、hogeに一致・部分一致したドキュメントが返却されます。

指定した引数については下記にまとめました。

  • query:検索条件を指定する。
  • query_parser:使用するクエリパーサーを指定する。Amazon CloudSearchには4種類のクエリパーサーがあります。クエリパーサーを指定しない場合はsimpleクエリパーサーがデフォルトで使用されます。
  • return:レスポンスに含めるフィールドと式の値を指定する。_no_fieldsを指定すると一致するドキュメントのドキュメントIDのみを返します。_scoreを指定するとドキュメントの関連性スコアを参照することもできます。
  • start:オフセットを指定する。下で説明しているsizeと一緒に使うとlimit/offset形式のページングを行うことができます。ただ、取得するデータが10,000件を超える場合は速度的に問題があるのでcursorを使用する方法をAWSが推奨しています。(詳しくはディープページ分割を参照してください。)
  • size:レスポンスに含める検索条件の一致したドキュメントの最大数を指定する。

検索結果の並び替えをしたい場合

検索結果の並び替えをしたい場合はsearchメソッドの引数にsortを追加すれば、すぐに実装できます。

response = client.search(
    query: 'hoge',
    query_parser: 'simple',
    return: '_no_fields',
    start: 0,
    size: 3,
    sort: 'created_at desc'
)

上記の例ではフィールド名を指定していますが、ドキュメントの関連性スコアを表す_scoreを指定して並び替えをすることも可能です。

複合クエリを使用して検索をしたい場合

続いては複数の条件を指定したい場合に使用する複合クエリについてです。下記が実装例になります。

response = client.search(
    # boost値:検索条件に一致したドキュメントのスコアを高くすることができ、複合クエリの特定の式の重要度を他より高めることができます。
    # term:任意のフィールドで個々の用語または値を検索する(CloudSearchがサポートする専門演算子)。
    # hogeという用語がusernameフィールドもしくはnicknameフィールドに存在しているかどうかを検索し、
    # usernameフィールドに存在している方が重要度が高くなります(_scoreの値が高くなる)。
    query: "(or (term field=username boost=10 'hoge')(term field=nickname 'hoge'))",
    # structuredクエリパーサーを指定することで複合クエリを使用できるようになります。
    query_parser: 'structured', 
    return: '_no_fields',
    sort: '_score desc'
)

紹介したのは一例だけですが、or以外にもブール演算子はandやnotがありますし、 CloudSearchがサポートしている専門演算子がterm以外にもたくさんあります。 なので組み合わせ次第では様々な複合クエリの作成が可能です。 こちらが参考になります。

式を定義し、それを検索結果の並び替えに使用したい場合

上の検索結果の並び替えではsortにフィールド名や_scoreを指定できるというお話をさせていただきましたが、実は独自の計算式を定義し、その式を検索結果の並び替えに使用することも可能です。

response = client.search(
    query: 'hoge',
    query_parser: 'simple',
    # _timeは現在のエポック時刻(ミリ秒)を表す。2592000000は30日間をミリ秒で表したもの。
    # created_atなどの時間を表すものはCloudSearch上にミリ秒単位でエポック時刻として保存されます。
    # ここの計算式が行っていることはcreated_atが1ヶ月以内であれば、_scoreを10倍にして重みをつけてあげているというものです。
    expr: "{'sample_expr':'_score * ((_time - created_at) < 2592000000 ? 10 : 1)'}",
    return: '_no_fields',
    size: 10,
    sort: 'sample_expr desc'
)

上記で新しく出てきた引数のexprで計算式を定義することができます。その定義した式の名前をsortで指定すると、その式で検索結果の並び替えを行ってくれます。ちなみに式の記述に使用できる演算子に関してはこちらが参考になります。

また、公式のドキュメントでも記載されているのですが、今回のように計算式をコード上に定義すると、場合によってはリクエストのオーバーヘッドが増加します。その結果として応答時間は遅くなる可能性があります。 なので複雑な計算式を定義する際はご注意ください。CloudSearch内の検索ドメインに式を定義する場合はこちらが参考になります。

最後に

Ruby+CloudSearchを用いたデータの検索機能の実装を一部紹介させていただきました。 複合クエリの実装例も一例しか紹介できなかったですし、Aws::CloudSearchDomain::Client#serchにも紹介できていない使用法がまだまだたくさんあります。 今回は触れませんでしたが、Aws::CloudSearchDomain::Client#upload_documentsを使えば、RubyでCloudSearchにデータを挿入することも可能です。

興味がある方は、CloudSearchの設定等は決して難しくないので遊んでみてはいかがでしょうか!!