search_index_es.rake 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. $LOAD_PATH << './lib'
  2. require 'rubygems'
  3. namespace :searchindex do
  4. task :drop, [:opts] => %i[environment searchindex:configured 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:configured 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:configured 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:configured 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:configured 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:configured searchindex:version_supported] do |_t, _args|
  152. print 'refresh all indexes...'
  153. SearchIndexBackend.refresh
  154. end
  155. task :rebuild, [:opts] => %i[environment searchindex:configured 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 Elasticsearch version is not supported! Please update your version to a greater equal than 5.6.0 (Your current version: #{es_version})."
  163. end
  164. task :configured, [:opts] => :environment do |_t, _args|
  165. next if es_configured?
  166. abort "You have not configured Elasticsearch (Setting.get('es_url'))."
  167. end
  168. end
  169. =begin
  170. This function will return a index mapping based on the
  171. attributes of the database table of the existing object.
  172. mapping = get_mapping_properties_object(Ticket)
  173. Returns:
  174. mapping = {
  175. User: {
  176. properties: {
  177. firstname: {
  178. type: 'keyword',
  179. },
  180. }
  181. }
  182. }
  183. =end
  184. def get_mapping_properties_object(object)
  185. name = object.name
  186. if es_multi_index?
  187. name = '_doc'
  188. end
  189. result = {
  190. name => {
  191. properties: {}
  192. }
  193. }
  194. store_columns = %w[preferences data]
  195. # for elasticsearch 6.x and later
  196. string_type = 'text'
  197. string_raw = { 'type': 'keyword', 'ignore_above': 5012 }
  198. boolean_raw = { 'type': 'boolean' }
  199. # for elasticsearch 5.6 and lower
  200. if !es_multi_index?
  201. string_type = 'string'
  202. string_raw = { 'type': 'string', 'index': 'not_analyzed' }
  203. boolean_raw = { 'type': 'boolean', 'index': 'not_analyzed' }
  204. end
  205. object.columns_hash.each do |key, value|
  206. if value.type == :string && value.limit && value.limit <= 5000 && store_columns.exclude?(key)
  207. result[name][:properties][key] = {
  208. type: string_type,
  209. fields: {
  210. keyword: string_raw,
  211. }
  212. }
  213. elsif value.type == :integer
  214. result[name][:properties][key] = {
  215. type: 'integer',
  216. }
  217. elsif value.type == :datetime
  218. result[name][:properties][key] = {
  219. type: 'date',
  220. }
  221. elsif value.type == :boolean
  222. result[name][:properties][key] = {
  223. type: 'boolean',
  224. fields: {
  225. keyword: boolean_raw,
  226. }
  227. }
  228. elsif value.type == :binary
  229. result[name][:properties][key] = {
  230. type: 'binary',
  231. }
  232. elsif value.type == :bigint
  233. result[name][:properties][key] = {
  234. type: 'long',
  235. }
  236. elsif value.type == :decimal
  237. result[name][:properties][key] = {
  238. type: 'float',
  239. }
  240. elsif value.type == :date
  241. result[name][:properties][key] = {
  242. type: 'date',
  243. }
  244. end
  245. end
  246. # es with mapper-attachments plugin
  247. if object.name == 'Ticket'
  248. # do not server attachments if document is requested
  249. result[name][:_source] = {
  250. excludes: ['article.attachment']
  251. }
  252. # for elasticsearch 5.5 and lower
  253. if !es_pipeline?
  254. result[name][:_source] = {
  255. excludes: ['article.attachment']
  256. }
  257. result[name][:properties][:article] = {
  258. type: 'nested',
  259. include_in_parent: true,
  260. properties: {
  261. attachment: {
  262. type: 'attachment',
  263. }
  264. }
  265. }
  266. end
  267. end
  268. if object.name == 'KnowledgeBase::Answer::Translation'
  269. # do not server attachments if document is requested
  270. result[name][:_source] = {
  271. excludes: ['attachment']
  272. }
  273. # for elasticsearch 5.5 and lower
  274. if !es_pipeline?
  275. result[name][:_source] = {
  276. excludes: ['attachment']
  277. }
  278. result[name][:properties][:attachment] = {
  279. type: 'attachment',
  280. }
  281. end
  282. end
  283. return result if es_type_in_mapping?
  284. result[name]
  285. end
  286. # get es version
  287. def es_version
  288. @es_version ||= begin
  289. info = SearchIndexBackend.info
  290. number = nil
  291. if info.present?
  292. number = info['version']['number'].to_s
  293. end
  294. number
  295. end
  296. end
  297. def es_version_supported?
  298. version_split = es_version.split('.')
  299. version = "#{version_split[0]}#{format('%<minor>03d', minor: version_split[1])}#{format('%<patch>03d', patch: version_split[2])}".to_i
  300. # only versions greater/equal than 5.6.0 are supported
  301. return if version < 5_006_000
  302. true
  303. end
  304. # no es_pipeline for elasticsearch 5.5 and lower
  305. def es_pipeline?
  306. number = es_version
  307. return false if number.blank?
  308. return false if number.match?(/^[2-4]\./)
  309. return false if number.match?(/^5\.[0-5]\./)
  310. true
  311. end
  312. # no multi index for elasticsearch 5.6 and lower
  313. def es_multi_index?
  314. number = es_version
  315. return false if number.blank?
  316. return false if number.match?(/^[2-5]\./)
  317. true
  318. end
  319. # no type in mapping
  320. def es_type_in_mapping?
  321. number = es_version
  322. return true if number.blank?
  323. return true if number.match?(/^[2-6]\./)
  324. false
  325. end
  326. # is es configured?
  327. def es_configured?
  328. return false if Setting.get('es_url').blank?
  329. true
  330. end