mongomapper_search 0.0.1

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