mongoid_search 0.2.8 → 0.3.0

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