elasticsearch_record 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +4 -0
  3. data/Gemfile.lock +10 -14
  4. data/README.md +180 -27
  5. data/docs/CHANGELOG.md +36 -18
  6. data/docs/LICENSE.txt +1 -1
  7. data/elasticsearch_record.gemspec +42 -0
  8. data/lib/active_record/connection_adapters/elasticsearch/column.rb +20 -6
  9. data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +142 -125
  10. data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +2 -23
  11. data/lib/active_record/connection_adapters/elasticsearch/schema_creation.rb +30 -0
  12. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/attribute_methods.rb +103 -0
  13. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/column_methods.rb +42 -0
  14. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/create_table_definition.rb +158 -0
  15. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_alias_definition.rb +32 -0
  16. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_definition.rb +132 -0
  17. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_mapping_definition.rb +110 -0
  18. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_setting_definition.rb +136 -0
  19. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/update_table_definition.rb +174 -0
  20. data/lib/active_record/connection_adapters/elasticsearch/schema_definitions.rb +37 -0
  21. data/lib/active_record/connection_adapters/elasticsearch/schema_dumper.rb +110 -0
  22. data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +398 -174
  23. data/lib/active_record/connection_adapters/elasticsearch/table_statements.rb +232 -0
  24. data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +2 -0
  25. data/lib/active_record/connection_adapters/elasticsearch/unsupported_implementation.rb +32 -0
  26. data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +112 -19
  27. data/lib/arel/collectors/elasticsearch_query.rb +0 -1
  28. data/lib/arel/visitors/elasticsearch.rb +7 -579
  29. data/lib/arel/visitors/elasticsearch_base.rb +234 -0
  30. data/lib/arel/visitors/elasticsearch_query.rb +463 -0
  31. data/lib/arel/visitors/elasticsearch_schema.rb +124 -0
  32. data/lib/elasticsearch_record/core.rb +44 -10
  33. data/lib/elasticsearch_record/errors.rb +13 -0
  34. data/lib/elasticsearch_record/gem_version.rb +6 -2
  35. data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +27 -9
  36. data/lib/elasticsearch_record/model_schema.rb +5 -0
  37. data/lib/elasticsearch_record/persistence.rb +31 -26
  38. data/lib/elasticsearch_record/query.rb +56 -17
  39. data/lib/elasticsearch_record/querying.rb +17 -0
  40. data/lib/elasticsearch_record/relation/calculation_methods.rb +3 -0
  41. data/lib/elasticsearch_record/relation/core_methods.rb +57 -17
  42. data/lib/elasticsearch_record/relation/query_clause_tree.rb +38 -1
  43. data/lib/elasticsearch_record/relation/query_methods.rb +6 -0
  44. data/lib/elasticsearch_record/relation/result_methods.rb +15 -9
  45. data/lib/elasticsearch_record/result.rb +32 -5
  46. data/lib/elasticsearch_record/statement_cache.rb +2 -1
  47. data/lib/elasticsearch_record.rb +2 -2
  48. metadata +29 -11
  49. data/.ruby-version +0 -1
  50. data/lib/elasticsearch_record/schema_migration.rb +0 -30
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Visitors
5
+ module ElasticsearchSchema
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ delegate :type_to_sql,
10
+ to: :connection, private: true
11
+ end
12
+
13
+ private
14
+
15
+ #################
16
+ # SCHEMA VISITS #
17
+ #################
18
+
19
+ def visit_CreateTableDefinition(o)
20
+ # prepare query
21
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_INDEX_CREATE)
22
+
23
+ # set the name of the index
24
+ claim(:index, visit(o.name))
25
+
26
+ # sets settings
27
+ resolve(o, :visit_TableSettings) if o.settings.present?
28
+
29
+ # sets mappings
30
+ resolve(o, :visit_TableMappings) if o.mappings.present?
31
+
32
+ # sets aliases
33
+ resolve(o, :visit_TableAliases) if o.aliases.present?
34
+ end
35
+
36
+ def visit_InterlacedUpdateTableDefinition(o)
37
+ # set the name of the index
38
+ claim(:index, visit(o.name))
39
+
40
+ # prepare definition
41
+ visit(o.definition)
42
+ end
43
+
44
+ def visit_ChangeMappingDefinition(o)
45
+ # prepare query
46
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_INDEX_UPDATE_MAPPING)
47
+
48
+ assign(:properties, {}) do
49
+ resolve(o.items, :visit_TableMappingDefinition)
50
+ end
51
+ end
52
+
53
+ alias :visit_AddMappingDefinition :visit_ChangeMappingDefinition
54
+
55
+ def visit_ChangeSettingDefinition(o)
56
+ # prepare query
57
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_INDEX_UPDATE_SETTING)
58
+
59
+ # special overcomplicated blocks to assign a hash of settings directly to the body
60
+ assign(:__claim__, {}) do
61
+ assign(:body, {}) do
62
+ resolve(o.items, :visit_TableSettingDefinition)
63
+ end
64
+ end
65
+ end
66
+
67
+ alias :visit_AddSettingDefinition :visit_ChangeSettingDefinition
68
+ alias :visit_DeleteSettingDefinition :visit_ChangeSettingDefinition
69
+
70
+ def visit_ChangeAliasDefinition(o)
71
+ # prepare query
72
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_INDEX_UPDATE_ALIAS)
73
+
74
+ claim(:argument, :name, o.item.name)
75
+ claim(:body, o.item.attributes)
76
+ end
77
+
78
+ alias :visit_AddAliasDefinition :visit_ChangeAliasDefinition
79
+
80
+ def visit_DeleteAliasDefinition(o)
81
+ # prepare query
82
+ claim(:type, ::ElasticsearchRecord::Query::TYPE_INDEX_DELETE_ALIAS)
83
+
84
+ claim(:argument, :name, o.items.map(&:name).join(','))
85
+ end
86
+
87
+ ##############
88
+ # SUB VISITS #
89
+ ##############
90
+
91
+ def visit_TableSettings(o)
92
+ assign(:settings, {}) do
93
+ resolve(o.settings, :visit_TableSettingDefinition)
94
+ end
95
+ end
96
+
97
+ def visit_TableMappings(o)
98
+ assign(:mappings, {}) do
99
+ assign(:properties, {}) do
100
+ resolve(o.mappings, :visit_TableMappingDefinition)
101
+ end
102
+ end
103
+ end
104
+
105
+ def visit_TableAliases(o)
106
+ assign(:aliases, {}) do
107
+ resolve(o.aliases, :visit_TableAliasDefinition)
108
+ end
109
+ end
110
+
111
+ def visit_TableSettingDefinition(o)
112
+ assign(o.name, o.value, :__force__)
113
+ end
114
+
115
+ def visit_TableMappingDefinition(o)
116
+ assign(o.name, o.attributes.merge({type: type_to_sql(o.type)}))
117
+ end
118
+
119
+ def visit_TableAliasDefinition(o)
120
+ assign(o.name, o.attributes)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -2,35 +2,69 @@ module ElasticsearchRecord
2
2
  module Core
3
3
  extend ActiveSupport::Concern
4
4
 
5
- # in default, this reads the primary key column's value (+_id+).
6
- # But since elasticsearch supports also additional "id" columns, we need to check against that.
5
+ included do
6
+ class_attribute :relay_id_attribute, instance_writer: false, default: false
7
+ end
8
+
9
+ # overwrite to provide a Elasticsearch version of returning a 'primary_key' attribute.
10
+ # Elasticsearch uses the static +_id+ column as primary_key, but also supports an additional +id+ column.
11
+ # To provide functionality of returning the +id+ attribute, this method must also support it.
12
+ # @return [Object]
7
13
  def id
8
- has_attribute?('id') ? _read_attribute('id') : super
14
+ # check, if the model has a +id+ attribute
15
+ return _read_attribute('id') if relay_id_attribute? && has_attribute?('id')
16
+
17
+ super
9
18
  end
10
19
 
11
- # in default, this sets the primary key column's value (+_id+).
12
- # But since elasticsearch supports also additional "id" columns, we need to check against that.
20
+ # overwrite to provide a Elasticsearch version of setting a 'primary_key' attribute.
21
+ # Elasticsearch uses the static +_id+ column as primary_key, but also supports an additional +id+ column.
22
+ # To provide functionality of setting the +id+ attribute, this method must also support it.
23
+ # @param [Object] value
13
24
  def id=(value)
14
- has_attribute?('id') ? _write_attribute('id', value) : super
25
+ # check, if the model has a +id+ attribute
26
+ return _write_attribute('id', value) if relay_id_attribute? && has_attribute?('id')
27
+
28
+ # auxiliary update the +_id+ virtual column if we have a different primary_key
29
+ _write_attribute('_id', value) if @primary_key != '_id'
30
+
31
+ super
15
32
  end
16
33
 
17
- # overwrite the write_attribute method to write 'id', if present?
34
+ # overwrite to provide a Elasticsearch version of returning a 'primary_key' was attribute.
35
+ # Elasticsearch uses the static +_id+ column as primary_key, but also supports an additional +id+ column.
36
+ # To provide functionality of returning the +id_Was+ attribute, this method must also support it.
37
+ def id_was
38
+ relay_id_attribute? && has_attribute?('id') ? attribute_was('id') : attribute_was(@primary_key)
39
+ end
40
+
41
+ # overwrite the write_attribute method to write 'id', if present
18
42
  # see @ ActiveRecord::AttributeMethods::Write#write_attribute
19
43
  def write_attribute(attr_name, value)
20
- return _write_attribute('id', value) if attr_name.to_s == 'id' && has_attribute?('id')
44
+ return _write_attribute('id', value) if attr_name.to_s == 'id' && relay_id_attribute? && has_attribute?('id')
21
45
 
22
46
  super
23
47
  end
24
48
 
25
- # overwrite read_attribute method to read 'id', if present?
49
+ # overwrite read_attribute method to read 'id', if present
26
50
  # see @ ActiveRecord::AttributeMethods::Read#read_attribute
27
51
  def read_attribute(attr_name, &block)
28
- return _read_attribute('id', &block) if attr_name.to_s == 'id' && has_attribute?('id')
52
+ return _read_attribute('id', &block) if attr_name.to_s == 'id' && relay_id_attribute? && has_attribute?('id')
29
53
 
30
54
  super
31
55
  end
32
56
 
57
+ module PrependClassMethods
58
+ # returns the table_name.
59
+ # Has to be prepended to provide automated compatibility to other gems.
60
+ def index_name
61
+ table_name
62
+ end
63
+ end
64
+
33
65
  module ClassMethods
66
+ prepend ElasticsearchRecord::Core::PrependClassMethods
67
+
34
68
  # used to create a cacheable statement.
35
69
  # This is a 1:1 copy, except that we use our own class +ElasticsearchRecord::StatementCache+
36
70
  # see @ ActiveRecord::Core::ClassMethods#cached_find_by_statement
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticsearchRecord
4
+ # Generic ElasticsearchRecord exception class.
5
+ class ElasticsearchRecordError < StandardError
6
+ end
7
+
8
+ class ResponseResultError < ElasticsearchRecordError
9
+ def initialize(expected, result)
10
+ super("expected response-result failed!\nreturned: '#{result}', but should be '#{expected}'")
11
+ end
12
+ end
13
+ end
@@ -8,10 +8,14 @@ module ElasticsearchRecord
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 1
11
- MINOR = 0
12
- TINY = 2
11
+ MINOR = 1
12
+ TINY = 0
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+
17
+ def self.to_s
18
+ STRING
19
+ end
16
20
  end
17
21
  end
@@ -5,7 +5,7 @@ module ElasticsearchRecord
5
5
  # attach to ElasticsearchRecord related events
6
6
  class LogSubscriber < ActiveSupport::LogSubscriber
7
7
 
8
- IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
8
+ IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN]
9
9
 
10
10
  def self.runtime=(value)
11
11
  Thread.current["elasticsearch_record_runtime"] = value
@@ -20,45 +20,63 @@ module ElasticsearchRecord
20
20
  rt
21
21
  end
22
22
 
23
- # Intercept `search.elasticsearch` events, and display them in the Rails log
24
- #
23
+ # Intercept `query.elasticsearch` events, and display them in the Rails log
25
24
  def query(event)
26
25
  self.class.runtime += event.duration
26
+
27
27
  return unless logger.debug?
28
28
 
29
29
  payload = event.payload
30
30
  return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
31
31
 
32
+ # build name from several payload data
32
33
  name = if payload[:async]
33
34
  "ASYNC #{payload[:name]} (#{payload[:lock_wait].round(1)}ms) (execution time #{event.duration.round(1)}ms)"
34
35
  else
35
36
  "#{payload[:name]} (#{event.duration.round(1)}ms)"
36
37
  end
37
- name = "CACHE #{name}" if payload[:cached]
38
+ name = "CACHE #{name}" if payload[:cached]
38
39
 
40
+ # nice feature: displays the REAL query-time (_qt)
39
41
  name = "#{name} (took: #{payload[:arguments][:_qt].round(1)}ms)" if payload[:arguments][:_qt]
40
42
 
41
- query = payload[:arguments].except(:index, :_qt).inspect.gsub(/:(\w+)=>/, '\1: ').presence || '-'
43
+ # build query
44
+ query = payload[:arguments].except(:_qt).inspect.gsub(/:(\w+)=>/, '\1: ').presence || '-'
42
45
 
43
46
  # final coloring
44
- name = color(name, CYAN, true)
45
- query = color(query, gate_color(payload[:gate]), true) if colorize_logging
47
+ name = color(name, name_color(payload[:name]), true)
48
+ query = color(query, gate_color(payload[:gate]), true) if colorize_logging
46
49
 
47
50
  debug " #{name} #{query}"
48
51
  end
49
52
 
50
53
  private
51
54
 
55
+ def name_color(name)
56
+ if name.blank? || name.match(/^[\p{Lu}\ ]+$/) # uppercase letters only : API, DROP, CREATE, ...
57
+ MAGENTA
58
+ else
59
+ CYAN
60
+ end
61
+ end
62
+
52
63
  def gate_color(gate)
53
64
  case gate
54
- when 'core.search', 'core.msearch'
65
+ # SELECTS
66
+ when 'core.get', 'core.mget', 'core.search', 'core.msearch', 'core.count', 'core.exists', 'sql.query'
55
67
  BLUE
68
+ # DELETES
56
69
  when 'core.delete', 'core.delete_by_query'
57
70
  RED
58
- when 'core.create'
71
+ # CREATES
72
+ when 'core.create', 'core.reindex'
59
73
  GREEN
74
+ # UPDATES
60
75
  when 'core.update', 'core.update_by_query'
61
76
  YELLOW
77
+ # MIXINS
78
+ when /indices\.\w+/, 'core.bulk', 'core.index'
79
+ WHITE
62
80
  else
63
81
  MAGENTA
64
82
  end
@@ -13,6 +13,11 @@ module ElasticsearchRecord
13
13
  index_base_name || super(model_name)
14
14
  end
15
15
 
16
+ # returns the configured +max_result_window+ (default: 10000)
17
+ def max_result_window
18
+ @max_result_window ||= connection.max_result_window(table_name)
19
+ end
20
+
16
21
  # returns an array with columns names, that are not virtual (and not a base structure).
17
22
  # so this is a array of real document (+_source+) attributes of the index.
18
23
  # @return [Array<String>]
@@ -6,17 +6,31 @@ module ElasticsearchRecord
6
6
 
7
7
  # insert a new record into the Elasticsearch index
8
8
  # NOTICE: We don't want to mess up with the Arel-builder - so we send new data directly to the API
9
+ # @param [ActiveModel::Attribute] values
10
+ # @return [Object] id
9
11
  def _insert_record(values)
10
- # values is not a "key=>values"-Hash, but a +ActiveModel::Attribute+ - so we need to resolve the casted values here
12
+ # values is not a "key=>values"-Hash, but a +ActiveModel::Attribute+ - so the casted values gets resolved here
11
13
  values = values.transform_values(&:value)
12
14
 
13
- # if a primary_key (+_id+) was provided, we need to extract this and allocate this to the arguments
14
- arguments = if (id = values.delete(self.primary_key)).present?
15
- { id: id }
15
+ update_auto_increment = false
16
+
17
+ # resolve possible provided primary_key value from values
18
+ arguments = if (id = values[self.primary_key]).present?
19
+ {id: id}
20
+ elsif self.columns_hash[self.primary_key]&.meta['auto_increment'] # BETA: should not be used on mass imports to the Elasticsearch-index
21
+ update_auto_increment = true
22
+ ids = [
23
+ connection.table_mappings(self.table_name).dig('properties', self.primary_key, 'meta', 'auto_increment').to_i + 1,
24
+ self.unscoped.all.maximum(self.primary_key).to_i + 1
25
+ ]
26
+ {id: ids.max}
16
27
  else
17
28
  {}
18
29
  end
19
30
 
31
+ # IMPORTANT: Always drop possible provided 'primary_key' column +_id+.
32
+ values.delete(self.primary_key)
33
+
20
34
  # build new query
21
35
  query = ElasticsearchRecord::Query.new(
22
36
  index: table_name,
@@ -25,17 +39,18 @@ module ElasticsearchRecord
25
39
  arguments: arguments,
26
40
  refresh: true)
27
41
 
28
- # execute query and build a RAW response
29
- response = connection.exec_query(query, "#{self} Create").response
42
+ # execute query and return inserted id
43
+ id = connection.insert(query, "#{self} Create")
30
44
 
31
- raise RecordNotSaved unless response['result'] == 'created'
45
+ if id.present? && update_auto_increment
46
+ connection.change_mapping_meta(table_name, self.primary_key, auto_increment: id)
47
+ end
32
48
 
33
- # return the new id
34
- response['_id']
49
+ id
35
50
  end
36
51
 
37
52
  # updates a persistent entry in the Elasticsearch index
38
- # NOTICE: We don't want to mess up with the Arel-builder - so we send new data directly to the API
53
+ # NOTICE: We don't want to mess up with the Arel-builder - so data is directly send to the API
39
54
  def _update_record(values, constraints)
40
55
  # values is not a "key=>values"-Hash, but a +ActiveModel::Attribute+ - so we need to resolve the casted values here
41
56
  values = values.transform_values(&:value)
@@ -45,20 +60,15 @@ module ElasticsearchRecord
45
60
  index: table_name,
46
61
  type: ElasticsearchRecord::Query::TYPE_UPDATE,
47
62
  body: { doc: values },
48
- arguments: { id: constraints['_id'] },
63
+ arguments: { id: constraints[self.primary_key] },
49
64
  refresh: true)
50
65
 
51
- # execute query and build a RAW response
52
- response = connection.exec_query(query, "#{self} Update").response
53
-
54
- raise RecordNotSaved unless response['result'] == 'updated'
55
-
56
- # return affected rows
57
- response['_shards']['total']
66
+ # execute query and return total updates
67
+ connection.update(query, "#{self} Update")
58
68
  end
59
69
 
60
70
  # removes a persistent entry from the Elasticsearch index
61
- # NOTICE: We don't want to mess up with the Arel-builder - so we send new data directly to the API
71
+ # NOTICE: We don't want to mess up with the Arel-builder - so data is directly send to the API
62
72
  def _delete_record(constraints)
63
73
  # build new query
64
74
  query = ElasticsearchRecord::Query.new(
@@ -67,13 +77,8 @@ module ElasticsearchRecord
67
77
  arguments: { id: constraints[self.primary_key] },
68
78
  refresh: true)
69
79
 
70
- # execute query and build a RAW response
71
- response = connection.exec_query(query, "#{self} Destroy").response
72
-
73
- raise RecordNotDestroyed unless response['result'] == 'deleted'
74
-
75
- # return affected rows
76
- response['_shards']['total']
80
+ # execute query and return total deletes
81
+ connection.delete(query, "#{self} Delete")
77
82
  end
78
83
  end
79
84
  end
@@ -4,33 +4,66 @@ module ElasticsearchRecord
4
4
  STATUS_VALID = :valid
5
5
  STATUS_FAILED = :failed
6
6
 
7
- # TYPE CONSTANTS
8
- TYPE_UNDEFINED = :undefined
9
- TYPE_COUNT = :count
10
- TYPE_SEARCH = :search
11
- TYPE_MSEARCH = :msearch
12
- TYPE_SQL = :sql
7
+ # -- UNDEFINED TYPE ------------------------------------------------------------------------------------------------
8
+ TYPE_UNDEFINED = :undefined
9
+
10
+ # -- QUERY TYPES ---------------------------------------------------------------------------------------------------
11
+ TYPE_COUNT = :count
12
+ TYPE_SEARCH = :search
13
+ TYPE_MSEARCH = :msearch
14
+ TYPE_SQL = :sql
15
+
16
+ # -- DOCUMENT TYPES ------------------------------------------------------------------------------------------------
13
17
  TYPE_CREATE = :create
14
18
  TYPE_UPDATE = :update
15
19
  TYPE_UPDATE_BY_QUERY = :update_by_query
16
20
  TYPE_DELETE = :delete
17
21
  TYPE_DELETE_BY_QUERY = :delete_by_query
18
22
 
23
+ # -- INDEX TYPES ---------------------------------------------------------------------------------------------------
24
+ TYPE_INDEX_CREATE = :index_create
25
+ # INDEX update is not implemented by Elasticsearch
26
+ # - this is handled through individual updates of +mappings+, +settings+ & +aliases+.
27
+ # INDEX delete is handled directly as API-call
28
+ TYPE_INDEX_UPDATE_MAPPING = :index_update_mapping
29
+ TYPE_INDEX_UPDATE_SETTING = :index_update_setting
30
+ TYPE_INDEX_UPDATE_ALIAS = :index_update_alias
31
+ TYPE_INDEX_DELETE_ALIAS = :index_delete_alias
32
+
19
33
  # includes valid types only
20
- TYPES = [TYPE_COUNT, TYPE_SEARCH, TYPE_MSEARCH, TYPE_SQL, TYPE_CREATE, TYPE_UPDATE, TYPE_UPDATE_BY_QUERY, TYPE_DELETE, TYPE_DELETE_BY_QUERY].freeze
34
+ TYPES = [
35
+ # -- QUERY TYPES
36
+ TYPE_COUNT, TYPE_SEARCH, TYPE_MSEARCH, TYPE_SQL,
37
+ # -- DOCUMENT TYPES
38
+ TYPE_CREATE, TYPE_UPDATE, TYPE_UPDATE_BY_QUERY, TYPE_DELETE, TYPE_DELETE_BY_QUERY,
39
+
40
+ # -- INDEX TYPES
41
+ TYPE_INDEX_CREATE, TYPE_INDEX_UPDATE_MAPPING, TYPE_INDEX_UPDATE_SETTING, TYPE_INDEX_UPDATE_ALIAS,
42
+ TYPE_INDEX_DELETE_ALIAS
43
+ ].freeze
21
44
 
22
45
  # includes reading types only
23
- READ_TYPES = [TYPE_COUNT, TYPE_SEARCH, TYPE_MSEARCH, TYPE_SQL].freeze
46
+ READ_TYPES = [
47
+ TYPE_COUNT, TYPE_SEARCH, TYPE_MSEARCH, TYPE_SQL
48
+ ].freeze
24
49
 
25
- # defines a query to be executed if the query fails - +(none)+ queries
50
+ # defines a body to be executed if the query fails - +(none)+
26
51
  # acts like the SQL-query "where('1=0')"
27
- FAILED_SEARCH_BODY = { size: 0, query: { bool: { filter: [{ term: { _id: '_' } }] } } }.freeze
52
+ FAILED_BODIES = {
53
+ TYPE_SEARCH => { size: 0, query: { bool: { filter: [{ term: { _id: '_' } }] } } },
54
+ TYPE_COUNT => { query: { bool: { filter: [{ term: { _id: '_' } }] } } }
55
+ }.freeze
28
56
 
29
57
  # defines special api gates to be used per type.
30
- # if not defined it simply uses +[:core,self.type]+
58
+ # if no special type is defined, it simply uses +[:core,self.type]+
31
59
  GATES = {
32
- TYPE_SQL => [:sql, :query]
33
- }
60
+ TYPE_SQL => [:sql, :query],
61
+ TYPE_INDEX_CREATE => [:indices, :create],
62
+ TYPE_INDEX_UPDATE_MAPPING => [:indices, :put_mapping],
63
+ TYPE_INDEX_UPDATE_SETTING => [:indices, :put_settings],
64
+ TYPE_INDEX_UPDATE_ALIAS => [:indices, :put_alias],
65
+ TYPE_INDEX_DELETE_ALIAS => [:indices, :delete_alias],
66
+ }.freeze
34
67
 
35
68
  # defines the index the query should be executed on
36
69
  # @!attribute String
@@ -52,7 +85,7 @@ module ElasticsearchRecord
52
85
 
53
86
  # defines the query body - in most cases this is a hash
54
87
  # @!attribute Hash
55
- attr_reader :body
88
+ # attr_reader :body
56
89
 
57
90
  # defines the query arguments to be passed to the API
58
91
  # @!attribute Hash
@@ -102,14 +135,20 @@ module ElasticsearchRecord
102
135
  !READ_TYPES.include?(self.type)
103
136
  end
104
137
 
138
+ # returns the query body - depends on the +status+!
139
+ # failed queried will return the related +FAILED_BODIES+ or +{}+ as fallback
140
+ # @return [Hash, nil]
141
+ def body
142
+ return (FAILED_BODIES[self.type].presence || {}) if self.status == STATUS_FAILED
143
+
144
+ @body
145
+ end
146
+
105
147
  # builds the final query arguments.
106
148
  # Depends on the query status, index, body & refresh attributes.
107
149
  # Also used possible PRE-defined arguments to be merged with those mentioned attributes.
108
150
  # @return [Hash]
109
151
  def query_arguments
110
- # check for failed status
111
- return { index: self.index, body: FAILED_SEARCH_BODY } if self.status == STATUS_FAILED
112
-
113
152
  args = @arguments.deep_dup
114
153
 
115
154
  # set index, if present
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ElasticsearchRecord
2
4
  module Querying
3
5
  extend ActiveSupport::Concern
@@ -88,6 +90,21 @@ module ElasticsearchRecord
88
90
  connection.exec_query(query, "#{name} Msearch", async: async)
89
91
  end
90
92
 
93
+ # executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded
94
+ def search(*args, &block)
95
+ begin
96
+ # require the Elasticsearch::DSL gem, if loaded
97
+ require 'elasticsearch/dsl'
98
+ query = ::Elasticsearch::DSL::Search::Search.new(*args, &block).to_hash
99
+ rescue LoadError
100
+ query = args.extract_options!
101
+ rescue
102
+ query = args.extract_options!
103
+ end
104
+
105
+ find_by_query(query)
106
+ end
107
+
91
108
  # execute query by msearch
92
109
  def _query_by_msearch(queries, async: false)
93
110
  connection.select_multiple(queries, "#{name} Msearch", async: async)
@@ -12,6 +12,9 @@ module ElasticsearchRecord
12
12
  # fallback to default
13
13
  return super() if block_given?
14
14
 
15
+ # check for already failed query
16
+ return 0 if null_relation?
17
+
15
18
  # reset column_name, if +:all+ was provided ...
16
19
  column_name = nil if column_name == :all
17
20
 
@@ -29,28 +29,68 @@ module ElasticsearchRecord
29
29
  to_sql.query_arguments
30
30
  end
31
31
 
32
- # executes the elasticsearch msearch on the related klass
32
+ # Allows to execute several search operations in one request.
33
+ # executes the elasticsearch +msearch+ on the related class-index.
34
+ #
35
+ # A optionally array of items will be each yielded with a spawn(of the current relation) and item.
36
+ # Each yield-return will resolve its +arel+ which will then transform to multiple queries and send in a single request.
37
+ #
38
+ # Responses can be refined by providing a +resolve+ option, to resolve specific results from each +ElasticsearchRecord::Result+
39
+ # (e.g. used to resolve 'aggregations, buckets, ...')
40
+ #
41
+ # As a default the method returns an array of (resolved) responses in the order of the provided +values+-array.
42
+ # This can be transformed into a hash of keys (provided items) and values (responses) by providing the +transpose+ flag.
43
+ #
44
+ # WARNING: if the current relation is a +NullRelation+ (**#none** was assigned), the method directly returns nil!
33
45
  #
34
46
  # @example
35
- # msearch([2020, 2019, 2018]).each{ |q, year| q.where!(year: year) }
47
+ # # msearch on the current relation
48
+ # msearch
49
+ # # > [ElasticsearchRecord::Result]
36
50
  #
37
- # @param [Array] values - values to be yielded
38
- # @param [nil, Symbol] response_type - optional type of search response (:took, :total , :hits , :aggregations , :length , :results, :each)
39
- def msearch(values = nil, response_type = nil)
40
- if values.nil?
41
- arels = [arel]
42
- else
43
- arels = values.map { |value|
44
- # spawn a new relation and return the arel-object
45
- yield(spawn, value).arel
46
- }
47
- end
51
+ # @example
52
+ # # msearch with provided items
53
+ # msearch([2020, 2019, 2018]).each{ |query, year| query.where!(year: year) }
54
+ # # > [ElasticsearchRecord::Result, ElasticsearchRecord::Result, ElasticsearchRecord::Result]
55
+ #
56
+ # @example
57
+ # # msearch with refining options
58
+ # msearch([2020, 2019, 2018], resolve: :aggregations, transpose: true).each{ |query, year| query.where!(year: year).aggregate!(:total, { sum: { field: :count }}) }
59
+ # # > {2020 => {...aggs...}, 2019 => {...aggs...}, 2018 => {...aggs...}}
60
+ #
61
+ # @example
62
+ # # msearch with none (NullRelation)
63
+ # scope = spawn.none
64
+ # scope.msearch([2020, 2019, 2018]).each{ |query, year| ... }
65
+ # # > nil
66
+ #
67
+ # @param [nil, Array] items - items to be yielded and used to provide individual queries (yields: |spawn, value| )
68
+ # @param [Hash] opts - additional options to refine the results
69
+ # @option opts[Symbol] :resolve - optionally resolve specific results from each result (:took, :total , :hits , :aggregations , :length , :results, :each)
70
+ # @option opts[Boolean] :transpose - transposes the provided values & results as Hash (default: false)
71
+ # @option opts[Boolean] :keep_null_relation - by provided true-value, a NullRelation will not be ignored - so items will be yielded & query will be executed (default: false)
72
+ # @return [Array, Hash, nil]
73
+ def msearch(items = nil, opts = {})
74
+ # prevent query on +NullRelation+!
75
+ return nil if null_relation? && !opts[:keep_null_relation]
76
+
77
+ # check if values are provided, if not we use the arel from the current relation-scope
78
+ arels = if items.nil?
79
+ [arel]
80
+ else
81
+ # spawn a new relation to the block and maps each arel-object
82
+ items.map { |value| yield(spawn, value).arel }
83
+ end
48
84
 
49
- # returns a response object with multiple single responses
50
- responses = klass._query_by_msearch(arels)
85
+ # check provided resolve method
86
+ responses = if opts[:resolve]
87
+ klass._query_by_msearch(arels).map(&opts[:resolve].to_sym)
88
+ else
89
+ klass._query_by_msearch(arels)
90
+ end
51
91
 
52
- if response_type
53
- responses.map(&response_type.to_sym)
92
+ if opts[:transpose]
93
+ [items, responses].transpose.to_h
54
94
  else
55
95
  responses
56
96
  end