Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 117 lines (104 sloc) 3.338 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
require 'active_support/core_ext/hash/except'

## Author: Xavier Shay
#
# 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)
module Searchable
  def searchable(columns, options = {})
    index_name = options.fetch(:index, 'search')
    __searches[index_name] = columns
  end

  def search(query, options = {})
    index_name = options.fetch(:index, 'search')
    finder = all(options.except(:index, :conditions).merge(
      :conditions => ["#{index_name}_vector @@ plainto_tsquery('english', ?)", query]
    ))
    finder &= all(options[:conditions]) if options[:conditions]
    finder
  end

  def search_raw query, options = {}
    limit = options.fetch(:limit, 50)
    page = (options[:page] || 1).to_i
    offset = (page - 1)*limit
    repository.adapter.select <<-SQL, query, limit, offset
SELECT
document_id, title, abstract, publish_date, obsoleted,
ts_rank_cd(search_vector, query) AS search_rank
FROM #{storage_name}, plainto_tsquery('english', ?) query
WHERE search_vector @@ query
ORDER BY popularity, search_rank DESC
LIMIT ? OFFSET ?
SQL
  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

  private

  def create_alter_table_sql(repository_name, name)
    <<-SQL
ALTER TABLE #{storage_name(repository_name)}
ADD COLUMN #{name}_vector tsvector NOT NULL
SQL
  end

  def create_index_sql(repository_name, name)
    <<-SQL
CREATE INDEX #{storage_name(repository_name)}_#{name}_vector_idx
ON #{storage_name(repository_name)} USING gin(#{name}_vector)
SQL
  end

  def create_trigger_sql(repository_name, name, columns)
    table_name = storage_name(repository_name)
    vector_column = "#{name}_vector"

    case columns
    when Array
      column_sql = columns.map {|col| send(col).field }.join(', ')
      trigger = "tsvector_update_trigger(#{vector_column}, 'pg_catalog.english', #{column_sql})"
      create_function = ''
    when Hash
      trigger = "#{table_name}_tsvector_update()"
      create_function = <<-SQL
CREATE FUNCTION #{trigger} RETURNS trigger AS $$
begin
new.#{vector_column} :=
#{columns.map {|col,w| "setweight(to_tsvector('pg_catalog.english', coalesce(new.#{col},'')), '#{w}')" }.join(" ||\n" + ' '*13)};
return new;
end
$$ LANGUAGE plpgsql;
SQL
    else
      raise ArgumentError, "unknown type: #{columns.class}"
    end

    <<-SQL
#{create_function}
CREATE TRIGGER #{table_name}_#{name}_vector_refresh
BEFORE INSERT OR UPDATE ON #{table_name}
FOR EACH ROW EXECUTE PROCEDURE #{trigger};
SQL
  end

  def __searches
    @__searches ||= {}
  end
end
Something went wrong with that request. Please try again.