mirror of
https://github.com/redmine/redmine.git
synced 2025-11-09 06:46:01 +01:00
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
This commit is contained in:
@@ -46,13 +46,8 @@ class Issue < ActiveRecord::Base
|
|||||||
acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
|
acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
|
||||||
acts_as_customizable
|
acts_as_customizable
|
||||||
acts_as_watchable
|
acts_as_watchable
|
||||||
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
|
acts_as_searchable :columns => ['subject', "#{table_name}.description"],
|
||||||
:preload => [:project, :status, :tracker],
|
: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_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
|
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}},
|
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ module Redmine
|
|||||||
searchable_options[:project_key] ||= "#{table_name}.project_id"
|
searchable_options[:project_key] ||= "#{table_name}.project_id"
|
||||||
searchable_options[:date_column] ||= :created_on
|
searchable_options[:date_column] ||= :created_on
|
||||||
|
|
||||||
# Should we search custom fields on this model ?
|
# Should we search additional associations on this model ?
|
||||||
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
|
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
|
send :include, Redmine::Acts::Searchable::InstanceMethods
|
||||||
end
|
end
|
||||||
@@ -75,11 +76,6 @@ module Redmine
|
|||||||
# Issue.search_result_ranks_and_ids("foo")
|
# Issue.search_result_ranks_and_ids("foo")
|
||||||
# # => [[1419595329, 69], [1419595622, 123]]
|
# # => [[1419595329, 69], [1419595622, 123]]
|
||||||
def search_result_ranks_and_ids(tokens, user=User.current, projects=nil, options={})
|
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)
|
tokens = [] << tokens unless tokens.is_a?(Array)
|
||||||
projects = [] << projects if projects.is_a?(Project)
|
projects = [] << projects if projects.is_a?(Project)
|
||||||
|
|
||||||
@@ -87,36 +83,63 @@ module Redmine
|
|||||||
columns = columns[0..0] if options[:titles_only]
|
columns = columns[0..0] if options[:titles_only]
|
||||||
|
|
||||||
token_clauses = columns.collect {|column| "(#{search_token_match_statement(column)})"}
|
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]
|
if !options[:titles_only] && searchable_options[:search_custom_fields]
|
||||||
searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
|
searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a
|
||||||
fields_by_visibility = searchable_custom_fields.group_by {|field|
|
|
||||||
field.visibility_by_project_condition(searchable_options[:project_key], user, "cfs.custom_field_id")
|
if searchable_custom_fields.any?
|
||||||
}
|
fields_by_visibility = searchable_custom_fields.group_by {|field|
|
||||||
# only 1 subquery for all custom fields with the same visibility statement
|
field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id")
|
||||||
fields_by_visibility.each do |visibility, fields|
|
}
|
||||||
ids = fields.map(&:id).join(',')
|
clauses = []
|
||||||
sql = "#{table_name}.id IN (SELECT cfs.customized_id FROM #{CustomValue.table_name} cfs" +
|
fields_by_visibility.each do |visibility, fields|
|
||||||
" WHERE cfs.customized_type='#{self.name}' AND cfs.customized_id=#{table_name}.id" +
|
clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))"
|
||||||
" AND cfs.custom_field_id IN (#{ids})" +
|
end
|
||||||
" AND #{search_token_match_statement('cfs.value')}" +
|
visibility = clauses.join(' OR ')
|
||||||
" AND #{visibility})"
|
|
||||||
token_clauses << sql
|
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
|
||||||
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).
|
sort_and_limit_results = true
|
||||||
reorder(searchable_options[:date_column] => :desc, :id => :desc).
|
end
|
||||||
where(tokens_conditions).
|
|
||||||
limit(options[:limit]).
|
if sort_and_limit_results
|
||||||
uniq.
|
r = r.sort.reverse
|
||||||
pluck(searchable_options[:date_column], :id).
|
if options[:limit] && r.size > options[:limit]
|
||||||
# converts timestamps to integers for faster sort
|
r = r[0, options[:limit]]
|
||||||
map {|timestamp, id| [timestamp.to_i, id]}
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
r
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_token_match_statement(column, value='?')
|
def search_token_match_statement(column, value='?')
|
||||||
@@ -129,8 +152,24 @@ module Redmine
|
|||||||
end
|
end
|
||||||
private :search_token_match_statement
|
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
|
# Returns the search scope for user and projects
|
||||||
def search_scope(user, projects)
|
def search_scope(user, projects)
|
||||||
|
if projects.is_a?(Array) && projects.empty?
|
||||||
|
# no results
|
||||||
|
return none
|
||||||
|
end
|
||||||
|
|
||||||
scope = (searchable_options[:scope] || self)
|
scope = (searchable_options[:scope] || self)
|
||||||
if scope.is_a? Proc
|
if scope.is_a? Proc
|
||||||
scope = scope.call
|
scope = scope.call
|
||||||
|
|||||||
Reference in New Issue
Block a user