elasticsearch_record 1.0.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 +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +74 -0
- data/README.md +216 -0
- data/Rakefile +8 -0
- data/docs/CHANGELOG.md +44 -0
- data/docs/CODE_OF_CONDUCT.md +84 -0
- data/docs/LICENSE.txt +21 -0
- data/lib/active_record/connection_adapters/elasticsearch/column.rb +32 -0
- data/lib/active_record/connection_adapters/elasticsearch/database_statements.rb +149 -0
- data/lib/active_record/connection_adapters/elasticsearch/quoting.rb +38 -0
- data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +134 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/format_string.rb +28 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb +52 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/object.rb +44 -0
- data/lib/active_record/connection_adapters/elasticsearch/type/range.rb +42 -0
- data/lib/active_record/connection_adapters/elasticsearch/type.rb +16 -0
- data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +197 -0
- data/lib/arel/collectors/elasticsearch_query.rb +112 -0
- data/lib/arel/nodes/select_agg.rb +22 -0
- data/lib/arel/nodes/select_configure.rb +9 -0
- data/lib/arel/nodes/select_kind.rb +9 -0
- data/lib/arel/nodes/select_query.rb +20 -0
- data/lib/arel/visitors/elasticsearch.rb +589 -0
- data/lib/elasticsearch_record/base.rb +14 -0
- data/lib/elasticsearch_record/core.rb +59 -0
- data/lib/elasticsearch_record/extensions/relation.rb +15 -0
- data/lib/elasticsearch_record/gem_version.rb +17 -0
- data/lib/elasticsearch_record/instrumentation/controller_runtime.rb +39 -0
- data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +70 -0
- data/lib/elasticsearch_record/instrumentation/railtie.rb +16 -0
- data/lib/elasticsearch_record/instrumentation.rb +17 -0
- data/lib/elasticsearch_record/model_schema.rb +43 -0
- data/lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb +85 -0
- data/lib/elasticsearch_record/patches/arel/select_core_patch.rb +64 -0
- data/lib/elasticsearch_record/patches/arel/select_manager_patch.rb +91 -0
- data/lib/elasticsearch_record/patches/arel/select_statement_patch.rb +41 -0
- data/lib/elasticsearch_record/patches/arel/update_manager_patch.rb +46 -0
- data/lib/elasticsearch_record/patches/arel/update_statement_patch.rb +60 -0
- data/lib/elasticsearch_record/persistence.rb +80 -0
- data/lib/elasticsearch_record/query.rb +129 -0
- data/lib/elasticsearch_record/querying.rb +90 -0
- data/lib/elasticsearch_record/relation/calculation_methods.rb +155 -0
- data/lib/elasticsearch_record/relation/core_methods.rb +64 -0
- data/lib/elasticsearch_record/relation/query_clause.rb +43 -0
- data/lib/elasticsearch_record/relation/query_clause_tree.rb +94 -0
- data/lib/elasticsearch_record/relation/query_methods.rb +276 -0
- data/lib/elasticsearch_record/relation/result_methods.rb +222 -0
- data/lib/elasticsearch_record/relation/value_methods.rb +54 -0
- data/lib/elasticsearch_record/result.rb +236 -0
- data/lib/elasticsearch_record/statement_cache.rb +87 -0
- data/lib/elasticsearch_record/version.rb +10 -0
- data/lib/elasticsearch_record.rb +60 -0
- data/sig/elasticsearch_record.rbs +4 -0
- metadata +175 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Elasticsearch
|
6
|
+
module Quoting # :nodoc:
|
7
|
+
# Quotes the column value to help prevent
|
8
|
+
def quote(value)
|
9
|
+
case value
|
10
|
+
# those values do not need to be quoted
|
11
|
+
when BigDecimal, Numeric, String, Symbol, nil, true, false then value
|
12
|
+
when ActiveSupport::Duration then value.to_i
|
13
|
+
when Array then value.map { |val| quote(val) }
|
14
|
+
when Hash then value.transform_values { |val| quote(val) }
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def quoted_true
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def unquoted_true
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def quoted_false
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def unquoted_false
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module Elasticsearch
|
6
|
+
module SchemaStatements # :nodoc:
|
7
|
+
# Returns the relation names usable to back Active Record models.
|
8
|
+
# For Elasticsearch this means all indices - which also includes system +dot+ '.' indices.
|
9
|
+
# @see ActiveRecord::ConnectionAdapters::SchemaStatements#data_sources
|
10
|
+
# @return [Array<String>]
|
11
|
+
def data_sources
|
12
|
+
api(:indices, :get_settings, { index: :_all }, 'SCHEMA').keys
|
13
|
+
end
|
14
|
+
|
15
|
+
# returns a hash of all mappings by provided index_name
|
16
|
+
# @param [String] index_name
|
17
|
+
# @return [Hash]
|
18
|
+
def mappings(index_name)
|
19
|
+
api(:indices, :get_mapping, { index: index_name }, 'SCHEMA').dig(index_name, 'mappings')
|
20
|
+
end
|
21
|
+
|
22
|
+
# returns a hash of all settings by provided index_name
|
23
|
+
# @param [String] index_name
|
24
|
+
# @return [Hash]
|
25
|
+
def settings(index_name)
|
26
|
+
api(:indices, :get_settings, { index: index_name }, 'SCHEMA').dig(index_name, 'settings','index')
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the list of a table's column names, data types, and default values.
|
30
|
+
# @see ActiveRecord::ConnectionAdapters::SchemaStatements#columns
|
31
|
+
# @see ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#column_definitions
|
32
|
+
# @param [String] table_name
|
33
|
+
# @return [Array<Hash>]
|
34
|
+
def column_definitions(table_name)
|
35
|
+
structure = mappings(table_name)
|
36
|
+
raise(ActiveRecord::StatementInvalid, "Could not find elasticsearch index '#{table_name}'") if structure.blank? || structure['properties'].blank?
|
37
|
+
|
38
|
+
# since the received mappings do not have the "primary" +_id+-column we manually need to add this here
|
39
|
+
# The BASE_STRUCTURE will also include some meta keys like '_score', '_type', ...
|
40
|
+
ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::BASE_STRUCTURE + structure['properties'].map { |key, prop|
|
41
|
+
# mappings can have +fields+ - we also want them for 'query-conditions'
|
42
|
+
# that can be resolved through +.column_names+
|
43
|
+
fields = prop.delete('fields') || []
|
44
|
+
|
45
|
+
# we need to merge the name & possible nested fields (which are also searchable)
|
46
|
+
prop.merge('name' => key, 'fields' => fields.map { |fkey, _field| "#{key}.#{fkey}" })
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
# creates a new column object from provided field Hash
|
51
|
+
# @see ActiveRecord::ConnectionAdapters::SchemaStatements#columns
|
52
|
+
# @see ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#new_column_from_field
|
53
|
+
# @param [String] _table_name
|
54
|
+
# @param [Hash] field
|
55
|
+
# @return [ActiveRecord::ConnectionAdapters::Column]
|
56
|
+
def new_column_from_field(_table_name, field)
|
57
|
+
# fallback for possible empty type
|
58
|
+
field_type = field['type'].presence || (field['properties'].present? ? 'nested' : 'object')
|
59
|
+
|
60
|
+
ActiveRecord::ConnectionAdapters::Elasticsearch::Column.new(
|
61
|
+
field["name"],
|
62
|
+
field["null_value"],
|
63
|
+
fetch_type_metadata(field_type),
|
64
|
+
field['null'].nil? ? true : field['null'],
|
65
|
+
nil,
|
66
|
+
comment: field['meta'] ? field['meta'].map { |k, v| "#{k}: #{v}" }.join(' | ') : nil,
|
67
|
+
virtual: field['virtual'],
|
68
|
+
fields: field['fields']
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
# lookups from building the @columns_hash.
|
73
|
+
# since Elasticsearch has the "feature" to provide multicast values on any type, we need to fetch them ...
|
74
|
+
# you know, ES can return an integer or an array of integers for any column ...
|
75
|
+
# @param [ActiveRecord::ConnectionAdapters::Elasticsearch::Column] column
|
76
|
+
# @return [ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue]
|
77
|
+
def lookup_cast_type_from_column(column)
|
78
|
+
type_map.lookup(:multicast_value, super)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a array of tables primary keys.
|
82
|
+
# PLEASE NOTE: Elasticsearch does not have a concept of primary key.
|
83
|
+
# The only thing that uniquely identifies a document is the index together with the +_id+.
|
84
|
+
# To not break the "ConnectionAdapters" concept we simulate this through the BASE_STRUCTURE.
|
85
|
+
# We know, we can just return '_id' here ...
|
86
|
+
# @see ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#primary_keys
|
87
|
+
# @param [String] _table_name
|
88
|
+
def primary_keys(_table_name)
|
89
|
+
ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::BASE_STRUCTURE
|
90
|
+
.select { |f| f["primary"] }
|
91
|
+
.map { |f| f["name"] }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Checks to see if the data source +name+ exists on the database.
|
95
|
+
#
|
96
|
+
# data_source_exists?(:ebooks)
|
97
|
+
# @see ActiveRecord::ConnectionAdapters::SchemaStatements#data_source_exists?
|
98
|
+
# @param [String, Symbol] name
|
99
|
+
# @return [Boolean]
|
100
|
+
def data_source_exists?(name)
|
101
|
+
# response returns boolean
|
102
|
+
api(:indices, :exists?, { index: name }, 'SCHEMA')
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns an array of table names defined in the database.
|
106
|
+
# For Elasticsearch this means all normal indices (no system +dot+ '.' indices)
|
107
|
+
# @see ActiveRecord::ConnectionAdapters::SchemaStatements#tables
|
108
|
+
# @return [Array<String>]
|
109
|
+
def tables
|
110
|
+
data_sources.reject { |key| key[0] == '.' }
|
111
|
+
end
|
112
|
+
|
113
|
+
# Checks to see if the table +table_name+ exists on the database.
|
114
|
+
#
|
115
|
+
# table_exists?(:developers)
|
116
|
+
#
|
117
|
+
# @see ActiveRecord::ConnectionAdapters::SchemaStatements#table_exists?
|
118
|
+
# @param [String, Symbol] table_name
|
119
|
+
# @return [Boolean]
|
120
|
+
def table_exists?(table_name)
|
121
|
+
# just reference to the data sources
|
122
|
+
data_source_exists?(table_name)
|
123
|
+
end
|
124
|
+
|
125
|
+
# returns the maximum allowed size for queries.
|
126
|
+
# The query will raise an ActiveRecord::StatementInvalid if the requested limit is above this value.
|
127
|
+
# @return [Integer]
|
128
|
+
def max_result_window
|
129
|
+
10000
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Elasticsearch
|
4
|
+
module Type # :nodoc:
|
5
|
+
class FormatString < ActiveRecord::Type::String
|
6
|
+
attr_reader :format
|
7
|
+
|
8
|
+
def initialize(**args)
|
9
|
+
@format = args.delete(:format).presence || /.*/
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def type
|
14
|
+
:format_string
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def cast_value(value)
|
20
|
+
return value unless ::String === value
|
21
|
+
return '' unless value.match(format)
|
22
|
+
value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Elasticsearch
|
4
|
+
module Type # :nodoc:
|
5
|
+
class MulticastValue < ActiveRecord::Type::Value
|
6
|
+
|
7
|
+
attr_reader :nested_type
|
8
|
+
|
9
|
+
def initialize(nested_type: nil, **)
|
10
|
+
@nested_type = nested_type || ActiveRecord::Type::Value.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def type
|
14
|
+
nested_type.type
|
15
|
+
end
|
16
|
+
|
17
|
+
# overwrites the default deserialize behaviour
|
18
|
+
# @param [Object] value
|
19
|
+
# @return [Object,nil] deserialized object
|
20
|
+
def deserialize(value)
|
21
|
+
cast(_deserialize(value))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def _deserialize(value)
|
27
|
+
# check for the special +object+ type which is forced to be casted
|
28
|
+
return _deserialize_by_nested_type(value) if nested_type.type == :object && nested_type.forced?
|
29
|
+
|
30
|
+
if value.is_a?(Array)
|
31
|
+
value.map { |val| _deserialize_by_nested_type(val) }
|
32
|
+
elsif value.is_a?(Hash)
|
33
|
+
value.reduce({}) { |m, (key, val)|
|
34
|
+
m[key] = _deserialize_by_nested_type(val)
|
35
|
+
m
|
36
|
+
}
|
37
|
+
else
|
38
|
+
_deserialize_by_nested_type(value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# in some cases we cannot deserialize, since the ES-type don't match well with the provided value
|
43
|
+
# but the result should be ok, as it is (e.g. 'Hash')...
|
44
|
+
# so we rescue here with just the provided value
|
45
|
+
def _deserialize_by_nested_type(value)
|
46
|
+
nested_type.deserialize(value) rescue value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Elasticsearch
|
4
|
+
module Type # :nodoc:
|
5
|
+
class Object < ActiveRecord::Type::Value
|
6
|
+
attr_reader :cast_type, :default
|
7
|
+
|
8
|
+
# creates a new object type which can be natively every value.
|
9
|
+
# Providing a +cast+ will call the value with this method (or callback)
|
10
|
+
# @param [nil, Symbol, String, Proc] cast - the cast type
|
11
|
+
# @param [nil, Object] default
|
12
|
+
# @param [Boolean] force - force value to be casted (used by the MulticastValue type) - (default: false)
|
13
|
+
def initialize(cast: nil, default: nil, force: false, **)
|
14
|
+
@cast_type = cast
|
15
|
+
@default = default
|
16
|
+
@force = force
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
:object
|
21
|
+
end
|
22
|
+
|
23
|
+
def forced?
|
24
|
+
@force
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# cast value by provided cast_method
|
30
|
+
def cast_value(value)
|
31
|
+
case self.cast_type
|
32
|
+
when Symbol, String
|
33
|
+
value.public_send(self.cast_type) rescue default
|
34
|
+
when Proc
|
35
|
+
self.cast_type.(value) rescue default
|
36
|
+
else
|
37
|
+
value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Elasticsearch
|
4
|
+
module Type # :nodoc:
|
5
|
+
class Range < MulticastValue
|
6
|
+
|
7
|
+
def type
|
8
|
+
"range_#{nested_type.type}".to_sym
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def cast_value(value)
|
14
|
+
return (0..0) unless value.is_a?(Hash)
|
15
|
+
# check for existing gte & lte
|
16
|
+
|
17
|
+
min_value = if value['gte']
|
18
|
+
value['gte']
|
19
|
+
elsif value['gt']
|
20
|
+
value['gt'] + 1
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
max_value = if value['lte']
|
26
|
+
value['lte']
|
27
|
+
elsif value['lt']
|
28
|
+
value['lt'] - 1
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
return (0..0) if min_value.nil? || max_value.nil?
|
34
|
+
|
35
|
+
# build & return range
|
36
|
+
(min_value..max_value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters/elasticsearch/type/format_string'
|
4
|
+
require 'active_record/connection_adapters/elasticsearch/type/multicast_value'
|
5
|
+
require 'active_record/connection_adapters/elasticsearch/type/object'
|
6
|
+
require 'active_record/connection_adapters/elasticsearch/type/range'
|
7
|
+
|
8
|
+
module ActiveRecord
|
9
|
+
module ConnectionAdapters
|
10
|
+
module Elasticsearch
|
11
|
+
module Type # :nodoc:
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters'
|
4
|
+
|
5
|
+
# new
|
6
|
+
require 'active_record/connection_adapters/elasticsearch/column'
|
7
|
+
require 'active_record/connection_adapters/elasticsearch/database_statements'
|
8
|
+
require 'active_record/connection_adapters/elasticsearch/quoting'
|
9
|
+
require 'active_record/connection_adapters/elasticsearch/schema_statements'
|
10
|
+
require 'active_record/connection_adapters/elasticsearch/type'
|
11
|
+
|
12
|
+
require 'arel/visitors/elasticsearch'
|
13
|
+
require 'arel/collectors/elasticsearch_query'
|
14
|
+
|
15
|
+
gem 'elasticsearch'
|
16
|
+
require 'elasticsearch'
|
17
|
+
|
18
|
+
module ActiveRecord # :nodoc:
|
19
|
+
module ConnectionHandling # :nodoc:
|
20
|
+
def elasticsearch_connection(config)
|
21
|
+
config = config.symbolize_keys
|
22
|
+
|
23
|
+
# move 'host' to 'hosts'
|
24
|
+
config[:hosts] = config.delete(:host) if config[:host]
|
25
|
+
|
26
|
+
# enable logging (Rails.logger)
|
27
|
+
config[:logger] = logger if config.delete(:log)
|
28
|
+
|
29
|
+
ConnectionAdapters::ElasticsearchAdapter.new(
|
30
|
+
ConnectionAdapters::ElasticsearchAdapter.new_client(config),
|
31
|
+
logger,
|
32
|
+
config
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module ConnectionAdapters # :nodoc:
|
38
|
+
class ElasticsearchAdapter < AbstractAdapter
|
39
|
+
ADAPTER_NAME = "Elasticsearch"
|
40
|
+
|
41
|
+
# defines the Elasticsearch 'base' structure, which is always included but cannot be resolved through mappings ...
|
42
|
+
BASE_STRUCTURE = [
|
43
|
+
{ 'name' => '_id', 'type' => 'string', 'null' => false, 'primary' => true },
|
44
|
+
{ 'name' => '_index', 'type' => 'string', 'null' => false, 'virtual' => true },
|
45
|
+
{ 'name' => '_score', 'type' => 'float', 'null' => false, 'virtual' => true },
|
46
|
+
{ 'name' => '_type', 'type' => 'string', 'null' => false, 'virtual' => true }
|
47
|
+
].freeze
|
48
|
+
|
49
|
+
include Elasticsearch::Quoting
|
50
|
+
include Elasticsearch::SchemaStatements
|
51
|
+
include Elasticsearch::DatabaseStatements
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def base_structure_keys
|
55
|
+
@base_structure_keys ||= BASE_STRUCTURE.map { |struct| struct['name'] }.freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
def new_client(config)
|
59
|
+
# IMPORTANT: remove +adapter+ from config - otherwise we mess up with Faraday::AdapterRegistry
|
60
|
+
client = ::Elasticsearch::Client.new(config.except(:adapter))
|
61
|
+
client.ping
|
62
|
+
client
|
63
|
+
rescue ::Elastic::Transport::Transport::ServerError => error
|
64
|
+
raise ::ActiveRecord::ConnectionNotEstablished, error.message
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def initialize_type_map(m)
|
70
|
+
m.register_type 'binary', Type::Binary.new
|
71
|
+
m.register_type 'boolean', Type::Boolean.new
|
72
|
+
m.register_type 'keyword', Type::String.new
|
73
|
+
|
74
|
+
m.alias_type 'constant_keyword', 'keyword'
|
75
|
+
m.alias_type 'wildcard', 'keyword'
|
76
|
+
|
77
|
+
# maybe use integer 8 here ...
|
78
|
+
m.register_type 'long', Type::BigInteger.new
|
79
|
+
m.register_type 'integer', Type::Integer.new
|
80
|
+
m.register_type 'short', Type::Integer.new(limit: 2)
|
81
|
+
m.register_type 'byte', Type::Integer.new(limit: 1)
|
82
|
+
m.register_type 'double', Type::Float.new(limit: 8)
|
83
|
+
m.register_type 'float', Type::Float.new(limit: 4)
|
84
|
+
m.register_type 'half_float', Type::Float.new(limit: 2)
|
85
|
+
m.register_type 'scaled_float', Type::Float.new(limit: 8, scale: 8)
|
86
|
+
m.register_type 'unsigned_long', Type::UnsignedInteger.new
|
87
|
+
|
88
|
+
m.register_type 'date', Type::DateTime.new
|
89
|
+
|
90
|
+
# force a hash
|
91
|
+
m.register_type 'object', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object.new(cast: :to_h, force: true)
|
92
|
+
m.alias_type 'flattened', "object"
|
93
|
+
|
94
|
+
# array of objects
|
95
|
+
m.register_type 'nested', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object.new(cast: :to_h)
|
96
|
+
|
97
|
+
ip_type = ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString.new(format: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)
|
98
|
+
|
99
|
+
m.register_type 'integer_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Integer.new)
|
100
|
+
m.register_type 'float_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Float.new(limit: 4))
|
101
|
+
m.register_type 'long_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Integer.new(limit: 8))
|
102
|
+
m.register_type 'double_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::Float.new(limit: 8))
|
103
|
+
m.register_type 'date_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: Type::DateTime.new)
|
104
|
+
m.register_type 'ip_range', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range.new(nested_type: ip_type)
|
105
|
+
|
106
|
+
m.register_type 'ip', ip_type
|
107
|
+
m.register_type 'version', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString.new(format: /^\d+\.\d+\.\d+[\-\+A-Za-z\.]*$/)
|
108
|
+
# m.register_type 'murmur3', Murmur3.new
|
109
|
+
|
110
|
+
m.register_type 'text', Type::Text.new
|
111
|
+
|
112
|
+
# this special Type is required to parse a ES-value into the +nested_type+, array or hash.
|
113
|
+
# For arrays & hashes it tries to cast the values with the provided +nested_type+
|
114
|
+
# but falls back to provided value if cast fails.
|
115
|
+
# This type cannot be accessed through the mapping and is only called @ #lookup_cast_type_from_column
|
116
|
+
# @see ActiveRecord::ConnectionAdapters::Elasticsearch::SchemaStatements#lookup_cast_type_from_column
|
117
|
+
m.register_type :multicast_value do |_type, nested_type|
|
118
|
+
ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue.new(nested_type: nested_type)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# reinitialize the constant with new types
|
124
|
+
TYPE_MAP = ActiveRecord::Type::HashLookupTypeMap.new.tap { |m| initialize_type_map(m) }
|
125
|
+
|
126
|
+
def initialize(*args)
|
127
|
+
super(*args)
|
128
|
+
|
129
|
+
# prepared statements are not supported by Elasticsearch.
|
130
|
+
# documentation for mysql prepares statements @ https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
|
131
|
+
@prepared_statements = false
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def type_map
|
137
|
+
TYPE_MAP
|
138
|
+
end
|
139
|
+
|
140
|
+
# catch Elasticsearch Transport-errors to be treated as +StatementInvalid+ (the original message is still readable ...)
|
141
|
+
def translate_exception(exception, message:, sql:, binds:)
|
142
|
+
case exception
|
143
|
+
when Elastic::Transport::Transport::ServerError
|
144
|
+
::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
|
145
|
+
else
|
146
|
+
# just forward the exception ...
|
147
|
+
exception
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# provide a custom log instrumenter for elasticsearch subscribers
|
152
|
+
def log(gate, arguments, name, async: false, &block)
|
153
|
+
@instrumenter.instrument(
|
154
|
+
"query.elasticsearch_record",
|
155
|
+
gate: gate,
|
156
|
+
name: name,
|
157
|
+
arguments: gate == 'core.msearch' ? arguments.deep_dup : arguments,
|
158
|
+
async: async) do
|
159
|
+
@lock.synchronize(&block)
|
160
|
+
rescue => e
|
161
|
+
raise translate_exception_class(e, arguments, [])
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# returns a new collector for the Arel visitor.
|
166
|
+
# @return [Arel::Collectors::ElasticsearchQuery]
|
167
|
+
def collector
|
168
|
+
# IMPORTANT: since prepared statements doesn't make sense for elasticsearch,
|
169
|
+
# we don't have to check for +prepared_statements+ here.
|
170
|
+
# Also, bindings are (currently) not supported.
|
171
|
+
# So, we just need a query collector...
|
172
|
+
Arel::Collectors::ElasticsearchQuery.new
|
173
|
+
end
|
174
|
+
|
175
|
+
# returns a new visitor to compile Arel into Elasticsearch Hashes (in this case we use a query object)
|
176
|
+
# @return [Arel::Visitors::Elasticsearch]
|
177
|
+
def arel_visitor
|
178
|
+
Arel::Visitors::Elasticsearch.new(self)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Builds the result object.
|
182
|
+
#
|
183
|
+
# This is an internal hook to make possible connection adapters to build
|
184
|
+
# custom result objects with response-specific data.
|
185
|
+
# @return [ElasticsearchRecord::Result]
|
186
|
+
def build_result(response, columns: [], column_types: {})
|
187
|
+
ElasticsearchRecord::Result.new(response, columns, column_types)
|
188
|
+
end
|
189
|
+
|
190
|
+
# register native types
|
191
|
+
ActiveRecord::Type.register(:format_string, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString, adapter: :elasticsearch)
|
192
|
+
ActiveRecord::Type.register(:multicast_value, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::MulticastValue, adapter: :elasticsearch)
|
193
|
+
ActiveRecord::Type.register(:object, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object, adapter: :elasticsearch, override: false)
|
194
|
+
ActiveRecord::Type.register(:range, ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Range, adapter: :elasticsearch)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'elasticsearch_record/query'
|
4
|
+
|
5
|
+
module Arel # :nodoc: all
|
6
|
+
module Collectors
|
7
|
+
class ElasticsearchQuery < ::ElasticsearchRecord::Query
|
8
|
+
|
9
|
+
# required for ActiveRecord
|
10
|
+
attr_accessor :preparable
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
# force initialize a body as hash
|
14
|
+
super(body: {})
|
15
|
+
|
16
|
+
# @binds = []
|
17
|
+
@bind_index = 1
|
18
|
+
end
|
19
|
+
|
20
|
+
# send a proposal to this query
|
21
|
+
# @param [Symbol] action - the claim action
|
22
|
+
# @param [Array] args - args to claim
|
23
|
+
def claim(action, *args)
|
24
|
+
case action
|
25
|
+
when :index
|
26
|
+
# change the index name
|
27
|
+
@index = args[0]
|
28
|
+
when :type
|
29
|
+
# change the query type
|
30
|
+
@type = args[0]
|
31
|
+
when :status
|
32
|
+
# change the query status
|
33
|
+
@status = args[0]
|
34
|
+
when :columns
|
35
|
+
# change the query columns
|
36
|
+
@columns = args[0]
|
37
|
+
when :arguments
|
38
|
+
# change the query arguments
|
39
|
+
@arguments = args[0]
|
40
|
+
when :argument
|
41
|
+
# adds / sets any argument
|
42
|
+
if args.length == 2
|
43
|
+
@arguments[args[0]] = args[1]
|
44
|
+
else # should be a hash
|
45
|
+
@arguments.merge!(args[0])
|
46
|
+
end
|
47
|
+
when :body
|
48
|
+
# set the body var
|
49
|
+
@body = args[0]
|
50
|
+
when :assign
|
51
|
+
# calls a assign on the body
|
52
|
+
assign(*args)
|
53
|
+
else
|
54
|
+
raise "Unsupported claim action: #{action}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def <<(claim)
|
59
|
+
self.claim(claim[0], *claim[1])
|
60
|
+
end
|
61
|
+
|
62
|
+
# used by the +Arel::Visitors::Elasticsearch#compile+ method (and default Arel visitors)
|
63
|
+
# todo: maybe return arguments with :_meta information instead of self ...
|
64
|
+
def value
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# IMPORTANT: For SQL defaults (see @ Arel::Collectors::SubstituteBinds) a value
|
69
|
+
# will +not+ be directly assigned (see @ Arel::Visitors::ToSql#visit_Arel_Nodes_HomogeneousIn).
|
70
|
+
# instead it will be send as bind and then re-delegated to the SQL collector.
|
71
|
+
#
|
72
|
+
# This only works for linear SQL-queries and not nested Hashes
|
73
|
+
# (otherwise we have to collect those binds, and replace them afterwards).
|
74
|
+
#
|
75
|
+
# This will be ignored by the ElasticsearchQuery collector, but supports statement caches on the other side
|
76
|
+
# (see @ ActiveRecord::StatementCache::PartialQueryCollector)
|
77
|
+
def add_bind(bind, &block)
|
78
|
+
@bind_index += 1
|
79
|
+
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# @see Arel::Collectors::ElasticsearchQuery#add_bind
|
84
|
+
def add_binds(binds, proc_for_binds = nil, &block)
|
85
|
+
@bind_index += binds.size
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# calls a assign on the body
|
92
|
+
def assign(key, value)
|
93
|
+
# check for special provided key, to claim through an assign
|
94
|
+
if key == :__claim__
|
95
|
+
if value.is_a?(Array)
|
96
|
+
value.each do |arg|
|
97
|
+
vkey = arg.keys.first
|
98
|
+
claim(vkey, arg[vkey])
|
99
|
+
end
|
100
|
+
else
|
101
|
+
vkey = value.keys.first
|
102
|
+
claim(vkey, value[vkey])
|
103
|
+
end
|
104
|
+
elsif value.nil?
|
105
|
+
@body.delete(key)
|
106
|
+
else
|
107
|
+
@body[key] = value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Arel # :nodoc: all
|
4
|
+
module Nodes
|
5
|
+
class SelectAgg < Unary
|
6
|
+
|
7
|
+
def left
|
8
|
+
expr[0]
|
9
|
+
end
|
10
|
+
|
11
|
+
def right
|
12
|
+
return expr[1].reduce({}) { |m, data| m.merge(data) } if expr[1].is_a?(Array)
|
13
|
+
|
14
|
+
expr[1]
|
15
|
+
end
|
16
|
+
|
17
|
+
def opts
|
18
|
+
expr[2]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|