elasticsearch_record 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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