From 742895183aebd0e88194ca07c11d94e43e7d24c2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 8 Jan 2015 22:04:00 +0000 Subject: [PATCH] Search custom fields and journals with different queries to take advantage of indexes on text columns if present. git-svn-id: http://svn.redmine.org/redmine/trunk@13855 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/issue.rb | 9 +- .../lib/acts_as_searchable.rb | 99 +++++++++++++------ 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 75e59ea32..e7a444d7d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -46,13 +46,8 @@ class Issue < ActiveRecord::Base acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed acts_as_customizable acts_as_watchable - acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], - :preload => [:project, :status, :tracker], - :scope => lambda { joins(:project). - joins("LEFT OUTER JOIN #{Journal.table_name} ON #{Journal.table_name}.journalized_type='Issue'" + - " AND #{Journal.table_name}.journalized_id = #{Issue.table_name}.id" + - " AND (#{Journal.table_name}.private_notes = #{connection.quoted_false}" + - " OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))") } + acts_as_searchable :columns => ['subject', "#{table_name}.description"], + :preload => [:project, :status, :tracker] acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, diff --git a/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb index 74e33ceef..25d6e225f 100644 --- a/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb +++ b/lib/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -48,8 +48,9 @@ module Redmine searchable_options[:project_key] ||= "#{table_name}.project_id" searchable_options[:date_column] ||= :created_on - # Should we search custom fields on this model ? - searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? + # Should we search additional associations on this model ? + searchable_options[:search_custom_fields] = reflect_on_association(:custom_values).present? + searchable_options[:search_journals] = reflect_on_association(:journals).present? send :include, Redmine::Acts::Searchable::InstanceMethods end @@ -75,11 +76,6 @@ module Redmine # Issue.search_result_ranks_and_ids("foo") # # => [[1419595329, 69], [1419595622, 123]] def search_result_ranks_and_ids(tokens, user=User.current, projects=nil, options={}) - if projects.is_a?(Array) && projects.empty? - # no results - return [] - end - tokens = [] << tokens unless tokens.is_a?(Array) projects = [] << projects if projects.is_a?(Project) @@ -87,36 +83,63 @@ module Redmine columns = columns[0..0] if options[:titles_only] token_clauses = columns.collect {|column| "(#{search_token_match_statement(column)})"} + sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort] + + r = fetch_ranks_and_ids(search_scope(user, projects).where(tokens_conditions), options[:limit]) + sort_and_limit_results = false if !options[:titles_only] && searchable_options[:search_custom_fields] - searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true) - fields_by_visibility = searchable_custom_fields.group_by {|field| - field.visibility_by_project_condition(searchable_options[:project_key], user, "cfs.custom_field_id") - } - # only 1 subquery for all custom fields with the same visibility statement - fields_by_visibility.each do |visibility, fields| - ids = fields.map(&:id).join(',') - sql = "#{table_name}.id IN (SELECT cfs.customized_id FROM #{CustomValue.table_name} cfs" + - " WHERE cfs.customized_type='#{self.name}' AND cfs.customized_id=#{table_name}.id" + - " AND cfs.custom_field_id IN (#{ids})" + - " AND #{search_token_match_statement('cfs.value')}" + - " AND #{visibility})" - token_clauses << sql + searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a + + if searchable_custom_fields.any? + fields_by_visibility = searchable_custom_fields.group_by {|field| + field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id") + } + clauses = [] + fields_by_visibility.each do |visibility, fields| + clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))" + end + visibility = clauses.join(' OR ') + + sql = ([search_token_match_statement("#{CustomValue.table_name}.value")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort] + + r |= fetch_ranks_and_ids( + search_scope(user, projects). + joins(:custom_values). + where(visibility). + where(tokens_conditions), + options[:limit] + ) + + sort_and_limit_results = true end end - sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + if !options[:titles_only] && searchable_options[:search_journals] + sql = ([search_token_match_statement("#{Journal.table_name}.notes")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort] - tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort] + r |= fetch_ranks_and_ids( + search_scope(user, projects). + joins(:journals). + where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false). + where(tokens_conditions), + options[:limit] + ) - search_scope(user, projects). - reorder(searchable_options[:date_column] => :desc, :id => :desc). - where(tokens_conditions). - limit(options[:limit]). - uniq. - pluck(searchable_options[:date_column], :id). - # converts timestamps to integers for faster sort - map {|timestamp, id| [timestamp.to_i, id]} + sort_and_limit_results = true + end + + if sort_and_limit_results + r = r.sort.reverse + if options[:limit] && r.size > options[:limit] + r = r[0, options[:limit]] + end + end + + r end def search_token_match_statement(column, value='?') @@ -129,8 +152,24 @@ module Redmine end private :search_token_match_statement + def fetch_ranks_and_ids(scope, limit) + scope. + reorder(searchable_options[:date_column] => :desc, :id => :desc). + limit(limit). + uniq. + pluck(searchable_options[:date_column], :id). + # converts timestamps to integers for faster sort + map {|timestamp, id| [timestamp.to_i, id]} + end + private :fetch_ranks_and_ids + # Returns the search scope for user and projects def search_scope(user, projects) + if projects.is_a?(Array) && projects.empty? + # no results + return none + end + scope = (searchable_options[:scope] || self) if scope.is_a? Proc scope = scope.call -- 2.39.5