Skip to content

Commit 857ff62

Browse files
committed
added support for flattening nested filters in merge strategty of parameters
1 parent 209de33 commit 857ff62

File tree

1 file changed

+126
-1
lines changed

1 file changed

+126
-1
lines changed

lib/chewy/search/parameters.rb

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,21 @@ def except!(names)
9393
#
9494
# @see Chewy::Search::Parameters::Storage#merge!
9595
# @return [{Symbol => Chewy::Search::Parameters::Storage}] storages from other parameters
96+
# Merges storages from another `parameters` instance into this one.
97+
# This method iterates over each storage defined in the other instance and merges
98+
# them into the current instance based on the type of storage (query, filter, or post_filter).
99+
#
100+
# @param [ChewyParametersExtensions] other The other instance to merge storages from.
96101
def merge!(other)
102+
97103
other.storages.each do |name, storage|
98-
modify!(name) { merge!(storage) }
104+
# Handle query-related storages with a specialized merge function
105+
if name.to_sym.in?([:query, :filter, :post_filter])
106+
merge_queries_and_filters(name, storage)
107+
else
108+
# For other types of storages, use a general purpose merge method
109+
modify!(name) { merge!(storage) }
110+
end
99111
end
100112
end
101113

@@ -175,6 +187,119 @@ def render_query(replace_post_filter: false)
175187
{query: {bool: filter}}
176188
end
177189
end
190+
191+
private
192+
193+
# Smartly wraps a query in a bool must unless it is already correctly structured.
194+
# This method helps maintain logical grouping and avoid unnecessary nesting in queries.
195+
#
196+
# @param [Hash, Array] query The query to wrap.
197+
# @return [Hash] The wrapped or original query.
198+
#
199+
# Example:
200+
# input: { term: { status: 'active' } }
201+
# output: { bool: { must: [{ term: { status: 'active' } }] } }
202+
#
203+
# input: { bool: { must: [{ term: { status: 'active' } }] } }
204+
# output: { bool: { must: [{ term: { status: 'active' } }] } }
205+
def smart_wrap_in_bool_must(query)
206+
return nil if query.nil?
207+
query = query.deep_symbolize_keys if query.is_a?(Hash)
208+
209+
# Normalize to ensure it's always in an array form for 'must' unless already properly formatted.
210+
normalized_query = query.is_a?(Array) ? query : [query]
211+
212+
# Check if the query already has a 'bool' structure
213+
if query.is_a?(Hash) && query.key?(:bool)
214+
# Check the components of the 'bool' structure
215+
has_only_must = query[:bool].key?(:must) && query[:bool].keys.size == 1
216+
217+
# If it has only a 'must' and nothing else, use it as is
218+
if has_only_must
219+
query
220+
else
221+
# If it contains other components like 'should' or 'must_not', wrap in a new 'bool' 'must'
222+
{ bool: { must: normalized_query } }
223+
end
224+
else
225+
# If no 'bool' structure is present, wrap the query in a 'bool' 'must'
226+
{ bool: { must: normalized_query } }
227+
end
228+
end
229+
230+
231+
# Combines two boolean queries into a single well-formed boolean query without redundant nesting.
232+
#
233+
# @param [Hash] query1 The first query component.
234+
# @param [Hash] query2 The second query component.
235+
# @return [Hash] A combined boolean query.
236+
#
237+
# Example:
238+
# query1: { bool: { must: [{ term: { status: 'active' } }] } }
239+
# query2: { bool: { must: [{ term: { age: 25 } }] } }
240+
# result: { bool: { must: [{ term: { status: 'active' } }, { term: { age: 25 } }] } }
241+
def merge_bool_queries(query1, query2)
242+
# Extract the :must components, ensuring they are arrays. ideally this should be the case anyway
243+
# but this is a safety check for cases like OrganizationChartFilter where the query is not properly formatted.
244+
# Eg index.query(
245+
# {
246+
# bool: {
247+
# must: {
248+
# term: {
249+
# has_org_chart_note: has_org_chart_note
250+
# }
251+
# },
252+
# }
253+
# }
254+
# )
255+
must1 = ensure_array(query1.dig(:bool, :must))
256+
must2 = ensure_array(query2.dig(:bool, :must))
257+
258+
# Combine the arrays; if both are empty, wrap the entire queries as fallback.
259+
if must1.empty? && must2.empty?
260+
{ bool: { must: [query1, query2].compact } } # Use compact to remove any nils.
261+
else
262+
{ bool: { must: must1 + must2 } }
263+
end
264+
end
265+
266+
267+
# Merges queries or filters from two different storages into a single storage efficiently.
268+
#
269+
# @param [Symbol] name The type of storage (query, filter, post_filter).
270+
# @param [Storage] other_storage The storage object from another instance.
271+
def merge_queries_and_filters(name, other_storage)
272+
current_storage = self.storages[name]
273+
# other_storage = other.storages[name]
274+
# Render each storage to get the DSL
275+
current_query = smart_wrap_in_bool_must(current_storage.render&.[](name))
276+
other_query = smart_wrap_in_bool_must(!other_storage.render.nil? ? other_storage.render[name] : nil)
277+
278+
if current_query && other_query
279+
# Custom merging logic for queries and filters
280+
281+
# Combine rendered queries inside a single bool must
282+
combined_storage = merge_bool_queries(current_query, other_query)
283+
284+
self.storages[name].replace!(combined_storage) # Directly set the modified storage
285+
else
286+
# Default merge if one is nil
287+
replacement_query = current_query || other_query
288+
if replacement_query
289+
self.storages[name].replace!(replacement_query)
290+
end
291+
end
292+
end
293+
294+
# Helper to ensure the :must key is always an array
295+
def ensure_array(value)
296+
case value
297+
when Array
298+
value # Already an array, no changes needed.
299+
when Hash, nil
300+
[value].compact # Wrap hashes or non-nil values in an array, remove nils.
301+
end
302+
end
178303
end
179304
end
180305
end

0 commit comments

Comments
 (0)