maeshimaの日記

メモ書きです

SCSSの関数で出来ること

lighten(), darken() に感動したので、他にSCSSでなにが出来るか気になった。

Module: Sass::Script::Functionsを見てざっくりおおまかにまとめました。

出来ること

  • 赤色、緑色、青色の調整
  • 色相、彩度、光度の調整
  • 透明度の調整
  • 明るさの調整
  • 二色をまぜる
  • クォートの追加/除去
  • 数値の調整(round, ceil, floor, abs)
  • 単位(%, px, em)の操作
  • 単位付きの数値の比較(例:pxとemの比較)
  • 複数要素の操作(length, nth, join)
  • if 文

その他

そもそもSCSS(というかSass)はRubyで実装されてるらしい。Sass::Script::Functions あたりにメソッドを追加したら、自分で関数を追加できるみたい

メタプログラミングRuby 4章

カレントクラス

カレントクラスはカレントオブジェクト(self)とは別。Rubyのプログラムは常にカレントオブジェクトとカレントクラスを持っている。メソッドを定義したときに、そのメソッドはカレントクラスのインスタンスメソッドになる。

class MyClass
  def method_one
    def method_two; 'Hello!'; end
  end
end

method_two が定義されたときのカレントクラスは self のクラスである MyClass

class_eval と instance_eval の違い

  • class_eval は、self とカレントクラスをレシーバにする。
  • instance_eval は、self をレシーバにし、カレントクラスをレシーバの特異クラスにする。

class_eval と class ... end の違い

  • class は定数が必要だけど、class_eval はクラスを参照する変数なら何でも使える
  • class は新しいスコープをオープンする。class_eval はフラットスコープを持つ

class キーワードを使わずにクラスを定義する

Class.new を使えばOK。

特異メソッド

既存のクラスを少しいじったインスタンスを使いたいけど、継承したりするには大げさなときに使えるっぽい。

特異クラス

  • メソッド探索は、オブジェクトの右(クラス)に進み、そこから継承関係を上に辿っていく。
  • 特異メソッドを定義すると、オブジェクトの一つ右が特異クラスになり、その上がオブジェクトのクラスになる
  • オブジェクトがクラスだった場合は特殊。特異クラスの上はスーパークラスの特異クラスになり、BasicObjectの特異クラスの上は Class クラスになる。

メソッドエイリアス、アラウンドエイリアス

alias や alias_method で既存のメソッドのエイリアスを作り、その後既存のメソッドを修正する。モンキーパッチをあてるだけよりかは、元のメソッドを残しているのでいくらか良心的。でも結局は既存のメソッドを直接修正することになるので影響範囲をよく見て使う必要がありそう。

sunspotで検索する

Working with search - GitHubの意訳。

検索ことはじめ

Sunspot.search メソッドで検索できる。引数は検索で使う一つ以上のクラス(モデル)。オプションでブロックに検索条件を書ける。一番簡単な例としては、渡したクラスのインスタンス全部を検索する物がある

search = Sunspot.search(Post)

同時に二つ以上のクラスから検索したいときは、複数のクラスを引数として渡す

search = Sunspot.search(Post, Comment)

Sunspot::Rails を使っているなら、モデルの暮らすから直接 search メソッドを呼び出せる

search = Post.search

ブロック中で指定する検索条件については別の章で。この章では検索結果を扱う。

検索結果を得る

検索結果の情報を得たければ、hits メソッドを使う。hits メソッドは Sunspot::Search::Hit オブジェクトを返す。Sunspot::Search::Hit オブジェクトは検索結果の情報をカプセル化したもの。

実際のモデルオブジェクトの参照は、Hit#result を呼び出す。もし検索結果のメタデータ(関連度のスコア、geo distane, stored fields, キーワードハイライトなど)を気にしないなら results メソッドで直接検索したオブジェクトの配列にアクセスできる。

willpaginate を使う

will_paginate をロードしていたら、sunspot は自動で hits と results メソッドと will_paginate を統合する。テンプレートには下記のように書ける

<div class="pagination">
  <%= will_paginate(@search.hits) %>
</div>

検索時にページを指定するには下記のようにする

Sunspot.search(Post) do
  paginate(:page => params[:page])
end

メタデータ

実際の検索結果のインスタンスに加えて、Solr はいくつかの検索結果の情報を提供する。検索結果のクラスが持っているフィールドを設定していた場合、Solr はフィールドの値を返す。もしキーワードで検索していた場合は関連度のスコアを返す。下記の例で使っている each_hit_with_result メソッドは Hit オブジェクトと検索結果のモデルを返す便利なイテレータ

<div class="results>
  <% @search.each_hit_with_result do |hit, post| -%>
    <div class="result">
      <h2><%= hit.stored(:title) %></h2>
      <div class="score"><%= hit.score %></div>
      <p><%= h(post.body) %></p>
    </div>
  <% end -%>
</div>

Hit オブジェクトを扱うとき、実際 Hit で参照されるオブジェクトは result メソッドを呼ぶまでインスタンスが作られない。result メソッドは全ての Hit オブジェクトに検索結果を紐づける。格納されたフィールドを使うだけならインスタンスは必要ない。

キーワードハイライト

Hit オブジェクトに highlights メソッドがあり、その中にはハイライトされたフレーズが格納されている。ハイライトのフィールドを限定したい場合、オプションとしてフィールドの名前を引数として渡せる。 highlight メソッドは、フィールドの名前が引数として必須で、指定したフィールドの最初のハイライトが返ってくる。

Hightlight オブジェクトには format メソッドがあり、ブロック中にハイライトの文字列辺のフォーマットを指定できる。例

<ul class="search_results">
  <% @search.each_hit_with_result do |hit, post| -%>
    <li>
      <h3><%= h(post.title) %></h3>
      <p class="summary"><%= hit.highlight(:body).format { |fragment| content_tag(:em, fragment) } %></p>
    </li>
  <% end -%>
</ul>

この例では、ハイライトされたキーワードはタグで囲まれる。

不一致を検知して照合する

Solr は今のところリアルタイム検索を実装していない。言い換えると、書き込み頻度の多いアプリは常に最新のインデックスを参照することは出来ない。通常であれば、追加や更新の操作をした数秒〜数分でインデックスが同期される。削除の操作がすぐに同期されないのは問題。

Sunspot は不一致を推測する。Solr は DB に入っていないオブジェクトを検索結果として参照していても問題ない。results と each_hit_with_result メソッドを使った時、データベースにある結果を返す。データベースにない参照は捨てる。

hits メソッドはちょっと違って、デフォルトでは DB を触らずに Solr とだけやりとりする。DB に入っているデータだけを取得するようにちゃんとチェックしたい場合は、 :verify => true を hits メソッドの引数として渡す必要がある。

その他

検索に引っかかったインデックスの数は、Search#total メソッドで取得できる。

facets

facet は Search#facet メソッドで取得できる。

facet オブジェクトは rows メソッドを持っていて、FacetRow オブジェクトの配列を返す。FacetRow は count と value メソッドを持っている。

references オプションで設定をされたフィールドで、facets オブジェクトを作ることが出来る。facet は、そのプライマリキーで取得されたインスタンスを返す instance メソッドを持っている。hits メソッドは、facet インスタンスを lazy-load する。facet row の instance メソッドは lazy じゃない。

<div class="facets">
  <h3>Browse by Category</h3>
  <ul class="facet">
    <% for row in @search.facet(:category_ids).rows -%>
      <li><%= link_to(row.value, url_for(:category_id => row.value)) %> (<%= row.count %>)</li>
    <% end -%>
  </ul>
</div>

sunspot-rails-tester

justinko/sunspot-rails-tester - GitHubのREADMEの意訳。

この gem は特定の箇所のテストで solr をオンにするためのもの。solr を使用せず、sunspot を stub 化することで不必要なインデックス構築を避けている。

RSpec2 の spec_helper.rb の例。

$original_sunspot_session = Sunspot.session

RSpec.configure do |config|
  config.mock_with :rspec

  config.before do
    Sunspot.session = Sunspot::Rails::StubSessionProxy.new($original_sunspot_session)
  end

  config.before :solr => true do
    Sunspot::Rails::Tester.start_original_sunspot_session
    Sunspot.session = $original_sunspot_session
    Sunspot.remove_all!
  end
end
  • $original_sunspot_session はオリジナルの sunspot セッションを格納する。デフォルトで、 sunspot_rails は SessionProxy::ThreadLocalSessionProxy を使う。
  • 最初の before ブロックでは、 stub セッションを全ての example 用にセットしている。 Sunspot::Rails::StubSessionProxy はインデックス作成をスキップさせるダミークラス。
  • 二つ目の before ブロックは、:solr => true とすることで RSpec2 のメタデータ機能を使っている。:solr => true とした example または example group ではオリジナルの sunspot session を使う。Sunspot::Rails::tester.start_original_sunspot_session はもし solr のインスタンスが動いてなかったら動かす。

sunspot-rails-tester を使った例

require 'spec_helper'

describe 'search page' do
  it 'highlights the active tab in the navigation' do
    # uses the stub session
  end

  it 'finds and displays a person', :solr => true do
    # uses actual solr - indexing will happen
  end
end

sunspot_rails

sunspot_rails/README.rdoc at master from outoftime/sunspot - GitHubの意訳。

Sunspot::Rails は Sunspot の Solr 検索を Rails に統合するためのプラグイン。下記のような機能を提供する。

  • config/sunspot.yml で sunspot の設定が出来る
  • ActiveRecord を拡張してインデックスの作成/設定と検索とを楽にする
  • ActiveRecordのオブジェクトが保存されたときや削除されたときに自動でインデックスも作成/削除される(機能をオフにすることも出来る)
  • 自動で各リクエストの最後に Solr の変更をコミットする(機能をオフにすることも出来る)
  • 孤立したドキュメントを探して直してインデックスを再構築するメソッドを提供
  • Solr インスタンスを sunspot.yml の設定を利用しつつ起動したり中止したり出来る

Rails の 2.3 と 3.0 でテストされている。

インストール

Gemfile

gem 'sunspot_rails'

Sunspot::Rails を使う

Solr スキーマを修正したい場合、RAILS_ROOT/solr/conf を作って、Solr gem の solr/solr/conf ディレクトリの中身をコピーする。ファイルが正しい場所に存在する場合、Sunspot::Rails はそれを検知して Solr にその設定を使うように指示する。schema.xml を修正したときには注意すべき。Sunspot relies on the field naming scheme in the packaged schema file.

Solr インスタンスを開始するには下記のようにする。

rake sunspot:solr:start

Sunspot と一緒にパッケージされている Solr インスタンスは開発用であり、production では推奨されない。詳しくはSunspot のドキュメント読んで。

検索とインデックス作成用の設定

検索とインデックス作成用の設定は下記のようにする。

class Post < ActiveRecord::Base
  searchable do
    text :title, :body
    integer :blog_id
    time :updated_at
    string :sort_title do
      title.downcase.sub(/^(an?|the) /, '')
    end
  end
end

ブロック中に何が出来るのかは Sunspot.setup のドキュメントを参照のこと。

インデックスの更新

保存したときに自動でインデックスの更新、削除したときに自動でインデックスの削除をするのを下記のようにすることでやめさせることが出来る。

class Post < ActiveRecord::Base
  searchable :auto_index => false, :auto_remove => false do
    # setup...
  end
end

auto_remove は推奨されない。インデックスを削除せずにオブジェクトを削除すると、孤立したドキュメントがインデックスに残ってしまう。これはよくない。auto_indexをオフにするのは、手動でインデックスを更新すればいいので問題ない。

もしインデックス変更のフックをオフにした場合、下記のように直接インデックスを変更できる

post = Post.create
post.index
post.remove_from_index

committing

Solr 内でデータが変更されたとき、それは最初にメモリに入れられる。そのときに走っている検索用のインスタンスは変更したデータを使えない。Solr にコミットすると、変更をディスクに書き込んで、新しい検索用のインスタンスを作る。この操作はかなりコストが高い。ドキュメントが作成されるか削除されるかしたときに毎回コミットするようなことをせずに、Sunspot::Rails は各リクエストの最後にコミットする。すぐにコミットしたい場合は下記のように!をつける。

post = Post.create
post.index!
# this is the same as...
post.index
Sunspot.commit

コントローラのリクエストの外のコンテキストのテストを書くときには、これらの二つのどちらかやり方を使う。

検索

こんな風にする

Post.search do
  with :blog_id, 1
  with(:updated_at).greater_than(Time.now - 2.weeks)
  order :sort_title, :asc
  paginate :page => 1, :per_page => 15
end

ブロック中で使える全てのオプションを知りたければ Sunspot.serach のドキュメントを見るべし。

ID の検索

検索したときに、モデルのIDだけ取得したいときがあるかもしれない。search_ids メソッドを使うと、search と同じ検索の仕方で、戻り値はIDの配列。

複数テーブルの検索

Sunspot は検索の対象が一つなのか二つ以上なのか知らない。Sunspot::Rails はこのへんサポートしてないので Sunspotのインタフェースを使う。

Sunspot.search(Post, Comment) do
  with :blog_id, 1
  order :created_at, :asc
end

詳しくは Sunspot のドキュメントを参照のこと。

検索機能を mixin で追加する

SunSpot は一箇所に設定を置く必要がない。Sunspot.setup メソッドは二回以上呼び出すことが出来る。これはmixinで検索機能を加えるときに使える。例えば追加の検索フィールドを、Ratable モジュールを mixin したクラスに加えたい場合は下記のようにする

module Ratable
  def self.included(base)
    if base.searchable?
      base.searchable do
        float :average_rating do
          ratings.average(:value)
        end
      end
    end
  end
end

base.serachable? は検索の設定がすでにされており、追加で設定可能かどうかを返す。上記のコードを使うには、 mixin する前に既に searchable を設定している必要がある。

ユーティリティメソッド

モデルに紐付いたインデックスを再構築するには下記のようにすることができる

Post.reindex

もし何らかの理由でモデルがDBから削除され、インデックスからは削除されなかった場合、それらは孤立する。インデックスにあるけどDBにないモデルのIDを得るために、 index_orphans メソッドが用意されている。インデックスからそれらを削除するために、 clean_index_orphans メソッドが用意されている。いずれのメソッドも使う必要があるような状況にするべきではない。

Rspec で Solr を使ったテストをする

ActiveRecord のモデルを扱う際に、sunspot と Solr の統合をオフにするにはまず require 'sunspot/rails/spec_helper' とする。それから下記のように disconnect_sunspot メソッドを使う

require 'sunspot/rails/spec_helper'

describe Post do
  disconnect_sunspot

  it 'should have some behavior'
    # ...
  end
end

上記の examples はすべての Sunspot の呼び出しが stub out される。Sunspot#serach メソッドが stub 化された、結果が何も入っていない search オブジェクトを返すようになる。

もっとくわしく

Sunspot のドキュメントを読むべき。Sunspot::Rails は Sunspot のラッパなので、Sunspot::Rails の機能はすべて Sunspot に実装されている。

Sunspot 1.2.rc3 - Solr-powered search for Ruby objects - API Documentation