elasticgraph-graphql 0.18.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/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/elasticgraph-graphql.gemspec +23 -0
- data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +79 -0
- data/lib/elastic_graph/graphql/aggregation/computation.rb +39 -0
- data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +83 -0
- data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +47 -0
- data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +26 -0
- data/lib/elastic_graph/graphql/aggregation/key.rb +87 -0
- data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +37 -0
- data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +129 -0
- data/lib/elastic_graph/graphql/aggregation/path_segment.rb +31 -0
- data/lib/elastic_graph/graphql/aggregation/query.rb +172 -0
- data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +345 -0
- data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +187 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +41 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +44 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +30 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +64 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +83 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +82 -0
- data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +32 -0
- data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +118 -0
- data/lib/elastic_graph/graphql/client.rb +43 -0
- data/lib/elastic_graph/graphql/config.rb +81 -0
- data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +100 -0
- data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +142 -0
- data/lib/elastic_graph/graphql/datastore_query/paginator.rb +199 -0
- data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +239 -0
- data/lib/elastic_graph/graphql/datastore_query.rb +372 -0
- data/lib/elastic_graph/graphql/datastore_response/document.rb +78 -0
- data/lib/elastic_graph/graphql/datastore_response/search_response.rb +79 -0
- data/lib/elastic_graph/graphql/datastore_search_router.rb +151 -0
- data/lib/elastic_graph/graphql/decoded_cursor.rb +120 -0
- data/lib/elastic_graph/graphql/filtering/boolean_query.rb +45 -0
- data/lib/elastic_graph/graphql/filtering/field_path.rb +81 -0
- data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +58 -0
- data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +526 -0
- data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +148 -0
- data/lib/elastic_graph/graphql/filtering/range_query.rb +56 -0
- data/lib/elastic_graph/graphql/http_endpoint.rb +229 -0
- data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +56 -0
- data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +48 -0
- data/lib/elastic_graph/graphql/query_adapter/filters.rb +161 -0
- data/lib/elastic_graph/graphql/query_adapter/pagination.rb +27 -0
- data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +124 -0
- data/lib/elastic_graph/graphql/query_adapter/sort.rb +32 -0
- data/lib/elastic_graph/graphql/query_details_tracker.rb +60 -0
- data/lib/elastic_graph/graphql/query_executor.rb +200 -0
- data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +49 -0
- data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +114 -0
- data/lib/elastic_graph/graphql/resolvers/list_records.rb +29 -0
- data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +74 -0
- data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +85 -0
- data/lib/elastic_graph/graphql/resolvers/query_source.rb +46 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +71 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +65 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +82 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +40 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +42 -0
- data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +56 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +35 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +64 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +60 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +30 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +47 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +24 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +44 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +32 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +634 -0
- data/lib/elastic_graph/graphql/schema/arguments.rb +78 -0
- data/lib/elastic_graph/graphql/schema/enum_value.rb +30 -0
- data/lib/elastic_graph/graphql/schema/field.rb +147 -0
- data/lib/elastic_graph/graphql/schema/relation_join.rb +103 -0
- data/lib/elastic_graph/graphql/schema/type.rb +263 -0
- data/lib/elastic_graph/graphql/schema.rb +164 -0
- data/lib/elastic_graph/graphql.rb +253 -0
- data/script/dump_time_zones +81 -0
- data/script/dump_time_zones.java +17 -0
- metadata +503 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Use of this source code is governed by an MIT-style
|
|
4
|
+
# license that can be found in the LICENSE file or at
|
|
5
|
+
# https://opensource.org/licenses/MIT.
|
|
6
|
+
#
|
|
7
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
require "elastic_graph/error"
|
|
10
|
+
require "elastic_graph/support/memoizable_data"
|
|
11
|
+
|
|
12
|
+
module ElasticGraph
|
|
13
|
+
class GraphQL
|
|
14
|
+
class DatastoreQuery
|
|
15
|
+
# A generic pagination implementation, designed to handle both document pagination and
|
|
16
|
+
# aggregation pagination. Not tested directly; tests drive the `Query` interface instead.
|
|
17
|
+
#
|
|
18
|
+
# Our pagination support is designed to support Facebook's Relay Cursor Connections Spec.
|
|
19
|
+
# The description of the pagination algorithm is directly implemented by this class:
|
|
20
|
+
#
|
|
21
|
+
# https://facebook.github.io/relay/graphql/connections.htm#sec-Pagination-algorithm
|
|
22
|
+
#
|
|
23
|
+
# As described by the spec, we support 4 pagination arguments, and apply them in this order:
|
|
24
|
+
#
|
|
25
|
+
# - `after`: items with a cursor value on or before this value are excluded
|
|
26
|
+
# - `before`: items with a cursor value on or after this value are excluded
|
|
27
|
+
# - `first`: after applying before/after, all but the first `N` items are excluded
|
|
28
|
+
# - `last`: after applying before/after/first, all but the last `N` items are excluded
|
|
29
|
+
#
|
|
30
|
+
# Note that `first` is applied before `last`, meaning that when both are provided (as in
|
|
31
|
+
# `first: 10, last: 4`) it is interpreted as "the last 4 of the first 10". However, the Relay
|
|
32
|
+
# spec itself discourages clients from passing both, but servers must still support it:
|
|
33
|
+
#
|
|
34
|
+
# > Including a value for both first and last is strongly discouraged, as it is likely to lead
|
|
35
|
+
# > to confusing queries and results.
|
|
36
|
+
#
|
|
37
|
+
# For document pagination, the relay semantics are implemented on top of Elasticsearch/OpenSearch's `search_after` feature:
|
|
38
|
+
#
|
|
39
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-search-after.html
|
|
40
|
+
#
|
|
41
|
+
# For aggregation pagination, the relay semantics are implemented on top of the composite aggregation
|
|
42
|
+
# pagination feature:
|
|
43
|
+
#
|
|
44
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations-bucket-composite-aggregation.html#_pagination
|
|
45
|
+
#
|
|
46
|
+
# In either case, the `search_after` (or `after`) argument is directly analogous to Relay's `after`.
|
|
47
|
+
# To support the full Relay spec, we have to do some additional clever things:
|
|
48
|
+
#
|
|
49
|
+
# - When necessary (such as for `last: 50, before: some_cursor`), we have to _reverse_ the
|
|
50
|
+
# sort, perform the query with a size of `last`, and then reverse the returned items
|
|
51
|
+
# to the originally requested order.
|
|
52
|
+
# - In some cases, we have to apply `after`, `before` or `last` as a post-processing step
|
|
53
|
+
# to the items returned by the datastore.
|
|
54
|
+
#
|
|
55
|
+
# Note, however, that the sort key data type used for these two cases is a bit different:
|
|
56
|
+
#
|
|
57
|
+
# - For document pagination, `search_after` is a list of scalar values, corresponding to the order
|
|
58
|
+
# of `sort` clauses. That is, if we are sorting on `amount` ascending and `createdAt` descending,
|
|
59
|
+
# then the `search_after` value (and the `sort` value of each document) will be an
|
|
60
|
+
# `[amount, createdAt]` tuple.
|
|
61
|
+
# - For aggregation pagination, `after` (and the `key` of each aggregation bucket is an unordered
|
|
62
|
+
# hash of sort values. The sort field order is instead implied by the composite aggregation
|
|
63
|
+
# `sources`.
|
|
64
|
+
class Paginator < Support::MemoizableData.define(:default_page_size, :max_page_size, :first, :after, :last, :before, :schema_element_names)
|
|
65
|
+
# These methods are provided by `Data.define`:
|
|
66
|
+
# @dynamic default_page_size, max_page_size, first, after, last, before, schema_element_names, initialize
|
|
67
|
+
|
|
68
|
+
def requested_page_size
|
|
69
|
+
# `+ 1` so we can tell if there are more docs for `has_next_page`/`has_previous_page`
|
|
70
|
+
# ...but only if we need to get anything at all.
|
|
71
|
+
(desired_page_size == 0) ? 0 : desired_page_size + 1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Indicates if we need to search in reverse or not in order to satisfy the Relay pagination args.
|
|
75
|
+
# If searching in reverse is necessary, `process_items_and_build_page_info` will take care of
|
|
76
|
+
# reversing the reversed results back to their original order.
|
|
77
|
+
def search_in_reverse?
|
|
78
|
+
# If `first` has been provided then we _must not_ search in reverse.
|
|
79
|
+
# The relay spec requires us to apply `first` before `last`, and searching
|
|
80
|
+
# in reverse would prevent us from being able to return the first `N`.
|
|
81
|
+
return false if first_n
|
|
82
|
+
|
|
83
|
+
# If we do not have to return the first N results, we are free to search in
|
|
84
|
+
# reverse if needed. Either `last` or `before` requires it.
|
|
85
|
+
last_n || before
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The cursor values to search after (if we need to search after one at all).
|
|
89
|
+
def search_after
|
|
90
|
+
search_in_reverse? ? before : after
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# In some cases, we're forced to search in reverse; in those caes, this is used to restore
|
|
94
|
+
# the ordering of the items to the intended order.
|
|
95
|
+
def restore_intended_item_order(items)
|
|
96
|
+
search_in_reverse? ? items.reverse : items
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Used for post-processing a list of items from a search result, truncating the list as needed. Truncation
|
|
100
|
+
# may be necessary because we may request an extra item as part of our pagination implementation.
|
|
101
|
+
def truncate_items(items)
|
|
102
|
+
# Remove the extra doc we requested by doing `size: size + 1`, if an extra was returned.
|
|
103
|
+
# Removing the first or last doc (as this will do) will signal to `bulid_page_info`
|
|
104
|
+
# that there definitely is a previous or next page.
|
|
105
|
+
# Note: we use `to_a` to satisfy steep, since `Array#[]` can return `nil`--but with the arg
|
|
106
|
+
# we pass, never does when items is non-empty, which our conditional enforces here.
|
|
107
|
+
items = items[search_in_reverse? ? 1..-1 : 0...-1].to_a if items.size > desired_page_size
|
|
108
|
+
|
|
109
|
+
# We can't always use `before` and `after` in the datastore query (such as when both are provided!),
|
|
110
|
+
# so here we drop items from the start that come on or before `after`, and items from the
|
|
111
|
+
# end that come on or after `before`.
|
|
112
|
+
if (after_cursor = after)
|
|
113
|
+
items = items.drop_while do |doc|
|
|
114
|
+
item_sort_values_satisfy?(yield(doc, after_cursor), :<=)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if (before_cursor = before)
|
|
119
|
+
items = items.take_while do |doc|
|
|
120
|
+
item_sort_values_satisfy?(yield(doc, before_cursor), :<)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# We are not always able to use `last` as the query `size` (such as when `first` is also provided)
|
|
125
|
+
# so here we apply `last`. If it has already been used this line will be a no-op.
|
|
126
|
+
items = (_ = items).last(last_n.to_i) if last_n
|
|
127
|
+
items
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def paginated_from_singleton_cursor?
|
|
131
|
+
before == DecodedCursor::SINGLETON || after == DecodedCursor::SINGLETON
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def desired_page_size
|
|
135
|
+
# The relay spec requires us to apply `first` before `last`, but if neither
|
|
136
|
+
# is provided we fall back to `default_page_size`.
|
|
137
|
+
@desired_page_size ||= [first_n || last_n || default_page_size, max_page_size].min.to_i
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def first_n
|
|
143
|
+
@first_n ||= size_arg_value(:first, first)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def last_n
|
|
147
|
+
@last_n ||= size_arg_value(:last, last)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def size_arg_value(arg_name, value)
|
|
151
|
+
if value && value < 0
|
|
152
|
+
raise ::GraphQL::ExecutionError, "`#{schema_element_names.public_send(arg_name)}` cannot be negative, but is #{value}."
|
|
153
|
+
else
|
|
154
|
+
value
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A bit like `Array#<=>`, but understands ascending vs descending sorts.
|
|
159
|
+
# We can't simply use doc_sort_values <=> cursor_sort_values` because our
|
|
160
|
+
# sort might mix ascending and descending sorts. So, we have to go value-by-value
|
|
161
|
+
# and compare each.
|
|
162
|
+
def item_sort_values_satisfy?(sort_values, comparison_operator)
|
|
163
|
+
if (first_unequal_sort_value = sort_values.find(&:unequal?))
|
|
164
|
+
# Since each subsequent sort field is a tie breaker that only gets used if two documents
|
|
165
|
+
# have the same values for all the prior sort fields, as soon as we find a sort value that
|
|
166
|
+
# is unequal we can just do the comparison based on it.
|
|
167
|
+
first_unequal_sort_value.item_satisfies_compared_to_cursor?(comparison_operator)
|
|
168
|
+
else
|
|
169
|
+
# The doc values and cursor values are all exactly equal. Return true or false on
|
|
170
|
+
# the basis of whether or not the comparison operator allows exact equality.
|
|
171
|
+
comparison_operator == :<= || comparison_operator == :>=
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
SortValue = ::Data.define(:from_item, :from_cursor, :sort_direction) do
|
|
176
|
+
# @implements SortValue
|
|
177
|
+
def unequal?
|
|
178
|
+
from_item != from_cursor
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def item_satisfies_compared_to_cursor?(comparison_operator)
|
|
182
|
+
if from_item.nil?
|
|
183
|
+
# nil values sort first when sorting ascending, and last when sorting descending.
|
|
184
|
+
# (see `DocumentPaginator#sort` for a more thorough explanation).
|
|
185
|
+
sort_direction == :asc
|
|
186
|
+
elsif from_cursor.nil?
|
|
187
|
+
# nil values sort first when sorting ascending, and last when sorting descending.
|
|
188
|
+
# (see `DocumentPaginator#sort` for a more thorough explanation).
|
|
189
|
+
sort_direction == :desc
|
|
190
|
+
else # both `from_item` and `from_cursor` are non-nil, and can be compared.
|
|
191
|
+
result = from_item.public_send(comparison_operator, from_cursor)
|
|
192
|
+
(sort_direction == :asc) ? result : !result
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Use of this source code is governed by an MIT-style
|
|
4
|
+
# license that can be found in the LICENSE file or at
|
|
5
|
+
# https://opensource.org/licenses/MIT.
|
|
6
|
+
#
|
|
7
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
require "elastic_graph/graphql/filtering/filter_value_set_extractor"
|
|
10
|
+
|
|
11
|
+
module ElasticGraph
|
|
12
|
+
class GraphQL
|
|
13
|
+
class DatastoreQuery
|
|
14
|
+
# Responsible for picking routing values for a specific query based on the filters.
|
|
15
|
+
class RoutingPicker
|
|
16
|
+
def initialize(schema_names:)
|
|
17
|
+
# @type var all_values_set: _RoutingValueSet
|
|
18
|
+
all_values_set = RoutingValueSet::ALL
|
|
19
|
+
|
|
20
|
+
@filter_value_set_extractor = Filtering::FilterValueSetExtractor.new(schema_names, all_values_set) do |operator, filter_value|
|
|
21
|
+
if operator == :equal_to_any_of
|
|
22
|
+
# This calls `.compact` to remove `nil` filter_value values
|
|
23
|
+
RoutingValueSet.of(filter_value.compact)
|
|
24
|
+
else # gt, lt, gte, lte, matches
|
|
25
|
+
# With one of these inexact/inequality operators, we don't have a way to precisely represent
|
|
26
|
+
# the set of values. Instead, we represent it with the special UnboundedWithExclusions
|
|
27
|
+
# implementation since when these operators are used the set is unbounded (there's an infinite
|
|
28
|
+
# number of values in the set) but it doesn't contain all values (it has some exclusions).
|
|
29
|
+
RoutingValueSet::UnboundedWithExclusions
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Given a list of `filter_hashes` and a list of `routing_field_paths`, returns a list of
|
|
35
|
+
# routing values that can safely be used to limit what index shards we search
|
|
36
|
+
# without risking missing any matching documents that could exist on other shards.
|
|
37
|
+
#
|
|
38
|
+
# If an eligible list of routing values cannot be determined, returns `nil`.
|
|
39
|
+
#
|
|
40
|
+
# Importantly, we have to be careful to not return routing values unless we are 100% sure
|
|
41
|
+
# that the set of values will route to the full set of shards on which documents matching
|
|
42
|
+
# the filters could live. If a document matching the filters lived on a shard that our
|
|
43
|
+
# search does not route to, it will not be included in the search response.
|
|
44
|
+
#
|
|
45
|
+
# Essentially, this method guarantees that the following pseudo code is always satisfied:
|
|
46
|
+
#
|
|
47
|
+
# ``` ruby
|
|
48
|
+
# if (routing_values = extract_eligible_routing_values(filter_hashes, routing_field_paths))
|
|
49
|
+
# Datastore.all_documents_matching(filter_hashes).each do |document|
|
|
50
|
+
# routing_field_paths.each do |field_path|
|
|
51
|
+
# expect(routing_values).to include(document.value_at(field_path))
|
|
52
|
+
# end
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
# ```
|
|
56
|
+
def extract_eligible_routing_values(filter_hashes, routing_field_paths)
|
|
57
|
+
@filter_value_set_extractor.extract_filter_value_set(filter_hashes, routing_field_paths).to_return_value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class RoutingValueSet < Data.define(:type, :routing_values)
|
|
62
|
+
# @dynamic ==
|
|
63
|
+
|
|
64
|
+
def self.of(routing_values)
|
|
65
|
+
new(:inclusive, routing_values.to_set)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.of_all_except(routing_values)
|
|
69
|
+
new(:exclusive, routing_values.to_set)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
ALL = of_all_except([])
|
|
73
|
+
|
|
74
|
+
def intersection(other_set)
|
|
75
|
+
# Here we return `self` to preserve the commutative property of `intersection`. Returning `self`
|
|
76
|
+
# here matches the behavior of `UnboundedWithExclusions.intersection`. See the comment there for
|
|
77
|
+
# rationale.
|
|
78
|
+
return self if other_set == UnboundedWithExclusions
|
|
79
|
+
|
|
80
|
+
# @type var other: RoutingValueSet
|
|
81
|
+
other = _ = other_set
|
|
82
|
+
|
|
83
|
+
if inclusive? && other.inclusive?
|
|
84
|
+
# Since both sets are inclusive, we can just delegate to `Set#intersection` here.
|
|
85
|
+
RoutingValueSet.of(routing_values.intersection(other.routing_values))
|
|
86
|
+
elsif exclusive? && other.exclusive?
|
|
87
|
+
# Since both sets are exclusive, we need to return an exclusive set of the union of the
|
|
88
|
+
# excluded values. For example, when dealing with positive integers:
|
|
89
|
+
#
|
|
90
|
+
# s1 = RoutingValueSet.of_all_except([1, 2, 3]) # > 3
|
|
91
|
+
# s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
|
|
92
|
+
#
|
|
93
|
+
# s3 = s1.intersection(s2)
|
|
94
|
+
#
|
|
95
|
+
# Here s3 would be all values > 5 (the same as `RoutingValueSet.of_all_except([1, 2, 3, 4, 5])`)
|
|
96
|
+
RoutingValueSet.of_all_except(routing_values.union(other.routing_values))
|
|
97
|
+
else
|
|
98
|
+
# Since one set is inclusive and one set is exclusive, we need to return an inclusive set of
|
|
99
|
+
# `included_values - excluded_values`. For example, when dealing with positive integers:
|
|
100
|
+
#
|
|
101
|
+
# s1 = RoutingValueSet.of([1, 2, 3]) # 1, 2, 3
|
|
102
|
+
# s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
|
|
103
|
+
#
|
|
104
|
+
# s3 = s1.intersection(s2)
|
|
105
|
+
#
|
|
106
|
+
# Here s3 would be just `1, 2`.
|
|
107
|
+
included_values, excluded_values = get_included_and_excluded_values(other)
|
|
108
|
+
RoutingValueSet.of(included_values - excluded_values)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def union(other_set)
|
|
113
|
+
# Here we return `other` to preserve the commutative property of `union`. Returning `other`
|
|
114
|
+
# here matches the behavior of `UnboundedWithExclusions.union`. See the comment there for
|
|
115
|
+
# rationale.
|
|
116
|
+
return other_set if other_set == UnboundedWithExclusions
|
|
117
|
+
|
|
118
|
+
# @type var other: RoutingValueSet
|
|
119
|
+
other = _ = other_set
|
|
120
|
+
|
|
121
|
+
if inclusive? && other.inclusive?
|
|
122
|
+
# Since both sets are inclusive, we can just delegate to `Set#union` here.
|
|
123
|
+
RoutingValueSet.of(routing_values.union(other.routing_values))
|
|
124
|
+
elsif exclusive? && other.exclusive?
|
|
125
|
+
# Since both sets are exclusive, we need to return an exclusive set of the intersection of the
|
|
126
|
+
# excluded values. For example, when dealing with positive integers:
|
|
127
|
+
#
|
|
128
|
+
# s1 = RoutingValueSet.of_all_except([1, 2, 3]) # > 3
|
|
129
|
+
# s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
|
|
130
|
+
#
|
|
131
|
+
# s3 = s1.union(s2)
|
|
132
|
+
#
|
|
133
|
+
# Here s3 would be all 1, 2, > 3 (the same as `RoutingValueSet.of_all_except([3])`)
|
|
134
|
+
RoutingValueSet.of_all_except(routing_values.intersection(other.routing_values))
|
|
135
|
+
else
|
|
136
|
+
# Since one set is inclusive and one set is exclusive, we need to return an exclusive set of
|
|
137
|
+
# `excluded_values - included_values`. For example, when dealing with positive integers:
|
|
138
|
+
#
|
|
139
|
+
# s1 = RoutingValueSet.of([1, 2, 3]) # 1, 2, 3
|
|
140
|
+
# s2 = RoutingValueSet.of_all_except([3, 4, 5]) # 1, 2, > 5
|
|
141
|
+
#
|
|
142
|
+
# s3 = s1.union(s2)
|
|
143
|
+
#
|
|
144
|
+
# Here s3 would be 1, 2, 3, > 5 (the same as `RoutingValueSet.of_all_except([4, 5])`)
|
|
145
|
+
included_values, excluded_values = get_included_and_excluded_values(other)
|
|
146
|
+
RoutingValueSet.of_all_except(excluded_values - included_values)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def negate
|
|
151
|
+
with(type: INVERTED_TYPES.fetch(type))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
INVERTED_TYPES = {inclusive: :exclusive, exclusive: :inclusive}
|
|
155
|
+
|
|
156
|
+
def to_return_value
|
|
157
|
+
# Elasticsearch/OpenSearch have no routing value syntax to tell it to avoid searching a specific shard
|
|
158
|
+
# (and the fact that we are excluding a routing value doesn't mean that other documents that
|
|
159
|
+
# live on the same shard with different routing values can't match!) so we return `nil` to
|
|
160
|
+
# force the datastore to search all shards.
|
|
161
|
+
return nil if exclusive?
|
|
162
|
+
|
|
163
|
+
routing_values.to_a
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
protected
|
|
167
|
+
|
|
168
|
+
def inclusive?
|
|
169
|
+
type == :inclusive
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def exclusive?
|
|
173
|
+
type == :exclusive
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def get_included_and_excluded_values(other)
|
|
179
|
+
inclusive? ? [routing_values, other.routing_values] : [other.routing_values, routing_values]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# This `RoutingValueSet` implementation is used for otherwise unrepresentable cases. We use it when
|
|
183
|
+
# a filter on one of the `routing_field_paths` uses an inequality like:
|
|
184
|
+
#
|
|
185
|
+
# {routing_field: {gt: "abc"}}
|
|
186
|
+
#
|
|
187
|
+
# In a case like that, the set is unbounded (there's an infinite number of values that are greater
|
|
188
|
+
# than `"abc"`...), but it's not `RoutingValueSet::ALL`--since it's based on an inequality, there are
|
|
189
|
+
# _some_ values that are excluded from the set. But we can't use `RoutingValueSet.of_all_except(...)`
|
|
190
|
+
# because the set of exclusions is also unbounded!
|
|
191
|
+
#
|
|
192
|
+
# When our filter value extraction results in this set, we must search all shards of the index and
|
|
193
|
+
# cannot pass any `routing` value to the datastore at all.
|
|
194
|
+
module UnboundedWithExclusions
|
|
195
|
+
# @dynamic self.==
|
|
196
|
+
|
|
197
|
+
def self.intersection(other)
|
|
198
|
+
# Technically, the "true" intersection would be `other - values_of(self)` but as we don't have
|
|
199
|
+
# any known values from this unbounded set, we just return `other`. It's OK to include extra values
|
|
200
|
+
# in the set (we'll search additional shards) but not OK to fail to include necessary values in
|
|
201
|
+
# the set (we'd avoid searching a shard that may have matching documents) so we err on the side of
|
|
202
|
+
# including more values.
|
|
203
|
+
other
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.union(other)
|
|
207
|
+
# Since our set here is unbounded, the resulting union is also unbounded. This errs on the side
|
|
208
|
+
# of safety since this set's `to_return_value` returns `nil` to cause the datastore to search
|
|
209
|
+
# all shards.
|
|
210
|
+
self
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def self.negate
|
|
214
|
+
# This here is the only difference in behavior of this set implementation vs `RoutingValueSet::ALL`.
|
|
215
|
+
# Where as `ALL.negate` returns an empty set, we treat `negate` as a no-op. We do that because the
|
|
216
|
+
# negation of an inexact unbounded set is still an inexact unbounded set. While it flips which values
|
|
217
|
+
# are in or out of the set, this object is still the representation in our datamodel for that case.
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def self.to_return_value
|
|
222
|
+
# Here we return `nil` to make sure that the datastore searches all shards, since we don't have
|
|
223
|
+
# any information we can use to safely limit what shards it searches.
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# `Query::RoutingPicker` exists only for use by `Query` and is effectively private.
|
|
230
|
+
private_constant :RoutingPicker
|
|
231
|
+
# `RoutingValueSet` exists only for use here and is effectively private.
|
|
232
|
+
private_constant :RoutingValueSet
|
|
233
|
+
|
|
234
|
+
# Steep is complaining that it can't find some `Query` but they are not in this file...
|
|
235
|
+
# @dynamic aggregations, shard_routing_values, search_index_definitions, merge_with, search_index_expression
|
|
236
|
+
# @dynamic with, to_datastore_msearch_header_and_body, document_paginator
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|