##// END OF EJS Templates
Cache search result ids for faster search pagination (#18631)....
Jean-Philippe Lang -
r13388:717f491f503d
parent child
Show More
@@ -1,84 +1,82
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class SearchController < ApplicationController
19 19 before_filter :find_optional_project
20 20
21 @@search_cache_store ||= ActiveSupport::Cache.lookup_store :memory_store
22
23 21 def index
24 22 @question = params[:q] || ""
25 23 @question.strip!
26 24 @all_words = params[:all_words] ? params[:all_words].present? : true
27 25 @titles_only = params[:titles_only] ? params[:titles_only].present? : false
28 26
29 27 # quick jump to an issue
30 28 if (m = @question.match(/^#?(\d+)$/)) && (issue = Issue.visible.find_by_id(m[1].to_i))
31 29 redirect_to issue_path(issue)
32 30 return
33 31 end
34 32
35 33 projects_to_search =
36 34 case params[:scope]
37 35 when 'all'
38 36 nil
39 37 when 'my_projects'
40 38 User.current.projects
41 39 when 'subprojects'
42 40 @project ? (@project.self_and_descendants.active.to_a) : nil
43 41 else
44 42 @project
45 43 end
46 44
47 45 @object_types = Redmine::Search.available_search_types.dup
48 46 if projects_to_search.is_a? Project
49 47 # don't search projects
50 48 @object_types.delete('projects')
51 49 # only show what the user is allowed to view
52 50 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
53 51 end
54 52
55 53 @scope = @object_types.select {|t| params[t]}
56 54 @scope = @object_types if @scope.empty?
57 55
58 56 fetcher = Redmine::Search::Fetcher.new(
59 57 @question, User.current, @scope, projects_to_search,
60 :all_words => @all_words, :titles_only => @titles_only
58 :all_words => @all_words, :titles_only => @titles_only, :cache => params[:page].present?
61 59 )
62 60
63 61 if fetcher.tokens.present?
64 62 @result_count = fetcher.result_count
65 63 @result_count_by_type = fetcher.result_count_by_type
66 64 @tokens = fetcher.tokens
67 65
68 66 @result_pages = Paginator.new @result_count, 10, params['page']
69 67 @results = fetcher.results(@result_pages.offset, @result_pages.per_page)
70 68 else
71 69 @question = ""
72 70 end
73 71 render :layout => false if request.xhr?
74 72 end
75 73
76 74 private
77 75 def find_optional_project
78 76 return true unless params[:id]
79 77 @project = Project.find(params[:id])
80 78 check_project_privacy
81 79 rescue ActiveRecord::RecordNotFound
82 80 render_404
83 81 end
84 82 end
@@ -1,56 +1,64
1 1 require File.expand_path('../boot', __FILE__)
2 2
3 3 require 'rails/all'
4 4
5 5 Bundler.require(*Rails.groups)
6 6
7 7 module RedmineApp
8 8 class Application < Rails::Application
9 9 # Settings in config/environments/* take precedence over those specified here.
10 10 # Application configuration should go into files in config/initializers
11 11 # -- all .rb files in that directory are automatically loaded.
12 12
13 13 # Custom directories with classes and modules you want to be autoloadable.
14 14 config.autoload_paths += %W(#{config.root}/lib)
15 15
16 16 # Only load the plugins named here, in the order given (default is alphabetical).
17 17 # :all can be used as a placeholder for all plugins not explicitly named.
18 18 # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
19 19
20 20 config.active_record.store_full_sti_class = true
21 21 config.active_record.default_timezone = :local
22 22
23 23 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
24 24 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
25 25 # config.time_zone = 'Central Time (US & Canada)'
26 26
27 27 # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
28 28 # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
29 29 # config.i18n.default_locale = :de
30 30
31 31 I18n.enforce_available_locales = true
32 32
33 33 # Configure the default encoding used in templates for Ruby 1.9.
34 34 config.encoding = "utf-8"
35 35
36 36 # Configure sensitive parameters which will be filtered from the log file.
37 37 config.filter_parameters += [:password]
38 38
39 39 # Enable the asset pipeline
40 40 config.assets.enabled = false
41 41
42 42 # Version of your assets, change this if you want to expire all your assets
43 43 config.assets.version = '1.0'
44 44
45 45 config.action_mailer.perform_deliveries = false
46 46
47 47 # Do not include all helpers
48 48 config.action_controller.include_all_helpers = false
49 49
50 # Specific cache for search results, the default file store cache is not
51 # a good option as it could grow fast. A memory store (32MB max) is used
52 # as the default. If you're running multiple server processes, it's
53 # recommended to switch to a shared cache store (eg. mem_cache_store).
54 # See http://guides.rubyonrails.org/caching_with_rails.html#cache-stores
55 # for more options (same options as config.cache_store).
56 config.redmine_search_cache_store = :memory_store
57
50 58 config.session_store :cookie_store, :key => '_redmine_session'
51 59
52 60 if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
53 61 instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
54 62 end
55 63 end
56 64 end
@@ -1,138 +1,174
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module Search
20 20
21 21 mattr_accessor :available_search_types
22 22 @@available_search_types = []
23 23
24 24 class << self
25 25 def map(&block)
26 26 yield self
27 27 end
28 28
29 29 # Registers a search provider
30 30 def register(search_type, options={})
31 31 search_type = search_type.to_s
32 32 @@available_search_types << search_type unless @@available_search_types.include?(search_type)
33 33 end
34
35 # Returns the cache store for search results
36 # Can be configured with config.redmine_search_cache_store= in config/application.rb
37 def cache_store
38 @@cache_store ||= begin
39 # if config.search_cache_store was not previously set, a no method error would be raised
40 config = Rails.application.config.redmine_search_cache_store rescue :memory_store
41 if config
42 ActiveSupport::Cache.lookup_store config
43 end
44 end
45 end
34 46 end
35 47
36 48 class Fetcher
37 49 attr_reader :tokens
38 50
39 51 def initialize(question, user, scope, projects, options={})
40 52 @user = user
41 53 @question = question.strip
42 54 @scope = scope
43 55 @projects = projects
56 @cache = options.delete(:cache)
44 57 @options = options
45 58
46 59 # extract tokens from the question
47 60 # eg. hello "bye bye" => ["hello", "bye bye"]
48 61 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
49 62 # tokens must be at least 2 characters long
50 63 @tokens = @tokens.uniq.select {|w| w.length > 1 }
51 64 # no more than 5 tokens to search for
52 65 @tokens.slice! 5..-1
53 66 end
54 67
68 # Returns the total result count
55 69 def result_count
56 70 result_ids.size
57 71 end
58 72
73 # Returns the result count by type
59 74 def result_count_by_type
60 75 ret = Hash.new {|h,k| h[k] = 0}
61 76 result_ids.group_by(&:first).each do |scope, ids|
62 77 ret[scope] += ids.size
63 78 end
64 79 ret
65 80 end
66 81
82 # Returns the results for the given offset and limit
67 83 def results(offset, limit)
68 84 result_ids_to_load = result_ids[offset, limit] || []
69 85
70 86 results_by_scope = Hash.new {|h,k| h[k] = []}
71 87 result_ids_to_load.group_by(&:first).each do |scope, scope_and_ids|
72 88 klass = scope.singularize.camelcase.constantize
73 89 results_by_scope[scope] += klass.search_results_from_ids(scope_and_ids.map(&:last))
74 90 end
75 91
76 92 result_ids_to_load.map do |scope, id|
77 93 results_by_scope[scope].detect {|record| record.id == id}
78 94 end.compact
79 95 end
80 96
97 # Returns the results ids, sorted by rank
81 98 def result_ids
82 @ranks_and_ids ||= load_result_ids
99 @ranks_and_ids ||= load_result_ids_from_cache
83 100 end
84 101
85 102 private
86 103
104 def project_ids
105 Array.wrap(@projects).map(&:id)
106 end
107
108 def load_result_ids_from_cache
109 if Redmine::Search.cache_store
110 cache_key = ActiveSupport::Cache.expand_cache_key(
111 [@question, @user.id, @scope.sort, @options, project_ids.sort]
112 )
113
114 Redmine::Search.cache_store.fetch(cache_key, :force => !@cache) do
115 load_result_ids
116 end
117 else
118 load_result_ids
119 end
120 end
121
87 122 def load_result_ids
88 123 ret = []
89 124 # get all the results ranks and ids
90 125 @scope.each do |scope|
91 126 klass = scope.singularize.camelcase.constantize
92 127 ranks_and_ids_in_scope = klass.search_result_ranks_and_ids(@tokens, User.current, @projects, @options)
93 128 # converts timestamps to integers for faster sort
94 129 ret += ranks_and_ids_in_scope.map {|rank, id| [scope, [rank.to_i, id]]}
95 130 end
96 131 # sort results, higher rank and id first
97 132 ret.sort! {|a,b| b.last <=> a.last}
133 # only keep ids now that results are sorted
98 134 ret.map! {|scope, r| [scope, r.last]}
99 135 ret
100 136 end
101 137 end
102 138
103 139 module Controller
104 140 def self.included(base)
105 141 base.extend(ClassMethods)
106 142 end
107 143
108 144 module ClassMethods
109 145 @@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}
110 146 mattr_accessor :default_search_scopes
111 147
112 148 # Set the default search scope for a controller or specific actions
113 149 # Examples:
114 150 # * search_scope :issues # => sets the search scope to :issues for the whole controller
115 151 # * search_scope :issues, :only => :index
116 152 # * search_scope :issues, :only => [:index, :show]
117 153 def default_search_scope(id, options = {})
118 154 if actions = options[:only]
119 155 actions = [] << actions unless actions.is_a?(Array)
120 156 actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}
121 157 else
122 158 default_search_scopes[controller_name.to_sym][:default] = id.to_s
123 159 end
124 160 end
125 161 end
126 162
127 163 def default_search_scopes
128 164 self.class.default_search_scopes
129 165 end
130 166
131 167 # Returns the default search scope according to the current action
132 168 def default_search_scope
133 169 @default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] ||
134 170 default_search_scopes[controller_name.to_sym][:default]
135 171 end
136 172 end
137 173 end
138 174 end
General Comments 0
You need to be logged in to leave comments. Login now