mongoid-fts 1.1.1 → 2.0.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 +8 -8
- data/README.md +33 -16
- data/lib/mongoid-fts.rb +97 -642
- data/lib/mongoid-fts/able.rb +68 -0
- data/lib/mongoid-fts/error.rb +5 -0
- data/lib/mongoid-fts/index.rb +293 -0
- data/lib/mongoid-fts/rails.rb +11 -0
- data/lib/mongoid-fts/raw.rb +16 -0
- data/lib/mongoid-fts/results.rb +195 -0
- data/lib/mongoid-fts/stemming.rb +81 -0
- data/lib/mongoid-fts/stemming/stopwords/english.txt +32 -0
- data/lib/mongoid-fts/stemming/stopwords/extended_english.txt +216 -0
- data/lib/mongoid-fts/stemming/stopwords/full_danish.txt +94 -0
- data/lib/mongoid-fts/stemming/stopwords/full_dutch.txt +101 -0
- data/lib/mongoid-fts/stemming/stopwords/full_english.txt +174 -0
- data/lib/mongoid-fts/stemming/stopwords/full_finnish.txt +0 -0
- data/lib/mongoid-fts/stemming/stopwords/full_french.txt +155 -0
- data/lib/mongoid-fts/stemming/stopwords/full_german.txt +231 -0
- data/lib/mongoid-fts/stemming/stopwords/full_italian.txt +279 -0
- data/lib/mongoid-fts/stemming/stopwords/full_norwegian.txt +176 -0
- data/lib/mongoid-fts/stemming/stopwords/full_portuguese.txt +203 -0
- data/lib/mongoid-fts/stemming/stopwords/full_russian.txt +101 -0
- data/lib/mongoid-fts/stemming/stopwords/full_russiankoi8_r.txt +101 -0
- data/lib/mongoid-fts/stemming/stopwords/full_spanish.txt +313 -0
- data/lib/mongoid-fts/util.rb +322 -0
- data/mongoid-fts.gemspec +37 -3
- data/test/helper.rb +44 -0
- data/test/mongoid-fts_test.rb +177 -0
- data/test/testing.rb +196 -0
- metadata +69 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZmVkZjQ4M2YxMjBlOTY2YWQ2NTFkMDc1OWMyNTQwMDNhNWJlNjMwYw==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
M2YwMTExNjM1ODU3OTNjN2ZkY2VhODBlZWMwNjM3YWMyOTQyYTA1MA==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZGUwMDc0NjYzZDU4ZGE5NThmNDBhMmU2ZmZkNDRhMmI4N2I1MzA3ZTc4ODc5
|
10
|
+
OTZmMTkxYzk0MzM1NTdkNzJiOTk4MjljZWM3MjViYjliNGEwODQ3NTFlY2E1
|
11
|
+
NmRjYjcwZWJkMjIzMzkwYmIxMjA4NDk5ZGUyNjEzODU0M2NjMTU=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MzViODY3NzE3ZGM4OThhNjFhZmE5MzVkMjljMGQyZTc4ZWE4ZDYzOWEzNjg1
|
14
|
+
ZWRiZWIwNTZjNTQ3MzNlYzRiODM5NzNmZmUwMDE5YzRmYTk4NmU5MDZjY2M5
|
15
|
+
MWRlYjdkZGI0ZDE2NDMxMDBkNzc2NzA0Y2U3ZmE2MTQ3MTY5NjY=
|
data/README.md
CHANGED
@@ -1,20 +1,32 @@
|
|
1
1
|
NAME
|
2
2
|
------------------
|
3
3
|
|
4
|
-
|
4
|
+
mongoid-fts.rb
|
5
5
|
|
6
6
|
DESCRIPTION
|
7
7
|
------------------
|
8
8
|
|
9
9
|
enable mongodb's new fulltext simply and quickly on your mongoid models.
|
10
10
|
|
11
|
-
|
11
|
+
mongodb's built-in fulltext is handy, but has several warts which make it
|
12
|
+
difficult to use in real projects:
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
- queries are boolean OR by default, meaning a user typing more and more
|
15
|
+
specific search queries gets back less and less specific search results
|
16
|
+
- lack of limit+offset on queries, making pagination a beast
|
17
|
+
- lack of substring/fuzzy search
|
18
|
+
- lack of literal search (find by id, uuid, etc. without hitting stemming/stopword functionality)
|
19
|
+
- ability to search across collections
|
20
|
+
|
21
|
+
mongoid-fts smoothes over these warts, and more, for your mongoid 3 and 4
|
22
|
+
projects supporting:
|
23
|
+
|
24
|
+
- pagination
|
25
|
+
- strict literal searching (including stopwords)
|
26
|
+
- cross models searching
|
27
|
+
- index is automatically kept in sync
|
28
|
+
- customize ranking with #to_search
|
29
|
+
- fuzzy search (title and keywords included by default)
|
18
30
|
|
19
31
|
INSTALL
|
20
32
|
------------------
|
@@ -51,7 +63,7 @@ SYNOPSIS
|
|
51
63
|
#
|
52
64
|
class A
|
53
65
|
include Mongoid::Document
|
54
|
-
include Mongoid::FTS
|
66
|
+
include Mongoid::FTS::Able
|
55
67
|
|
56
68
|
field(:title)
|
57
69
|
field(:keywords, :type => Array)
|
@@ -63,14 +75,20 @@ SYNOPSIS
|
|
63
75
|
|
64
76
|
class B
|
65
77
|
include Mongoid::Document
|
66
|
-
include Mongoid::FTS
|
78
|
+
include Mongoid::FTS::Able
|
67
79
|
|
68
80
|
field(:a)
|
69
81
|
field(:b, :type => Array, :default => [])
|
70
82
|
field(:c)
|
71
83
|
|
72
84
|
def to_search
|
73
|
-
{
|
85
|
+
{
|
86
|
+
:literals => [id, sku],
|
87
|
+
:title => a,
|
88
|
+
:keywords => (b + ['foobar']),
|
89
|
+
:fuzzy => [a, b],
|
90
|
+
:fulltext => c
|
91
|
+
}
|
74
92
|
end
|
75
93
|
end
|
76
94
|
|
@@ -83,12 +101,14 @@ SYNOPSIS
|
|
83
101
|
|
84
102
|
p A.search('cat').size #=> 2
|
85
103
|
|
104
|
+
p A.search(:literal => A.first.id).size #=> 1
|
105
|
+
|
86
106
|
# you can to cross-model searches like so
|
87
107
|
#
|
88
108
|
p Mongoid::FTS.search('cat', :models => [A, B])
|
89
109
|
p Mongoid::FTS.search('dog', :models => [A, B])
|
90
110
|
|
91
|
-
# pagination is supported
|
111
|
+
# pagination is supported
|
92
112
|
|
93
113
|
A.search('cats').page(10).per(3)
|
94
114
|
|
@@ -109,11 +129,8 @@ SYNOPSIS
|
|
109
129
|
|
110
130
|
````
|
111
131
|
|
112
|
-
|
113
|
-
|
114
|
-
https://groups.google.com/forum/#!topic/mongodb-user/2hUgOAN4KKk
|
115
|
-
|
116
|
-
for details
|
132
|
+
- this implementation has a work around for fulltext pagination, ref: https://groups.google.com/forum/#!topic/mongodb-user/2hUgOAN4KKk
|
133
|
+
- this implementation has a work around for the lack of logical and ref: https://groups.google.com/forum/#!topic/mongodb-user/2hUgOAN4KKk
|
117
134
|
|
118
135
|
regardless, the *interface* of this mixin is uber simple and should be quite
|
119
136
|
future proof. as the mongodb teams moves search forward i'll track the new
|
data/lib/mongoid-fts.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
module Mongoid
|
2
2
|
module FTS
|
3
3
|
#
|
4
|
-
const_set(:Version, '
|
4
|
+
const_set(:Version, '2.0.0') unless const_defined?(:Version)
|
5
5
|
|
6
|
+
#
|
6
7
|
class << FTS
|
7
8
|
def version
|
8
|
-
const_get
|
9
|
+
const_get(:Version)
|
9
10
|
end
|
10
11
|
|
11
12
|
def dependencies
|
@@ -13,6 +14,9 @@ module Mongoid
|
|
13
14
|
'mongoid' => [ 'mongoid' , '~> 3.1' ] ,
|
14
15
|
'map' => [ 'map' , '~> 6.5' ] ,
|
15
16
|
'coerce' => [ 'coerce' , '~> 0.0' ] ,
|
17
|
+
'unicode_utils' => [ 'unicode_utils' , '~> 1.4' ] ,
|
18
|
+
'stringex' => [ 'stringex' , '~> 2.0' ] ,
|
19
|
+
'fast_stemmer' => [ 'fast-stemmer' , '~> 1.0' ] ,
|
16
20
|
}
|
17
21
|
end
|
18
22
|
|
@@ -36,6 +40,9 @@ module Mongoid
|
|
36
40
|
end
|
37
41
|
end
|
38
42
|
|
43
|
+
#
|
44
|
+
require 'digest/md5'
|
45
|
+
|
39
46
|
begin
|
40
47
|
require 'rubygems'
|
41
48
|
rescue LoadError
|
@@ -44,7 +51,7 @@ module Mongoid
|
|
44
51
|
|
45
52
|
if defined?(gem)
|
46
53
|
dependencies.each do |lib, dependency|
|
47
|
-
gem(*dependency)
|
54
|
+
#gem(*dependency)
|
48
55
|
require(lib)
|
49
56
|
end
|
50
57
|
end
|
@@ -56,7 +63,18 @@ module Mongoid
|
|
56
63
|
end
|
57
64
|
|
58
65
|
#
|
59
|
-
|
66
|
+
require 'unicode_utils/u'
|
67
|
+
require 'unicode_utils/each_word'
|
68
|
+
require 'unicode_utils/each_grapheme'
|
69
|
+
|
70
|
+
#
|
71
|
+
load FTS.libdir('error.rb')
|
72
|
+
load FTS.libdir('util.rb')
|
73
|
+
load FTS.libdir('stemming.rb')
|
74
|
+
load FTS.libdir('raw.rb')
|
75
|
+
load FTS.libdir('results.rb')
|
76
|
+
load FTS.libdir('index.rb')
|
77
|
+
load FTS.libdir('able.rb')
|
60
78
|
|
61
79
|
#
|
62
80
|
def FTS.search(*args)
|
@@ -68,684 +86,117 @@ module Mongoid
|
|
68
86
|
end
|
69
87
|
|
70
88
|
def FTS._search(*args)
|
89
|
+
#
|
71
90
|
options = Map.options_for!(args)
|
72
91
|
|
73
|
-
|
74
|
-
|
75
|
-
literals = FTS.literals_for(*words)
|
76
|
-
search = [literals, search].join(' ')
|
77
|
-
|
78
|
-
text = options.delete(:text) || Index.default_collection_name.to_s
|
79
|
-
limit = [Integer(options.delete(:limit) || 128), 1].max
|
80
|
-
models = [options.delete(:models), options.delete(:model)].flatten.compact
|
81
|
-
|
82
|
-
models = FTS.models if models.empty?
|
83
|
-
|
84
|
-
_searches =
|
85
|
-
models.map do |model|
|
86
|
-
context_type = model.name.to_s
|
87
|
-
|
88
|
-
cmd = Hash.new
|
89
|
-
|
90
|
-
cmd[:text] ||= text
|
91
|
-
|
92
|
-
cmd[:limit] ||= limit
|
93
|
-
|
94
|
-
(cmd[:search] ||= '') << search
|
95
|
-
|
96
|
-
cmd[:project] ||= {'_id' => 1, 'context_type' => 1, 'context_id' => 1}
|
97
|
-
|
98
|
-
cmd[:filter] ||= {'context_type' => context_type}
|
99
|
-
|
100
|
-
options.each do |key, value|
|
101
|
-
cmd[key] = value
|
102
|
-
end
|
103
|
-
|
104
|
-
Map.for(session.command(cmd)).tap do |_search|
|
105
|
-
_search[:_model] = model
|
106
|
-
_search[:_cmd] = cmd
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
Raw.new(_searches, :_search => search, :_text => text, :_limit => limit, :_models => models)
|
111
|
-
end
|
112
|
-
|
113
|
-
#
|
114
|
-
class Raw < ::Array
|
115
|
-
attr_accessor :_search
|
116
|
-
attr_accessor :_text
|
117
|
-
attr_accessor :_limit
|
118
|
-
attr_accessor :_models
|
119
|
-
|
120
|
-
def initialize(_searches, options = {})
|
121
|
-
replace(_searches)
|
122
|
-
ensure
|
123
|
-
options.each{|k, v| send("#{ k }=", v)}
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
#
|
128
|
-
class Results < ::Array
|
129
|
-
attr_accessor :_searches
|
130
|
-
attr_accessor :_models
|
131
|
-
|
132
|
-
def initialize(_searches)
|
133
|
-
@_searches = _searches
|
134
|
-
@_models = []
|
135
|
-
_denormalize!
|
136
|
-
@page = 1
|
137
|
-
@per = size
|
138
|
-
@num_pages = 1
|
139
|
-
end
|
140
|
-
|
141
|
-
def paginate(*args)
|
142
|
-
results = self
|
143
|
-
options = Map.options_for!(args)
|
92
|
+
#
|
93
|
+
literals = Coerce.list_of_strings(options[:literals], options[:literal])
|
144
94
|
|
145
|
-
|
146
|
-
per = args.shift || options[:per] || options[:size]
|
147
|
-
|
148
|
-
if per.nil?
|
149
|
-
return Promise.new(results, page)
|
150
|
-
else
|
151
|
-
per = Integer(per)
|
152
|
-
end
|
153
|
-
|
154
|
-
@page = [page.abs, 1].max
|
155
|
-
@per = [per.abs, 1].max
|
156
|
-
@num_pages = (size.to_f / @per).ceil
|
157
|
-
|
158
|
-
offset = (@page - 1) * @per
|
159
|
-
length = @per
|
160
|
-
|
161
|
-
slice = Array(@_models[offset, length])
|
162
|
-
|
163
|
-
replace(slice)
|
164
|
-
|
165
|
-
self
|
166
|
-
end
|
167
|
-
|
168
|
-
class Promise
|
169
|
-
attr_accessor :results
|
170
|
-
attr_accessor :page
|
171
|
-
|
172
|
-
def initialize(results, page)
|
173
|
-
@results = results
|
174
|
-
@page = page
|
175
|
-
end
|
176
|
-
|
177
|
-
def per(per)
|
178
|
-
results.per(:page => page, :per => per)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
def page(*args)
|
183
|
-
if args.empty?
|
184
|
-
return @page
|
185
|
-
else
|
186
|
-
options = Map.options_for!(args)
|
187
|
-
page = args.shift || options[:page]
|
188
|
-
options[:page] = page
|
189
|
-
paginate(options)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
alias_method(:current_page, :page)
|
194
|
-
|
195
|
-
def per(*args)
|
196
|
-
if args.empty?
|
197
|
-
return @per
|
198
|
-
else
|
199
|
-
options = Map.options_for!(args)
|
200
|
-
per = args.shift || options[:per]
|
201
|
-
options[:per] = per
|
202
|
-
paginate(options)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
def num_pages
|
207
|
-
@num_pages
|
208
|
-
end
|
209
|
-
|
210
|
-
def total_pages
|
211
|
-
num_pages
|
212
|
-
end
|
213
|
-
|
214
|
-
# TODO - text sorting more...
|
215
|
-
#
|
216
|
-
def _denormalize!
|
217
|
-
#
|
218
|
-
collection = self
|
219
|
-
|
220
|
-
collection.clear
|
221
|
-
@_models = []
|
222
|
-
|
223
|
-
return self if @_searches.empty?
|
224
|
-
|
225
|
-
#
|
226
|
-
_models = @_searches._models
|
227
|
-
|
228
|
-
_position = proc do |model|
|
229
|
-
_models.index(model) or raise("no position for #{ model.inspect }!?")
|
230
|
-
end
|
95
|
+
terms = Coerce.list_of_strings(options[:terms], options[:searches], options[:term], options[:search], args)
|
231
96
|
|
232
|
-
|
233
|
-
@_searches.map do |_search|
|
234
|
-
_search['results'] ||= []
|
97
|
+
fuzzy = Coerce.list_of_strings(options[:fuzzy])
|
235
98
|
|
236
|
-
|
237
|
-
|
238
|
-
|
99
|
+
#
|
100
|
+
operator =
|
101
|
+
case
|
102
|
+
when options[:all] || options[:operator].to_s == 'and'
|
103
|
+
if options[:all] != true
|
104
|
+
terms.push(*Coerce.list_of_strings(options[:all]))
|
239
105
|
end
|
106
|
+
:and
|
240
107
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
results.flatten!
|
245
|
-
results.compact!
|
246
|
-
|
247
|
-
results.sort! do |a, b|
|
248
|
-
score = Float(b['score']) <=> Float(a['score'])
|
249
|
-
|
250
|
-
case score
|
251
|
-
when 0
|
252
|
-
a['_position'] <=> b['_position']
|
253
|
-
else
|
254
|
-
score
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
#
|
259
|
-
batches = Hash.new{|h,k| h[k] = []}
|
260
|
-
|
261
|
-
results.each do |entry|
|
262
|
-
obj = entry['obj']
|
263
|
-
|
264
|
-
context_type, context_id = obj['context_type'], obj['context_id']
|
265
|
-
|
266
|
-
batches[context_type].push(context_id)
|
267
|
-
end
|
268
|
-
|
269
|
-
#
|
270
|
-
models = FTS.find_in_batches(batches)
|
271
|
-
|
272
|
-
#
|
273
|
-
limit = @_searches._limit
|
274
|
-
|
275
|
-
#
|
276
|
-
replace(@_models = models[0 ... limit])
|
277
|
-
|
278
|
-
self
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
#
|
283
|
-
class Index
|
284
|
-
include Mongoid::Document
|
285
|
-
|
286
|
-
belongs_to(:context, :polymorphic => true)
|
287
|
-
|
288
|
-
field(:literals, :type => Array)
|
289
|
-
|
290
|
-
field(:literal_title, :type => String)
|
291
|
-
field(:title, :type => String)
|
292
|
-
|
293
|
-
field(:literal_keywords, :type => Array)
|
294
|
-
field(:keywords, :type => Array)
|
295
|
-
|
296
|
-
field(:fulltext, :type => String)
|
297
|
-
|
298
|
-
index(
|
299
|
-
{
|
300
|
-
:context_type => 1,
|
301
|
-
:context_id => 1
|
302
|
-
},
|
303
|
-
|
304
|
-
{
|
305
|
-
:unique => true,
|
306
|
-
:sparse => true
|
307
|
-
}
|
308
|
-
)
|
309
|
-
|
310
|
-
index(
|
311
|
-
{
|
312
|
-
:context_type => 1,
|
313
|
-
:literals => 'text',
|
314
|
-
:literal_title => 'text',
|
315
|
-
:title => 'text',
|
316
|
-
:literal_keywords => 'text',
|
317
|
-
:keywords => 'text',
|
318
|
-
:fulltext => 'text'
|
319
|
-
},
|
320
|
-
|
321
|
-
{
|
322
|
-
:name => 'search_index',
|
323
|
-
|
324
|
-
:weights => {
|
325
|
-
:literals => 200,
|
326
|
-
:literal_title => 100,
|
327
|
-
:title => 90,
|
328
|
-
:literal_keywords => 60,
|
329
|
-
:keywords => 50,
|
330
|
-
:fulltext => 1
|
331
|
-
}
|
332
|
-
}
|
333
|
-
)
|
334
|
-
|
335
|
-
before_validation do |index|
|
336
|
-
index.normalize
|
337
|
-
end
|
338
|
-
|
339
|
-
before_upsert do |index|
|
340
|
-
index.normalize
|
341
|
-
end
|
342
|
-
|
343
|
-
before_save do |index|
|
344
|
-
index.normalize
|
345
|
-
end
|
346
|
-
|
347
|
-
validates_presence_of(:context_type)
|
348
|
-
|
349
|
-
def normalize
|
350
|
-
if !defined?(@normalized) or !@normalized
|
351
|
-
normalize!
|
352
|
-
end
|
353
|
-
end
|
354
|
-
|
355
|
-
def normalize!
|
356
|
-
index = self
|
357
|
-
|
358
|
-
unless [index.literals].join.strip.empty?
|
359
|
-
index.literals = FTS.list_of_strings(index.literals)
|
360
|
-
end
|
361
|
-
|
362
|
-
unless [index.title].join.strip.empty?
|
363
|
-
index.title = index.title.to_s.strip
|
364
|
-
end
|
365
|
-
|
366
|
-
unless [index.literal_title].join.strip.empty?
|
367
|
-
index.literal_title = index.literal_title.to_s.strip
|
368
|
-
end
|
369
|
-
|
370
|
-
unless [index.keywords].join.strip.empty?
|
371
|
-
index.keywords = FTS.list_of_strings(index.keywords)
|
372
|
-
end
|
373
|
-
|
374
|
-
unless [index.literal_keywords].join.strip.empty?
|
375
|
-
index.literal_keywords = FTS.list_of_strings(index.literal_keywords)
|
376
|
-
end
|
377
|
-
|
378
|
-
unless [index.fulltext].join.strip.empty?
|
379
|
-
index.fulltext = index.fulltext.to_s.strip
|
380
|
-
end
|
381
|
-
|
382
|
-
ensure
|
383
|
-
@normalized = true
|
384
|
-
end
|
385
|
-
|
386
|
-
def inspect(*args, &block)
|
387
|
-
Map.for(as_document).inspect(*args, &block)
|
388
|
-
end
|
389
|
-
|
390
|
-
def Index.teardown!
|
391
|
-
Index.remove_indexes
|
392
|
-
Index.destroy_all
|
393
|
-
end
|
394
|
-
|
395
|
-
def Index.setup!
|
396
|
-
Index.create_indexes
|
397
|
-
end
|
398
|
-
|
399
|
-
def Index.reset!
|
400
|
-
teardown!
|
401
|
-
setup!
|
402
|
-
end
|
403
|
-
|
404
|
-
def Index.rebuild!
|
405
|
-
batches = Hash.new{|h,k| h[k] = []}
|
406
|
-
|
407
|
-
each do |index|
|
408
|
-
context_type, context_id = index.context_type, index.context_id
|
409
|
-
next unless context_type && context_id
|
410
|
-
(batches[context_type] ||= []).push(context_id)
|
411
|
-
end
|
412
|
-
|
413
|
-
models = FTS.find_in_batches(batches)
|
414
|
-
|
415
|
-
reset!
|
416
|
-
|
417
|
-
models.each{|model| add(model)}
|
418
|
-
end
|
419
|
-
|
420
|
-
def Index.add!(model)
|
421
|
-
to_search = Index.to_search(model)
|
422
|
-
|
423
|
-
literals = to_search.has_key?(:literals) ? Coerce.list_of_strings(to_search[:literals]) : nil
|
424
|
-
|
425
|
-
title = to_search.has_key?(:title) ? Coerce.string(to_search[:title]) : nil
|
426
|
-
literal_title = to_search.has_key?(:literal_title) ? Coerce.string(to_search[:literal_title]) : nil
|
427
|
-
|
428
|
-
keywords = to_search.has_key?(:keywords) ? Coerce.list_of_strings(to_search[:keywords]) : nil
|
429
|
-
literal_keywords = to_search.has_key?(:literal_keywords) ? Coerce.list_of_strings(to_search[:literal_keywords]) : nil
|
430
|
-
|
431
|
-
fulltext = to_search.has_key?(:fulltext) ? Coerce.string(to_search[:fulltext]) : nil
|
432
|
-
|
433
|
-
context_type = model.class.name.to_s
|
434
|
-
context_id = model.id
|
435
|
-
|
436
|
-
conditions = {
|
437
|
-
:context_type => context_type,
|
438
|
-
:context_id => context_id
|
439
|
-
}
|
440
|
-
|
441
|
-
attributes = {
|
442
|
-
:literals => literals,
|
443
|
-
|
444
|
-
:title => title,
|
445
|
-
:literal_title => literal_title,
|
446
|
-
|
447
|
-
:keywords => keywords,
|
448
|
-
:literal_keywords => literal_keywords,
|
449
|
-
|
450
|
-
:fulltext => fulltext
|
451
|
-
}
|
452
|
-
|
453
|
-
index = nil
|
454
|
-
n = 42
|
455
|
-
|
456
|
-
n.times do |i|
|
457
|
-
index = where(conditions).first
|
458
|
-
break if index
|
459
|
-
|
460
|
-
begin
|
461
|
-
index = create!(conditions)
|
462
|
-
break if index
|
463
|
-
rescue Object
|
464
|
-
nil
|
465
|
-
end
|
466
|
-
|
467
|
-
sleep(rand) if i < (n - 1)
|
468
|
-
end
|
469
|
-
|
470
|
-
if index
|
471
|
-
begin
|
472
|
-
index.update_attributes!(attributes)
|
473
|
-
rescue Object
|
474
|
-
raise Error.new("failed to update index for #{ conditions.inspect }")
|
475
|
-
end
|
476
|
-
else
|
477
|
-
raise Error.new("failed to create index for #{ conditions.inspect }")
|
478
|
-
end
|
479
|
-
|
480
|
-
index
|
481
|
-
end
|
482
|
-
|
483
|
-
def Index.add(*args, &block)
|
484
|
-
begin
|
485
|
-
add!(*args, &block)
|
486
|
-
rescue Object
|
487
|
-
false
|
488
|
-
end
|
489
|
-
end
|
490
|
-
|
491
|
-
def Index.remove!(*args, &block)
|
492
|
-
options = args.extract_options!.to_options!
|
493
|
-
models = args.flatten.compact
|
494
|
-
|
495
|
-
model_ids = {}
|
496
|
-
|
497
|
-
models.each do |model|
|
498
|
-
model_name = model.class.name.to_s
|
499
|
-
model_ids[model_name] ||= []
|
500
|
-
model_ids[model_name].push(model.id)
|
501
|
-
end
|
502
|
-
|
503
|
-
conditions = model_ids.map do |model_name, model_ids|
|
504
|
-
{:context_type => model_name, :context_id.in => model_ids}
|
505
|
-
end
|
506
|
-
|
507
|
-
any_of(conditions).destroy_all
|
508
|
-
end
|
509
|
-
|
510
|
-
def Index.remove(*args, &block)
|
511
|
-
begin
|
512
|
-
remove!(*args, &block)
|
513
|
-
rescue Object
|
514
|
-
false
|
515
|
-
end
|
516
|
-
end
|
517
|
-
|
518
|
-
def Index.to_search(model)
|
519
|
-
#
|
520
|
-
to_search = nil
|
521
|
-
|
522
|
-
#
|
523
|
-
if model.respond_to?(:to_search)
|
524
|
-
to_search = Map.for(model.to_search)
|
525
|
-
else
|
526
|
-
to_search = Map.new
|
527
|
-
|
528
|
-
to_search[:literals] =
|
529
|
-
%w( id ).map do |attr|
|
530
|
-
model.send(attr) if model.respond_to?(attr)
|
531
|
-
end
|
532
|
-
|
533
|
-
to_search[:title] =
|
534
|
-
%w( title ).map do |attr|
|
535
|
-
model.send(attr) if model.respond_to?(attr)
|
536
|
-
end
|
537
|
-
|
538
|
-
to_search[:keywords] =
|
539
|
-
%w( keywords tags ).map do |attr|
|
540
|
-
model.send(attr) if model.respond_to?(attr)
|
108
|
+
when options[:any] || options[:operator].to_s == 'or'
|
109
|
+
if options[:any] != true
|
110
|
+
terms.push(*Coerce.list_of_strings(options[:any]))
|
541
111
|
end
|
112
|
+
:or
|
542
113
|
|
543
|
-
|
544
|
-
|
545
|
-
model.send(attr) if model.respond_to?(attr)
|
546
|
-
end
|
547
|
-
end
|
548
|
-
|
549
|
-
#
|
550
|
-
unless %w( literals title keywords fulltext ).detect{|key| to_search.has_key?(key)}
|
551
|
-
raise ArgumentError, "you need to define #{ model }#to_search"
|
114
|
+
else
|
115
|
+
:and
|
552
116
|
end
|
553
117
|
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
keywords = FTS.normalized_array(to_search[:keywords])
|
558
|
-
fulltext = FTS.normalized_array(to_search[:fulltext])
|
559
|
-
|
560
|
-
#
|
561
|
-
to_search[:literals] = FTS.literals_for(literals)
|
562
|
-
|
563
|
-
to_search[:literal_title] = FTS.literals_for(title).join(' ').strip
|
564
|
-
to_search[:title] = title.join(' ').strip
|
565
|
-
|
566
|
-
to_search[:literal_keywords] = FTS.literals_for(keywords).join(' ').strip
|
567
|
-
to_search[:keywords] = keywords.join(' ').strip
|
568
|
-
|
569
|
-
to_search[:fulltext] = fulltext.join(' ').strip
|
570
|
-
|
571
|
-
#
|
572
|
-
to_search
|
118
|
+
#
|
119
|
+
if fuzzy.empty?
|
120
|
+
fuzzy = terms
|
573
121
|
end
|
574
|
-
end
|
575
122
|
|
576
|
-
|
577
|
-
|
578
|
-
end
|
123
|
+
#
|
124
|
+
searches = []
|
579
125
|
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
126
|
+
#
|
127
|
+
strings =
|
128
|
+
[
|
129
|
+
FTS.literals_for(literals),
|
130
|
+
FTS.terms_for(terms)
|
131
|
+
].uniq
|
132
|
+
|
133
|
+
search =
|
134
|
+
case operator
|
135
|
+
when :and
|
136
|
+
FTS.boolean_and(strings)
|
137
|
+
when :or
|
138
|
+
FTS.boolean_or(strings)
|
591
139
|
end
|
592
|
-
end.compact
|
593
|
-
end
|
594
140
|
|
595
|
-
|
596
|
-
if args.empty? and block.nil?
|
597
|
-
Index
|
598
|
-
else
|
599
|
-
Index.add(*args, &block)
|
600
|
-
end
|
601
|
-
end
|
141
|
+
searches.push(search)
|
602
142
|
|
603
|
-
|
604
|
-
|
605
|
-
|
143
|
+
#
|
144
|
+
search = FTS.boolean_or(FTS.fuzzy_for(fuzzy))
|
145
|
+
searches.push(search)
|
606
146
|
|
607
|
-
|
608
|
-
|
609
|
-
|
147
|
+
#
|
148
|
+
text = options.delete(:text) || Index.default_collection_name.to_s
|
149
|
+
limit = [Integer(options.delete(:limit) || 128), 1].max
|
150
|
+
models = [options.delete(:models), options.delete(:model)].flatten.compact
|
610
151
|
|
611
|
-
|
612
|
-
|
613
|
-
end
|
152
|
+
#
|
153
|
+
models = FTS.models if models.empty?
|
614
154
|
|
615
|
-
|
616
|
-
|
617
|
-
def Mixin.code
|
618
|
-
@code ||= proc do
|
619
|
-
class << self
|
620
|
-
def search(*args, &block)
|
621
|
-
options = Map.options_for!(args)
|
155
|
+
#
|
156
|
+
last = searches.size - 1
|
622
157
|
|
623
|
-
|
158
|
+
searches.each_with_index do |search, i|
|
159
|
+
_searches =
|
160
|
+
if search.strip.empty?
|
161
|
+
[]
|
162
|
+
else
|
163
|
+
models.map do |model|
|
164
|
+
context_type = model.name.to_s
|
165
|
+
|
166
|
+
cmd = Hash.new
|
624
167
|
|
625
|
-
|
168
|
+
cmd[:text] ||= text
|
626
169
|
|
627
|
-
|
628
|
-
end
|
170
|
+
cmd[:limit] ||= limit
|
629
171
|
|
630
|
-
|
631
|
-
options = Map.options_for!(args)
|
172
|
+
(cmd[:search] ||= '') << search
|
632
173
|
|
633
|
-
|
174
|
+
cmd[:project] ||= {'_id' => 1, 'context_type' => 1, 'context_id' => 1}
|
634
175
|
|
635
|
-
|
176
|
+
cmd[:filter] ||= {'context_type' => context_type}
|
636
177
|
|
637
|
-
|
178
|
+
Map.for(session.command(cmd)).tap do |_search|
|
179
|
+
_search[:_model] = model
|
180
|
+
_search[:_cmd] = cmd
|
181
|
+
end
|
638
182
|
end
|
639
183
|
end
|
640
184
|
|
641
|
-
|
642
|
-
|
643
|
-
end
|
644
|
-
|
645
|
-
after_destroy do |model|
|
646
|
-
FTS::Index.remove(model) rescue nil
|
647
|
-
end
|
648
|
-
|
649
|
-
has_one(:search_index, :as => :context, :class_name => '::Mongoid::FTS::Index')
|
650
|
-
end
|
651
|
-
end
|
652
|
-
|
653
|
-
def Mixin.included(other)
|
654
|
-
unless other.is_a?(Mixin)
|
655
|
-
begin
|
656
|
-
super
|
657
|
-
ensure
|
658
|
-
other.module_eval(&Mixin.code)
|
659
|
-
|
660
|
-
FTS.models.dup.each do |model|
|
661
|
-
FTS.models.delete(model) if model.name == other.name
|
662
|
-
end
|
663
|
-
|
664
|
-
FTS.models.push(other)
|
665
|
-
FTS.models.uniq!
|
666
|
-
end
|
667
|
-
end
|
185
|
+
raw = Raw.new(_searches, :_search => search, :_text => text, :_limit => limit, :_models => models)
|
186
|
+
return raw if(i == last || !raw.empty?)
|
668
187
|
end
|
669
188
|
end
|
670
189
|
|
671
190
|
def FTS.included(other)
|
672
|
-
|
673
|
-
|
674
|
-
end
|
675
|
-
end
|
676
|
-
|
677
|
-
#
|
678
|
-
def FTS.models
|
679
|
-
@models ||= []
|
680
|
-
end
|
681
|
-
|
682
|
-
def FTS.list_of_strings(*args)
|
683
|
-
args.flatten.compact.map{|arg| arg.to_s}.select{|arg| !arg.empty?}.uniq
|
684
|
-
end
|
685
|
-
|
686
|
-
def FTS.session
|
687
|
-
@session ||= Mongoid::Sessions.default
|
688
|
-
end
|
689
|
-
|
690
|
-
def FTS.session=(session)
|
691
|
-
@session = session
|
692
|
-
end
|
693
|
-
|
694
|
-
def FTS.find_in_batches(queries = {})
|
695
|
-
models =
|
696
|
-
queries.map do |model_class, model_ids|
|
697
|
-
unless model_class.is_a?(Class)
|
698
|
-
model_class = eval(model_class.to_s)
|
699
|
-
end
|
700
|
-
|
701
|
-
model_ids = Array(model_ids)
|
702
|
-
|
703
|
-
begin
|
704
|
-
model_class.find(model_ids)
|
705
|
-
rescue Mongoid::Errors::DocumentNotFound
|
706
|
-
model_ids.map do |model_id|
|
707
|
-
begin
|
708
|
-
model_class.find(model_id)
|
709
|
-
rescue Mongoid::Errors::DocumentNotFound
|
710
|
-
nil
|
711
|
-
end
|
712
|
-
end
|
713
|
-
end
|
714
|
-
end
|
715
|
-
|
716
|
-
models.flatten!
|
717
|
-
models.compact!
|
718
|
-
models
|
719
|
-
end
|
720
|
-
|
721
|
-
def FTS.enable!(*args)
|
722
|
-
options = Map.options_for!(args)
|
723
|
-
|
724
|
-
unless options.has_key?(:warn)
|
725
|
-
options[:warn] = true
|
726
|
-
end
|
727
|
-
|
728
|
-
begin
|
729
|
-
session = Mongoid::Sessions.default
|
730
|
-
session.with(database: :admin).command({ setParameter: 1, textSearchEnabled: true })
|
731
|
-
rescue Object => e
|
732
|
-
unless e.is_a?(Mongoid::Errors::NoSessionsConfig)
|
733
|
-
warn "failed to enable search with #{ e.class }(#{ e.message })"
|
734
|
-
end
|
735
|
-
end
|
191
|
+
other.send(:include, Able)
|
192
|
+
super
|
736
193
|
end
|
737
194
|
end
|
738
195
|
|
739
196
|
Fts = FTS
|
740
197
|
|
741
198
|
if defined?(Rails)
|
742
|
-
|
743
|
-
paths['app/models'] = ::File.dirname(__FILE__)
|
744
|
-
|
745
|
-
config.before_initialize do
|
746
|
-
Mongoid::FTS.enable!(:warn => true)
|
747
|
-
end
|
748
|
-
end
|
199
|
+
load FTS.libdir('rails.rb')
|
749
200
|
else
|
750
201
|
Mongoid::FTS.enable!(:warn => true)
|
751
202
|
end
|
@@ -754,6 +205,10 @@ end
|
|
754
205
|
|
755
206
|
=begin
|
756
207
|
|
208
|
+
http://docs.mongodb.org/manual/reference/operator/query/text/
|
209
|
+
|
210
|
+
http://docs.mongodb.org/v2.4/reference/command/text/
|
211
|
+
|
757
212
|
Model.mongo_session.command(text: "collection_name", search: "my search string", filter: { ... }, project: { ... }, limit: 10, language: "english")
|
758
213
|
|
759
214
|
http://blog.serverdensity.com/full-text-search-in-mongodb/
|