mongoid_search 0.2.8 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|