mongoid_fulltext 0.6.1 → 0.7.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.
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