アペフチ

日記に検索機能をつけた

この日記に検索機能を付けてフッターに置いた。軽快にインクリメンタルサーチができていて、なかなかいい。

Groongaを使った全文検索

この際に、二つやったことのない作業があって、その一つは検索エンジンを使った検索機能の作成。正直、静的サイトの検索なんてGoogle Custom SearchSwiftypeを使えばいいと思うが、自分で遊ぶための場所を持つというのも日記を移行した目的の一つだったから、自分でやってみた。

Middlemanでサイトをビルドした後、Rakeタスクでビルド済みディレクトリーから日記本文を抽出して、Groongaに投げ込んでインデックスを作っている。始め、Middlemanのビルド時のidentical、updated、created、removedという状態変化の情報を利用して、必要な分だけGroonga上のインデックスを更新しようかと思っていた(そのための調査結果はQiitaのMiddlemanで「変更なし」「作成」「削除」「変更」の状態を取るという記事に書いた)。でも、実用上それで問題ないのだが、一応「ビルド環境が変わったらそういった状態変化の情報は変わる、ポータブルではない」ということに配慮して、毎回全日記の分をGroongaに投げるようにしている。今のところ、パフォーマンスが問題になったりということは全然ない。

そのように毎回全部作り直すことにしたので、ローカルでインデックス構築済みgroonga-httpdDockerイメージを作って、それをデプロイするようにしようかとも思った。実際イメージを作るまではやっていた。が、せっかくGroongaがなるべくOSの機能を引き出すように作られているのに、仮想環境で動かすのはもったいないと思って「普通」にインストールしてsystemdで起動して使っている(Linodeを使っているから、まあ、仮想環境ではあるが)。

何故かsnippet_html()関数がうまく動いていないので、別途調査が必要だ。

そう言えば、記事を削除した時の対応もRakeタスクにない。消すことがあったら考えよう。記事データをGroongaを投げる時に削除できるよう自動化するのでなく、PostgreSQLのVACUUMみたいに、ユーザー側で明示的に実行するのでいいと思う。

Polymerコンポーネントの作成

もう一つは、検索エリアを作るのに、Polymerを使って検索用のコンポーネントを作った(HTMLソースを見れば<blog-search>というタグが見付かるはずだ)こと。

作り始め、全く何も表示できず、Polymerに慣れてもいないので自分が何か間違っているかと色々試しすごい時間を費したが、内部で使っているコンポーネントを読み込む<link>要素のimport属性をipmortと書き間違えていただけだった。分かった時には脱力した。

ユーザー入力(<paper-input>)、Groongaとの通信(<iron-ajax>)、その二つの繋ぎ(<blog-search>)という構成で作ったが、<blog-search>は昔ながらのjQueryを使った素朴な同期処理のようになってしまった。Polymerにはデータバインディングの仕組みがあるのにもったいない。もう少しすっきりするよう書き換えたい。

しかし、他のコンポーネント指向のライブラリーもそうだと思うが、閉じたスコープのことだけを考えてHTML、CSS、JavaScriptを書けばよいというのは大分ストレスフリーだ。


前からずっと、「あの記事はどこだっけ」と思った時にgit grepしてURIを調べてからページを表示していたので、それが無くなって自分が嬉しい機能だ。

追記

と思いきや、このサイトは外にリンクを置く時はHTTPSで置いているのだけどgroonga-httpdのdebパッケージにはTLSモジュールが組み込まれていなかった。取り敢えずNginxを前に立てたけど、groonga-httpdのウェブサーバー機能もNginxなので、こちらのビルド時にTLSモジュールを組み込むのが正しいと思う。後でメーリングリストに送って入れてもらうか自分でビルドするかしよう。

Sendagaya.rb #135

Sendagaya.rb #135に行って来た。今日も前半は『メタプログラミングRuby 第2版』を読んで、後半はRails 5のチェンジログからActive Record 5.0.0.beta1のチェンジログを眺めていた。

メタプログラミングRuby Refinements

今日は「2.4.2 メソッドの実行」から「2.4.3 Refinements」まで。

メソッドの実行では他の言語から来るとprivateとかに戸惑うよねとか、protectedって何に使うんだろうねという話をした。

お次はいよいよRefinements

class MyClass
  def my_method
    "original my_method"
  end

  def anothor_method
    my_method
  end
end

module MyClassRefinements
  refine MyClass do
    def my_method
      "refined my_method"
    end
  end
end

using MyClassRefinements
MyClass.new.my_method      # => "refined my_method"
MyClass.new.another_method # => "original my_method"

この最後の行は驚かないだろうか。僕は驚いた。

この挙動を本では

Refinementsが有効になっているコードは、(略)インクルードやプリペンドしたモジュールのコードよりも優先される。

と説明している。しかしこれだけで、メソッド探索の順番を覚えられるだろうか。@tkawaさんの説明が素晴らしかった。

  1. クラスやインクルードやプリペンドを考慮するより先に、まずRefinementsを探す
  2. 次に通常のメソッド探索手順に従って、プリペンドされた物、インクルードした物、特異クラス、クラス……とメソッドを探す

ということだ。上の例で言うと、まずmy_methodを呼ぶ場合

  1. Refinementsを探す
  2. MyClassRefinementsが見付かる
  3. my_methodが定義されている
  4. MyClassRefinements#my_methodを呼ぶ

で、結果は"refined my_method"になる。一方another_method

  1. Refinementsを探す
  2. MyClassRefinementsが見付かる
  3. another_methodは定義されていない
  4. Refinementsの探索は終わり、今後二度と探索されない
  5. プリペンドされたモジュールやインクルードされたモジュールを探すが、ない
  6. クラスを見る
  7. MyClassである
  8. another_methodが定義されている
  9. another_methodを実行する
  10. my_methodが呼ばれている
  11. Refinementsの探索は終わっているので、クラスのmy_methodが見付かる
  12. MyClass#my_methodを実行する

ということで、結果が"original my_method"になる、というわけだ。

子の説明を聞いて僕は「あ。」と声が漏れるくらい腹に落ちた。

Active Record 5.0.0.beta1チェンジログ

その後もちょいちょいRefinementsと遊んでからはActive Record 5.0.0.beta1のチェンジログをざっと流しながら気になった所で止めて、あーだこーだ言っていた。結構RDBMSの個別機能に対応していたり、細かなユースケースを拾ったりしていて、「Active Recordは基本は既に成熟しているんだろうなあ」という感想を持った。

次回は『メタプログラミングRuby』読むほか、fukajunさんがElectronについての発表をしたいということなのでそれは聞けるはずだ。あとはその場で決まるんだろう。その場で決まるので、聞きたいことを持って行けば、聞けると思う。

次回分もすぐさまイベントが作られて、申し込める。
https://sendagayarb.doorkeeper.jp/events/38655

ノーマライズと形態素解析

Groongaで学ぶ全文検索 2016-01-29に行って来た。今日は濃かった。その分面白かった。

まずノーマライザーの話をして、その後形態素解析の話をした。

ノーマライザー

ノーマライザーはノーマライズする物。ノーマライズは、半角「カレー」と全角「カレー」、ひらがな「りんご」とかたかな「リンゴ」など、厳密には違う物を同じ物に寄せるという処理。どういう基準でノーマライズするかというのは色々あって、その色々ごとにノーマライザーの種類がある。

インデックスを作る時、インデックスのキーにノーマライズした物を選んでおくと、例えば「りんご」で検索した時に、「りんご」を含む文書と「リンゴ」を含む文書の両方を返すことができる。これがノーマライザーを使う理由。

ただ、この状況下で「リンゴ」で検索すると、インデックスのキーワードには「りんご」しかないので、何もヒットしない(「リンゴ」というキーワードはインデックスを作る時にノーマライズされて「りんご」になっているから、入っていない)。だから、検索時にクエリーの方もノーマライズする。この時にはインデックス作成時と同じノーマライザーを使わないと、キーワードの集合が変わるので、いけない。

(面白い質疑応答二つあった、後で書く。かも。)

形態素解析

形態素解析は、文などを形態素に分ける処理。形態素が何かは各自調べられたい。迂闊なことを言えない。日本語の文を構成する、意味的に分割できる一番小さな単位、とかそんな感じ。全文検索では、インデックスのキーワードをどのように選ぶかというやり方の一つとして、形態素解析器を使う(他にN-gramがあるのは以前書いた通り:日本語文書の全文検索)。

例えばクエリーの中の「りんご」と「リンゴ」を同一視するには、文字単位で見ていって、かたかなを見付ける度に全部ひらがなにしていけばいい。こういうノーマライズは文字単位で処理すればできる。が、「PC」で検索した時に「パソコン」を含む文書を返す、二つを同一視するにはこの方法では不可能。「パソコン」を見つけたら「PC」にする必要がある。この時に

  • 「パソコン」が一つの単位になるように、クエリーを分割する方法(形態素に分割する方法)、「パソ」みたいに「パソコン」より細かく分割しないし、「パソコンを」みたいに他の語とくっついた形で大きな粒度で分割しない方法。
  • 「パソコン」と「PC」は同じ物だ、という日本語の知識(扱うアプリケーションによる。同じ物と扱いたくないアプリケーションもあるだろう)

の知識が必要になる。

前者が形態素解析と呼ばれる処理。形態素解析器は「『パソコン』というのは名詞である」「『で』というのは助詞である」「『で』は文の最初には現れない」といった知識(辞書)を持っている。この知識を元に文を解析して、日本語としてありそうな切り方を何パターンか出す。その中の一番ありそうな物を結果として返す(それを全文検索エンジンが使う)。

ところで、どのように切るか、というのは、ドメインによって変わる。新聞で使われる言葉、ツイッターで使われる言葉、若者の言葉、年寄りの言葉……。 アプリケーションが扱うドメインが分かっているなら、一般的(?)な切り方でなく、それぞれに適した切り方ができるはずだ。この「それぞれに適した」は、それぞれ用の辞書を使うことで対応する(形態素解析器自体は変えない)。時代とともに検索結果が古びてきたようだと、辞書を新しくする必要も出てくるだろう。

辞書を新しくするということは、インデックスのキーワードの選び方が変わるということだから、その時にはインデックスを作り直す必要がある。

後者、単語(形態素)の同一視は、ノーマライズの前のクエリー展開というフェイズでやっている。そこは辞書を元にクエリーを見て、

「パソコンほしい」 OR 「PCほしい」 OR 「パソコン欲しい」 OR 「PC欲しい」

みたいなクエリーに変換して検索エンジンに投げている。

「パソコン」と「PC」を同一視するには、形態素解析の結果が欲しいところだがクエリー展開はその前なので、ここは独自に頑張ったりする。その場合は「(二重に同じ処理をすることになのるで)トークナイズノーマライズ(勉強会中に発表し、直してもらった)はしないように」といった仕様上の注意が生まれる。

今回は話題が広くて、また勉強会中に公開したいという制約とでまとめられないがこんな感じだった。あとで追記するかも知れない。