本地向量引擎 scroll 忽略 offset 参数,导致记录数>1 的 URI 覆盖写永久 500
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
Research direction
Start with openviking/storage/viking_vector_index_backend.py: read scroll() around lines 1055–1077 and _strict_scan() around lines 1822–1874, then trace how the local vector engine handles strict_query ordering and offset. Reproduce the repeated-page behavior with the debug scroll endpoint described in the issue. Done means pagination reaches distinct records and URI replacement succeeds; the linked pull request #7471 is already open.
Written by the indexing model from the issue text.
Description
环境
- openviking v0.5.4.1(v0.5.4 同样复现)
- 自建单节点,Docker,Linux x86_64,local vectordb backend
- 单 collection
context,indexdefault,1024 维 - 数据规模:53,003 条向量记录
现象
对记录数 > 1 的 URI 执行覆盖写(POST /api/v1/content/write,mode=replace)持续返回 HTTP 500,且不可自愈:
Incremental inventory under viking://.../README.md was incomplete; retrying full inventory attempt=1/3
error=Incremental inventory under viking://.../README.md ended after 0 of 1 records
...
openviking.server.app - ERROR - [request_id=...] Unhandled exception
File ".../openviking/server/routers/content.py", line 296, in write
RuntimeError: Incremental inventory under viking://.../README.md ended after 0 of 1 records
关键特征:删除该 URI 后首次写入成功,第二次(覆盖写)必失败,形成"假恢复"。
根因
openviking/storage/viking_vector_index_backend.py:1055-1077 的 scroll() 用 offset 游标分页:
async def scroll(self, filter=None, limit=100, cursor=None, output_fields=None):
offset = int(cursor) if cursor else 0
records = await self.strict_query(
filter=filter, limit=limit, offset=offset, output_fields=output_fields,
order_by="updated_at", order_desc=False,
)
next_cursor = str(offset + len(records)) if len(records) == limit else None
return records, next_cursor
_strict_scan()(同文件 1822-1874)先取 strict_count(bitmap 聚合)作为 expected_count,再逐页 scroll;页空或游标耗尽即抛 ended after X of Y records。
实测证明:本地向量引擎忽略了 offset 参数。
可复现证据(只读 debug 接口)
# A) offset 是否生效(limit=5,offset=0 vs offset=5 应返回不同记录)
GET /api/v1/debug/vector/scroll?limit=5&offset=0
-> ['d35311db','f41eb525','e04cdfa1','a8c1ede4','dcbc1895']
GET /api/v1/debug/vector/scroll?limit=5&offset=5
-> ['d35311db','f41eb525','e04cdfa1','a8c1ede4','dcbc1895']
identical: True # offset 被完全忽略
# B) 对照组:limit 生效,排除接口整体失效
limit=1 -> returned 1 records
limit=5 -> returned 5 records
limit=50 -> returned 50 records
# C) 分页穷尽测试(limit=200,翻 400 页)
collection total count: 53003
scroll reached: 80000 unique ids: 200 pages: 400
gap: 52803 # 永远只能拿到第一页的 200 条
失败链路
strict_count走 bitmap 聚合,正确返回expected=2(旧 + 新记录)scroll第 1 页(offset=0)返回 1 条,next_cursor="1"- 第 2 页(offset=1)又返回同一条,去重后
scanned_count仍为 1 - 游标耗尽时
scanned_count(1) != expected_count(2)→ 抛cursor ended after 1 of 2 records get_incremental_inventory_under_uri重试 3 次全失败 → 向上抛 →content.write500
为什么"删除后首次写回"能成功
删除后该 URI 只剩 1 条记录,expected=1,第一页即拿满,无需翻页 → 通过。
一旦覆盖写产生第 2 条(旧 + 新),必须翻页 → 必然失败。
影响面
- 所有记录数 > 1 的 URI 的覆盖写都会 500(content.write / 资源更新 / 部分 add_resource 收尾)
- 全库 53,003 条记录,scroll 最多只能读到 200 条
- 相关派生现象:
Candidate data is None for label index ... (label: 18446744073709551615)警告、_strict_scanfail-closed 报错、reindex报cursor ended after 4 of 7 - v0.5.4 → v0.5.4.1 升级未修复
疑点
scroll() 注释写道:
The local engine's scalar sorter does not return records for path/string fields.
updated_atis an indexed date-time field on every context collection and provides stable offset pages.
说明作者已知本地引擎 sorter 存在字段限制。实测表明 offset 本身也不被本地引擎的 sorter 尊重,怀疑:
order_by="updated_at"+ offset 的组合在本地引擎未实现;- 或 offset 参数在 adapter → native 层丢失(类似
delete_field_data failed, offset %d is invalid when element_size is %lu那条日志里%d/%lu未格式化、参数在 native→logging 桥接时丢失的问题)。
建议修复方向
- 直接用本地引擎 API 验证
strict_query(order_by=..., offset=N)是否返回不同页; - 若 offset 不可用,改用基于
updated_at值的 range 游标(updated_at > last_updated_at)而非 offset 分页; - 或在
_strict_scan中对"同页重复"做显式检测,报更明确的错误(当前报错指向"记录缺失",误导排查方向)。
临时规避
- 对受影响 URI:删除后只写一次,不要覆盖写;
- 或写入新路径,旧路径保留只读;
- 无服务端配置可绕过(
_strict_scan是 fail-closed 保护)。
- Dominant language
- Python
- Stars
- 41.8k
- Forks
- 3.4k
- Avg merge
- 19h 29m
- Merged PRs (30d)
- 500
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from volcengine/OpenViking
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
volcengine/OpenViking#7485 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
volcengine/OpenViking#7210 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
volcengine/OpenViking#7205 · 1 comment ·
Maintainers usually reply within 1 day
-
[Bug]: Retrieval rerank fallback statistics stay zero after provider failurePossibly taken @97Three claimed this 8 days ago. Openbug kernel
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
volcengine/OpenViking#7014 ·
Maintainers usually reply within 1 day
-
[Bug]: memory.prefetch_search_topn is inert above 5 (call site omits limit), plus two docstrings state the wrong defaultsPossibly taken @peterzhang987723 claimed this 8 days ago. Openkernel
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
volcengine/OpenViking#6923 · 2 comments ·
Maintainers usually reply within 1 day
All issues in volcengine/OpenViking
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
FinanceFlash/unvibecode#699 ·
Maintainers usually reply within 1 day
-
enhancement Website
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OWASP/OWASP-Hunting#96 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
micropython/micropython#19942 ·
Maintainers usually reply within 1 day
-
A rebuild posts "not part of this build" success to the commit's failed `nix-build` gate and effectsPossibly taken @Padraic-O-Mhuiris claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
`epoch.Integer` raises a raw OverflowError for integers too large to convert to a floatPossibly taken @arindam77 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
pydantic/pydantic-extra-types#479 ·
Maintainers usually reply within 1 day