mongoid-fts 0.5.0 → 1.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
- ZjY5OWU1OTAxM2YzZDFiZmUzNTVhODFmMGM5NjNjNjI3OTgyODE1OQ==
4
+ YzRkOTk2NjNkYmQ5ZjM2NjU3YzkxYTM3N2I1MzRhNTFjZjAzMzAwMA==
5
5
  data.tar.gz: !binary |-
6
- YzkwMGEzM2Q3OTM3NDg5OWI3OWVkM2IxYjJhOTFjYTQ1Y2MzYTBhOQ==
6
+ YjUzZGEyOWE2YWViZGRmZmQ4NWJjNTgxZTc4ZGM1MjUyYWI3MDBjMw==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- MWQzYTQ0ZmRhYWJjYThlNDJlOTFhNjgyNDYxYzVlZWI0NjJjYzc0MzNhZjhi
10
- ODliMTRjYmRjMWRmOWYwYjgyYjc0YjlhZTM2ZWIxYWRlMTgxZjRiNDIzMmJm
11
- NDIzMDllYjMwMzJkMjMxMGUyYjc5NjJiMWQ3OGFjZDA3ZDM4ODQ=
9
+ YTEyMWRkYmQ1NWM5YTVmZmJlOGUzOGI4NmFiZmU3M2UzNzRjNjg2MmE5NDEx
10
+ NjUxODE1YzE3NDJmMjJmNTZkZjFhZTljMzkzMDNmZDhmY2NkNDAyNTJhZmMy
11
+ MzM0MmFjNGFkMGMwZmYxNDU2MmRkNWVjM2JkMDM0MWFjMTY5OWQ=
12
12
  data.tar.gz: !binary |-
13
- YjY2ZWM0YTgwMGViZjRmMDU2ZjFkODQxZmY1MTUzMTdkMTJiN2MwY2ZlMjJi
14
- N2VjZTVmMDA1YzAwNjFjYThkZWRhY2I2ZDIyNTk5MzdjYmZkOTMwYjJmM2Yz
15
- MGNjZWM2YTcyNWIzNmUwZGVhNTZjN2QyYjg1MGQwNjBhMmI5ZDg=
13
+ NWE3OGY3MDcwNTE1ZDQ0MmVmYWFhZGE5YmJjZDJlMzFlZGRjM2ZiNTM5ODQw
14
+ MmVhMmNhMGQ5NWJiNmYwOTAwYWY4NzgwNzNhNGRiYzUxY2NjZGU2NzI2OTBi
15
+ NzczZjA2OWQzYWMwN2UwODBjZTJmNzY1NTMzYmFjZjk2YTVhYjY=
data/README.md CHANGED
@@ -4,8 +4,14 @@ NAME
4
4
 
5
5
  DESCRIPTION
6
6
 
7
- enable mongodb's new fulltext simply and quickly on your mongoid models, including pagination.
7
+ enable mongodb's new fulltext simply and quickly on your mongoid models.
8
8
 
9
+ supports
10
+ * pagination
11
+ * strict literal searching (including stopwords)
12
+ * cross models searching
13
+ * index is automatically kept in sync
14
+ * customize ranking with #to_search
9
15
 
10
16
  INSTALL
11
17
 
@@ -59,7 +65,7 @@ SYNOPSIS
59
65
  field(:c)
60
66
 
61
67
  def to_search
62
- {:title => a, :keywords => (b + ['foobar']), :fulltext => c}
68
+ {:literals => [id, sku], :title => a, :keywords => (b + ['foobar']), :fulltext => c}
63
69
  end
64
70
  end
65
71
 
@@ -92,6 +98,10 @@ SYNOPSIS
92
98
 
93
99
  Mongoid::FTS::Index.reset! # completely drop/create indexes - lose all objects
94
100
 
101
+ Mongoid::FTS.index(model) # add an object to the fts index
102
+
103
+ Mongoid::FTS.unindex(model) # remove and object from the fts index
104
+
95
105
  ````
96
106
 
97
107
  the implementation has a temporary work around for pagination, see
@@ -100,7 +110,6 @@ the implementation has a temporary work around for pagination, see
100
110
 
101
111
  for details
102
112
 
103
-
104
113
  regardless, the *interface* of this mixin is uber simple and should be quite
105
114
  future proof. as the mongodb teams moves search forward i'll track the new
106
115
  implementation and preserve the current interface. until it settles down,
@@ -1,7 +1,7 @@
1
1
  module Mongoid
2
2
  module FTS
3
3
  #
4
- const_set(:Version, '0.5.0') unless const_defined?(:Version)
4
+ const_set(:Version, '1.0.0') unless const_defined?(:Version)
5
5
 
6
6
  class << FTS
7
7
  def version
@@ -70,7 +70,10 @@ module Mongoid
70
70
  def FTS._search(*args)
71
71
  options = Map.options_for!(args)
72
72
 
73
- search = args.join(' ')
73
+ search = args.join(' ')
74
+ words = search.strip.split(/\s+/)
75
+ literals = FTS.literals_for(*words)
76
+ search = [literals, search].join(' ')
74
77
 
75
78
  text = options.delete(:text) || Index.default_collection_name.to_s
76
79
  limit = [Integer(options.delete(:limit) || 128), 1].max
@@ -259,18 +262,51 @@ module Mongoid
259
262
 
260
263
  belongs_to(:context, :polymorphic => true)
261
264
 
265
+ field(:literals, :type => Array)
266
+
267
+ field(:literal_title, :type => String)
262
268
  field(:title, :type => String)
269
+
270
+ field(:literal_keywords, :type => Array)
263
271
  field(:keywords, :type => Array)
272
+
264
273
  field(:fulltext, :type => String)
265
274
 
266
275
  index(
267
- {:context_type => 1, :title => 'text', :keywords => 'text', :fulltext => 'text'},
268
- {:weights => { :title => 100, :keywords => 50, :fulltext => 1 }, :name => 'search_index'}
276
+ {
277
+ :context_type => 1,
278
+ :context_id => 1
279
+ },
280
+
281
+ {
282
+ :unique => true,
283
+ :sparse => true
284
+ }
269
285
  )
270
286
 
271
287
  index(
272
- {:context_type => 1, :context_id => 1},
273
- {:unique => true, :sparse => true}
288
+ {
289
+ :context_type => 1,
290
+ :literals => 'text',
291
+ :literal_title => 'text',
292
+ :title => 'text',
293
+ :literal_keywords => 'text',
294
+ :keywords => 'text',
295
+ :fulltext => 'text'
296
+ },
297
+
298
+ {
299
+ :name => 'search_index',
300
+
301
+ :weights => {
302
+ :literals => 200,
303
+ :literal_title => 100,
304
+ :title => 90,
305
+ :literal_keywords => 60,
306
+ :keywords => 50,
307
+ :fulltext => 1
308
+ }
309
+ }
274
310
  )
275
311
 
276
312
  before_validation do |index|
@@ -281,6 +317,10 @@ module Mongoid
281
317
  index.normalize
282
318
  end
283
319
 
320
+ before_save do |index|
321
+ index.normalize
322
+ end
323
+
284
324
  validates_presence_of(:context_type)
285
325
 
286
326
  def normalize
@@ -292,16 +332,24 @@ module Mongoid
292
332
  def normalize!
293
333
  index = self
294
334
 
295
- unless [index.keywords].join.strip.empty?
296
- index.keywords = FTS.list_of_strings(index.keywords)
335
+ unless [index.literals].join.strip.empty?
336
+ index.literals = FTS.list_of_strings(index.literals)
297
337
  end
298
338
 
299
339
  unless [index.title].join.strip.empty?
300
340
  index.title = index.title.to_s.strip
301
341
  end
302
342
 
343
+ unless [index.literal_title].join.strip.empty?
344
+ index.literal_title = index.literal_title.to_s.strip
345
+ end
346
+
303
347
  unless [index.keywords].join.strip.empty?
304
- index.keywords = index.keywords.map{|keyword| keyword.strip}
348
+ index.keywords = FTS.list_of_strings(index.keywords)
349
+ end
350
+
351
+ unless [index.literal_keywords].join.strip.empty?
352
+ index.literal_keywords = FTS.list_of_strings(index.literal_keywords)
305
353
  end
306
354
 
307
355
  unless [index.fulltext].join.strip.empty?
@@ -312,6 +360,10 @@ module Mongoid
312
360
  @normalized = true
313
361
  end
314
362
 
363
+ def inspect(*args, &block)
364
+ Map.for(as_document).inspect(*args, &block)
365
+ end
366
+
315
367
  def Index.teardown!
316
368
  Index.remove_indexes
317
369
  Index.destroy_all
@@ -342,12 +394,18 @@ module Mongoid
342
394
  models.each{|model| add(model)}
343
395
  end
344
396
 
345
- def Index.add(model)
397
+ def Index.add!(model)
346
398
  to_search = Index.to_search(model)
347
399
 
348
- title = to_search.has_key?(:title) ? Coerce.string(to_search[:title]) : nil
349
- keywords = to_search.has_key?(:keywords) ? Coerce.list_of_strings(to_search[:keywords]) : nil
350
- fulltext = to_search.has_key?(:fulltext) ? Coerce.string(to_search[:fulltext]) : nil
400
+ literals = to_search.has_key?(:literals) ? Coerce.list_of_strings(to_search[:literals]) : nil
401
+
402
+ title = to_search.has_key?(:title) ? Coerce.string(to_search[:title]) : nil
403
+ literal_title = to_search.has_key?(:literal_title) ? Coerce.string(to_search[:literal_title]) : nil
404
+
405
+ keywords = to_search.has_key?(:keywords) ? Coerce.list_of_strings(to_search[:keywords]) : nil
406
+ literal_keywords = to_search.has_key?(:literal_keywords) ? Coerce.list_of_strings(to_search[:literal_keywords]) : nil
407
+
408
+ fulltext = to_search.has_key?(:fulltext) ? Coerce.string(to_search[:fulltext]) : nil
351
409
 
352
410
  context_type = model.class.name.to_s
353
411
  context_id = model.id
@@ -358,87 +416,177 @@ module Mongoid
358
416
  }
359
417
 
360
418
  attributes = {
361
- :title => title,
362
- :keywords => keywords,
363
- :fulltext => fulltext
419
+ :literals => literals,
420
+
421
+ :title => title,
422
+ :literal_title => literal_title,
423
+
424
+ :keywords => keywords,
425
+ :literal_keywords => literal_keywords,
426
+
427
+ :fulltext => fulltext
364
428
  }
365
429
 
366
- begin
367
- new(conditions).upsert
368
- rescue Object => e
369
- warn "#{ e.message } (#{ e.class })"
370
-
371
- 4.times do
372
- begin
373
- break if create(conditions)
374
- rescue Object => e
375
- warn "#{ e.message } (#{ e.class })"
376
- nil
377
- end
430
+ index = nil
431
+ n = 42
432
+
433
+ n.times do |i|
434
+ index = where(conditions).first
435
+ break if index
436
+
437
+ begin
438
+ index = create!(conditions)
439
+ break if index
440
+ rescue Object
441
+ nil
378
442
  end
379
- end
380
443
 
381
- # FIXME - go BOOM here if none found...
382
- #
383
- index = where(conditions).first
444
+ sleep(rand) if i < (n - 1)
445
+ end
384
446
 
385
447
  if index
386
- index.update_attributes(attributes)
448
+ begin
449
+ index.update_attributes!(attributes)
450
+ rescue Object
451
+ raise Error.new("failed to update index for #{ conditions.inspect }")
452
+ end
387
453
  else
388
454
  raise Error.new("failed to create index for #{ conditions.inspect }")
389
455
  end
456
+
457
+ index
390
458
  end
391
459
 
392
- def Index.remove(model)
393
- context_type = model.class.name.to_s
394
- context_id = model.id
460
+ def Index.add(*args, &block)
461
+ begin
462
+ add!(*args, &block)
463
+ rescue Object
464
+ false
465
+ end
466
+ end
395
467
 
396
- conditions = {
397
- :context_type => context_type,
398
- :context_id => context_id
399
- }
468
+ def Index.remove!(*args, &block)
469
+ options = args.extract_options!.to_options!
470
+ models = args.flatten.compact
400
471
 
401
- where(conditions).first.tap do |index|
402
- if index
403
- index.destroy rescue nil
404
- end
472
+ model_ids = {}
473
+
474
+ models.each do |model|
475
+ model_name = model.class.name.to_s
476
+ model_ids[model_name] ||= []
477
+ model_ids[model_name].push(model.id)
478
+ end
479
+
480
+ conditions = model_ids.map do |model_name, model_ids|
481
+ {:context_type => model_name, :context_id.in => model_ids}
482
+ end
483
+
484
+ any_of(conditions).destroy_all
485
+ end
486
+
487
+ def Index.remove(*args, &block)
488
+ begin
489
+ remove!(*args, &block)
490
+ rescue Object
491
+ false
405
492
  end
406
493
  end
407
494
 
408
495
  def Index.to_search(model)
496
+ #
409
497
  to_search = nil
410
498
 
499
+ #
411
500
  if model.respond_to?(:to_search)
412
501
  to_search = Map.for(model.to_search)
413
502
  else
414
503
  to_search = Map.new
415
504
 
505
+ to_search[:literals] =
506
+ %w( id ).map do |attr|
507
+ model.send(attr) if model.respond_to?(attr)
508
+ end
509
+
416
510
  to_search[:title] =
417
511
  %w( title ).map do |attr|
418
512
  model.send(attr) if model.respond_to?(attr)
419
- end.compact.join(' ')
513
+ end
420
514
 
421
515
  to_search[:keywords] =
422
516
  %w( keywords tags ).map do |attr|
423
517
  model.send(attr) if model.respond_to?(attr)
424
- end.compact
518
+ end
425
519
 
426
520
  to_search[:fulltext] =
427
521
  %w( fulltext text content body description ).map do |attr|
428
522
  model.send(attr) if model.respond_to?(attr)
429
- end.compact.join(' ')
523
+ end
430
524
  end
431
525
 
432
- unless %w( title keywords fulltext ).detect{|key| to_search.has_key?(key)}
526
+ #
527
+ unless %w( literals title keywords fulltext ).detect{|key| to_search.has_key?(key)}
433
528
  raise ArgumentError, "you need to define #{ model }#to_search"
434
529
  end
435
530
 
531
+ #
532
+ literals = FTS.normalized_array(to_search[:literals])
533
+ title = FTS.normalized_array(to_search[:title])
534
+ keywords = FTS.normalized_array(to_search[:keywords])
535
+ fulltext = FTS.normalized_array(to_search[:fulltext])
536
+
537
+ #
538
+ to_search[:literals] = FTS.literals_for(literals)
539
+
540
+ to_search[:literal_title] = FTS.literals_for(title).join(' ').strip
541
+ to_search[:title] = title.join(' ').strip
542
+
543
+ to_search[:literal_keywords] = FTS.literals_for(keywords).join(' ').strip
544
+ to_search[:keywords] = keywords.join(' ').strip
545
+
546
+ to_search[:fulltext] = fulltext.join(' ').strip
547
+
548
+ #
436
549
  to_search
437
550
  end
438
551
  end
439
552
 
440
- def FTS.index
441
- Index
553
+ def FTS.normalized_array(*array)
554
+ array.flatten.map{|_| _.to_s.strip}.select{|_| !_.empty?}
555
+ end
556
+
557
+ def FTS.literals_for(*words)
558
+ words = words.join(' ').strip.split(/\s+/)
559
+
560
+ words.map do |word|
561
+ next if word.empty?
562
+ if word =~ /\A__.*__\Z/
563
+ word
564
+ else
565
+ without_delimeters = word.to_s.scan(/[0-9a-zA-Z]/).join
566
+ literal = "__#{ without_delimeters }__"
567
+ literal
568
+ end
569
+ end.compact
570
+ end
571
+
572
+ def FTS.index(*args, &block)
573
+ if args.empty? and block.nil?
574
+ Index
575
+ else
576
+ Index.add(*args, &block)
577
+ end
578
+ end
579
+
580
+ def FTS.unindex(*args, &block)
581
+ Index.remove(*args, &block)
582
+ end
583
+
584
+ def FTS.index!(*args, &block)
585
+ Index.add!(*args, &block)
586
+ end
587
+
588
+ def FTS.unindex!(*args, &block)
589
+ Index.remove!(*args, &block)
442
590
  end
443
591
 
444
592
  #
@@ -485,6 +633,11 @@ module Mongoid
485
633
  super
486
634
  ensure
487
635
  other.module_eval(&Mixin.code)
636
+
637
+ FTS.models.dup.each do |model|
638
+ FTS.models.delete(model) if model.name == other.name
639
+ end
640
+
488
641
  FTS.models.push(other)
489
642
  FTS.models.uniq!
490
643
  end
@@ -3,7 +3,7 @@
3
3
 
4
4
  Gem::Specification::new do |spec|
5
5
  spec.name = "mongoid-fts"
6
- spec.version = "0.5.0"
6
+ spec.version = "1.0.0"
7
7
  spec.platform = Gem::Platform::RUBY
8
8
  spec.summary = "mongoid-fts"
9
9
  spec.description = "enable mongodb's new fulltext simply and quickly on your mongoid models, including pagination."
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid-fts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ara T. Howard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-08-01 00:00:00.000000000 Z
11
+ date: 2013-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mongoid