Created
March 10, 2011 22:43
-
-
Save xaviershay/865099 to your computer and use it in GitHub Desktop.
searchable.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Mix this module into a DataMapper::Resource to get fast, indexed full | |
# text searching. | |
# | |
# class Post | |
# include DataMapper::Resource | |
# include Searchable | |
# | |
# property :title, String | |
# property :body, Text | |
# | |
# searchable [:title, :body] | |
# searchable [:title], :index => :title_only | |
# end | |
# | |
# Post.search("hello") | |
# Post.search("hello", :index => :title_only) | |
# | |
# This code could potentially be extracted to a gem | |
module Searchable | |
def self.included(klass) | |
klass.extend ClassMethods | |
end | |
module ClassMethods | |
def searchable(columns, opts = {}) | |
opts[:index] ||= 'search' | |
__searches[opts[:index]] = columns | |
end | |
def search(q, opts = {}) | |
opts[:index] ||= 'search' | |
finder = all(opts.except(:index, :conditions).merge(:conditions => [ | |
"#{opts[:index]}_vector @@ plainto_tsquery('english', ?)", q])) | |
finder &= all(opts[:conditions]) if opts[:conditions] | |
finder | |
end | |
def auto_migrate_up!(repository_name) | |
super | |
__searches.each do |name, columns| | |
[ | |
create_alter_table_sql(repository_name, name), | |
create_index_sql(repository_name, name), | |
create_trigger_sql(repository_name, name, columns) | |
].each do |sql| | |
repository(repository_name).adapter.execute sql | |
end | |
end | |
end | |
def auto_upgrade!(repository_name) | |
super | |
__searches.each do |name, columns| | |
next if repository(repository_name).adapter.field_exists?( | |
storage_name(repository_name), | |
"#{name}_vector" | |
) | |
[ | |
create_alter_table_sql(repository_name, name), | |
create_index_sql(repository_name, name), | |
create_trigger_sql(repository_name, name, columns) | |
].each do |sql| | |
repository(repository_name).adapter.execute sql | |
end | |
end | |
end | |
private | |
def create_alter_table_sql(repository_name, name) | |
<<-EOS | |
ALTER TABLE #{storage_name(repository_name)} | |
ADD COLUMN #{name}_vector tsvector NOT NULL default to_tsvector('') | |
EOS | |
end | |
def create_index_sql(repository_name, name) | |
<<-EOS | |
CREATE INDEX #{storage_name(repository_name)}_#{name}_vector_idx | |
ON #{storage_name(repository_name)} USING gin(#{name}_vector) | |
EOS | |
end | |
def create_trigger_sql(repository_name, name, columns) | |
<<-EOS | |
CREATE TRIGGER #{storage_name(repository_name)}_#{name}_vector_refresh | |
BEFORE INSERT OR UPDATE ON #{storage_name(repository_name)} | |
FOR EACH ROW EXECUTE PROCEDURE | |
tsvector_update_trigger(#{name}_vector, 'pg_catalog.english', | |
#{column_sql(columns)}); | |
EOS | |
end | |
def __searches | |
@__searches ||= {} | |
end | |
def column_sql(columns) | |
columns.map {|column| send(column).field }.join(", ") | |
end | |
# This is fugly prototype code that can be used to index an Author with: | |
# searchable email, user.full_name | |
# | |
# properties = [Author.properties.detect {|x| x.name == :name }, user.email] | |
# basic_properties = properties.reject {|x| x.respond_to?(:relationships) } | |
# child_properties = properties.select {|x| x.respond_to?(:relationships) } | |
# | |
# child_sql = child_properties.map do |x| | |
# <<-EOS | |
# SELECT string_agg(#{x.field}, ' ') INTO child_search | |
# FROM #{x.relationships[0].parent_model.storage_name(repository_name)} | |
# WHERE #{x.relationships[0].parent_key.first.field} = new.#{x.relationships[0].child_key.first.field}; | |
# | |
# search := search || ' ' || child_search; | |
# EOS | |
# end.join("\n") | |
# puts properties.inspect | |
# repository(repository_name).adapter.execute <<-EOS | |
# CREATE OR REPLACE FUNCTION messages_trigger() RETURNS trigger AS $$ | |
# DECLARE | |
# search TEXT; | |
# child_search TEXT; | |
# begin | |
# search := ''; | |
# #{basic_properties.map {|x| "search := search || ' ' || coalesce(new.#{x.field});" }.join("\n")} | |
# #{child_sql} | |
# new.denormalized := search; | |
# return new; | |
# end | |
# $$ LANGUAGE plpgsql; | |
# | |
# CREATE TRIGGER #{storage_name(repository_name)}_search_vector_refresh | |
# BEFORE INSERT OR UPDATE ON #{storage_name(repository_name)} | |
# FOR EACH ROW EXECUTE PROCEDURE | |
# messages_trigger(); | |
# EOS | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment