search_knowledge_base_backend.rb 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
  2. class SearchKnowledgeBaseBackend
  3. attr_reader :knowledge_base
  4. # @param [Hash] params the paramsused to initialize search instance
  5. # @option params [KnowledgeBase, <KnowledgeBase>] :knowledge_base (nil) knowledge base instance
  6. # @option params [KnowledgeBase::Locale, <KnowledgeBase::Locale>, String] :locale (nil) KB Locale or string identifier
  7. # @option params [KnowledgeBase::Category] :scope (nil) optional search scope
  8. # @option params [Symbol] :flavor (:public) agent or public to indicate source and narrow down to internal or public answers accordingly
  9. # @option params [String, Array<String>] :index (nil) indexes to limit search to, searches all indexes if nil
  10. # @option params [Integer] :limit per page param for paginatin
  11. # @option params [Boolean] :highlight_enabled (true) highlight matching text
  12. # @option params [Hash<String=>String>, Hash<Symbol=>Symbol>] :order_by hash with column => asc/desc
  13. def initialize(params)
  14. @params = params.compact
  15. prepare_scope_ids
  16. end
  17. def search(query, user: nil, pagination: nil)
  18. raw_results = raw_results(query, user, pagination: pagination)
  19. filtered = filter_results raw_results, user
  20. if pagination
  21. filtered = filtered.slice pagination.offset, pagination.limit
  22. elsif @params[:limit]
  23. filtered = filtered.slice 0, @params[:limit]
  24. end
  25. filtered
  26. end
  27. def search_fallback(query, indexes, options)
  28. indexes
  29. .map { |index| search_fallback_for_index(query, index, options) }
  30. .flatten
  31. end
  32. def search_fallback_for_index(query, index, options)
  33. index
  34. .constantize
  35. .search_fallback("%#{query}%", @cached_scope_ids, options: options)
  36. .where(kb_locale: kb_locales)
  37. .order(**search_fallback_order)
  38. .pluck(:id)
  39. .map { |id| { id: id, type: index } }
  40. end
  41. def search_fallback_order
  42. @params[:order_by].presence || { updated_at: :desc }
  43. end
  44. def raw_results(query, user, pagination: nil)
  45. return search_fallback(query, indexes, { user: user }) if !SearchIndexBackend.enabled?
  46. SearchIndexBackend
  47. .search(query, indexes, options(pagination: pagination))
  48. .map do |hash|
  49. hash[:id] = hash[:id].to_i
  50. hash
  51. end
  52. end
  53. def filter_results(raw_results, user)
  54. raw_results
  55. .group_by { |result| result[:type] }
  56. .map { |group_name, grouped_results| filter_type(group_name, grouped_results, user) }
  57. .flatten
  58. end
  59. def filter_type(type, grouped_results, user)
  60. translation_ids = translation_ids_for_type(type, user)
  61. if !translation_ids
  62. return []
  63. end
  64. grouped_results.select { |result| translation_ids&.include? result[:id].to_i }
  65. end
  66. def translation_ids_for_type(type, user)
  67. case type
  68. when KnowledgeBase::Answer::Translation.name
  69. translation_ids_for_answers(user)
  70. when KnowledgeBase::Category::Translation.name
  71. translation_ids_for_categories(user)
  72. when KnowledgeBase::Translation.name
  73. translation_ids_for_kbs(user)
  74. end
  75. end
  76. def translation_ids_for_answers(user)
  77. scope = KnowledgeBase::Answer
  78. .joins(:category)
  79. .where(knowledge_base_categories: { knowledge_base_id: knowledge_bases })
  80. # rubocop:disable Style/RedundantSelfAssignmentBranch
  81. scope = if user&.permissions?('knowledge_base.editor')
  82. scope
  83. elsif user&.permissions?('knowledge_base.reader') && flavor == :agent
  84. scope.internal
  85. else
  86. scope.published
  87. end
  88. # rubocop:enable Style/RedundantSelfAssignmentBranch
  89. flatten_translation_ids(scope)
  90. end
  91. def translation_ids_for_categories(user)
  92. scope = KnowledgeBase::Category.where knowledge_base_id: knowledge_bases
  93. if user&.permissions?('knowledge_base.editor')
  94. flatten_translation_ids scope
  95. elsif user&.permissions?('knowledge_base.reader') && flavor == :agent
  96. flatten_answer_translation_ids(scope, :internal)
  97. else
  98. flatten_answer_translation_ids(scope, :public)
  99. end
  100. end
  101. def translation_ids_for_kbs(_user)
  102. flatten_translation_ids KnowledgeBase.active.where(id: knowledge_bases)
  103. end
  104. def indexes
  105. return Array(@params.fetch(:index)) if @params.key?(:index)
  106. %w[
  107. KnowledgeBase::Answer::Translation
  108. KnowledgeBase::Category::Translation
  109. KnowledgeBase::Translation
  110. ]
  111. end
  112. def kb_locales
  113. @kb_locales ||= begin
  114. case @params.fetch(:locale, nil)
  115. when KnowledgeBase::Locale
  116. Array(@params.fetch(:locale))
  117. when String
  118. KnowledgeBase::Locale
  119. .joins(:system_locale)
  120. .where(knowledge_base_id: knowledge_bases, locales: { locale: @params.fetch(:locale) })
  121. else
  122. KnowledgeBase::Locale
  123. .where(knowledge_base_id: knowledge_bases)
  124. end
  125. end
  126. end
  127. def kb_locales_in(knowledge_base_id)
  128. @kb_locales_in ||= {}
  129. @kb_locales_in[knowledge_base_id] ||= @kb_locales.select { |locale| locale.knowledge_base_id == knowledge_base_id }
  130. end
  131. def kb_locale_ids
  132. @kb_locale_ids ||= kb_locales.pluck(:id)
  133. end
  134. def knowledge_bases
  135. @knowledge_bases ||= begin
  136. if @params.key? :knowledge_base
  137. Array(@params.fetch(:knowledge_base))
  138. else
  139. KnowledgeBase.active
  140. end
  141. end
  142. end
  143. def flavor
  144. @params.fetch(:flavor, :public).to_sym
  145. end
  146. def base_options
  147. {
  148. query_extension: {
  149. bool: {
  150. must: [ { terms: { kb_locale_id: kb_locale_ids } } ]
  151. }
  152. }
  153. }
  154. end
  155. def options_apply_highlight(hash)
  156. return if !@params.fetch(:highlight_enabled, true)
  157. hash[:highlight_fields_by_indexes] = {
  158. 'KnowledgeBase::Answer::Translation': %w[title content.body attachment.content tags],
  159. 'KnowledgeBase::Category::Translation': %w[title],
  160. 'KnowledgeBase::Translation': %w[title]
  161. }
  162. end
  163. def options_apply_scope(hash)
  164. return if !@params.fetch(:scope, nil)
  165. hash[:query_extension][:bool][:must].push({ terms: { scope_id: @cached_scope_ids } })
  166. end
  167. def options_apply_pagination(hash, pagination)
  168. if @params[:from] && @params[:limit]
  169. hash[:from] = @params[:from]
  170. hash[:limit] = @params[:limit]
  171. elsif pagination
  172. hash[:from] = 0
  173. hash[:limit] = pagination.limit * 99
  174. end
  175. end
  176. def options_apply_order(hash)
  177. return if @params[:order_by].blank?
  178. hash[:sort_by] = @params[:order_by].keys
  179. hash[:order_by] = @params[:order_by].values
  180. end
  181. def options(pagination: nil)
  182. output = base_options
  183. options_apply_highlight(output)
  184. options_apply_scope(output)
  185. options_apply_pagination(output, pagination)
  186. options_apply_order(output)
  187. output
  188. end
  189. def flatten_translation_ids(collection)
  190. collection
  191. .eager_load(:translations)
  192. .map { |elem| elem.translations.pluck(:id) }
  193. .flatten
  194. end
  195. def flatten_answer_translation_ids(collection, visibility)
  196. collection
  197. .eager_load(:translations)
  198. .map { |elem| visible_category_translation_ids(elem, visibility) }
  199. .flatten
  200. end
  201. def visible_category_translation_ids(category, visibility)
  202. category
  203. .translations
  204. .to_a
  205. .select { |elem| visible_translation?(elem, visibility) }
  206. .pluck(:id)
  207. end
  208. def visible_translation?(translation, visibility)
  209. if kb_locales_in(translation.category.knowledge_base_id).exclude?(translation.kb_locale)
  210. return false
  211. end
  212. translation.category.send("#{visibility}_content?", translation.kb_locale)
  213. end
  214. def prepare_scope_ids
  215. return if !@params.key? :scope
  216. @cached_scope_ids = @params.fetch(:scope).self_with_children_ids
  217. end
  218. end