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.
- checksums.yaml +4 -4
- data/.yardopts +4 -0
- data/Gemfile.lock +10 -14
- data/README.md +180 -27
- data/docs/CHANGELOG.md +36 -18
- data/docs/LICENSE.txt +1 -1
- data/elasticsearch_record.gemspec +42 -0
- data/lib/active_record/connection_adapters/elasticsearch/column.rb +20 -6
- data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +142 -125
- data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +2 -23
- data/lib/active_record/connection_adapters/elasticsearch/schema_creation.rb +30 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/attribute_methods.rb +103 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/column_methods.rb +42 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/create_table_definition.rb +158 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_alias_definition.rb +32 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_definition.rb +132 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_mapping_definition.rb +110 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/table_setting_definition.rb +136 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions/update_table_definition.rb +174 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_definitions.rb +37 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_dumper.rb +110 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +398 -174
- data/lib/active_record/connection_adapters/elasticsearch/table_statements.rb +232 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +2 -0
- data/lib/active_record/connection_adapters/elasticsearch/unsupported_implementation.rb +32 -0
- data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +112 -19
- data/lib/arel/collectors/elasticsearch_query.rb +0 -1
- data/lib/arel/visitors/elasticsearch.rb +7 -579
- data/lib/arel/visitors/elasticsearch_base.rb +234 -0
- data/lib/arel/visitors/elasticsearch_query.rb +463 -0
- data/lib/arel/visitors/elasticsearch_schema.rb +124 -0
- data/lib/elasticsearch_record/core.rb +44 -10
- data/lib/elasticsearch_record/errors.rb +13 -0
- data/lib/elasticsearch_record/gem_version.rb +6 -2
- data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +27 -9
- data/lib/elasticsearch_record/model_schema.rb +5 -0
- data/lib/elasticsearch_record/persistence.rb +31 -26
- data/lib/elasticsearch_record/query.rb +56 -17
- data/lib/elasticsearch_record/querying.rb +17 -0
- data/lib/elasticsearch_record/relation/calculation_methods.rb +3 -0
- data/lib/elasticsearch_record/relation/core_methods.rb +57 -17
- data/lib/elasticsearch_record/relation/query_clause_tree.rb +38 -1
- data/lib/elasticsearch_record/relation/query_methods.rb +6 -0
- data/lib/elasticsearch_record/relation/result_methods.rb +15 -9
- data/lib/elasticsearch_record/result.rb +32 -5
- data/lib/elasticsearch_record/statement_cache.rb +2 -1
- data/lib/elasticsearch_record.rb +2 -2
- metadata +29 -11
- data/.ruby-version +0 -1
- 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
|
-
|
6
|
-
|
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
|
-
|
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
|
-
#
|
12
|
-
#
|
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
|
-
|
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
|
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
|
@@ -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 = [
|
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 `
|
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
|
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
|
-
|
43
|
+
# build query
|
44
|
+
query = payload[:arguments].except(:_qt).inspect.gsub(/:(\w+)=>/, '\1: ').presence || '-'
|
42
45
|
|
43
46
|
# final coloring
|
44
|
-
name
|
45
|
-
query
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
29
|
-
|
42
|
+
# execute query and return inserted id
|
43
|
+
id = connection.insert(query, "#{self} Create")
|
30
44
|
|
31
|
-
|
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
|
-
|
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
|
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[
|
63
|
+
arguments: { id: constraints[self.primary_key] },
|
49
64
|
refresh: true)
|
50
65
|
|
51
|
-
# execute query and
|
52
|
-
|
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
|
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
|
71
|
-
|
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
|
8
|
-
TYPE_UNDEFINED
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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 = [
|
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 = [
|
46
|
+
READ_TYPES = [
|
47
|
+
TYPE_COUNT, TYPE_SEARCH, TYPE_MSEARCH, TYPE_SQL
|
48
|
+
].freeze
|
24
49
|
|
25
|
-
# defines a
|
50
|
+
# defines a body to be executed if the query fails - +(none)+
|
26
51
|
# acts like the SQL-query "where('1=0')"
|
27
|
-
|
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
|
58
|
+
# if no special type is defined, it simply uses +[:core,self.type]+
|
31
59
|
GATES = {
|
32
|
-
TYPE_SQL
|
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)
|
@@ -29,28 +29,68 @@ module ElasticsearchRecord
|
|
29
29
|
to_sql.query_arguments
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
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
|
47
|
+
# # msearch on the current relation
|
48
|
+
# msearch
|
49
|
+
# # > [ElasticsearchRecord::Result]
|
36
50
|
#
|
37
|
-
# @
|
38
|
-
#
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
#
|
50
|
-
responses =
|
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
|
53
|
-
responses.
|
92
|
+
if opts[:transpose]
|
93
|
+
[items, responses].transpose.to_h
|
54
94
|
else
|
55
95
|
responses
|
56
96
|
end
|