@@ -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
180305end
0 commit comments