Skip to content

Commit 5a906ef

Browse files
Yuning Bianjixua
authored andcommitted
fix(parse): 修正稀疏向量 bucket 来源校验
1 parent 28df491 commit 5a906ef

2 files changed

Lines changed: 43 additions & 14 deletions

File tree

src/core/sparse_vector/indexing.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
- chunks 中每条的 ``dense_vector_status`` 必须是 ``SUCCESS``——业务硬约束:sparse
1313
向量追加在 dense point 上,dense 没成功就不能跑 sparse。本模块在入口前置断言
1414
(fail-fast)兜底;多值 CAS 只能保护 ``sparse_vector_status`` 维度,拦不住这条前置条件。
15-
- chunks 自带 ``bucket_id``,本模块从首条取作权威;不再接受外部 ``bucket_id`` 入参
16-
(顺手关闭 GitHub issue #95:旧实现误把 ``payload.dataset_id`` 当作 bucket_id)。
15+
- chunks 自带 ``bucket_id``,本模块从首条取作权威,并 fail-fast 校验同批一致;
16+
不再接受外部 ``bucket_id`` 入参(顺手关闭 GitHub issue #95:旧实现误把
17+
``payload.dataset_id`` 当作 bucket_id)。
1718
- 空集短路:传入 chunks 为空(调用方过滤后无待处理)→ 幂等 no-op SUCCESS。
1819
"""
1920

@@ -120,16 +121,22 @@ async def run(
120121
)
121122

122123
# ③ bucket_id 从 chunks 自带字段取(同文档下由写入路径保证一致),不再外部入参。
123-
# 下游 Qdrant 按 bucket_id 路由 collection;不一致属于上游 bug。ORM 字段名义
124-
# 类型为 int | None,但 chunking 阶段 bulk_insert_pending 要求 bucket_id 必填,
125-
# 运行期不可能为 None;显式断言收紧类型并给出可定位的失败原因。
124+
# 下游 Qdrant 按 bucket_id 路由 collection;不一致属于上游 bug,直接 fail-fast。
125+
# ORM 字段名义类型为 int | None,但 chunking 阶段 bulk_insert_pending 要求
126+
# bucket_id 必填,运行期不可能为 None;显式断言收紧类型并给出可定位的失败原因。
126127
first_bucket_id = records[0].bucket_id
127128
if first_bucket_id is None:
128129
raise SparseIndexingError(
129-
"SPARSE_VECTORIZING_FAILED:missing_bucket_id;"
130-
f"chunk_id={records[0].chunk_id}"
130+
"SPARSE_VECTORIZING_FAILED:missing_bucket_id;" f"chunk_id={records[0].chunk_id}"
131131
)
132132
bucket_id = int(first_bucket_id)
133+
inconsistent = [r for r in records if r.bucket_id is None or int(r.bucket_id) != bucket_id]
134+
if inconsistent:
135+
sample = inconsistent[0]
136+
raise SparseIndexingError(
137+
"SPARSE_VECTORIZING_FAILED:bucket_id_mismatch;"
138+
f"expected={bucket_id},actual={sample.bucket_id},chunk_id={sample.chunk_id}"
139+
)
133140

134141
# ④ 分批编排:encode → Qdrant upsert → mark INDEXED;任一批失败抛文件级异常。
135142
service = self._get_sparse_vector_service()
@@ -199,9 +206,7 @@ async def _run_batch(
199206
)
200207

201208
# 4.3 写 Qdrant:先 ensure schema,再 upsert sparse vectors。
202-
await store.ensure_sparse_vector_schema(
203-
bucket_id=bucket_id, vector_name=vector_name
204-
)
209+
await store.ensure_sparse_vector_schema(bucket_id=bucket_id, vector_name=vector_name)
205210
points = [
206211
sparse_indexed_point_from_record(row, vec, vector_name=vector_name)
207212
for row, vec in zip(batch, vectors)
@@ -228,9 +233,7 @@ async def _run_batch(
228233
await self._safe_mark_failed(db, chunk_ids, reason=str(exc), task_id=task_id)
229234
raise
230235
except Exception as exc:
231-
reason = (
232-
f"SPARSE_VECTORIZING_FAILED:{type(exc).__name__}: {exc}"
233-
)
236+
reason = f"SPARSE_VECTORIZING_FAILED:{type(exc).__name__}: {exc}"
234237
await self._safe_mark_failed(db, chunk_ids, reason=reason, task_id=task_id)
235238
raise SparseIndexingError(reason) from exc
236239

tests/unit/core/sparse_vector/test_sparse_indexing_pipeline.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
- 空集短路:传入空 chunks → 幂等 no-op,不触达 encoder / Qdrant。
88
- 前置断言:任一 chunk ``dense_vector_status != SUCCESS`` → fail-fast 抛
99
SparseIndexingError(多值 CAS 拦不住"dense 没成功就跑 sparse"这条前置条件)。
10-
- bucket_id 从 chunks[0] 自带字段取(关闭 #95:旧实现误传 dataset_id)。
10+
- bucket_id 从 chunks[0] 自带字段取,并 fail-fast 校验同批一致(关闭 #95:旧实现
11+
误传 dataset_id)。
1112
- 批处理用多值 CAS ``allowed_statuses=(PENDING, FAILED)`` 切 INDEXING。
1213
"""
1314

@@ -148,6 +149,31 @@ async def test_missing_bucket_id_raises():
148149
assert "missing_bucket_id" in exc.value.reason
149150

150151

152+
@pytest.mark.asyncio
153+
async def test_mixed_bucket_ids_raise_fail_fast():
154+
repo = _RecordingRepo()
155+
store = _RecordingStore()
156+
pipeline = SparseIndexingPipeline(
157+
chunk_repository=repo,
158+
sparse_vector_service=_RecordingService(),
159+
qdrant_store=store,
160+
)
161+
162+
rows = [
163+
_row(chunk_id="c1", bucket_id=42),
164+
_row(chunk_id="c2", bucket_id=43),
165+
]
166+
with pytest.raises(SparseIndexingError) as exc:
167+
await pipeline.run(chunks=rows, task_id="t1", db=_FakeDB())
168+
169+
assert "bucket_id_mismatch" in exc.value.reason
170+
assert "expected=42" in exc.value.reason
171+
assert "actual=43" in exc.value.reason
172+
assert repo.sparse_indexing_calls == []
173+
assert store.ensured == []
174+
assert store.upserts == []
175+
176+
151177
@pytest.mark.asyncio
152178
async def test_happy_path_extracts_bucket_id_and_uses_multivalue_cas(monkeypatch):
153179
monkeypatch.setattr(

0 commit comments

Comments
 (0)