mongoid_search 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4f3da97e4dd6f65400548a1e6f12e957fe0c2d5c8a390e015cd8d20572474c73
4
+ data.tar.gz: 71423ee292ac1f9e53cb4ef4171d4309c25ca56895203bd53c6bc2d8641fb385
5
+ SHA512:
6
+ metadata.gz: 59b2511bea3017819e52c2633e8ac8cc19158e7da43042584b9fccc4f815b2a2e57fa4b1407e2ee5726d36710473f0cc9143b7b750586c802658869a0c6f7c23
7
+ data.tar.gz: 65bfb18a5361a333d815b92bafc34bbe5133c04b28c9e4540397605f98eb76123338119b2b5ee911b5074e85d68d84cc6547ab059845bd4698eb0b5beaaf2f02
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Mauricio Zaffari
1
+ Copyright (c) 2009 Mauricio Zaffari, 2013 Semenyuk Dmitriy (additions)
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,168 +1,231 @@
1
- Mongoid Search
2
- ============
1
+ # Mongoid Search
3
2
 
4
- Mongoid Search is a simple full text search implementation for Mongoid ORM. It performs well for small data sets. If your searchable model is big (i.e. 1.000.000+ records), solr or sphinx may suit you better.
3
+ Mongoid Search is a simple full text search implementation for Mongoid ORM. It supports Mongoid 3, 4, 5 and 6 and performs well for small data sets. If your searchable model is big (i.e. 1.000.000+ records), [mongoid_fulltext](https://github.com/mongoid/mongoid_fulltext), ElasticSearch, Solr or Sphinx may suit you better.
5
4
 
6
- Installation
7
- --------
5
+ [![Build Status](https://travis-ci.org/mongoid/mongoid_search.svg?branch=master)](https://travis-ci.org/mongoid/mongoid_search)
8
6
 
9
- In your Gemfile:
10
-
11
- gem 'mongoid_search'
7
+ ## Installation
12
8
 
13
- If your project is still using mongoid 2.x.x, stick to mongoid_search 0.2.x:
9
+ In your Gemfile:
14
10
 
15
- gem 'mongoid_search', '~> 0.2.8'
11
+ ```ruby
12
+ gem 'mongoid_search'
13
+ ```
16
14
 
17
15
  Then:
18
16
 
19
- bundle install
20
-
21
- Examples
22
- --------
23
-
24
- class Product
25
- include Mongoid::Document
26
- include Mongoid::Search
27
- field :brand
28
- field :name
29
-
30
- has_many :tags
31
- belongs_to :category
32
-
33
- search_in :brand, :name, :tags => :name, :category => :name
34
- end
35
-
36
- class Tag
37
- include Mongoid::Document
38
- field :name
17
+ ```
18
+ bundle install
19
+ ```
20
+
21
+ ## Examples
22
+
23
+ ```ruby
24
+ class Product
25
+ include Mongoid::Document
26
+ include Mongoid::Search
27
+ field :brand
28
+ field :name
29
+ field :unit
30
+ field :info, type: Hash
31
+
32
+ has_many :tags
33
+ belongs_to :category
34
+
35
+ search_in :brand, :name, tags: :name, category: :name, info: %i[summary description]
36
+ search_in :unit, index: :_unit_keywords
37
+ end
38
+
39
+ class Tag
40
+ include Mongoid::Document
41
+ field :name
42
+
43
+ belongs_to :product
44
+ end
45
+
46
+ class Category
47
+ include Mongoid::Document
48
+ field :name
49
+
50
+ has_many :products
51
+ end
52
+ ```
53
+
54
+ Now when you save a product, you get a `_keywords` field automatically:
55
+
56
+ ```ruby
57
+ p = Product.new brand: 'Apple', name: 'iPhone', unit: 'kilogram', info: { summary: 'Info-summary', description: 'Info-description' }
58
+ p.tags << Tag.new(name: 'Amazing')
59
+ p.tags << Tag.new(name: 'Awesome')
60
+ p.tags << Tag.new(name: 'Superb')
61
+ p.save
62
+ # => true
63
+ p._keywords
64
+ # => ["amazing", "apple", "awesome", "iphone", "superb", "Info-summary", "Info-description"]
65
+ p._unit_keywords
66
+ # => ["kilogram"]
67
+ ```
68
+
69
+ Now you can run search, which will look in the `_keywords` field and return all matching results:
70
+
71
+ ```ruby
72
+ Product.full_text_search("apple iphone").size
73
+ # => 1
74
+ ```
75
+
76
+ You can also search in "virtual" fields by defining them as methods. This can be useful when you have a method with dynamic fields (i.e. variable schema).
77
+ ```ruby
78
+ class ModelWithDynamicFields
79
+
80
+ ...
81
+
82
+ search_in :search_data
83
+
84
+ def search_data
85
+ # concatenate all String fields' values
86
+ self.attributes.select{|k,v| v.is_a?(String) }.values.join(' ')
87
+ end
88
+ end
89
+ ```
90
+ Mongoid_search will run the method before save and use it's output to populate the `_keywords` field.
91
+
92
+ Of course, some models could have more than one index. For instance, two different searches with different fields, so you could even specify from which index should be searched:
93
+
94
+ ```ruby
95
+ Product.full_text_search("kilogram", index: :_unit_keywords).size
96
+ # => 1
97
+ ```
39
98
 
40
- belongs_to :product
41
- end
42
-
43
- class Category
44
- include Mongoid::Document
45
- field :name
46
-
47
- has_many :products
48
- end
49
-
50
- Now when you save a product, you get a _keywords field automatically:
51
-
52
- p = Product.new :brand => "Apple", :name => "iPhone"
53
- p.tags << Tag.new(:name => "Amazing")
54
- p.tags << Tag.new(:name => "Awesome")
55
- p.tags << Tag.new(:name => "Superb")
56
- p.save
57
- => true
58
- p._keywords
59
- => ["amazing", "apple", "awesome", "iphone", "superb"]
99
+ Note that the search is case insensitive, and accept partial searching too:
60
100
 
61
- Now you can run search, which will look in the _keywords field and return all matching results:
101
+ ```ruby
102
+ Product.full_text_search('ipho').size
103
+ # => 1
104
+ ```
62
105
 
63
- Product.full_text_search("apple iphone").size
64
- => 1
106
+ Assuming you have a category with multiple products you can use the following code to search for 'iphone' in products cheaper than $499.
65
107
 
66
- Note that the search is case insensitive, and accept partial searching too:
108
+ ```ruby
109
+ category.products.where(:price.lt => 499).full_text_search('iphone').asc(:price)
110
+ ```
67
111
 
68
- Product.full_text_search("ipho").size
69
- => 1
112
+ To index or reindex all existing records, run this rake task
70
113
 
71
- Assuming you have a category with multiple products you can use the following
72
- code to search for 'iphone' in products cheaper than $499
114
+ ```
115
+ $ rake mongoid_search:index
116
+ ```
73
117
 
74
- @category.products.where(:price.lt => 499).full_text_search('iphone').asc(:price)
118
+ ## Options
75
119
 
120
+ ### match
76
121
 
77
- Options
78
- -------
122
+ * `:any` - match any occurrence
123
+ * `:all` - match all occurrences
79
124
 
80
- match:
125
+ Default is `:any`.
81
126
 
82
- _:any_ - match any occurrence
127
+ ```ruby
128
+ Product.full_text_search('apple motorola', match: :any).size
129
+ # => 1
83
130
 
84
- _:all_ - match all ocurrences
131
+ Product.full_text_search('apple motorola', match: :all).size
132
+ # => 0
133
+ ```
85
134
 
86
- Default is _:any_.
135
+ ### allow\_empty\_search
87
136
 
88
- Product.full_text_search("apple motorola", match: :any).size
89
- => 1
137
+ * `true` - will return `Model.all`
138
+ * `false` - will return `[]`
90
139
 
91
- Product.full_text_search("apple motorola", match: :all).size
92
- => 0
140
+ Default is `false`.
93
141
 
94
- allow\_empty\_search:
142
+ ```ruby
143
+ Product.full_text_search('', allow_empty_search: true).size
144
+ # => 1
145
+ ```
95
146
 
96
- _true_ - will return Model.all
147
+ ### relevant_search
97
148
 
98
- _false_ - will return []
149
+ * `true` - adds relevance information to the results
150
+ * `false` - no relevance information
99
151
 
100
- Default is _false_.
152
+ Default is `false`.
101
153
 
102
- Product.full_text_search("", allow_empty_search: true).size
103
- => 1
154
+ ```ruby
155
+ Product.full_text_search('amazing apple', relevant_search: true)
156
+ # => [#<Product _id: 5016e7d16af54efe1c000001, _type: nil, brand: "Apple", name: "iPhone", attrs: nil, info: nil, category_id: nil, _keywords: ["amazing", "apple", "awesome", "iphone", "superb"], relevance: 2.0>]
157
+ ```
104
158
 
105
- relevant_search:
159
+ Please note that relevant_search will return an Array and not a Criteria object. The search method should always be called in the end of the method chain.
106
160
 
107
- _true_ - Adds relevance information to the results
161
+ ### index
108
162
 
109
- _false_ - No relevance information
163
+ Default is `_keywords`.
110
164
 
111
- Default is _false_.
165
+ ```ruby
166
+ Product.full_text_search('amazing apple', index: :_keywords)
167
+ # => [#<Product _id: 5016e7d16af54efe1c000001, _type: nil, brand: "Apple", name: "iPhone", unit: "l", attrs: nil, info: nil, category_id: nil, _keywords: ["amazing", "apple", "awesome", "iphone", "superb"], _unit_keywords: ["l"], relevance: 2.0>]
112
168
 
113
- Product.full_text_search('amazing apple', relevant_search: true)
114
- => [#<Product _id: 5016e7d16af54efe1c000001, _type: nil, brand: "Apple", name: "iPhone", attrs: nil, info: nil, category_id: nil, _keywords: ["amazing", "apple", "awesome", "iphone", "superb"], relevance: 2.0>]
169
+ Product.full_text_search('kg', index: :_unit_keywords)
170
+ # => [#<Product _id: 5016e7d16af54efe1c000001, _type: nil, brand: "Apple", name: "iPhone", unit: "kg", attrs: nil, info: nil, category_id: nil, _keywords: ["amazing", "apple", "awesome", "iphone", "superb"], _unit_keywords: ["kg"], relevance: 2.0>]
171
+ ```
115
172
 
116
- Please note that relevant_search will return an Array and not a Criteria object. The search method shoud always be called in the end of the method chain.
173
+ index enables to have two or more different searches, with different or same fields. It should be noted that indexes are exclusive per each one.
117
174
 
118
- Initializer
119
- -----------
175
+ ## Initializer
120
176
 
121
177
  Alternatively, you can create an initializer to setup those options:
122
178
 
123
- Mongoid::Search.setup do |config|
124
- ## Default matching type. Match :any or :all searched keywords
125
- config.match = :any
179
+ ```ruby
180
+ Mongoid::Search.setup do |config|
181
+ ## Default matching type. Match :any or :all searched keywords
182
+ config.match = :any
183
+
184
+ ## If true, an empty search will return all objects
185
+ config.allow_empty_search = false
126
186
 
127
- ## If true, an empty search will return all objects
128
- config.allow_empty_search = false
187
+ ## If true, will search with relevance information
188
+ config.relevant_search = false
129
189
 
130
- ## If true, will search with relevance information
131
- config.relevant_search = false
190
+ ## Stem keywords
191
+ config.stem_keywords = false
132
192
 
133
- ## Stem keywords
134
- config.stem_keywords = false
193
+ ## Add a custom proc returning strings to replace the default stemmer
194
+ # For example using ruby-stemmer:
195
+ # config.stem_proc = Proc.new { |word| Lingua.stemmer(word, :language => 'nl') }
135
196
 
136
- ## Add a custom proc returning strings to replace the default stemmer
137
- # For example using ruby-stemmer:
138
- # config.stem_proc = Proc.new { |word| Lingua.stemmer(word, :language => 'nl') }
197
+ ## Words to ignore
198
+ config.ignore_list = []
139
199
 
140
- ## Words to ignore
141
- config.ignore_list = []
200
+ ## An array of words
201
+ # config.ignore_list = %w{ a an to from as }
142
202
 
143
- ## An array of words
144
- # config.ignore_list = %w{ a an to from as }
203
+ ## Or from a file
204
+ # config.ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
145
205
 
146
- ## Or from a file
147
- # config.ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
206
+ ## Search using regex (slower)
207
+ config.regex_search = true
148
208
 
149
- ## Search using regex (slower)
150
- config.regex_search = true
209
+ ## Regex to search
151
210
 
152
- ## Regex to search
211
+ ## Match partial words on both sides (slower)
212
+ config.regex = Proc.new { |query| /#{query}/ }
153
213
 
154
- ## Match partial words on both sides (slower)
155
- config.regex = Proc.new { |query| /#{query}/ }
214
+ ## Match partial words on the beginning or in the end (slightly faster)
215
+ # config.regex = Proc.new { |query| /^#{query}/ }
216
+ # config.regex = Proc.new { |query| /#{query}$/ }
156
217
 
157
- ## Match partial words on the beginning or in the end (slightly faster)
158
- # config.regex = Proc.new { |query| /ˆ#{query}/ }
159
- # config.regex = Proc.new { |query| /#{query}$/ }
218
+ # Ligatures to be replaced
219
+ # http://en.wikipedia.org/wiki/Typographic_ligature
220
+ config.ligatures = { "œ"=>"oe", "æ"=>"ae" }
160
221
 
161
- # Ligatures to be replaced
162
- # http://en.wikipedia.org/wiki/Typographic_ligature
163
- config.ligatures = { "œ"=>"oe", "æ"=>"ae" }
222
+ # Strip symbols regex to be replaced. These symbols will be replaced by space
223
+ config.strip_symbols = /[._:;'\"`,?|+={}()!@#%^&*<>~\$\-\\\/\[\]]/
164
224
 
165
- # Minimum word size. Words smaller than it won't be indexed
166
- config.minimum_word_size = 2
167
- end
225
+ # Strip accents regex to be replaced. These sybols will be removed after strip_symbols replacing
226
+ config.strip_accents = /[^\s\p{Alnum}]/
168
227
 
228
+ # Minimum word size. Words smaller than it won't be indexed
229
+ config.minimum_word_size = 2
230
+ end
231
+ ```
data/Rakefile CHANGED
@@ -3,8 +3,11 @@ require 'rake'
3
3
 
4
4
  require 'rspec/core/rake_task'
5
5
  RSpec::Core::RakeTask.new(:spec) do |spec|
6
- spec.rspec_opts = ["-c", "-f progress"]
6
+ spec.rspec_opts = ['-c', '-f progress']
7
7
  spec.pattern = 'spec/**/*_spec.rb'
8
8
  end
9
9
 
10
- task :default => :spec
10
+ require 'rubocop/rake_task'
11
+ RuboCop::RakeTask.new(:rubocop)
12
+
13
+ task default: %i[rubocop spec]
@@ -1,7 +1,6 @@
1
- # encoding: utf-8
1
+ require 'mongoid_search/mongoid_search'
2
2
 
3
3
  require 'mongoid_search/railtie' if defined?(Rails)
4
- require 'mongoid_search/mongoid_search'
5
4
 
6
5
  module Mongoid::Search
7
6
  ## Default matching type. Match :any or :all searched keywords
@@ -22,7 +21,7 @@ module Mongoid::Search
22
21
 
23
22
  ## Stem procedure
24
23
  mattr_accessor :stem_proc
25
- @@stem_proc = Proc.new { |word| word.stem }
24
+ @@stem_proc = proc { |word| word.stem }
26
25
 
27
26
  ## Words to ignore
28
27
  mattr_accessor :ignore_list
@@ -42,25 +41,33 @@ module Mongoid::Search
42
41
  mattr_accessor :regex
43
42
 
44
43
  ## Match partial words on both sides (slower)
45
- @@regex = Proc.new { |query| /#{query}/ }
44
+ @@regex = proc { |query| /#{query}/ }
46
45
 
47
46
  ## Match partial words on the beginning or in the end (slightly faster)
48
- # @@regex = Proc.new { |query| /ˆ#{query}/ }
47
+ # @@regex = Proc.new { |query| /^#{query}/ }
49
48
  # @@regex = Proc.new { |query| /#{query}$/ }
50
49
 
51
50
  # Ligatures to be replaced
52
51
  # http://en.wikipedia.org/wiki/Typographic_ligature
53
52
  mattr_accessor :ligatures
54
- @@ligatures = { "œ"=>"oe", "æ"=>"ae" }
53
+ @@ligatures = { 'œ' => 'oe', 'æ' => 'ae', 'ꜵ' => 'ao' }
55
54
 
56
55
  # Minimum word size. Words smaller than it won't be indexed
57
56
  mattr_accessor :minimum_word_size
58
57
  @@minimum_word_size = 2
59
58
 
59
+ # Strip special symbols
60
+ mattr_accessor :strip_symbols
61
+ @@strip_symbols = /[._:;'\"`,?|+={}()!@#%^&*<>~\$\-\\\/\[\]]/
62
+
63
+ # Strip accents
64
+ mattr_accessor :strip_accents
65
+ @@strip_accents = /[^\s\p{Alnum}]/
66
+
60
67
  def self.setup
61
68
  yield self
62
69
  end
63
70
  end
64
71
 
65
72
  require 'mongoid_search/util'
66
- require 'mongoid_search/log'
73
+ require 'mongoid_search/log'
@@ -14,7 +14,8 @@ class Mongoid::Search::Log
14
14
  end
15
15
 
16
16
  private
17
+
17
18
  def self.colorize(text, code)
18
19
  "\033[#{code}m#{text}\033[0m"
19
20
  end
20
- end
21
+ end
@@ -1,11 +1,10 @@
1
1
  module Mongoid::Search
2
- def self.included(base)
3
- base.send(:cattr_accessor, :search_fields)
4
-
5
- base.extend ClassMethods
2
+ extend ActiveSupport::Concern
6
3
 
4
+ included do
5
+ cattr_accessor :search_fields
7
6
  @@classes ||= []
8
- @@classes << base
7
+ @@classes << self
9
8
  end
10
9
 
11
10
  def self.classes
@@ -16,17 +15,19 @@ module Mongoid::Search
16
15
  # Set a field or a number of fields as sources for search
17
16
  def search_in(*args)
18
17
  args, options = args_and_options(args)
19
- self.search_fields = (self.search_fields || []).concat args
18
+ set_search_fields(options[:index], args)
20
19
 
21
- field :_keywords, :type => Array
20
+ field options[:index], type: Array
22
21
 
23
- index({ :_keywords => 1 }, { :background => true })
22
+ index({ options[:index] => 1 }, background: true)
24
23
 
25
24
  before_save :set_keywords
26
25
  end
27
26
 
28
- def full_text_search(query, options={})
27
+ def full_text_search(query, options = {})
29
28
  options = extract_options(options)
29
+ attr_accessor :relevance if options[:relevant_search].eql? true
30
+
30
31
  return (options[:allow_empty_search] ? criteria.all : []) if query.blank?
31
32
 
32
33
  if options[:relevant_search]
@@ -43,33 +44,46 @@ module Mongoid::Search
43
44
  # Goes through all documents in the class that includes Mongoid::Search
44
45
  # and indexes the keywords.
45
46
  def index_keywords!
46
- all.each { |d| d.index_keywords! ? Log.green(".") : Log.red("F") }
47
+ all.each { |d| d.index_keywords! ? Log.green('.') : Log.red('F') }
47
48
  end
48
49
 
49
50
  private
51
+
52
+ def set_search_fields(index, fields)
53
+ self.search_fields ||= {}
54
+
55
+ (self.search_fields[index] ||= []).concat fields
56
+ end
57
+
50
58
  def query(keywords, options)
51
59
  keywords_hash = keywords.map do |kw|
52
- kw = Mongoid::Search.regex.call(kw) if Mongoid::Search.regex_search
53
- { :_keywords => kw }
60
+ if Mongoid::Search.regex_search
61
+ escaped_kw = Regexp.escape(kw)
62
+ kw = Mongoid::Search.regex.call(escaped_kw)
63
+ end
64
+
65
+ { options[:index] => kw }
54
66
  end
55
67
 
56
- criteria.send("#{(options[:match]).to_s}_of", *keywords_hash)
68
+ criteria.send("#{(options[:match])}_of", *keywords_hash)
57
69
  end
58
70
 
59
71
  def args_and_options(args)
60
72
  options = args.last.is_a?(Hash) &&
61
- [:match,
62
- :allow_empty_search,
63
- :relevant_search].include?(args.last.keys.first) ? args.pop : {}
73
+ %i[match
74
+ allow_empty_search
75
+ index
76
+ relevant_search].include?(args.last.keys.first) ? args.pop : {}
64
77
 
65
78
  [args, extract_options(options)]
66
79
  end
67
80
 
68
81
  def extract_options(options)
69
82
  {
70
- :match => options[:match] || Mongoid::Search.match,
71
- :allow_empty_search => options[:allow_empty_search] || Mongoid::Search.allow_empty_search,
72
- :relevant_search => options[:relevant_search] || Mongoid::Search.relevant_search
83
+ match: options[:match] || Mongoid::Search.match,
84
+ allow_empty_search: options[:allow_empty_search] || Mongoid::Search.allow_empty_search,
85
+ relevant_search: options[:relevant_search] || Mongoid::Search.relevant_search,
86
+ index: options[:index] || :_keywords
73
87
  }
74
88
  end
75
89
 
@@ -79,52 +93,57 @@ module Mongoid::Search
79
93
 
80
94
  def search_relevant(query, options)
81
95
  results_with_relevance(query, options).sort { |o| o['value'] }.map do |r|
82
-
83
- new(r['_id'].merge(:relevance => r['value'])) do |o|
96
+ new(r['_id'].merge(relevance: r['value'])) do |o|
84
97
  # Need to match the actual object
85
98
  o.instance_variable_set('@new_record', false)
86
99
  o._id = r['_id']['_id']
87
100
  end
88
-
89
101
  end
90
102
  end
91
103
 
92
104
  def results_with_relevance(query, options)
93
105
  keywords = Mongoid::Search::Util.normalize_keywords(query)
94
106
 
95
- map = %Q{
96
- function() {
97
- var entries = 0;
98
- for(i in keywords) {
99
- for(j in this._keywords) {
100
- if(this._keywords[j] == keywords[i]) {
101
- entries++;
107
+ map = %{
108
+ function() {
109
+ var entries = 0;
110
+ for(i in keywords) {
111
+ for(j in this.#{options[:index]}) {
112
+ if(this.#{options[:index]}[j] == keywords[i]) {
113
+ entries++;
114
+ }
102
115
  }
103
116
  }
117
+ if(entries > 0) {
118
+ emit(this, entries);
119
+ }
104
120
  }
105
- if(entries > 0) {
106
- emit(this, entries);
107
- }
108
- }
109
121
  }
110
122
 
111
- reduce = %Q{
112
- function(key, values) {
113
- return(values);
114
- }
123
+ reduce = %{
124
+ function(key, values) {
125
+ return(values);
126
+ }
115
127
  }
116
128
 
117
- query(keywords, options).map_reduce(map, reduce).scope(:keywords => keywords).out(:inline => 1)
129
+ query(keywords, options).map_reduce(map, reduce).scope(keywords: keywords).out(inline: 1)
118
130
  end
119
131
  end
120
132
 
121
133
  def index_keywords!
122
- update_attribute(:_keywords, set_keywords)
134
+ search_fields.map do |index, fields|
135
+ update_attribute(index, get_keywords(fields))
136
+ end
123
137
  end
124
138
 
125
- private
126
139
  def set_keywords
127
- self._keywords = Mongoid::Search::Util.keywords(self, self.search_fields).
128
- flatten.reject{|k| k.nil? || k.empty?}.uniq.sort
140
+ search_fields.each do |index, fields|
141
+ send("#{index}=", get_keywords(fields))
142
+ end
143
+ end
144
+
145
+ def get_keywords(fields)
146
+ Mongoid::Search::Util.keywords(self, fields)
147
+ .flatten.reject { |k| k.nil? || k.empty? }.uniq.sort
129
148
  end
130
149
  end