mongoid_search 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +86 -43
- data/lib/mongoid_search/log.rb +1 -1
- data/lib/mongoid_search/mongoid_search.rb +82 -62
- data/lib/mongoid_search/util.rb +13 -11
- data/lib/mongoid_search.rb +60 -2
- data/spec/models/category.rb +1 -1
- data/spec/models/product.rb +3 -3
- data/spec/models/subproduct.rb +1 -1
- data/spec/models/tag.rb +1 -1
- data/spec/mongoid_search_spec.rb +74 -40
- data/spec/spec_helper.rb +1 -2
- data/spec/util_spec.rb +22 -14
- metadata +14 -25
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Mongoid Search
|
2
2
|
============
|
3
3
|
|
4
|
-
Mongoid Search is a simple full text search implementation for Mongoid ORM.
|
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.
|
5
5
|
|
6
6
|
Installation
|
7
7
|
--------
|
@@ -10,6 +10,10 @@ In your Gemfile:
|
|
10
10
|
|
11
11
|
gem 'mongoid_search'
|
12
12
|
|
13
|
+
If your project is still using mongoid 2.x.x, stick to mongoid_search 0.2.x:
|
14
|
+
|
15
|
+
gem 'mongoid_search', '~> 0.2.8'
|
16
|
+
|
13
17
|
Then:
|
14
18
|
|
15
19
|
bundle install
|
@@ -23,8 +27,8 @@ Examples
|
|
23
27
|
field :brand
|
24
28
|
field :name
|
25
29
|
|
26
|
-
|
27
|
-
|
30
|
+
has_many :tags
|
31
|
+
belongs_to :category
|
28
32
|
|
29
33
|
search_in :brand, :name, :tags => :name, :category => :name
|
30
34
|
end
|
@@ -33,14 +37,14 @@ Examples
|
|
33
37
|
include Mongoid::Document
|
34
38
|
field :name
|
35
39
|
|
36
|
-
|
40
|
+
belongs_to :product
|
37
41
|
end
|
38
42
|
|
39
43
|
class Category
|
40
44
|
include Mongoid::Document
|
41
45
|
field :name
|
42
46
|
|
43
|
-
|
47
|
+
has_many :products
|
44
48
|
end
|
45
49
|
|
46
50
|
Now when you save a product, you get a _keywords field automatically:
|
@@ -52,70 +56,109 @@ Now when you save a product, you get a _keywords field automatically:
|
|
52
56
|
p.save
|
53
57
|
=> true
|
54
58
|
p._keywords
|
59
|
+
=> ["amazing", "apple", "awesome", "iphone", "superb"]
|
55
60
|
|
56
61
|
Now you can run search, which will look in the _keywords field and return all matching results:
|
57
62
|
|
58
|
-
Product.
|
63
|
+
Product.full_text_search("apple iphone").size
|
59
64
|
=> 1
|
60
65
|
|
61
66
|
Note that the search is case insensitive, and accept partial searching too:
|
62
67
|
|
63
|
-
Product.
|
68
|
+
Product.full_text_search("ipho").size
|
64
69
|
=> 1
|
65
|
-
|
66
|
-
Assuming you have a category with multiple products you can now use the following
|
67
|
-
code to search for 'iphone' in products cheaper than $499
|
68
70
|
|
69
|
-
|
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
|
70
73
|
|
71
|
-
|
72
|
-
Mongoid defines it's own Criteria.search method.
|
74
|
+
@category.products.where(:price.lt => 499).full_text_search('iphone').asc(:price)
|
73
75
|
|
74
76
|
|
75
77
|
Options
|
76
78
|
-------
|
77
79
|
|
78
80
|
match:
|
81
|
+
|
79
82
|
_:any_ - match any occurrence
|
83
|
+
|
80
84
|
_:all_ - match all ocurrences
|
85
|
+
|
81
86
|
Default is _:any_.
|
82
87
|
|
83
|
-
|
88
|
+
Product.full_text_search("apple motorola", match: :any).size
|
89
|
+
=> 1
|
84
90
|
|
85
|
-
Product.
|
91
|
+
Product.full_text_search("apple motorola", match: :all).size
|
92
|
+
=> 0
|
93
|
+
|
94
|
+
allow\_empty\_search:
|
95
|
+
|
96
|
+
_true_ - will return Model.all
|
97
|
+
|
98
|
+
_false_ - will return []
|
99
|
+
|
100
|
+
Default is _false_.
|
101
|
+
|
102
|
+
Product.full_text_search("", allow_empty_search: true).size
|
86
103
|
=> 1
|
87
104
|
|
88
|
-
|
105
|
+
relevant_search:
|
89
106
|
|
90
|
-
|
91
|
-
|
107
|
+
_true_ - Adds relevance information to the results
|
108
|
+
|
109
|
+
_false_ - No relevance information
|
92
110
|
|
93
|
-
allow_empty_search:
|
94
|
-
_true_ - match any occurrence
|
95
|
-
_false_ - match all ocurrences
|
96
111
|
Default is _false_.
|
97
112
|
|
98
|
-
|
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>]
|
99
115
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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.
|
117
|
+
|
118
|
+
Initializer
|
119
|
+
-----------
|
120
|
+
|
121
|
+
Alternatively, you can create an initializer to setup those options:
|
122
|
+
|
123
|
+
Mongoid::Search.setup do |config|
|
124
|
+
## Default matching type. Match :any or :all searched keywords
|
125
|
+
config.match = :any
|
126
|
+
|
127
|
+
## If true, an empty search will return all objects
|
128
|
+
config.allow_empty_search = false
|
129
|
+
|
130
|
+
## If true, will search with relevance information
|
131
|
+
config.relevant_search = false
|
132
|
+
|
133
|
+
## Stem keywords
|
134
|
+
config.stem_keywords = false
|
135
|
+
|
136
|
+
## Words to ignore
|
137
|
+
config.ignore_list = []
|
138
|
+
|
139
|
+
## An array of words
|
140
|
+
# config.ignore_list = %w{ a an to from as }
|
141
|
+
|
142
|
+
## Or from a file
|
143
|
+
# config.ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
|
144
|
+
|
145
|
+
## Search using regex (slower)
|
146
|
+
config.regex_search = true
|
147
|
+
|
148
|
+
## Regex to search
|
149
|
+
|
150
|
+
## Match partial words on both sides (slower)
|
151
|
+
config.regex = Proc.new { |query| /#{query}/ }
|
152
|
+
|
153
|
+
## Match partial words on the beginning or in the end (slightly faster)
|
154
|
+
# config.regex = Proc.new { |query| /ˆ#{query}/ }
|
155
|
+
# config.regex = Proc.new { |query| /#{query}$/ }
|
156
|
+
|
157
|
+
# Ligatures to be replaced
|
158
|
+
# http://en.wikipedia.org/wiki/Typographic_ligature
|
159
|
+
config.ligatures = { "œ"=>"oe", "æ"=>"ae" }
|
160
|
+
|
161
|
+
# Minimum word size. Words smaller than it won't be indexed
|
162
|
+
config.minimum_word_size = 2
|
163
|
+
end
|
121
164
|
|
data/lib/mongoid_search/log.rb
CHANGED
@@ -1,100 +1,120 @@
|
|
1
1
|
module Mongoid::Search
|
2
|
-
|
2
|
+
def self.included(base)
|
3
|
+
base.send(:cattr_accessor, :search_fields)
|
3
4
|
|
4
|
-
|
5
|
-
cattr_accessor :search_fields, :match, :allow_empty_search, :relevant_search, :stem_keywords, :ignore_list
|
6
|
-
end
|
5
|
+
base.extend ClassMethods
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
@classes << base
|
7
|
+
@@classes ||= []
|
8
|
+
@@classes << base
|
11
9
|
end
|
12
10
|
|
13
11
|
def self.classes
|
14
|
-
|
12
|
+
@@classes
|
15
13
|
end
|
16
14
|
|
17
15
|
module ClassMethods #:nodoc:
|
18
16
|
# Set a field or a number of fields as sources for search
|
19
17
|
def search_in(*args)
|
20
|
-
|
21
|
-
self.
|
22
|
-
self.allow_empty_search = [true, false].include?(options[:allow_empty_search]) ? options[:allow_empty_search] : false
|
23
|
-
self.relevant_search = [true, false].include?(options[:relevant_search]) ? options[:allow_empty_search] : false
|
24
|
-
self.stem_keywords = [true, false].include?(options[:stem_keywords]) ? options[:allow_empty_search] : false
|
25
|
-
self.ignore_list = YAML.load(File.open(options[:ignore_list]))["ignorelist"] if options[:ignore_list].present?
|
26
|
-
self.search_fields = (self.search_fields || []).concat args
|
18
|
+
args, options = args_and_options(args)
|
19
|
+
self.search_fields = (self.search_fields || []).concat args
|
27
20
|
|
28
21
|
field :_keywords, :type => Array
|
29
|
-
|
30
|
-
|
22
|
+
|
23
|
+
index({ :_keywords => 1 }, { :background => true })
|
31
24
|
|
32
25
|
before_save :set_keywords
|
33
26
|
end
|
34
27
|
|
35
|
-
def
|
36
|
-
|
28
|
+
def full_text_search(query, options={})
|
29
|
+
options = extract_options(options)
|
30
|
+
return (options[:allow_empty_search] ? criteria.all : []) if query.blank?
|
31
|
+
|
32
|
+
if options[:relevant_search]
|
37
33
|
search_relevant(query, options)
|
38
34
|
else
|
39
35
|
search_without_relevance(query, options)
|
40
36
|
end
|
41
37
|
end
|
42
38
|
|
43
|
-
#
|
44
|
-
|
45
|
-
alias
|
39
|
+
# Keeping these aliases for compatibility purposes
|
40
|
+
alias csearch full_text_search
|
41
|
+
alias search full_text_search
|
46
42
|
|
47
|
-
|
48
|
-
|
49
|
-
|
43
|
+
# Goes through all documents in the class that includes Mongoid::Search
|
44
|
+
# and indexes the keywords.
|
45
|
+
def index_keywords!
|
46
|
+
all.each { |d| d.index_keywords! ? Log.green(".") : Log.red("F") }
|
50
47
|
end
|
51
48
|
|
52
|
-
|
53
|
-
|
49
|
+
private
|
50
|
+
def query(keywords, options)
|
51
|
+
keywords_hash = keywords.map do |kw|
|
52
|
+
kw = Mongoid::Search.regex.call(kw) if Mongoid::Search.regex_search
|
53
|
+
{ :_keywords => kw }
|
54
|
+
end
|
54
55
|
|
55
|
-
|
56
|
+
criteria.send("#{(options[:match]).to_s}_of", *keywords_hash)
|
57
|
+
end
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
if(this._keywords[j] == keywords[i])
|
63
|
-
entries++
|
64
|
-
}
|
65
|
-
if(entries > 0)
|
66
|
-
emit(this._id, entries)
|
67
|
-
}
|
68
|
-
EOS
|
69
|
-
reduce = <<-EOS
|
70
|
-
function(key, values) {
|
71
|
-
return(values[0])
|
72
|
-
}
|
73
|
-
EOS
|
59
|
+
def args_and_options(args)
|
60
|
+
options = args.last.is_a?(Hash) &&
|
61
|
+
[:match,
|
62
|
+
:allow_empty_search,
|
63
|
+
:relevant_search].include?(args.last.keys.first) ? args.pop : {}
|
74
64
|
|
75
|
-
|
65
|
+
[args, extract_options(options)]
|
66
|
+
end
|
76
67
|
|
77
|
-
|
78
|
-
|
79
|
-
|
68
|
+
def extract_options(options)
|
69
|
+
{
|
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
|
73
|
+
}
|
74
|
+
end
|
80
75
|
|
81
|
-
|
76
|
+
def search_without_relevance(query, options)
|
77
|
+
query(Util.normalize_keywords(query), options)
|
78
|
+
end
|
82
79
|
|
83
|
-
|
80
|
+
def search_relevant(query, options)
|
81
|
+
results_with_relevance(query, options).sort { |o| o['value'] }.map do |r|
|
84
82
|
|
85
|
-
|
86
|
-
|
87
|
-
|
83
|
+
new(r['_id'].merge(:relevance => r['value'])) do |o|
|
84
|
+
# Need to match the actual object
|
85
|
+
o.instance_variable_set('@new_record', false)
|
86
|
+
o._id = r['_id']['_id']
|
87
|
+
end
|
88
88
|
|
89
|
-
|
90
|
-
# res.find.sort(['value', -1]) # Cursor
|
91
|
-
collection.map_reduce(map, reduce, options)
|
89
|
+
end
|
92
90
|
end
|
93
91
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
92
|
+
def results_with_relevance(query, options)
|
93
|
+
keywords = Mongoid::Search::Util.normalize_keywords(query)
|
94
|
+
|
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++;
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
105
|
+
if(entries > 0) {
|
106
|
+
emit(this, entries);
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
reduce = %Q{
|
112
|
+
function(key, values) {
|
113
|
+
return(values);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
query(keywords, options).map_reduce(map, reduce).scope(:keywords => keywords).out(:inline => 1)
|
98
118
|
end
|
99
119
|
end
|
100
120
|
|
@@ -105,7 +125,7 @@ module Mongoid::Search
|
|
105
125
|
private
|
106
126
|
def set_keywords
|
107
127
|
self._keywords = self.search_fields.map do |field|
|
108
|
-
Util.keywords(self, field
|
128
|
+
Mongoid::Search::Util.keywords(self, field)
|
109
129
|
end.flatten.reject{|k| k.nil? || k.empty?}.uniq.sort
|
110
130
|
end
|
111
131
|
end
|
data/lib/mongoid_search/util.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
module Util
|
2
|
+
module Mongoid::Search::Util
|
3
3
|
|
4
|
-
def self.keywords(klass, field
|
4
|
+
def self.keywords(klass, field)
|
5
5
|
if field.is_a?(Hash)
|
6
6
|
field.keys.map do |key|
|
7
7
|
attribute = klass.send(key)
|
@@ -9,30 +9,32 @@ module Util
|
|
9
9
|
method = field[key]
|
10
10
|
if attribute.is_a?(Array)
|
11
11
|
if method.is_a?(Array)
|
12
|
-
method.map {|m| attribute.map { |a|
|
12
|
+
method.map {|m| attribute.map { |a| normalize_keywords a.send(m) } }
|
13
13
|
else
|
14
|
-
attribute.map(&method).map { |t|
|
14
|
+
attribute.map(&method).map { |t| normalize_keywords t }
|
15
15
|
end
|
16
16
|
elsif attribute.is_a?(Hash)
|
17
17
|
if method.is_a?(Array)
|
18
|
-
method.map {|m|
|
18
|
+
method.map {|m| normalize_keywords attribute[m.to_sym] }
|
19
19
|
else
|
20
|
-
|
20
|
+
normalize_keywords(attribute[method.to_sym])
|
21
21
|
end
|
22
22
|
else
|
23
|
-
|
23
|
+
normalize_keywords(attribute.send(method))
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
27
27
|
else
|
28
28
|
value = klass[field]
|
29
29
|
value = value.join(' ') if value.respond_to?(:join)
|
30
|
-
|
30
|
+
normalize_keywords(value) if value
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
def self.normalize_keywords(text
|
35
|
-
ligatures
|
34
|
+
def self.normalize_keywords(text)
|
35
|
+
ligatures = Mongoid::Search.ligatures
|
36
|
+
ignore_list = Mongoid::Search.ignore_list
|
37
|
+
stem_keywords = Mongoid::Search.stem_keywords
|
36
38
|
|
37
39
|
return [] if text.blank?
|
38
40
|
text = text.to_s.
|
@@ -44,7 +46,7 @@ module Util
|
|
44
46
|
gsub(/[^[:alnum:]\s]/,''). # strip accents
|
45
47
|
gsub(/[#{ligatures.keys.join("")}]/) {|c| ligatures[c]}.
|
46
48
|
split(' ').
|
47
|
-
reject { |word| word.size <
|
49
|
+
reject { |word| word.size < Mongoid::Search.minimum_word_size }
|
48
50
|
text = text.reject { |word| ignore_list.include?(word) } unless ignore_list.blank?
|
49
51
|
text = text.map(&:stem) if stem_keywords
|
50
52
|
text
|
data/lib/mongoid_search.rb
CHANGED
@@ -1,4 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'mongoid_search/railtie' if defined?(Rails)
|
2
|
-
require 'mongoid_search/util'
|
3
|
-
require 'mongoid_search/log'
|
4
4
|
require 'mongoid_search/mongoid_search'
|
5
|
+
|
6
|
+
module Mongoid::Search
|
7
|
+
## Default matching type. Match :any or :all searched keywords
|
8
|
+
mattr_accessor :match
|
9
|
+
@@match = :any
|
10
|
+
|
11
|
+
## If true, an empty search will return all objects
|
12
|
+
mattr_accessor :allow_empty_search
|
13
|
+
@@allow_empty_search = false
|
14
|
+
|
15
|
+
## If true, will search with relevance information
|
16
|
+
mattr_accessor :relevant_search
|
17
|
+
@@relevant_search = false
|
18
|
+
|
19
|
+
## Stem keywords
|
20
|
+
mattr_accessor :stem_keywords
|
21
|
+
@@stem_keywords = false
|
22
|
+
|
23
|
+
## Words to ignore
|
24
|
+
mattr_accessor :ignore_list
|
25
|
+
@@ignore_list = []
|
26
|
+
|
27
|
+
## An array of words
|
28
|
+
# @@ignore_list = %w{ a an to from as }
|
29
|
+
|
30
|
+
## Or from a file
|
31
|
+
# @@ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
|
32
|
+
|
33
|
+
## Search using regex (slower)
|
34
|
+
mattr_accessor :regex_search
|
35
|
+
@@regex_search = true
|
36
|
+
|
37
|
+
## Regex to search
|
38
|
+
mattr_accessor :regex
|
39
|
+
|
40
|
+
## Match partial words on both sides (slower)
|
41
|
+
@@regex = Proc.new { |query| /#{query}/ }
|
42
|
+
|
43
|
+
## Match partial words on the beginning or in the end (slightly faster)
|
44
|
+
# @@regex = Proc.new { |query| /ˆ#{query}/ }
|
45
|
+
# @@regex = Proc.new { |query| /#{query}$/ }
|
46
|
+
|
47
|
+
# Ligatures to be replaced
|
48
|
+
# http://en.wikipedia.org/wiki/Typographic_ligature
|
49
|
+
mattr_accessor :ligatures
|
50
|
+
@@ligatures = { "œ"=>"oe", "æ"=>"ae" }
|
51
|
+
|
52
|
+
# Minimum word size. Words smaller than it won't be indexed
|
53
|
+
mattr_accessor :minimum_word_size
|
54
|
+
@@minimum_word_size = 2
|
55
|
+
|
56
|
+
def self.setup
|
57
|
+
yield self
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
require 'mongoid_search/util'
|
62
|
+
require 'mongoid_search/log'
|
data/spec/models/category.rb
CHANGED
data/spec/models/product.rb
CHANGED
@@ -6,9 +6,9 @@ class Product
|
|
6
6
|
field :attrs, :type => Array
|
7
7
|
field :info, :type => Hash
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
embeds_many
|
9
|
+
has_many :tags
|
10
|
+
belongs_to :category
|
11
|
+
embeds_many :subproducts
|
12
12
|
|
13
13
|
search_in :brand, :name, :outlet, :attrs, :tags => :name, :category => :name,
|
14
14
|
:subproducts => [:brand, :name], :info => [ :summary, :description ]
|
data/spec/models/subproduct.rb
CHANGED
data/spec/models/tag.rb
CHANGED
data/spec/mongoid_search_spec.rb
CHANGED
@@ -5,8 +5,9 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
5
5
|
describe Mongoid::Search do
|
6
6
|
|
7
7
|
before(:each) do
|
8
|
-
|
9
|
-
|
8
|
+
Mongoid::Search.match = :any
|
9
|
+
Mongoid::Search.stem_keywords = false
|
10
|
+
Mongoid::Search.ignore_list = nil
|
10
11
|
@product = Product.create :brand => "Apple",
|
11
12
|
:name => "iPhone",
|
12
13
|
:tags => ["Amazing", "Awesome", "Olé"].map { |tag| Tag.new(:name => tag) },
|
@@ -19,8 +20,8 @@ describe Mongoid::Search do
|
|
19
20
|
describe "Serialized hash fields" do
|
20
21
|
context "when the hash is populated" do
|
21
22
|
it "should return the product" do
|
22
|
-
Product.
|
23
|
-
Product.
|
23
|
+
Product.full_text_search("Info-summary").first.should eq @product
|
24
|
+
Product.full_text_search("Info-description").first.should eq @product
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
@@ -31,29 +32,29 @@ describe Mongoid::Search do
|
|
31
32
|
end
|
32
33
|
|
33
34
|
it "should not return the product" do
|
34
|
-
Product.
|
35
|
-
Product.
|
35
|
+
Product.full_text_search("Info-description").size.should eq 0
|
36
|
+
Product.full_text_search("Info-summary").size.should eq 0
|
36
37
|
end
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
41
|
context "utf-8 characters" do
|
41
|
-
before
|
42
|
-
|
43
|
-
|
42
|
+
before do
|
43
|
+
Mongoid::Search.stem_keywords = false
|
44
|
+
Mongoid::Search.ignore_list = nil
|
44
45
|
@product = Product.create :brand => "Эльбрус",
|
45
46
|
:name => "Процессор",
|
46
47
|
:tags => ["Amazing", "Awesome", "Olé"].map { |tag| Tag.new(:name => tag) },
|
47
48
|
:category => Category.new(:name => "процессоры"),
|
48
49
|
:subproducts => []
|
49
|
-
|
50
|
+
end
|
50
51
|
|
51
52
|
it "should leave utf8 characters" do
|
52
53
|
@product._keywords.should == ["amazing", "awesome", "ole", "процессор", "процессоры", "эльбрус"]
|
53
54
|
end
|
54
55
|
|
55
56
|
it "should return results in search when case doesn't match" do
|
56
|
-
Product.
|
57
|
+
Product.full_text_search("ЭЛЬБРУС").size.should == 1
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
@@ -83,7 +84,7 @@ describe Mongoid::Search do
|
|
83
84
|
:subproducts => [Subproduct.new(:brand => "Apple", :name => "Craddle")],
|
84
85
|
:color => :white
|
85
86
|
variant._keywords.should include 'white'
|
86
|
-
Variant.
|
87
|
+
Variant.full_text_search(:name => 'Apple', :color => :white).should eq [variant]
|
87
88
|
end
|
88
89
|
|
89
90
|
it "should expand the ligature to ease searching" do
|
@@ -91,19 +92,20 @@ describe Mongoid::Search do
|
|
91
92
|
variant1 = Variant.create :tags => ["œuvre"].map {|tag| Tag.new(:name => tag)}
|
92
93
|
variant2 = Variant.create :tags => ["æquo"].map {|tag| Tag.new(:name => tag)}
|
93
94
|
|
94
|
-
Variant.
|
95
|
-
Variant.
|
96
|
-
Variant.
|
97
|
-
Variant.
|
95
|
+
Variant.full_text_search("œuvre").should eq [variant1]
|
96
|
+
Variant.full_text_search("oeuvre").should eq [variant1]
|
97
|
+
Variant.full_text_search("æquo").should eq [variant2]
|
98
|
+
Variant.full_text_search("aequo").should eq [variant2]
|
98
99
|
end
|
100
|
+
|
99
101
|
it "should set the _keywords field with stemmed words if stem is enabled" do
|
100
|
-
|
102
|
+
Mongoid::Search.stem_keywords = true
|
101
103
|
@product.save!
|
102
104
|
@product._keywords.sort.should == ["amaz", "appl", "awesom", "craddl", "iphon", "mobil", "ol", "info", "descript", "summari"].sort
|
103
105
|
end
|
104
106
|
|
105
107
|
it "should ignore keywords in an ignore list" do
|
106
|
-
|
108
|
+
Mongoid::Search.ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
|
107
109
|
@product.save!
|
108
110
|
@product._keywords.sort.should == ["apple", "craddle", "iphone", "mobile", "ole", "info", "description", "summary"].sort
|
109
111
|
end
|
@@ -115,67 +117,68 @@ describe Mongoid::Search do
|
|
115
117
|
:category => Category.new(:name => "Vehicle")
|
116
118
|
|
117
119
|
@product.save!
|
118
|
-
@product._keywords.should == ["1908","amazing", "car", "first", "ford", "vehicle"]
|
120
|
+
@product._keywords.should == ["1908", "amazing", "car", "first", "ford", "vehicle"]
|
119
121
|
end
|
120
122
|
|
121
123
|
|
122
124
|
it "should return results in search" do
|
123
|
-
Product.
|
125
|
+
Product.full_text_search("apple").size.should == 1
|
124
126
|
end
|
125
127
|
|
126
128
|
it "should return results in search for dynamic attribute" do
|
127
129
|
@product[:outlet] = "online shop"
|
128
130
|
@product.save!
|
129
|
-
Product.
|
131
|
+
Product.full_text_search("online").size.should == 1
|
130
132
|
end
|
131
133
|
|
132
134
|
it "should return results in search even searching a accented word" do
|
133
|
-
Product.
|
134
|
-
Product.
|
135
|
+
Product.full_text_search("Ole").size.should == 1
|
136
|
+
Product.full_text_search("Olé").size.should == 1
|
135
137
|
end
|
136
138
|
|
137
139
|
it "should return results in search even if the case doesn't match" do
|
138
|
-
Product.
|
140
|
+
Product.full_text_search("oLe").size.should == 1
|
139
141
|
end
|
140
142
|
|
141
|
-
it "should return results in search with a partial word" do
|
142
|
-
Product.
|
143
|
+
it "should return results in search with a partial word by default" do
|
144
|
+
Product.full_text_search("iph").size.should == 1
|
143
145
|
end
|
144
146
|
|
145
147
|
it "should return results for any matching word with default search" do
|
146
|
-
Product.
|
148
|
+
Product.full_text_search("apple motorola").size.should == 1
|
147
149
|
end
|
148
150
|
|
149
151
|
it "should not return results when all words do not match, if using :match => :all" do
|
150
|
-
|
151
|
-
Product.
|
152
|
+
Mongoid::Search.match = :all
|
153
|
+
Product.full_text_search("apple motorola").size.should == 0
|
152
154
|
end
|
153
155
|
|
154
|
-
it "should return results for any matching word, using :match => :all, passing :match => :any to .
|
155
|
-
|
156
|
-
Product.
|
156
|
+
it "should return results for any matching word, using :match => :all, passing :match => :any to .full_text_search" do
|
157
|
+
Mongoid::Search.match = :all
|
158
|
+
Product.full_text_search("apple motorola", :match => :any).size.should == 1
|
157
159
|
end
|
158
160
|
|
159
|
-
it "should not return results when all words do not match, passing :match => :all to .
|
160
|
-
Product.
|
161
|
+
it "should not return results when all words do not match, passing :match => :all to .full_text_search" do
|
162
|
+
Product.full_text_search("apple motorola", :match => :all).size.should == 0
|
161
163
|
end
|
162
164
|
|
163
165
|
it "should return no results when a blank search is made" do
|
164
|
-
|
166
|
+
Mongoid::Search.allow_empty_search = false
|
167
|
+
Product.full_text_search("").size.should == 0
|
165
168
|
end
|
166
169
|
|
167
170
|
it "should return results when a blank search is made when :allow_empty_search is true" do
|
168
|
-
|
169
|
-
Product.
|
171
|
+
Mongoid::Search.allow_empty_search = true
|
172
|
+
Product.full_text_search("").size.should == 1
|
170
173
|
end
|
171
174
|
|
172
175
|
it "should search for embedded documents" do
|
173
|
-
Product.
|
176
|
+
Product.full_text_search("craddle").size.should == 1
|
174
177
|
end
|
175
178
|
|
176
179
|
it 'should work in a chainable fashion' do
|
177
|
-
@product.category.products.where(:brand => 'Apple').
|
178
|
-
@product.category.products.
|
180
|
+
@product.category.products.where(:brand => 'Apple').full_text_search('apple').size.should == 1
|
181
|
+
@product.category.products.full_text_search('craddle').where(:brand => 'Apple').size.should == 1
|
179
182
|
end
|
180
183
|
|
181
184
|
it 'should return the classes that include the search module' do
|
@@ -190,5 +193,36 @@ describe Mongoid::Search do
|
|
190
193
|
Product.index_keywords!.should_not include(false)
|
191
194
|
end
|
192
195
|
|
196
|
+
context "when regex search is false" do
|
197
|
+
before do
|
198
|
+
Mongoid::Search.regex_search = false
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should not return results in search with a partial word if not using regex search" do
|
202
|
+
Product.full_text_search("iph").size.should == 0
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should return results in search with a full word if not using regex search" do
|
206
|
+
Product.full_text_search("iphone").size.should == 1
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context "relevant search" do
|
211
|
+
before do
|
212
|
+
Mongoid::Search.relevant_search = true
|
213
|
+
@imac = Product.create :name => 'apple imac'
|
214
|
+
end
|
215
|
+
|
216
|
+
it "should return results ordered by relevance and with correct ids" do
|
217
|
+
Product.full_text_search('apple imac').map(&:_id).should == [@imac._id, @product._id]
|
218
|
+
end
|
219
|
+
|
220
|
+
it "results should be recognized as persisted objects" do
|
221
|
+
Product.full_text_search('apple imac').map(&:persisted?).should_not include false
|
222
|
+
end
|
193
223
|
|
224
|
+
it "should include relevance information" do
|
225
|
+
Product.full_text_search('apple imac').map(&:relevance).should == [2, 1]
|
226
|
+
end
|
227
|
+
end
|
194
228
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -10,8 +10,7 @@ require 'yaml'
|
|
10
10
|
require 'mongoid_search'
|
11
11
|
|
12
12
|
Mongoid.configure do |config|
|
13
|
-
|
14
|
-
config.master = Mongo::Connection.new.db(name)
|
13
|
+
config.connect_to "mongoid_search_test"
|
15
14
|
end
|
16
15
|
|
17
16
|
Dir["#{File.dirname(__FILE__)}/models/*.rb"].each { |file| require file }
|
data/spec/util_spec.rb
CHANGED
@@ -1,48 +1,56 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
3
|
|
4
|
-
describe Util do
|
4
|
+
describe Mongoid::Search::Util do
|
5
|
+
before do
|
6
|
+
Mongoid::Search.stem_keywords = false
|
7
|
+
Mongoid::Search.ignore_list = ""
|
8
|
+
end
|
9
|
+
|
5
10
|
it "should return an empty array if no text is passed" do
|
6
|
-
Util.normalize_keywords(""
|
11
|
+
Mongoid::Search::Util.normalize_keywords("").should == []
|
7
12
|
end
|
8
13
|
|
9
14
|
it "should return an array of keywords" do
|
10
|
-
Util.normalize_keywords("keyword"
|
15
|
+
Mongoid::Search::Util.normalize_keywords("keyword").class.should == Array
|
11
16
|
end
|
12
17
|
|
13
18
|
it "should return an array of strings" do
|
14
|
-
Util.normalize_keywords("keyword"
|
19
|
+
Mongoid::Search::Util.normalize_keywords("keyword").first.class.should == String
|
15
20
|
end
|
16
21
|
|
17
22
|
it "should remove accents from the text passed" do
|
18
|
-
Util.normalize_keywords("café"
|
23
|
+
Mongoid::Search::Util.normalize_keywords("café").should == ["cafe"]
|
19
24
|
end
|
20
25
|
|
21
26
|
it "should downcase the text passed" do
|
22
|
-
Util.normalize_keywords("CaFé"
|
27
|
+
Mongoid::Search::Util.normalize_keywords("CaFé").should == ["cafe"]
|
23
28
|
end
|
24
29
|
|
25
30
|
it "should downcase utf-8 chars of the text passed" do
|
26
|
-
Util.normalize_keywords("Кафе"
|
31
|
+
Mongoid::Search::Util.normalize_keywords("Кафе").should == ["кафе"]
|
27
32
|
end
|
28
33
|
|
29
34
|
it "should split whitespaces, hifens, dots, underlines, etc.." do
|
30
|
-
Util.normalize_keywords("CaFé-express.com delicious;come visit, and 'win' an \"iPad\""
|
35
|
+
Mongoid::Search::Util.normalize_keywords("CaFé-express.com delicious;come visit, and 'win' an \"iPad\"").should == ["cafe", "express", "com", "delicious", "come", "visit", "and", "win", "an", "ipad"]
|
31
36
|
end
|
32
37
|
|
33
38
|
it "should stem keywords" do
|
34
|
-
|
39
|
+
Mongoid::Search.stem_keywords = true
|
40
|
+
Mongoid::Search::Util.normalize_keywords("A runner running and eating").should == ["runner", "run", "and", "eat"]
|
35
41
|
end
|
36
42
|
|
37
43
|
it "should ignore keywords from ignore list" do
|
38
|
-
|
44
|
+
Mongoid::Search.stem_keywords = true
|
45
|
+
Mongoid::Search.ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
|
46
|
+
Mongoid::Search::Util.normalize_keywords("An amazing awesome runner running and eating").should == ["an", "runner", "run", "and", "eat"]
|
39
47
|
end
|
40
48
|
|
41
49
|
it "should ignore keywords with less than two words" do
|
42
|
-
Util.normalize_keywords("A runner running"
|
50
|
+
Mongoid::Search::Util.normalize_keywords("A runner running").should_not include "a"
|
43
51
|
end
|
44
52
|
|
45
|
-
|
46
|
-
|
47
|
-
|
53
|
+
it "should not ignore numbers" do
|
54
|
+
Mongoid::Search::Util.normalize_keywords("Ford T 1908").should include "1908"
|
55
|
+
end
|
48
56
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongoid_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,29 +13,18 @@ date: 2012-07-30 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mongoid
|
16
|
-
requirement: &
|
16
|
+
requirement: &70200267450400 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version:
|
21
|
+
version: 3.0.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
25
|
-
- !ruby/object:Gem::Dependency
|
26
|
-
name: bson_ext
|
27
|
-
requirement: &70364721930900 !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
|
-
requirements:
|
30
|
-
- - ! '>='
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '1.2'
|
33
|
-
type: :runtime
|
34
|
-
prerelease: false
|
35
|
-
version_requirements: *70364721930900
|
24
|
+
version_requirements: *70200267450400
|
36
25
|
- !ruby/object:Gem::Dependency
|
37
26
|
name: fast-stemmer
|
38
|
-
requirement: &
|
27
|
+
requirement: &70200267465820 !ruby/object:Gem::Requirement
|
39
28
|
none: false
|
40
29
|
requirements:
|
41
30
|
- - ~>
|
@@ -43,21 +32,21 @@ dependencies:
|
|
43
32
|
version: 1.0.0
|
44
33
|
type: :runtime
|
45
34
|
prerelease: false
|
46
|
-
version_requirements: *
|
35
|
+
version_requirements: *70200267465820
|
47
36
|
- !ruby/object:Gem::Dependency
|
48
37
|
name: database_cleaner
|
49
|
-
requirement: &
|
38
|
+
requirement: &70200267464480 !ruby/object:Gem::Requirement
|
50
39
|
none: false
|
51
40
|
requirements:
|
52
|
-
- -
|
41
|
+
- - ! '>='
|
53
42
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.
|
43
|
+
version: 0.8.0
|
55
44
|
type: :development
|
56
45
|
prerelease: false
|
57
|
-
version_requirements: *
|
46
|
+
version_requirements: *70200267464480
|
58
47
|
- !ruby/object:Gem::Dependency
|
59
48
|
name: rake
|
60
|
-
requirement: &
|
49
|
+
requirement: &70200267462600 !ruby/object:Gem::Requirement
|
61
50
|
none: false
|
62
51
|
requirements:
|
63
52
|
- - ~>
|
@@ -65,10 +54,10 @@ dependencies:
|
|
65
54
|
version: 0.8.7
|
66
55
|
type: :development
|
67
56
|
prerelease: false
|
68
|
-
version_requirements: *
|
57
|
+
version_requirements: *70200267462600
|
69
58
|
- !ruby/object:Gem::Dependency
|
70
59
|
name: rspec
|
71
|
-
requirement: &
|
60
|
+
requirement: &70200267461180 !ruby/object:Gem::Requirement
|
72
61
|
none: false
|
73
62
|
requirements:
|
74
63
|
- - ~>
|
@@ -76,7 +65,7 @@ dependencies:
|
|
76
65
|
version: '2.4'
|
77
66
|
type: :development
|
78
67
|
prerelease: false
|
79
|
-
version_requirements: *
|
68
|
+
version_requirements: *70200267461180
|
80
69
|
description: Simple full text search implementation.
|
81
70
|
email:
|
82
71
|
- mauricio@papodenerd.net
|