Skip to content

Commit d112695

Browse files
committed
Merge branch 'fix/join-attrs-docs' into feature/spanner
2 parents 4eecee5 + 0c80039 commit d112695

File tree

2 files changed

+78
-32
lines changed

2 files changed

+78
-32
lines changed

docs/content/advanced/advanced_search.md

+43-12
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,48 @@ Advanced Search is a powerful feature that allows you to search across multiple
3131

3232
### Advanced Features
3333

34-
- **Search Chain**
35-
- Follow relationships between entries
36-
- Search through referenced objects
37-
- Chain multiple searches to traverse complex relationships
38-
- Results include both direct matches and related entries
39-
40-
- **Export Functionality**
41-
- Export search results to various formats
42-
- Asynchronous processing for large result sets
43-
- Progress tracking for export tasks
44-
- Download exported files when ready
34+
#### Join Attrs
35+
36+
Join Attrs enables relationship traversal in search results. Key points:
37+
38+
- **Implementation**
39+
- Sequential processing: root -> join targets
40+
- Each join triggers new Elasticsearch query
41+
- Supports OBJECT and ARRAY type references
42+
43+
- **Critical Considerations**
44+
1. **Pagination Behavior**
45+
```python
46+
# Example: Request 100 items
47+
root_results = search(limit=100) # Returns 100 root items
48+
joined_results = join_and_filter() # May return 0-100 items
49+
next_page_starts_at = 101 # Regardless of joined result size
50+
```
51+
- Pagination applies to root level only
52+
- Join/filter operations may reduce result size
53+
- Each page may return fewer items than requested
54+
55+
2. **Performance Impact**
56+
- N+1 query pattern with multiple joins
57+
- No optimization for deep joins with filters
58+
59+
3. **Result Count Accuracy**
60+
- Total count represents root level matches only
61+
- Actual result count may be lower after joins/filters
62+
- Cannot predict exact total after joins without full scan
63+
64+
#### Search Chain
65+
- Follow relationships between entries
66+
- Search through referenced objects
67+
- Chain multiple searches to traverse complex relationships
68+
- Results include both direct matches and related entries
69+
70+
#### Export Functionality
71+
72+
- Export search results to various formats
73+
- Asynchronous processing for large result sets
74+
- Progress tracking for export tasks
75+
- Download exported files when ready
4576

4677
## Access Methods
4778

@@ -87,7 +118,6 @@ Access Advanced Search programmatically through REST endpoints:
87118
- Leverage search chains for complex relationship queries
88119
- Monitor export task progress for large result sets
89120
- Consider pagination for large result sets in API usage
90-
91121
## For Developers
92122

93123
### Architecture Overview
@@ -173,3 +203,4 @@ Access Advanced Search programmatically through REST endpoints:
173203
- Integration tests for API endpoints
174204
- Performance tests for search operations
175205
- ACL verification tests
206+

entry/api_v2/views.py

+35-20
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
)
2828
from airone.lib.elasticsearch import (
2929
AdvancedSearchResultRecord,
30-
AdvancedSearchResultRecordAttr,
3130
AdvancedSearchResults,
3231
AttrHint,
3332
)
@@ -245,6 +244,11 @@ class AdvancedSearchAPI(generics.GenericAPIView):
245244
"""
246245
NOTE for now it's just copied from /api/v1/entry/search, but it should be
247246
rewritten with DRF components.
247+
248+
Join Attrs implementation notes:
249+
- Pagination is applied at root level first, then join & filter operations
250+
- This may result in fewer items than requested limit
251+
- Each join triggers a new ES query (N+1 pattern)
248252
"""
249253

250254
@extend_schema(
@@ -280,8 +284,18 @@ def _get_joined_resp(
280284
prev_results: list[AdvancedSearchResultRecord], join_attr: AdvancedSearchJoinAttrInfo
281285
) -> tuple[bool, AdvancedSearchResults]:
282286
"""
283-
This is a helper method for join_attrs that will get specified attr values
284-
that prev_result's ones refer to.
287+
Process join operation for a single attribute.
288+
289+
Flow:
290+
1. Get related entities from prev_results
291+
2. Extract referral IDs and names
292+
3. Execute new ES query for joined entities
293+
4. Apply filters if specified
294+
295+
Note:
296+
- Each call triggers new ES query
297+
- Results may be reduced by join filters
298+
- Pagination from root level may lead to incomplete results
285299
"""
286300
entities = Entity.objects.filter(
287301
id__in=[result.entity["id"] for result in prev_results]
@@ -369,21 +383,20 @@ def _get_joined_resp(
369383

370384
# === End of Function: _get_joined_resp() ===
371385

372-
def _get_ref_id_from_es_result(attrinfo):
373-
if attrinfo["type"] == AttrType.OBJECT:
374-
if attrinfo.get("value") is not None:
386+
def _get_ref_id_from_es_result(attrinfo) -> list[int | None]:
387+
match attrinfo["type"]:
388+
case AttrType.OBJECT if attrinfo.get("value") is not None:
375389
return [attrinfo["value"].get("id")]
376390

377-
if attrinfo["type"] == AttrType.NAMED_OBJECT:
378-
if attrinfo.get("value") is not None:
391+
case AttrType.NAMED_OBJECT if attrinfo.get("value") is not None:
379392
[ref_info] = attrinfo["value"].values()
380393
return [ref_info.get("id")]
381394

382-
if attrinfo["type"] == AttrType.ARRAY_OBJECT:
383-
return [x.get("id") for x in attrinfo["value"]]
395+
case AttrType.ARRAY_OBJECT:
396+
return [x.get("id") for x in attrinfo["value"]]
384397

385-
if attrinfo["type"] == AttrType.ARRAY_NAMED_OBJECT:
386-
return sum([[y["id"] for y in x.values()] for x in attrinfo["value"]], [])
398+
case AttrType.ARRAY_NAMED_OBJECT:
399+
return sum([[y["id"] for y in x.values()] for x in attrinfo["value"]], [])
387400

388401
return []
389402

@@ -461,11 +474,13 @@ def _get_ref_id_from_es_result(attrinfo):
461474

462475
if not settings.AIRONE_SPANNER_ENABLED:
463476
for join_attr in join_attrs:
477+
# Note: Each iteration here represents a potential N+1 query
478+
# The trade-off is between query performance and result accuracy
464479
(will_filter_by_joined_attr, joined_resp) = _get_joined_resp(
465480
resp.ret_values, join_attr
466481
)
467-
# Prepare blank joining info for entries without matches
468-
blank_joining_info: dict[str, AdvancedSearchResultRecordAttr] = {
482+
# This is needed to set result as blank value
483+
blank_joining_info = {
469484
"%s.%s" % (join_attr.name, k.name): {
470485
"is_readable": True,
471486
"type": AttrType.STRING,
@@ -484,11 +499,11 @@ def _get_ref_id_from_es_result(attrinfo):
484499
for x in joined_resp.ret_values
485500
}
486501

487-
# Insert results to previous search results
488-
new_ret_values = []
489-
joined_ret_values = []
502+
# this inserts result to previous search result
503+
new_ret_values: list[AdvancedSearchResultRecord] = []
504+
joined_ret_values: list[AdvancedSearchResultRecord] = []
490505
for resp_result in resp.ret_values:
491-
# Get referral info from joined search result
506+
# joining search result to original one
492507
ref_info = resp_result.attrs.get(join_attr.name)
493508

494509
# This get referral Item-ID from joined search result
@@ -504,12 +519,12 @@ def _get_ref_id_from_es_result(attrinfo):
504519

505520
else:
506521
# join EMPTY value
507-
resp_result.attrs |= blank_joining_info
522+
resp_result.attrs |= blank_joining_info # type: ignore
508523
joined_ret_values.append(deepcopy(resp_result))
509524

510525
if len(ref_list) == 0:
511526
# join EMPTY value
512-
resp_result.attrs |= blank_joining_info
527+
resp_result.attrs |= blank_joining_info # type: ignore
513528
joined_ret_values.append(deepcopy(resp_result))
514529

515530
if will_filter_by_joined_attr:

0 commit comments

Comments
 (0)