module Mongoid::Search extend ActiveSupport::Concern included do cattr_accessor :search_fields @@classes ||= [] @@classes << self end def self.classes @@classes end module ClassMethods #:nodoc: # Set a field or a number of fields as sources for search def search_in(*args) args, options = args_and_options(args) set_search_fields(options[:index], args) field options[:index], type: Array index({ options[:index] => 1 }, background: true) before_save :set_keywords end def full_text_search(query, options = {}) options = extract_options(options) attr_accessor :relevance if options[:relevant_search].eql? true 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 # 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 set_search_fields(index, fields) self.search_fields ||= {} (self.search_fields[index] ||= []).concat fields end def query(keywords, options) keywords_hash = keywords.map do |kw| if Mongoid::Search.regex_search escaped_kw = Regexp.escape(kw) kw = Mongoid::Search.regex.call(escaped_kw) end { options[:index] => kw } end criteria.send("#{(options[:match])}_of", *keywords_hash) end def args_and_options(args) options = args.last.is_a?(Hash) && %i[match allow_empty_search index relevant_search].include?(args.last.keys.first) ? args.pop : {} [args, extract_options(options)] end 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, index: options[:index] || :_keywords } end def search_without_relevance(query, options) query(Util.normalize_keywords(query), options) end def search_relevant(query, options) results_with_relevance(query, options).sort { |o| o['value'] }.map do |r| new(r['_id'].merge(relevance: r['value'])) do |o| # Need to match the actual object o.instance_variable_set('@new_record', false) o._id = r['_id']['_id'] end end end def results_with_relevance(query, options) keywords = Mongoid::Search::Util.normalize_keywords(query) map = %{ function() { var entries = 0; for(i in keywords) { for(j in this.#{options[:index]}) { if(this.#{options[:index]}[j] == keywords[i]) { entries++; } } } if(entries > 0) { emit(this, entries); } } } reduce = %{ function(key, values) { return(values); } } query(keywords, options).map_reduce(map, reduce).scope(keywords: keywords).out(inline: 1) end end def index_keywords! search_fields.map do |index, fields| update_attribute(index, get_keywords(fields)) end end def set_keywords search_fields.each do |index, fields| send("#{index}=", get_keywords(fields)) end end def get_keywords(fields) Mongoid::Search::Util.keywords(self, fields) .flatten.reject { |k| k.nil? || k.empty? }.uniq.sort end end