forked from mislav/rfc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearchable.rb
116 lines (104 loc) · 3.26 KB
/
searchable.rb
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