fix(vector): address PR review round 16 — type-aware index check, comments
Detect pre-existing payload indexes with the wrong schema type in `_ensure_payload_indexes`. The previous "field already in existing_schema → skip" branch silently survived a collection migrated from the int-doc_id era where `doc_id` is indexed as INTEGER, letting `MatchValue(value="123")` searches keep failing with HTTP 400 on Qdrant Cloud strict mode — exactly the production failure this PR was meant to fix. New behaviour: compare `existing_schema[field].data_type` against the declared type; on mismatch log a WARNING and append to `failed_fields` so the consolidated end-of-function summary picks it up. No auto-repair (operator intervention only — see docs/configuration.md recovery procedure). New test exercises the doc_id-INTEGER scenario end-to-end and asserts both the per-field WARNING and the summary line. Clarify the `_verify_news_items` malformed-doc_id rationale: the news API has no per-item endpoint, so a malformed doc_id genuinely cannot be verified against the source of truth. We err toward false-positive (keep) over false-negative (drop) — same conservative posture as `_verify_notes` and `_verify_deck_cards`. The producer-side validation is the real security boundary; the verifier is defence-in-depth. Both the inline comment and the WARNING message now spell this out. Add a TODO in `get_last_indexed_timestamp` flagging the O(N) cost on every incremental sync tick. The previous single-page `limit=10_000` silently bounded the scroll; paginating fixed correctness but made the unbounded cost visible. The follow-up tracker (canonical TODO at `api/visualization.py`) covers migrating the max-`indexed_at` to a sentinel point or collection metadata for O(1) lookup. Consolidate the duplicate non-numeric-doc_type TODOs at `api/visualization.py:508` and `auth/viz_routes.py:570` into a single canonical comment in `visualization.py`; `viz_routes.py` is reduced to a back-reference. Removes the rot risk of "fixed in one place, forgotten in the other." The canonical comment also references the O(1) timestamp follow-up in `scanner.py`. Document the `batch_size = 256` (qdrant_client.py) vs `_DELETION_TRACKING_PAGE_SIZE = 1024` (scanner.py) split with cross-referencing comments at each site: the smaller batch is for the read-write backfill upsert path (Qdrant accepts ~256-point chunks comfortably); the larger page is for read-only deletion-tracking scrolls where no per-page write round-trip applies. Replace `assert qdrant_client is not None` in `scan_user_documents` with `cast(AsyncQdrantClient, qdrant_client)` plus an explanatory comment. `assert` is silently elided under `-O`; `cast` is the conventional zero-cost narrower for branches the type checker can't infer from the surrounding `if not initial_sync` ternary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
68506f96c5
commit
8f4f5c0079
@@ -182,9 +182,31 @@ async def _ensure_payload_indexes(
|
||||
failed_fields: list[str] = []
|
||||
for field, schema_type in _PAYLOAD_INDEX_FIELDS.items():
|
||||
if field in existing_schema:
|
||||
# Index already present — silent skip. Logging here on every
|
||||
# restart would be noise that hides the genuinely interesting
|
||||
# "first-time creation" line in _create_one_payload_index.
|
||||
# Index already present. Confirm the existing schema type matches
|
||||
# what we'd create — a pre-existing collection with `doc_id`
|
||||
# indexed as INTEGER (the bug this PR fixes) would otherwise
|
||||
# silently survive here, and searches using
|
||||
# MatchValue(value="123") would keep failing with HTTP 400 on
|
||||
# Qdrant Cloud strict mode. Compare via PayloadSchemaType
|
||||
# equality; PayloadIndexInfo.data_type is the same enum
|
||||
# we wrote with.
|
||||
existing_info = existing_schema[field]
|
||||
existing_type = getattr(existing_info, "data_type", None)
|
||||
if existing_type is not None and existing_type != schema_type:
|
||||
logger.warning(
|
||||
"Payload index on '%s' has wrong schema type "
|
||||
"(got %s, expected %s); searches filtering on this "
|
||||
"field will fail with HTTP 400 until the index is "
|
||||
"dropped and recreated. See docs/configuration.md "
|
||||
"for the recovery procedure.",
|
||||
field,
|
||||
getattr(existing_type, "name", existing_type),
|
||||
schema_type.name,
|
||||
)
|
||||
failed_fields.append(field)
|
||||
# Either way, skip the create call: a matching index needs no
|
||||
# work, and a mismatch must not be auto-repaired (operator
|
||||
# intervention only — see docs/configuration.md).
|
||||
continue
|
||||
if not await _create_one_payload_index(
|
||||
client, collection_name, field, schema_type
|
||||
@@ -338,6 +360,12 @@ async def _backfill_doc_id_to_string(
|
||||
# Qdrant scroll returns next_offset as PointId | None — keep it untyped here
|
||||
# so the qdrant client's full union (UUID/int/str/PointId) flows through.
|
||||
next_offset = None
|
||||
# Smaller than ``_DELETION_TRACKING_PAGE_SIZE = 1024`` in
|
||||
# ``vector/scanner.py`` because this is a read-write path: every batch
|
||||
# is followed by a ``set_payload`` upsert, and 256-point upserts are
|
||||
# the working size where Qdrant comfortably accepts writes without
|
||||
# timing out under load. The scanner-side scroll has no per-page write
|
||||
# round-trip, so it can use a larger page.
|
||||
batch_size = 256
|
||||
# Log progress every N batches so a long-running migration on a large
|
||||
# collection (≥ 50k points) doesn't look like a startup hang. At batch
|
||||
|
||||
Reference in New Issue
Block a user