Permalink
Browse files

Working on a series of improvements:

* Updates Mongoid to 3.0, discontinue support for 2.0
* Changes how configurations are made
* Refactors a bunch of methods
* Applies a series of minor fixes

Relevant search is broken, fix to come in another commit.
  • Loading branch information...
1 parent 67008e9 commit 3fcb5f6230572476dab0f1174b745f98d3eed065 @mauriciozaffari mauriciozaffari committed Jul 10, 2012
View
@@ -4,32 +4,33 @@ PATH
mongoid_search (0.2.7)
bson_ext (>= 1.2)
fast-stemmer (~> 1.0.0)
- mongoid (>= 2.0.0)
+ mongoid (>= 3.0.0)
GEM
remote: http://rubygems.org/
specs:
- activemodel (3.1.3)
- activesupport (= 3.1.3)
+ activemodel (3.2.6)
+ activesupport (= 3.2.6)
builder (~> 3.0.0)
+ activesupport (3.2.6)
i18n (~> 0.6)
- activesupport (3.1.3)
multi_json (~> 1.0)
- bson (1.5.2)
- bson_ext (1.5.2)
- bson (= 1.5.2)
+ bson (1.6.4)
+ bson_ext (1.6.4)
+ bson (~> 1.6.4)
builder (3.0.0)
- database_cleaner (0.6.7)
+ database_cleaner (0.8.0)
diff-lcs (1.1.2)
- fast-stemmer (1.0.0)
+ fast-stemmer (1.0.1)
i18n (0.6.0)
- mongo (1.5.2)
- bson (= 1.5.2)
- mongoid (2.3.4)
+ mongoid (3.0.0)
activemodel (~> 3.1)
- mongo (~> 1.3)
+ moped (~> 1.1.1)
+ origin (~> 1.0.3)
tzinfo (~> 0.3.22)
- multi_json (1.0.4)
+ moped (1.1.1)
+ multi_json (1.3.6)
+ origin (1.0.4)
rake (0.8.7)
rspec (2.6.0)
rspec-core (~> 2.6.0)
@@ -42,13 +43,13 @@ GEM
simplecov (0.4.2)
simplecov-html (~> 0.4.4)
simplecov-html (0.4.5)
- tzinfo (0.3.31)
+ tzinfo (0.3.33)
PLATFORMS
ruby
DEPENDENCIES
- database_cleaner (~> 0.6.4)
+ database_cleaner (>= 0.8.0)
mongoid_search!
rake (~> 0.8.7)
rspec (~> 2.4)
View
@@ -1,4 +1,53 @@
+# encoding: utf-8
+
require 'mongoid_search/railtie' if defined?(Rails)
require 'mongoid_search/util'
require 'mongoid_search/log'
require 'mongoid_search/mongoid_search'
+
+module Mongoid::Search
+ # Default matching type. Match :any or :all searched keywords
+ mattr_accessor :match
+ @@match = :any
+
+ # If true, an empty search will return all objects
+ mattr_accessor :allow_empty_search
+ @@allow_empty_search = false
+
+ # If true, will search with relevance information
+ mattr_accessor :relevant_search
+ @@relevant_search = false
+
+ # Stem keywords
+ mattr_accessor :stem_keywords
+ @@stem_keywords = false
+
+ # Words to ignore
+ mattr_accessor :ignore_list
+ @@ignore_list = %w{ a an to from as }
+ # From a file
+ # @@ignore_list = YAML.load(File.open(File.dirname(__FILE__) + '/config/ignorelist.yml'))["ignorelist"]
+
+ # Search using regex (slower)
+ mattr_accessor :regex_search
+ @@regex_search = true
+
+ # Regex to search
+ mattr_accessor :regex
+
+ # Match partial words on both sides (slower)
+ @@regex = Proc.new { |query| /#{query}/ }
+
+ # Match partial words on the beginning or in the end (slightly faster)
+ # @@regex = Proc.new { |query| /ˆ#{query}/ }
+ # @@regex = Proc.new { |query| /#{query}$/ }
+
+ # Ligatures to be replaced
+ # http://en.wikipedia.org/wiki/Typographic_ligature
+ mattr_accessor :ligatures
+ @@ligatures = { "œ"=>"oe", "æ"=>"ae" }
+
+ # Minimum word size. Words smaller than it won't be indexed
+ mattr_accessor :minimum_word_size
+ @@minimum_word_size = 2
+end
@@ -1,100 +1,103 @@
module Mongoid::Search
- extend ActiveSupport::Concern
+ def self.included(base)
+ base.send(:cattr_accessor, :search_fields)
- included do
- cattr_accessor :search_fields, :match, :allow_empty_search, :relevant_search, :stem_keywords, :ignore_list
- end
+ base.extend ClassMethods
- def self.included(base)
- @classes ||= []
- @classes << base
+ @@classes ||= []
+ @@classes << base
end
def self.classes
- @classes
+ @@classes
end
module ClassMethods #:nodoc:
# Set a field or a number of fields as sources for search
def search_in(*args)
- options = args.last.is_a?(Hash) && [:match, :allow_empty_search, :relevant_search, :stem_keywords, :ignore_list].include?(args.last.keys.first) ? args.pop : {}
- self.match = [:any, :all].include?(options[:match]) ? options[:match] : :any
- self.allow_empty_search = [true, false].include?(options[:allow_empty_search]) ? options[:allow_empty_search] : false
- self.relevant_search = [true, false].include?(options[:relevant_search]) ? options[:allow_empty_search] : false
- self.stem_keywords = [true, false].include?(options[:stem_keywords]) ? options[:allow_empty_search] : false
- self.ignore_list = YAML.load(File.open(options[:ignore_list]))["ignorelist"] if options[:ignore_list].present?
- self.search_fields = (self.search_fields || []).concat args
+ args, options = args_and_options(args)
+ self.search_fields = (self.search_fields || []).concat args
field :_keywords, :type => Array
-
- Gem.loaded_specs["mongoid"].version.to_s.include?("3.0") ? (index({_keywords: 1}, {background: true})) : (index :_keywords, :background => true)
+
+ index({ :_keywords => 1 }, { :background => true })
before_save :set_keywords
end
- def search(query, options={})
- if relevant_search
+ def full_text_search(query, options={})
+ options = extract_options(options)
+ return (options[:allow_empty_search] ? criteria.all : []) if query.blank?
+
+ if options[:relevant_search]
search_relevant(query, options)
else
search_without_relevance(query, options)
end
end
- # Mongoid 2.0.0 introduces Criteria.seach so we need to provide
- # alternate method
- alias csearch search
+ # Keeping these aliases for compatibility purposes
+ alias csearch full_text_search
+ alias search full_text_search
+
+ # Goes through all documents in the class that includes Mongoid::Search
+ # and indexes the keywords.
+ def index_keywords!
+ all.each { |d| d.index_keywords! ? Log.green(".") : Log.red("F") }
+ end
+
+ private
+ def query(keywords, options)
+ criteria.send("#{(options[:match]).to_s}_of", *keywords.map { |kw| { :_keywords => Mongoid::Search.regex.call(kw) } })
+ end
+
+ def args_and_options(args)
+ options = args.last.is_a?(Hash) &&
+ [:match,
+ :allow_empty_search,
+ :relevant_search].include?(args.last.keys.first) ? args.pop : {}
+
+ [args, extract_options(options)]
+ end
- def search_without_relevance(query, options={})
- return criteria.all if query.blank? && allow_empty_search
- criteria.send("#{(options[:match]||self.match).to_s}_in", :_keywords => Util.normalize_keywords(query, stem_keywords, ignore_list).map { |q| /#{q}/ })
+ def extract_options(options)
+ {
+ :match => options[:match] || Mongoid::Search.match,
+ :allow_empty_search => options[:allow_empty_search] || Mongoid::Search.allow_empty_search,
+ :relevant_search => options[:relevant_search] || Mongoid::Search.relevant_search
+ }
end
- def search_relevant(query, options={})
- return criteria.all if query.blank? && allow_empty_search
+ def search_without_relevance(query, options)
+ query(Util.normalize_keywords(query), options)
+ end
- keywords = Util.normalize_keywords(query, stem_keywords, ignore_list)
+ def search_relevant(query, options)
+ keywords = Util.normalize_keywords(query)
- map = <<-EOS
+ map = %Q{
function() {
- var entries = 0
- for(i in keywords)
+ var entries = 0;
+ for(i in keywords) {
for(j in this._keywords) {
- if(this._keywords[j] == keywords[i])
- entries++
+ if(this._keywords[j] == keywords[i]) {
+ entries++;
+ }
}
- if(entries > 0)
- emit(this._id, entries)
+ }
+ if(entries > 0) {
+ emit(this._id, entries);
+ }
}
- EOS
- reduce = <<-EOS
+ }
+
+ reduce = %Q{
function(key, values) {
- return(values[0])
+ return(values[0]);
}
- EOS
-
- #raise [self.class, self.inspect].inspect
-
- kw_conditions = keywords.map do |kw|
- {:_keywords => kw}
- end
-
- criteria = (criteria || self).any_of(*kw_conditions)
-
- query = criteria.selector
+ }
- options.delete(:limit)
- options.delete(:skip)
- options.merge! :scope => {:keywords => keywords}, :query => query
-
- # res = collection.map_reduce(map, reduce, options)
- # res.find.sort(['value', -1]) # Cursor
- collection.map_reduce(map, reduce, options)
- end
-
- # Goes through all documents in the class that includes Mongoid::Search
- # and indexes the keywords.
- def index_keywords!
- all.each { |d| d.index_keywords! ? Log.green(".") : Log.red("F") }
+ query(keywords, options).map_reduce(map, reduce).out(:inline => 1)
end
end
@@ -105,7 +108,7 @@ def index_keywords!
private
def set_keywords
self._keywords = self.search_fields.map do |field|
- Util.keywords(self, field, stem_keywords, ignore_list)
+ Util.keywords(self, field)
end.flatten.reject{|k| k.nil? || k.empty?}.uniq.sort
end
end
View
@@ -1,38 +1,40 @@
# encoding: utf-8
module Util
- def self.keywords(klass, field, stem_keywords, ignore_list)
+ def self.keywords(klass, field)
if field.is_a?(Hash)
field.keys.map do |key|
attribute = klass.send(key)
unless attribute.blank?
method = field[key]
if attribute.is_a?(Array)
if method.is_a?(Array)
- method.map {|m| attribute.map { |a| Util.normalize_keywords a.send(m), stem_keywords, ignore_list } }
+ method.map {|m| attribute.map { |a| Util.normalize_keywords a.send(m) } }
else
- attribute.map(&method).map { |t| Util.normalize_keywords t, stem_keywords, ignore_list }
+ attribute.map(&method).map { |t| Util.normalize_keywords t }
end
elsif attribute.is_a?(Hash)
if method.is_a?(Array)
- method.map {|m| Util.normalize_keywords attribute[m.to_sym], stem_keywords, ignore_list }
+ method.map {|m| Util.normalize_keywords attribute[m.to_sym] }
else
- Util.normalize_keywords(attribute[method.to_sym], stem_keywords, ignore_list)
+ Util.normalize_keywords(attribute[method.to_sym])
end
else
- Util.normalize_keywords(attribute.send(method), stem_keywords, ignore_list)
+ Util.normalize_keywords(attribute.send(method))
end
end
end
else
value = klass[field]
value = value.join(' ') if value.respond_to?(:join)
- Util.normalize_keywords(value, stem_keywords, ignore_list) if value
+ Util.normalize_keywords(value) if value
end
end
- def self.normalize_keywords(text, stem_keywords, ignore_list)
- ligatures = {"œ"=>"oe", "æ"=>"ae"}
+ def self.normalize_keywords(text)
+ ligatures = Mongoid::Search.ligatures
+ ignore_list = Mongoid::Search.ignore_list
+ stem_keywords = Mongoid::Search.stem_keywords
return [] if text.blank?
text = text.to_s.
@@ -44,7 +46,7 @@ def self.normalize_keywords(text, stem_keywords, ignore_list)
gsub(/[^[:alnum:]\s]/,''). # strip accents
gsub(/[#{ligatures.keys.join("")}]/) {|c| ligatures[c]}.
split(' ').
- reject { |word| word.size < 2 }
+ reject { |word| word.size < Mongoid::Search.minimum_word_size }
text = text.reject { |word| ignore_list.include?(word) } unless ignore_list.blank?
text = text.map(&:stem) if stem_keywords
text
View
@@ -13,11 +13,11 @@ Gem::Specification.new do |s|
s.required_rubygems_version = ">= 1.3.6"
- s.add_dependency("mongoid", [">= 2.0.0"])
+ s.add_dependency("mongoid", [">= 3.0.0"])
s.add_dependency("bson_ext", [">= 1.2"])
s.add_dependency("fast-stemmer", ["~> 1.0.0"])
- s.add_development_dependency("database_cleaner", ["~> 0.6.4"])
+ s.add_development_dependency("database_cleaner", [">= 0.8.0"])
s.add_development_dependency("rake", ["~> 0.8.7"])
s.add_development_dependency("rspec", ["~> 2.4"])
View
@@ -2,5 +2,5 @@ class Category
include Mongoid::Document
field :name
- references_many :products
+ has_many :products
end
View
@@ -6,9 +6,9 @@ class Product
field :attrs, :type => Array
field :info, :type => Hash
- references_many :tags
- referenced_in :category
- embeds_many :subproducts
+ has_many :tags
+ belongs_to :category
+ embeds_many :subproducts
search_in :brand, :name, :outlet, :attrs, :tags => :name, :category => :name,
:subproducts => [:brand, :name], :info => [ :summary, :description ]
@@ -4,5 +4,5 @@ class Subproduct
field :brand
field :name
- embedded_in :product, :inverse_of => :subproducts
+ belongs_to :product, :inverse_of => :subproducts
end
Oops, something went wrong.

0 comments on commit 3fcb5f6

Please sign in to comment.