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.
- 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
|