elasticsearch_record 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bd7858c5e59826439234e17437d5b8ff62eebbced936a5effee9da19159de5e
4
- data.tar.gz: ae5d249a7d888363381bee306184c40979445aea26c5e1eec98ac94e11009e88
3
+ metadata.gz: 0d9553144d67b0b52192c1da8165a42ee27feeadfde5b8e6cc186466c52606ce
4
+ data.tar.gz: bf7bcb6fd5bbb97d8595066260349f08c0d8a1fa53da3fc3686b6e6e7a9bced5
5
5
  SHA512:
6
- metadata.gz: 23d1062c88940694455c0ef93b7fa7f9c648b20011ed69125234304b9e5e1f61a3ccffa87967af8b757fac7e9846c66051b3517b210d3a91f3e41d71ccd7c057
7
- data.tar.gz: c975c08bfe47faa8b098f7a457ceb828a8314e3a51c0e0554caa6f73d08b85ce168b4ca3466a54f392336debdae5c44ddebe267c83bda1eb2ee94c9f2b059362
6
+ metadata.gz: 709fb46f9e622a0d0eefff1eae49879a72301cd12cb6c92f465d689a7016269aa6f4b1a80773116c6d6c1e22f77ce40d3688ab12ec3757cca514984421fc7d26
7
+ data.tar.gz: 8ddc6805b580aabc3ea085cf61446ec498b712280d5bf7ac2899aa1ef8c9fa02478282c3c807749703406d8bc898f9b9154c32d04394eff3aa813ea395a53cf7
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # ElasticsearchRecord
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/elasticsearch_record.svg)](https://badge.fury.io/rb/elasticsearch_record)
4
+
3
5
  ActiveRecord functionality for Elasticsearch indexes & documents.
4
6
 
5
- _ElasticsearchRecord is a ActiveRecord-fork and tries to provide the same functionality for Elasticsearch._
7
+ _ElasticsearchRecord is a ActiveRecord-fork and provides similar functionality for Elasticsearch._
6
8
 
7
9
  -----
8
10
 
@@ -118,7 +120,7 @@ Different to the default where-method you can now use it in different ways.
118
120
 
119
121
  Using it by default with a Hash, the method decides itself to either add a filter, or must_not query.
120
122
 
121
- _Hint: If not overwritten through ```kind(...)``` a default kind **:bool** will be used._
123
+ _Hint: If not provided through ```#kind```-method a default kind **:bool** will be used._
122
124
  ```ruby
123
125
  # use it by default
124
126
  Search.where(name: 'A nice object')
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # ElasticsearchRecord - CHANGELOG
2
2
 
3
+ ## [1.0.1] - 2022-10-19
4
+ * [add] ```ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Nested``` class to cast nested values
5
+ * [add] **properties** to column definition (so they are also searchable by _Relation_ conditions)
6
+ * [add] exception for _Relation_ #pit_results if batch size is too large
7
+ * [add] a default _#find_by_id_-method to proved a 'fallback' functionality for the primary '_id' column
8
+ * [fix] nested properties are not cast for column-type "object"
9
+ * [fix] ```ActiveRecord::ConnectionAdapters::Elasticsearch::SchemaStatements``` fields and property detection
10
+ * [fix] ```ElasticsearchRecord::StatementCache::PartialQuery``` reference manipulation of cached hash _(missing .deep_dup )_
11
+ * [ref] ```ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object``` class to only cast object values
12
+ * [ref] ```Arel::Visitors::Elasticsearch#visit_Sort``` to detect a random sort with correct keyword: "**\_\_rand\_\_**"
13
+
3
14
  ## [1.0.0] - 2022-10-18
4
15
  * [add] patch for ```ActiveRecord::Relation::Merger``` - to support AR-relations
5
16
  * [add] ```ElasticsearchRecord::Relation#pit_results``` _offset_ & _yield_ support
@@ -5,11 +5,12 @@ module ActiveRecord
5
5
  module Elasticsearch
6
6
  class Column < ConnectionAdapters::Column # :nodoc:
7
7
 
8
- attr_reader :virtual, :fields
8
+ attr_reader :virtual, :fields, :properties
9
9
 
10
10
  def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, **kwargs)
11
- @virtual = kwargs.delete(:virtual)
12
- @fields = kwargs.delete(:fields)
11
+ @virtual = kwargs.delete(:virtual)
12
+ @fields = kwargs.delete(:fields)
13
+ @properties = kwargs.delete(:properties)
13
14
  super(name, default, sql_type_metadata, null, default_function, **kwargs)
14
15
  end
15
16
 
@@ -26,6 +27,29 @@ module ActiveRecord
26
27
  def fields?
27
28
  fields.present?
28
29
  end
30
+
31
+ # returns true if this column has nested properties
32
+ # To receive the nested names just call +#properties+ on this object.
33
+ # @return [Boolean]
34
+ def properties?
35
+ properties.present?
36
+ end
37
+
38
+ # returns a array of field names
39
+ # @return [Array]
40
+ def field_names
41
+ return [] unless fields?
42
+
43
+ fields.map { |field| field['name'] }
44
+ end
45
+
46
+ # returns a array of property names
47
+ # @return [Array]
48
+ def property_names
49
+ return [] unless properties?
50
+
51
+ properties.map { |property| property['name'] }
52
+ end
29
53
  end
30
54
  end
31
55
  end
@@ -23,7 +23,7 @@ module ActiveRecord
23
23
  # @param [String] index_name
24
24
  # @return [Hash]
25
25
  def settings(index_name)
26
- api(:indices, :get_settings, { index: index_name }, 'SCHEMA').dig(index_name, 'settings','index')
26
+ api(:indices, :get_settings, { index: index_name }, 'SCHEMA').dig(index_name, 'settings', 'index')
27
27
  end
28
28
 
29
29
  # Returns the list of a table's column names, data types, and default values.
@@ -38,12 +38,14 @@ module ActiveRecord
38
38
  # since the received mappings do not have the "primary" +_id+-column we manually need to add this here
39
39
  # The BASE_STRUCTURE will also include some meta keys like '_score', '_type', ...
40
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') || []
41
+ # resolve (nested) fields and properties
42
+ fields, properties = resolve_fields_and_properties(key, prop, true)
44
43
 
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}" })
44
+ # fallback for possible empty type
45
+ type = prop['type'].presence || (properties.present? ? 'object' : 'nested')
46
+
47
+ # return a new hash
48
+ prop.merge('name' => key, 'type' => type, 'fields' => fields, 'properties' => properties)
47
49
  }
48
50
  end
49
51
 
@@ -54,18 +56,16 @@ module ActiveRecord
54
56
  # @param [Hash] field
55
57
  # @return [ActiveRecord::ConnectionAdapters::Column]
56
58
  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
59
  ActiveRecord::ConnectionAdapters::Elasticsearch::Column.new(
61
60
  field["name"],
62
61
  field["null_value"],
63
- fetch_type_metadata(field_type),
62
+ fetch_type_metadata(field["type"]),
64
63
  field['null'].nil? ? true : field['null'],
65
64
  nil,
66
- comment: field['meta'] ? field['meta'].map { |k, v| "#{k}: #{v}" }.join(' | ') : nil,
67
- virtual: field['virtual'],
68
- fields: field['fields']
65
+ comment: field['meta'] ? field['meta'].map { |k, v| "#{k}: #{v}" }.join(' | ') : nil,
66
+ virtual: field['virtual'],
67
+ fields: field['fields'],
68
+ properties: field['properties']
69
69
  )
70
70
  end
71
71
 
@@ -128,6 +128,59 @@ module ActiveRecord
128
128
  def max_result_window
129
129
  10000
130
130
  end
131
+
132
+ private
133
+
134
+ # returns a multidimensional array with fields & properties from the provided +prop+.
135
+ # Nested fields & properties will be also detected.
136
+ # .
137
+ # resolve_fields_and_properties('user', {...})
138
+ # # > [
139
+ # # fields
140
+ # [0] [
141
+ # [0] {
142
+ # "name" => "user.name.analyzed",
143
+ # "type" => "text"
144
+ # }
145
+ # ],
146
+ # # properties
147
+ # [1] [
148
+ # [0] {
149
+ # "name" => "user.id",
150
+ # "type" => "integer"
151
+ # },
152
+ # [1] {
153
+ # "name" => "user.name",
154
+ # "type" => "keyword"
155
+ # }
156
+ # ]
157
+ # ]
158
+ #
159
+ # @param [String] key
160
+ # @param [Hash] prop
161
+ # @param [Boolean] root - provide true, if this is a top property entry (default: false)
162
+ # @return [[Array, Array]]
163
+ def resolve_fields_and_properties(key, prop, root = false)
164
+ # mappings can have +fields+ - we also want them for 'query-conditions'
165
+ fields = (prop['fields'] || {}).map { |field_key, field_def|
166
+ { 'name' => "#{key}.#{field_key}", 'type' => field_def['type'] }
167
+ }
168
+
169
+ # initial empty array
170
+ properties = []
171
+
172
+ if prop['properties'].present?
173
+ prop['properties'].each do |nested_key, nested_prop|
174
+ nested_fields, nested_properties = resolve_fields_and_properties("#{key}.#{nested_key}", nested_prop)
175
+ fields |= nested_fields
176
+ properties |= nested_properties
177
+ end
178
+ elsif !root # don't add the root property as sub-property
179
+ properties << { 'name' => key, 'type' => prop['type'] }
180
+ end
181
+
182
+ [fields, properties]
183
+ end
131
184
  end
132
185
  end
133
186
  end
@@ -25,7 +25,7 @@ module ActiveRecord
25
25
 
26
26
  def _deserialize(value)
27
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?
28
+ return _deserialize_by_nested_type(value) if nested_type.type == :object
29
29
 
30
30
  if value.is_a?(Array)
31
31
  value.map { |val| _deserialize_by_nested_type(val) }
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Elasticsearch
4
+ module Type # :nodoc:
5
+ class Nested < ActiveRecord::Type::Value
6
+
7
+ def type
8
+ :nested
9
+ end
10
+
11
+ private
12
+
13
+ # cast value
14
+ def cast_value(value)
15
+ value.to_h
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -3,39 +3,15 @@ module ActiveRecord
3
3
  module Elasticsearch
4
4
  module Type # :nodoc:
5
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
6
  def type
20
7
  :object
21
8
  end
22
9
 
23
- def forced?
24
- @force
25
- end
26
-
27
10
  private
28
11
 
29
12
  # cast value by provided cast_method
30
13
  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
14
+ value.to_h
39
15
  end
40
16
  end
41
17
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'active_record/connection_adapters/elasticsearch/type/format_string'
4
4
  require 'active_record/connection_adapters/elasticsearch/type/multicast_value'
5
+ require 'active_record/connection_adapters/elasticsearch/type/nested'
5
6
  require 'active_record/connection_adapters/elasticsearch/type/object'
6
7
  require 'active_record/connection_adapters/elasticsearch/type/range'
7
8
 
@@ -88,11 +88,11 @@ module ActiveRecord # :nodoc:
88
88
  m.register_type 'date', Type::DateTime.new
89
89
 
90
90
  # force a hash
91
- m.register_type 'object', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object.new(cast: :to_h, force: true)
91
+ m.register_type 'object', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object.new
92
92
  m.alias_type 'flattened', "object"
93
93
 
94
94
  # array of objects
95
- m.register_type 'nested', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Object.new(cast: :to_h)
95
+ m.register_type 'nested', ActiveRecord::ConnectionAdapters::Elasticsearch::Type::Nested.new
96
96
 
97
97
  ip_type = ActiveRecord::ConnectionAdapters::Elasticsearch::Type::FormatString.new(format: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)
98
98
 
@@ -410,8 +410,8 @@ module Arel # :nodoc: all
410
410
  key = visit(o.expr)
411
411
  dir = visit(o.direction)
412
412
 
413
- # we support a special key: _rand to create a simple random method ...
414
- if key == '_rand'
413
+ # we support a special key: __rand__ to create a simple random method ...
414
+ if key == '__rand__'
415
415
  assign({
416
416
  "_script" => {
417
417
  "script" => "Math.random()",
@@ -9,7 +9,7 @@ module ElasticsearchRecord
9
9
  module VERSION
10
10
  MAJOR = 1
11
11
  MINOR = 0
12
- TINY = 0
12
+ TINY = 1
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -20,12 +20,13 @@ module ElasticsearchRecord
20
20
  @source_column_names ||= columns.reject(&:virtual).map(&:name) - ActiveRecord::ConnectionAdapters::ElasticsearchAdapter.base_structure_keys
21
21
  end
22
22
 
23
- # returns an array with columns names, that are searchable (also includes nested )
23
+ # returns an array with columns names, that are searchable (also includes nested fields & properties )
24
24
  def searchable_column_names
25
25
  @searchable_column_names ||= columns.reject(&:virtual).reduce([]) { |m, column|
26
26
  m << column.name
27
- m += column.fields if column.fields?
28
- m
27
+ m += column.field_names
28
+ m += column.property_names
29
+ m.uniq
29
30
  }
30
31
  end
31
32
 
@@ -16,6 +16,13 @@ module ElasticsearchRecord
16
16
  ].freeze # :nodoc:
17
17
  delegate(*ES_QUERYING_METHODS, to: :all)
18
18
 
19
+ # finds a single record by provided id.
20
+ # This method is overwritten to support the primary key column (+_id+).
21
+ # @param [Object] id
22
+ def find_by_id(id)
23
+ has_attribute?('id') ? super(id) : public_send(:find_by__id, id)
24
+ end
25
+
19
26
  # finds records by sql, query-arguments or query-object.
20
27
  #
21
28
  # PLEASE NOTE: This method is used by different other methods:
@@ -3,10 +3,10 @@ module ElasticsearchRecord
3
3
  module CalculationMethods
4
4
  # Count the records.
5
5
  #
6
- # Person.count
6
+ # Person.all.count
7
7
  # => the total count of all people
8
8
  #
9
- # Person.count(:age)
9
+ # Person.all.count(:age)
10
10
  # => returns the total count of all people whose age is present in database
11
11
  def count(column_name = nil)
12
12
  # fallback to default
@@ -46,7 +46,7 @@ module ElasticsearchRecord
46
46
  # percentiles over numeric values extracted from the aggregated documents.
47
47
  # Returns a hash with empty values (but keys still exists) if there is no row.
48
48
  #
49
- # Person.percentiles(:year)
49
+ # Person.all.percentiles(:year)
50
50
  # > {
51
51
  # "1.0" => 2016.0,
52
52
  # "5.0" => 2016.0,
@@ -68,7 +68,7 @@ module ElasticsearchRecord
68
68
  # For example, if a value is greater than or equal to 95% of the observed values it is
69
69
  # said to be at the 95th percentile rank.
70
70
  #
71
- # Person.percentile_ranks(:year, [500,600])
71
+ # Person.all.percentile_ranks(:year, [500,600])
72
72
  # > {
73
73
  # "1.0" => 2016.0,
74
74
  # "5.0" => 2016.0,
@@ -86,7 +86,7 @@ module ElasticsearchRecord
86
86
 
87
87
  # Calculates the cardinality on a given column. Returns +0+ if there's no row.
88
88
  #
89
- # Person.cardinality(:age)
89
+ # Person.all.cardinality(:age)
90
90
  # > 12
91
91
  #
92
92
  # @param [Symbol, String] column_name
@@ -96,7 +96,7 @@ module ElasticsearchRecord
96
96
 
97
97
  # Calculates the average value on a given column. Returns +nil+ if there's no row. See #calculate for examples with options.
98
98
  #
99
- # Person.average(:age) # => 35.8
99
+ # Person.all.average(:age) # => 35.8
100
100
  #
101
101
  # @param [Symbol, String] column_name
102
102
  def average(column_name)
@@ -106,7 +106,7 @@ module ElasticsearchRecord
106
106
  # Calculates the minimum value on a given column. The value is returned
107
107
  # with the same data type of the column, or +nil+ if there's no row.
108
108
  #
109
- # Person.minimum(:age)
109
+ # Person.all.minimum(:age)
110
110
  # > 7
111
111
  #
112
112
  # @param [Symbol, String] column_name
@@ -118,7 +118,7 @@ module ElasticsearchRecord
118
118
  # with the same data type of the column, or +nil+ if there's no row. See
119
119
  # #calculate for examples with options.
120
120
  #
121
- # Person.maximum(:age) # => 93
121
+ # Person.all.maximum(:age) # => 93
122
122
  #
123
123
  # @param [Symbol, String] column_name
124
124
  def maximum(column_name)
@@ -129,7 +129,7 @@ module ElasticsearchRecord
129
129
  # with the same data type of the column, +0+ if there's no row. See
130
130
  # #calculate for examples with options.
131
131
  #
132
- # Person.sum(:age) # => 4562
132
+ # Person.all.sum(:age) # => 4562
133
133
  #
134
134
  # @param [Symbol, String] column_name (optional)
135
135
  def sum(column_name)
@@ -89,6 +89,8 @@ module ElasticsearchRecord
89
89
  # @param [String] keep_alive - how long to keep alive (for each single request) - default: '1m'
90
90
  # @param [Integer] batch_size - how many results per query (default: 1000 - this means at least 10 queries before reaching the +max_result_window+)
91
91
  def pit_results(keep_alive: '1m', batch_size: 1000)
92
+ raise ArgumentError, "Batch size cannot be above the 'max_result_window' (#{klass.connection.max_result_window}) !" if batch_size > klass.connection.max_result_window
93
+
92
94
  # check if a limit or offset values was provided
93
95
  results_limit = limit_value ? limit_value : Float::INFINITY
94
96
  results_offset = offset_value ? offset_value : 0
@@ -116,7 +118,7 @@ module ElasticsearchRecord
116
118
  current_response = relation.spawn.configure!(current_pit_hash).limit!(batch_size).resolve('Pit').response
117
119
 
118
120
  # resolve only data from hits->hits[{_source}]
119
- current_results = current_response['hits']['hits'].map { |result| result['_source'] }
121
+ current_results = current_response['hits']['hits'].map { |result| result['_source'].merge('_id' => result['_id']) }
120
122
  current_results_length = current_results.length
121
123
 
122
124
  # check if we reached the required offset
@@ -11,7 +11,7 @@ module ElasticsearchRecord
11
11
 
12
12
  def sql_for(binds, connection)
13
13
  # dup original array
14
- claims = @values.dup
14
+ claims = @values.deep_dup
15
15
 
16
16
  # substitute binds
17
17
  claims.each do |claim|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Gonsior
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-17 00:00:00.000000000 Z
11
+ date: 2022-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -80,8 +80,8 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '13.0'
83
- description: 'ElasticsearchRecord is a ActiveRecord-fork and tries to provide the
84
- same functionality for Elasticsearch.
83
+ description: 'ElasticsearchRecord is a ActiveRecord-fork and provides similar functionality
84
+ for Elasticsearch.
85
85
 
86
86
  '
87
87
  email:
@@ -106,6 +106,7 @@ files:
106
106
  - lib/active_record/connection_adapters/elasticsearch/type.rb
107
107
  - lib/active_record/connection_adapters/elasticsearch/type/format_string.rb
108
108
  - lib/active_record/connection_adapters/elasticsearch/type/multicast_value.rb
109
+ - lib/active_record/connection_adapters/elasticsearch/type/nested.rb
109
110
  - lib/active_record/connection_adapters/elasticsearch/type/object.rb
110
111
  - lib/active_record/connection_adapters/elasticsearch/type/range.rb
111
112
  - lib/active_record/connection_adapters/elasticsearch_adapter.rb