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.
- checksums.yaml +15 -0
- data/.gitignore +47 -0
- data/.rspec +1 -1
- data/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +101 -0
- data/.travis.yml +11 -3
- data/CHANGELOG.md +9 -2
- data/Gemfile +19 -9
- data/LICENSE +1 -1
- data/README.md +12 -9
- data/Rakefile +9 -29
- data/lib/mongoid/full_text_search/version.rb +5 -0
- data/lib/mongoid/full_text_search.rb +372 -0
- data/lib/mongoid/indexable.rb +13 -0
- data/lib/mongoid/indexes.rb +13 -0
- data/lib/mongoid_fulltext.rb +1 -341
- data/mongoid_fulltext.gemspec +16 -82
- data/spec/models/accentless_artwork.rb +1 -1
- data/spec/models/advanced_artwork.rb +1 -1
- data/spec/models/basic_artwork.rb +0 -1
- data/spec/models/delayed_artwork.rb +1 -2
- data/spec/models/external_artist.rb +1 -2
- data/spec/models/external_artwork.rb +1 -2
- data/spec/models/external_artwork_no_fields_supplied.rb +2 -2
- data/spec/models/filtered_artist.rb +4 -4
- data/spec/models/filtered_artwork.rb +7 -7
- data/spec/models/filtered_other.rb +3 -3
- data/spec/models/hidden_dragon.rb +0 -1
- data/spec/models/multi_external_artwork.rb +3 -3
- data/spec/models/multi_field_artist.rb +1 -1
- data/spec/models/multi_field_artwork.rb +1 -1
- data/spec/models/partitioned_artist.rb +8 -9
- data/spec/models/russian_artwork.rb +2 -2
- data/spec/models/short_prefixes_artwork.rb +3 -4
- data/spec/models/stopwords_artwork.rb +3 -4
- data/spec/mongoid/full_text_search_spec.rb +752 -0
- data/spec/spec_helper.rb +11 -7
- metadata +27 -68
- data/VERSION +0 -1
- data/lib/mongoid_indexes.rb +0 -12
- data/spec/config/mongoid.yml +0 -6
- data/spec/mongoid/fulltext_spec.rb +0 -799
@@ -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, :
|
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, :
|
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 :
|
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, :
|
6
|
-
|
7
|
-
|
8
|
-
|
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, :
|
5
|
-
field :colors, :
|
6
|
-
fulltext_search_in :title, :
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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, :
|
8
|
-
|
9
|
-
|
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
|
@@ -4,7 +4,7 @@ class MultiExternalArtwork
|
|
4
4
|
field :title
|
5
5
|
field :year
|
6
6
|
field :artist
|
7
|
-
fulltext_search_in :title, :
|
8
|
-
fulltext_search_in :year, :
|
9
|
-
fulltext_search_in :title, :year, :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'
|
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, :
|
6
|
+
fulltext_search_in :full_name, :birth_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, :
|
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
|
@@ -3,8 +3,7 @@ class StopwordsArtwork
|
|
3
3
|
include Mongoid::FullTextSearch
|
4
4
|
|
5
5
|
field :title
|
6
|
-
fulltext_search_in :title,
|
7
|
-
|
8
|
-
|
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
|