mongomapper_search 0.0.1 → 0.1.0

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