mongomapper_search 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (C) 2011 by Mário Peixoto
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
21
+
22
+ Based on mongoid_search, Copyright by Mauricio Zaffari, 2009
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ MongoMapper Search
2
+ ============
3
+
4
+ MongoMapper Search is a simple full text search implementation for MongoMapper ODM based on mongoid_search (https://github.com/mauriciozaffari/mongoid_search).
5
+
6
+ Installation
7
+ --------
8
+
9
+ In your Gemfile:
10
+
11
+ gem 'mongomapper_search'
12
+
13
+ Then:
14
+
15
+ bundle install
16
+
17
+ Examples
18
+ --------
19
+
20
+ class Product
21
+ include MongoMapper::Document
22
+ include MongoMapper::Search
23
+ key :brand, String
24
+ key :name, String
25
+
26
+ many :tags
27
+ belongs_to :category
28
+
29
+ search_in :brand, :name, :tags => :name, :category => :name
30
+ end
31
+
32
+ class Tag
33
+ include MongoMapper::Document
34
+ key :name, Stirng
35
+
36
+ belongs_to :product
37
+ end
38
+
39
+ class Category
40
+ include MongoMapper::Document
41
+ key :name, String
42
+
43
+ many :products
44
+ end
45
+
46
+ Now when you save a product, you get a _keywords field automatically:
47
+
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
+
56
+ Now you can run search, which will look in the _keywords field and return all matching results:
57
+
58
+ Product.search("apple iphone").size
59
+ => 1
60
+
61
+ Note that the search is case insensitive, and accept partial searching too:
62
+
63
+ Product.search("ipho").size
64
+ => 1
65
+
66
+ You can use search in a chainable way:
67
+
68
+ Product.where(:brand => "Apple").search('iphone').sort(:price.asc)
69
+
70
+
71
+ Options
72
+ -------
73
+
74
+ match:
75
+ _:any_ - match any occurrence
76
+ _:all_ - match all ocurrences
77
+ Default is _:any_.
78
+
79
+ search_in :brand, :name, { :tags => :name }, { :match => :any }
80
+
81
+ Product.search("apple motorola").size
82
+ => 1
83
+
84
+ search_in :brand, :name, { :tags => :name }, { :match => :all }
85
+
86
+ Product.search("apple motorola").size
87
+ => 0
88
+
89
+ allow_empty_search:
90
+ _true_ - match any occurrence
91
+ _false_ - match all ocurrences
92
+ Default is _false_.
93
+
94
+ search_in :brand, :name, { :tags => :name }, { :allow_empty_search => true }
95
+
96
+ Product.search("").size
97
+ => 1
98
+
99
+ RoadMap
100
+ ----
101
+
102
+ - Create a ignore list so the search can ignore some words
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.rspec_opts = ["-c", "-f progress"]
7
+ spec.pattern = 'spec/**/*_spec.rb'
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,13 @@
1
+ module Plucky
2
+ class Query
3
+ def search(query, options={})
4
+ #Fix class search
5
+ if first
6
+ to_merge = first.class.search(query, options)
7
+ find_each(to_merge.criteria.to_hash).to_a
8
+ else
9
+ self
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ module MongoMapper
2
+ module Search
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ cattr_accessor :search_fields, :allow_empty_search, :stem_keywords, :match
7
+ end
8
+
9
+ def self.included(base)
10
+ @classes ||= []
11
+ @classes << base
12
+ end
13
+
14
+ def self.classes
15
+ @classes
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ def search_in(*args)
21
+ options = args.last.is_a?(Hash) && [:allow_empty_search, :stem_keywords].include?(args.last.keys.first) ? args.pop : {}
22
+ self.match = [:any, :all].include?(options[:match]) ? options[:match] : :any
23
+ self.allow_empty_search = [true, false].include?(options[:allow_empty_search]) ? options[:allow_empty_search] : false
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
29
+
30
+ before_save :set_keywords
31
+ end
32
+
33
+ def search(query, options={})
34
+ return all if query.blank? && allow_empty_search
35
+
36
+ keywords = Util.normalize_keywords(query, stem_keywords)
37
+
38
+ regexed_keywords = []
39
+
40
+ keywords.each do |keyword|
41
+ regexed_keywords.concat([/#{keyword}/])
42
+ end
43
+
44
+ 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 )
50
+ end
51
+ end
52
+
53
+ # Goes through all documents in the class that includes Mongoid::Search
54
+ # and indexes the keywords.
55
+ def index_keywords!
56
+ all.each { |d| d.index_keywords! }
57
+ end
58
+ end
59
+
60
+ module InstanceMethods #:nodoc:
61
+ # Indexes the document keywords
62
+ def index_keywords!
63
+ update_attribute(:_keywords, set_keywords)
64
+ end
65
+ end
66
+
67
+ private
68
+ 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
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,43 @@
1
+ module Util
2
+
3
+ def self.keywords(klass, field, stem_keywords, ignore_list=[])
4
+ if field.is_a?(Hash)
5
+ field.keys.map do |key|
6
+ attribute = klass.send(key)
7
+ unless attribute.blank?
8
+ method = field[key]
9
+ if attribute.is_a?(Array)
10
+ if method.is_a?(Array)
11
+ method.map {|m| attribute.map { |a| Util.normalize_keywords a.send(m), stem_keywords, ignore_list } }
12
+ else
13
+ attribute.map(&method).map { |t| Util.normalize_keywords t, stem_keywords, ignore_list }
14
+ end
15
+ else
16
+ Util.normalize_keywords(attribute.send(method), stem_keywords, ignore_list)
17
+ end
18
+ end
19
+ end
20
+ else
21
+ value = klass[field]
22
+ value = value.join(' ') if value.respond_to?(:join)
23
+ Util.normalize_keywords(value, stem_keywords, ignore_list) if value
24
+ end
25
+ end
26
+
27
+ def self.normalize_keywords(text, stem_keywords, ignore_list=[])
28
+ 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 }
38
+ text = text.reject { |word| ignore_list.include?(word) } unless ignore_list.blank?
39
+ text = text.map(&:stem) if stem_keywords
40
+ text
41
+ end
42
+
43
+ end
@@ -0,0 +1,3 @@
1
+ require "mongomapper_search/search"
2
+ require "mongomapper_search/util"
3
+ require "mongomapper_search/query"
@@ -0,0 +1,6 @@
1
+ class Category
2
+ include MongoMapper::Document
3
+ key :name, String
4
+
5
+ many :products
6
+ end
@@ -0,0 +1,13 @@
1
+ class Product
2
+ include MongoMapper::Document
3
+ include MongoMapper::Search
4
+ key :brand, String
5
+ key :name, String
6
+ key :attrs, Array
7
+
8
+ many :tags
9
+ many :subproducts
10
+ belongs_to :category
11
+
12
+ search_in :brand, :name, :outlet, :attrs, :tags => :name, :category => :name, :subproducts => [:brand, :name]
13
+ end
@@ -0,0 +1,7 @@
1
+ class Subproduct
2
+ include MongoMapper::EmbeddedDocument
3
+
4
+ key :brand, String
5
+ key :name, String
6
+
7
+ end
@@ -0,0 +1,6 @@
1
+ class Tag
2
+ include MongoMapper::Document
3
+ key :name, String
4
+
5
+ belongs_to :product
6
+ end
@@ -0,0 +1,4 @@
1
+ class Variant < Product
2
+ key :color, String
3
+ search_in :color
4
+ end
@@ -0,0 +1,150 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+
5
+ describe MongoMapper::Search do
6
+
7
+ before(:each) do
8
+ Product.stem_keywords = false
9
+ @product = Product.create :brand => "Apple",
10
+ :name => "iPhone",
11
+ :tags => ["Amazing", "Awesome", "Olé"].map { |tag| Tag.new(:name => tag) },
12
+ :category => Category.new(:name => "Mobile"),
13
+ :subproducts => [Subproduct.new(:brand => "Apple", :name => "Craddle")]
14
+ end
15
+
16
+ context "utf-8 characters" do
17
+ before(:each) {
18
+ Product.stem_keywords = false
19
+ @product = Product.create :brand => "Эльбрус",
20
+ :name => "Процессор",
21
+ :tags => ["Amazing", "Awesome", "Olé"].map { |tag| Tag.new(:name => tag) },
22
+ :category => Category.new(:name => "процессоры"),
23
+ :subproducts => []
24
+ }
25
+
26
+ it "should leave utf8 characters" do
27
+ @product._keywords.should == ["amazing", "awesome", "ole", "Процессор", "Эльбрус", "процессоры"]
28
+ end
29
+ end
30
+
31
+ context "when references are nil" do
32
+ context "when instance is being created" do
33
+ it "should not complain about method missing" do
34
+ lambda { Product.create! }.should_not raise_error
35
+ end
36
+ end
37
+
38
+ subject { Product.create :brand => "Apple", :name => "iPhone" }
39
+
40
+ its(:_keywords) { should == ["apple", "iphone"] }
41
+ end
42
+
43
+ it "should set the _keywords field for array fields also" do
44
+ @product.attrs = ['lightweight', 'plastic', :red]
45
+ @product.save!
46
+ @product._keywords.should include 'lightweight', 'plastic', 'red'
47
+ end
48
+
49
+ it "should inherit _keywords field and build upon" do
50
+ variant = Variant.create :brand => "Apple",
51
+ :name => "iPhone",
52
+ :tags => ["Amazing", "Awesome", "Olé"].map { |tag| Tag.new(:name => tag) },
53
+ :category => Category.new(:name => "Mobile"),
54
+ :subproducts => [Subproduct.new(:brand => "Apple", :name => "Craddle")],
55
+ :color => :white
56
+ variant._keywords.should include 'white'
57
+ Variant.search("Apple white").to_a.should eq [variant]
58
+ end
59
+
60
+ it "should set the _keywords field with stemmed words if stem is enabled" do
61
+ Product.stem_keywords = true
62
+ @product.save!
63
+ @product._keywords.should == ["amaz", "appl", "awesom", "craddl", "iphon", "mobil", "ol"]
64
+ end
65
+
66
+ it "should incorporate numbers as keywords" do
67
+ @product = Product.create :brand => "Ford",
68
+ :name => "T 1908",
69
+ :tags => ["Amazing", "First", "Car"].map { |tag| Tag.new(:name => tag) },
70
+ :category => Category.new(:name => "Vehicle")
71
+
72
+ @product.save!
73
+ @product._keywords.should == ["1908","amazing", "car", "first", "ford", "vehicle"]
74
+ end
75
+
76
+
77
+ it "should return results in search" do
78
+ Product.search("apple").size.should == 1
79
+ end
80
+
81
+ it "should return results in search for dynamic attribute" do
82
+ @product[:outlet] = "online shop"
83
+ @product.save!
84
+ Product.search("online").size.should == 1
85
+ end
86
+
87
+ it "should return results in search even searching a accented word" do
88
+ Product.search("Ole").size.should == 1
89
+ Product.search("Olé").size.should == 1
90
+ end
91
+
92
+ it "should return results in search even if the case doesn't match" do
93
+ Product.search("oLe").size.should == 1
94
+ end
95
+
96
+ it "should return results in search with a partial word" do
97
+ Product.search("iph").size.should == 1
98
+ end
99
+
100
+ it "should return results for any matching word with default search" do
101
+ Product.search("apple motorola").size.should == 1
102
+ end
103
+
104
+ it "should not return results when all words do not match, if using :match => :all" do
105
+ Product.match = :all
106
+ Product.search("apple motorola").size.should == 0
107
+ end
108
+
109
+ it "should return results for any matching word, using :match => :all, passing :match => :any to .search" do
110
+ Product.match = :all
111
+ Product.search("apple motorola", :match => :any).size.should == 1
112
+ end
113
+
114
+ it "should not return results when all words do not match, passing :match => :all to .search" do
115
+ Product.search("apple motorola", :match => :all).size.should == 0
116
+ end
117
+
118
+ it "should return no results when a blank search is made" do
119
+ Product.search("").size.should == 0
120
+ end
121
+
122
+ it "should return results when a blank search is made when :allow_empty_search is true" do
123
+ Product.allow_empty_search = true
124
+ Product.search("").size.should == 1
125
+ end
126
+
127
+ it "should search for embedded documents" do
128
+ Product.search("craddle").size.should == 1
129
+ end
130
+
131
+ it 'should work in a chainable fashion' do
132
+ @product.category.products.where(:brand => 'Apple').search('apple').size.should == 1
133
+ @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
+ end
137
+
138
+ it 'should return the classes that include the search module' do
139
+ MongoMapper::Search.classes.should == [Product]
140
+ end
141
+
142
+ it 'should have a method to index keywords' do
143
+ @product.index_keywords!.should == true
144
+ end
145
+
146
+ it 'should have a class method to index all documents keywords' do
147
+ Product.index_keywords!.should_not include(false)
148
+ end
149
+
150
+ end
@@ -0,0 +1,31 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'mongo_mapper'
7
+ require 'database_cleaner'
8
+ require 'fast_stemmer'
9
+ require 'yaml'
10
+ require 'mongomapper_search'
11
+
12
+ MongoMapper.connection = Mongo::Connection.new("localhost", 27017)
13
+ MongoMapper.database = "mongomapper_search_test"
14
+
15
+ Dir["#{File.dirname(__FILE__)}/models/*.rb"].each { |file| require file }
16
+
17
+ DatabaseCleaner.orm = :mongo_mapper
18
+
19
+ RSpec.configure do |config|
20
+ config.before(:all) do
21
+ DatabaseCleaner.strategy = :truncation
22
+ end
23
+
24
+ config.before(:each) do
25
+ DatabaseCleaner.start
26
+ end
27
+
28
+ config.after(:each) do
29
+ DatabaseCleaner.clean
30
+ end
31
+ end
data/spec/util_spec.rb ADDED
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ describe Util do
5
+ it "should return an empty array if no text is passed" do
6
+ Util.normalize_keywords("", false, "").should == []
7
+ end
8
+
9
+ it "should return an array of keywords" do
10
+ Util.normalize_keywords("keyword", false, "").class.should == Array
11
+ end
12
+
13
+ it "should return an array of strings" do
14
+ Util.normalize_keywords("keyword", false, "").first.class.should == String
15
+ end
16
+
17
+ it "should remove accents from the text passed" do
18
+ Util.normalize_keywords("café", false, "").should == ["cafe"]
19
+ end
20
+
21
+ it "should downcase the text passed" do
22
+ Util.normalize_keywords("CaFé", false, "").should == ["cafe"]
23
+ end
24
+
25
+ it "should split whitespaces, hifens, dots, underlines, etc.." do
26
+ Util.normalize_keywords("CaFé-express.com delicious;come visit, and 'win' an \"iPad\"", false, "").should == ["cafe", "express", "com", "delicious", "come", "visit", "and", "win", "an", "ipad"]
27
+ end
28
+
29
+ it "should stem keywords" do
30
+ Util.normalize_keywords("A runner running and eating", true, "").should == ["runner", "run", "and", "eat"]
31
+ end
32
+
33
+ it "should ignore keywords with less than two words" do
34
+ Util.normalize_keywords("A runner running", false, "").should_not include "a"
35
+ end
36
+
37
+ it "should not ignore numbers" do
38
+ Util.normalize_keywords("Ford T 1908", false, "").should include "1908"
39
+ end
40
+
41
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongomapper_search
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mário Peixoto
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-18 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongo_mapper
16
+ requirement: &70143029026220 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.9.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70143029026220
25
+ - !ruby/object:Gem::Dependency
26
+ name: bson_ext
27
+ requirement: &70143029025740 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 1.2.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70143029025740
36
+ - !ruby/object:Gem::Dependency
37
+ name: fast-stemmer
38
+ requirement: &70143029025260 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.0.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70143029025260
47
+ - !ruby/object:Gem::Dependency
48
+ name: database_cleaner
49
+ requirement: &70143029024780 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.6.4
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70143029024780
58
+ - !ruby/object:Gem::Dependency
59
+ name: rake
60
+ requirement: &70143029024300 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 0.9.2
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70143029024300
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: &70143029023820 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '2.4'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70143029023820
80
+ description: Simple full text search for MongoMapper ODM
81
+ email:
82
+ - mario.peixoto@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - lib/mongomapper_search/query.rb
88
+ - lib/mongomapper_search/search.rb
89
+ - lib/mongomapper_search/util.rb
90
+ - lib/mongomapper_search.rb
91
+ - LICENSE
92
+ - README.md
93
+ - Rakefile
94
+ - spec/models/category.rb
95
+ - spec/models/product.rb
96
+ - spec/models/subproduct.rb
97
+ - spec/models/tag.rb
98
+ - spec/models/variant.rb
99
+ - spec/search_spec.rb
100
+ - spec/spec_helper.rb
101
+ - spec/util_spec.rb
102
+ homepage: http://github.com/mariopeixoto/mongomapper_search
103
+ licenses: []
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: 1.3.6
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 1.8.7
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: Search implementation for MongoMapper ODM
126
+ test_files:
127
+ - spec/models/category.rb
128
+ - spec/models/product.rb
129
+ - spec/models/subproduct.rb
130
+ - spec/models/tag.rb
131
+ - spec/models/variant.rb
132
+ - spec/search_spec.rb
133
+ - spec/spec_helper.rb
134
+ - spec/util_spec.rb