diff --git a/lib/smart_search.rb b/lib/smart_search.rb index 15f3574..b4f7f50 100755 --- a/lib/smart_search.rb +++ b/lib/smart_search.rb @@ -17,29 +17,40 @@ def self.included(base) module ClassMethods # Enable SmartSearch for the current ActiveRecord model. def smart_search(options = {:on => [], :split => false}) - if table_exists? - # Check if search_tags exists - if !is_smart_search? - - cattr_accessor :condition_default, :group_default, :tags, :order_default, :enable_similarity, :default_template_path - - # BETA! - cattr_accessor :split_searchable_fields - self.split_searchable_fields = options[:split] - - send :include, InstanceMethods - self.send(:after_commit, :create_search_tags, :if => :update_search_tags?) unless options[:auto] == false - self.send(:after_destroy, :clear_search_tags) - self.enable_similarity ||= true + begin + if table_exists? + if SmartSearch::Config.search_models.index(self.name).nil? + SmartSearch::Config.search_models << self.name + end + # Check if search_tags exists + if !is_smart_search? + + cattr_accessor :condition_default, :group_default, :tags, :order_default, :enable_similarity, :default_template_path + + # BETA! + cattr_accessor :split_searchable_fields + self.split_searchable_fields = options[:split] + + send :include, InstanceMethods + self.send(:after_commit, :create_search_tags, :if => :update_search_tags?) unless options[:auto] == false + self.send(:after_destroy, :clear_search_tags) + if options[:disable_similarity].present? + self.enable_similarity = false + else + self.enable_similarity = true + end - attr_accessor :query_score, :dont_update_search_tags + attr_accessor :query_score, :dont_update_search_tags - self.tags = options[:on] || [] - elsif is_smart_search? && Rails.env.production? - # Allow re-adding attributes for search - logger.info("Re-Adding search data on #{self.name}: #{options[:on].inspect}".yellow) - self.tags += options[:on] + self.tags = options[:on] || [] + elsif is_smart_search? + # Allow re-adding attributes for search + logger.info("Re-Adding search data on #{self.name}: #{options[:on].inspect}".yellow) + self.tags += options[:on] + end end + rescue ActiveRecord::NoDatabaseError => e + puts "Could not set up smart_search due to missing database" end end @@ -49,40 +60,54 @@ def is_smart_search? end # Serach database for given search tags - def find_by_tags(tags = "", options = {}) + def find_by_tags(tags = "", options = {}, &block) tags = store_history_and_get_sanitized_search_tags(tags) tags = map_similarity_tags(tags) - # Load ranking from Search tags - result_ids = [] - result_scores = {} + base_select = "#{self.quoted_table_name}.* #{SmartSearch::Config.order_by_score ? ', SUM(boost) AS score' : ''}" - results = SmartSearchTag.select("entry_id, sum(boost) as score").group(:entry_id).where(table_name: self.table_name) + join_query = "LEFT JOIN #{SmartSearchTag.quoted_table_name} + ON #{SmartSearchTag.quoted_table_name}.#{ActiveRecord::Base.connection.quote_column_name('table_name')} = '#{self.table_name}' + AND #{SmartSearchTag.quoted_table_name}.#{ActiveRecord::Base.connection.quote_column_name('entry_id')} = #{self.quoted_table_name}.#{self.primary_key}" - query = case ActiveRecord::Base.connection.adapter_name - when 'PostgreSQL' - "select entry_id, sum(boost) as score - from smart_search_tags where #{ActiveRecord::Base.connection.quote_column_name('table_name')}= '#{self.table_name}' - group by entry_id - HAVING (#{tags.join(' AND ')}) - order by score DESC" - else - "select entry_id, sum(boost) as score, #{adapater_based_group_method}(search_tags) as grouped_tags - from smart_search_tags where #{ActiveRecord::Base.connection.quote_column_name('table_name')}= '#{self.table_name}' and - (#{tags.join(' AND ')}) group by entry_id order by score DESC" - end + group_clause = "#{self.quoted_table_name}.#{self.primary_key}, #{SmartSearchTag.column_names.map {|c| SmartSearchTag.quoted_table_name + '.' + c }.join(", ") }" + + order_clause = if self.order_default + "#{self.order_default}" + elsif SmartSearch::Config.order_by_score + "score DESC" + end + + results = case ActiveRecord::Base.connection.adapter_name + when 'PostgreSQL' + self.select(base_select) + .joins(join_query) + .group(group_clause) + .having("#{tags.join(' AND ')}") + else + self.select(base_select) + .joins(join_query) + .group(group_clause) + .having("#{tags.join(' AND ')}") + end + + if options[:distinct] == true + results = results.distinct + end - SmartSearchTag.connection.select_all(query).each do |r| - result_ids << r["entry_id"].to_i - result_scores[r["entry_id"].to_i] = r['score'].to_f + if block_given? + results = yield(results) end - results = self.where(self.primary_key => result_ids) + results = results.offset(options[:offset]) if options[:offset] + results = results.limit(options[:per_page]) if options[:per_page] + + return results end - def find_by_splitted_tags(search_fields = {}) + def find_by_splitted_tags(search_fields = {}, &block) sanitized_search_fields = {} search_fields.each do |field, tags| next if tags.blank? @@ -100,14 +125,18 @@ def find_by_splitted_tags(search_fields = {}) end result_ids = eval(result_list.map(&:to_s).join(" & ")) - self.where(self.primary_key => result_ids) + results = self.where(self.primary_key => result_ids) + + if block_given? + results = yield(results) + end end # Private Query Helper Methods private def store_history_and_get_sanitized_search_tags(orig_tags) orig_tags = orig_tags.join(" ") if orig_tags.is_a?(Array) - sanitized_tags = orig_tags.gsub(/[\(\)\[\]\'\"\*\%\|\&]/, '').split(/[\ -]/).select {|t| !t.blank?} + sanitized_tags = orig_tags.gsub(/[\(\)\[\]\'\"\*\%\|\&\+\.\$\?]/, ' ').split(/[\ -]/).select {|t| !t.blank?} # Save Data for similarity analysis if sanitized_tags.join(' ').size > 3 @@ -131,7 +160,7 @@ def map_similarity_tags(tags) when 'PostgreSQL' "string_agg(search_tags, ' ') ~* '#{similars}'" else - "search_tags REGEXP '#{similars}'" + "search_tags REGEXP '#{similars}'" end end @@ -187,23 +216,33 @@ def update_search_tags? end # create search tags for this very record based on the attributes defined in ':on' option passed to the 'Class.smart_search' method - def create_search_tags + def create_search_tags(options = {}) # storing tags must never fail the systems begin self.get_calculated_tags_list! self.clear_search_tags + data = [] + (self.class.split_searchable_fields ? @calculated_tags_list : get_merged_calculated_tags).each do |t| if !t[:search_tags].blank? && t[:search_tags].size > 1 - begin - SmartSearchTag.create(t.merge!(:table_name => self.class.table_name, :entry_id => self.id, :search_tags => t[:search_tags].strip.split(" ").uniq.join(" "))) - rescue Exception => e - + if options[:export] + data << t.merge!(:table_name => self.class.table_name, :entry_id => self.id, :search_tags => t[:search_tags].strip.split(" ").uniq.join(" ")) + else + begin + SmartSearchTag.create(t.merge!(:table_name => self.class.table_name, :entry_id => self.id, :search_tags => t[:search_tags].strip.split(" ").uniq.join(" "))) + rescue Exception => e + + end end end end + if options[:export] + return data + end + rescue Exception => e Rails.logger.error "SMART SEARCH FAILED TO TO STORE SEARCH TAGS #{self.class.name} #{self.id}" Rails.logger.error e.message @@ -268,9 +307,11 @@ class Config cattr_accessor :search_models cattr_accessor :public_models + cattr_accessor :order_by_score self.search_models = [] self.public_models = [] + self.order_by_score = true def self.get_search_models self.search_models.map {|m| m.constantize} @@ -279,6 +320,30 @@ def self.get_search_models def self.get_public_models self.public_models.map {|m| m.constantize} end + + def self.rebuild_index + puts "Rebuilding Search index..." + + SmartSearch::Config.search_models.each do |name| + puts "... #{name}" + end + + ActiveRecord::Base.logger = nil + + model_bar = ProgressBar.create(:title => "Building models", :total => SmartSearch::Config.search_models.size, format: "%t: (%c/%C) |%W| %f") + + SmartSearch::Config.get_search_models.each do |model| + puts model.tags.join("\n") + entry_bar = ProgressBar.create(:title => model.name, :total => model.all.size, format: "%t: (%c/%C) |%W| %f") + model.all.each do |entry| + entry.create_search_tags + entry_bar.increment + end + + model_bar.increment + puts "\n\n" + end + end end diff --git a/lib/smart_search/engine.rb b/lib/smart_search/engine.rb index 56537e0..d5b9a41 100755 --- a/lib/smart_search/engine.rb +++ b/lib/smart_search/engine.rb @@ -2,10 +2,9 @@ module SmartSearch class Engine < Rails::Engine engine_name 'smart_search' - + isolate_namespace SmartSearch require "friendly_extensions" - require "amatch" end end diff --git a/lib/smart_similarity.rb b/lib/smart_similarity.rb index 02dfeac..bae2537 100755 --- a/lib/smart_similarity.rb +++ b/lib/smart_similarity.rb @@ -17,12 +17,6 @@ class SmartSimilarity < ActiveRecord::Base #== Konstanten # Defines the min. result of word simililarity check SIMILARITY_FACTOR = 0.81 - # Defines first simililarity check method - SIMILARITY_METHOD_1 = :jarowinkler - # Defines first simililarity check method - SIMILARITY_METHOD_2 = :levenshtein - - # An average of both results will generated and compered with 'SIMILARITY_FACTOR' # Limit Number of similar words SIMILARITY_LIMIT = 5 @@ -100,8 +94,8 @@ def self.similars(word, options = {}) # Return match score for two words bases und the two defined similarity methods def self.match_words(word1, word2) - x1 = word1.downcase.send("#{SIMILARITY_METHOD_1}_similar", word2.downcase) - x2 = word1.downcase.send("#{SIMILARITY_METHOD_2}_similar", word2.downcase) + x1 = DidYouMean::JaroWinkler.distance(word1.downcase, word2.downcase) + x2 = DidYouMean::Levenshtein.distance(word1.downcase, word2.downcase) return (x1+x2)/2.0 end diff --git a/lib/tasks/smart_search.rake b/lib/tasks/smart_search.rake index d3b6c78..624c8ef 100755 --- a/lib/tasks/smart_search.rake +++ b/lib/tasks/smart_search.rake @@ -4,6 +4,11 @@ namespace :smart_search do require File.expand_path("../../smart_similarity", __FILE__) SmartSimilarity.load_from_query_history end + + desc "Rebuild search index for all avialble models" + task :rebuild_index => :environment do + SmartSearch::Config.rebuild_index + end end diff --git a/smart_search.gemspec b/smart_search.gemspec index 84f6278..94414e8 100755 --- a/smart_search.gemspec +++ b/smart_search.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'smart_search' - s.version = '1.0.2' + s.version = '1.2.2' s.summary = "Simple, easy to use search MySQL based search for ActiveRecord" s.description = "SmartSearch adds full-text search functions to ActiveRecord running with MySQL, including search for similiar words. Its fast, simple, and works with almost zero-config!" s.authors = ["Florian Eck"] @@ -9,10 +9,6 @@ Gem::Specification.new do |s| s.test_files = Dir.glob("test/**/*") s.homepage = 'https://github.com/florianeck/smart_search' - s.add_dependency "rails", ">= 4.0.4" - s.add_dependency "amatch" s.add_dependency "friendly_extensions" s.add_dependency "unicode-emoji" - s.add_dependency "ruby-progressbar" - s.add_dependency "parallel" end \ No newline at end of file