has_search_index_backend.rb 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # Copyright (C) 2012-2023 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 do |key, _value|
  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. data = {
  49. attribute[:ref_name] => value,
  50. }
  51. where = {
  52. attribute[:name] => id
  53. }
  54. SearchIndexBackend.update_by_query(index_class.to_s, data, where)
  55. end
  56. =begin
  57. update search index, if configured - will be executed automatically
  58. model = Organizations.find(123)
  59. result = model.search_index_update_associations
  60. returns
  61. # Updates asscociation data for users and tickets of the organization in this example
  62. result = true
  63. =end
  64. def search_index_update_associations
  65. # start background job to transfer data to search index
  66. return true if !SearchIndexBackend.enabled?
  67. new_search_index_value = search_index_attribute_lookup(include_references: false)
  68. return if new_search_index_value.blank?
  69. search_index_indexable.each do |index_class|
  70. search_index_indexable_attributes(index_class).each do |attribute|
  71. search_index_update_delta(index_class: index_class, value: new_search_index_value, attribute: attribute)
  72. end
  73. end
  74. true
  75. end
  76. =begin
  77. delete search index object, will be executed automatically
  78. model = Model.find(123)
  79. model.search_index_destroy
  80. =end
  81. def search_index_destroy
  82. return true if ignore_search_indexing?(:destroy)
  83. SearchIndexBackend.remove(self.class.to_s, id)
  84. true
  85. end
  86. =begin
  87. collect data to index and send to backend
  88. ticket = Ticket.find(123)
  89. result = ticket.search_index_update_backend
  90. returns
  91. result = true # false
  92. =end
  93. def search_index_update_backend
  94. # fill up with search data
  95. attributes = search_index_attribute_lookup
  96. return true if !attributes
  97. # update backend
  98. SearchIndexBackend.add(self.class.to_s, attributes)
  99. true
  100. end
  101. def ignore_search_indexing?(_action)
  102. false
  103. end
  104. # methods defined here are going to extend the class, not the instance of it
  105. class_methods do # rubocop:disable Metrics/BlockLength
  106. =begin
  107. serve method to ignore model attributes in search index
  108. class Model < ApplicationModel
  109. include HasSearchIndexBackend
  110. search_index_attributes_ignored :password, :image
  111. end
  112. =end
  113. def search_index_attributes_ignored(*attributes)
  114. @search_index_attributes_ignored = attributes
  115. end
  116. def search_index_attributes_relevant(*attributes)
  117. @search_index_attributes_relevant = attributes
  118. end
  119. =begin
  120. reload search index with full data
  121. Model.search_index_reload
  122. =end
  123. def search_index_reload(silent: false)
  124. tolerance = 10
  125. tolerance_count = 0
  126. query = order(created_at: :desc)
  127. total = query.count
  128. record_count = 0
  129. batch_size = 100
  130. query.as_batches(size: batch_size) do |record|
  131. if !record.ignore_search_indexing?(:destroy)
  132. begin
  133. record.search_index_update_backend
  134. rescue => e
  135. logger.error "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}"
  136. tolerance_count += 1
  137. sleep 15
  138. raise "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}" if tolerance_count == tolerance
  139. end
  140. end
  141. next if silent
  142. record_count += 1
  143. if (record_count % batch_size).zero? || record_count == total
  144. print "\r #{record_count}/#{total}" # rubocop:disable Rails/Output
  145. end
  146. end
  147. end
  148. end
  149. end