アペフチ

A Tour of Goのエクササイズをやってみた

A Tour of Goのエクササイズをやって、GitHubに上げてみた:github.com/KitaitiMakoto/a-tour-of-go-exercises

ウェブクローラーの課題(exercise-web-crawler.go)が難しくて、まずgoroutineを使って非同期にクロールさせるのに手こずった。一つのHTTPリクエストに一つのgoroutineを割り当てた時、終了の待ち合わせはどうするのがいいんだろう。僕は、一々チャンネルを閉じるようにした。閉じるのの待ち合わせは

for range ch {}

とやった。この他に、go呼び出しの辺りにラベルを付けて、groutineから戻ったところでbreakするというやり方もあるようだ。エクササイズは本当は教師がいて答え合わせしてもらえるととてもいいのだけど、残念ながらいないので、誰か、「こうしたらもっといいよ」というの教えてください。まあ、色んなソースを読むというのが、よいのだろうとは思うが。

次に、「一度フェッチしたURIは二度フェッチしないようにする」というのも課題の一部で、ヒントに「その管理にマップを使うのはいいけど、マップは単独では並行処理に関して安全ではない」とあって、この排他制御にもちょっと困った。大きなロックを獲得して、その中でフェッチすると、並行処理させている意味が無くなっちゃう。でも、どのタイミングでロックを取ればいいのか、何のロックを取ればいいのか、というのに迷った。「一つのURIにつき一つのミューテックスを作る」ということを最初考えたのだけど、そもそもあるURIに対応するミューテックスが存在するかの確認処理と、その後ミューテックスを作るまでの間に他のgoroutineが同じ物を触る可能性があるわけで、うまくいかない。結局、単純にマップその物をロックするようにした。そうすると、deferを使わない実装になってしまったのだけど、もっといいやり方がないものだろうか。

Sendagaya.rb #137

Sendagaya.rb #137に参加して来た。『メタプログラミングRuby 第2版』と、Active Record enumsの話をして来た。

メタプログラミングRuby 3章 メソッド

今回もメタプログラミングRubyを読んだ。「3章 メソッド」から(こういう時、章にもリンクを貼りたいものだ)。

同じようなメソッドの定義を繰り返すのではなく、動的に定義することで、重複した記述を減らす方法が紹介される。

class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

   def self.define_component(name)
    define_method(name) do
      info = @data_source.send "get_#{name}_info", @id
      price = @data_source.send "get_#{name}_price", @id
      result = "#{name.capitalize}: #{info} ($#{price})"
      return "* #{result}" if price >= 100
      result
    end
  end

  define_component :mouse
  define_component :cpu
  define_component :keyboard
end

これで

  def mouse
    info = @data_source.get_mouse_info(@id)
    price = @data_source.get_mouse_price(@id)
    result = "Mouse: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end

みたいなメソッドを幾つも書く作業から開放される。例によってふんふんなるほどと読んでいたが、例によって甘かった。十五分読んだ後にみんなで話している時に、このコードの「危なさ」を指摘する声が上がった。

initializedata_sourceを引数に受け取っているが、別々のインスタンス初期化時に別々のdata_sourceを受け取り得るから、クラス全体が和集合のような不要なメソッドも持った物になる」とのことだった。僕にはぴんとこなかった。その後、コードを使って説明してくれた。

methods = [:moge, :hoge]
methods2 = [:moge, :hoge, :age]

class Computer
  def initialize(methods)
    methods.each do |method|
      self.class.define_kick method
      puts method
    end
  end
  def self.define_kick(name)
    define_method("#{name}_kick") do
    puts 'name'
    end
  end
end
c1 = Computer.new(methods)         # => "moge"、"hoge"を出力
p c1.class.instance_methods(false) # => [:moge_kick, :hoge_kick]
c2 = Computer.new(methods2)        # => "moge"、"hoge"、"age"を出力
p c2.class.instance_methods(false) # => [:moge_kick, :hoge_kick, :age_kick]
p c1.class == c2.class             # => true
p c1.class.instance_methods(false) # => [:moge_kick, :hoge_kick, :age_kick]

(少しコメントを足し、改変した)

最後の行、c1でも期待しないメソッド#age_kickが使えることを示している(その前の行からも明らかだが)。こういう危なさがあることに、僕は気が付かなかった。勿論、これが「危ない」かどうかは作るアプリケーション次第だ。別にこれで構わないということも理論上ある。それは何かと話していて、「サンプル」かなとなった。

ActiveRecord enumsのDB内での値を文字列にする

その後、今日は何を話そうかと話していると、Rails 4.1から入ったActiveRecord enumsの機能についての相談が上がった。

この機能はRDBMSなんかでも実装されているenum(列挙型)の機能をRailsのレイヤーで実装した物で、ユーザー(Railsを使うプログラマー)はモデルオブジェクトが提供する、人間が読みやすいインターフェイスだけ扱っていればいいようになる。バックエンドではActiveRecordが数値に変換してDBに格納し、ユーザーとの間を取り持ってくれる。

今回の相談は、この機能ではDBに格納する値も文字列にすることができるが、それは普通ではないのだろうか? という物。

始め、「何となく気持ち悪いですね」くらいしか言うことができなかった。一応、データサイズが増えるというのもあるが、このご時世、そこは気にするポイントではないだろうとのこと。しばらく話しながら思い付いたのは、文字列にするとインデックスを張ることになるだろうから、インサート時にインデックスを更新するコストが掛かって(遅くなって)しまうということだった。これはそれなりに妥当だと受け取られて、「じゃあ、やっぱり(ActiveRecordの)enumのバックエンドは数値にしておくのが普通なんですね」ということになった。

ちなみに相談者が文字列を使いたい一番の背景は、Railsは分からないがSQLなら分かるという人のいる場(に、今いるらしい)では、値が数値で返って来ると人間の方で対応する文字列を参照しないといけないので、嫌だ、ということだった。これは勿論妥当だと思うので、この人のケースでは、文字列を使うのは正解だと思う(が、RailsAdminがエラーになるという問題があるらしかった)。それは置いておいても割と一般的に文字列を使いたいこだわりがありそうだったが、そこは深く話さなかった。今思うともったいなかったかも知れない。

「そう言えば、Railsを使うとマスターテーブルを作ることをしなくなるな」という話もした。『エンタープライズRails』には「アプリケーションのフレームワークや言語よりも、データベースのデータのほうが長く残るから、データだけで完結できるようにしておくべきだ」と書いてあったように思うのだけど、時代も違うし、エンタープライズだとウェブのコンシューマー向けサービスとは領域も違うということだろうか。

実例を元にした全文検索エンジン(Groonga)のテーブル設計

Groongaで学ぶ全文検索 2016-02-12に行って来た。今日は、参加者が勉強にと国立国会図書館の書誌情報データを取って来てGroongaに入れてみている、だが今のテーブル設計がいいのか分からないということだったので、それをまず説明してもらって、@ktouさんが各テーブル作成時のオプションなどを説明してくれた。

インデックスを作る前と後で検索結果を比較していて、インデックス作ってそっちで検索すると20倍速くなったということで、インデックスの有用性が証明されてた。

そんなわけで個別に色々説明してくれたのだが、まとまっているわけではないので、幾つか取り上げることにする。

WITH_POSITIONの活躍

今回はインデックステーブルを作る時に、タイトルに対応するインデックスカラムを作る時にCOLUMN_INDEX|WITH_POSITIONと、WITH_POSITIONが指定されていた。これがうまく活きる状況の話があった。

『一、二年生の基礎英作文』というタイトルの本があった。これに対して「一年生」で検索した時に、この本はヒットするだろうか。但し、実際のトークナイザーはTokenBigramが選ばれていたが、仮にTokenMecabにしたとする。

タイトルは「一」「、」「二」「年生」「の」……とMeCabはトークナイズした。検索クエリーは「一」「年生」になる。とすると、このタイトルはクエリーのトークン「一」にも「年生」にもヒットするので、ヒットしそうだ。しかししない。なぜならGroongaはこのタイトルカラムではキーワードの位置情報も持っていて、「一」と「年生」がこの順番で隣り合っていないことを知っているからだ。同様の理由で、仮に『二年生の基礎英作文(一)』みたいなタイトルの本があったとしてもヒットしない。このように、「キーワードが文書中でどの位置にあるか」という情報も持たせるオプションがWITH_POSITION。なので逆に、これを指定していなければ上のクエリーでこの文書はヒットするだろう。

MeCabみたいな形態素解析をせずbi-gramでWITH_POSITIONなし、みたいなカラムを作ると、色々な物がヒットし過ぎて検索には使い物にならないカラムになったりもする。

bi-gramでトークナイズしている時は、一文字では検索できないのか?

bi-gramでトークナイズしている時は、インデックスのキーには二文字の文字列が入っている(文末など場合によっては一文字もある)。そういう時に、検索クエリーが一文字の時には、何もヒットしなくなるのでは? という疑問が出た。

答えはヒットする。今回の実例では、インデックスキーをパトリシリアトライで作っていた(=ハッシュにしていなかった)から。パトリシアトライなら前方一致検索ができるので、一文字のクエリーでも探すことができる。なぜパトリシアトライなら前方一致検索ができるのか? 忘れた。もうパトリシアトライの構造を忘れたので後で見ておく……。

尚、MySQL 5.7から入ったInnoDBの全文検索機能では、bi-gramでトークナイズする設定にすると、実際に一文字での検索ではヒットしなくなるらしい。

テーブルのキーに選ぶべき物

今回は、国会図書館から提供されているTSVのうち、URL、タイトル、著者、出版社をGroongaに入れていた。テーブルとしてはハッシュテーブルを選び、キーにURIを入れていた。これは正しい設計だろうか?

具体的なアプリケーションの要件によって、考慮するべきことがある。

  • ハッシュテーブルでいいだろうか?
  • いいとして、キーはURLでいいだろうか?

ハッシュテーブルのいい所は、キーが存在していて、そのキーを使ってレコードを一意に特定できる所。逆に言うと特定する必要がない、つまり後からレコードを探して更新や削除をすることが無いのであれば、ハッシュテーブルを使う必要はない。追加しか無いのなら、配列を使ってもいい。

ハッシュテーブルを選んだとして、そのキーカラムのサイズは(現在のGroongaでは)4KiBになっている。普通それならURLを入れても大丈夫だろうと思うが、溢れることがあると@ktouさんは主張していた(何か嫌な思い出がありそうだった)。ので、今回の場合だと、選択したカラムにはなかったが、ISBNを使うのがよさそうとのこと。また、そういう一意かつ短めの物が選べない時は、一意な物のハッシュ値を計算してキーとして入れるといいとのこと。