mongoid-fts 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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