search_index_es.rake 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. $LOAD_PATH << './lib'
  2. require 'rubygems'
  3. namespace :searchindex do
  4. task :drop, [:opts] => %i[environment searchindex:version_supported] do |_t, _args|
  5. print 'drop indexes...'
  6. # drop indexes
  7. if es_multi_index?
  8. Models.indexable.each do |local_object|
  9. SearchIndexBackend.index(
  10. action: 'delete',
  11. name: local_object.name,
  12. )
  13. end
  14. else
  15. SearchIndexBackend.index(
  16. action: 'delete',
  17. )
  18. end
  19. puts 'done'
  20. Rake::Task['searchindex:drop_pipeline'].execute
  21. end
  22. task :create, [:opts] => %i[environment searchindex:version_supported] do |_t, _args|
  23. print 'create indexes...'
  24. if es_multi_index?
  25. Setting.set('es_multi_index', true)
  26. else
  27. Setting.set('es_multi_index', false)
  28. end
  29. settings = {
  30. 'index.mapping.total_fields.limit': 2000,
  31. }
  32. # create indexes
  33. if es_multi_index?
  34. Models.indexable.each do |local_object|
  35. SearchIndexBackend.index(
  36. action: 'create',
  37. name: local_object.name,
  38. data: {
  39. mappings: get_mapping_properties_object(local_object),
  40. settings: settings,
  41. }
  42. )
  43. end
  44. else
  45. mapping = {}
  46. Models.indexable.each do |local_object|
  47. mapping.merge!(get_mapping_properties_object(local_object))
  48. end
  49. SearchIndexBackend.index(
  50. action: 'create',
  51. data: {
  52. mappings: mapping,
  53. settings: settings,
  54. }
  55. )
  56. end
  57. puts 'done'
  58. Rake::Task['searchindex:create_pipeline'].execute
  59. end
  60. task :create_pipeline, [:opts] => %i[environment searchindex:version_supported] do |_t, _args|
  61. if !es_pipeline?
  62. Setting.set('es_pipeline', '')
  63. next
  64. end
  65. # update processors
  66. pipeline = Setting.get('es_pipeline')
  67. if pipeline.blank?
  68. pipeline = "zammad#{rand(999_999_999_999)}"
  69. Setting.set('es_pipeline', pipeline)
  70. end
  71. # define pipeline_field_attributes
  72. # ES 5.6 and nower has no ignore_missing support
  73. pipeline_field_attributes = {
  74. ignore_failure: true,
  75. }
  76. if es_multi_index?
  77. pipeline_field_attributes = {
  78. ignore_failure: true,
  79. ignore_missing: true,
  80. }
  81. end
  82. print 'create pipeline (pipeline)... '
  83. SearchIndexBackend.processors(
  84. "_ingest/pipeline/#{pipeline}": [
  85. {
  86. action: 'delete',
  87. },
  88. {
  89. action: 'create',
  90. description: 'Extract zammad-attachment information from arrays',
  91. processors: [
  92. {
  93. foreach: {
  94. field: 'article',
  95. processor: {
  96. foreach: {
  97. field: '_ingest._value.attachment',
  98. processor: {
  99. attachment: {
  100. target_field: '_ingest._value',
  101. field: '_ingest._value._content',
  102. }.merge(pipeline_field_attributes),
  103. }
  104. }.merge(pipeline_field_attributes),
  105. }
  106. }.merge(pipeline_field_attributes),
  107. },
  108. {
  109. foreach: {
  110. field: 'attachment',
  111. processor: {
  112. attachment: {
  113. target_field: '_ingest._value',
  114. field: '_ingest._value._content',
  115. }.merge(pipeline_field_attributes),
  116. }
  117. }.merge(pipeline_field_attributes),
  118. }
  119. ]
  120. }
  121. ]
  122. )
  123. puts 'done'
  124. end
  125. task :drop_pipeline, [:opts] => %i[environment searchindex:version_supported] do |_t, _args|
  126. next if !es_pipeline?
  127. # update processors
  128. pipeline = Setting.get('es_pipeline')
  129. next if pipeline.blank?
  130. print 'delete pipeline (pipeline)... '
  131. SearchIndexBackend.processors(
  132. "_ingest/pipeline/#{pipeline}": [
  133. {
  134. action: 'delete',
  135. },
  136. ]
  137. )
  138. puts 'done'
  139. end
  140. task :reload, [:opts] => %i[environment searchindex:version_supported] do |_t, _args|
  141. puts 'reload data...'
  142. Models.indexable.each do |model_class|
  143. puts " reload #{model_class}"
  144. started_at = Time.zone.now
  145. puts " - started at #{started_at}"
  146. model_class.search_index_reload
  147. took = Time.zone.now - started_at
  148. puts " - took #{took.to_i} seconds"
  149. end
  150. end
  151. task :refresh, [:opts] => %i[environment searchindex:version_supported] do |_t, _args|
  152. print 'refresh all indexes...'
  153. SearchIndexBackend.refresh
  154. end
  155. task :rebuild, [:opts] => %i[environment searchindex:version_supported] do |_t, _args|
  156. Rake::Task['searchindex:drop'].execute
  157. Rake::Task['searchindex:create'].execute
  158. Rake::Task['searchindex:reload'].execute
  159. end
  160. task :version_supported, [:opts] => :environment do |_t, _args|
  161. next if es_version_supported?
  162. abort "Your elastic search version is not supported! Please update your version to a greater equal than 5.6.0 (Your current version: #{es_version})."
  163. end
  164. end
  165. =begin
  166. This function will return a index mapping based on the
  167. attributes of the database table of the existing object.
  168. mapping = get_mapping_properties_object(Ticket)
  169. Returns:
  170. mapping = {
  171. User: {
  172. properties: {
  173. firstname: {
  174. type: 'keyword',
  175. },
  176. }
  177. }
  178. }
  179. =end
  180. def get_mapping_properties_object(object)
  181. name = object.name
  182. if es_multi_index?
  183. name = '_doc'
  184. end
  185. result = {
  186. name => {
  187. properties: {}
  188. }
  189. }
  190. store_columns = %w[preferences data]
  191. # for elasticsearch 6.x and later
  192. string_type = 'text'
  193. string_raw = { 'type': 'keyword', 'ignore_above': 5012 }
  194. boolean_raw = { 'type': 'boolean' }
  195. # for elasticsearch 5.6 and lower
  196. if !es_multi_index?
  197. string_type = 'string'
  198. string_raw = { 'type': 'string', 'index': 'not_analyzed' }
  199. boolean_raw = { 'type': 'boolean', 'index': 'not_analyzed' }
  200. end
  201. object.columns_hash.each do |key, value|
  202. if value.type == :string && value.limit && value.limit <= 5000 && store_columns.exclude?(key)
  203. result[name][:properties][key] = {
  204. type: string_type,
  205. fields: {
  206. keyword: string_raw,
  207. }
  208. }
  209. elsif value.type == :integer
  210. result[name][:properties][key] = {
  211. type: 'integer',
  212. }
  213. elsif value.type == :datetime
  214. result[name][:properties][key] = {
  215. type: 'date',
  216. }
  217. elsif value.type == :boolean
  218. result[name][:properties][key] = {
  219. type: 'boolean',
  220. fields: {
  221. keyword: boolean_raw,
  222. }
  223. }
  224. elsif value.type == :binary
  225. result[name][:properties][key] = {
  226. type: 'binary',
  227. }
  228. elsif value.type == :bigint
  229. result[name][:properties][key] = {
  230. type: 'long',
  231. }
  232. elsif value.type == :decimal
  233. result[name][:properties][key] = {
  234. type: 'float',
  235. }
  236. elsif value.type == :date
  237. result[name][:properties][key] = {
  238. type: 'date',
  239. }
  240. end
  241. end
  242. # es with mapper-attachments plugin
  243. if object.name == 'Ticket'
  244. # do not server attachments if document is requested
  245. result[name][:_source] = {
  246. excludes: ['article.attachment']
  247. }
  248. # for elasticsearch 5.5 and lower
  249. if !es_pipeline?
  250. result[name][:_source] = {
  251. excludes: ['article.attachment']
  252. }
  253. result[name][:properties][:article] = {
  254. type: 'nested',
  255. include_in_parent: true,
  256. properties: {
  257. attachment: {
  258. type: 'attachment',
  259. }
  260. }
  261. }
  262. end
  263. end
  264. if object.name == 'KnowledgeBase::Answer::Translation'
  265. # do not server attachments if document is requested
  266. result[name][:_source] = {
  267. excludes: ['attachment']
  268. }
  269. # for elasticsearch 5.5 and lower
  270. if !es_pipeline?
  271. result[name][:_source] = {
  272. excludes: ['attachment']
  273. }
  274. result[name][:properties][:attachment] = {
  275. type: 'attachment',
  276. }
  277. end
  278. end
  279. return result if es_type_in_mapping?
  280. result[name]
  281. end
  282. # get es version
  283. def es_version
  284. @es_version ||= begin
  285. info = SearchIndexBackend.info
  286. number = nil
  287. if info.present?
  288. number = info['version']['number'].to_s
  289. end
  290. number
  291. end
  292. end
  293. def es_version_supported?
  294. version_split = es_version.split('.')
  295. version = "#{version_split[0]}#{format('%<minor>03d', minor: version_split[1])}#{format('%<patch>03d', patch: version_split[2])}".to_i
  296. # only versions greater/equal than 5.6.0 are supported
  297. return if version < 5_006_000
  298. true
  299. end
  300. # no es_pipeline for elasticsearch 5.5 and lower
  301. def es_pipeline?
  302. number = es_version
  303. return false if number.blank?
  304. return false if number.match?(/^[2-4]\./)
  305. return false if number.match?(/^5\.[0-5]\./)
  306. true
  307. end
  308. # no multi index for elasticsearch 5.6 and lower
  309. def es_multi_index?
  310. number = es_version
  311. return false if number.blank?
  312. return false if number.match?(/^[2-5]\./)
  313. true
  314. end
  315. # no type in mapping
  316. def es_type_in_mapping?
  317. number = es_version
  318. return true if number.blank?
  319. return true if number.match?(/^[2-6]\./)
  320. false
  321. end