mongoid-fts 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ NAME
2
+ mongoid-fts.rb
3
+
4
+ DESCRIPTION
5
+ enable mongodb's new fulltext simply and quickly on your mongoid models, including pagination.
6
+
7
+ SYNOPSIS
8
+
9
+ ````ruby
10
+
11
+ class A
12
+ include Mongoid::Document
13
+ include Mongoid::FTS
14
+
15
+ field(:title)
16
+ field(:body)
17
+
18
+ def to_search
19
+ {:title => title, :fulltext => body}
20
+ end
21
+ end
22
+
23
+
24
+ class B
25
+ include Mongoid::Document
26
+ include Mongoid::FTS
27
+
28
+ field(:title)
29
+ field(:body)
30
+
31
+ def to_search
32
+ {:title => title, :fulltext => body}
33
+ end
34
+ end
35
+
36
+
37
+ A.create!(:title => 'foo', :body => 'cats')
38
+ A.create!(:title => 'bar', :body => 'dogs')
39
+
40
+ B.create!(:title => 'foo', :body => 'cats')
41
+ B.create!(:title => 'bar', :body => 'dogs')
42
+
43
+ p FTS.search('cat', :models => [A, B])
44
+ p FTS.search('dog', :models => [A, B])
45
+
46
+ p A.search('cat')
47
+ p B.search('cat')
48
+ p A.search('dog')
49
+ p B.search('dog')
50
+
51
+ p A.search('cat dog').page(1).per(1)
52
+
53
+ ````
data/Rakefile ADDED
@@ -0,0 +1,446 @@
1
+ This.name =
2
+ "Mongoid::FTS"
3
+
4
+ This.synopsis =
5
+ "enable mongodb's new fulltext simply and quickly on your mongoid models, including pagination."
6
+
7
+ This.rubyforge_project = 'codeforpeople'
8
+ This.author = "Ara T. Howard"
9
+ This.email = "ara.t.howard@gmail.com"
10
+ This.homepage = "https://github.com/ahoward/#{ This.lib }"
11
+
12
+ This.setup!
13
+
14
+
15
+
16
+ task :default do
17
+ puts((Rake::Task.tasks.map{|task| task.name.gsub(/::/,':')} - ['default']).sort)
18
+ end
19
+
20
+ task :test do
21
+ This.run_tests!
22
+ end
23
+
24
+ namespace :test do
25
+ task(:unit){ This.run_tests!(:unit) }
26
+ task(:functional){ This.run_tests!(:functional) }
27
+ task(:integration){ This.run_tests!(:integration) }
28
+ end
29
+
30
+ def This.run_tests!(which = nil)
31
+ which ||= '**'
32
+ test_dir = File.join(This.dir, "test")
33
+ test_glob ||= File.join(test_dir, "#{ which }/**_test.rb")
34
+ test_rbs = Dir.glob(test_glob).sort
35
+
36
+ div = ('=' * 119)
37
+ line = ('-' * 119)
38
+
39
+ test_rbs.each_with_index do |test_rb, index|
40
+ testno = index + 1
41
+ command = "#{ File.basename(This.ruby) } -I ./lib -I ./test/lib #{ test_rb }"
42
+
43
+ puts
44
+ This.say(div, :color => :cyan, :bold => true)
45
+ This.say("@#{ testno } => ", :bold => true, :method => :print)
46
+ This.say(command, :color => :cyan, :bold => true)
47
+ This.say(line, :color => :cyan, :bold => true)
48
+
49
+ system(command)
50
+
51
+ This.say(line, :color => :cyan, :bold => true)
52
+
53
+ status = $?.exitstatus
54
+
55
+ if status.zero?
56
+ This.say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
57
+ This.say("SUCCESS", :color => :green, :bold => true)
58
+ else
59
+ This.say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
60
+ This.say("FAILURE", :color => :red, :bold => true)
61
+ end
62
+ This.say(line, :color => :cyan, :bold => true)
63
+
64
+ exit(status) unless status.zero?
65
+ end
66
+ end
67
+
68
+
69
+ task :gemspec do
70
+ ignore_extensions = ['git', 'svn', 'tmp', /sw./, 'bak', 'gem']
71
+ ignore_directories = ['pkg', 'db']
72
+ ignore_files = ['test/log', 'test/db.yml', 'a.rb', 'b.rb'] + Dir['db/*'] + %w'db'
73
+
74
+ shiteless =
75
+ lambda do |list|
76
+ list.delete_if do |entry|
77
+ next unless test(?e, entry)
78
+ extension = File.basename(entry).split(%r/[.]/).last
79
+ ignore_extensions.any?{|ext| ext === extension}
80
+ end
81
+ list.delete_if do |entry|
82
+ next unless test(?d, entry)
83
+ dirname = File.expand_path(entry)
84
+ ignore_directories.any?{|dir| File.expand_path(dir) == dirname}
85
+ end
86
+ list.delete_if do |entry|
87
+ next unless test(?f, entry)
88
+ filename = File.expand_path(entry)
89
+ ignore_files.any?{|file| File.expand_path(file) == filename}
90
+ end
91
+ end
92
+
93
+ lib = This.lib
94
+ object = This.object
95
+ version = This.version
96
+ files = shiteless[Dir::glob("**/**")]
97
+ executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)}
98
+ #has_rdoc = true #File.exist?('doc')
99
+ test_files = test(?e, "test/#{ lib }.rb") ? "test/#{ lib }.rb" : nil
100
+ summary = This.summary || This.synopsis || "#{ lib } kicks the ass"
101
+ description = This.description || summary
102
+
103
+ if This.extensions.nil?
104
+ This.extensions = []
105
+ extensions = This.extensions
106
+ %w( Makefile configure extconf.rb ).each do |ext|
107
+ extensions << ext if File.exists?(ext)
108
+ end
109
+ end
110
+ extensions = [extensions].flatten.compact
111
+
112
+ # TODO
113
+ if This.dependencies.nil?
114
+ dependencies = []
115
+ else
116
+ case This.dependencies
117
+ when Hash
118
+ dependencies = This.dependencies.values
119
+ when Array
120
+ dependencies = This.dependencies
121
+ end
122
+ end
123
+
124
+ template =
125
+ if test(?e, 'gemspec.erb')
126
+ This.template_for{ IO.read('gemspec.erb') }
127
+ else
128
+ This.template_for {
129
+ <<-__
130
+ ## <%= lib %>.gemspec
131
+ #
132
+
133
+ Gem::Specification::new do |spec|
134
+ spec.name = <%= lib.inspect %>
135
+ spec.version = <%= version.inspect %>
136
+ spec.platform = Gem::Platform::RUBY
137
+ spec.summary = <%= lib.inspect %>
138
+ spec.description = <%= description.inspect %>
139
+
140
+ spec.files =\n<%= files.sort.pretty_inspect %>
141
+ spec.executables = <%= executables.inspect %>
142
+
143
+ spec.require_path = "lib"
144
+
145
+ spec.test_files = <%= test_files.inspect %>
146
+
147
+ <% dependencies.each do |lib_version| %>
148
+ spec.add_dependency(*<%= Array(lib_version).flatten.inspect %>)
149
+ <% end %>
150
+
151
+ spec.extensions.push(*<%= extensions.inspect %>)
152
+
153
+ spec.rubyforge_project = <%= This.rubyforge_project.inspect %>
154
+ spec.author = <%= This.author.inspect %>
155
+ spec.email = <%= This.email.inspect %>
156
+ spec.homepage = <%= This.homepage.inspect %>
157
+ end
158
+ __
159
+ }
160
+ end
161
+
162
+ FileUtils.mkdir_p(This.pkgdir)
163
+ gemspec = "#{ lib }.gemspec"
164
+ open(gemspec, "w"){|fd| fd.puts(template)}
165
+ This.gemspec = gemspec
166
+ end
167
+
168
+ task :gem => [:clean, :gemspec] do
169
+ FileUtils.mkdir_p(This.pkgdir)
170
+ before = Dir['*.gem']
171
+ cmd = "gem build #{ This.gemspec }"
172
+ `#{ cmd }`
173
+ after = Dir['*.gem']
174
+ gem = ((after - before).first || after.first) or abort('no gem!')
175
+ FileUtils.mv(gem, This.pkgdir)
176
+ This.gem = File.join(This.pkgdir, File.basename(gem))
177
+ end
178
+
179
+ task :readme do
180
+ samples = ''
181
+ prompt = '~ > '
182
+ lib = This.lib
183
+ version = This.version
184
+
185
+ Dir['sample*/*'].sort.each do |sample|
186
+ samples << "\n" << " <========< #{ sample } >========>" << "\n\n"
187
+
188
+ cmd = "cat #{ sample }"
189
+ samples << This.util.indent(prompt + cmd, 2) << "\n\n"
190
+ samples << This.util.indent(`#{ cmd }`, 4) << "\n"
191
+
192
+ cmd = "ruby #{ sample }"
193
+ samples << This.util.indent(prompt + cmd, 2) << "\n\n"
194
+
195
+ cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'"
196
+ samples << This.util.indent(`#{ cmd } 2>&1`, 4) << "\n"
197
+ end
198
+
199
+ template =
200
+ if test(?e, 'readme.erb')
201
+ This.template_for{ IO.read('readme.erb') }
202
+ else
203
+ This.template_for {
204
+ <<-__
205
+ NAME
206
+ #{ lib }
207
+
208
+ DESCRIPTION
209
+
210
+ INSTALL
211
+ gem install #{ lib }
212
+
213
+ SAMPLES
214
+ #{ samples }
215
+ __
216
+ }
217
+ end
218
+
219
+ open("README", "w"){|fd| fd.puts template}
220
+ end
221
+
222
+
223
+ task :clean do
224
+ Dir[File.join(This.pkgdir, '**/**')].each{|entry| FileUtils.rm_rf(entry)}
225
+ end
226
+
227
+
228
+ task :release => [:clean, :gemspec, :gem] do
229
+ gems = Dir[File.join(This.pkgdir, '*.gem')].flatten
230
+ raise "which one? : #{ gems.inspect }" if gems.size > 1
231
+ raise "no gems?" if gems.size < 1
232
+
233
+ cmd = "gem push #{ This.gem }"
234
+ puts cmd
235
+ puts
236
+ system(cmd)
237
+ abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
238
+
239
+ #cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.gem }"
240
+ #puts cmd
241
+ #puts
242
+ #system(cmd)
243
+ #abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
244
+ end
245
+
246
+
247
+
248
+
249
+
250
+ BEGIN {
251
+ # support for this rakefile
252
+ #
253
+ $VERBOSE = nil
254
+
255
+ require 'erb'
256
+ require 'fileutils'
257
+ require 'rbconfig'
258
+ require 'pp'
259
+
260
+ # cache a bunch of stuff about this rakefile/environment
261
+ #
262
+
263
+ This =
264
+ Class.new(Hash) do
265
+
266
+ def method_missing(method, *args, &block)
267
+ if method.to_s =~ /=/
268
+ key = method.to_s.chomp('=')
269
+ value = block ? block : args.shift
270
+ self[key] = value
271
+ else
272
+ key = method.to_s
273
+ if block
274
+ value = block
275
+ self[key] = value
276
+ else
277
+ value = self[key]
278
+
279
+ if value.respond_to?(:call)
280
+ self[key] = value.call()
281
+ else
282
+ value
283
+ end
284
+ end
285
+ end
286
+ end
287
+
288
+ def inspect
289
+ expand!
290
+ PP.pp(self, '')
291
+ end
292
+
293
+ def expand!
294
+ keys.each do |key|
295
+ value = self[key]
296
+ if value.respond_to?(:call)
297
+ self[key] = value.call()
298
+ end
299
+ end
300
+ end
301
+
302
+ end.new()
303
+
304
+ This.file = File.expand_path(__FILE__)
305
+ This.dir = File.dirname(This.file)
306
+ This.pkgdir = File.join(This.dir, 'pkg')
307
+
308
+ # defaults
309
+ #
310
+ This.lib do
311
+ File.basename(Dir.pwd)
312
+ end
313
+
314
+ def This.setup!
315
+ begin
316
+ require "./lib/#{ This.lib }"
317
+ rescue LoadError
318
+ abort("could not load #{ This.lib }")
319
+ end
320
+ end
321
+
322
+ This.name do
323
+ This.name = This.lib.capitalize
324
+ end
325
+
326
+ This.object do
327
+ begin
328
+ This.object = eval(This.name)
329
+ rescue Object
330
+ abort("could not determine object from #{ This.name }")
331
+ end
332
+ end
333
+
334
+ This.version do
335
+ This.object.send(:version)
336
+ end
337
+
338
+ This.dependencies do
339
+ if This.object.respond_to?(:dependencies)
340
+ This.object.dependencies
341
+ end
342
+ end
343
+
344
+ This.ruby do
345
+ c = Config::CONFIG
346
+ bindir = c["bindir"] || c['BINDIR']
347
+ ruby_install_name = c['ruby_install_name'] || c['RUBY_INSTALL_NAME'] || 'ruby'
348
+ ruby_ext = c['EXEEXT'] || ''
349
+ File.join(bindir, (ruby_install_name + ruby_ext))
350
+ end
351
+
352
+ # some utils
353
+ #
354
+ This.util = Module.new do
355
+ def indent(s, n = 2)
356
+ s = unindent(s)
357
+ ws = ' ' * n
358
+ s.gsub(%r/^/, ws)
359
+ end
360
+
361
+ def unindent(s)
362
+ indent = nil
363
+ s.each_line do |line|
364
+ next if line =~ %r/^\s*$/
365
+ indent = line[%r/^\s*/] and break
366
+ end
367
+ indent ? s.gsub(%r/^#{ indent }/, "") : s
368
+ end
369
+
370
+ extend self
371
+ end
372
+
373
+ # template support
374
+ #
375
+ This.template = Class.new do
376
+ def initialize(&block)
377
+ @block = block
378
+ @template = block.call.to_s
379
+ end
380
+
381
+ def expand(b=nil)
382
+ ERB.new(This.util.unindent(@template)).result((b||@block).binding)
383
+ end
384
+
385
+ alias_method 'to_s', 'expand'
386
+ end
387
+
388
+ def This.template_for(*args, &block)
389
+ This.template.new(*args, &block)
390
+ end
391
+
392
+ # colored console output support
393
+ #
394
+ This.ansi = {
395
+ :clear => "\e[0m",
396
+ :reset => "\e[0m",
397
+ :erase_line => "\e[K",
398
+ :erase_char => "\e[P",
399
+ :bold => "\e[1m",
400
+ :dark => "\e[2m",
401
+ :underline => "\e[4m",
402
+ :underscore => "\e[4m",
403
+ :blink => "\e[5m",
404
+ :reverse => "\e[7m",
405
+ :concealed => "\e[8m",
406
+ :black => "\e[30m",
407
+ :red => "\e[31m",
408
+ :green => "\e[32m",
409
+ :yellow => "\e[33m",
410
+ :blue => "\e[34m",
411
+ :magenta => "\e[35m",
412
+ :cyan => "\e[36m",
413
+ :white => "\e[37m",
414
+ :on_black => "\e[40m",
415
+ :on_red => "\e[41m",
416
+ :on_green => "\e[42m",
417
+ :on_yellow => "\e[43m",
418
+ :on_blue => "\e[44m",
419
+ :on_magenta => "\e[45m",
420
+ :on_cyan => "\e[46m",
421
+ :on_white => "\e[47m"
422
+ }
423
+
424
+ def This.say(something, *args)
425
+ options = args.last.is_a?(Hash) ? args.pop : {}
426
+ options[:color] = args.shift.to_s.to_sym unless args.empty?
427
+ keys = options.keys
428
+ keys.each{|key| options[key.to_s.to_sym] = options.delete(key)}
429
+
430
+ color = options[:color]
431
+ bold = options.has_key?(:bold)
432
+
433
+ parts = [something]
434
+ parts.unshift(This.ansi[color]) if color
435
+ parts.unshift(This.ansi[:bold]) if bold
436
+ parts.push(This.ansi[:clear]) if parts.size > 1
437
+
438
+ method = options[:method] || :puts
439
+
440
+ Kernel.send(method, parts.join)
441
+ end
442
+
443
+ # always run out of the project dir
444
+ #
445
+ Dir.chdir(This.dir)
446
+ }
@@ -0,0 +1 @@
1
+ Mongoid::FTS::Index
@@ -0,0 +1,552 @@
1
+ module Mongoid
2
+ module FTS
3
+ #
4
+ const_set(:Version, '0.0.1') unless const_defined?(:Version)
5
+
6
+ class << FTS
7
+ def version
8
+ const_get :Version
9
+ end
10
+
11
+ def dependencies
12
+ {
13
+ 'mongoid' => [ 'mongoid' , '~> 3.1' ] ,
14
+ 'map' => [ 'map' , '~> 6.5' ] ,
15
+ 'coerce' => [ 'coerce' , '~> 0.0' ] ,
16
+ }
17
+ end
18
+
19
+ def libdir(*args, &block)
20
+ @libdir ||= File.expand_path(__FILE__).sub(/\.rb$/,'')
21
+ args.empty? ? @libdir : File.join(@libdir, *args)
22
+ ensure
23
+ if block
24
+ begin
25
+ $LOAD_PATH.unshift(@libdir)
26
+ block.call()
27
+ ensure
28
+ $LOAD_PATH.shift()
29
+ end
30
+ end
31
+ end
32
+
33
+ def load(*libs)
34
+ libs = libs.join(' ').scan(/[^\s+]+/)
35
+ libdir{ libs.each{|lib| Kernel.load(lib) } }
36
+ end
37
+ end
38
+
39
+ begin
40
+ require 'rubygems'
41
+ rescue LoadError
42
+ nil
43
+ end
44
+
45
+ if defined?(gem)
46
+ dependencies.each do |lib, dependency|
47
+ gem(*dependency)
48
+ require(lib)
49
+ end
50
+ end
51
+
52
+ begin
53
+ require 'pry'
54
+ rescue LoadError
55
+ nil
56
+ end
57
+
58
+
59
+ #
60
+ def FTS.search(*args)
61
+ options = args.extract_options!.to_options!
62
+
63
+ args.push(options)
64
+
65
+ _searches = FTS._search(*args)
66
+
67
+ Results.new(_searches)
68
+ end
69
+
70
+ def FTS._search(*args)
71
+ options = args.extract_options!.to_options!
72
+
73
+ search = args.join(' ')
74
+
75
+ text = options.delete(:text) || Index.default_collection_name.to_s
76
+ limit = [Integer(options.delete(:limit) || 128), 1].max
77
+ models = [options.delete(:models), options.delete(:model)].flatten.compact
78
+
79
+ models = FTS.models if models.blank?
80
+
81
+ _searches =
82
+ models.map do |model|
83
+ context_type = model.name.to_s
84
+
85
+ cmd = Hash.new
86
+
87
+ cmd[:text] ||= text
88
+
89
+ cmd[:limit] ||= limit
90
+
91
+ (cmd[:search] ||= '') << search
92
+
93
+ cmd[:project] ||= {'_id' => 1, 'context_type' => 1, 'context_id' => 1}
94
+
95
+ cmd[:filter] ||= {'context_type' => context_type}
96
+
97
+ options.each do |key, value|
98
+ cmd[key] = value
99
+ end
100
+
101
+ Map.for(session.command(cmd)).tap do |_search|
102
+ _search[:_model] = model
103
+ _search[:_cmd] = cmd
104
+ end
105
+ end
106
+
107
+ Raw.new(_searches, :_search => search, :_text => text, :_limit => limit, :_models => models)
108
+ end
109
+
110
+ #
111
+ class Raw < ::Array
112
+ attr_accessor :_search
113
+ attr_accessor :_text
114
+ attr_accessor :_limit
115
+ attr_accessor :_models
116
+
117
+ def initialize(_searches, options = {})
118
+ replace(_searches)
119
+ ensure
120
+ options.each{|k, v| send("#{ k }=", v)}
121
+ end
122
+ end
123
+
124
+ #
125
+ class Results < ::Array
126
+ attr_accessor :_searches
127
+ attr_accessor :_models
128
+
129
+ def initialize(_searches)
130
+ @_searches = _searches
131
+ @_models = []
132
+ _denormalize!
133
+ @page = 1
134
+ @per = size
135
+ end
136
+
137
+ def paginate(*args)
138
+ options = args.extract_options!.to_options!
139
+
140
+ page = Integer(args.shift || options[:page] || @page)
141
+ per = Integer(args.shift || options[:per] || options[:size] || @per)
142
+
143
+ @page = [page.abs, 1].max
144
+ @per = [per.abs, 1].max
145
+
146
+ offset = (@page - 1) * @per
147
+ length = @per
148
+
149
+ slice = Array(@_models[offset, length])
150
+
151
+ replace(slice)
152
+
153
+ self
154
+ end
155
+
156
+ def page(*args)
157
+ if args.empty?
158
+ return @page
159
+ else
160
+ options = args.extract_options!.to_options!
161
+ page = args.shift || options[:page]
162
+ options[:page] = page
163
+ paginate(options)
164
+ end
165
+ end
166
+
167
+ def per(*args)
168
+ if args.empty?
169
+ return @per
170
+ else
171
+ options = args.extract_options!.to_options!
172
+ per = args.shift || options[:per]
173
+ options[:per] = per
174
+ paginate(options)
175
+ end
176
+ end
177
+
178
+ def num_pages
179
+ (size.to_f / per).ceil
180
+ end
181
+
182
+ def total_pages
183
+ num_pages
184
+ end
185
+
186
+ # TODO - text sorting more...
187
+ #
188
+ def _denormalize!
189
+ #
190
+ collection = self
191
+
192
+ collection.clear
193
+ @_models = []
194
+
195
+ return self if @_searches.empty?
196
+
197
+ #
198
+ _models = @_searches._models
199
+
200
+ _position = proc do |model|
201
+ _models.index(model) or raise("no position for #{ model.inspect }!?")
202
+ end
203
+
204
+ results =
205
+ @_searches.map do |_search|
206
+ _search['results'] ||= []
207
+
208
+ _search['results'].each do |result|
209
+ result['_model'] = _search._model
210
+ result['_position'] = _position[_search._model]
211
+ end
212
+
213
+ _search['results']
214
+ end
215
+
216
+ results.flatten!
217
+ results.compact!
218
+
219
+ results.sort! do |a, b|
220
+ score = Float(b['score']) <=> Float(a['score'])
221
+
222
+ case score
223
+ when 0
224
+ a['_position'] <=> b['_position']
225
+ else
226
+ score
227
+ end
228
+ end
229
+
230
+ #
231
+ batches = Hash.new{|h,k| h[k] = []}
232
+
233
+ results.each do |entry|
234
+ obj = entry['obj']
235
+
236
+ context_type, context_id = obj['context_type'], obj['context_id']
237
+
238
+ batches[context_type].push(context_id)
239
+ end
240
+
241
+ #
242
+ models = FTS.find_in_batches(batches)
243
+
244
+ #
245
+ limit = @_searches._limit
246
+
247
+ #
248
+ replace(@_models = models[0 ... limit])
249
+
250
+ self
251
+ end
252
+ end
253
+
254
+ #
255
+ class Index
256
+ include Mongoid::Document
257
+
258
+ belongs_to(:context, :polymorphic => true)
259
+
260
+ field(:title, :type => String)
261
+ field(:keywords, :type => Array)
262
+ field(:fulltext, :type => String)
263
+
264
+ index(
265
+ {:context_type => 1, :title => 'text', :keywords => 'text', :fulltext => 'text'},
266
+ {:weights => { :title => 100, :keywords => 50, :fulltext => 1 }, :name => 'search_index'}
267
+ )
268
+
269
+ index(
270
+ {:context_type => 1, :context_id => 1},
271
+ {:unique => true, :sparse => true}
272
+ )
273
+
274
+ before_validation do |index|
275
+ index.normalize
276
+ end
277
+
278
+ before_upsert do |index|
279
+ index.normalize
280
+ end
281
+
282
+ validates_presence_of(:context_type)
283
+
284
+ def normalize
285
+ if !defined?(@normalized) or !@normalized
286
+ normalize!
287
+ end
288
+ end
289
+
290
+ def normalize!
291
+ index = self
292
+
293
+ unless index.keywords.blank?
294
+ index.keywords = FTS.list_of_strings(index.keywords)
295
+ end
296
+
297
+ unless index.title.blank?
298
+ index.title = index.title.to_s.strip
299
+ end
300
+
301
+ unless index.keywords.blank?
302
+ index.keywords = index.keywords.map{|keyword| keyword.strip}
303
+ end
304
+
305
+ unless index.fulltext.blank?
306
+ index.fulltext = index.fulltext.to_s.strip
307
+ end
308
+
309
+ ensure
310
+ @normalized = true
311
+ end
312
+
313
+ def Index.teardown!
314
+ Index.remove_indexes
315
+ Index.destroy_all
316
+ end
317
+
318
+ def Index.setup!
319
+ Index.create_indexes
320
+ end
321
+
322
+ def Index.reset!
323
+ teardown!
324
+ setup!
325
+ end
326
+
327
+ def Index.rebuild!
328
+ batches = Hash.new{|h,k| h[k] = []}
329
+
330
+ each do |index|
331
+ context_type, context_id = index.context_type, index.context_id
332
+ next unless context_type && context_id
333
+ (batches[context_type] ||= []).push(context_id)
334
+ end
335
+
336
+ models = FTS.find_in_batches(batches)
337
+
338
+ reset!
339
+
340
+ models.each{|model| add(model)}
341
+ end
342
+
343
+ def Index.add(model)
344
+ to_search = Index.to_search(model)
345
+
346
+ title = to_search.has_key?(:title) ? Coerce.string(to_search[:title]) : nil
347
+ keywords = to_search.has_key?(:keywords) ? Coerce.list_of_strings(to_search[:keywords]) : nil
348
+ fulltext = to_search.has_key?(:fulltext) ? Coerce.string(to_search[:fulltext]) : nil
349
+
350
+ context_type = model.class.name.to_s
351
+ context_id = model.id
352
+
353
+ conditions = {
354
+ :context_type => context_type,
355
+ :context_id => context_id
356
+ }
357
+
358
+ attributes = {
359
+ :title => title,
360
+ :keywords => keywords,
361
+ :fulltext => fulltext
362
+ }
363
+
364
+ new(conditions).upsert
365
+
366
+ where(conditions).first.tap do |index|
367
+ if index
368
+ index.update_attributes(attributes)
369
+ end
370
+ end
371
+ end
372
+
373
+ def Index.remove(model)
374
+ context_type = model.class.name.to_s
375
+ context_id = model.id
376
+
377
+ conditions = {
378
+ :context_type => context_type,
379
+ :context_id => context_id
380
+ }
381
+
382
+ where(conditions).first.tap do |index|
383
+ if index
384
+ index.destroy rescue nil
385
+ end
386
+ end
387
+ end
388
+
389
+ def Index.to_search(model)
390
+ to_search = nil
391
+
392
+ if model.respond_to?(:to_search)
393
+ to_search = Map.for(model.to_search)
394
+ else
395
+ to_search = Map.new
396
+
397
+ to_search[:title] =
398
+ %w( title ).map do |attr|
399
+ model.send(attr) if model.respond_to?(attr)
400
+ end.compact.join(' ')
401
+
402
+ to_search[:keywords] =
403
+ %w( keywords tags ).map do |attr|
404
+ model.send(attr) if model.respond_to?(attr)
405
+ end.compact
406
+
407
+ to_search[:fulltext] =
408
+ %w( fulltext text content body description ).map do |attr|
409
+ model.send(attr) if model.respond_to?(attr)
410
+ end.compact.join(' ')
411
+ end
412
+
413
+ unless %w( title keywords fulltext ).detect{|key| to_search.has_key?(key)}
414
+ raise ArgumentError, "you need to define #{ model }#to_search"
415
+ end
416
+
417
+ to_search
418
+ end
419
+ end
420
+
421
+ def FTS.index
422
+ Index
423
+ end
424
+
425
+ #
426
+ module Mixin
427
+ def Mixin.code
428
+ @code ||= proc do
429
+ class << self
430
+ def search(*args, &block)
431
+ args.push(options = args.extract_options!.to_options!)
432
+
433
+ options[:model] = self
434
+
435
+ FTS.search(*args, &block)
436
+ end
437
+
438
+ def _search(*args, &block)
439
+ args.push(options = args.extract_options!.to_options!)
440
+
441
+ options[:model] = self
442
+
443
+ FTS.search(*args, &block)
444
+ end
445
+ end
446
+
447
+ after_save do |model|
448
+ FTS::Index.add(model) rescue nil
449
+ end
450
+
451
+ after_destroy do |model|
452
+ FTS::Index.remove(model) rescue nil
453
+ end
454
+ end
455
+ end
456
+
457
+ def Mixin.included(other)
458
+ unless other.is_a?(Mixin)
459
+ begin
460
+ super
461
+ ensure
462
+ other.module_eval(&Mixin.code)
463
+ FTS.models.push(other)
464
+ FTS.models.uniq!
465
+ end
466
+ end
467
+ end
468
+ end
469
+
470
+ def FTS.included(other)
471
+ unless other.is_a?(FTS::Mixin)
472
+ other.send(:include, FTS::Mixin)
473
+ end
474
+ end
475
+
476
+ #
477
+ def FTS.models
478
+ @models ||= []
479
+ end
480
+
481
+ def FTS.list_of_strings(*args)
482
+ args.flatten.compact.map{|arg| arg.to_s}.select{|arg| !arg.empty?}.uniq
483
+ end
484
+
485
+ def FTS.session
486
+ @session ||= Mongoid::Sessions.default
487
+ end
488
+
489
+ def FTS.session=(session)
490
+ @session = session
491
+ end
492
+
493
+ def FTS.find_in_batches(queries = {})
494
+ models =
495
+ queries.map do |model_class, model_ids|
496
+ unless model_class.is_a?(Class)
497
+ model_class = eval(model_class.to_s)
498
+ end
499
+
500
+ model_ids = Array(model_ids)
501
+
502
+ begin
503
+ model_class.find(model_ids)
504
+ rescue Mongoid::Errors::DocumentNotFound
505
+ model_ids.map do |model_id|
506
+ begin
507
+ model_class.find(model_id)
508
+ rescue Mongoid::Errors::DocumentNotFound
509
+ nil
510
+ end
511
+ end
512
+ end
513
+ end
514
+
515
+ models.flatten!
516
+ models.compact!
517
+ models
518
+ end
519
+
520
+ def FTS.enable!
521
+ session = Mongoid::Sessions.default
522
+ session.with(database: :admin).command({ setParameter: 1, textFTSEnabled: true })
523
+ end
524
+
525
+ begin
526
+ FTS.enable!
527
+ rescue Object => e
528
+ warn "failed to enable search with #{ e.class }(#{ e.message })"
529
+ end
530
+ end
531
+
532
+ Fts = FTS
533
+
534
+ if defined?(Rails)
535
+ class FTS::Engine < ::Rails::Engine
536
+ paths['app/models'] = ::File.dirname(__FILE__)
537
+ end
538
+ end
539
+ end
540
+
541
+
542
+ =begin
543
+
544
+ Model.mongo_session.command(text: "collection_name", search: "my search string", filter: { ... }, project: { ... }, limit: 10, language: "english")
545
+
546
+ http://blog.serverdensity.com/full-text-search-in-mongodb/
547
+
548
+ http://blog.mongohq.com/mongodb-and-full-text-search-my-first-week-with-mongodb-2-4-development-release/
549
+
550
+ http://docs.mongodb.org/manual/single/index.html#document-tutorial/enable-text-search
551
+
552
+ =end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-fts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ara T. Howard
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongoid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.1'
30
+ - !ruby/object:Gem::Dependency
31
+ name: map
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '6.5'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '6.5'
46
+ - !ruby/object:Gem::Dependency
47
+ name: coerce
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.0'
62
+ description: enable mongodb's new fulltext simply and quickly on your mongoid models,
63
+ including pagination.
64
+ email: ara.t.howard@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - README.md
70
+ - Rakefile
71
+ - lib/app/mongoid/fts/index.rb
72
+ - lib/mongoid-fts.rb
73
+ homepage: https://github.com/ahoward/mongoid-fts
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project: codeforpeople
93
+ rubygems_version: 1.8.23
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: mongoid-fts
97
+ test_files: []