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