mongoid_fulltext 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|