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