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.
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/