mongodb_fulltext_search_er 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c5c193692d9add9fc4b9faa7f5d212b3007e3279
4
+ data.tar.gz: 20382936bcc690b7de6ddafcbd0bf2069beb42d1
5
+ SHA512:
6
+ metadata.gz: e919d05ebe300c5a467763c9ef2a6a029d08019af77575ee5cfac57c1ee4e34db07a1de72f2d7ed049866da4a7087116af6a2767a84ae68dbadea8f2f04813ec
7
+ data.tar.gz: 30e36fdcbcc829f1e50c9883df3327c764b8de6e841a8eb903a8aae1b1a18172b91c4068ff07940fb390e014a4fec4ff1f5459e4faa75cf091a84fc3ec71de82
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Christopher Fuller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,164 @@
1
+ == same as chrisfuller's mongodb_fulltext_search, but excludes rails as a dependency
2
+
3
+ This was done to allow use of mongodb_fulltext_search with any version/branch of rails
4
+
5
+ = mongodb_fulltext_search
6
+
7
+ A gem that adds fulltext search capability to Mongoid[http://mongoid.org/] or MongoMapper[http://mongomapper.com/] documents. The MongoDB[http://mongodb.org/] aggregation framework is utilized to perform searches quickly and efficiently.
8
+
9
+ == Prerequisites
10
+
11
+ Rails must be configured to use either Mongoid or MongoMapper and MongoDB[http://mongodb.org/] version 2.1.0 (or higher) is required.
12
+
13
+ Please get in touch if you’d like to see added support for another MongoDB object mapper (ODM).
14
+
15
+ == Installation
16
+
17
+ $ gem install mongodb_fulltext_search
18
+
19
+ == Rails Configuration
20
+
21
+ Add to Gemfile:
22
+
23
+ gem 'mongodb_fulltext_search'
24
+
25
+ == Indexes
26
+
27
+ This gem implements indexes specifically for the fulltext search.
28
+
29
+ The fulltext indexes are maintained automatically via the +before_save+ and +before_destroy+ callbacks on models, however you MUST run the rake task (below) to rebuild the indexes any time your models change.
30
+
31
+ The indexes are implemented as MongoDB collections (since MongoDB's built-in indexes do not currently support fulltext search), therefore the fulltext indexes are actually "index collections". The "index collections" are stored in the same database that the models are persisted to.
32
+
33
+ The "index collections" benefit from using MongoDB indexes themselves and have MongoDB indexes defined via the underlying ODM, therefore be sure to create the indexes following the instructions for the ODM you are using.
34
+
35
+ With Mongoid for example, use the normal rake command <tt>rake db:mongoid:create_indexes</tt>. With MongoMapper for example, the indexes should be created automatically by default.
36
+
37
+ For the remainder of the documentation, the "index collections" will now simply be referred to as "indexes".
38
+
39
+ == Examples
40
+
41
+ Mongoid example:
42
+
43
+ class Widget
44
+
45
+ include Mongoid::Document
46
+ include Mongoid::FullTextSearch
47
+
48
+ field :desc, :type => String
49
+ field :tags, :type => Array
50
+
51
+ fulltext_search_in :desc
52
+
53
+ end
54
+
55
+ MongoMapper example:
56
+
57
+ class Widget
58
+
59
+ include MongoMapper::Document
60
+ include MongoMapper::FullTextSearch
61
+
62
+ key :desc, String
63
+ key :tags, Array
64
+
65
+ fulltext_search_in :desc
66
+
67
+ end
68
+
69
+ Or index multiple attributes together:
70
+
71
+ fulltext_search_in :desc, :tags
72
+
73
+ Or index virtual attributes:
74
+
75
+ fulltext_search_in :desc_and_tags
76
+
77
+ def desc_and_tags
78
+ [ desc ] + tags
79
+ end
80
+
81
+ NOTE: All attributes (and virtual attributes) supplied to +fulltext_search_in+ must return either a string or an array of strings.
82
+
83
+ == Performing Searches
84
+
85
+ In controllers, perform searches with:
86
+
87
+ @widgets = Widget.fulltext_search params[:search_query]
88
+
89
+ After fulltext searches are performed, access scores via a +fulltext_search_score+ attribute on models:
90
+
91
+ @widgets.each { |widget| widget.fulltext_search_score }
92
+
93
+ == Multiple Indexes
94
+
95
+ It is possible to create more than one index on a model:
96
+
97
+ fulltext_search_in :desc, :index => 'desc_index'
98
+ fulltext_search_in :tags, :index => 'tags_index'
99
+
100
+ In this case, you MUST specify an index when performing a search:
101
+
102
+ @widgets = Widget.fulltext_search params[:search_query], :index => 'desc_index'
103
+
104
+ or
105
+
106
+ @widgets = Widget.fulltext_search params[:search_query], :index => 'tags_index'
107
+
108
+ NOTE: An +ArgumentError+ exception will be raised if multiple indexes exist but an index is not specified.
109
+
110
+ == Options
111
+
112
+ To match partial words, turn off exact matching:
113
+
114
+ @widgets = Widget.fulltext_search params[:search_query], :exact => false
115
+
116
+ By default, results are limited to 20 models but you can specify a limit when searching:
117
+
118
+ @widgets = Widget.fulltext_search params[:search_query], :limit => 10
119
+
120
+ And you can also specify an offset:
121
+
122
+ @widgets = Widget.fulltext_search params[:search_query], :offset => 20
123
+
124
+ To return document identifiers and search scores as a +Hash+ (instead of the returning the model objects):
125
+
126
+ @scores = Widget.fulltext_search params[:search_query], :return_scores => true
127
+ @scores.each { |id, score| id + ': ' + score }
128
+
129
+ == Normalization
130
+
131
+ Strings are normalized using <tt>normalize(:kd)</tt> (for queries and also indexes), so for example +resumé+ and +resume+ would match.
132
+
133
+ == Stop Words
134
+
135
+ Stop words (i.e. blacklisted words) can be specified in a YAML config file.
136
+
137
+ To generate a config file with a default set of stop words:
138
+
139
+ $ rails g mongodb_fulltext_search:config
140
+
141
+ The default stop words are:
142
+
143
+ a, an, and, are, as, at, be, but, by, for, if, in, into, is, it, no, not, of, on, or, such, that, the, their, then, there, these, they, this, to, was, will, with
144
+
145
+ == Rake Tasks
146
+
147
+ To rebuild indexes for all models:
148
+
149
+ $ rake db:mongo:rebuild_fulltext_search_indexes
150
+
151
+ To rebuild indexes for one model:
152
+
153
+ $ rake db:mongo:rebuild_fulltext_search_indexes[Widget]
154
+
155
+ NOTE: The indexes are maintained automatically via the +before_save+ and +before_destroy+ callbacks on models, however you MUST run this rake task any time your models change.
156
+
157
+ == To Do
158
+
159
+ * Filtered indexes
160
+ * Shared indexes
161
+
162
+ == Copyright
163
+
164
+ Copyright (c) 2012 Christopher Fuller. See MIT-LICENSE[http://github.com/chrisfuller/mongodb_fulltext_search/blob/master/MIT-LICENSE] for details.
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'MongodbFulltextSearch'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
@@ -0,0 +1,9 @@
1
+ module MongodbFulltextSearch::Generators
2
+ class ConfigGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('../templates', __FILE__)
4
+ desc 'Creates a MongodbFulltextSearch configuration file at config/fulltext_search.yml'
5
+ def create_config_file
6
+ template 'fulltext_search.yml', File.join('config', 'fulltext_search.yml')
7
+ end
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ stop_words: a, an, and, are, as, at, be, but, by, for, if, in, into, is, it, no, not, of, on, or, such, that, the, their, then, there, these, they, this, to, was, will, with
@@ -0,0 +1,12 @@
1
+ module MongodbFulltextSearch
2
+ end
3
+
4
+ require 'mongodb_fulltext_search/mongodb_fulltext_search'
5
+
6
+ module MongodbFulltextSearch
7
+ class Railtie < Rails::Railtie
8
+ rake_tasks do
9
+ load 'tasks/mongodb_fulltext_search_tasks.rake'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,51 @@
1
+ module MongodbFulltextSearch::Helpers
2
+
3
+ def mongo_session(model)
4
+ if mongoid?
5
+ mongo_session = model.mongo_session
6
+ elsif mongomapper?
7
+ mongo_session = MongoMapper.database
8
+ end
9
+ mongo_session
10
+ end
11
+
12
+ def words_for(text)
13
+ words = []
14
+ if text.is_a? String
15
+ text.mb_chars.normalize(:kd).to_s.downcase.gsub(/[^a-z0-9\s]/, '').split(/\s/).each do |word|
16
+ words << word unless word.blank? or stop_words.include? word
17
+ end
18
+ end
19
+ words
20
+ end
21
+
22
+ def mongoid?
23
+ if @mongoid.nil?
24
+ @mongoid = Object.const_defined?('Mongoid')
25
+ end
26
+ @mongoid
27
+ end
28
+
29
+ def mongomapper?
30
+ if @mongomapper.nil?
31
+ @mongomapper = Object.const_defined?('MongoMapper')
32
+ end
33
+ @mongomapper
34
+ end
35
+
36
+ private
37
+
38
+ def stop_words
39
+ if @stop_words.nil?
40
+ file = "#{Rails.root}/config/fulltext_search.yml"
41
+ config = YAML.load(ERB.new(File.read(file)).result) if File.exists? file
42
+ if not config.nil? and config.include? 'stop_words'
43
+ @stop_words = config['stop_words'].gsub(/\s/, '').split ','
44
+ else
45
+ @stop_words = []
46
+ end
47
+ end
48
+ @stop_words
49
+ end
50
+
51
+ end
@@ -0,0 +1,221 @@
1
+ module MongodbFulltextSearch::Mixins
2
+
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+
7
+ cattr_accessor :fulltext_search_options
8
+
9
+ self.fulltext_search_options = {}
10
+
11
+ if MongodbFulltextSearch.mongoid?
12
+ (class << self; self; end).class_eval do
13
+ create_indexes = instance_method :create_indexes
14
+ define_method :create_indexes do
15
+ create_indexes.bind(self).call
16
+ fulltext_search_options.values.each do |options|
17
+ if options[:model].respond_to? :create_indexes
18
+ options[:model].send :create_indexes
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ module ClassMethods
28
+
29
+ def fulltext_search_in(*args)
30
+
31
+ options = args.last.is_a?(Hash) ? args.pop : {}
32
+
33
+ if options.has_key? :index
34
+ collection_name = (options.delete :index).to_sym
35
+ else
36
+ count = fulltext_search_options.count
37
+ collection_name = "fulltext_search_index_#{collection.name}_#{count}".to_sym
38
+ end
39
+
40
+ if args.empty?
41
+ options[:attributes] = [:to_s]
42
+ else
43
+ options[:attributes] = args
44
+ end
45
+
46
+ if MongodbFulltextSearch.mongoid?
47
+ options[:model] = Class.new {
48
+ include Mongoid::Document
49
+ store_in collection: collection_name
50
+ index({
51
+ :source => 1,
52
+ 'counts.word' => 1
53
+ })
54
+ field :source , :type => String
55
+ field :counts , :type => Array
56
+ }
57
+ elsif MongodbFulltextSearch.mongomapper?
58
+ options[:model] = Class.new {
59
+ include MongoMapper::Document
60
+ set_collection_name collection_name
61
+ ensure_index([
62
+ [ :source , 1 ],
63
+ [ 'counts.word' , 1 ]
64
+ ])
65
+ key :source , String
66
+ key :counts , Array
67
+ }
68
+ end
69
+
70
+ fulltext_search_options[collection_name] = options
71
+
72
+ attr_accessor :fulltext_search_score
73
+
74
+ before_save :update_in_fulltext_search_indexes
75
+
76
+ before_destroy :remove_from_fulltext_search_indexes
77
+
78
+ end
79
+
80
+ def fulltext_search(query, options = {})
81
+
82
+ options = {
83
+ :exact => true,
84
+ :limit => 20,
85
+ :offset => 0,
86
+ :return_scores => false
87
+ }.merge options
88
+
89
+ results = []
90
+
91
+ if query.is_a? String
92
+
93
+ words = MongodbFulltextSearch.words_for query
94
+
95
+ unless words.empty?
96
+
97
+ queries = []; words.each do |word|
98
+ if !!options[:exact]
99
+ queries << { 'counts.word' => word }
100
+ else
101
+ queries << { 'counts.word' => { '$regex' => Regexp.escape(word) } }
102
+ end
103
+ end
104
+
105
+ limit = options[:limit].to_i
106
+
107
+ pipeline = [
108
+ { '$match' => { '$and' => queries } },
109
+ { '$unwind' => '$counts' },
110
+ { '$match' => { '$or' => queries } },
111
+ { '$sort' => { 'source' => 1 } },
112
+ { '$group' => {
113
+ '_id' => { 'source' => 1 },
114
+ 'score' => { '$sum' => '$counts.count' }
115
+ } },
116
+ { '$sort' => { 'score' => -1 } },
117
+ { '$limit' => limit }
118
+ ]
119
+
120
+ skip = options[:offset].to_i * limit
121
+
122
+ pipeline << { '$skip' => skip } if skip > 0
123
+ pipeline << { '$project' => { 'score' => 1 } }
124
+
125
+ if options.has_key? :index
126
+ collection_name = options[:index]
127
+ else
128
+ if fulltext_search_options.count == 1
129
+ collection_name = fulltext_search_options.keys.first
130
+ else
131
+ raise ArgumentError, 'index not specified', caller
132
+ end
133
+ end
134
+
135
+ aggregate = MongodbFulltextSearch.mongo_session(self).command(
136
+ :aggregate => collection_name.to_s,
137
+ :pipeline => pipeline
138
+ )
139
+
140
+ scores = {}; aggregate['result'].each do |result|
141
+ scores[result['_id']['source']] = result['score']
142
+ end
143
+
144
+ unless scores.empty?
145
+ if options[:return_scores]
146
+ results = scores
147
+ else
148
+ find(scores.keys).each do |result|
149
+ result.fulltext_search_score = scores[result._id.to_s]
150
+ results << result
151
+ end
152
+ results.sort_by! { |result| -result.fulltext_search_score }
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+
160
+ results
161
+
162
+ end
163
+
164
+ end
165
+
166
+ private
167
+
168
+ def update_in_fulltext_search_indexes
169
+
170
+ fulltext_search_options.each do |collection_name, options|
171
+
172
+ if MongodbFulltextSearch.mongoid?
173
+ index = options[:model].find_or_initialize_by :source => _id.to_s
174
+ elsif MongodbFulltextSearch.mongomapper?
175
+ index = options[:model].find_or_initialize_by_source _id.to_s
176
+ end
177
+
178
+ index.counts = []
179
+
180
+ values = []; options[:attributes].each do |attribute|
181
+ if respond_to? attribute.to_sym
182
+ value_array = send(attribute.to_sym)
183
+ value_array = [value_array] if value_array.is_a? String
184
+ if value_array.is_a? Array
185
+ value_array.each do |value|
186
+ values << value if value.is_a? String
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ unless values.nil?
193
+ temp = {}; values.each do |value|
194
+ MongodbFulltextSearch.words_for(value).each do |word|
195
+ temp[word] = 0 if temp[word].nil?
196
+ temp[word] += 1
197
+ end
198
+ end
199
+ temp.sort.each do |word, count|
200
+ index.counts << { :word => word, :count => count }
201
+ end
202
+ end
203
+
204
+ index.save
205
+
206
+ end
207
+
208
+ end
209
+
210
+ def remove_from_fulltext_search_indexes
211
+ fulltext_search_options.values.each do |options|
212
+ if MongodbFulltextSearch.mongoid?
213
+ index = options[:model].where :source => _id.to_s
214
+ elsif MongodbFulltextSearch.mongomapper?
215
+ index = options[:model].find_by_source _id.to_s
216
+ end
217
+ index.destroy unless index.nil?
218
+ end
219
+ end
220
+
221
+ end
@@ -0,0 +1,12 @@
1
+ require 'mongodb_fulltext_search/helpers'
2
+ require 'mongodb_fulltext_search/mixins'
3
+
4
+ module MongodbFulltextSearch
5
+ extend MongodbFulltextSearch::Helpers
6
+ end
7
+
8
+ if MongodbFulltextSearch.mongoid?
9
+ Mongoid.const_set :FullTextSearch, MongodbFulltextSearch::Mixins
10
+ elsif MongodbFulltextSearch.mongomapper?
11
+ MongoMapper.const_set :FullTextSearch, MongodbFulltextSearch::Mixins
12
+ end
@@ -0,0 +1,3 @@
1
+ module MongodbFulltextSearch
2
+ VERSION = "0.0.14"
3
+ end
@@ -0,0 +1,45 @@
1
+ namespace :db do
2
+ namespace :mongo do
3
+ desc "Rebuild the fulltext search indexes"
4
+ task :rebuild_fulltext_search_indexes, [:model] => [:environment] do |t, args|
5
+ args.with_defaults(:model => false)
6
+ models = []; Dir[Rails.root.to_s + '/app/models/**/*.rb'].each do |path|
7
+ model_name = File.basename(path, '.rb').camelize
8
+ begin
9
+ model_class = model_name.constantize
10
+ if model_class.respond_to? :fulltext_search_options
11
+ models << model_name if model_class.send(:fulltext_search_options).is_a? Hash
12
+ end
13
+ rescue
14
+ end
15
+ end
16
+ error = false
17
+ if args.model
18
+ if models.include? args.model
19
+ models = [args.model]
20
+ else
21
+ models = []
22
+ puts "[Aborted] Unable to load model '#{args.model}'"
23
+ error = true
24
+ end
25
+ end
26
+ if models.empty?
27
+ unless error
28
+ puts "[Aborted] Unable to locate any models with fulltext search implemented"
29
+ end
30
+ else
31
+ models.each do |model_name|
32
+ puts "Recreating fulltext search index for model '#{model_name}' ..."
33
+ model_class = model_name.constantize
34
+ model_class.fulltext_search_options.keys.each do |collection_name|
35
+ MongodbFulltextSearch.mongo_session(model_class)[collection_name.to_s].drop
36
+ end
37
+ model_class.all.each do |model|
38
+ model.send :update_in_fulltext_search_indexes
39
+ end
40
+ end
41
+ puts "Operation completed"
42
+ end
43
+ end
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongodb_fulltext_search_er
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.14
5
+ platform: ruby
6
+ authors:
7
+ - Ryan T Hosford
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Same as mongodb_fulltext_search, except excludes rails as a dependency
14
+ email:
15
+ - tad.hosford@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/mongodb_fulltext_search.rb
21
+ - lib/mongodb_fulltext_search/version.rb
22
+ - lib/mongodb_fulltext_search/mongodb_fulltext_search.rb
23
+ - lib/mongodb_fulltext_search/helpers.rb
24
+ - lib/mongodb_fulltext_search/mixins.rb
25
+ - lib/generators/mongodb_fulltext_search/templates/fulltext_search.yml
26
+ - lib/generators/mongodb_fulltext_search/config_generator.rb
27
+ - lib/tasks/mongodb_fulltext_search_tasks.rake
28
+ - MIT-LICENSE
29
+ - Rakefile
30
+ - README.rdoc
31
+ homepage: http://github.com/chrisfuller/mongodb_fulltext_search
32
+ licenses: []
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.1.11
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Same as mongodb_fulltext_search, except excludes rails as a dependency
54
+ test_files: []