railsでコメント数をどう実装するかで悩んでいます。

https://gyazo.com/c7ac20038c53ad302091f718fd948d15

ニコ動だとこんな感じ。

https://gyazo.com/a124a4bbf487715768730ec4aea9d810

怖話でもこんな感じでコメント数/再生数などを保存しています。

一定以上の数になると素朴な処理では速度的に無理が来ます。

怖話での実装方法

怖話でのカウント数の実装の歴史。

  1. 何もせずDBに1レコードずつ保存時代。
  2. 一覧ページなどが重いのでrailsのcounter cahcheを使う。
  3. コメントや閲覧はpolymorphic関連なのでcounter cacheが対応してない。conditional_counter_cacheを使う。
  4. 削除に時間がかかり過ぎる。 ← イマココ

一覧の表示はconditional_counter_cacheで大丈夫なのですが、削除時の処理がかかりすぎてタイムアップする問題がでてきました。

削除時の問題

ここではわかりやすく閲覧数ではなくコメント数で説明します。

コメント数が1万件ある話を削除すると下記のような処理が走ります。

  1. storyを削除する。
  2. dependent: :destroyで依存するコメント1が自動的に削除される。
  3. コメントが削除されたのでstoryのcomments_countが-1でUPDATEされる。
  4. dependent: :destroyで依存するコメント2が自動的に削除される。
  5. コメントが削除されたのでstoryのcomments_countが-1でUPDATEされる。
  6. 以下1万回繰り返し

1件につき100msだとしても1000sかかるので無理がある。

これまでの考え

  • memcacheやredisを使うのは手間なので無理が来るまで避けたい。(開発環境の構築の手間が増える)
  • ランキングで集計するので時間の情報は欲しい。
  • 一番楽な方法で実装しよう。

今の考え

  • RDBは無理がある。memcacheやredis、他のストレージもやむなし。
  • rails的に一般的な実装はなんだろう?
  • ありがちな問題なので一般的な対処方法を構築したい。

皆さんこういうのどう実装されてますか?こんな風にやってるよという方がいらっしゃったら @komagata などにメッセージいただけるとありがたいです。 :bow:

更新:railsでコメント数の実装の悩み〜解決編〜 - komagataのブログ

ぼっち演算子を使える2.3系にアップデート。

$ CONFIGURE_OPTS="--with-openssl-dir=`brew --prefix openssl` --with-readline-dir=`brew --prefix readline`" rbenv install 2.3.1

railsのプロジェクトでbundleしたらeventmachineのインストールでコケた。

Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /usr/local/var/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/eventmachine-1.0.7/ext
/usr/local/var/rbenv/versions/2.3.1/bin/ruby -r ./siteconf20160518-21178-1i9l3os.rb extconf.rb
checking for rb_trap_immediate in ruby.h,rubysig.h... no
checking for rb_thread_blocking_region()... no
checking for ruby/thread.h... yes
checking for rb_thread_call_without_gvl() in ruby/thread.h... yes
checking for inotify_init() in sys/inotify.h... no
checking for __NR_inotify_init in sys/syscall.h... no
checking for writev() in sys/uio.h... yes
checking for rb_thread_fd_select()... yes
checking for rb_fdset_t in ruby/intern.h... yes
checking for rb_wait_for_single_fd()... yes
checking for rb_enable_interrupt()... no
checking for rb_time_new()... yes
checking for sys/event.h... yes
checking for sys/queue.h... yes
checking for clock_gettime()... no
checking for gethrtime()... no
creating Makefile

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /usr/local/var/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/extensions/x86_64-darwin-15/2.3.0-static/eventmachine-1.0.7/mkmf.log

current directory: /usr/local/var/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/eventmachine-1.0.7/ext
make "DESTDIR=" clean

current directory: /usr/local/var/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/eventmachine-1.0.7/ext
make "DESTDIR="
compiling binder.cpp
In file included from binder.cpp:20:
./project.h:116:10: fatal error: 'openssl/ssl.h' file not found
#include 
         ^
1 error generated.
make: *** [binder.o] Error 1

make failed, exit code 2

Gem files will remain installed in /usr/local/var/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/eventmachine-1.0.7 for inspection.
Results logged to /usr/local/var/rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/extensions/x86_64-darwin-15/2.3.0-static/eventmachine-1.0.7/gem_make.out

下記を参考にして設定したらいけました。

$ bundle config build.eventmachine --with-cppflags=-I$(brew --prefix openssl)/include
参照:Ruby2.3.0 igaiga diary(2015-12-25)

railsらしいやり方、turbolinksともマッチするjavascriptの書き方ができるturboctrlというgemをリリースしました。

komagata/turboctrl

これは何?

/posts/1にアクセスしたらapp/assets/javascripts/controllers/posts_controller.js.coffeeにあるPostsControllerクラスのshowメソッドが呼ばれるというものです。
# app/assets/javascripts/controllers/posts_controller.js.coffee:
class @PostsController
  show: ->
    console.log "Hey!"

なぜ作ったのか

「さっさとwebアプリを書きたい。」

「RailsのRailから外れる面倒なことはしたくない。」

「Railsはjsの書き方についてRailがなさすぎる。」

「Railsっぽいままjsを書きたい」

「Sprockets捨てるとかではなく、既存の仕組みのまま行きたい」

というのがモチベーションです。

「coffeeの1ファイルに1クラスだけ書く」というルールすら共有されてないと行くプロジェクト行くプロジェクトで辛いんですよね。

class @Foo

sprocketsを使う以上、上記のようなクラスの書き方しますよってとこも共通認識にしたい。

ご意見、ご要望、Issue、PR歓迎です。

検索順位のチェック

Google検索で自分のサイトがあるキーワードで何番目に来るかというのはサイトを運営する上で非常に重要なデータであり指標だと思うんですが、なかなか簡単で正しい取得方法というのが無いような気がします。

要はブラウザでシークレットアクセスで検索した時の順位がプログラムから取得できればいいんですが、Googleの検索結果ページというのは例外的に「検索エンジンからどう見えるか?」というのを無視した作りになっており(自分自身が世界一の検索エンジンだから)、超速度重視のページ構造になっています。

seleniumやcapybara-webkitやphantomjsでスクレイプするスクリプトを書いたとしてもクラス名がgとか_Rmなんでxpathやcss selectorがすぐ変わりそうで信用できません。

そこでプラグラムから取得する真っ当な方法はGoogle Custom Search APIを使って取ることだと思います。(これが本当にシークレットアクセスでブラウザから取った時と全く同じかどうかはわかりませんが、自分の実用上は誤差の範囲のように思います)

Custom Search APIは無料枠が1日100リクエストかつ1リクエスト辺り10件まで取得可能で100位までなので、1日1回だけ調べるとすると最悪10キーワードまでしか無料でできないことになります。

しかし有料といっても大した金額にはならないですし、Google検索結果は重要なデータなので怖話のダッシュボードを作るときにこのCustom Search APIを使いました。

gem作った

ただ、GoogleのAPIは扱いが面倒なので検索順位が簡単に取れるgemを作りました。

komagata/google-search_rank

$ gem install google-search_rank
client = Google::SearchRank.new(api_key: "xxxx", cse_id: "xxxx")
client.find("怖い話", %r{http://kowabana.jp/.*}) # => 3

怖話は「怖い話」で検索すると今は3番目に出てくるので3が取得できます。

Google Cloud Platform

Google Custom Search APIで検索するにはAPI KeyとCSE ID(CustomSearchEngine ID)が必要です。Google Developers Consoleから下記の要領でProjectとCustom Search Engineを作成してKeyとIDをゲットしてください。

Custom Search APIを有効にする。

公開APIのサーバーキーを作成する。

カスタム検索エンジンを作成し、カスタム検索エンジンID(CSE ID)を取得する。

Google Cloud Platformはちょっと取っ付きづらいですが、Googleの全てのAPIを統一的に扱うことができるので一度慣れれば凄く便利です。

APIを検索するAPIがあって、そこでAPIのエンドポイントを取ってきてから使うみたいな感じになっています。

認証も課金もGoogle関係の全てのサービスが共通のAPIで使えるのは超汎用的ですが、昔の個別のAPIからマイグレーションをする必要が発生した人にとってはとても煩雑に見えることでしょう。

毎回ゲストとペアプロする画面を公開するペアプロキャストの2回目の後編を公開しました。

@kjirouがドライバー、@komagataがナビゲーターでkowabana.jp(rails)をペアプロしています。rails4.1でdeprecatedになった機能を外していくという非常に地味な内容になっています。

railsでkowabana.jpを開発 - ペアプロキャストep2後編@kjirou - YouTube

railsでkowabana.jpを開発 - ペアプロキャストep2後編@kjirou - ニコニコ動画:GINZA

flunkとか使ってます?ニコ動コメント等でフィードバックいただけるとありがたいです。

ペアプロキャスト - YouTube再生リスト

ペアプロキャスト - ニコニコ動画マイリスト

有料化どうこう以前にピンチ。Herokuが6月16日でbambooスタックの全dynoを停止するそうです。

未だにbambooスタックで動いてたアプリなんてレガシーに決まってるので、普通にcedar-14にするとruby 2.0.0になって動かない。

cedar-14で対応してる最も古いrubyは1.9.3-p551だそうなのでGemfileに明示的に書けばなるべく穏便に移行できます。

source 'https://rubygems.org'
ruby '1.9.3'

Heroku Ruby Support | Heroku Dev Center

Lokkaで沢山作っていたブログなども移行出来てホッとしました。

一時期は乱立したgemのgeneratorをbundle gemがほぼ統一。travisのファイルが入ってたり、Code of Conductが入ったり、rspecかminitestか選べたりといろいろ新しくなってました。

% bundle gem google-search_rank
Creating gem 'google-search_rank'...
Do you want to include a code of conduct in gems you generate?
Codes of conduct can increase contributions to your project by contributors who prefer collaborative, safe spaces. You can read more about the code of conduct at contributor-covenat.org. Having a code of conduct means agreeing to the responsibility of enforcing it, so be sure that you are prepared to do that. For suggestions about how to enforce codes of conuct, see bit.ly/coc-enforcement. y/(n): y
Do you want to license your code permissively under the MIT license?
This means that any other developer or company will be legally allowed to use your code for free as long as they admit you created it. You can read more about the MIT license at choosealicense.com/licenses/mit. y/(n): y
Do you want to generate tests with your gem?
Type 'rspec' or 'minitest' to generate those test files now and in the future. rspec/minitest/(none): minitest
      create  google-search_rank/Gemfile
      create  google-search_rank/.gitignore
      create  google-search_rank/lib/google/search_rank.rb
      create  google-search_rank/lib/google/search_rank/version.rb
      create  google-search_rank/google-search_rank.gemspec
      create  google-search_rank/Rakefile
      create  google-search_rank/README.md
      create  google-search_rank/bin/console
      create  google-search_rank/bin/setup
      create  google-search_rank/CODE_OF_CONDUCT.md
      create  google-search_rank/LICENSE.txt
      create  google-search_rank/.travis.yml
      create  google-search_rank/test/minitest_helper.rb
      create  google-search_rank/test/test_google/search_rank.rb
Initializing git repo in /Users/komagata/code/google-search_rank

一回選択したら設定ファイルに書かれるみたいです。

% cat ~/.bundle/config 
---
BUNDLE_GEM__COC: true
BUNDLE_GEM__MIT: true
BUNDLE_GEM__TEST: minitest

もちろん僕はminitestちゃん!

Ruby OpenSSL ホスト名検証の脆弱性が修正されているそうです。

$ CONFIGURE_OPTS="--with-openssl-dir=`brew --prefix openssl` --with-readline-dir=`brew --prefix readline`" rbenv install 2.2.2

現時点の最新版であるRails4.2.1以下MySQLデフォルトだと絵文字が保存できません。コンシューマー向けサービスのコメント欄など今どきは普通に絵文字を入力されるのですぐに問題になります。(Incorrect string valueエラーになる)

実直な対応方法はmysqlでutf8ではなくutf8mb4を使うというものです。4byteのunicodeも保存できるようになるので絵文字も問題無しです。絵文字の種類が増えても問題無いでしょう。

ActiveRecordをutf8mb4で動かす - Qiita

穏便な解決方法

rails + mysqlデフォで動かないのと、一部のカラムでだけ対応したいこと、全テーブルのインデックスが長くなるとパフォーマンスに影響でそう、mysqlが古いと対応してない、など後ろ向きの理由があって、怖話ではDBに格納するときだけhuman friendlyな文字に変換し、出すときに戻すという実装にしました。

前向きの理由としては、画像への変換と組み合わせて怖話独自の絵文字を追加し易いという点があります。(LINEスタンプ的なのやりたかった)

実装

class Comment
  def body=(text)
    write_attribute(:body, Rumoji.encode(text))
  end
    
  def body
    text = read_attribute(:body)    
    Rumoji.decode(text) if text.present?
  end
end
mysql> select body from comments order by id desc limit 1;
+-----------------------------------------+
| body                                    |
+-----------------------------------------+
| テスト:poop::thumbsup::musical_note:    |
+-----------------------------------------+
1 row in set (0.00 sec)

rumojiはまさにそのために作られたgemでとっても簡単です。

絵文字共通化問題

非対応プラットフォームでも表示できるよう、画像に変換するというのはまた別のお話・・・。

カラー絵文字ライセンス問題 - komagata

Initial content

$ CONFIGURE_OPTS="--with-openssl-dir=`brew --prefix openssl` --with-readline-dir=`brew --prefix readline`" rbenv install 2.2.1

debianだと2.1.2から2.2.1にアップデートするのに必要なpackageにlibffi-devが増えてるので注意。