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,129 @@
|
|
1
|
+
module ElasticsearchRecord
|
2
|
+
class Query
|
3
|
+
# STATUS CONSTANTS
|
4
|
+
STATUS_VALID = :valid
|
5
|
+
STATUS_FAILED = :failed
|
6
|
+
|
7
|
+
# TYPE CONSTANTS
|
8
|
+
TYPE_UNDEFINED = :undefined
|
9
|
+
TYPE_COUNT = :count
|
10
|
+
TYPE_SEARCH = :search
|
11
|
+
TYPE_MSEARCH = :msearch
|
12
|
+
TYPE_SQL = :sql
|
13
|
+
TYPE_CREATE = :create
|
14
|
+
TYPE_UPDATE = :update
|
15
|
+
TYPE_UPDATE_BY_QUERY = :update_by_query
|
16
|
+
TYPE_DELETE = :delete
|
17
|
+
TYPE_DELETE_BY_QUERY = :delete_by_query
|
18
|
+
|
19
|
+
# includes valid types only
|
20
|
+
TYPES = [TYPE_COUNT, TYPE_SEARCH, TYPE_MSEARCH, TYPE_SQL, TYPE_CREATE, TYPE_UPDATE, TYPE_UPDATE_BY_QUERY, TYPE_DELETE, TYPE_DELETE_BY_QUERY].freeze
|
21
|
+
|
22
|
+
# includes reading types only
|
23
|
+
READ_TYPES = [TYPE_COUNT, TYPE_SEARCH, TYPE_MSEARCH, TYPE_SQL].freeze
|
24
|
+
|
25
|
+
# defines a query to be executed if the query fails - +(none)+ queries
|
26
|
+
# acts like the SQL-query "where('1=0')"
|
27
|
+
FAILED_SEARCH_BODY = { size: 0, query: { bool: { filter: [{ term: { _id: '_' } }] } } }.freeze
|
28
|
+
|
29
|
+
# defines special api gates to be used per type.
|
30
|
+
# if not defined it simply uses +[:core,self.type]+
|
31
|
+
GATES = {
|
32
|
+
TYPE_SQL => [:sql, :query]
|
33
|
+
}
|
34
|
+
|
35
|
+
# defines the index the query should be executed on
|
36
|
+
# @!attribute String
|
37
|
+
attr_reader :index
|
38
|
+
|
39
|
+
# defines the query type.
|
40
|
+
# @see TYPES
|
41
|
+
# @!attribute Symbol
|
42
|
+
attr_reader :type
|
43
|
+
|
44
|
+
# defines the query status.
|
45
|
+
# @see STATUSES
|
46
|
+
# @!attribute Symbol
|
47
|
+
attr_reader :status
|
48
|
+
|
49
|
+
# defines if the affected shards gets refreshed to make this operation visible to search
|
50
|
+
# @!attribute Boolean
|
51
|
+
attr_reader :refresh
|
52
|
+
|
53
|
+
# defines the query body - in most cases this is a hash
|
54
|
+
# @!attribute Hash
|
55
|
+
attr_reader :body
|
56
|
+
|
57
|
+
# defines the query arguments to be passed to the API
|
58
|
+
# @!attribute Hash
|
59
|
+
attr_reader :arguments
|
60
|
+
|
61
|
+
# defines the columns to assign from the query
|
62
|
+
# @!attribute Array
|
63
|
+
attr_reader :columns
|
64
|
+
|
65
|
+
def initialize(index: nil, type: TYPE_UNDEFINED, status: STATUS_VALID, body: nil, refresh: nil, arguments: {}, columns: [])
|
66
|
+
@index = index
|
67
|
+
@type = type
|
68
|
+
@status = status
|
69
|
+
@refresh = refresh
|
70
|
+
@body = body
|
71
|
+
@arguments = arguments
|
72
|
+
@columns = columns
|
73
|
+
end
|
74
|
+
|
75
|
+
# sets the failed status for this query.
|
76
|
+
# returns self
|
77
|
+
# @return [ElasticsearchRecord::Query]
|
78
|
+
def failed!
|
79
|
+
@status = STATUS_FAILED
|
80
|
+
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
# returns true, if the query is valid (e.g. index & type defined)
|
85
|
+
# @return [Boolean]
|
86
|
+
def valid?
|
87
|
+
# type mus be valid + index must be present (not required for SQL)
|
88
|
+
TYPES.include?(self.type) #&& (index.present? || self.type == TYPE_SQL)
|
89
|
+
end
|
90
|
+
|
91
|
+
# returns the API gate to be called to execute the query.
|
92
|
+
# each query type needs a different endpoint.
|
93
|
+
# @see Elasticsearch::API
|
94
|
+
# @return [Array<Symbol, Symbol>] - API gate [<namespace>,<action>]
|
95
|
+
def gate
|
96
|
+
GATES[self.type].presence || [:core, self.type]
|
97
|
+
end
|
98
|
+
|
99
|
+
# returns true if this is a write query
|
100
|
+
# @return [Boolean]
|
101
|
+
def write?
|
102
|
+
!READ_TYPES.include?(self.type)
|
103
|
+
end
|
104
|
+
|
105
|
+
# builds the final query arguments.
|
106
|
+
# Depends on the query status, index, body & refresh attributes.
|
107
|
+
# Also used possible PRE-defined arguments to be merged with those mentioned attributes.
|
108
|
+
# @return [Hash]
|
109
|
+
def query_arguments
|
110
|
+
# check for failed status
|
111
|
+
return { index: self.index, body: FAILED_SEARCH_BODY } if self.status == STATUS_FAILED
|
112
|
+
|
113
|
+
args = @arguments.deep_dup
|
114
|
+
|
115
|
+
# set index, if present
|
116
|
+
args[:index] = self.index if self.index.present?
|
117
|
+
|
118
|
+
# set body, if present
|
119
|
+
args[:body] = self.body if self.body.present?
|
120
|
+
|
121
|
+
# set refresh, if defined (also includes false value)
|
122
|
+
args[:refresh] = self.refresh unless self.refresh.nil?
|
123
|
+
|
124
|
+
args
|
125
|
+
end
|
126
|
+
|
127
|
+
alias :to_query :query_arguments
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module ElasticsearchRecord
|
2
|
+
module Querying
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
# define additional METHODS to be delegated to the Relation
|
7
|
+
# @see ::ActiveRecord::Querying::QUERYING_METHODS
|
8
|
+
ES_QUERYING_METHODS = [
|
9
|
+
:query,
|
10
|
+
:filter,
|
11
|
+
:must,
|
12
|
+
:must_not,
|
13
|
+
:should,
|
14
|
+
:aggregate,
|
15
|
+
:msearch
|
16
|
+
].freeze # :nodoc:
|
17
|
+
delegate(*ES_QUERYING_METHODS, to: :all)
|
18
|
+
|
19
|
+
# finds records by sql, query-arguments or query-object.
|
20
|
+
#
|
21
|
+
# PLEASE NOTE: This method is used by different other methods:
|
22
|
+
# - ActiveRecord::Relation#exec_queries
|
23
|
+
# - ActiveRecord::StatementCache#execute
|
24
|
+
# - <directly on demand>
|
25
|
+
#
|
26
|
+
# We cannot rewrite all call-sources since this will mess up the whole logic end will end in other problems.
|
27
|
+
# So we check here what kind of query is provided and decide what to do.
|
28
|
+
#
|
29
|
+
# PLEASE NOTE: since ths is also used by +ActiveRecord::StatementCache#execute+ we cannot remove
|
30
|
+
# the unused params +preparable+.
|
31
|
+
# see @ ActiveRecord::Querying#find_by_sql
|
32
|
+
#
|
33
|
+
# @param [String, Hash, ElasticsearchRecord::Query] sql
|
34
|
+
# @param [Array] binds
|
35
|
+
# @param [nil] preparable
|
36
|
+
# @param [Proc] block
|
37
|
+
def find_by_sql(sql, binds = [], preparable: nil, &block)
|
38
|
+
query = case sql
|
39
|
+
when String # really find by SQL
|
40
|
+
ElasticsearchRecord::Query.new(
|
41
|
+
type: ElasticsearchRecord::Query::TYPE_SQL,
|
42
|
+
body: { query: query_or_sql },
|
43
|
+
# IMPORTANT: Always provide all columns
|
44
|
+
columns: source_column_names)
|
45
|
+
when Hash
|
46
|
+
ElasticsearchRecord::Query.new(
|
47
|
+
type: ElasticsearchRecord::Query::TYPE_SEARCH,
|
48
|
+
arguments: sql,
|
49
|
+
# IMPORTANT: Always provide all columns
|
50
|
+
columns: source_column_names)
|
51
|
+
else
|
52
|
+
sql
|
53
|
+
end
|
54
|
+
|
55
|
+
_load_from_sql(_query_by_sql(query, binds), &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# finds records by query arguments
|
59
|
+
def find_by_query(arguments, &block)
|
60
|
+
# build new query
|
61
|
+
query = ElasticsearchRecord::Query.new(
|
62
|
+
index: table_name,
|
63
|
+
type: ElasticsearchRecord::Query::TYPE_SEARCH,
|
64
|
+
arguments: arguments,
|
65
|
+
# IMPORTANT: Always provide all columns to prevent unknown attributes that should be nil ...
|
66
|
+
columns: source_column_names)
|
67
|
+
|
68
|
+
_load_from_sql(_query_by_sql(query), &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
# executes a msearch by provided +RAW+ queries
|
72
|
+
def msearch(queries, async: false)
|
73
|
+
# build new msearch query
|
74
|
+
query = ElasticsearchRecord::Query.new(
|
75
|
+
index: table_name,
|
76
|
+
type: ElasticsearchRecord::Query::TYPE_MSEARCH,
|
77
|
+
body: queries.map { |q| { search: q } },
|
78
|
+
# IMPORTANT: Always provide all columns
|
79
|
+
columns: source_column_names)
|
80
|
+
|
81
|
+
connection.exec_query(query, "#{name} Msearch", async: async)
|
82
|
+
end
|
83
|
+
|
84
|
+
# execute query by msearch
|
85
|
+
def _query_by_msearch(queries, async: false)
|
86
|
+
connection.select_multiple(queries, "#{name} Msearch", async: async)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module ElasticsearchRecord
|
2
|
+
module Relation
|
3
|
+
module CalculationMethods
|
4
|
+
# Count the records.
|
5
|
+
#
|
6
|
+
# Person.count
|
7
|
+
# => the total count of all people
|
8
|
+
#
|
9
|
+
# Person.count(:age)
|
10
|
+
# => returns the total count of all people whose age is present in database
|
11
|
+
def count(column_name = nil)
|
12
|
+
# fallback to default
|
13
|
+
return super() if block_given?
|
14
|
+
|
15
|
+
# reset column_name, if +:all+ was provided ...
|
16
|
+
column_name = nil if column_name == :all
|
17
|
+
|
18
|
+
# check for combined cases
|
19
|
+
if self.distinct_value && column_name
|
20
|
+
self.cardinality(column_name)
|
21
|
+
elsif column_name
|
22
|
+
where(:filter, { exists: { field: column_name } }).count
|
23
|
+
elsif self.group_values.any?
|
24
|
+
self.composite(*self.group_values)
|
25
|
+
elsif self.select_values.any?
|
26
|
+
self.composite(*self.select_values)
|
27
|
+
elsif limit_value == 0 # Shortcut when limit is zero.
|
28
|
+
return 0
|
29
|
+
elsif limit_value
|
30
|
+
# since total will be limited to 10000 results, we need to resolve the real values by a custom query.
|
31
|
+
# This query is called through +#select_count+.
|
32
|
+
#
|
33
|
+
# HINT: :__claim__ directly interacts with the query-object and sets a 'terminate_after' argument
|
34
|
+
# (see @ Arel::Collectors::ElasticsearchQuery#assign)
|
35
|
+
arel = spawn.unscope!(:offset, :limit, :order, :configure, :aggs).configure!(:__claim__, argument: { terminate_after: limit_value }).arel
|
36
|
+
klass.connection.select_count(arel, "#{klass.name} Count")
|
37
|
+
else
|
38
|
+
# since total will be limited to 10000 results, we need to resolve the real values by a custom query.
|
39
|
+
# This query is called through +#select_count+.
|
40
|
+
arel = spawn.unscope!(:offset, :limit, :order, :configure, :aggs)
|
41
|
+
klass.connection.select_count(arel, "#{klass.name} Count")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# A multi-value metrics aggregation that calculates one or more
|
46
|
+
# percentiles over numeric values extracted from the aggregated documents.
|
47
|
+
# Returns a hash with empty values (but keys still exists) if there is no row.
|
48
|
+
#
|
49
|
+
# Person.percentiles(:year)
|
50
|
+
# > {
|
51
|
+
# "1.0" => 2016.0,
|
52
|
+
# "5.0" => 2016.0,
|
53
|
+
# "25.0" => 2016.0,
|
54
|
+
# "50.0" => 2017.0,
|
55
|
+
# "75.0" => 2017.0,
|
56
|
+
# "95.0" => 2021.0,
|
57
|
+
# "99.0" => 2022.0
|
58
|
+
# }
|
59
|
+
# @param [Symbol, String] column_name
|
60
|
+
def percentiles(column_name)
|
61
|
+
calculate(:percentiles, column_name, node: :values)
|
62
|
+
end
|
63
|
+
|
64
|
+
# A multi-value metrics aggregation that calculates one or more
|
65
|
+
# percentile ranks over numeric values extracted from the aggregated documents.
|
66
|
+
#
|
67
|
+
# Percentile rank show the percentage of observed values which are below certain value.
|
68
|
+
# For example, if a value is greater than or equal to 95% of the observed values it is
|
69
|
+
# said to be at the 95th percentile rank.
|
70
|
+
#
|
71
|
+
# Person.percentile_ranks(:year, [500,600])
|
72
|
+
# > {
|
73
|
+
# "1.0" => 2016.0,
|
74
|
+
# "5.0" => 2016.0,
|
75
|
+
# "25.0" => 2016.0,
|
76
|
+
# "50.0" => 2017.0,
|
77
|
+
# "75.0" => 2017.0,
|
78
|
+
# "95.0" => 2021.0,
|
79
|
+
# "99.0" => 2022.0
|
80
|
+
# }
|
81
|
+
# @param [Symbol, String] column_name
|
82
|
+
# @param [Array] values
|
83
|
+
def percentile_ranks(column_name, values)
|
84
|
+
calculate(:percentiles, column_name, opts: { values: values }, node: :values)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Calculates the cardinality on a given column. Returns +0+ if there's no row.
|
88
|
+
#
|
89
|
+
# Person.cardinality(:age)
|
90
|
+
# > 12
|
91
|
+
#
|
92
|
+
# @param [Symbol, String] column_name
|
93
|
+
def cardinality(column_name)
|
94
|
+
calculate(:cardinality, column_name)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Calculates the average value on a given column. Returns +nil+ if there's no row. See #calculate for examples with options.
|
98
|
+
#
|
99
|
+
# Person.average(:age) # => 35.8
|
100
|
+
#
|
101
|
+
# @param [Symbol, String] column_name
|
102
|
+
def average(column_name)
|
103
|
+
calculate(:avg, column_name)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Calculates the minimum value on a given column. The value is returned
|
107
|
+
# with the same data type of the column, or +nil+ if there's no row.
|
108
|
+
#
|
109
|
+
# Person.minimum(:age)
|
110
|
+
# > 7
|
111
|
+
#
|
112
|
+
# @param [Symbol, String] column_name
|
113
|
+
def minimum(column_name)
|
114
|
+
calculate(:min, column_name)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Calculates the maximum value on a given column. The value is returned
|
118
|
+
# with the same data type of the column, or +nil+ if there's no row. See
|
119
|
+
# #calculate for examples with options.
|
120
|
+
#
|
121
|
+
# Person.maximum(:age) # => 93
|
122
|
+
#
|
123
|
+
# @param [Symbol, String] column_name
|
124
|
+
def maximum(column_name)
|
125
|
+
calculate(:max, column_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Calculates the sum of values on a given column. The value is returned
|
129
|
+
# with the same data type of the column, +0+ if there's no row. See
|
130
|
+
# #calculate for examples with options.
|
131
|
+
#
|
132
|
+
# Person.sum(:age) # => 4562
|
133
|
+
#
|
134
|
+
# @param [Symbol, String] column_name (optional)
|
135
|
+
def sum(column_name)
|
136
|
+
calculate(:sum, column_name)
|
137
|
+
end
|
138
|
+
|
139
|
+
# creates a aggregation with the provided metric (e.g. :sum) and column.
|
140
|
+
# returns the metric node (default: :value) from the aggregations result.
|
141
|
+
# @param [Symbol, String] metric
|
142
|
+
# @param [Symbol, String] column
|
143
|
+
# @param [Hash] opts - additional arguments that get merged with the metric definition
|
144
|
+
# @param [Symbol] node (default :value)
|
145
|
+
def calculate(metric, column, opts: {}, node: :value)
|
146
|
+
metric_key = "#{column}_#{metric}"
|
147
|
+
|
148
|
+
# spawn a new aggregation and return the aggs
|
149
|
+
response = aggregate(metric_key, { metric => { field: column }.merge(opts) }).aggregations
|
150
|
+
|
151
|
+
response[metric_key][node]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ElasticsearchRecord
|
2
|
+
module Relation
|
3
|
+
module CoreMethods
|
4
|
+
def instantiate_records(rows, &block)
|
5
|
+
# slurp the total value from the rows (rows = ElasticsearchRecord::Result)
|
6
|
+
@total = rows.is_a?(::ElasticsearchRecord::Result) ? rows.total : rows.length
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
# transforms the current relation into arel, compiles it to query and executes the query.
|
11
|
+
# returns the result object.
|
12
|
+
#
|
13
|
+
# PLEASE NOTE: This makes the query +immutable+ and raises a +ActiveRecord::ImmutableRelation+
|
14
|
+
# if you try to change it's values.
|
15
|
+
#
|
16
|
+
# PLEASE NOTE: resolving records _(instantiate)_ is never possible after calling this method!
|
17
|
+
#
|
18
|
+
# @return [ElasticsearchRecord::Result]
|
19
|
+
# @param [String] name - custom instrumentation name (default: 'Load')
|
20
|
+
def resolve(name = 'Load')
|
21
|
+
# this acts the same like +#_query_by_sql+ but we can customize the instrumentation name and
|
22
|
+
# do not store the records.
|
23
|
+
klass.connection.select_all(arel, "#{klass.name} #{name}")
|
24
|
+
end
|
25
|
+
|
26
|
+
# returns the query hash for the current relation
|
27
|
+
# @return [Hash]
|
28
|
+
def to_query
|
29
|
+
to_sql.query_arguments
|
30
|
+
end
|
31
|
+
|
32
|
+
# executes the elasticsearch msearch on the related klass
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# msearch([2020, 2019, 2018]).each{ |q, year| q.where!(year: year) }
|
36
|
+
#
|
37
|
+
# @param [Array] values - values to be yielded
|
38
|
+
# @param [nil, Symbol] response_type - optional type of search response (:took, :total , :hits , :aggregations , :length , :results, :each)
|
39
|
+
def msearch(values = nil, response_type = nil)
|
40
|
+
if values.nil?
|
41
|
+
arels = [arel]
|
42
|
+
else
|
43
|
+
arels = values.map { |value|
|
44
|
+
# spawn a new relation and return the arel-object
|
45
|
+
yield(spawn, value).arel
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# returns a response object with multiple single responses
|
50
|
+
responses = klass._query_by_msearch(arels)
|
51
|
+
|
52
|
+
if response_type
|
53
|
+
responses.map(&response_type.to_sym)
|
54
|
+
else
|
55
|
+
responses
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ElasticsearchRecord # :nodoc:
|
4
|
+
module Relation
|
5
|
+
class QueryClause
|
6
|
+
delegate :any?, :empty?, to: :predicates
|
7
|
+
|
8
|
+
attr_reader :key
|
9
|
+
|
10
|
+
def initialize(key, predicates, opts = {})
|
11
|
+
@key = key
|
12
|
+
@predicates = predicates
|
13
|
+
@opts = opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def hash
|
17
|
+
[self.class, key, predicates, opts].hash
|
18
|
+
end
|
19
|
+
|
20
|
+
def ast
|
21
|
+
[key, (predicates.one? ? predicates[0] : predicates), opts]
|
22
|
+
end
|
23
|
+
|
24
|
+
def +(other)
|
25
|
+
::ElasticsearchRecord::Relation::QueryClause.new(key, predicates + other.predicates, opts.merge(other.opts))
|
26
|
+
end
|
27
|
+
|
28
|
+
def -(other)
|
29
|
+
::ElasticsearchRecord::Relation::QueryClause.new(key, predicates - other.predicates, Hash[opts.to_a - other.opts.to_a])
|
30
|
+
end
|
31
|
+
|
32
|
+
def |(other)
|
33
|
+
::ElasticsearchRecord::Relation::QueryClause.new(key, predicates | other.predicates, Hash[opts.to_a | other.opts.to_a])
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
attr_reader :predicates
|
39
|
+
attr_reader :opts
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ElasticsearchRecord # :nodoc:
|
4
|
+
module Relation
|
5
|
+
class QueryClauseTree
|
6
|
+
delegate :any?, :empty?, :key?, :each, to: :predicates
|
7
|
+
|
8
|
+
def self.empty
|
9
|
+
@empty ||= new({}).freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(predicates)
|
13
|
+
@predicates = predicates
|
14
|
+
end
|
15
|
+
|
16
|
+
def key
|
17
|
+
:tree
|
18
|
+
end
|
19
|
+
|
20
|
+
def hash
|
21
|
+
[self.class, predicates].hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def ast
|
25
|
+
predicates.values.map(&:ast)
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge(other)
|
29
|
+
dups = dupredicates
|
30
|
+
|
31
|
+
other.each do |key, values|
|
32
|
+
if dups.key?(key)
|
33
|
+
dups[key] = (dups[key] + values).uniq
|
34
|
+
else
|
35
|
+
dups[key] = values
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
QueryClauseTree.new(dups)
|
40
|
+
end
|
41
|
+
|
42
|
+
def +(other)
|
43
|
+
dups = dupredicates
|
44
|
+
|
45
|
+
if key?(other.key)
|
46
|
+
dups[other.key] += other
|
47
|
+
else
|
48
|
+
dups[other.key] = other
|
49
|
+
end
|
50
|
+
|
51
|
+
QueryClauseTree.new(dups)
|
52
|
+
end
|
53
|
+
|
54
|
+
def -(other)
|
55
|
+
dups = dupredicates
|
56
|
+
|
57
|
+
if key?(other.key)
|
58
|
+
dups[other.key] -= other
|
59
|
+
dups.delete(other.key) if dups[other.key].blank?
|
60
|
+
end
|
61
|
+
|
62
|
+
QueryClauseTree.new(dups)
|
63
|
+
end
|
64
|
+
|
65
|
+
def |(other)
|
66
|
+
dups = dupredicates
|
67
|
+
|
68
|
+
if key?(other.key)
|
69
|
+
dups[other.key] |= other
|
70
|
+
else
|
71
|
+
dups[other.key] = other
|
72
|
+
end
|
73
|
+
|
74
|
+
QueryClauseTree.new(dups)
|
75
|
+
end
|
76
|
+
|
77
|
+
def ==(other)
|
78
|
+
other.is_a?(::ElasticsearchRecord::Relation::QueryClauseTree) &&
|
79
|
+
predicates == other.predicates
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
attr_reader :predicates
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def dupredicates
|
89
|
+
# we only dup the hash - no need to dup the lower elements
|
90
|
+
predicates.dup
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|