can_search.rb 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. module CanSearch
  3. extend ActiveSupport::Concern
  4. included do
  5. =begin
  6. This function provides the possibility to add model specific sql extensions
  7. for the searches in the DB. E.g. role or group specific conditions
  8. in user model.
  9. see e.g. also app/models/user/search.rb
  10. =end
  11. scope :search_sql_extension, ->(_params) {}
  12. =begin
  13. This function defines the sql search query for the text fields which are searched in. By default
  14. it is all string columns but can be modified.
  15. see e.g. also app/models/ticket/search.rb
  16. =end
  17. scope :search_sql_query_extension, lambda { |params|
  18. return if params[:query].blank?
  19. search_columns = columns.select { |row| row.type == :string && !row.try(:array) }.map(&:name)
  20. return if search_columns.blank?
  21. where_or_cis(search_columns, "%#{SqlHelper.quote_like(params[:query].to_s.downcase)}%")
  22. }
  23. # Scope to specific IDs if they're given in params.
  24. # Usually those IDs are pre-filled in .search_params_pre method.
  25. scope :search_sql_ids, lambda { |params|
  26. where(id: params[:ids]) if params[:ids].present?
  27. }
  28. end
  29. class_methods do
  30. =begin
  31. This defines the default search sort by for the search function.
  32. =end
  33. def search_default_sort_by
  34. 'updated_at'
  35. end
  36. =begin
  37. This defines the default search order by for the search function.
  38. =end
  39. def search_default_order_by
  40. 'desc'
  41. end
  42. =begin
  43. This function can be used to fix parameters for the model
  44. e.g. is used to restrict the result set of organization searches
  45. to only return customer organizations in case of a customer user
  46. see e.g. also app/models/organization/search.rb
  47. =end
  48. def search_params_pre(params)
  49. # optional
  50. end
  51. =begin
  52. This function provides the possibility to add model specific query extensions
  53. for the searches in the elasticsearch. E.g. role or group specific conditions
  54. in user model.
  55. see e.g. also app/models/user/search.rb
  56. =end
  57. def search_query_extension(params)
  58. # optional
  59. end
  60. =begin
  61. search objects via search index
  62. result = Model.search(
  63. current_user: User.find(123),
  64. query: 'search something',
  65. limit: 15,
  66. offset: 100,
  67. )
  68. returns
  69. result = [obj1, obj2, obj3]
  70. search objects via search index with total count
  71. result = Model.search(
  72. current_user: User.find(123),
  73. query: 'search something',
  74. limit: 15,
  75. offset: 100,
  76. with_total_count: true
  77. )
  78. returns
  79. result = {
  80. object_ids: [1,2,3],
  81. count: 3,
  82. }
  83. search objects via search index with ONLY total count
  84. result = Model.search(
  85. current_user: User.find(123),
  86. query: 'search something',
  87. limit: 15,
  88. offset: 100,
  89. only_total_count: true
  90. )
  91. returns
  92. result = {
  93. count: 3,
  94. }
  95. search objects via search index
  96. result = Model.search(
  97. current_user: User.find(123),
  98. query: 'search something',
  99. limit: 15,
  100. offset: 100,
  101. full: false,
  102. )
  103. returns
  104. result = [1,2,3]
  105. search objects via database
  106. result = Group.search(
  107. current_user: User.find(123),
  108. query: 'some query', # query or condition is required
  109. condition: {
  110. 'groups.id' => {
  111. operator: 'is',
  112. value: [1,2,3],
  113. },
  114. },
  115. limit: 15,
  116. offset: 100,
  117. # sort single column
  118. sort_by: 'created_at',
  119. order_by: 'asc',
  120. # sort multiple columns
  121. sort_by: [ 'created_at', 'updated_at' ],
  122. order_by: [ 'asc', 'desc' ],
  123. full: false,
  124. )
  125. returns
  126. result = [1,2,3]
  127. =end
  128. def search(params)
  129. # It's possible to search objects that don't have .search_preferences method.
  130. # However, if .search_preferences exist and return falsey value, search is not authorized in a given context!
  131. # Thus we need to check if method exist instead of using try()!
  132. return if defined?(search_preferences) && !search_preferences(params[:current_user])
  133. params = search_build_params(params)
  134. # try search index backend
  135. # we only search in elastic search when we have a query present
  136. # else we try to use the database result, since it is more up to date
  137. object_ids, object_count = if SearchIndexBackend.enabled? && included_modules.include?(HasSearchIndexBackend) && params[:query].present?
  138. search_es(params)
  139. else
  140. search_sql(params)
  141. end
  142. search_result(params, object_ids, object_count)
  143. end
  144. def search_result(params, object_ids, object_count)
  145. if params[:only_total_count].present?
  146. {
  147. total_count: object_count,
  148. }
  149. elsif params[:with_total_count].present?
  150. if params[:full].present?
  151. return {
  152. objects: object_ids.map { |id| lookup(id: id) },
  153. total_count: object_count
  154. }
  155. end
  156. {
  157. object_ids: object_ids,
  158. total_count: object_count
  159. }
  160. elsif params[:full].present?
  161. object_ids.map { |id| lookup(id: id) }
  162. else
  163. object_ids
  164. end
  165. end
  166. def search_build_params(params)
  167. search_params_pre(params)
  168. sql_helper = ::SqlHelper.new(object: self)
  169. params[:condition] ||= {}
  170. params[:limit] ||= 50
  171. params[:query] = params[:query]&.delete('*')
  172. params[:offset] = params[:offset].presence || params[:from].presence || 0
  173. params[:full] = !params.key?(:full) || ActiveModel::Type::Boolean.new.cast(params[:full])
  174. params[:sort_by] = sql_helper.get_sort_by(params, search_default_sort_by)
  175. params[:order_by] = sql_helper.get_order_by(params, search_default_order_by)
  176. params
  177. end
  178. def search_es(params)
  179. result = SearchIndexBackend.search_by_index(
  180. params[:query],
  181. to_s,
  182. params.merge(query_extension: search_query_extension(params), with_total_count: true)
  183. )
  184. if params[:only_total_count].blank?
  185. object_ids = result&.dig(:object_metadata)&.pluck(:id) || []
  186. end
  187. object_count = result&.dig(:total_count) || 0
  188. [object_ids, object_count]
  189. end
  190. def search_sql(params)
  191. scope = search_sql_base(params)
  192. objects_order_sql = sql_helper.get_order(params[:sort_by], params[:order_by], "#{table_name}.updated_at DESC")
  193. objects_scope = scope
  194. .reorder(Arel.sql(objects_order_sql))
  195. .offset(params[:offset])
  196. .limit(params[:limit])
  197. .group(:id)
  198. if params[:only_total_count].blank?
  199. object_ids = objects_scope.pluck(:id)
  200. end
  201. object_count = scope.count("DISTINCT #{table_name}.id")
  202. [object_ids, object_count]
  203. end
  204. def search_sql_base(params)
  205. query_condition, bind_condition, tables = selector2sql(params[:condition])
  206. scope = params[:scope].present? ? params[:scope].new(params[:current_user]).resolve : all
  207. scope
  208. .joins(tables).where(query_condition, *bind_condition)
  209. .search_sql_extension(params)
  210. .search_sql_query_extension(params)
  211. .search_sql_ids(params)
  212. end
  213. def sql_helper
  214. @sql_helper ||= ::SqlHelper.new(object: self)
  215. end
  216. end
  217. end