Skip to content

Commit 45ed375

Browse files
rdmmfSegmondFaultclaude
committed
fix(search): keep unfiltered function pages in stable id order (#17)
* Avoid Lua full-set reads for unfiltered function lists * fix(search): keep unfiltered function pages in stable id order The unfiltered function-list path added in #13 paged straight off SSCAN, which has no stable ordering. Consecutive pages could overlap or skip rows, and the same request could return a different page each time — the old Lua path sorted by id ascending. There is no id-ordered index to range over (save_function only does SADD on {col}:all_functions), so bound the scan instead: collect up to SORT_SCAN_CAP members, sort by id, slice the page, and report anything beyond the cap via the existing pool_truncated flag. The scan stays incremental, so it never holds Kvrocks the way the SMEMBERS-in-Lua path did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(search): keep unfiltered function pages in stable id order The unfiltered function-list path added in #13 paged straight off SSCAN, which has no stable ordering. Consecutive pages could overlap or skip rows, and the same request could return a different page each time — the old Lua path sorted by id ascending. Use the same shape as the unfiltered paths in search_file and search_feature: load the set, sort in Python, slice. The hang in #12 came from doing this inside Lua under the Kvrocks global EVAL lock, which #13 already removed; from Python the sibling routes have done it this way all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: SegmondFault <izaakalfredgray@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent bcb7fb0 commit 45ed375

1 file changed

Lines changed: 17 additions & 20 deletions

File tree

bsimvis/app/routes/search_function.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,21 @@
1919
MAX_POOL_LIMIT = 1000000
2020

2121

22-
def _sscan_page(r, key, offset, limit):
23-
cursor = 0
24-
seen = 0
25-
doc_ids = []
26-
scan_count = max(100, min(1000, offset + limit))
27-
28-
while True:
29-
cursor, batch = r.sscan(key, cursor=cursor, count=scan_count)
30-
for doc_id in batch:
31-
if seen >= offset and len(doc_ids) < limit:
32-
doc_ids.append(doc_id)
33-
seen += 1
34-
if len(doc_ids) >= limit:
35-
break
36-
if cursor == 0 or len(doc_ids) >= limit:
37-
break
38-
39-
return doc_ids
22+
def _id_sorted_page(r, key, offset, limit):
23+
"""Id-sorted page of an unordered set, matching the Lua tiebreak.
24+
25+
Same shape as the unfiltered paths in search_file/search_feature: load the
26+
set, sort in Python, slice. SSCAN can't replace this — it gives no stable
27+
ordering, so paging off it makes pages overlap and skip.
28+
29+
ponytail: O(N) full-set load only on the unfiltered path, as in the sibling
30+
routes. all_functions is the biggest of the three sets, so if any of them
31+
needs a maintained id index, this one goes first.
32+
"""
33+
doc_ids = sorted(
34+
d.decode() if isinstance(d, bytes) else str(d) for d in r.smembers(key)
35+
)
36+
return doc_ids[offset : offset + limit]
4037

4138

4239
def search_functions():
@@ -464,9 +461,9 @@ def _paths_for_source(source_lvl, field):
464461
else r.zrange(sort_key, offset, offset + limit - 1)
465462
)
466463
else:
467-
doc_ids = _sscan_page(r, all_key, offset, limit)
464+
doc_ids = _id_sorted_page(r, all_key, offset, limit)
468465
else:
469-
doc_ids = _sscan_page(r, all_key, offset, limit)
466+
doc_ids = _id_sorted_page(r, all_key, offset, limit)
470467
else:
471468
# Lua Exec
472469
search_script = lua_manager.get_script("search_function")

0 commit comments

Comments
 (0)