mongoid_fulltext 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +47 -0
  3. data/.rspec +1 -1
  4. data/.rubocop.yml +6 -0
  5. data/.rubocop_todo.yml +101 -0
  6. data/.travis.yml +11 -3
  7. data/CHANGELOG.md +9 -2
  8. data/Gemfile +19 -9
  9. data/LICENSE +1 -1
  10. data/README.md +12 -9
  11. data/Rakefile +9 -29
  12. data/lib/mongoid/full_text_search/version.rb +5 -0
  13. data/lib/mongoid/full_text_search.rb +372 -0
  14. data/lib/mongoid/indexable.rb +13 -0
  15. data/lib/mongoid/indexes.rb +13 -0
  16. data/lib/mongoid_fulltext.rb +1 -341
  17. data/mongoid_fulltext.gemspec +16 -82
  18. data/spec/models/accentless_artwork.rb +1 -1
  19. data/spec/models/advanced_artwork.rb +1 -1
  20. data/spec/models/basic_artwork.rb +0 -1
  21. data/spec/models/delayed_artwork.rb +1 -2
  22. data/spec/models/external_artist.rb +1 -2
  23. data/spec/models/external_artwork.rb +1 -2
  24. data/spec/models/external_artwork_no_fields_supplied.rb +2 -2
  25. data/spec/models/filtered_artist.rb +4 -4
  26. data/spec/models/filtered_artwork.rb +7 -7
  27. data/spec/models/filtered_other.rb +3 -3
  28. data/spec/models/hidden_dragon.rb +0 -1
  29. data/spec/models/multi_external_artwork.rb +3 -3
  30. data/spec/models/multi_field_artist.rb +1 -1
  31. data/spec/models/multi_field_artwork.rb +1 -1
  32. data/spec/models/partitioned_artist.rb +8 -9
  33. data/spec/models/russian_artwork.rb +2 -2
  34. data/spec/models/short_prefixes_artwork.rb +3 -4
  35. data/spec/models/stopwords_artwork.rb +3 -4
  36. data/spec/mongoid/full_text_search_spec.rb +752 -0
  37. data/spec/spec_helper.rb +11 -7
  38. metadata +27 -68
  39. data/VERSION +0 -1
  40. data/lib/mongoid_indexes.rb +0 -12
  41. data/spec/config/mongoid.yml +0 -6
  42. data/spec/mongoid/fulltext_spec.rb +0 -799
@@ -3,5 +3,5 @@ class AccentlessArtwork
3
3
  include Mongoid::FullTextSearch
4
4
 
5
5
  field :title
6
- fulltext_search_in :title, :remove_accents => false
6
+ fulltext_search_in :title, remove_accents: false
7
7
  end
@@ -3,5 +3,5 @@ class AdvancedArtwork
3
3
  include Mongoid::FullTextSearch
4
4
 
5
5
  field :title
6
- fulltext_search_in :title, :ngram_width => 4, :alphabet => 'abcdefg'
6
+ fulltext_search_in :title, ngram_width: 4, alphabet: 'abcdefg'
7
7
  end
@@ -4,5 +4,4 @@ class BasicArtwork
4
4
 
5
5
  field :title
6
6
  fulltext_search_in :title
7
-
8
7
  end
@@ -3,6 +3,5 @@ class DelayedArtwork
3
3
  include Mongoid::FullTextSearch
4
4
 
5
5
  field :title
6
- fulltext_search_in :title, :reindex_immediately => false
7
-
6
+ fulltext_search_in :title, reindex_immediately: false
8
7
  end
@@ -2,10 +2,9 @@ class ExternalArtist
2
2
  include Mongoid::Document
3
3
  include Mongoid::FullTextSearch
4
4
  field :full_name
5
- fulltext_search_in :full_name, :index_name => 'mongoid_fulltext.artworks_and_artists'
5
+ fulltext_search_in :full_name, index_name: 'mongoid_fulltext.artworks_and_artists'
6
6
 
7
7
  def to_s
8
8
  full_name
9
9
  end
10
-
11
10
  end
@@ -2,10 +2,9 @@ class ExternalArtwork
2
2
  include Mongoid::Document
3
3
  include Mongoid::FullTextSearch
4
4
  field :title
5
- fulltext_search_in :title, :index_name => 'mongoid_fulltext.artworks_and_artists'
5
+ fulltext_search_in :title, index_name: 'mongoid_fulltext.artworks_and_artists'
6
6
 
7
7
  def to_s
8
8
  title
9
9
  end
10
-
11
10
  end
@@ -4,9 +4,9 @@ class ExternalArtworkNoFieldsSupplied
4
4
  field :title
5
5
  field :year
6
6
  field :artist
7
- fulltext_search_in :index_name => 'mongoid_fulltext.artworks_and_artists'
7
+ fulltext_search_in index_name: 'mongoid_fulltext.artworks_and_artists'
8
8
 
9
9
  def to_s
10
- '%s (%s %s)' % [title, artist, year]
10
+ '%s (%s %s)' % [title, artist, year]
11
11
  end
12
12
  end
@@ -2,9 +2,9 @@ class FilteredArtist
2
2
  include Mongoid::Document
3
3
  include Mongoid::FullTextSearch
4
4
  field :full_name
5
- fulltext_search_in :full_name, :index_name => 'mongoid_fulltext.artworks_and_artists',
6
- :filters => { :is_foobar => lambda { |x| x.full_name == 'foobar' },
7
- :is_artist => lambda { |x| true },
8
- :is_artwork => lambda { |x| false }
5
+ fulltext_search_in :full_name, index_name: 'mongoid_fulltext.artworks_and_artists',
6
+ filters: { is_foobar: ->(x) { x.full_name == 'foobar' },
7
+ is_artist: ->(_x) { true },
8
+ is_artwork: ->(_x) { false }
9
9
  }
10
10
  end
@@ -1,12 +1,12 @@
1
1
  class FilteredArtwork
2
2
  include Mongoid::Document
3
3
  include Mongoid::FullTextSearch
4
- field :title, :type => String
5
- field :colors, :type => Array, :default => []
6
- fulltext_search_in :title, :index_name => 'mongoid_fulltext.artworks_and_artists',
7
- :filters => { :is_foobar => lambda { |x| x.title == 'foobar' },
8
- :is_artwork => lambda { |x| true },
9
- :is_artist => lambda { |x| false },
10
- :colors? => lambda { |x| x.colors }
4
+ field :title, type: String
5
+ field :colors, type: Array, default: []
6
+ fulltext_search_in :title, index_name: 'mongoid_fulltext.artworks_and_artists',
7
+ filters: { is_foobar: ->(x) { x.title == 'foobar' },
8
+ is_artwork: ->(_x) { true },
9
+ is_artist: ->(_x) { false },
10
+ colors?: ->(x) { x.colors }
11
11
  }
12
12
  end
@@ -4,8 +4,8 @@ class FilteredOther
4
4
  include Mongoid::Document
5
5
  include Mongoid::FullTextSearch
6
6
  field :name
7
- fulltext_search_in :name, :index_name => 'mongoid_fulltext.artworks_and_artists',
8
- :filters => { :is_fuzzy => lambda { |x| true },
9
- :is_awesome => lambda { |x| false }
7
+ fulltext_search_in :name, index_name: 'mongoid_fulltext.artworks_and_artists',
8
+ filters: { is_fuzzy: ->(_x) { true },
9
+ is_awesome: ->(_x) { false }
10
10
  }
11
11
  end
@@ -2,5 +2,4 @@
2
2
  class HiddenDragon
3
3
  include Mongoid::Document
4
4
  include Mongoid::FullTextSearch
5
-
6
5
  end
@@ -4,7 +4,7 @@ class MultiExternalArtwork
4
4
  field :title
5
5
  field :year
6
6
  field :artist
7
- fulltext_search_in :title, :index_name => 'mongoid_fulltext.titles'
8
- fulltext_search_in :year, :index_name => 'mongoid_fulltext.years'
9
- fulltext_search_in :title, :year, :artist, :index_name => 'mongoid_fulltext.all'
7
+ fulltext_search_in :title, index_name: 'mongoid_fulltext.titles'
8
+ fulltext_search_in :year, index_name: 'mongoid_fulltext.years'
9
+ fulltext_search_in :title, :year, :artist, index_name: 'mongoid_fulltext.all'
10
10
  end
@@ -3,5 +3,5 @@ class MultiFieldArtist
3
3
  include Mongoid::FullTextSearch
4
4
  field :full_name
5
5
  field :birth_year
6
- fulltext_search_in :full_name, :birth_year, :index_name => 'mongoid_fulltext.artworks_and_artists'
6
+ fulltext_search_in :full_name, :birth_year, index_name: 'mongoid_fulltext.artworks_and_artists'
7
7
  end
@@ -3,5 +3,5 @@ class MultiFieldArtwork
3
3
  include Mongoid::FullTextSearch
4
4
  field :title
5
5
  field :year
6
- fulltext_search_in :title, :year, :index_name => 'mongoid_fulltext.artworks_and_artists'
6
+ fulltext_search_in :title, :year, index_name: 'mongoid_fulltext.artworks_and_artists'
7
7
  end
@@ -1,15 +1,14 @@
1
1
  class PartitionedArtist
2
2
  include Mongoid::Document
3
3
  include Mongoid::FullTextSearch
4
-
4
+
5
5
  field :full_name
6
- field :exhibitions, :type => Array, :default => []
7
-
8
- fulltext_search_in :full_name,
9
- :index_name => 'mongoid_fulltext.partitioned_artists',
10
- :filters => {
11
- :has_exhibitions => lambda { |x| x.exhibitions.size > 0 },
12
- :exhibitions => lambda { |x| [ x.exhibitions ].flatten },
13
- }
6
+ field :exhibitions, type: Array, default: []
14
7
 
8
+ fulltext_search_in :full_name,
9
+ index_name: 'mongoid_fulltext.partitioned_artists',
10
+ filters: {
11
+ has_exhibitions: ->(x) { x.exhibitions.size > 0 },
12
+ exhibitions: ->(x) { [x.exhibitions].flatten }
13
+ }
15
14
  end
@@ -6,5 +6,5 @@ class RussianArtwork
6
6
  Alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789абвгдежзиклмнопрстуфхцчшщъыьэюя'
7
7
 
8
8
  field :title
9
- fulltext_search_in :title, :alphabet => Alphabet
10
- end
9
+ fulltext_search_in :title, alphabet: Alphabet
10
+ end
@@ -4,8 +4,7 @@ class ShortPrefixesArtwork
4
4
 
5
5
  field :title
6
6
  fulltext_search_in :title,
7
- :ngram_width => 4,
8
- :index_short_prefixes => true,
9
- :index_full_words => false
10
-
7
+ ngram_width: 4,
8
+ index_short_prefixes: true,
9
+ index_full_words: false
11
10
  end
@@ -3,8 +3,7 @@ class StopwordsArtwork
3
3
  include Mongoid::FullTextSearch
4
4
 
5
5
  field :title
6
- fulltext_search_in :title,
7
- :index_full_words => true,
8
- :stop_words => { 'and' => true, 'by' => true}
9
-
6
+ fulltext_search_in :title,
7
+ index_full_words: true,
8
+ stop_words: { 'and' => true, 'by' => true }
10
9
  end
@@ -0,0 +1,752 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Mongoid::FullTextSearch do
5
+ context 'with several config options defined' do
6
+ let!(:abcdef) { AdvancedArtwork.create(title: 'abcdefg hijklmn') }
7
+ let!(:cesar) { AccentlessArtwork.create(title: "C\u00e9sar Galicia") }
8
+ let!(:julio) { AccentlessArtwork.create(title: 'Julio Cesar Morales') }
9
+
10
+ it 'should recognize all options' do
11
+ # AdvancedArtwork is defined with an ngram_width of 4 and a different alphabet (abcdefg)
12
+ expect(AdvancedArtwork.fulltext_search('abc')).to eq([])
13
+ expect(AdvancedArtwork.fulltext_search('abcd').first).to eq(abcdef)
14
+ expect(AdvancedArtwork.fulltext_search('defg').first).to eq(abcdef)
15
+ expect(AdvancedArtwork.fulltext_search('hijklmn')).to eq([])
16
+ # AccentlessArtwork is just like BasicArtwork, except that we set :remove_accents to false,
17
+ # so this behaves like the ``old'' version of fulltext_search
18
+ expect(AccentlessArtwork.fulltext_search('cesar').first).to eq(julio)
19
+ end
20
+ end
21
+
22
+ context 'with default settings' do
23
+ let!(:flower_myth) { BasicArtwork.create(title: 'Flower Myth') }
24
+ let!(:flowers) { BasicArtwork.create(title: 'Flowers') }
25
+ let!(:lowered) { BasicArtwork.create(title: 'Lowered') }
26
+ let!(:cookies) { BasicArtwork.create(title: 'Cookies') }
27
+ let!(:empty) { BasicArtwork.create(title: '') }
28
+ let!(:cesar) { BasicArtwork.create(title: "C\u00e9sar Galicia") }
29
+ let!(:julio) { BasicArtwork.create(title: 'Julio Cesar Morales') }
30
+ let!(:csar) { BasicArtwork.create(title: 'Csar') }
31
+ let!(:percent) { BasicArtwork.create(title: 'Untitled (cal%desert)') }
32
+
33
+ it 'returns empty for empties' do
34
+ expect(BasicArtwork.fulltext_search(nil, max_results: 1)).to eq([])
35
+ expect(BasicArtwork.fulltext_search('', max_results: 1)).to eq([])
36
+ end
37
+
38
+ it 'finds percents' do
39
+ expect(BasicArtwork.fulltext_search('cal%desert'.force_encoding('ASCII-8BIT'), max_results: 1).first).to eq(percent)
40
+ expect(BasicArtwork.fulltext_search('cal%desert'.force_encoding('UTF-8'), max_results: 1).first).to eq(percent)
41
+ end
42
+
43
+ it 'forgets accents' do
44
+ expect(BasicArtwork.fulltext_search('cesar', max_results: 1).first).to eq(cesar)
45
+ expect(BasicArtwork.fulltext_search('cesar g', max_results: 1).first).to eq(cesar)
46
+ expect(BasicArtwork.fulltext_search("C\u00e9sar", max_results: 1).first).to eq(cesar)
47
+ expect(BasicArtwork.fulltext_search("C\303\251sar".force_encoding('UTF-8'), max_results: 1).first).to eq(cesar)
48
+ expect(BasicArtwork.fulltext_search(CGI.unescape('c%C3%A9sar'), max_results: 1).first).to eq(cesar)
49
+ expect(BasicArtwork.fulltext_search(CGI.unescape('c%C3%A9sar'.encode('ASCII-8BIT')), max_results: 1).first).to eq(cesar)
50
+ end
51
+
52
+ it 'returns exact matches' do
53
+ expect(BasicArtwork.fulltext_search('Flower Myth', max_results: 1).first).to eq(flower_myth)
54
+ expect(BasicArtwork.fulltext_search('Flowers', max_results: 1).first).to eq(flowers)
55
+ expect(BasicArtwork.fulltext_search('Cookies', max_results: 1).first).to eq(cookies)
56
+ expect(BasicArtwork.fulltext_search('Lowered', max_results: 1).first).to eq(lowered)
57
+ end
58
+
59
+ it 'returns exact matches regardless of case' do
60
+ expect(BasicArtwork.fulltext_search('fLOWER mYTH', max_results: 1).first).to eq(flower_myth)
61
+ expect(BasicArtwork.fulltext_search('FLOWERS', max_results: 1).first).to eq(flowers)
62
+ expect(BasicArtwork.fulltext_search('cOOkies', max_results: 1).first).to eq(cookies)
63
+ expect(BasicArtwork.fulltext_search('lOWERED', max_results: 1).first).to eq(lowered)
64
+ end
65
+
66
+ it 'returns all relevant results, sorted by relevance' do
67
+ expect(BasicArtwork.fulltext_search('Flowers')).to eq([flowers, flower_myth, lowered])
68
+ end
69
+
70
+ it 'prefers prefix matches' do
71
+ expect([flowers, flower_myth]).to include(BasicArtwork.fulltext_search('Floweockies').first)
72
+ expect(BasicArtwork.fulltext_search('Lowers').first).to eq(lowered)
73
+ expect(BasicArtwork.fulltext_search('Cookilowers').first).to eq(cookies)
74
+ end
75
+
76
+ it 'returns an empty result set for an empty query' do
77
+ expect(BasicArtwork.fulltext_search('').empty?).to be_truthy
78
+ end
79
+
80
+ it "returns an empty result set for a query that doesn't contain any characters in the alphabet" do
81
+ expect(BasicArtwork.fulltext_search('_+=--@!##%#$%%').empty?).to be_truthy
82
+ end
83
+
84
+ it 'returns results for a query that contains only a single ngram' do
85
+ expect(BasicArtwork.fulltext_search('coo').first).to eq(cookies)
86
+ expect(BasicArtwork.fulltext_search('c!!!oo').first).to eq(cookies)
87
+ end
88
+ end
89
+
90
+ context 'with default settings' do
91
+ let!(:flower_myth) { Gallery::BasicArtwork.create(title: 'Flower Myth') }
92
+ let!(:flowers) { Gallery::BasicArtwork.create(title: 'Flowers') }
93
+ let!(:lowered) { Gallery::BasicArtwork.create(title: 'Lowered') }
94
+ let!(:cookies) { Gallery::BasicArtwork.create(title: 'Cookies') }
95
+ let!(:empty) { Gallery::BasicArtwork.create(title: '') }
96
+
97
+ it 'returns exact matches for model within a module' do
98
+ expect(Gallery::BasicArtwork.fulltext_search('Flower Myth', max_results: 1).first).to eq(flower_myth)
99
+ expect(Gallery::BasicArtwork.fulltext_search('Flowers', max_results: 1).first).to eq(flowers)
100
+ expect(Gallery::BasicArtwork.fulltext_search('Cookies', max_results: 1).first).to eq(cookies)
101
+ expect(Gallery::BasicArtwork.fulltext_search('Lowered', max_results: 1).first).to eq(lowered)
102
+ end
103
+ end
104
+
105
+ context 'with default settings' do
106
+ let!(:yellow) { BasicArtwork.create(title: 'Yellow') }
107
+ let!(:yellow_leaves_2) { BasicArtwork.create(title: 'Yellow Leaves 2') }
108
+ let!(:yellow_leaves_3) { BasicArtwork.create(title: 'Yellow Leaves 3') }
109
+ let!(:yellow_leaves_20) { BasicArtwork.create(title: 'Yellow Leaves 20') }
110
+ let!(:yellow_cup) { BasicArtwork.create(title: 'Yellow Cup') }
111
+
112
+ it 'prefers the best prefix that matches a given string' do
113
+ expect(BasicArtwork.fulltext_search('yellow').first).to eq(yellow)
114
+ expect(BasicArtwork.fulltext_search('yellow leaves', max_results: 3).sort_by(&:title)).to eq( \
115
+ [yellow_leaves_2, yellow_leaves_3, yellow_leaves_20].sort_by(&:title)
116
+ )
117
+ expect(BasicArtwork.fulltext_search('yellow cup').first).to eq(yellow_cup)
118
+ end
119
+ end
120
+
121
+ context 'with default settings' do
122
+ let!(:monet) { BasicArtwork.create(title: 'claude monet') }
123
+ let!(:one_month_weather_permitting) { BasicArtwork.create(title: 'one month weather permitting monday') }
124
+
125
+ it 'finds better matches within exact strings' do
126
+ expect(BasicArtwork.fulltext_search('monet').first).to eq(monet)
127
+ end
128
+ end
129
+
130
+ context 'with default settings' do
131
+ let!(:abc) { BasicArtwork.create(title: 'abc') }
132
+ let!(:abcd) { BasicArtwork.create(title: 'abcd') }
133
+ let!(:abcde) { BasicArtwork.create(title: 'abcde') }
134
+ let!(:abcdef) { BasicArtwork.create(title: 'abcdef') }
135
+ let!(:abcdefg) { BasicArtwork.create(title: 'abcdefg') }
136
+ let!(:abcdefgh) { BasicArtwork.create(title: 'abcdefgh') }
137
+
138
+ it 'returns exact matches from a list of similar prefixes' do
139
+ expect(BasicArtwork.fulltext_search('abc').first).to eq(abc)
140
+ expect(BasicArtwork.fulltext_search('abcd').first).to eq(abcd)
141
+ expect(BasicArtwork.fulltext_search('abcde').first).to eq(abcde)
142
+ expect(BasicArtwork.fulltext_search('abcdef').first).to eq(abcdef)
143
+ expect(BasicArtwork.fulltext_search('abcdefg').first).to eq(abcdefg)
144
+ expect(BasicArtwork.fulltext_search('abcdefgh').first).to eq(abcdefgh)
145
+ end
146
+ end
147
+
148
+ context 'with an index name specified' do
149
+ let!(:pablo_picasso) { ExternalArtist.create(full_name: 'Pablo Picasso') }
150
+ let!(:portrait_of_picasso) { ExternalArtwork.create(title: 'Portrait of Picasso') }
151
+ let!(:andy_warhol) { ExternalArtist.create(full_name: 'Andy Warhol') }
152
+ let!(:warhol) { ExternalArtwork.create(title: 'Warhol') }
153
+ let!(:empty) { ExternalArtwork.create(title: '') }
154
+
155
+ it 'returns results of different types from the same query' do
156
+ results = ExternalArtwork.fulltext_search('picasso', max_results: 2).map { |result| result }
157
+ expect(results.member?(portrait_of_picasso)).to be_truthy
158
+ expect(results.member?(pablo_picasso)).to be_truthy
159
+ results = ExternalArtist.fulltext_search('picasso', max_results: 2).map { |result| result }
160
+ expect(results.member?(portrait_of_picasso)).to be_truthy
161
+ expect(results.member?(pablo_picasso)).to be_truthy
162
+ end
163
+
164
+ it 'returns exact matches' do
165
+ expect(ExternalArtwork.fulltext_search('Pablo Picasso', max_results: 1).first).to eq(pablo_picasso)
166
+ expect(ExternalArtwork.fulltext_search('Portrait of Picasso', max_results: 1).first).to eq(portrait_of_picasso)
167
+ expect(ExternalArtwork.fulltext_search('Andy Warhol', max_results: 1).first).to eq(andy_warhol)
168
+ expect(ExternalArtwork.fulltext_search('Warhol', max_results: 1).first).to eq(warhol)
169
+ expect(ExternalArtist.fulltext_search('Pablo Picasso', max_results: 1).first).to eq(pablo_picasso)
170
+ expect(ExternalArtist.fulltext_search('Portrait of Picasso', max_results: 1).first).to eq(portrait_of_picasso)
171
+ expect(ExternalArtist.fulltext_search('Andy Warhol', max_results: 1).first).to eq(andy_warhol)
172
+ expect(ExternalArtist.fulltext_search('Warhol', max_results: 1).first).to eq(warhol)
173
+ end
174
+
175
+ it 'returns exact matches regardless of case' do
176
+ expect(ExternalArtwork.fulltext_search('pABLO pICASSO', max_results: 1).first).to eq(pablo_picasso)
177
+ expect(ExternalArtist.fulltext_search('PORTRAIT OF PICASSO', max_results: 1).first).to eq(portrait_of_picasso)
178
+ expect(ExternalArtwork.fulltext_search('andy warhol', max_results: 1).first).to eq(andy_warhol)
179
+ expect(ExternalArtwork.fulltext_search('wArHoL', max_results: 1).first).to eq(warhol)
180
+ end
181
+
182
+ it 'returns all relevant results, sorted by relevance' do
183
+ expect(ExternalArtist.fulltext_search('Pablo Picasso')).to eq([pablo_picasso, portrait_of_picasso])
184
+ expect(ExternalArtwork.fulltext_search('Pablo Picasso')).to eq([pablo_picasso, portrait_of_picasso])
185
+ expect(ExternalArtist.fulltext_search('Portrait of Picasso')).to eq([portrait_of_picasso, pablo_picasso])
186
+ expect(ExternalArtwork.fulltext_search('Portrait of Picasso')).to eq([portrait_of_picasso, pablo_picasso])
187
+ expect(ExternalArtist.fulltext_search('Andy Warhol')).to eq([andy_warhol, warhol])
188
+ expect(ExternalArtwork.fulltext_search('Andy Warhol')).to eq([andy_warhol, warhol])
189
+ expect(ExternalArtist.fulltext_search('Warhol')).to eq([warhol, andy_warhol])
190
+ expect(ExternalArtwork.fulltext_search('Warhol')).to eq([warhol, andy_warhol])
191
+ end
192
+
193
+ it 'prefers prefix matches' do
194
+ expect(ExternalArtist.fulltext_search('PabloWarhol').first).to eq(pablo_picasso)
195
+ expect(ExternalArtist.fulltext_search('AndyPicasso').first).to eq(andy_warhol)
196
+ end
197
+
198
+ it 'returns an empty result set for an empty query' do
199
+ expect(ExternalArtist.fulltext_search('').empty?).to be_truthy
200
+ end
201
+
202
+ it "returns an empty result set for a query that doesn't contain any characters in the alphabet" do
203
+ expect(ExternalArtwork.fulltext_search('#$%!$#*%*').empty?).to be_truthy
204
+ end
205
+
206
+ it 'returns results for a query that contains only a single ngram' do
207
+ expect(ExternalArtist.fulltext_search('and').first).to eq(andy_warhol)
208
+ end
209
+ end
210
+
211
+ context 'with an index name specified' do
212
+ let!(:andy_warhol) { ExternalArtist.create(full_name: 'Andy Warhol') }
213
+ let!(:warhol) { ExternalArtwork.create(title: 'Warhol') }
214
+
215
+ it "doesn't blow up if garbage is in the index collection" do
216
+ expect(ExternalArtist.fulltext_search('warhol')).to eq([warhol, andy_warhol])
217
+ index_collection = ExternalArtist.collection.database[ExternalArtist.mongoid_fulltext_config.keys.first]
218
+ index_collection.find('document_id' => warhol.id).each do |idef|
219
+ if Mongoid::Compatibility::Version.mongoid3?
220
+ index_collection.find('_id' => idef['_id']).update('document_id' => Moped::BSON::ObjectId.new)
221
+ elsif Mongoid::Compatibility::Version.mongoid4?
222
+ index_collection.find('_id' => idef['_id']).update('document_id' => BSON::ObjectId.new)
223
+ else
224
+ index_collection.find('_id' => idef['_id']).update_one('document_id' => BSON::ObjectId.new)
225
+ end
226
+ end
227
+ # We should no longer be able to find warhol, but that shouldn't keep it from returning results
228
+ expect(ExternalArtist.fulltext_search('warhol')).to eq([andy_warhol])
229
+ end
230
+ end
231
+
232
+ context 'with an index name specified' do
233
+ let!(:pop) { ExternalArtwork.create(title: 'Pop') }
234
+ let!(:pop_culture) { ExternalArtwork.create(title: 'Pop Culture') }
235
+ let!(:contemporary_pop) { ExternalArtwork.create(title: 'Contemporary Pop') }
236
+ let!(:david_poppie) { ExternalArtist.create(full_name: 'David Poppie') }
237
+ let!(:kung_fu_lollipop) { ExternalArtwork.create(title: 'Kung-Fu Lollipop') }
238
+
239
+ it 'prefers the best prefix that matches a given string' do
240
+ expect(ExternalArtwork.fulltext_search('pop').first).to eq(pop)
241
+ expect(ExternalArtwork.fulltext_search('poppie').first).to eq(david_poppie)
242
+ expect(ExternalArtwork.fulltext_search('pop cult').first).to eq(pop_culture)
243
+ expect(ExternalArtwork.fulltext_search('pop', max_results: 5)[4]).to eq(kung_fu_lollipop)
244
+ end
245
+ end
246
+ context 'with an index name specified' do
247
+ let!(:abc) { ExternalArtwork.create(title: 'abc') }
248
+ let!(:abcd) { ExternalArtwork.create(title: 'abcd') }
249
+ let!(:abcde) { ExternalArtwork.create(title: 'abcde') }
250
+ let!(:abcdef) { ExternalArtwork.create(title: 'abcdef') }
251
+ let!(:abcdefg) { ExternalArtwork.create(title: 'abcdefg') }
252
+ let!(:abcdefgh) { ExternalArtwork.create(title: 'abcdefgh') }
253
+
254
+ it 'returns exact matches from a list of similar prefixes' do
255
+ expect(ExternalArtwork.fulltext_search('abc').first).to eq(abc)
256
+ expect(ExternalArtwork.fulltext_search('abcd').first).to eq(abcd)
257
+ expect(ExternalArtwork.fulltext_search('abcde').first).to eq(abcde)
258
+ expect(ExternalArtwork.fulltext_search('abcdef').first).to eq(abcdef)
259
+ expect(ExternalArtwork.fulltext_search('abcdefg').first).to eq(abcdefg)
260
+ expect(ExternalArtwork.fulltext_search('abcdefgh').first).to eq(abcdefgh)
261
+ end
262
+ end
263
+
264
+ context 'with an index name specified' do
265
+ it "cleans up item from the index after they're destroyed" do
266
+ foobar = ExternalArtwork.create(title: 'foobar')
267
+ barfoo = ExternalArtwork.create(title: 'barfoo')
268
+ expect(ExternalArtwork.fulltext_search('foobar')).to eq([foobar, barfoo])
269
+ foobar.destroy
270
+ expect(ExternalArtwork.fulltext_search('foobar')).to eq([barfoo])
271
+ barfoo.destroy
272
+ expect(ExternalArtwork.fulltext_search('foobar')).to eq([])
273
+ end
274
+ end
275
+
276
+ context 'with an index name specified and no fields provided to index' do
277
+ let!(:big_bang) { ExternalArtworkNoFieldsSupplied.create(title: 'Big Bang', artist: 'David Poppie', year: '2009') }
278
+
279
+ it 'indexes the string returned by to_s' do
280
+ expect(ExternalArtworkNoFieldsSupplied.fulltext_search('big bang').first).to eq(big_bang)
281
+ expect(ExternalArtworkNoFieldsSupplied.fulltext_search('poppie').first).to eq(big_bang)
282
+ expect(ExternalArtworkNoFieldsSupplied.fulltext_search('2009').first).to eq(big_bang)
283
+ end
284
+ end
285
+
286
+ context 'with multiple indexes defined' do
287
+ let!(:pop) { MultiExternalArtwork.create(title: 'Pop', year: '1970', artist: 'Joe Schmoe') }
288
+ let!(:pop_culture) { MultiExternalArtwork.create(title: 'Pop Culture', year: '1977', artist: 'Jim Schmoe') }
289
+ let!(:contemporary_pop) { MultiExternalArtwork.create(title: 'Contemporary Pop', year: '1800', artist: 'Bill Schmoe') }
290
+ let!(:kung_fu_lollipop) { MultiExternalArtwork.create(title: 'Kung-Fu Lollipop', year: '2006', artist: 'Michael Anderson') }
291
+
292
+ it 'allows searches to hit a particular index' do
293
+ title_results = MultiExternalArtwork.fulltext_search('pop', index: 'mongoid_fulltext.titles').sort_by(&:title)
294
+ expect(title_results).to eq([pop, pop_culture, contemporary_pop, kung_fu_lollipop].sort_by(&:title))
295
+ year_results = MultiExternalArtwork.fulltext_search('197', index: 'mongoid_fulltext.years').sort_by(&:title)
296
+ expect(year_results).to eq([pop, pop_culture].sort_by(&:title))
297
+ all_results = MultiExternalArtwork.fulltext_search('1800 and', index: 'mongoid_fulltext.all').sort_by(&:title)
298
+ expect(all_results).to eq([contemporary_pop, kung_fu_lollipop].sort_by(&:title))
299
+ end
300
+
301
+ it "should raise an error if you don't specify which index to search with" do
302
+ expect { MultiExternalArtwork.fulltext_search('foobar') }.to raise_error(Mongoid::FullTextSearch::UnspecifiedIndexError)
303
+ end
304
+ end
305
+
306
+ context 'with multiple fields indexed and the same index used by multiple models' do
307
+ let!(:andy_warhol) { MultiFieldArtist.create(full_name: 'Andy Warhol', birth_year: '1928') }
308
+ let!(:warhol) { MultiFieldArtwork.create(title: 'Warhol', year: '2010') }
309
+ let!(:pablo_picasso) { MultiFieldArtist.create(full_name: 'Pablo Picasso', birth_year: '1881') }
310
+ let!(:portrait_of_picasso) { MultiFieldArtwork.create(title: 'Portrait of Picasso', year: '1912') }
311
+
312
+ it 'allows searches across all models on both fields indexed' do
313
+ expect(MultiFieldArtist.fulltext_search('2010').first).to eq(warhol)
314
+ expect(MultiFieldArtist.fulltext_search('andy').first).to eq(andy_warhol)
315
+ expect(MultiFieldArtist.fulltext_search('pablo').first).to eq(pablo_picasso)
316
+ expect(MultiFieldArtist.fulltext_search('1881').first).to eq(pablo_picasso)
317
+ expect(MultiFieldArtist.fulltext_search('portrait 1912').first).to eq(portrait_of_picasso)
318
+
319
+ expect(MultiFieldArtwork.fulltext_search('2010').first).to eq(warhol)
320
+ expect(MultiFieldArtwork.fulltext_search('andy').first).to eq(andy_warhol)
321
+ expect(MultiFieldArtwork.fulltext_search('pablo').first).to eq(pablo_picasso)
322
+ expect(MultiFieldArtwork.fulltext_search('1881').first).to eq(pablo_picasso)
323
+ expect(MultiFieldArtwork.fulltext_search('portrait 1912').first).to eq(portrait_of_picasso)
324
+ end
325
+ end
326
+ context 'with filters applied to multiple models' do
327
+ let!(:foobar_artwork) { FilteredArtwork.create(title: 'foobar') }
328
+ let!(:barfoo_artwork) { FilteredArtwork.create(title: 'barfoo') }
329
+ let!(:foobar_artist) { FilteredArtist.create(full_name: 'foobar') }
330
+ let!(:barfoo_artist) { FilteredArtist.create(full_name: 'barfoo') }
331
+
332
+ it 'allows filtered searches' do
333
+ expect(FilteredArtwork.fulltext_search('foobar', is_artwork: true)).to eq([foobar_artwork, barfoo_artwork])
334
+ expect(FilteredArtist.fulltext_search('foobar', is_artwork: true)).to eq([foobar_artwork, barfoo_artwork])
335
+
336
+ expect(FilteredArtwork.fulltext_search('foobar', is_artwork: true, is_foobar: true)).to eq([foobar_artwork])
337
+ expect(FilteredArtwork.fulltext_search('foobar', is_artwork: true, is_foobar: false)).to eq([barfoo_artwork])
338
+ expect(FilteredArtwork.fulltext_search('foobar', is_artwork: false, is_foobar: true)).to eq([foobar_artist])
339
+ expect(FilteredArtwork.fulltext_search('foobar', is_artwork: false, is_foobar: false)).to eq([barfoo_artist])
340
+
341
+ expect(FilteredArtist.fulltext_search('foobar', is_artwork: true, is_foobar: true)).to eq([foobar_artwork])
342
+ expect(FilteredArtist.fulltext_search('foobar', is_artwork: true, is_foobar: false)).to eq([barfoo_artwork])
343
+ expect(FilteredArtist.fulltext_search('foobar', is_artwork: false, is_foobar: true)).to eq([foobar_artist])
344
+ expect(FilteredArtist.fulltext_search('foobar', is_artwork: false, is_foobar: false)).to eq([barfoo_artist])
345
+ end
346
+ end
347
+
348
+ context 'with different filters applied to multiple models' do
349
+ let!(:foo_artwork) { FilteredArtwork.create(title: 'foo') }
350
+ let!(:bar_artist) { FilteredArtist.create(full_name: 'bar') }
351
+ let!(:baz_other) { FilteredOther.create(name: 'baz') }
352
+
353
+ # These three models are all indexed by the same mongoid_fulltext index, but have different filters
354
+ # applied. The index created on the mongoid_fulltext collection should include the ngram and score
355
+ # fields as well as the union of all the filter fields to allow for efficient lookups.
356
+
357
+ it 'creates a proper index for searching efficiently' do
358
+ [FilteredArtwork, FilteredArtist, FilteredOther].each(&:create_indexes)
359
+ index_collection = FilteredArtwork.collection.database['mongoid_fulltext.artworks_and_artists']
360
+ ngram_indexes = []
361
+ index_collection.indexes.each { |idef| ngram_indexes << idef if idef['key'].key?('ngram') }
362
+ expect(ngram_indexes.length).to eq(1)
363
+ keys = ngram_indexes.first['key'].keys
364
+ expected_keys = ['ngram', 'score', 'filter_values.is_fuzzy', 'filter_values.is_awesome',
365
+ 'filter_values.is_foobar', 'filter_values.is_artwork', 'filter_values.is_artist', 'filter_values.colors?'].sort
366
+ expect(keys.sort).to eq(expected_keys)
367
+ end
368
+ end
369
+
370
+ context 'with partitions applied to a model' do
371
+ let!(:artist_2) { PartitionedArtist.create(full_name: 'foobar', exhibitions: ['Art Basel 2011', 'Armory NY']) }
372
+ let!(:artist_1) { PartitionedArtist.create(full_name: 'foobar', exhibitions: ['Art Basel 2011']) }
373
+ let!(:artist_0) { PartitionedArtist.create(full_name: 'foobar', exhibitions: []) }
374
+
375
+ it 'allows partitioned searches' do
376
+ artists_by_exhibition_length = [artist_0, artist_1, artist_2].sort_by { |x| x.exhibitions.length }
377
+ expect(PartitionedArtist.fulltext_search('foobar').sort_by { |x| x.exhibitions.length }).to eq(artists_by_exhibition_length)
378
+ expect(PartitionedArtist.fulltext_search('foobar', exhibitions: ['Armory NY'])).to eq([artist_2])
379
+ art_basel_only = PartitionedArtist.fulltext_search('foobar', exhibitions: ['Art Basel 2011']).sort_by { |x| x.exhibitions.length }
380
+ expect(art_basel_only).to eq([artist_1, artist_2].sort_by { |x| x.exhibitions.length })
381
+ expect(PartitionedArtist.fulltext_search('foobar', exhibitions: ['Art Basel 2011', 'Armory NY'])).to eq([artist_2])
382
+ end
383
+ end
384
+
385
+ context 'using search options' do
386
+ let!(:patterns) { BasicArtwork.create(title: 'Flower Patterns') }
387
+ let!(:flowers) { BasicArtwork.create(title: 'Flowers') }
388
+
389
+ it 'returns max_results' do
390
+ expect(BasicArtwork.fulltext_search('flower', max_results: 1).length).to eq(1)
391
+ end
392
+
393
+ it 'returns scored results' do
394
+ results = BasicArtwork.fulltext_search('flowers', return_scores: true)
395
+ first_result = results[0]
396
+ expect(first_result.is_a?(Array)).to be_truthy
397
+ expect(first_result.size).to eq(2)
398
+ expect(first_result[0]).to eq(flowers)
399
+ expect(first_result[1].is_a?(Float)).to be_truthy
400
+ end
401
+ end
402
+
403
+ context 'with various word separators' do
404
+ let!(:hard_edged_painting) { BasicArtwork.create(title: 'Hard-edged painting') }
405
+ let!(:edgy_painting) { BasicArtwork.create(title: 'Edgy painting') }
406
+ let!(:hard_to_find_ledge) { BasicArtwork.create(title: 'Hard to find ledge') }
407
+
408
+ it 'should treat dashes as word separators, giving a score boost to each dash-separated word' do
409
+ expect(BasicArtwork.fulltext_search('hard-edged').first).to eq(hard_edged_painting)
410
+ expect(BasicArtwork.fulltext_search('hard edge').first).to eq(hard_edged_painting)
411
+ expect(BasicArtwork.fulltext_search('hard edged').first).to eq(hard_edged_painting)
412
+ end
413
+ end
414
+
415
+ context 'returning scores' do
416
+ # Since we return scores, let's make some weak guarantees about what they actually mean
417
+
418
+ let!(:mao_yan) { ExternalArtist.create(full_name: 'Mao Yan') }
419
+ let!(:mao) { ExternalArtwork.create(title: 'Mao by Andy Warhol') }
420
+ let!(:maox) { ExternalArtwork.create(title: 'Maox by Randy Morehall') }
421
+ let!(:somao) { ExternalArtwork.create(title: 'Somao by Randy Morehall') }
422
+
423
+ it "returns basic matches that don't match a whole word and aren't prefixes with score < 1" do
424
+ %w(paox porehall).each do |query|
425
+ results = ExternalArtist.fulltext_search(query, return_scores: true)
426
+ expect(results.length).to be > 0
427
+ expect(results.map { |result| result[-1] }.inject(true) { |accum, item| accum &= (item < 1) }).to be_truthy
428
+ end
429
+ end
430
+
431
+ it 'returns prefix matches with a score >= 1 but < 2' do
432
+ %w(warho rand).each do |query|
433
+ results = ExternalArtist.fulltext_search(query, return_scores: true)
434
+ expect(results.length).to be > 0
435
+ expect(results.map { |result| result[-1] if result[0].to_s.starts_with?(query) }.compact.inject(true) { |accum, item| accum &= (item >= 1 && item < 2) }).to be_truthy
436
+ end
437
+ end
438
+
439
+ it 'returns full-word matches with a score >= 2' do
440
+ %w(andy warhol mao).each do |query|
441
+ results = ExternalArtist.fulltext_search(query, return_scores: true)
442
+ expect(results.length).to be > 0
443
+ expect(results.map { |result| result[-1] if result[0].to_s.split(' ').member?(query) }.compact.inject(true) { |accum, item| accum &= (item >= 2) }).to be_truthy
444
+ end
445
+ end
446
+ end
447
+
448
+ context 'with stop words defined' do
449
+ let!(:flowers) { StopwordsArtwork.create(title: 'Flowers by Andy Warhol') }
450
+ let!(:many_ands) { StopwordsArtwork.create(title: 'Foo and bar and baz and foobar') }
451
+ let!(:harry) { StopwordsArtwork.create(title: 'Harry in repose by JK Rowling') }
452
+
453
+ it "doesn't give a full-word score boost to stopwords" do
454
+ expect(StopwordsArtwork.fulltext_search('andy').map(&:title)).to eq([flowers.title, many_ands.title])
455
+ expect(StopwordsArtwork.fulltext_search('warhol and other stuff').map(&:title)).to eq([flowers.title, many_ands.title])
456
+ end
457
+
458
+ it 'allows searching on words that are more than one letter, less than the ngram length and not stopwords' do
459
+ expect(StopwordsArtwork.fulltext_search('jk').map(&:title)).to eq([harry.title])
460
+ expect(StopwordsArtwork.fulltext_search('by').map(&:title)).to eq([])
461
+ end
462
+ end
463
+
464
+ context 'indexing short prefixes' do
465
+ let!(:dimethyl_mercury) { ShortPrefixesArtwork.create(title: 'Dimethyl Mercury by Damien Hirst') }
466
+ let!(:volume) { ShortPrefixesArtwork.create(title: 'Volume by Dadamaino') }
467
+ let!(:damaged) { ShortPrefixesArtwork.create(title: 'Damaged: Photographs from the Chicago Daily News 1902-1933 (Governor) by Lisa Oppenheim') }
468
+ let!(:frozen) { ShortPrefixesArtwork.create(title: 'Frozen Fountain XXX by Evelyn Rosenberg') }
469
+ let!(:skull) { ShortPrefixesArtwork.create(title: 'Skull by Andy Warhol') }
470
+
471
+ it 'finds the most relevant items with prefix indexing' do
472
+ expect(ShortPrefixesArtwork.fulltext_search('damien').first).to eq(dimethyl_mercury)
473
+ expect(ShortPrefixesArtwork.fulltext_search('dami').first).to eq(dimethyl_mercury)
474
+ expect(ShortPrefixesArtwork.fulltext_search('dama').first).to eq(damaged)
475
+ expect(ShortPrefixesArtwork.fulltext_search('dam').first).not_to eq(volume)
476
+ expect(ShortPrefixesArtwork.fulltext_search('dadamaino').first).to eq(volume)
477
+ expect(ShortPrefixesArtwork.fulltext_search('kull').first).to eq(skull)
478
+ end
479
+
480
+ it "doesn't index prefixes of stopwords" do
481
+ # damaged has the word "from" in it, which shouldn't get indexed.
482
+ expect(ShortPrefixesArtwork.fulltext_search('fro')).to eq([frozen])
483
+ end
484
+
485
+ it 'does index prefixes that would be stopwords taken alone' do
486
+ # skull has the word "andy" in it, which should get indexed as "and" even though "and" is a stopword
487
+ expect(ShortPrefixesArtwork.fulltext_search('and')).to eq([skull])
488
+ end
489
+ end
490
+
491
+ context 'remove_from_ngram_index' do
492
+ let!(:flowers1) { BasicArtwork.create(title: 'Flowers 1') }
493
+ let!(:flowers2) { BasicArtwork.create(title: 'Flowers 1') }
494
+
495
+ it 'removes all records from the index' do
496
+ BasicArtwork.remove_from_ngram_index
497
+ expect(BasicArtwork.fulltext_search('flower').length).to eq(0)
498
+ end
499
+
500
+ it 'removes a single record from the index' do
501
+ flowers1.remove_from_ngram_index
502
+ expect(BasicArtwork.fulltext_search('flower').length).to eq(1)
503
+ end
504
+ end
505
+
506
+ context 'update_ngram_index' do
507
+ let!(:flowers1) { BasicArtwork.create(title: 'Flowers 1') }
508
+ let!(:flowers2) { BasicArtwork.create(title: 'Flowers 2') }
509
+
510
+ context 'when config[:update_if] exists' do
511
+ let(:painting) { BasicArtwork.new title: 'Painting' }
512
+ let(:conditional_index) { BasicArtwork.mongoid_fulltext_config['mongoid_fulltext.index_conditional'] }
513
+
514
+ before(:each) do
515
+ BasicArtwork.class_eval do
516
+ fulltext_search_in :title, index_name: 'mongoid_fulltext.index_conditional'
517
+ end
518
+ end
519
+
520
+ after(:all) do
521
+ # Moped 1.0.0rc raises an error when removing a collection that does not exist
522
+ # Will be fixed soon.
523
+ begin
524
+ Mongoid.default_session['mongoid_fulltext.index_conditional'].drop
525
+ rescue Moped::Errors::OperationFailure => e
526
+ end
527
+ BasicArtwork.mongoid_fulltext_config.delete 'mongoid_fulltext.index_conditional'
528
+ end
529
+
530
+ context 'and is a symbol' do
531
+ before(:each) do
532
+ conditional_index[:update_if] = :persisted?
533
+ end
534
+
535
+ context 'when sending the symbol to the document evaluates to false' do
536
+ it "doesn't update the index for the document" do
537
+ painting.update_ngram_index
538
+ expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
539
+ end
540
+ end
541
+ end
542
+
543
+ context 'and is a string' do
544
+ before(:each) do
545
+ conditional_index[:update_if] = 'false'
546
+ end
547
+
548
+ context "when evaluating the string within the document's instance evaluates to false" do
549
+ it "doesn't update the index for the document" do
550
+ painting.update_ngram_index
551
+ expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
552
+ end
553
+ end
554
+ end
555
+
556
+ context 'and is a proc' do
557
+ before(:each) do
558
+ conditional_index[:update_if] = proc { false }
559
+ end
560
+
561
+ context "when evaluating the string within the document's instance evaluates to false" do
562
+ it "doesn't update the index for the document" do
563
+ painting.update_ngram_index
564
+ expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
565
+ end
566
+ end
567
+ end
568
+
569
+ context 'and is not a symbol, string, or proc' do
570
+ before(:each) do
571
+ conditional_index[:update_if] = %w(this isn't a symbol, string, or proc)
572
+ end
573
+
574
+ it "doesn't update the index for the document" do
575
+ painting.update_ngram_index
576
+ expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
577
+ end
578
+ end
579
+ end
580
+
581
+ context 'from scratch' do
582
+ before(:each) do
583
+ Mongoid.default_session['mongoid_fulltext.index_basicartwork_0'].drop
584
+ end
585
+
586
+ it 'updates index on a single record' do
587
+ flowers1.update_ngram_index
588
+ expect(BasicArtwork.fulltext_search('flower').length).to eq(1)
589
+ end
590
+
591
+ it 'updates index on all records' do
592
+ BasicArtwork.update_ngram_index
593
+ expect(BasicArtwork.fulltext_search('flower').length).to eq(2)
594
+ end
595
+ end
596
+
597
+ context 'incremental' do
598
+ it 'removes an existing record' do
599
+ coll = Mongoid.default_session['mongoid_fulltext.index_basicartwork_0']
600
+ if Mongoid::Compatibility::Version.mongoid5?
601
+ coll.find('document_id' => flowers1._id).delete_many
602
+ else
603
+ coll.find('document_id' => flowers1._id).remove_all
604
+ end
605
+ expect(coll.find('document_id' => flowers1._id).first).to be nil
606
+ flowers1.update_ngram_index
607
+ end
608
+ end
609
+
610
+ context 'mongoid indexes' do
611
+ it 'can re-create dropped indexes' do
612
+ # there're no indexes by default as Mongoid.autocreate_indexes is set to false
613
+ # but mongo will automatically attempt to index _id in the background
614
+ expect(Mongoid.default_session['mongoid_fulltext.index_basicartwork_0'].indexes.count).to be <= 1
615
+ BasicArtwork.create_indexes
616
+ expected_indexes = %w(_id_ fts_index document_id_1).sort
617
+ current_indexes = []
618
+ Mongoid.default_session['mongoid_fulltext.index_basicartwork_0'].indexes.each do |idef|
619
+ current_indexes << idef['name']
620
+ end
621
+ expect(current_indexes.sort).to eq(expected_indexes)
622
+ end
623
+
624
+ it "doesn't fail on models that don't have a fulltext index" do
625
+ expect { HiddenDragon.create_indexes }.not_to raise_error
626
+ end
627
+
628
+ it "doesn't blow up when the Mongoid.logger is set to false" do
629
+ Mongoid.logger = false
630
+ BasicArtwork.create_indexes
631
+ end
632
+ end
633
+ end
634
+
635
+ context 'batched reindexing' do
636
+ let!(:flowers1) { DelayedArtwork.create(title: 'Flowers 1') }
637
+
638
+ it 'should not rebuild index until explicitly invoked' do
639
+ expect(DelayedArtwork.fulltext_search('flowers').length).to eq(0)
640
+ DelayedArtwork.update_ngram_index
641
+ expect(DelayedArtwork.fulltext_search('flowers').length).to eq(1)
642
+ end
643
+ end
644
+
645
+ # For =~ operator documentation
646
+ # https://github.com/dchelimsky/rspec/blob/master/lib/spec/matchers/match_array.rb#L53
647
+
648
+ context 'with artwork that returns an array of colors as a filter' do
649
+ let!(:title) { 'title' }
650
+ let!(:nomatch) { 'nomatch' }
651
+ let!(:red) { 'red' }
652
+ let!(:green) { 'green' }
653
+ let!(:blue) { 'blue' }
654
+ let!(:yellow) { 'yellow' }
655
+ let!(:brown) { 'brown' }
656
+
657
+ let!(:rgb_artwork) { FilteredArtwork.create(title: "#{title} rgb", colors: [red, green, blue]) }
658
+ let!(:holiday_artwork) { FilteredArtwork.create(title: "#{title} holiday", colors: [red, green]) }
659
+ let!(:aqua_artwork) { FilteredArtwork.create(title: "#{title} aqua", colors: [green, blue]) }
660
+
661
+ context 'with a fulltext search passing red, green, and blue to the colors filter' do
662
+ it 'should return the rgb artwork' do
663
+ expect(FilteredArtwork.fulltext_search(title, colors?: [red, green, blue])).to eq([rgb_artwork])
664
+ end
665
+ end
666
+
667
+ context 'with a fulltext search passing blue and red to the colors filter' do
668
+ it 'should return the rgb artwork' do
669
+ expect(FilteredArtwork.fulltext_search(title, colors?: [blue, red])).to eq([rgb_artwork])
670
+ end
671
+ end
672
+
673
+ context 'with a fulltext search passing green to the colors filter' do
674
+ it 'should return all artwork' do
675
+ expect(FilteredArtwork.fulltext_search(title, colors?: [green])).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
676
+ end
677
+ end
678
+
679
+ context 'with a fulltext search passing no colors to the filter' do
680
+ it 'should return all artwork' do
681
+ expect(FilteredArtwork.fulltext_search(title)).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
682
+ end
683
+ end
684
+
685
+ context 'with a fulltext search passing green and yellow to the colors filter' do
686
+ it 'should return no artwork' do
687
+ expect(FilteredArtwork.fulltext_search(title, colors?: [green, yellow])).to eq([])
688
+ end
689
+ end
690
+
691
+ context 'with the query operator overridden to use $in instead of the default $all' do
692
+ context 'with a fulltext search passing green and yellow to the colors filter' do
693
+ it 'should return all of the artwork' do
694
+ expect(FilteredArtwork.fulltext_search(title, colors?: { any: [green, yellow] })).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
695
+ end
696
+ end
697
+
698
+ context 'with a fulltext search passing brown and yellow to the colors filter' do
699
+ it 'should return none of the artwork' do
700
+ expect(FilteredArtwork.fulltext_search(title, colors?: { any: [brown, yellow] })).to eq([])
701
+ end
702
+ end
703
+
704
+ context 'with a fulltext search passing blue to the colors filter' do
705
+ it 'should return the rgb and aqua artwork' do
706
+ expect(FilteredArtwork.fulltext_search(title, colors?: { any: [blue] })).to eq([rgb_artwork, aqua_artwork])
707
+ end
708
+ end
709
+
710
+ context "with a fulltext search term that won't match" do
711
+ it 'should return none of the artwork' do
712
+ expect(FilteredArtwork.fulltext_search(nomatch, colors?: { any: [green, yellow] })).to eq([])
713
+ end
714
+ end
715
+ end
716
+
717
+ context 'with the query operator overridden to use $all' do
718
+ context 'with a fulltext search passing red, green, and blue to the colors filter' do
719
+ it 'should return the rgb artwork' do
720
+ expect(FilteredArtwork.fulltext_search(title, colors?: { all: [red, green, blue] })).to eq([rgb_artwork])
721
+ end
722
+ end
723
+
724
+ context 'with a fulltext search passing green to the colors filter' do
725
+ it 'should return all artwork' do
726
+ expect(FilteredArtwork.fulltext_search(title, colors?: { all: [green] })).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
727
+ end
728
+ end
729
+ end
730
+
731
+ context 'with an unknown query operator used to override the default $all' do
732
+ context 'with a fulltext search passing red, green, and blue to the colors filter' do
733
+ it 'should raise an error' do
734
+ expect do
735
+ FilteredArtwork.fulltext_search(title, colors?: { unknown: [red, green, blue] })
736
+ end.to raise_error(Mongoid::FullTextSearch::UnknownFilterQueryOperator)
737
+ end
738
+ end
739
+ end
740
+
741
+ context 'should properly work with non-latin strings (i.e. cyrillic)' do
742
+ let!(:morning) { RussianArtwork.create(title: 'Утро в сосновом лесу Шишкин Morning in a Pine Forest Shishkin') }
743
+
744
+ it 'should find a match if query is non-latin string' do
745
+ # RussianArtwork is just like BasicArtwork, except that we set :alphabet to
746
+ # 'abcdefghijklmnopqrstuvwxyz0123456789абвгдежзиклмнопрстуфхцчшщъыьэюя'
747
+ expect(RussianArtwork.fulltext_search('shishkin').first).to eq(morning)
748
+ expect(RussianArtwork.fulltext_search('шишкин').first).to eq(morning)
749
+ end
750
+ end
751
+ end
752
+ end