mongomapper_search 0.0.1 → 0.1.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 CHANGED
@@ -26,12 +26,12 @@ Examples
26
26
  many :tags
27
27
  belongs_to :category
28
28
 
29
- search_in :brand, :name, :tags => :name, :category => :name
29
+ search_in :brand, :name, {:tags => :name} => 1, {:category => :name} => 2
30
30
  end
31
31
 
32
32
  class Tag
33
33
  include MongoMapper::Document
34
- key :name, Stirng
34
+ key :name, String
35
35
 
36
36
  belongs_to :product
37
37
  end
@@ -43,17 +43,19 @@ Examples
43
43
  many :products
44
44
  end
45
45
 
46
- Now when you save a product, you get a _keywords field automatically:
46
+ Syntax:
47
+
48
+ search_in :brand, :name => 3, {:tags => :name} => 1
49
+
50
+ The search will be done using fields named as the symbols passed.
51
+ You can pass a boost parameter to smooth your search like in:
52
+
53
+ :name => 3 #It means that keywords found on name is 3 times more important than keywords found on :brand
47
54
 
48
- p = Product.new :brand => "Apple", :name => "iPhone"
49
- p.tags << Tag.new(:name => "Amazing")
50
- p.tags << Tag.new(:name => "Awesome")
51
- p.tags << Tag.new(:name => "Superb")
52
- p.save
53
- => true
54
- p._keywords
55
+ The default boost is 1.
56
+ For while, complex attributes like {:tags => :name} must be declared with a boost value.
55
57
 
56
- Now you can run search, which will look in the _keywords field and return all matching results:
58
+ Now you can run search, which will look in the search field and return all matching results:
57
59
 
58
60
  Product.search("apple iphone").size
59
61
  => 1
@@ -65,7 +67,7 @@ Note that the search is case insensitive, and accept partial searching too:
65
67
 
66
68
  You can use search in a chainable way:
67
69
 
68
- Product.where(:brand => "Apple").search('iphone').sort(:price.asc)
70
+ Product.where(:brand => "Apple").search('iphone')
69
71
 
70
72
 
71
73
  Options
@@ -76,12 +78,12 @@ match:
76
78
  _:all_ - match all ocurrences
77
79
  Default is _:any_.
78
80
 
79
- search_in :brand, :name, { :tags => :name }, { :match => :any }
81
+ search_in :brand, :name, { :tags => :name } => 1, { :match => :any }
80
82
 
81
83
  Product.search("apple motorola").size
82
84
  => 1
83
85
 
84
- search_in :brand, :name, { :tags => :name }, { :match => :all }
86
+ search_in :brand, :name, { :tags => :name } => 1, { :match => :all }
85
87
 
86
88
  Product.search("apple motorola").size
87
89
  => 0
@@ -91,7 +93,7 @@ allow_empty_search:
91
93
  _false_ - match all ocurrences
92
94
  Default is _false_.
93
95
 
94
- search_in :brand, :name, { :tags => :name }, { :allow_empty_search => true }
96
+ search_in :brand, :name, { :tags => :name } => 1, { :allow_empty_search => true }
95
97
 
96
98
  Product.search("").size
97
99
  => 1
@@ -3,8 +3,7 @@ module Plucky
3
3
  def search(query, options={})
4
4
  #Fix class search
5
5
  if first
6
- to_merge = first.class.search(query, options)
7
- find_each(to_merge.criteria.to_hash).to_a
6
+ first.class.search(query, options, self.criteria.source)
8
7
  else
9
8
  self
10
9
  end
@@ -0,0 +1,11 @@
1
+ class RankedDocument
2
+ attr_accessor :document, :rank
3
+
4
+ def initialize(document)
5
+ self.document = document
6
+ end
7
+
8
+ def ==(other_ranked_doc)
9
+ self.document == other_ranked_doc.document
10
+ end
11
+ end
@@ -5,7 +5,7 @@ module MongoMapper
5
5
  included do
6
6
  cattr_accessor :search_fields, :allow_empty_search, :stem_keywords, :match
7
7
  end
8
-
8
+
9
9
  def self.included(base)
10
10
  @classes ||= []
11
11
  @classes << base
@@ -22,32 +22,113 @@ module MongoMapper
22
22
  self.match = [:any, :all].include?(options[:match]) ? options[:match] : :any
23
23
  self.allow_empty_search = [true, false].include?(options[:allow_empty_search]) ? options[:allow_empty_search] : false
24
24
  self.stem_keywords = [true, false].include?(options[:stem_keywords]) ? options[:allow_empty_search] : false
25
- self.search_fields = (self.search_fields || []).concat args
26
-
27
- key :_keywords, Array
28
- ensure_index :_keywords, :background => true
25
+ self.search_fields = self.search_fields || {}
29
26
 
27
+ args.each do |arg|
28
+ if arg.class == Hash
29
+ arg.each do |key, value|
30
+ if key.class == Hash
31
+ key.each do |sub_key, sub_value|
32
+ field = "_#{sub_key}_#{sub_value}".to_sym
33
+ key field, Array
34
+ ensure_index field, :background => true
35
+ self.search_fields[key] = value
36
+ end
37
+ else
38
+ field = "_#{key}".to_sym
39
+ key field, Array
40
+ ensure_index field, :background => true
41
+ self.search_fields[key] = value
42
+ end
43
+ end
44
+ else
45
+ field = "_#{arg}".to_sym
46
+ key field, Array
47
+ ensure_index field, :background => true
48
+ self.search_fields[arg] = 1
49
+ end
50
+ end
51
+
30
52
  before_save :set_keywords
31
53
  end
32
54
 
33
- def search(query, options={})
34
- return all if query.blank? && allow_empty_search
55
+ def search_a_field(search_match, field, regexed_keywords, results, boost, terms, total, criteria)
56
+ if search_match == :all
57
+ field_results = where(criteria).where(field => { "$all" => regexed_keywords })
58
+ elsif search_match == :any
59
+ field_results = where(criteria).where(field => regexed_keywords )
60
+ end
61
+ field_results.each do |field_result|
62
+ result = RankedDocument.new(field_result)
63
+ rank = 0
64
+ terms.each do |term|
65
+ rank += boost * tf_idf(field, term, result.document[field], total)
66
+ end
67
+
68
+ if !results.include?(result)
69
+ result.rank = rank
70
+ results << result
71
+ else
72
+ i = results.index result
73
+ results[i].rank += rank
74
+ end
75
+ end
76
+ end
77
+
78
+ def tf_idf(field, term, terms, total)
79
+ tf(term, terms) * idf(field, term, total)
80
+ end
81
+
82
+ def tf(term, terms)
83
+ terms.count(term)
84
+ end
85
+
86
+ def idf(field, term, total)
87
+ df = where(field => /#{term}/ ).count
88
+ if df != 0
89
+ Math.log(total/df)
90
+ else
91
+ 0
92
+ end
93
+ end
94
+
95
+ def search(query, options={}, criteria = {})
96
+ return all(criteria) if query.blank? && (options[:allow_empty_search] || allow_empty_search)
35
97
 
36
98
  keywords = Util.normalize_keywords(query, stem_keywords)
99
+
100
+ unique_keywords = keywords.uniq
37
101
 
38
102
  regexed_keywords = []
39
103
 
40
- keywords.each do |keyword|
104
+ unique_keywords.each do |keyword|
41
105
  regexed_keywords.concat([/#{keyword}/])
42
106
  end
43
107
 
44
108
  search_match = options[:match]||self.match
45
-
46
- if search_match == :all
47
- where(:_keywords => { "$all" => regexed_keywords })
48
- elsif search_match == :any
49
- where(:_keywords => regexed_keywords )
109
+
110
+ total = all.count
111
+
112
+ results = []
113
+ self.search_fields.each do |key, value|
114
+ if key.class == Hash
115
+ key.each do |sub_key, sub_value|
116
+ field = "_#{sub_key}_#{sub_value}".to_sym
117
+ search_a_field search_match, field, regexed_keywords, results, value, unique_keywords, total, criteria
118
+ end
119
+ else
120
+ field = "_#{key}".to_sym
121
+ search_a_field search_match, field, regexed_keywords, results, value, unique_keywords, total, criteria
122
+ end
50
123
  end
124
+
125
+ results.sort { |a,b| b.rank <=> a.rank }
126
+ documents = []
127
+ results.each do |result|
128
+ documents << result.document
129
+ end
130
+
131
+ documents
51
132
  end
52
133
 
53
134
  # Goes through all documents in the class that includes Mongoid::Search
@@ -55,20 +136,54 @@ module MongoMapper
55
136
  def index_keywords!
56
137
  all.each { |d| d.index_keywords! }
57
138
  end
139
+
58
140
  end
59
141
 
60
142
  module InstanceMethods #:nodoc:
61
143
  # Indexes the document keywords
62
144
  def index_keywords!
63
- update_attribute(:_keywords, set_keywords)
145
+ self.search_fields.each do |key, value|
146
+ if key.class == Hash
147
+ key.each do |sub_key, sub_value|
148
+ set_search_field key
149
+ end
150
+ else
151
+ set_search_field key
152
+ end
153
+ end
154
+ true
64
155
  end
65
156
  end
66
157
 
67
158
  private
159
+ def get_keywords(key)
160
+ Util.keywords(self, key, stem_keywords)
161
+ .flatten.reject{|k| k.nil? || k.empty?}
162
+ end
163
+
164
+ def set_search_field(key)
165
+ if key.class == Hash
166
+ key.each do |sub_key, sub_value|
167
+ keywords = get_keywords(key)
168
+ instance_variable_set "@_#{sub_key}_#{sub_value}".to_sym, keywords
169
+ end
170
+ else
171
+ keywords = get_keywords(key)
172
+ instance_variable_set "@_#{key}".to_sym, keywords
173
+ end
174
+
175
+ end
176
+
68
177
  def set_keywords
69
- self._keywords = self.search_fields.map do |field|
70
- Util.keywords(self, field, stem_keywords)
71
- end.flatten.reject{|k| k.nil? || k.empty?}.uniq.sort
178
+ self.search_fields.each do |key, value|
179
+ if key.class == Hash
180
+ key.each do |sub_key, sub_value|
181
+ set_search_field key
182
+ end
183
+ else
184
+ set_search_field key
185
+ end
186
+ end
72
187
  end
73
188
 
74
189
  end
@@ -19,22 +19,26 @@ module Util
19
19
  end
20
20
  else
21
21
  value = klass[field]
22
- value = value.join(' ') if value.respond_to?(:join)
23
- Util.normalize_keywords(value, stem_keywords, ignore_list) if value
22
+ if value
23
+ value = value.join(' ') if value.respond_to?(:join)
24
+ Util.normalize_keywords(value, stem_keywords, ignore_list) if value
25
+ else
26
+ []
27
+ end
24
28
  end
25
29
  end
26
30
 
27
31
  def self.normalize_keywords(text, stem_keywords, ignore_list=[])
28
32
  return [] if text.blank?
29
- text = text.to_s.
30
- mb_chars.
31
- normalize(:kd).
32
- to_s.
33
- gsub(/[._:;'"`,?|+={}()!@#%^&*<>~\$\-\\\/\[\]]/, ' '). # strip punctuation
34
- gsub(/[^[:alnum:]\s]/,''). # strip accents
35
- downcase.
36
- split(' ').
37
- reject { |word| word.size < 2 }
33
+ text = text.to_s
34
+ .mb_chars
35
+ .normalize(:kd)
36
+ .to_s
37
+ .gsub(/[._:;'"`,?|+={}()!@#%^&*<>~\$\-\\\/\[\]]/, ' ') # strip punctuation
38
+ .gsub(/[^[:alnum:]\s]/,'') # strip accents
39
+ .downcase
40
+ .split(' ')
41
+ .reject { |word| word.size < 2 }
38
42
  text = text.reject { |word| ignore_list.include?(word) } unless ignore_list.blank?
39
43
  text = text.map(&:stem) if stem_keywords
40
44
  text
@@ -1,3 +1,4 @@
1
1
  require "mongomapper_search/search"
2
2
  require "mongomapper_search/util"
3
- require "mongomapper_search/query"
3
+ require "mongomapper_search/query"
4
+ require "mongomapper_search/ranked_document"
@@ -9,5 +9,5 @@ class Product
9
9
  many :subproducts
10
10
  belongs_to :category
11
11
 
12
- search_in :brand, :name, :outlet, :attrs, :tags => :name, :category => :name, :subproducts => [:brand, :name]
12
+ search_in :brand, :attrs, :outlet, :name => 3 , {:tags => :name} => 1, {:category => :name} => 1, {:subproducts => :brand} => 1
13
13
  end
data/spec/search_spec.rb CHANGED
@@ -1,9 +1,7 @@
1
1
  # encoding: utf-8
2
-
3
2
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
3
 
5
4
  describe MongoMapper::Search do
6
-
7
5
  before(:each) do
8
6
  Product.stem_keywords = false
9
7
  @product = Product.create :brand => "Apple",
@@ -24,9 +22,34 @@ describe MongoMapper::Search do
24
22
  }
25
23
 
26
24
  it "should leave utf8 characters" do
27
- @product._keywords.should == ["amazing", "awesome", "ole", "Процессор", "Эльбрус", "процессоры"]
25
+ @product._brand.should == ["Эльбрус"]
26
+ @product._name.should == ["Процессор"]
27
+ @product._tags_name.should == ["amazing", "awesome", "ole"]
28
+ @product._category_name.should == ["процессоры"]
29
+ @product._subproducts_brand.should == []
28
30
  end
29
31
  end
32
+
33
+ it "should create search fields right" do
34
+ @product._brand.should == ["apple"]
35
+ @product._name.should == ["iphone"]
36
+ @product._tags_name.should == ["amazing", "awesome", "ole"]
37
+ @product._category_name.should == ["mobile"]
38
+ @product._subproducts_brand.should == ["apple"]
39
+ end
40
+
41
+ it "should return result for a valid search" do
42
+ Product.search("iphone").size.should == 1
43
+ Product.search("ipad").size.should == 0
44
+ Product.search("apple").size.should == 1
45
+ Product.search("").size.should == 0
46
+ Product.search("", {:allow_empty_search => true}).size.should == 1
47
+ end
48
+
49
+ it "should find products by category name" do
50
+ Product.search("mobile").size.should == 1
51
+ Product.search("nokia").size.should == 0
52
+ end
30
53
 
31
54
  context "when references are nil" do
32
55
  context "when instance is being created" do
@@ -34,33 +57,39 @@ describe MongoMapper::Search do
34
57
  lambda { Product.create! }.should_not raise_error
35
58
  end
36
59
  end
37
-
60
+
38
61
  subject { Product.create :brand => "Apple", :name => "iPhone" }
39
62
 
40
- its(:_keywords) { should == ["apple", "iphone"] }
63
+ its(:_brand) { should == ["apple"] }
64
+ its(:_name) { should == ["iphone"] }
41
65
  end
42
66
 
43
- it "should set the _keywords field for array fields also" do
67
+ it "should set the search field for array fields also" do
44
68
  @product.attrs = ['lightweight', 'plastic', :red]
45
69
  @product.save!
46
- @product._keywords.should include 'lightweight', 'plastic', 'red'
70
+ @product._attrs.should include 'lightweight', 'plastic', 'red'
47
71
  end
48
72
 
49
- it "should inherit _keywords field and build upon" do
73
+ it "should inherit search fields and build upon" do
50
74
  variant = Variant.create :brand => "Apple",
51
75
  :name => "iPhone",
52
76
  :tags => ["Amazing", "Awesome", "Olé"].map { |tag| Tag.new(:name => tag) },
53
77
  :category => Category.new(:name => "Mobile"),
54
78
  :subproducts => [Subproduct.new(:brand => "Apple", :name => "Craddle")],
55
79
  :color => :white
56
- variant._keywords.should include 'white'
57
- Variant.search("Apple white").to_a.should eq [variant]
80
+ variant._color.should include 'white'
81
+ variant._name.should include 'iphone'
82
+ Variant.search("Apple white").first.should == variant
58
83
  end
59
84
 
60
- it "should set the _keywords field with stemmed words if stem is enabled" do
85
+ it "should set the search field with stemmed words if stem is enabled" do
61
86
  Product.stem_keywords = true
62
87
  @product.save!
63
- @product._keywords.should == ["amaz", "appl", "awesom", "craddl", "iphon", "mobil", "ol"]
88
+ @product._brand.should == ["appl"]
89
+ @product._name.should == ["iphon"]
90
+ @product._tags_name.should == ["amaz","awesom", "ol"]
91
+ @product._category_name.should == ["mobil"]
92
+ @product._subproducts_brand.should == ["appl"]
64
93
  end
65
94
 
66
95
  it "should incorporate numbers as keywords" do
@@ -70,7 +99,7 @@ describe MongoMapper::Search do
70
99
  :category => Category.new(:name => "Vehicle")
71
100
 
72
101
  @product.save!
73
- @product._keywords.should == ["1908","amazing", "car", "first", "ford", "vehicle"]
102
+ @product._name.should == ["1908"]
74
103
  end
75
104
 
76
105
 
@@ -125,14 +154,13 @@ describe MongoMapper::Search do
125
154
  end
126
155
 
127
156
  it "should search for embedded documents" do
128
- Product.search("craddle").size.should == 1
157
+ Product.search("craddle").size.should == 0
129
158
  end
130
159
 
131
160
  it 'should work in a chainable fashion' do
161
+ @product.category.products.where(:brand => 'Apple').search("", {:allow_empty_search => true}).size.should == 1
132
162
  @product.category.products.where(:brand => 'Apple').search('apple').size.should == 1
133
163
  @product.category.products.where(:brand => 'Apple').search('troll').size.should == 0
134
- @product.category.products.search('craddle').where(:brand => 'Apple').size.should == 1
135
- @product.category.products.search('craddle').where(:brand => 'Dell').size.should == 0
136
164
  end
137
165
 
138
166
  it 'should return the classes that include the search module' do
@@ -146,5 +174,5 @@ describe MongoMapper::Search do
146
174
  it 'should have a class method to index all documents keywords' do
147
175
  Product.index_keywords!.should_not include(false)
148
176
  end
149
-
177
+
150
178
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongomapper_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-08-18 00:00:00.000000000Z
12
+ date: 2011-09-21 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mongo_mapper
16
- requirement: &70143029026220 !ruby/object:Gem::Requirement
16
+ requirement: &70189799279580 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.9.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70143029026220
24
+ version_requirements: *70189799279580
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bson_ext
27
- requirement: &70143029025740 !ruby/object:Gem::Requirement
27
+ requirement: &70189799278780 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 1.2.0
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70143029025740
35
+ version_requirements: *70189799278780
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: fast-stemmer
38
- requirement: &70143029025260 !ruby/object:Gem::Requirement
38
+ requirement: &70189799277700 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 1.0.0
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70143029025260
46
+ version_requirements: *70189799277700
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: database_cleaner
49
- requirement: &70143029024780 !ruby/object:Gem::Requirement
49
+ requirement: &70189799276940 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.6.4
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70143029024780
57
+ version_requirements: *70189799276940
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rake
60
- requirement: &70143029024300 !ruby/object:Gem::Requirement
60
+ requirement: &70189799275820 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 0.9.2
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70143029024300
68
+ version_requirements: *70189799275820
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
- requirement: &70143029023820 !ruby/object:Gem::Requirement
71
+ requirement: &70189799274780 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,7 +76,7 @@ dependencies:
76
76
  version: '2.4'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70143029023820
79
+ version_requirements: *70189799274780
80
80
  description: Simple full text search for MongoMapper ODM
81
81
  email:
82
82
  - mario.peixoto@gmail.com
@@ -85,6 +85,7 @@ extensions: []
85
85
  extra_rdoc_files: []
86
86
  files:
87
87
  - lib/mongomapper_search/query.rb
88
+ - lib/mongomapper_search/ranked_document.rb
88
89
  - lib/mongomapper_search/search.rb
89
90
  - lib/mongomapper_search/util.rb
90
91
  - lib/mongomapper_search.rb
@@ -119,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
120
  version: 1.3.6
120
121
  requirements: []
121
122
  rubyforge_project:
122
- rubygems_version: 1.8.7
123
+ rubygems_version: 1.8.10
123
124
  signing_key:
124
125
  specification_version: 3
125
126
  summary: Search implementation for MongoMapper ODM