search_index_backend.rb 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. class SearchIndexBackend
  3. =begin
  4. info about used search index machine
  5. SearchIndexBackend.info
  6. =end
  7. def self.info
  8. url = Setting.get('es_url').to_s
  9. return if url.blank?
  10. response = make_request(url)
  11. if response.success?
  12. installed_version = response.data.dig('version', 'number')
  13. raise "Unable to get elasticsearch version from response: #{response.inspect}" if installed_version.blank?
  14. version_supported = Gem::Version.new(installed_version) < Gem::Version.new('8')
  15. raise "Version #{installed_version} of configured elasticsearch is not supported." if !version_supported
  16. version_supported = Gem::Version.new(installed_version) > Gem::Version.new('2.3')
  17. raise "Version #{installed_version} of configured elasticsearch is not supported." if !version_supported
  18. return response.data
  19. end
  20. raise humanized_error(
  21. verb: 'GET',
  22. url: url,
  23. response: response,
  24. )
  25. end
  26. =begin
  27. update processors
  28. SearchIndexBackend.processors(
  29. _ingest/pipeline/attachment: {
  30. description: 'Extract attachment information from arrays',
  31. processors: [
  32. {
  33. foreach: {
  34. field: 'ticket.articles.attachments',
  35. processor: {
  36. attachment: {
  37. target_field: '_ingest._value.attachment',
  38. field: '_ingest._value.data'
  39. }
  40. }
  41. }
  42. }
  43. ]
  44. }
  45. )
  46. =end
  47. def self.processors(data)
  48. data.each do |key, items|
  49. url = "#{Setting.get('es_url')}/#{key}"
  50. items.each do |item|
  51. if item[:action] == 'delete'
  52. response = make_request(url, method: :delete)
  53. next if response.success?
  54. next if response.code.to_s == '404'
  55. raise humanized_error(
  56. verb: 'DELETE',
  57. url: url,
  58. response: response,
  59. )
  60. end
  61. item.delete(:action)
  62. make_request_and_validate(url, data: item, method: :put)
  63. end
  64. end
  65. true
  66. end
  67. =begin
  68. create/update/delete index
  69. SearchIndexBackend.index(
  70. :action => 'create', # create/update/delete
  71. :name => 'Ticket',
  72. :data => {
  73. :mappings => {
  74. :Ticket => {
  75. :properties => {
  76. :articles => {
  77. :type => 'nested',
  78. :properties => {
  79. 'attachment' => { :type => 'attachment' }
  80. }
  81. }
  82. }
  83. }
  84. }
  85. }
  86. )
  87. SearchIndexBackend.index(
  88. :action => 'delete', # create/update/delete
  89. :name => 'Ticket',
  90. )
  91. =end
  92. def self.index(data)
  93. url = build_url(data[:name], nil, false, false)
  94. return if url.blank?
  95. if data[:action] && data[:action] == 'delete'
  96. return SearchIndexBackend.remove(data[:name])
  97. end
  98. make_request_and_validate(url, data: data[:data], method: :put)
  99. end
  100. =begin
  101. add new object to search index
  102. SearchIndexBackend.add('Ticket', some_data_object)
  103. =end
  104. def self.add(type, data)
  105. url = build_url(type, data['id'])
  106. return if url.blank?
  107. make_request_and_validate(url, data: data, method: :post)
  108. end
  109. =begin
  110. remove whole data from index
  111. SearchIndexBackend.remove('Ticket', 123)
  112. SearchIndexBackend.remove('Ticket')
  113. =end
  114. def self.remove(type, o_id = nil)
  115. url = if o_id
  116. build_url(type, o_id, false, true)
  117. else
  118. build_url(type, o_id, false, false)
  119. end
  120. return if url.blank?
  121. response = make_request(url, method: :delete)
  122. return true if response.success?
  123. return true if response.code.to_s == '400'
  124. humanized_error = humanized_error(
  125. verb: 'DELETE',
  126. url: url,
  127. response: response,
  128. )
  129. Rails.logger.info "NOTICE: can't delete index: #{humanized_error}"
  130. false
  131. end
  132. =begin
  133. @param query [String] search query
  134. @param index [String, Array<String>] indexes to search in (see search_by_index)
  135. @param options [Hash] search options (see build_query)
  136. @return search result
  137. @example Sample queries
  138. result = SearchIndexBackend.search('search query', ['User', 'Organization'], limit: limit)
  139. - result = SearchIndexBackend.search('search query', 'User', limit: limit)
  140. result = SearchIndexBackend.search('search query', 'User', limit: limit, sort_by: ['updated_at'], order_by: ['desc'])
  141. result = SearchIndexBackend.search('search query', 'User', limit: limit, sort_by: ['active', updated_at'], order_by: ['desc', 'desc'])
  142. result = [
  143. {
  144. :id => 123,
  145. :type => 'User',
  146. },
  147. {
  148. :id => 125,
  149. :type => 'User',
  150. },
  151. {
  152. :id => 15,
  153. :type => 'Organization',
  154. }
  155. ]
  156. =end
  157. def self.search(query, index, options = {})
  158. if !index.is_a? Array
  159. return search_by_index(query, index, options)
  160. end
  161. index
  162. .map { |local_index| search_by_index(query, local_index, options) }
  163. .compact
  164. .flatten(1)
  165. end
  166. =begin
  167. @param query [String] search query
  168. @param index [String] index name
  169. @param options [Hash] search options (see build_query)
  170. @return search result
  171. =end
  172. def self.search_by_index(query, index, options = {})
  173. return [] if query.blank?
  174. url = build_url
  175. return [] if url.blank?
  176. url += build_search_url(index)
  177. # real search condition
  178. condition = {
  179. 'query_string' => {
  180. 'query' => append_wildcard_to_simple_query(query),
  181. 'default_operator' => 'AND',
  182. 'analyze_wildcard' => true,
  183. }
  184. }
  185. if (fields = options.dig(:highlight_fields_by_indexes, index.to_sym))
  186. condition['query_string']['fields'] = fields
  187. end
  188. query_data = build_query(condition, options)
  189. if (fields = options.dig(:highlight_fields_by_indexes, index.to_sym))
  190. fields_for_highlight = fields.each_with_object({}) { |elem, memo| memo[elem] = {} }
  191. query_data[:highlight] = { fields: fields_for_highlight }
  192. end
  193. response = make_request(url, data: query_data)
  194. if !response.success?
  195. Rails.logger.error humanized_error(
  196. verb: 'GET',
  197. url: url,
  198. payload: query_data,
  199. response: response,
  200. )
  201. return []
  202. end
  203. data = response.data&.dig('hits', 'hits')
  204. return [] if !data
  205. data.map do |item|
  206. Rails.logger.info "... #{item['_type']} #{item['_id']}"
  207. output = {
  208. id: item['_id'],
  209. type: index,
  210. }
  211. if options.dig(:highlight_fields_by_indexes, index.to_sym)
  212. output[:highlight] = item['highlight']
  213. end
  214. output
  215. end
  216. end
  217. def self.search_by_index_sort(sort_by = nil, order_by = nil)
  218. result = []
  219. sort_by&.each_with_index do |value, index|
  220. next if value.blank?
  221. next if order_by&.at(index).blank?
  222. # for sorting values use .keyword values (no analyzer is used - plain values)
  223. if value !~ /\./ && value !~ /_(time|date|till|id|ids|at)$/
  224. value += '.keyword'
  225. end
  226. result.push(
  227. value => {
  228. order: order_by[index],
  229. },
  230. )
  231. end
  232. if result.blank?
  233. result.push(
  234. updated_at: {
  235. order: 'desc',
  236. },
  237. )
  238. end
  239. result.push('_score')
  240. result
  241. end
  242. =begin
  243. get count of tickets and tickets which match on selector
  244. result = SearchIndexBackend.selectors(index, selector)
  245. example with a simple search:
  246. result = SearchIndexBackend.selectors('Ticket', { 'category' => { 'operator' => 'is', 'value' => 'aa::ab' } })
  247. result = [
  248. { id: 1, type: 'Ticket' },
  249. { id: 2, type: 'Ticket' },
  250. { id: 3, type: 'Ticket' },
  251. ]
  252. you also can get aggregations
  253. result = SearchIndexBackend.selectors(index, selector, options, aggs_interval)
  254. example for aggregations within one year
  255. aggs_interval = {
  256. from: '2015-01-01',
  257. to: '2015-12-31',
  258. interval: 'month', # year, quarter, month, week, day, hour, minute, second
  259. field: 'created_at',
  260. }
  261. options = {
  262. limit: 123,
  263. current_user: User.find(123),
  264. }
  265. result = SearchIndexBackend.selectors('Ticket', { 'category' => { 'operator' => 'is', 'value' => 'aa::ab' } }, options, aggs_interval)
  266. result = {
  267. hits:{
  268. total:4819,
  269. },
  270. aggregations:{
  271. time_buckets:{
  272. buckets:[
  273. {
  274. key_as_string:"2014-10-01T00:00:00.000Z",
  275. key:1412121600000,
  276. doc_count:420
  277. },
  278. {
  279. key_as_string:"2014-11-01T00:00:00.000Z",
  280. key:1414800000000,
  281. doc_count:561
  282. },
  283. ...
  284. ]
  285. }
  286. }
  287. }
  288. =end
  289. def self.selectors(index, selectors = nil, options = {}, aggs_interval = nil)
  290. raise 'no selectors given' if !selectors
  291. url = build_url(nil, nil, false, false)
  292. return if url.blank?
  293. url += build_search_url(index)
  294. data = selector2query(selectors, options, aggs_interval)
  295. response = make_request(url, data: data)
  296. if !response.success?
  297. raise humanized_error(
  298. verb: 'GET',
  299. url: url,
  300. payload: data,
  301. response: response,
  302. )
  303. end
  304. Rails.logger.debug { response.data.to_json }
  305. if aggs_interval.blank? || aggs_interval[:interval].blank?
  306. ticket_ids = []
  307. response.data['hits']['hits'].each do |item|
  308. ticket_ids.push item['_id']
  309. end
  310. # in lower ES 6 versions, we get total count directly, in higher
  311. # versions we need to pick it from total has
  312. count = response.data['hits']['total']
  313. if response.data['hits']['total'].class != Integer
  314. count = response.data['hits']['total']['value']
  315. end
  316. return {
  317. count: count,
  318. ticket_ids: ticket_ids,
  319. }
  320. end
  321. response.data
  322. end
  323. DEFAULT_SELECTOR_OPTIONS = {
  324. limit: 10
  325. }.freeze
  326. def self.selector2query(selector, options, aggs_interval)
  327. options = DEFAULT_QUERY_OPTIONS.merge(options.deep_symbolize_keys)
  328. query_must = []
  329. query_must_not = []
  330. relative_map = {
  331. day: 'd',
  332. year: 'y',
  333. month: 'M',
  334. hour: 'h',
  335. minute: 'm',
  336. }
  337. if selector.present?
  338. selector.each do |key, data|
  339. key_tmp = key.sub(/^.+?\./, '')
  340. wildcard_or_term = 'term'
  341. t = {}
  342. # use .keyword in case of compare exact values
  343. if data['operator'] == 'is' || data['operator'] == 'is not'
  344. if data['value'].is_a?(Array)
  345. data['value'].each do |value|
  346. next if !value.is_a?(String) || value !~ /[A-z]/
  347. wildcard_or_term = 'terms'
  348. key_tmp += '.keyword'
  349. break
  350. end
  351. elsif data['value'].is_a?(String) && /[A-z]/.match?(data['value'])
  352. key_tmp += '.keyword'
  353. wildcard_or_term = 'term'
  354. end
  355. end
  356. # use .keyword and wildcard search in cases where query contains non A-z chars
  357. if data['operator'] == 'contains' || data['operator'] == 'contains not'
  358. if data['value'].is_a?(Array)
  359. data['value'].each_with_index do |value, index|
  360. next if !value.is_a?(String) || value !~ /[A-z]/ || value !~ /\W/
  361. data['value'][index] = "*#{value}*"
  362. key_tmp += '.keyword'
  363. wildcard_or_term = 'wildcards'
  364. break
  365. end
  366. elsif data['value'].is_a?(String) && /[A-z]/.match?(data['value']) && data['value'] =~ /\W/
  367. data['value'] = "*#{data['value']}*"
  368. key_tmp += '.keyword'
  369. wildcard_or_term = 'wildcard'
  370. end
  371. end
  372. # is/is not/contains/contains not
  373. if data['operator'] == 'is' || data['operator'] == 'is not' || data['operator'] == 'contains' || data['operator'] == 'contains not'
  374. t[wildcard_or_term] = {}
  375. t[wildcard_or_term][key_tmp] = data['value']
  376. if data['operator'] == 'is' || data['operator'] == 'contains'
  377. query_must.push t
  378. elsif data['operator'] == 'is not' || data['operator'] == 'contains not'
  379. query_must_not.push t
  380. end
  381. elsif data['operator'] == 'contains all' || data['operator'] == 'contains one' || data['operator'] == 'contains all not' || data['operator'] == 'contains one not'
  382. values = data['value'].split(',').map(&:strip)
  383. t[:query_string] = {}
  384. if data['operator'] == 'contains all'
  385. t[:query_string][:query] = "#{key_tmp}:\"#{values.join('" AND "')}\""
  386. query_must.push t
  387. elsif data['operator'] == 'contains one not'
  388. t[:query_string][:query] = "#{key_tmp}:\"#{values.join('" OR "')}\""
  389. query_must_not.push t
  390. elsif data['operator'] == 'contains one'
  391. t[:query_string][:query] = "#{key_tmp}:\"#{values.join('" OR "')}\""
  392. query_must.push t
  393. elsif data['operator'] == 'contains all not'
  394. t[:query_string][:query] = "#{key_tmp}:\"#{values.join('" AND "')}\""
  395. query_must_not.push t
  396. end
  397. # within last/within next (relative)
  398. elsif data['operator'] == 'within last (relative)' || data['operator'] == 'within next (relative)'
  399. range = relative_map[data['range'].to_sym]
  400. if range.blank?
  401. raise "Invalid relative_map for range '#{data['range']}'."
  402. end
  403. t[:range] = {}
  404. t[:range][key_tmp] = {}
  405. if data['operator'] == 'within last (relative)'
  406. t[:range][key_tmp][:gte] = "now-#{data['value']}#{range}"
  407. else
  408. t[:range][key_tmp][:lt] = "now+#{data['value']}#{range}"
  409. end
  410. query_must.push t
  411. # before/after (relative)
  412. elsif data['operator'] == 'before (relative)' || data['operator'] == 'after (relative)'
  413. range = relative_map[data['range'].to_sym]
  414. if range.blank?
  415. raise "Invalid relative_map for range '#{data['range']}'."
  416. end
  417. t[:range] = {}
  418. t[:range][key_tmp] = {}
  419. if data['operator'] == 'before (relative)'
  420. t[:range][key_tmp][:lt] = "now-#{data['value']}#{range}"
  421. else
  422. t[:range][key_tmp][:gt] = "now+#{data['value']}#{range}"
  423. end
  424. query_must.push t
  425. # before/after (absolute)
  426. elsif data['operator'] == 'before (absolute)' || data['operator'] == 'after (absolute)'
  427. t[:range] = {}
  428. t[:range][key_tmp] = {}
  429. if data['operator'] == 'before (absolute)'
  430. t[:range][key_tmp][:lt] = (data['value'])
  431. else
  432. t[:range][key_tmp][:gt] = (data['value'])
  433. end
  434. query_must.push t
  435. else
  436. raise "unknown operator '#{data['operator']}' for #{key}"
  437. end
  438. end
  439. end
  440. data = {
  441. query: {},
  442. size: options[:limit],
  443. }
  444. # add aggs to filter
  445. if aggs_interval.present?
  446. if aggs_interval[:interval].present?
  447. data[:size] = 0
  448. data[:aggs] = {
  449. time_buckets: {
  450. date_histogram: {
  451. field: aggs_interval[:field],
  452. interval: aggs_interval[:interval],
  453. }
  454. }
  455. }
  456. if aggs_interval[:timezone].present?
  457. data[:aggs][:time_buckets][:date_histogram][:time_zone] = aggs_interval[:timezone]
  458. end
  459. end
  460. r = {}
  461. r[:range] = {}
  462. r[:range][aggs_interval[:field]] = {
  463. from: aggs_interval[:from],
  464. to: aggs_interval[:to],
  465. }
  466. query_must.push r
  467. end
  468. data[:query][:bool] ||= {}
  469. if query_must.present?
  470. data[:query][:bool][:must] = query_must
  471. end
  472. if query_must_not.present?
  473. data[:query][:bool][:must_not] = query_must_not
  474. end
  475. # add sort
  476. if aggs_interval.present? && aggs_interval[:field].present? && aggs_interval[:interval].blank?
  477. sort = []
  478. sort[0] = {}
  479. sort[0][aggs_interval[:field]] = {
  480. order: 'desc'
  481. }
  482. sort[1] = '_score'
  483. data['sort'] = sort
  484. else
  485. data['sort'] = search_by_index_sort(options[:sort_by], options[:order_by])
  486. end
  487. data
  488. end
  489. =begin
  490. return true if backend is configured
  491. result = SearchIndexBackend.enabled?
  492. =end
  493. def self.enabled?
  494. return false if Setting.get('es_url').blank?
  495. true
  496. end
  497. def self.build_index_name(index)
  498. local_index = "#{Setting.get('es_index')}_#{Rails.env}"
  499. "#{local_index}_#{index.underscore.tr('/', '_')}"
  500. end
  501. =begin
  502. generate url for index or document access (only for internal use)
  503. # url to access single document in index (in case with_pipeline or not)
  504. url = SearchIndexBackend.build_url('User', 123, with_pipeline)
  505. # url to access whole index
  506. url = SearchIndexBackend.build_url('User')
  507. # url to access document definition in index (only es6 and higher)
  508. url = SearchIndexBackend.build_url('User', nil, false, true)
  509. # base url
  510. url = SearchIndexBackend.build_url
  511. =end
  512. def self.build_url(type = nil, o_id = nil, with_pipeline = true, with_document_type = true)
  513. return if !SearchIndexBackend.enabled?
  514. # for elasticsearch 5.6 and lower
  515. index = "#{Setting.get('es_index')}_#{Rails.env}"
  516. if Setting.get('es_multi_index') == false
  517. url = Setting.get('es_url')
  518. url = if type
  519. if with_pipeline == true
  520. url_pipline = Setting.get('es_pipeline')
  521. if url_pipline.present?
  522. url_pipline = "?pipeline=#{url_pipline}"
  523. end
  524. end
  525. if o_id
  526. "#{url}/#{index}/#{type}/#{o_id}#{url_pipline}"
  527. else
  528. "#{url}/#{index}/#{type}#{url_pipline}"
  529. end
  530. else
  531. "#{url}/#{index}"
  532. end
  533. return url
  534. end
  535. # for elasticsearch 6.x and higher
  536. url = Setting.get('es_url')
  537. if with_pipeline == true
  538. url_pipline = Setting.get('es_pipeline')
  539. if url_pipline.present?
  540. url_pipline = "?pipeline=#{url_pipline}"
  541. end
  542. end
  543. if type
  544. index = build_index_name(type)
  545. # access (e. g. creating or dropping) whole index
  546. if with_document_type == false
  547. return "#{url}/#{index}"
  548. end
  549. # access single document in index (e. g. drop or add document)
  550. if o_id
  551. return "#{url}/#{index}/_doc/#{o_id}#{url_pipline}"
  552. end
  553. # access document type (e. g. creating or dropping document mapping)
  554. return "#{url}/#{index}/_doc#{url_pipline}"
  555. end
  556. "#{url}/"
  557. end
  558. =begin
  559. generate url searchaccess (only for internal use)
  560. # url search access with single index
  561. url = SearchIndexBackend.build_search_url('User')
  562. # url to access all over es
  563. url = SearchIndexBackend.build_search_url
  564. =end
  565. def self.build_search_url(index = nil)
  566. # for elasticsearch 5.6 and lower
  567. if Setting.get('es_multi_index') == false
  568. if index
  569. return "/#{index}/_search"
  570. end
  571. return '/_search'
  572. end
  573. # for elasticsearch 6.x and higher
  574. "#{build_index_name(index)}/_doc/_search"
  575. end
  576. def self.humanized_error(verb:, url:, payload: nil, response:)
  577. prefix = "Unable to process #{verb} request to elasticsearch URL '#{url}'."
  578. suffix = "\n\nResponse:\n#{response.inspect}\n\nPayload:\n#{payload.inspect}"
  579. if payload.respond_to?(:to_json)
  580. suffix += "\n\nPayload size: #{payload.to_json.bytesize / 1024 / 1024}M"
  581. end
  582. message = if response&.error&.match?('Connection refused')
  583. "Elasticsearch is not reachable, probably because it's not running or even installed."
  584. elsif url.end_with?('pipeline/zammad-attachment', 'pipeline=zammad-attachment') && response.code == 400
  585. 'The installed attachment plugin could not handle the request payload. Ensure that the correct attachment plugin is installed (5.6 => ingest-attachment, 2.4 - 5.5 => mapper-attachments).'
  586. else
  587. 'Check the response and payload for detailed information: '
  588. end
  589. result = "#{prefix} #{message}#{suffix}"
  590. Rails.logger.error result.first(40_000)
  591. result
  592. end
  593. # add * on simple query like "somephrase23"
  594. def self.append_wildcard_to_simple_query(query)
  595. query.strip!
  596. query += '*' if !query.match?(/:/)
  597. query
  598. end
  599. =begin
  600. @param condition [Hash] search condition
  601. @param options [Hash] search options
  602. @option options [Integer] :from
  603. @option options [Integer] :limit
  604. @option options [Hash] :query_extension applied to ElasticSearch query
  605. @option options [Array<String>] :order_by ordering directions, desc or asc
  606. @option options [Array<String>] :sort_by fields to sort by
  607. =end
  608. DEFAULT_QUERY_OPTIONS = {
  609. from: 0,
  610. limit: 10
  611. }.freeze
  612. def self.build_query(condition, options = {})
  613. options = DEFAULT_QUERY_OPTIONS.merge(options.deep_symbolize_keys)
  614. data = {
  615. from: options[:from],
  616. size: options[:limit],
  617. sort: search_by_index_sort(options[:sort_by], options[:order_by]),
  618. query: {
  619. bool: {
  620. must: []
  621. }
  622. }
  623. }
  624. if (extension = options.dig(:query_extension))
  625. data[:query].deep_merge! extension.deep_dup
  626. end
  627. data[:query][:bool][:must].push condition
  628. data
  629. end
  630. =begin
  631. refreshes all indexes to make previous request data visible in future requests
  632. SearchIndexBackend.refresh
  633. =end
  634. def self.refresh
  635. return if !enabled?
  636. url = "#{Setting.get('es_url')}/_all/_refresh"
  637. make_request_and_validate(url, method: :post)
  638. end
  639. =begin
  640. helper method for making HTTP calls
  641. @param url [String] url
  642. @option params [Hash] :data is a payload hash
  643. @option params [Symbol] :method is a HTTP method
  644. @option params [Integer] :open_timeout is HTTP request open timeout
  645. @option params [Integer] :read_timeout is HTTP request read timeout
  646. @return UserAgent response
  647. =end
  648. def self.make_request(url, data: {}, method: :get, open_timeout: 8, read_timeout: 60)
  649. Rails.logger.info "# curl -X #{method} \"#{url}\" "
  650. Rails.logger.debug { "-d '#{data.to_json}'" } if data.present?
  651. options = {
  652. json: true,
  653. open_timeout: open_timeout,
  654. read_timeout: read_timeout,
  655. open_socket_tries: 3,
  656. user: Setting.get('es_user'),
  657. password: Setting.get('es_password'),
  658. }
  659. response = UserAgent.send(method, url, data, options)
  660. Rails.logger.info "# #{response.code}"
  661. response
  662. end
  663. =begin
  664. helper method for making HTTP calls and raising error if response was not success
  665. @param url [String] url
  666. @option args [Hash] see {make_request}
  667. @return [Boolean] always returns true. Raises error if something went wrong.
  668. =end
  669. def self.make_request_and_validate(url, **args)
  670. response = make_request(url, args)
  671. return true if response.success?
  672. raise humanized_error(
  673. verb: args[:method],
  674. url: url,
  675. payload: args[:data],
  676. response: response
  677. )
  678. end
  679. end