has_search_index_backend.rb 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. module HasSearchIndexBackend
  3. extend ActiveSupport::Concern
  4. included do
  5. after_commit :search_index_update, if: :persisted?
  6. after_destroy :search_index_destroy
  7. end
  8. =begin
  9. update search index, if configured - will be executed automatically
  10. model = Model.find(123)
  11. model.search_index_update
  12. =end
  13. def search_index_update
  14. return true if ignore_search_indexing?(:update)
  15. # start background job to transfer data to search index
  16. return true if !SearchIndexBackend.enabled?
  17. return true if previous_changes.blank?
  18. SearchIndexJob.perform_later(self.class.to_s, id)
  19. SearchIndexAssociationsJob.perform_later(self.class.to_s, id)
  20. true
  21. end
  22. def search_index_indexable
  23. Models.indexable.reject { |local_class| local_class == self.class }
  24. end
  25. def search_index_indexable_attributes(index_class)
  26. result = []
  27. index_class.new.attributes.each_key do |key|
  28. attribute_name = key.to_s
  29. next if attribute_name.blank?
  30. # due to performance reasons, we only want to process some attributes for specific classes (e.g. tickets)
  31. next if !index_class.search_index_attribute_relevant?(attribute_name)
  32. attribute_ref_name = index_class.search_index_attribute_ref_name(attribute_name)
  33. next if attribute_ref_name.blank?
  34. association = index_class.reflect_on_association(attribute_ref_name)
  35. next if association.blank?
  36. next if association.options[:polymorphic]
  37. attribute_class = association.klass
  38. next if attribute_class.blank?
  39. next if attribute_class != self.class
  40. result << {
  41. name: attribute_name,
  42. ref_name: attribute_ref_name,
  43. }
  44. end
  45. result
  46. end
  47. def search_index_update_delta(index_class:, value:, attribute:)
  48. raise "Attribute lookup data needs updated_at for delta updates (object: #{self.class}, #{id})" if value['updated_at'].blank?
  49. data = {
  50. attribute[:ref_name] => value,
  51. }
  52. where = {
  53. bool: {
  54. must: [
  55. {
  56. term: {
  57. attribute[:name] => id,
  58. },
  59. },
  60. {
  61. range: {
  62. "#{attribute[:ref_name]}.updated_at" => {
  63. lt: value['updated_at'].strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
  64. },
  65. },
  66. },
  67. ],
  68. },
  69. }
  70. SearchIndexBackend.update_by_query(index_class.to_s, data, where)
  71. end
  72. =begin
  73. update search index, if configured - will be executed automatically
  74. model = Organizations.find(123)
  75. result = model.search_index_update_associations
  76. returns
  77. # Updates asscociation data for users and tickets of the organization in this example
  78. result = true
  79. =end
  80. def search_index_update_associations
  81. return if !SearchIndexBackend.enabled?
  82. new_search_index_value = search_index_attribute_lookup(include_references: false)
  83. return if new_search_index_value.blank?
  84. search_index_indexable.each_with_object([]) do |index_class, result|
  85. search_index_indexable_attributes(index_class).each do |attribute|
  86. result << search_index_update_delta(index_class: index_class, value: new_search_index_value, attribute: attribute)
  87. end
  88. end
  89. end
  90. =begin
  91. delete search index object, will be executed automatically
  92. model = Model.find(123)
  93. model.search_index_destroy
  94. =end
  95. def search_index_destroy
  96. return true if ignore_search_indexing?(:destroy)
  97. SearchIndexBackend.remove(self.class.to_s, id)
  98. true
  99. end
  100. =begin
  101. collect data to index and send to backend
  102. ticket = Ticket.find(123)
  103. result = ticket.search_index_update_backend
  104. returns
  105. result = true # false
  106. =end
  107. def search_index_update_backend
  108. # fill up with search data
  109. attributes = search_index_attribute_lookup
  110. return true if !attributes
  111. # update backend
  112. SearchIndexBackend.add(self.class.to_s, attributes)
  113. true
  114. end
  115. def ignore_search_indexing?(_action)
  116. false
  117. end
  118. # methods defined here are going to extend the class, not the instance of it
  119. class_methods do # rubocop:disable Metrics/BlockLength
  120. =begin
  121. serve method to ignore model attributes in search index
  122. class Model < ApplicationModel
  123. include HasSearchIndexBackend
  124. search_index_attributes_ignored :password, :image
  125. end
  126. =end
  127. def search_index_attributes_ignored(*attributes)
  128. @search_index_attributes_ignored = attributes
  129. end
  130. def search_index_attributes_relevant(*attributes)
  131. @search_index_attributes_relevant = attributes
  132. end
  133. =begin
  134. reload search index with full data
  135. Model.search_index_reload
  136. =end
  137. def search_index_reload(silent: false, worker: 0)
  138. tolerance = 10
  139. tolerance_count = 0
  140. query = reorder(created_at: :desc)
  141. total = query.count
  142. record_count = 0
  143. offset = 0
  144. batch_size = 200
  145. while query.offset(offset).limit(batch_size).count.positive?
  146. records = query.offset(offset).limit(batch_size)
  147. Parallel.map(records, { in_processes: worker }) do |record|
  148. next if record.ignore_search_indexing?(:destroy)
  149. begin
  150. record.search_index_update_backend
  151. rescue => e
  152. logger.error "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}"
  153. tolerance_count += 1
  154. sleep 15
  155. raise "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}" if tolerance_count == tolerance
  156. end
  157. end
  158. offset += batch_size
  159. next if silent
  160. record_count += records.count
  161. if (record_count % batch_size).zero? || record_count == total
  162. print "\r #{record_count}/#{total}" # rubocop:disable Rails/Output
  163. end
  164. end
  165. end
  166. end
  167. end