elasticsearch_record 1.0.0 → 1.0.1

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