読者です 読者をやめる 読者になる 読者になる

ボクココ

サービス開発を成功させるまでの歩み

従属接続詞の英文読解にチャレンジする

English

ども、@kimihom です。今年の GW はひたすら英語を勉強することに決めました。

そこで"英語リーディング教本"という本を読んで、まずは文の構成を正しく理解すること、そしてDUOで単語を覚えるという方法を実践しています。今回は英語リデーィング教本に出てきたサンプルセンテンスから引用。この本、一回さらっと読んで放置してたんですが、改めて読み直すととてもいい本ですね。

さて、まずは翻訳にチャレンジしてみてください。

That dance is becoming very popular among young people has been proven by a nationwide survey.

読解のプロセス

これを読んだらまずはほぼ 100% that dance は「あのダンス」になって主語だと考えるじゃあないですか。むしろそれ以外考えられん、と。そのまま読み進めていくと、has been proven と出てくる。おや、これはどっちがメインの動詞なんだろう、と悩むことになります。

私はこう考えました。きっとhas been proven 側でthat が省略されているんだろう、と。つまり、 young people that has been proven と言った感じに。いや、でもこういう時のthat って確か省略できなかったよな・・。と思います。そこで私のこの英文読解が詰みました(笑)

文頭の that が従属接続詞だと・・!?

改めて回答を見ると、なんと that が従属接続詞なんだそうです。いやいやいや。 そしたら dance ってのは a とか the がつかないといけないじゃあないか、と思うわけです。ただなんとこのdance、不可算名詞だそうで、dance単体で名詞になれるらしいです。そもそも文頭に that が来て、しかもそれが従属接続詞だった、なんてパターンを見たことがなかったので個人的にとても新鮮な文でした。

改めて読解

では正解を見ていきましょう。この文のメインの術語動詞は has been proven となります。That ~ people までが名詞節。そう考えると翻訳はさらっとできます。

ダンスは若者の間でとても流行ってきている"ということ"が、全国的な調査によって証明されている。

おぉ〜綺麗に翻訳出来ましたね!

終わりに

最近思うのですが、英語読解って文法をしっかりと押さえておけば例えば知らない単語が出てきたとしてもなんとかなったりするんですよね。 パッと例が思い使いないですが わからない単語も文法から判断できるようになると、英語読解の幅は半端なく広がります。

そう考えると、難しい単語だけを勉強し続けるようなアプリなどでてきていますが、割とそこまで頑張るべきか、というのを最近感じます。それだったらこうした文法をしっかりマスターし、リスニングやリーディングをたくさんすることで読解力を上げることのほうが重要ではないでしょうか。

最後に今回のサンプルで取り上げた英語リーディング教本をご紹介します。

基本からわかる英語リーディング教本

基本からわかる英語リーディング教本

Rails で複数モデルに関連したパラメータを検証する方法

Rails

ども、@kimihom です。

今回は Ruby on Rails における パラメーターの処理について。

よくある Rails の Scaffold では、送られてくるパラメーターとモデルが 1対1 で対応しているため、 Strong Parameter を使って綺麗に CRUD(Create, Read, Update, Delete) 処理ができている。ただ、実際は何かの登録フォームなどで関連するモデルを一括でフォームから登録する、という場合など多くあるかと思う。そんな時は Rails のレールから外れるから仕方なく汚いコード(特に検証のコード)を書いてしまった経験は一度や二度はあるだろう。しかし、実は Rails はそんな時でも綺麗に 書くことのできる方法を提供している。これは、データ保存を必要としないモデルを作成し、そこに ActiveModel をインクルードするのだ。これにより、DB保存はしないけど、Rails の Model と同等の検証機能を利用できる。

ではサンプルを見ていこう。

ActiveModel な Model を作成する

今回のサンプルは、会員登録で一気に複数のモデルを一括作成するようなケースを考えてみる。このような時は User 単体の Create 処理だけじゃ物足りず、その他サービスを利用する上で重要なデータの初期化などを行うことだろう。そんな時のパラメーターと Model に渡すパラメータは複数であることが多い。

通常の Model といえば、ActiveRecord::Base を継承したクラスを作成する。今回は各 Model は既に定義されているとして、パラメータ用のモデルActiveModel::Model をインクルードした RegisterParam を作成する。

今回は例えば契約情報(Contract)クラスがあり、その中に複数のユーザー(User)クラスがあるようなパターンを考えてみる。

class RegisterParam
  include ActiveModel::Model

  attr_accessor  :name, :email, :email_confirm, :tel1, :tel2, :tel3, :password, :password_confirm

  validates :name, presence: true, length: {maximum: 255}
  validates :email, presence: true, length: {maximum: 255}, confirmation: true
  validates :password, presence: true, length: {in: 8..50}, confirmation: true
  validates :tel1, presence: true, numericality: true, format: { with: /\A0\d+\Z/i }
  validates :tel2, presence: true, numericality: true
  validates :tel3, presence: true, numericality: true

  def initialize(attributes={})
    super
  end

  def contract_param
    {
      name: self.name,
      email: self.email,
      tel_number: [self.tel1, self.tel2, self.tel3].join("-")
    }
  end

  def user_param
    {
      email: self.email,
      password: self.password,
      password_confirmation: password_confirmation
    }
  end
end

このようにすることで嬉しい点が大きく2つある。

1つ目は Rails の バリデーション機能を複数のモデルが関連した時でも利用できる、ということ。実際にコントローラ側で検証するには、validate メソッドが利用できる。通常のバリデーションと同様に、エラーが発生した場合はメッセージを自動で生成してくれる。

2つ目は View 側で form_tag ではなく form_for が利用できる点だ。 form_tag は特に何も気にせずに form をかけて手っ取り早いんだけども、データのValue 値など全てinput要素毎に書かないといけない。また、label も同様に自分で書く必要がある。form_tagは UIの変更に非常に弱いのだ。しかし、 form_for を使えばそれらの苦労から解放される。以下のようにコントローラを記述すればform_for が View 側で利用できる。

どちらも、 Rails のレールに乗ることができる というのが最大のメリットである。

  def new
    @register = RegisterParam.new
  end

  def create
    ActiveRecord::Base.transaction do
      @register = RegisterParam.new(register_param)
      raise ActiveRecord::RecordInvalid.new(@register) unless @register.validate
      
      @contract = Contract.create!(@register.contract_param)
      @user = @contract.users.create!(@register.user_param)
    end
    
    rescue => e
       # 例外処理
       @error = e.message
    end
  end

  private
    def register_param
    params.require(:register_param).permit(
      :name,
      :password,
      :password_confirmation,
      :tel1,
      :tel2,
      :tel3,
      :email,
      :email_confirmation
    )
  end
  <%= form_for @register do |f| %>
    <%= f.label :name %>
    <%= f.text_field :name %>

    <!-- ..... -->

    <%= f.submit %>
  <% end %>

通常のモデル処理と同様、validate メソッドの検証に失敗した場合は、@registererror オブジェクトが入ってくるので、エラー箇所などを適切に表現することが可能だ。もちろん 今回のモデルは DB とは連携していないので、save メソッドなどは利用できない。

ちなみにエラーメッセージを出す時のフィールド名も 通常の Model と同様、 config/locales/ja.yml に置いておけば適切に見てくれる。その際、キーはactiverecordではなくactivemodel であることに注意しよう。

  activemodel:
    attributes:
      register_param:
        name: 氏名
        tel1: 電話番号1
        tel2: 電話番号2
        tel3: 電話番号3
        email: メールアドレス
        email_confirmation: メールアドレス確認

終わりに

このように複雑なパラメータ処理でも、美しいコーディングを実現することができた。 Rails のレールに乗っている時の気分の良さはたまらないものがある。しっかりと Rails を理解して書いているという満足感と美しくコードを書くことができたという誇らしさ、そしてそれがメンテンス性、テストの書きやすさにつながってくる。

初心者ほど Rails のレールを踏み外すことが多い。より良いコードを書くために、Rails のレールに乗せることを再度考えてみよう。

クラウド時代こそ PaaS を選択すべき理由

Heroku スタートアップ

ども、 @kimihomです。

さて、最近は AWS をはじめとしたクラウドサービスが盛り上がっている。そんな中で私はもっとサービスオリエンテッドなネット企業はもっと PaaS を活用すべきだと思ったので書いてみるとする。

貴社のインフラエンジニアは本当に必要か?

いや、この話は大企業とかお金たくさんあるような会社は別にいいんです。だがお金を節約したいのに、わざわざインフラをEC2のようなまんまのサーバーを立てる必要があるのか私には疑問だ。

サーバーに Apache や Nginx を入れて https にしていざ公開となるわけだが、そこから先の監視ツールの導入やミドルウェアのバージョンアップ、デプロイの自動化まで、まんまのサーバーを選択してしまったあなたはずっとそういった課題に対応しなければならない。それがたとえインフラエンジニアが一流で経験のある人でなくても、だ。

PaaS にすればそんな心配はすべて消える。このような共通の煩わしい作業は、PaaS 側ですべてやってくれる。PaaS にはプラグインがあり、監視ツールやデプロイ環境などワンクリックで構築できる。しかもやってくれるのはインフラのその道のプロフェッショナルたちだ。"多少ググって自分のサーバーでやってみる"、といった次元のレベルではない。あなたの会社がミドルウェア事情に詳しいインフラエンジニアに給料をちゃんと払えて、そこまでやる価値があるなら別にいいけど、そうでもないような会社がそうした不毛なところに時間をかけるより、もっとやるべきことがあるんじゃないか、と思う。インフラエンジニアに大したことをさせずに給料を払うくらいなら、PaaS サービスにしっかりとお金を払った方がみんなが幸せになれるだろう。PaaS会社よりしっかりとメンテナンスできる自信があなたにはあるだろうか?これに自信を持って YES と答えられるか。

PaaS の不自由さはなくなりつつある

IaaS 好きの方なら必ず言うだろう PaaS の不自由さに関する欠点についても語っておこう。はっきり言ってそんなことはほとんどなくなっている。あらゆるミドルウェアは外部サービスとして切り出されており、ボタンを数クリックするだけでPaaSに埋め込むことが可能だ。しかも、それらのメンテナンスも PaaS 関連企業に渡すことができる。どうしても独自でってところだけミドルウェアから構築して API として利用すればいい。そこはきっと自社の強みになるだろうから性能や運用などもこだわりたいだろうし。

基本的に IaaS で構築した Webサービスを運用するには、エンジニアが1人余計に必要になるだろう。そのエンジニアがPaaS サービス提供会社よりも腕ききである可能性は低いと言わざるをえない。PaaS サービスに金を払った方が間違いなくコストパフォーマンス的にはいいと私は思う。人件費よりもクラウドサービス代にお金をかけた方が結局はプロフェッショナル集団によるサービス運営が可能になるという現実がある。

終わりに

もっとサービスと顧客に寄り添った開発ができるようになると良いと思う。AWS, GCP などより Heroku や IBM Bluemix などをまずは検討してみてはいかがだろうか。スタートアップであれば、その選択はきっと間違えていないと思う。

あなたのスタートアップ論は間違っている

スタートアップ

ども、@kimihomです。

皆さんの中で起業したい、もしくはすでに起業している、という方は多いかもしれない。その中で、あなたの目標はなんだろうか? Apple や Google のような世界的に知られる企業になりたいのか、日本中で使われるサービスを作りたいのか。なるほど、そうした考えも一理あるし、インターネットであればそれは不可能ではないのかもしれない。

「インターネットサービスなんだから、もっと大きく狙わなきゃダメでしょ。」

これは誰もが常識のように感じていて、そうじゃなきゃ起業する意味がないとまで言う人までいる。 果たして、本当にそれがすべて正しいのだろうか?

会社を大きくするということは、コントロールを失うこと

会社が大きくなれば、当然みんなが自分のことを知ってもらえる、ということになる。確かにジョブズや孫正義など尊敬されるような大企業の社長もいるにはいる。でもここで考え直してみて欲しいんだけど、世界にほとんどいないようなそんな人にあなたはなりたいのだろうか。それには相当な努力(あなたが思っている以上の相当の努力)がいることを覚悟しなければならない。彼らは大企業であるのに、自分がコントロールできる株を大量に持っている。また、自分の意見をすべて押し通せるような絶対的な権限を持っている。これが難しいということは、以前のクックパッドの経営陣の内紛問題を見れば明らかだ。大企業なのにそういう独裁的なことができる人になるしか、あのような偉大な人になることはできない。

そもそもの話なんだが、そこまで大きくしたところであなたは幸せになれるだろうか? たくさんの人に知られ、有名人になって、あなたは満足した、成功したと思ってそれでいいのか。あなたの事業が成功したら、あなたは幸せになれるのか。

これはきっとイコールではないと私は思う。

自分の人生を自分で決められる。自分のしたいようにできることこそ、幸せなんじゃあないだろうか。

会社をコントロールできることは、自分の人生をコントロールできるということ。

誰もが会社をコントールできる方法がある。それは、外部資本を受け入れずに、小さいままを受け入れるという経営の方針だ。多くても30人くらいの会社の規模にとどめておき、地域に密着した働き方をする。この状態で安定的な収入を得られるように身の回りから感謝されるような仕事をする。

こうすればあなたは本当の自由を獲得できる。就業規則も自分で理想的な仕組みを作ればいい。そこでいちいち文句を言ってくる部外者もいない。外部の資本を入れていないから、あなたは本当の自由を手にすることができる。その自由な時間であなたは旅に出たり趣味に使ったり、家族ともっとたくさん会ったり地域の社会貢献に時間を使うことができる。

もう一度問いたい。世界一を取る!といって家族や地域とのコミュニティも持たずして会社を大きくしていくことが、あなたの本当の幸せなのだろうか?それがあなたの人生のゴールなのだろうか?それは外部資本を持つ人から言われた見せかけの成功なんじゃないか?

ネットサービスにおける私の挑戦

私は、インターネットサービスでこのような経営ができるようになるよう、現在も取り組んでいる。私は会社と製品と自分の人生をコントロールできるような人生にしたいと望んでいる。何百、何千と社員を抱える企業を目指したいなんて思わない。たくさんの社員を抱えることが貢献になるとの意見もあるだろうけど、私は他のところで貢献する方法もあると思っている。大企業にはできない密接な顧客や地域との関係だ。

顧客を増やすこと・会社を大きくすることばかりに目をやるのではなく、目の前の顧客と実際に会って、彼らに愛されるプロダクトを作る。誰から言われるのでもなく、自分たちがプロダクトの方向性を決めて実装する。時には 顧客に対して No ということもあるが、それも私たちの信念の1つだ。

最近、そうしたスタートアップを見かけることもちょいちょい出てきた。そんなスタートアップたちと協力して「本当の事業の成功とは何か」、を 問い続けていきたいと思っている。こうした考えに興味を持った方は、ベンチャーファイナンス論より以下をお勧めする。

小さなチーム、大きな仕事〔完全版〕: 37シグナルズ成功の法則

小さなチーム、大きな仕事〔完全版〕: 37シグナルズ成功の法則

  • 作者: ジェイソン・フリード,デイヴィッド・ハイネマイヤー・ハンソン,黒沢 健二,松永 肇一,美谷 広海,祐佳 ヤング
  • 出版社/メーカー: 早川書房
  • 発売日: 2012/01/11
  • メディア: 単行本
  • 購入: 21人 クリック: 325回
  • この商品を含むブログ (36件) を見る

Small Giants [スモール・ジャイアンツ] 事業拡大以上の価値を見出した14の企業

Small Giants [スモール・ジャイアンツ] 事業拡大以上の価値を見出した14の企業

Heroku Redis を使ってるなら利用したい Redis Monitor アドオン

Heroku

ども、@kimihom です。

Heroku といえば豊富なアドオンがあることが大きな魅力の1つだ。たまに眺めてみると意外な発見があるので面白い。そんな中、今回紹介したいのは RedisMonitor というアドオンだ。これは Heroku Redis を使っているなら是非導入したいところ。

RedisMonitor の利用

使い方は至って簡単で、Heroku アドオンから Redis Monitor を選択し、heroku config で表示される REDIS_URL を設定画面にコピペするだけで完了する。すると、Heroku Redis の管理画面よりも詳細な Redis の稼働状況を確認することが可能だ。以下はサンプル。

f:id:cevid_cpp:20160421231712p:plain

f:id:cevid_cpp:20160421231717p:plain

Redis 周りの様子がおかしいときに、何が限界に達していたり、異様に値が大きくなっていたりを確認することでトラブルシュートが容易になる。あらかじめ問題を検知するのはベテランの知識が必要だが、そうしたトラブルが起きた時でも落ち着いて原因を調査できるツールがあるのは心強い。

さらに月 $99 のPlusプランを使えば、 Redis Support を受けることもできる! Redis のスペシャリストを $99 で雇ったと思えば、めちゃめちゃ安い金額だと思う。ゆくゆくはアップグレードしてPlusプラン使いたいと思う。

よく見ると Resque のリンクが

RedisMonitor をよく見ると、 Resque というリンクがある。これをクリックすると、ここで Resque の管理画面を見ることができる。 まぁこれに関しては自分の Rails サーバーから見ることのできる画面だけども、何もしなくても Redis の情報と一緒に見れるってのは便利なような気がする。これを使うのに Sidekiq を使ってるってなるとなんか気持ちが悪い程度か。

Heroku アドオンについて思うこと

外部サービスでわざわざそのサービスでまたクレカ登録するより、Heroku で一括で払った方がもろもろ楽であろう。そういう意味でもアドオンは便利だと思う。

Heroku のアドオンは定期的に見回ってみると、いいことが起こる。自分の場合は pdf の出力をしなきゃな〜と思ってた時にふとプラグインを眺めると、めちゃ便利そうなアドオンとか見つけてラッキーみたいなことがあった。

動画や画像処理周りとか、便利だと思うので是非使ってみよう。大抵は無料から始められるし、気に入ればお金を払えばいいだけのこと。是非積極的に探して活用してみよう。

Elasticsearch Rails で 全文検索とサジェスト機能を実現

Rails Elasticsearch

ども、@kimihom です。

前回の記事で、Elasticsearch Rails の導入周りの調査内容をレポートした。

今回は調査内容をもとに、Elasticsearch と Rails を組み合わせて実装したので実践編としてまとめてみる。

Elasticsearch と Rails 間のデータ同期に関して

まず懸念事項のデータ同期に関する話。それなりにパフォーマンスを気にするのであれば、Resque や Sidekiq を用いてバックグラウンドに回して ActiveRecord のコールバックを活用してElasticsearch と 同期を取るのだけども、バックグラウンドジョブの実装は割とトラップが多く、その程度でバックグラウンドジョブを実現する価値があるのかだいぶ悩んだ。以下の記事が参考になった。

attracie.hatenablog.com

確かに実際に導入するのは簡単なんだけど、プロセスやメモリの管理をしっかりと考えないといけないし、実際に問題が起きた時に検知できて解決できるような仕組みを導入しないといけない。そこまでのことをしてまで導入するかどうかというのは一度冷静になって考えるべき、という意見はとても納得のいくものだった。

現時点でそこまで負荷のかかるサービスでもないし、今回はバックグラウンド処理は見送って、同期処理でやってみることにした。そうなると実装がかなりシンプルになる。

mapping などのデータ定義を考えなければ、modelに1個モジュールをincludeするだけで良くなる。これでデータのCRUD時に勝手に Elasitcsearch のデータもmapping定義に合わせて一緒に更新してくれる。実際にやってみたところそこまで動作が遅くなったという感じはしなかったので、この方法でひとまず正解だったと思っている。具体的にはモデルに以下を include すればおk。

    include Elasticsearch::Model
    include Elasticsearch::Model::Callbacks

これからのサンプルコードは全て Model 内に書くか、 Concern で切り出してそのモジュール内に書くかで対応できる。

Elasticsearch サービス

では Elasticsearch を使おうとなった時に、どのサービスを使えばいいのだろうか?自分でEC2などで建てる方法ももちろんあるだろうが、私は最も手軽な外部サービスを利用することにした。

そこで選んだのが、 Bonsai というサービスだ。テスト環境は無料から始められる。本番の Elasticsearch には $50 かかるが、実際に同様の環境を構築しなければならない時間やサーバコストを考えると、全然気にならない値段だ。

Bonsai に登録したら Elasticsearch の URL がゲットできるので、 Rails の config/initializers/bonsai.rb で初期化しよう。

Elasticsearch::Model.client = Elasticsearch::Client.new url: ENV['BONSAI_URL']

tokenizer, filter, analyzer

まず Elasticsearch Rails でぎょっとするのがスキーマ定義だろう。

私なりの理解で説明すると、tokenizer で文章をどのように区切るかという方法を定義し、filter でその区切った文字をどのように整形して保存するかを定義する。そして analyzer でどのtokenizerを使ってfilterで整形するかを定義する。最終的に各フィールドがどの analyzer で保存するかを決める必要がある。それを踏まえた上で、以下のサンプルコードを紹介する。参考にしたのはこちらのQiitaより。もろもろ調べた感じ、こちらが一番近いものになった。

    index_name "myapp"

    settings index: {
      analysis: {
        tokenizer: {
          kuromoji: {
            type: 'kuromoji_tokenizer'
          },
          ngram_tokenizer: {
            type: "nGram",
            min_gram: 3,
            max_gram: 11,
            token_chars: ['digit']
          }
        },
        filter: {
          pos_filter: {
            type:     'kuromoji_part_of_speech',
            stoptags: ['助詞-格助詞-一般', '助詞-終助詞'],
          },
          greek_lowercase_filter: {
            type:     'lowercase',
            language: 'greek',
          }
        },
        analyzer: {
          kuromoji_analyzer: {
            type:      'custom',
            tokenizer: 'kuromoji_tokenizer',
            filter:    ['kuromoji_baseform', 'pos_filter', 'greek_lowercase_filter', 'cjk_width'],
          },
          number_analyzer: {
            tokenizer: 'ngram_tokenizer'
          }
        }
      }
    } do
      mapping _source: { enabled: true }, _all: { enabled: true, analyzer: "kuromoji_analyzer" } do
        indexes :external_id, type: 'long', index: 'not_analyzed'
        indexes :name, type: 'string', analyzer: 'kuromoji_analyzer'
        indexes :text, type: 'string', analyzer: 'kuromoji_analyzer'
        indexes :number, type: 'string', analyzer: 'number_analyzer'
        self.instance_eval do # hack for including "suggest context"
          context = {external_id: {type: "category"}}
          @mapping[:suggest_field] = {type: "completion", context: context}
        end
      end
    end

tokenizer では kuromojingram を利用する。

kuromoji は日本語をいい感じに単語で区切ってくれて、それをキーワードとして分割してくれる。んでそれを'kuromoji_baseform', 'pos_filter', 'greek_lowercase_filter', 'cjk_width' などでより綺麗な形に整形して保存する。

対して nGram は min_gram から max_gram までを文字を分割して保存する方法だ。今回は番号検索とかでこちらを利用した。1234 って数字があった時、 min_gram 2, max_gram 3 の場合、12, 23, 34, 123, 234 って感じで保存してくれる。そんで 123 って検索した時にこれがヒットするようになるというわけだ。

デフォルトで使える各 tokenizer, filter, analyzer などは公式ドキュメントに全てまとまっている。やはり細かいオプションなどはこうしたところしか載っていないので必ず読むことになるだろう。

mapping メソッドにて、Elasticsearch に保存するキー名を定義する。基本的に Rails モデルで定義したカラム名のうち、検索で使いたいものだけ登録する形になると思う。ここで上部に定義した analyzer を指定してあげることで保存、検索にそのアナライザを使って検索してくれることになる。

今回は サジェスト 機能も実装したので、suggest_field も定義した。Completion Suggesterの公式ドキュメントを見ながら設定した。今回の仕様として、external_id 毎に検索のサジェストも分けるようにしたかったので、Elasticsearch の Context Suggester というのを同時に併用して実現した。これを使えば サジェスト機能でも where のような絞り込みをした上でサジェストしてくれるようになる。

そんで Elasticsearch Rails のソースコードを見ると、Context Suggesterを実現する上で必要な contextをセットできなかったので、instance_eval を使って強引に設定した。もっといい方法があるのかもしれないが、見た感じやり方がなかった。

以上でスキーマ定義が完了する。実際は上記設定を何度も変えて、データをインポートしながらちゃんと検索で出てくるのかをテストすることになる。

as_indexed_json の定義

続いて ActiveRecord と Elasitcsearch の関連付けを行う。 ActiveRecord カラムと Elasticsearch のキーは違うことになることが多いわけで、以下のようなメソッドを定義してあげる。

    INDEX_FIELDS = %w(external_id name text number).freeze
    SUGGEST_FIELDS = %w( name ).freeze
    def as_indexed_json(options={})
      indexed_json = self.as_json.select { |k, _| INDEX_FIELDS.include?(k) }
      indexed_json.merge!({
        suggest_field: {
          input: indexed_json.select { |k, _| SUGGEST_FIELDS.include?(k) }.values,
          context: {
            external_id: self.external_id
          }
        }
      })
    end

モデルに関連づいた key-value 形式のハッシュと、 suggest_field のついたハッシュが組み合わさったような形になる。 suggest_field は配列が指定できるので、サジェスト項目の候補を複数いれておくことも可能だ。

データインポート

さて、これらの定義を終えると次は DB -> ActiveRecord -> Elasticsearch でデータをインポートすることになる。

lib/tasks/elasticsearch.rakeに以下を追記。

require 'elasticsearch/rails/tasks/import'

これで rake タスクが追加される。以下を実行することで一括インポートが実行される。

bundle exec rake environment elasticsearch:import:all FORCE=y

あとは ローカルで起動した KibanaSense を起動して、データのクエリを投げて遊んでみよう。Sense の URL は Bonsai で登録した Elasitcsearch の URL に変更する。クエリの種類に関しては他サイトや公式サイト 見ながら組み立てよう。基本的に SQLでできることは Elasitcsearch でもだいたいできる。 LIKE 句は Elasticsearch だと tokenizer の分類になることに注意。LIKE をできるようにするために Elasitcsearch を入れているので当然のことだ。

Rails 側で検索、サジェスト

公式ドキュメントを読めば書いてあるけど一応載せておく。

    def self.search external_id, keyword
      query = { query: { and: [
        { constant_score: { filter: { term: { external_id: external_id } } } },
        { query_string: { query: keyword, fields: ["number", "name", "text"] } }
      ] } }
      Elasticsearch::Model.search( query ).records.to_a
    end

    def self.suggest external_id, keyword
      query = { suggest: {
        text: keyword,
        completion: {
          field: "suggest_field",
          size: 10,
          context: { external_id: external_id }
      } } }
      __elasticsearch__.client.suggest(body: query, index: INDEX_NAME)["suggest"].to_a.first.to_hash["options"]
    end

上記の クエリは、 where で 外部id をあらかじめ絞った場合 の検索だ。

その他知っておきたいこと

Sense でのクエリの書き方は知っておいたほうがためになる。といっても REST で body が JSON のを投げるだけなので Elasitcsearch の書き方を知るだけだが。例えばマッピング定義を確認したい時は、 GET my_index/_mapping で確認できる。いちいち Rails でコード書いてテストみたいなのは時間がかかるから、 Sense で動いたら Rails コードに落とし込むってやり方がいいかと思われる。

今回は ActiveRecord の saveupdate, destroy が呼ばれた時のコールバックで Elasitcsearch に登録している。つまり、直接生の SQL を書いていたり、外部から別で SQL を呼んでデータを挿入しているといった場合には、別途 Elasticsearch にもデータを更新することを忘れないようにしよう。不整合が起きる原因となる。

DELETE WHERE みたいなことがしたくなるのだけども、Elasticsearch 2 系からは DELETE Query が廃止されたようだ。なので定期的に再インポートしたり、Elasticsearch Rails の records メソッドを使って Elasitcsearch にはあるけど ActiveRecord にはないデータは取ってこないといったような工夫で対応しよう。

終わりに

Elasticsearch の公式ドキュメントをゼロから読んでいたのだが、Elasticsearch に関しては実践から始めた方がより理解できる気がした。割と感覚的に実装していけるのがこの技術の良さなのかもな。それからこだわりたい部分をしっかりと学んでいけば大丈夫。

この記事を書くまでにあらゆるサイトを巡って調査し、公式ドキュメントを読み、実現することができた。次の私のような方がこの記事で得られるものがあれば幸いである。

こうして技術というのは発展していくのかな、とも最近感じるようになったので、頑張って書いてみた。

Elasticsearch Rails の調査レポート ~実運用へ向けて~

Rails Elasticsearch

ども、@kimihom です。

今回は Ruby on Rails で Elasticsearch を使う方法について調査したので報告しよう。

特に ActiveRecord と Elasticsearch をどう連携させるのか、そこら辺を詳しく書こうと思う。

elasticsearch-rails の利用

さて、 Rails で Elasitcsearch を利用する場合、上記の gem を利用することになるが、この gem は何者なのか自分の理解でまとめる。

  • Elasticsearch での index, type の指定
  • ActiveRecord 上のオブジェクトで Elasticsearch へ保存するフィールドを指定
  • ActiveRecord(or Mongoid) でのデータを Elasticsearch に一括インポート
  • ActiveRecord でオブジェクトを作成・更新・削除したタイミングでデータを同期
  • Elasticsearch で検索

この Gem は3つに分かれるが、ActiveRecord を使う場合は、

gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'

の2つの gem を利用する。elasticsearch-model が今回重要なコアとなる ActiveRecord の拡張だ。 elasticsearch-rails は インポートに便利な rake タスクの提供やログの出力の設定などが実装されている。

elasticsearch-model で検索する

さて、細かな設定方法はドキュメントを読めばいいのだが、特に導入において重要な点を以下に挙げる。

ActiveRecord と Elasticsearch の同期

どうやって ActiveRecord と Elasticsearch を同期させるか、は私自身、とても悩んでいたことだった。実際に elasticsearch-model を見てみると、幾つかのやり方があるように見える。まず一番シンプルなのは、 ActiveRecord の create, update, destroy のコールバックを用いて、そのタイミングで Elasticsearch にリクエストを送り、データを同期させる方法だ。

class Article < ActiveRecord::Base
  include Elasticsearch::Model

  after_commit on: [:create] do
    __elasticsearch__.index_document 
  end

  after_commit on: [:update] do
    __elasticsearch__.update_document 
  end

  after_commit on: [:destroy] do
    __elasticsearch__.delete_document 
  end
end

こうすれば基本的に ActiveRecord でデータが変われば Elasticsearch も変わるようになる。ただし、その分リクエストが発生するので、大量アクセスのあるサービスの場合は Resque などを利用する必要がある。

少なくともこれで ActiveRecord の CRUD時は Elasticsearch と同期させた運用が可能になる。しかし自分の環境の場合、 ActiveRecord を介さないで PostgreSQL に保存する場合があり、その時にどうやって Elasticsearch へデータを入れるかは未だ調査が必要な点だ。

Elasticsearch を ActiveRecord::Relation で扱う

例えば、 Article.search("hoge") とした時に、 その戻りがActiveRecord::Relationだと、where でのさらなる絞り込みやページング、グルーピングなどが可能になる。シンプルに Article.search("hoge").recordsrecordsを付与すると変換してくれる。これにより、 Elasticsearch ではデータが残っていたけども、ActiveRecord ではデータが削除されていたので、それは取得しない、ということが可能になる。Elasticsearch で ids を取得し、さらに ActiveRecord でその ids で検索を内部で動作させるからだ。これは2回リクエストを送るというやや冗長な感じになるが、データが必ず存在することを担保することが可能だ。

インデックスの作成とデータインポート

インデックスの作成はまずプログラムで実行させるようなコードを書く必要がある。

      client = Elasticsearch::Client.new host: 'localhost:9200', logger: Logger
      client.indices.delete index: index_name rescue nil
      client.indices.create(index: index_name,
                            body: {
                                settings: settings.to_hash,
                                mappings: mappings.to_hash
                            })

こんな感じのを rake タスクなり model なりにうまい具合に記入し(上記コードじゃ動かないので変数にうまく値を入れる)、インデックスを作成しよう。そのあと、以下を lib/tasks/elasticsearch.rake に記入しよう。

# lib/tasks/elasticsearch.rake
require 'elasticsearch/rails/tasks/import'

これだけで bundle exec rake -D elasticsearch が打てるようになり、このコマンドで ActiveRecord から Elasticsearch へインポートする方法が出来上がる。例えば Article に Elasticsearch の定義をしたならば、以下のような形でインポートが可能だ。

bundle exec rake environment elasticsearch:import:model CLASS='Article'

一括でサクッとインポートできるので大変便利なコマンドだ。

終わりに

私自身、まだ調査段階のレポートであるため、実際に Elasticsearch Rails な環境で運用していくとまた違う問題などが発生すると思う。適宜本ブログで報告していきたい。