mongoid-fts 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module Mongoid
2
+ module FTS
3
+ module Able
4
+ def Able.code
5
+ @code ||= proc do
6
+ class << self
7
+ def fts_search(*args, &block)
8
+ options = Map.options_for!(args)
9
+
10
+ options[:model] = self
11
+
12
+ args.push(options)
13
+
14
+ FTS.search(*args, &block)
15
+ end
16
+ alias :search :fts_search
17
+
18
+ def _fts_search(*args, &block)
19
+ options = Map.options_for!(args)
20
+
21
+ options[:model] = self
22
+
23
+ args.push(options)
24
+
25
+ FTS.search(*args, &block)
26
+ end
27
+ alias :_search :_fts_search
28
+ end
29
+
30
+ after_save do |model|
31
+ FTS::Index.add(model)
32
+ end
33
+
34
+ after_destroy do |model|
35
+ FTS::Index.remove(model) rescue nil
36
+ end
37
+
38
+ has_one(:fts_index, :as => :context, :class_name => '::Mongoid::FTS::Index')
39
+ end
40
+ end
41
+
42
+ def Able.included(other)
43
+ unless other.is_a?(Able)
44
+ begin
45
+ super
46
+ ensure
47
+ other.module_eval(&Able.code)
48
+
49
+ FTS.models.dup.each do |model|
50
+ FTS.models.delete(model) if model.name == other.name
51
+ end
52
+
53
+ FTS.models.push(other)
54
+ FTS.models.uniq!
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def FTS.included(other)
61
+ other.send(:include, FTS::Able)
62
+ end
63
+
64
+ def FTS.able
65
+ Able
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module FTS
3
+ class Error < ::StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,293 @@
1
+ module Mongoid
2
+ module FTS
3
+ class Index
4
+ #
5
+ include Mongoid::Document
6
+
7
+ #
8
+ belongs_to(:context, :polymorphic => true)
9
+
10
+ #
11
+ field(:literals, :type => Array)
12
+
13
+ field(:title, :type => Array)
14
+
15
+ field(:keywords, :type => Array)
16
+
17
+ field(:fuzzy, :type => Array)
18
+
19
+ field(:fulltext, :type => Array)
20
+
21
+ #
22
+ index(
23
+ {
24
+ :context_type => 1,
25
+ :context_id => 1
26
+ },
27
+
28
+ {
29
+ :unique => true,
30
+ :sparse => true
31
+ }
32
+ )
33
+
34
+ # FIXME - this hack gets around https://github.com/mongoid/mongoid/issues/3080
35
+ #
36
+ index_options[
37
+
38
+ normalize_spec(
39
+ :context_type => 1,
40
+
41
+ :literals => 'text',
42
+
43
+ :title => 'text',
44
+
45
+ :keywords => 'text',
46
+
47
+ :fuzzy => 'text',
48
+
49
+ :fulltext => 'text'
50
+ )
51
+
52
+ ] =
53
+
54
+ normalize_index_options(
55
+ :name => 'search_index',
56
+
57
+ :default_language => 'none',
58
+
59
+ :weights => {
60
+ :literals => 200,
61
+
62
+ :title => 90,
63
+
64
+ :keywords => 50,
65
+
66
+ :fulltext => 1
67
+ }
68
+ )
69
+ #
70
+ #
71
+
72
+ before_validation do |index|
73
+ index.normalize
74
+ end
75
+
76
+ before_upsert do |index|
77
+ index.normalize
78
+ end
79
+
80
+ before_save do |index|
81
+ index.normalize
82
+ end
83
+
84
+ validates_presence_of(:context_type)
85
+
86
+ def normalize
87
+ if !defined?(@normalized) or !@normalized
88
+ normalize!
89
+ end
90
+ end
91
+
92
+ def normalize!
93
+ index = self
94
+
95
+ %w( literals title keywords fulltext ).each do |attr|
96
+ index[attr] = FTS.list_of_strings(index[attr])
97
+ end
98
+ ensure
99
+ @normalized = true
100
+ end
101
+
102
+ def inspect(*args, &block)
103
+ Map.for(as_document).inspect(*args, &block)
104
+ end
105
+
106
+ def Index.teardown!
107
+ Index.remove_indexes
108
+ Index.destroy_all
109
+ end
110
+
111
+ def Index.setup!
112
+ Index.create_indexes
113
+ end
114
+
115
+ def Index.reset!
116
+ teardown!
117
+ setup!
118
+ end
119
+
120
+ def Index.rebuild!
121
+ batches = Hash.new{|h,k| h[k] = []}
122
+
123
+ each do |index|
124
+ context_type, context_id = index.context_type, index.context_id
125
+ next unless context_type && context_id
126
+ (batches[context_type] ||= []).push(context_id)
127
+ end
128
+
129
+ models = FTS.find_in_batches(batches)
130
+
131
+ reset!
132
+
133
+ models.each{|model| add(model)}
134
+ end
135
+
136
+ def Index.add!(model)
137
+ to_search = Index.to_search(model)
138
+
139
+ literals = to_search.has_key?(:literals) ? Coerce.list_of_strings(to_search[:literals]) : nil
140
+
141
+ title = to_search.has_key?(:title) ? Coerce.string(to_search[:title]) : nil
142
+
143
+ keywords = to_search.has_key?(:keywords) ? Coerce.list_of_strings(to_search[:keywords]) : nil
144
+
145
+ fuzzy = to_search.has_key?(:fuzzy) ? Coerce.list_of_strings(to_search[:fuzzy]) : nil
146
+
147
+ fulltext = to_search.has_key?(:fulltext) ? Coerce.string(to_search[:fulltext]) : nil
148
+
149
+ context_type = model.class.name.to_s
150
+ context_id = model.id
151
+
152
+ conditions = {
153
+ :context_type => context_type,
154
+ :context_id => context_id
155
+ }
156
+
157
+ attributes = {
158
+ :literals => literals,
159
+ :title => title,
160
+ :keywords => keywords,
161
+ :fuzzy => fuzzy,
162
+ :fulltext => fulltext
163
+ }
164
+
165
+ index = nil
166
+ n = 42
167
+
168
+ n.times do |i|
169
+ index = where(conditions).first
170
+ break if index
171
+
172
+ begin
173
+ index = create!(conditions)
174
+ break if index
175
+ rescue Object
176
+ nil
177
+ end
178
+
179
+ sleep(rand) if i < (n - 1)
180
+ end
181
+
182
+ if index
183
+ begin
184
+ index.update_attributes!(attributes)
185
+ rescue Object
186
+ raise Error.new("failed to update index for #{ conditions.inspect }")
187
+ end
188
+ else
189
+ raise Error.new("failed to create index for #{ conditions.inspect }")
190
+ end
191
+
192
+ index
193
+ end
194
+
195
+ def Index.add(*args, &block)
196
+ add!(*args, &block)
197
+ end
198
+
199
+ def Index.remove!(*args, &block)
200
+ options = args.extract_options!.to_options!
201
+ models = args.flatten.compact
202
+
203
+ model_ids = {}
204
+
205
+ models.each do |model|
206
+ model_name = model.class.name.to_s
207
+ model_ids[model_name] ||= []
208
+ model_ids[model_name].push(model.id)
209
+ end
210
+
211
+ conditions = model_ids.map do |model_name, model_ids|
212
+ {:context_type => model_name, :context_id.in => model_ids}
213
+ end
214
+
215
+ any_of(conditions).destroy_all
216
+ end
217
+
218
+ def Index.remove(*args, &block)
219
+ remove!(*args, &block)
220
+ end
221
+
222
+ def Index.to_search(model)
223
+ #
224
+ to_search = nil
225
+
226
+ #
227
+ if model.respond_to?(:to_search)
228
+ to_search = Map.for(model.to_search)
229
+ else
230
+ to_search = Map.new
231
+
232
+ to_search[:literals] =
233
+ %w( id ).map do |attr|
234
+ model.send(attr) if model.respond_to?(attr)
235
+ end
236
+
237
+ to_search[:title] =
238
+ %w( title ).map do |attr|
239
+ model.send(attr) if model.respond_to?(attr)
240
+ end
241
+
242
+ to_search[:keywords] =
243
+ %w( keywords tags ).map do |attr|
244
+ model.send(attr) if model.respond_to?(attr)
245
+ end
246
+
247
+ to_search[:fulltext] =
248
+ %w( fulltext text content body description ).map do |attr|
249
+ model.send(attr) if model.respond_to?(attr)
250
+ end
251
+ end
252
+
253
+ #
254
+ required = %w( literals title keywords fuzzy fulltext )
255
+ actual = to_search.keys
256
+
257
+ =begin
258
+ missing = required - actual
259
+ unless missing.empty?
260
+ raise ArgumentError, "#{ model.class.inspect }#to_search missing keys #{ missing.inspect }"
261
+ end
262
+ =end
263
+
264
+ invalid = actual - required
265
+ unless invalid.empty?
266
+ raise ArgumentError, "#{ model.class.inspect }#to_search invalid keys #{ invalid.inspect }"
267
+ end
268
+
269
+ #
270
+ literals = FTS.normalized_array(to_search[:literals])
271
+ title = FTS.normalized_array(to_search[:title])
272
+ keywords = FTS.normalized_array(to_search[:keywords])
273
+ fuzzy = FTS.normalized_array(to_search[:fuzzy])
274
+ fulltext = FTS.normalized_array(to_search[:fulltext])
275
+
276
+ #
277
+ if to_search[:fuzzy].nil?
278
+ fuzzy = [title, keywords]
279
+ end
280
+
281
+ #
282
+ to_search[:literals] = FTS.literals_for(literals).uniq
283
+ to_search[:title] = (title + FTS.terms_for(title)).uniq
284
+ to_search[:keywords] = (keywords + FTS.terms_for(keywords)).uniq
285
+ to_search[:fuzzy] = FTS.fuzzy_for(fuzzy).uniq
286
+ to_search[:fulltext] = (FTS.terms_for(fulltext, :subterms => true)).uniq
287
+
288
+ #
289
+ to_search
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,11 @@
1
+ module Mongoid
2
+ module FTS
3
+ class Engine < ::Rails::Engine
4
+ paths['app/models'] = ::File.dirname(__FILE__)
5
+
6
+ config.before_initialize do
7
+ Mongoid::FTS.enable!(:warn => true)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Mongoid
2
+ module FTS
3
+ class Raw < ::Array
4
+ attr_accessor :_search
5
+ attr_accessor :_text
6
+ attr_accessor :_limit
7
+ attr_accessor :_models
8
+
9
+ def initialize(_searches, options = {})
10
+ replace(_searches)
11
+ ensure
12
+ options.each{|k, v| send("#{ k }=", v)}
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,195 @@
1
+ module Mongoid
2
+ module FTS
3
+ class Results < ::Array
4
+ attr_accessor :_searches
5
+ attr_accessor :_models
6
+
7
+ def initialize(_searches)
8
+ @_searches = _searches
9
+ @_models = []
10
+ _denormalize!
11
+ @page = 1
12
+ @per = size
13
+ @num_pages = 1
14
+ end
15
+
16
+ def paginate(*args)
17
+ results = self
18
+ options = Map.options_for!(args)
19
+
20
+ page = Integer(args.shift || options[:page] || @page)
21
+ per = args.shift || options[:per] || options[:size]
22
+
23
+ if per.nil?
24
+ return Promise.new(results, page)
25
+ else
26
+ per = Integer(per)
27
+ end
28
+
29
+ @page = [page.abs, 1].max
30
+ @per = [per.abs, 1].max
31
+ @num_pages = (size.to_f / @per).ceil
32
+
33
+ offset = (@page - 1) * @per
34
+ length = @per
35
+
36
+ slice = Array(@_models[offset, length])
37
+
38
+ replace(slice)
39
+
40
+ self
41
+ end
42
+
43
+ class Promise
44
+ attr_accessor :results
45
+ attr_accessor :page
46
+
47
+ def initialize(results, page)
48
+ @results = results
49
+ @page = page
50
+ end
51
+
52
+ def per(per)
53
+ results.per(:page => page, :per => per)
54
+ end
55
+ end
56
+
57
+ def page(*args)
58
+ if args.empty?
59
+ return @page
60
+ else
61
+ options = Map.options_for!(args)
62
+ page = args.shift || options[:page]
63
+ options[:page] = page
64
+ paginate(options)
65
+ end
66
+ end
67
+
68
+ alias_method(:current_page, :page)
69
+
70
+ def per(*args)
71
+ if args.empty?
72
+ return @per
73
+ else
74
+ options = Map.options_for!(args)
75
+ per = args.shift || options[:per]
76
+ options[:per] = per
77
+ paginate(options)
78
+ end
79
+ end
80
+
81
+ def num_pages
82
+ @num_pages
83
+ end
84
+
85
+ def total_pages
86
+ num_pages
87
+ end
88
+
89
+ # TODO - text sorting more...
90
+ #
91
+ def _denormalize!
92
+ #
93
+ collection = self
94
+
95
+ collection.clear
96
+ @_models = []
97
+
98
+ return self if @_searches.empty?
99
+
100
+ #
101
+ _models = @_searches._models
102
+
103
+ _position = proc do |model|
104
+ _models.index(model) or raise("no position for #{ model.inspect }!?")
105
+ end
106
+
107
+ results =
108
+ @_searches.map do |_search|
109
+ _search['results'] ||= []
110
+
111
+ _search['results'].each do |result|
112
+ result['_model'] = _search._model
113
+ result['_position'] = _position[_search._model]
114
+ end
115
+
116
+ _search['results']
117
+ end
118
+
119
+ results.flatten!
120
+ results.compact!
121
+
122
+ =begin
123
+ results.sort! do |a, b|
124
+ score = Float(b['score']) <=> Float(a['score'])
125
+
126
+ case score
127
+ when 0
128
+ a['_position'] <=> b['_position']
129
+ else
130
+ score
131
+ end
132
+ end
133
+ =end
134
+
135
+ #
136
+ batches = Hash.new{|h,k| h[k] = []}
137
+
138
+ results.each do |entry|
139
+ obj = entry['obj']
140
+
141
+ context_type, context_id = obj['context_type'], obj['context_id']
142
+
143
+ batches[context_type].push(context_id)
144
+ end
145
+
146
+ #
147
+ models = FTS.find_in_batches(batches)
148
+
149
+ #
150
+ result_index = {}
151
+
152
+ results.each do |result|
153
+ context_type = result['obj']['context_type'].to_s
154
+ context_id = result['obj']['context_id'].to_s
155
+ key = [context_type, context_id]
156
+
157
+ result_index[key] = result
158
+ end
159
+
160
+ #
161
+ models.each do |model|
162
+ context_type = model.class.name.to_s
163
+ context_id = model.id.to_s
164
+ key = [context_type, context_id]
165
+
166
+ result = result_index[key]
167
+ model['_fts_index'] = result
168
+ end
169
+
170
+ #
171
+ models.sort! do |model_a, model_b|
172
+ a = model_a['_fts_index']
173
+ b = model_b['_fts_index']
174
+
175
+ score = Float(b['score']) <=> Float(a['score'])
176
+
177
+ case score
178
+ when 0
179
+ a['_position'] <=> b['_position']
180
+ else
181
+ score
182
+ end
183
+ end
184
+
185
+ #
186
+ limit = @_searches._limit
187
+
188
+ #
189
+ replace(@_models = models[0 ... limit])
190
+
191
+ self
192
+ end
193
+ end
194
+ end
195
+ end