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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MTYzODIxZjUxMGVmN2QyMWI5MDNjMWMwNjViZTU3ZmI4ODFhMWJmOA==
4
+ ZmVkZjQ4M2YxMjBlOTY2YWQ2NTFkMDc1OWMyNTQwMDNhNWJlNjMwYw==
5
5
  data.tar.gz: !binary |-
6
- MzdjODk4YmY3MzA2NDJlYzQ3NDg3YTA1ZGRkNzczOTQyNmI0NWU4ZQ==
6
+ M2YwMTExNjM1ODU3OTNjN2ZkY2VhODBlZWMwNjM3YWMyOTQyYTA1MA==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- MmQwZWZjMzFhZDVmMTIzZTc2MWY0ODM5MTA0ODA5YjM5M2QyMGE4ZjE1Mjcy
10
- YWUxYTg2OTE2ZjkwNTVhZDk0M2EzMDY1MDViZDJjNzQwMWRjYWZhZjhkNGU2
11
- ZTc1ZTI5MTUzZDVjYWQwNWUyZWRiOGE5ODNjYjI4ZDFjMmNlYzU=
9
+ ZGUwMDc0NjYzZDU4ZGE5NThmNDBhMmU2ZmZkNDRhMmI4N2I1MzA3ZTc4ODc5
10
+ OTZmMTkxYzk0MzM1NTdkNzJiOTk4MjljZWM3MjViYjliNGEwODQ3NTFlY2E1
11
+ NmRjYjcwZWJkMjIzMzkwYmIxMjA4NDk5ZGUyNjEzODU0M2NjMTU=
12
12
  data.tar.gz: !binary |-
13
- M2IyNWU2MjhlYjJhNGI2Y2Y2MDA5ZTQwMmMxMGNmMzBhZGZhNDUzMDNmNDU4
14
- YWYwOTdjNjIyYzFlNDI0ODdmYTlmODQ4MGVhMTkyZWRkYWY3MWU3ZmIxMGFj
15
- M2EwMzFjNDA4ODdmNmZlYmQxNDIzY2E2MTg0NDMxMWVmZTlmY2Q=
13
+ MzViODY3NzE3ZGM4OThhNjFhZmE5MzVkMjljMGQyZTc4ZWE4ZDYzOWEzNjg1
14
+ ZWRiZWIwNTZjNTQ3MzNlYzRiODM5NzNmZmUwMDE5YzRmYTk4NmU5MDZjY2M5
15
+ MWRlYjdkZGI0ZDE2NDMxMDBkNzc2NzA0Y2U3ZmE2MTQ3MTY5NjY=
data/README.md CHANGED
@@ -1,20 +1,32 @@
1
1
  NAME
2
2
  ------------------
3
3
 
4
- mongoid-fts.rb
4
+ mongoid-fts.rb
5
5
 
6
6
  DESCRIPTION
7
7
  ------------------
8
8
 
9
9
  enable mongodb's new fulltext simply and quickly on your mongoid models.
10
10
 
11
- supports
11
+ mongodb's built-in fulltext is handy, but has several warts which make it
12
+ difficult to use in real projects:
12
13
 
13
- * pagination
14
- * strict literal searching (including stopwords)
15
- * cross models searching
16
- * index is automatically kept in sync
17
- * customize ranking with #to_search
14
+ - queries are boolean OR by default, meaning a user typing more and more
15
+ specific search queries gets back less and less specific search results
16
+ - lack of limit+offset on queries, making pagination a beast
17
+ - lack of substring/fuzzy search
18
+ - lack of literal search (find by id, uuid, etc. without hitting stemming/stopword functionality)
19
+ - ability to search across collections
20
+
21
+ mongoid-fts smoothes over these warts, and more, for your mongoid 3 and 4
22
+ projects supporting:
23
+
24
+ - pagination
25
+ - strict literal searching (including stopwords)
26
+ - cross models searching
27
+ - index is automatically kept in sync
28
+ - customize ranking with #to_search
29
+ - fuzzy search (title and keywords included by default)
18
30
 
19
31
  INSTALL
20
32
  ------------------
@@ -51,7 +63,7 @@ SYNOPSIS
51
63
  #
52
64
  class A
53
65
  include Mongoid::Document
54
- include Mongoid::FTS
66
+ include Mongoid::FTS::Able
55
67
 
56
68
  field(:title)
57
69
  field(:keywords, :type => Array)
@@ -63,14 +75,20 @@ SYNOPSIS
63
75
 
64
76
  class B
65
77
  include Mongoid::Document
66
- include Mongoid::FTS
78
+ include Mongoid::FTS::Able
67
79
 
68
80
  field(:a)
69
81
  field(:b, :type => Array, :default => [])
70
82
  field(:c)
71
83
 
72
84
  def to_search
73
- {:literals => [id, sku], :title => a, :keywords => (b + ['foobar']), :fulltext => c}
85
+ {
86
+ :literals => [id, sku],
87
+ :title => a,
88
+ :keywords => (b + ['foobar']),
89
+ :fuzzy => [a, b],
90
+ :fulltext => c
91
+ }
74
92
  end
75
93
  end
76
94
 
@@ -83,12 +101,14 @@ SYNOPSIS
83
101
 
84
102
  p A.search('cat').size #=> 2
85
103
 
104
+ p A.search(:literal => A.first.id).size #=> 1
105
+
86
106
  # you can to cross-model searches like so
87
107
  #
88
108
  p Mongoid::FTS.search('cat', :models => [A, B])
89
109
  p Mongoid::FTS.search('dog', :models => [A, B])
90
110
 
91
- # pagination is supported with an ugly hack
111
+ # pagination is supported
92
112
 
93
113
  A.search('cats').page(10).per(3)
94
114
 
@@ -109,11 +129,8 @@ SYNOPSIS
109
129
 
110
130
  ````
111
131
 
112
- the implementation has a temporary work around for pagination, see
113
-
114
- https://groups.google.com/forum/#!topic/mongodb-user/2hUgOAN4KKk
115
-
116
- for details
132
+ - this implementation has a work around for fulltext pagination, ref: https://groups.google.com/forum/#!topic/mongodb-user/2hUgOAN4KKk
133
+ - this implementation has a work around for the lack of logical and ref: https://groups.google.com/forum/#!topic/mongodb-user/2hUgOAN4KKk
117
134
 
118
135
  regardless, the *interface* of this mixin is uber simple and should be quite
119
136
  future proof. as the mongodb teams moves search forward i'll track the new
@@ -1,11 +1,12 @@
1
1
  module Mongoid
2
2
  module FTS
3
3
  #
4
- const_set(:Version, '1.1.1') unless const_defined?(:Version)
4
+ const_set(:Version, '2.0.0') unless const_defined?(:Version)
5
5
 
6
+ #
6
7
  class << FTS
7
8
  def version
8
- const_get :Version
9
+ const_get(:Version)
9
10
  end
10
11
 
11
12
  def dependencies
@@ -13,6 +14,9 @@ module Mongoid
13
14
  'mongoid' => [ 'mongoid' , '~> 3.1' ] ,
14
15
  'map' => [ 'map' , '~> 6.5' ] ,
15
16
  'coerce' => [ 'coerce' , '~> 0.0' ] ,
17
+ 'unicode_utils' => [ 'unicode_utils' , '~> 1.4' ] ,
18
+ 'stringex' => [ 'stringex' , '~> 2.0' ] ,
19
+ 'fast_stemmer' => [ 'fast-stemmer' , '~> 1.0' ] ,
16
20
  }
17
21
  end
18
22
 
@@ -36,6 +40,9 @@ module Mongoid
36
40
  end
37
41
  end
38
42
 
43
+ #
44
+ require 'digest/md5'
45
+
39
46
  begin
40
47
  require 'rubygems'
41
48
  rescue LoadError
@@ -44,7 +51,7 @@ module Mongoid
44
51
 
45
52
  if defined?(gem)
46
53
  dependencies.each do |lib, dependency|
47
- gem(*dependency)
54
+ #gem(*dependency)
48
55
  require(lib)
49
56
  end
50
57
  end
@@ -56,7 +63,18 @@ module Mongoid
56
63
  end
57
64
 
58
65
  #
59
- class Error < ::StandardError; end
66
+ require 'unicode_utils/u'
67
+ require 'unicode_utils/each_word'
68
+ require 'unicode_utils/each_grapheme'
69
+
70
+ #
71
+ load FTS.libdir('error.rb')
72
+ load FTS.libdir('util.rb')
73
+ load FTS.libdir('stemming.rb')
74
+ load FTS.libdir('raw.rb')
75
+ load FTS.libdir('results.rb')
76
+ load FTS.libdir('index.rb')
77
+ load FTS.libdir('able.rb')
60
78
 
61
79
  #
62
80
  def FTS.search(*args)
@@ -68,684 +86,117 @@ module Mongoid
68
86
  end
69
87
 
70
88
  def FTS._search(*args)
89
+ #
71
90
  options = Map.options_for!(args)
72
91
 
73
- search = args.join(' ')
74
- words = search.strip.split(/\s+/)
75
- literals = FTS.literals_for(*words)
76
- search = [literals, search].join(' ')
77
-
78
- text = options.delete(:text) || Index.default_collection_name.to_s
79
- limit = [Integer(options.delete(:limit) || 128), 1].max
80
- models = [options.delete(:models), options.delete(:model)].flatten.compact
81
-
82
- models = FTS.models if models.empty?
83
-
84
- _searches =
85
- models.map do |model|
86
- context_type = model.name.to_s
87
-
88
- cmd = Hash.new
89
-
90
- cmd[:text] ||= text
91
-
92
- cmd[:limit] ||= limit
93
-
94
- (cmd[:search] ||= '') << search
95
-
96
- cmd[:project] ||= {'_id' => 1, 'context_type' => 1, 'context_id' => 1}
97
-
98
- cmd[:filter] ||= {'context_type' => context_type}
99
-
100
- options.each do |key, value|
101
- cmd[key] = value
102
- end
103
-
104
- Map.for(session.command(cmd)).tap do |_search|
105
- _search[:_model] = model
106
- _search[:_cmd] = cmd
107
- end
108
- end
109
-
110
- Raw.new(_searches, :_search => search, :_text => text, :_limit => limit, :_models => models)
111
- end
112
-
113
- #
114
- class Raw < ::Array
115
- attr_accessor :_search
116
- attr_accessor :_text
117
- attr_accessor :_limit
118
- attr_accessor :_models
119
-
120
- def initialize(_searches, options = {})
121
- replace(_searches)
122
- ensure
123
- options.each{|k, v| send("#{ k }=", v)}
124
- end
125
- end
126
-
127
- #
128
- class Results < ::Array
129
- attr_accessor :_searches
130
- attr_accessor :_models
131
-
132
- def initialize(_searches)
133
- @_searches = _searches
134
- @_models = []
135
- _denormalize!
136
- @page = 1
137
- @per = size
138
- @num_pages = 1
139
- end
140
-
141
- def paginate(*args)
142
- results = self
143
- options = Map.options_for!(args)
92
+ #
93
+ literals = Coerce.list_of_strings(options[:literals], options[:literal])
144
94
 
145
- page = Integer(args.shift || options[:page] || @page)
146
- per = args.shift || options[:per] || options[:size]
147
-
148
- if per.nil?
149
- return Promise.new(results, page)
150
- else
151
- per = Integer(per)
152
- end
153
-
154
- @page = [page.abs, 1].max
155
- @per = [per.abs, 1].max
156
- @num_pages = (size.to_f / @per).ceil
157
-
158
- offset = (@page - 1) * @per
159
- length = @per
160
-
161
- slice = Array(@_models[offset, length])
162
-
163
- replace(slice)
164
-
165
- self
166
- end
167
-
168
- class Promise
169
- attr_accessor :results
170
- attr_accessor :page
171
-
172
- def initialize(results, page)
173
- @results = results
174
- @page = page
175
- end
176
-
177
- def per(per)
178
- results.per(:page => page, :per => per)
179
- end
180
- end
181
-
182
- def page(*args)
183
- if args.empty?
184
- return @page
185
- else
186
- options = Map.options_for!(args)
187
- page = args.shift || options[:page]
188
- options[:page] = page
189
- paginate(options)
190
- end
191
- end
192
-
193
- alias_method(:current_page, :page)
194
-
195
- def per(*args)
196
- if args.empty?
197
- return @per
198
- else
199
- options = Map.options_for!(args)
200
- per = args.shift || options[:per]
201
- options[:per] = per
202
- paginate(options)
203
- end
204
- end
205
-
206
- def num_pages
207
- @num_pages
208
- end
209
-
210
- def total_pages
211
- num_pages
212
- end
213
-
214
- # TODO - text sorting more...
215
- #
216
- def _denormalize!
217
- #
218
- collection = self
219
-
220
- collection.clear
221
- @_models = []
222
-
223
- return self if @_searches.empty?
224
-
225
- #
226
- _models = @_searches._models
227
-
228
- _position = proc do |model|
229
- _models.index(model) or raise("no position for #{ model.inspect }!?")
230
- end
95
+ terms = Coerce.list_of_strings(options[:terms], options[:searches], options[:term], options[:search], args)
231
96
 
232
- results =
233
- @_searches.map do |_search|
234
- _search['results'] ||= []
97
+ fuzzy = Coerce.list_of_strings(options[:fuzzy])
235
98
 
236
- _search['results'].each do |result|
237
- result['_model'] = _search._model
238
- result['_position'] = _position[_search._model]
99
+ #
100
+ operator =
101
+ case
102
+ when options[:all] || options[:operator].to_s == 'and'
103
+ if options[:all] != true
104
+ terms.push(*Coerce.list_of_strings(options[:all]))
239
105
  end
106
+ :and
240
107
 
241
- _search['results']
242
- end
243
-
244
- results.flatten!
245
- results.compact!
246
-
247
- results.sort! do |a, b|
248
- score = Float(b['score']) <=> Float(a['score'])
249
-
250
- case score
251
- when 0
252
- a['_position'] <=> b['_position']
253
- else
254
- score
255
- end
256
- end
257
-
258
- #
259
- batches = Hash.new{|h,k| h[k] = []}
260
-
261
- results.each do |entry|
262
- obj = entry['obj']
263
-
264
- context_type, context_id = obj['context_type'], obj['context_id']
265
-
266
- batches[context_type].push(context_id)
267
- end
268
-
269
- #
270
- models = FTS.find_in_batches(batches)
271
-
272
- #
273
- limit = @_searches._limit
274
-
275
- #
276
- replace(@_models = models[0 ... limit])
277
-
278
- self
279
- end
280
- end
281
-
282
- #
283
- class Index
284
- include Mongoid::Document
285
-
286
- belongs_to(:context, :polymorphic => true)
287
-
288
- field(:literals, :type => Array)
289
-
290
- field(:literal_title, :type => String)
291
- field(:title, :type => String)
292
-
293
- field(:literal_keywords, :type => Array)
294
- field(:keywords, :type => Array)
295
-
296
- field(:fulltext, :type => String)
297
-
298
- index(
299
- {
300
- :context_type => 1,
301
- :context_id => 1
302
- },
303
-
304
- {
305
- :unique => true,
306
- :sparse => true
307
- }
308
- )
309
-
310
- index(
311
- {
312
- :context_type => 1,
313
- :literals => 'text',
314
- :literal_title => 'text',
315
- :title => 'text',
316
- :literal_keywords => 'text',
317
- :keywords => 'text',
318
- :fulltext => 'text'
319
- },
320
-
321
- {
322
- :name => 'search_index',
323
-
324
- :weights => {
325
- :literals => 200,
326
- :literal_title => 100,
327
- :title => 90,
328
- :literal_keywords => 60,
329
- :keywords => 50,
330
- :fulltext => 1
331
- }
332
- }
333
- )
334
-
335
- before_validation do |index|
336
- index.normalize
337
- end
338
-
339
- before_upsert do |index|
340
- index.normalize
341
- end
342
-
343
- before_save do |index|
344
- index.normalize
345
- end
346
-
347
- validates_presence_of(:context_type)
348
-
349
- def normalize
350
- if !defined?(@normalized) or !@normalized
351
- normalize!
352
- end
353
- end
354
-
355
- def normalize!
356
- index = self
357
-
358
- unless [index.literals].join.strip.empty?
359
- index.literals = FTS.list_of_strings(index.literals)
360
- end
361
-
362
- unless [index.title].join.strip.empty?
363
- index.title = index.title.to_s.strip
364
- end
365
-
366
- unless [index.literal_title].join.strip.empty?
367
- index.literal_title = index.literal_title.to_s.strip
368
- end
369
-
370
- unless [index.keywords].join.strip.empty?
371
- index.keywords = FTS.list_of_strings(index.keywords)
372
- end
373
-
374
- unless [index.literal_keywords].join.strip.empty?
375
- index.literal_keywords = FTS.list_of_strings(index.literal_keywords)
376
- end
377
-
378
- unless [index.fulltext].join.strip.empty?
379
- index.fulltext = index.fulltext.to_s.strip
380
- end
381
-
382
- ensure
383
- @normalized = true
384
- end
385
-
386
- def inspect(*args, &block)
387
- Map.for(as_document).inspect(*args, &block)
388
- end
389
-
390
- def Index.teardown!
391
- Index.remove_indexes
392
- Index.destroy_all
393
- end
394
-
395
- def Index.setup!
396
- Index.create_indexes
397
- end
398
-
399
- def Index.reset!
400
- teardown!
401
- setup!
402
- end
403
-
404
- def Index.rebuild!
405
- batches = Hash.new{|h,k| h[k] = []}
406
-
407
- each do |index|
408
- context_type, context_id = index.context_type, index.context_id
409
- next unless context_type && context_id
410
- (batches[context_type] ||= []).push(context_id)
411
- end
412
-
413
- models = FTS.find_in_batches(batches)
414
-
415
- reset!
416
-
417
- models.each{|model| add(model)}
418
- end
419
-
420
- def Index.add!(model)
421
- to_search = Index.to_search(model)
422
-
423
- literals = to_search.has_key?(:literals) ? Coerce.list_of_strings(to_search[:literals]) : nil
424
-
425
- title = to_search.has_key?(:title) ? Coerce.string(to_search[:title]) : nil
426
- literal_title = to_search.has_key?(:literal_title) ? Coerce.string(to_search[:literal_title]) : nil
427
-
428
- keywords = to_search.has_key?(:keywords) ? Coerce.list_of_strings(to_search[:keywords]) : nil
429
- literal_keywords = to_search.has_key?(:literal_keywords) ? Coerce.list_of_strings(to_search[:literal_keywords]) : nil
430
-
431
- fulltext = to_search.has_key?(:fulltext) ? Coerce.string(to_search[:fulltext]) : nil
432
-
433
- context_type = model.class.name.to_s
434
- context_id = model.id
435
-
436
- conditions = {
437
- :context_type => context_type,
438
- :context_id => context_id
439
- }
440
-
441
- attributes = {
442
- :literals => literals,
443
-
444
- :title => title,
445
- :literal_title => literal_title,
446
-
447
- :keywords => keywords,
448
- :literal_keywords => literal_keywords,
449
-
450
- :fulltext => fulltext
451
- }
452
-
453
- index = nil
454
- n = 42
455
-
456
- n.times do |i|
457
- index = where(conditions).first
458
- break if index
459
-
460
- begin
461
- index = create!(conditions)
462
- break if index
463
- rescue Object
464
- nil
465
- end
466
-
467
- sleep(rand) if i < (n - 1)
468
- end
469
-
470
- if index
471
- begin
472
- index.update_attributes!(attributes)
473
- rescue Object
474
- raise Error.new("failed to update index for #{ conditions.inspect }")
475
- end
476
- else
477
- raise Error.new("failed to create index for #{ conditions.inspect }")
478
- end
479
-
480
- index
481
- end
482
-
483
- def Index.add(*args, &block)
484
- begin
485
- add!(*args, &block)
486
- rescue Object
487
- false
488
- end
489
- end
490
-
491
- def Index.remove!(*args, &block)
492
- options = args.extract_options!.to_options!
493
- models = args.flatten.compact
494
-
495
- model_ids = {}
496
-
497
- models.each do |model|
498
- model_name = model.class.name.to_s
499
- model_ids[model_name] ||= []
500
- model_ids[model_name].push(model.id)
501
- end
502
-
503
- conditions = model_ids.map do |model_name, model_ids|
504
- {:context_type => model_name, :context_id.in => model_ids}
505
- end
506
-
507
- any_of(conditions).destroy_all
508
- end
509
-
510
- def Index.remove(*args, &block)
511
- begin
512
- remove!(*args, &block)
513
- rescue Object
514
- false
515
- end
516
- end
517
-
518
- def Index.to_search(model)
519
- #
520
- to_search = nil
521
-
522
- #
523
- if model.respond_to?(:to_search)
524
- to_search = Map.for(model.to_search)
525
- else
526
- to_search = Map.new
527
-
528
- to_search[:literals] =
529
- %w( id ).map do |attr|
530
- model.send(attr) if model.respond_to?(attr)
531
- end
532
-
533
- to_search[:title] =
534
- %w( title ).map do |attr|
535
- model.send(attr) if model.respond_to?(attr)
536
- end
537
-
538
- to_search[:keywords] =
539
- %w( keywords tags ).map do |attr|
540
- model.send(attr) if model.respond_to?(attr)
108
+ when options[:any] || options[:operator].to_s == 'or'
109
+ if options[:any] != true
110
+ terms.push(*Coerce.list_of_strings(options[:any]))
541
111
  end
112
+ :or
542
113
 
543
- to_search[:fulltext] =
544
- %w( fulltext text content body description ).map do |attr|
545
- model.send(attr) if model.respond_to?(attr)
546
- end
547
- end
548
-
549
- #
550
- unless %w( literals title keywords fulltext ).detect{|key| to_search.has_key?(key)}
551
- raise ArgumentError, "you need to define #{ model }#to_search"
114
+ else
115
+ :and
552
116
  end
553
117
 
554
- #
555
- literals = FTS.normalized_array(to_search[:literals])
556
- title = FTS.normalized_array(to_search[:title])
557
- keywords = FTS.normalized_array(to_search[:keywords])
558
- fulltext = FTS.normalized_array(to_search[:fulltext])
559
-
560
- #
561
- to_search[:literals] = FTS.literals_for(literals)
562
-
563
- to_search[:literal_title] = FTS.literals_for(title).join(' ').strip
564
- to_search[:title] = title.join(' ').strip
565
-
566
- to_search[:literal_keywords] = FTS.literals_for(keywords).join(' ').strip
567
- to_search[:keywords] = keywords.join(' ').strip
568
-
569
- to_search[:fulltext] = fulltext.join(' ').strip
570
-
571
- #
572
- to_search
118
+ #
119
+ if fuzzy.empty?
120
+ fuzzy = terms
573
121
  end
574
- end
575
122
 
576
- def FTS.normalized_array(*array)
577
- array.flatten.map{|_| _.to_s.strip}.select{|_| !_.empty?}
578
- end
123
+ #
124
+ searches = []
579
125
 
580
- def FTS.literals_for(*words)
581
- words = words.join(' ').strip.split(/\s+/)
582
-
583
- words.map do |word|
584
- next if word.empty?
585
- if word =~ /\A__.*__\Z/
586
- word
587
- else
588
- without_delimeters = word.to_s.scan(/[0-9a-zA-Z]/).join
589
- literal = "__#{ without_delimeters }__"
590
- literal
126
+ #
127
+ strings =
128
+ [
129
+ FTS.literals_for(literals),
130
+ FTS.terms_for(terms)
131
+ ].uniq
132
+
133
+ search =
134
+ case operator
135
+ when :and
136
+ FTS.boolean_and(strings)
137
+ when :or
138
+ FTS.boolean_or(strings)
591
139
  end
592
- end.compact
593
- end
594
140
 
595
- def FTS.index(*args, &block)
596
- if args.empty? and block.nil?
597
- Index
598
- else
599
- Index.add(*args, &block)
600
- end
601
- end
141
+ searches.push(search)
602
142
 
603
- def FTS.unindex(*args, &block)
604
- Index.remove(*args, &block)
605
- end
143
+ #
144
+ search = FTS.boolean_or(FTS.fuzzy_for(fuzzy))
145
+ searches.push(search)
606
146
 
607
- def FTS.index!(*args, &block)
608
- Index.add!(*args, &block)
609
- end
147
+ #
148
+ text = options.delete(:text) || Index.default_collection_name.to_s
149
+ limit = [Integer(options.delete(:limit) || 128), 1].max
150
+ models = [options.delete(:models), options.delete(:model)].flatten.compact
610
151
 
611
- def FTS.unindex!(*args, &block)
612
- Index.remove!(*args, &block)
613
- end
152
+ #
153
+ models = FTS.models if models.empty?
614
154
 
615
- #
616
- module Mixin
617
- def Mixin.code
618
- @code ||= proc do
619
- class << self
620
- def search(*args, &block)
621
- options = Map.options_for!(args)
155
+ #
156
+ last = searches.size - 1
622
157
 
623
- options[:model] = self
158
+ searches.each_with_index do |search, i|
159
+ _searches =
160
+ if search.strip.empty?
161
+ []
162
+ else
163
+ models.map do |model|
164
+ context_type = model.name.to_s
165
+
166
+ cmd = Hash.new
624
167
 
625
- args.push(options)
168
+ cmd[:text] ||= text
626
169
 
627
- FTS.search(*args, &block)
628
- end
170
+ cmd[:limit] ||= limit
629
171
 
630
- def _search(*args, &block)
631
- options = Map.options_for!(args)
172
+ (cmd[:search] ||= '') << search
632
173
 
633
- options[:model] = self
174
+ cmd[:project] ||= {'_id' => 1, 'context_type' => 1, 'context_id' => 1}
634
175
 
635
- args.push(options)
176
+ cmd[:filter] ||= {'context_type' => context_type}
636
177
 
637
- FTS.search(*args, &block)
178
+ Map.for(session.command(cmd)).tap do |_search|
179
+ _search[:_model] = model
180
+ _search[:_cmd] = cmd
181
+ end
638
182
  end
639
183
  end
640
184
 
641
- after_save do |model|
642
- FTS::Index.add(model) rescue nil
643
- end
644
-
645
- after_destroy do |model|
646
- FTS::Index.remove(model) rescue nil
647
- end
648
-
649
- has_one(:search_index, :as => :context, :class_name => '::Mongoid::FTS::Index')
650
- end
651
- end
652
-
653
- def Mixin.included(other)
654
- unless other.is_a?(Mixin)
655
- begin
656
- super
657
- ensure
658
- other.module_eval(&Mixin.code)
659
-
660
- FTS.models.dup.each do |model|
661
- FTS.models.delete(model) if model.name == other.name
662
- end
663
-
664
- FTS.models.push(other)
665
- FTS.models.uniq!
666
- end
667
- end
185
+ raw = Raw.new(_searches, :_search => search, :_text => text, :_limit => limit, :_models => models)
186
+ return raw if(i == last || !raw.empty?)
668
187
  end
669
188
  end
670
189
 
671
190
  def FTS.included(other)
672
- unless other.is_a?(FTS::Mixin)
673
- other.send(:include, FTS::Mixin)
674
- end
675
- end
676
-
677
- #
678
- def FTS.models
679
- @models ||= []
680
- end
681
-
682
- def FTS.list_of_strings(*args)
683
- args.flatten.compact.map{|arg| arg.to_s}.select{|arg| !arg.empty?}.uniq
684
- end
685
-
686
- def FTS.session
687
- @session ||= Mongoid::Sessions.default
688
- end
689
-
690
- def FTS.session=(session)
691
- @session = session
692
- end
693
-
694
- def FTS.find_in_batches(queries = {})
695
- models =
696
- queries.map do |model_class, model_ids|
697
- unless model_class.is_a?(Class)
698
- model_class = eval(model_class.to_s)
699
- end
700
-
701
- model_ids = Array(model_ids)
702
-
703
- begin
704
- model_class.find(model_ids)
705
- rescue Mongoid::Errors::DocumentNotFound
706
- model_ids.map do |model_id|
707
- begin
708
- model_class.find(model_id)
709
- rescue Mongoid::Errors::DocumentNotFound
710
- nil
711
- end
712
- end
713
- end
714
- end
715
-
716
- models.flatten!
717
- models.compact!
718
- models
719
- end
720
-
721
- def FTS.enable!(*args)
722
- options = Map.options_for!(args)
723
-
724
- unless options.has_key?(:warn)
725
- options[:warn] = true
726
- end
727
-
728
- begin
729
- session = Mongoid::Sessions.default
730
- session.with(database: :admin).command({ setParameter: 1, textSearchEnabled: true })
731
- rescue Object => e
732
- unless e.is_a?(Mongoid::Errors::NoSessionsConfig)
733
- warn "failed to enable search with #{ e.class }(#{ e.message })"
734
- end
735
- end
191
+ other.send(:include, Able)
192
+ super
736
193
  end
737
194
  end
738
195
 
739
196
  Fts = FTS
740
197
 
741
198
  if defined?(Rails)
742
- class FTS::Engine < ::Rails::Engine
743
- paths['app/models'] = ::File.dirname(__FILE__)
744
-
745
- config.before_initialize do
746
- Mongoid::FTS.enable!(:warn => true)
747
- end
748
- end
199
+ load FTS.libdir('rails.rb')
749
200
  else
750
201
  Mongoid::FTS.enable!(:warn => true)
751
202
  end
@@ -754,6 +205,10 @@ end
754
205
 
755
206
  =begin
756
207
 
208
+ http://docs.mongodb.org/manual/reference/operator/query/text/
209
+
210
+ http://docs.mongodb.org/v2.4/reference/command/text/
211
+
757
212
  Model.mongo_session.command(text: "collection_name", search: "my search string", filter: { ... }, project: { ... }, limit: 10, language: "english")
758
213
 
759
214
  http://blog.serverdensity.com/full-text-search-in-mongodb/