@@ -240,6 +240,11 @@ class AdvancedSearchAPI(generics.GenericAPIView):
240
240
"""
241
241
NOTE for now it's just copied from /api/v1/entry/search, but it should be
242
242
rewritten with DRF components.
243
+
244
+ Join Attrs implementation notes:
245
+ - Pagination is applied at root level first, then join & filter operations
246
+ - This may result in fewer items than requested limit
247
+ - Each join triggers a new ES query (N+1 pattern)
243
248
"""
244
249
245
250
@extend_schema (
@@ -275,8 +280,18 @@ def _get_joined_resp(
275
280
prev_results : list [AdvancedSearchResultRecord ], join_attr : AdvancedSearchJoinAttrInfo
276
281
) -> tuple [bool , AdvancedSearchResults ]:
277
282
"""
278
- This is a helper method for join_attrs that will get specified attr values
279
- that prev_result's ones refer to.
283
+ Process join operation for a single attribute.
284
+
285
+ Flow:
286
+ 1. Get related entities from prev_results
287
+ 2. Extract referral IDs and names
288
+ 3. Execute new ES query for joined entities
289
+ 4. Apply filters if specified
290
+
291
+ Note:
292
+ - Each call triggers new ES query
293
+ - Results may be reduced by join filters
294
+ - Pagination from root level may lead to incomplete results
280
295
"""
281
296
entities = Entity .objects .filter (
282
297
id__in = [result .entity ["id" ] for result in prev_results ]
@@ -364,21 +379,20 @@ def _get_joined_resp(
364
379
365
380
# === End of Function: _get_joined_resp() ===
366
381
367
- def _get_ref_id_from_es_result (attrinfo ):
368
- if attrinfo ["type" ] == AttrType . OBJECT :
369
- if attrinfo .get ("value" ) is not None :
382
+ def _get_ref_id_from_es_result (attrinfo ) -> list [ int | None ] :
383
+ match attrinfo ["type" ]:
384
+ case AttrType . OBJECT if attrinfo .get ("value" ) is not None :
370
385
return [attrinfo ["value" ].get ("id" )]
371
386
372
- if attrinfo ["type" ] == AttrType .NAMED_OBJECT :
373
- if attrinfo .get ("value" ) is not None :
387
+ case AttrType .NAMED_OBJECT if attrinfo .get ("value" ) is not None :
374
388
[ref_info ] = attrinfo ["value" ].values ()
375
389
return [ref_info .get ("id" )]
376
390
377
- if attrinfo [ "type" ] == AttrType .ARRAY_OBJECT :
378
- return [x .get ("id" ) for x in attrinfo ["value" ]]
391
+ case AttrType .ARRAY_OBJECT :
392
+ return [x .get ("id" ) for x in attrinfo ["value" ]]
379
393
380
- if attrinfo [ "type" ] == AttrType .ARRAY_NAMED_OBJECT :
381
- return sum ([[y ["id" ] for y in x .values ()] for x in attrinfo ["value" ]], [])
394
+ case AttrType .ARRAY_NAMED_OBJECT :
395
+ return sum ([[y ["id" ] for y in x .values ()] for x in attrinfo ["value" ]], [])
382
396
383
397
return []
384
398
@@ -443,6 +457,8 @@ def _get_ref_id_from_es_result(attrinfo):
443
457
total_count = deepcopy (resp .ret_count )
444
458
445
459
for join_attr in join_attrs :
460
+ # Note: Each iteration here represents a potential N+1 query
461
+ # The trade-off is between query performance and result accuracy
446
462
(will_filter_by_joined_attr , joined_resp ) = _get_joined_resp (resp .ret_values , join_attr )
447
463
# This is needed to set result as blank value
448
464
blank_joining_info = {
@@ -465,8 +481,8 @@ def _get_ref_id_from_es_result(attrinfo):
465
481
}
466
482
467
483
# this inserts result to previous search result
468
- new_ret_values = []
469
- joined_ret_values = []
484
+ new_ret_values : list [ AdvancedSearchResultRecord ] = []
485
+ joined_ret_values : list [ AdvancedSearchResultRecord ] = []
470
486
for resp_result in resp .ret_values :
471
487
# joining search result to original one
472
488
ref_info = resp_result .attrs .get (join_attr .name )
0 commit comments