mongoid-fts 1.1.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/
|