mongoid_search 0.2.8 → 0.3.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
@@ -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
- references_many :tags
27
- refereced_in :category
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
- referenced_in :product
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
- references_many :products
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.search("apple iphone").size
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.search("ipho").size
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
- @category.products.where(:price.lt => 499).csearch('iphone').asc(:price)
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
- In this case we have to use csearch, an alias for search, because since v2.0.0
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
- search_in :brand, :name, { :tags => :name }, { :match => :any }
88
+ Product.full_text_search("apple motorola", match: :any).size
89
+ => 1
84
90
 
85
- Product.search("apple motorola").size
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
- search_in :brand, :name, { :tags => :name }, { :match => :all }
105
+ relevant_search:
89
106
 
90
- Product.search("apple motorola").size
91
- => 0
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
- search_in :brand, :name, { :tags => :name }, { :allow_empty_search => true }
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
- Product.search("").size
101
- => 1
102
-
103
- ignore_list:
104
- Pass in an ignore list location. Keywords in that list will be ignored.
105
-
106
- search_in :brand, :name, { :tags => :name }, { :ignore_list => Rails.root.join("config", "ignorelist.yml") }
107
-
108
- The list should look like:
109
-
110
- ignorelist:
111
- a, an, to, from, as
112
-
113
- You can include how many keywords you like.
114
-
115
- TODO
116
- ----
117
-
118
- * Strip html with sanitize (https://github.com/rgrove/sanitize)
119
- * Rewrite and test relevant search
120
- * Move all configurations to a configuration file. Maybe /config/mongoid_search.yml.
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
 
@@ -1,4 +1,4 @@
1
- class Log
1
+ class Mongoid::Search::Log
2
2
  cattr_accessor :silent
3
3
 
4
4
  def self.log(message)
@@ -1,100 +1,120 @@
1
1
  module Mongoid::Search
2
- extend ActiveSupport::Concern
2
+ def self.included(base)
3
+ base.send(:cattr_accessor, :search_fields)
3
4
 
4
- included do
5
- cattr_accessor :search_fields, :match, :allow_empty_search, :relevant_search, :stem_keywords, :ignore_list
6
- end
5
+ base.extend ClassMethods
7
6
 
8
- def self.included(base)
9
- @classes ||= []
10
- @classes << base
7
+ @@classes ||= []
8
+ @@classes << base
11
9
  end
12
10
 
13
11
  def self.classes
14
- @classes
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
- options = args.last.is_a?(Hash) && [:match, :allow_empty_search, :relevant_search, :stem_keywords, :ignore_list].include?(args.last.keys.first) ? args.pop : {}
21
- self.match = [:any, :all].include?(options[:match]) ? options[:match] : :any
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
- Gem.loaded_specs["mongoid"].version.to_s.include?("3.0") ? (index({_keywords: 1}, {background: true})) : (index :_keywords, :background => true)
22
+
23
+ index({ :_keywords => 1 }, { :background => true })
31
24
 
32
25
  before_save :set_keywords
33
26
  end
34
27
 
35
- def search(query, options={})
36
- if relevant_search
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
- # Mongoid 2.0.0 introduces Criteria.seach so we need to provide
44
- # alternate method
45
- alias csearch search
39
+ # Keeping these aliases for compatibility purposes
40
+ alias csearch full_text_search
41
+ alias search full_text_search
46
42
 
47
- def search_without_relevance(query, options={})
48
- return criteria.all if query.blank? && allow_empty_search
49
- criteria.send("#{(options[:match]||self.match).to_s}_in", :_keywords => Util.normalize_keywords(query, stem_keywords, ignore_list).map { |q| /#{q}/ })
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
- def search_relevant(query, options={})
53
- return criteria.all if query.blank? && allow_empty_search
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
- keywords = Util.normalize_keywords(query, stem_keywords, ignore_list)
56
+ criteria.send("#{(options[:match]).to_s}_of", *keywords_hash)
57
+ end
56
58
 
57
- map = <<-EOS
58
- function() {
59
- var entries = 0
60
- for(i in keywords)
61
- for(j in this._keywords) {
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
- #raise [self.class, self.inspect].inspect
65
+ [args, extract_options(options)]
66
+ end
76
67
 
77
- kw_conditions = keywords.map do |kw|
78
- {:_keywords => kw}
79
- end
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
- criteria = (criteria || self).any_of(*kw_conditions)
76
+ def search_without_relevance(query, options)
77
+ query(Util.normalize_keywords(query), options)
78
+ end
82
79
 
83
- query = criteria.selector
80
+ def search_relevant(query, options)
81
+ results_with_relevance(query, options).sort { |o| o['value'] }.map do |r|
84
82
 
85
- options.delete(:limit)
86
- options.delete(:skip)
87
- options.merge! :scope => {:keywords => keywords}, :query => query
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
- # res = collection.map_reduce(map, reduce, options)
90
- # res.find.sort(['value', -1]) # Cursor
91
- collection.map_reduce(map, reduce, options)
89
+ end
92
90
  end
93
91
 
94
- # Goes through all documents in the class that includes Mongoid::Search
95
- # and indexes the keywords.
96
- def index_keywords!
97
- all.each { |d| d.index_keywords! ? Log.green(".") : Log.red("F") }
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, stem_keywords, ignore_list)
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
@@ -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, stem_keywords, ignore_list)
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| Util.normalize_keywords a.send(m), stem_keywords, ignore_list } }
12
+ method.map {|m| attribute.map { |a| normalize_keywords a.send(m) } }
13
13
  else
14
- attribute.map(&method).map { |t| Util.normalize_keywords t, stem_keywords, ignore_list }
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| Util.normalize_keywords attribute[m.to_sym], stem_keywords, ignore_list }
18
+ method.map {|m| normalize_keywords attribute[m.to_sym] }
19
19
  else
20
- Util.normalize_keywords(attribute[method.to_sym], stem_keywords, ignore_list)
20
+ normalize_keywords(attribute[method.to_sym])
21
21
  end
22
22
  else
23
- Util.normalize_keywords(attribute.send(method), stem_keywords, ignore_list)
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
- Util.normalize_keywords(value, stem_keywords, ignore_list) if value
30
+ normalize_keywords(value) if value
31
31
  end
32
32
  end
33
33
 
34
- def self.normalize_keywords(text, stem_keywords, ignore_list)
35
- ligatures = {"œ"=>"oe", "æ"=>"ae"}
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 < 2 }
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
@@ -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'
@@ -2,5 +2,5 @@ class Category
2
2
  include Mongoid::Document
3
3
  field :name
4
4
 
5
- references_many :products
5
+ has_many :products
6
6
  end
@@ -6,9 +6,9 @@ class Product
6
6
  field :attrs, :type => Array
7
7
  field :info, :type => Hash
8
8
 
9
- references_many :tags
10
- referenced_in :category
11
- embeds_many :subproducts
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 ]
@@ -4,5 +4,5 @@ class Subproduct
4
4
  field :brand
5
5
  field :name
6
6
 
7
- embedded_in :product, :inverse_of => :subproducts
7
+ belongs_to :product, :inverse_of => :subproducts
8
8
  end
data/spec/models/tag.rb CHANGED
@@ -2,5 +2,5 @@ class Tag
2
2
  include Mongoid::Document
3
3
  field :name
4
4
 
5
- referenced_in :product
5
+ belongs_to :product
6
6
  end
@@ -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
- Product.stem_keywords = false
9
- Product.ignore_list = nil
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.search("Info-summary").first.should eq @product
23
- Product.search("Info-description").first.should eq @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.search("Info-description").size.should eq 0
35
- Product.search("Info-summary").size.should eq 0
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(:each) {
42
- Product.stem_keywords = false
43
- Product.ignore_list = nil
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.search("ЭЛЬБРУС").size.should == 1
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.search(:name => 'Apple', :color => :white).should eq [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.search("œuvre").should eq [variant1]
95
- Variant.search("oeuvre").should eq [variant1]
96
- Variant.search("æquo").should eq [variant2]
97
- Variant.search("aequo").should eq [variant2]
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
- Product.stem_keywords = true
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
- Product.ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
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.search("apple").size.should == 1
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.search("online").size.should == 1
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.search("Ole").size.should == 1
134
- Product.search("Olé").size.should == 1
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.search("oLe").size.should == 1
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.search("iph").size.should == 1
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.search("apple motorola").size.should == 1
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
- Product.match = :all
151
- Product.search("apple motorola").size.should == 0
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 .search" do
155
- Product.match = :all
156
- Product.search("apple motorola", :match => :any).size.should == 1
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 .search" do
160
- Product.search("apple motorola", :match => :all).size.should == 0
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
- Product.search("").size.should == 0
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
- Product.allow_empty_search = true
169
- Product.search("").size.should == 1
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.search("craddle").size.should == 1
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').csearch('apple').size.should == 1
178
- @product.category.products.csearch('craddle').where(:brand => 'Apple').size.should == 1
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
- name = "mongoid_search_test"
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("", false, "").should == []
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", false, "").class.should == Array
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", false, "").first.class.should == String
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é", false, "").should == ["cafe"]
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é", false, "").should == ["cafe"]
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("Кафе", false, "").should == ["кафе"]
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\"", false, "").should == ["cafe", "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
- Util.normalize_keywords("A runner running and eating", true, "").should == ["runner", "run", "and", "eat"]
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
- Util.normalize_keywords("An amazing awesome runner running and eating", true, YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]).should == ["an", "runner", "run", "and", "eat"]
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", false, "").should_not include "a"
50
+ Mongoid::Search::Util.normalize_keywords("A runner running").should_not include "a"
43
51
  end
44
52
 
45
- it "should not ignore numbers" do
46
- Util.normalize_keywords("Ford T 1908", false, "").should include "1908"
47
- end
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.2.8
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: &70364721915420 !ruby/object:Gem::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: 2.0.0
21
+ version: 3.0.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70364721915420
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: &70364721929380 !ruby/object:Gem::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: *70364721929380
35
+ version_requirements: *70200267465820
47
36
  - !ruby/object:Gem::Dependency
48
37
  name: database_cleaner
49
- requirement: &70364721927540 !ruby/object:Gem::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.6.4
43
+ version: 0.8.0
55
44
  type: :development
56
45
  prerelease: false
57
- version_requirements: *70364721927540
46
+ version_requirements: *70200267464480
58
47
  - !ruby/object:Gem::Dependency
59
48
  name: rake
60
- requirement: &70364721926220 !ruby/object:Gem::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: *70364721926220
57
+ version_requirements: *70200267462600
69
58
  - !ruby/object:Gem::Dependency
70
59
  name: rspec
71
- requirement: &70364721925360 !ruby/object:Gem::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: *70364721925360
68
+ version_requirements: *70200267461180
80
69
  description: Simple full text search implementation.
81
70
  email:
82
71
  - mauricio@papodenerd.net