Skip to content

fix(vector-store): create payload indexes even when the collection exists - #1578

Merged
wanghy73 merged 1 commit into
MemMachine:speedkickfrom
wanghy73:control/speedkick-1532
Sep 3, 2026
Merged

wanghy73 merged 1 commit into
MemMachine:speedkickfrom
wanghy73:control/speedkick-1532

Conversation

@wanghy73

@wanghy73 wanghy73 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The defect

_create_native_collection wrapped create_collection and every
create_payload_index
in one try, then swallowed "already exists" for the whole
block. A collection that already existed therefore raised on the first call, took the
already-exists path, and was left with no payload indexes at all — despite the
docstring promising both were created idempotently.

Two creators are easy to arrive at. The guarding lock is keyed on the
AsyncQdrantClient object:

_name_locks: ClassVar[WeakKeyDictionary[AsyncQdrantClient, defaultdict[tuple[str, str], asyncio.Lock]]]

so it serialises callers inside one process and nothing across them. With
MEMMACHINE_WORKERS above 1 each worker has its own client and its own lock. A crash
between the two calls leaves the same state.

The change

The collection and the indexes now sit under separate guards, and each index is created
individually and tolerant of already-exists.

Evidence

Against Qdrant 1.19 in testcontainers.

Before — test_indexes_are_created_when_the_collection_already_exists fails, and not
with one index missing:

AssertionError: the tenant partition index is missing: a collection that already
existed never had its payload indexes created ... present: []
assert 'sys-partition_key' in set()

payload_schema is empty — none of the twelve.

After — both new tests pass, along with the rest of the suite: 293 unit, 142
integration
. ruff and ruff format clean.

Scope, checked rather than assumed

A filtered query on an unindexed collection returns only the matching tenant's points, so
what a missing sys-partition_key index costs is the multitenant storage layout and
query speed, not isolation
. I verified this directly rather than reasoning from
is_tenant=True.

Only the already-exists path reproduces. Two clients creating simultaneously both
succeed, which the second test pins.

What a reviewer needs to know

These tests are marked integration and need a real server — local-mode Qdrant ignores
payload indexes, so the defect is invisible there. CI will not exercise them, since
addopts = ["-m", "not integration"]. To run them:

pytest packages/server/server_tests/memmachine_server/common/vector_store/test_qdrant_vector_store.py \
  -m integration -k TestCollectionLifecycleAcrossWorkers

Docker required; the fixture starts its own Qdrant.

Not affected

The mm-tb6 benchmark collection was checked and holds all twelve payload indexes
including sys-partition_key, covering every point — no measurement ran against an
unindexed collection.

…ists

_create_native_collection wrapped create_collection and every
create_payload_index in one try and swallowed "already exists" for the whole
block. A collection that already existed therefore raised on the first call,
took the already-exists path, and was left with no payload indexes at all -
despite the docstring promising both were created idempotently.

Two creators are easy to arrive at. The guarding lock is keyed on the
AsyncQdrantClient object, so it serialises callers inside one process and
nothing across them; with MEMMACHINE_WORKERS above 1 each worker has its own
client and its own lock. A crash between the two calls leaves the same state.

The collection and the indexes now sit under separate guards, and each index
is created individually and tolerant of already-exists.

Verified against Qdrant 1.19 in testcontainers. Before the change,
test_indexes_are_created_when_the_collection_already_exists fails with an
empty payload_schema - not a missing index, none of the twelve. After it, both
new tests pass, along with the rest of the vector store suite: 293 unit and
142 integration.

Scope, checked rather than assumed: a filtered query on an unindexed
collection returns only the matching tenant's points, so what a missing
sys-partition_key index costs is the multitenant storage layout and query
speed, not isolation. Note also that only the already-exists path reproduces;
two clients creating simultaneously both succeed, which the second test pins.

The tests need a real server and are marked integration - local-mode Qdrant
ignores payload indexes, so the defect is invisible there and CI, which runs
with `-m "not integration"`, will not exercise them.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nr9kacmpFVTTfkZRw6esxP

@edwinyyyu edwinyyyu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up: The implementation needs to change completely to properly support horizontal scalability without sharding. Also having dynamic schema/index creation is not good, so that should likely change as well.

@wanghy73
wanghy73 merged commit a2a1754 into MemMachine:speedkick Sep 3, 2026
25 of 39 checks passed
@wanghy73
wanghy73 deleted the control/speedkick-1532 branch September 3, 2026 18:11
This was referenced Sep 16, 2026
wanghy73 added a commit to wanghy73/MemMachine that referenced this pull request Sep 18, 2026
…ists (MemMachine#1578)

_create_native_collection wrapped create_collection and every
create_payload_index in one try and swallowed "already exists" for the whole
block. A collection that already existed therefore raised on the first call,
took the already-exists path, and was left with no payload indexes at all -
despite the docstring promising both were created idempotently.

Two creators are easy to arrive at. The guarding lock is keyed on the
AsyncQdrantClient object, so it serialises callers inside one process and
nothing across them; with MEMMACHINE_WORKERS above 1 each worker has its own
client and its own lock. A crash between the two calls leaves the same state.

The collection and the indexes now sit under separate guards, and each index
is created individually and tolerant of already-exists.

Verified against Qdrant 1.19 in testcontainers. Before the change,
test_indexes_are_created_when_the_collection_already_exists fails with an
empty payload_schema - not a missing index, none of the twelve. After it, both
new tests pass, along with the rest of the vector store suite: 293 unit and
142 integration.

Scope, checked rather than assumed: a filtered query on an unindexed
collection returns only the matching tenant's points, so what a missing
sys-partition_key index costs is the multitenant storage layout and query
speed, not isolation. Note also that only the already-exists path reproduces;
two clients creating simultaneously both succeed, which the second test pins.

The tests need a real server and are marked integration - local-mode Qdrant
ignores payload indexes, so the defect is invisible there and CI, which runs
with `-m "not integration"`, will not exercise them.

Claude-Session: https://claude.ai/code/session_01Nr9kacmpFVTTfkZRw6esxP

Co-authored-by: Claude Opus 5 <[email protected]>
(cherry picked from commit a2a1754)
Signed-off-by: Haiyan Wang <[email protected]>
malatewang pushed a commit that referenced this pull request Sep 18, 2026
…ists (port of #1578 to main) (#1681)

* fix(vector-store): create payload indexes even when the collection exists (#1578)

_create_native_collection wrapped create_collection and every
create_payload_index in one try and swallowed "already exists" for the whole
block. A collection that already existed therefore raised on the first call,
took the already-exists path, and was left with no payload indexes at all -
despite the docstring promising both were created idempotently.

Two creators are easy to arrive at. The guarding lock is keyed on the
AsyncQdrantClient object, so it serialises callers inside one process and
nothing across them; with MEMMACHINE_WORKERS above 1 each worker has its own
client and its own lock. A crash between the two calls leaves the same state.

The collection and the indexes now sit under separate guards, and each index
is created individually and tolerant of already-exists.

Verified against Qdrant 1.19 in testcontainers. Before the change,
test_indexes_are_created_when_the_collection_already_exists fails with an
empty payload_schema - not a missing index, none of the twelve. After it, both
new tests pass, along with the rest of the vector store suite: 293 unit and
142 integration.

Scope, checked rather than assumed: a filtered query on an unindexed
collection returns only the matching tenant's points, so what a missing
sys-partition_key index costs is the multitenant storage layout and query
speed, not isolation. Note also that only the already-exists path reproduces;
two clients creating simultaneously both succeed, which the second test pins.

The tests need a real server and are marked integration - local-mode Qdrant
ignores payload indexes, so the defect is invisible there and CI, which runs
with `-m "not integration"`, will not exercise them.

Claude-Session: https://claude.ai/code/session_01Nr9kacmpFVTTfkZRw6esxP

Co-authored-by: Claude Opus 5 <[email protected]>
(cherry picked from commit a2a1754)
Signed-off-by: Haiyan Wang <[email protected]>

* style: reformat an assert ruff 0.15.14 formats differently (speedkick) (#1592)

style: reformat an assert ruff 0.15.14 formats differently

`ruff format --check` fails on speedkick as it stands: the pinned ruff
(0.15.14, from the root pyproject the lint workflow reads its version
from) moves the message of a multi-line `assert` onto its own line, and
one assert in the Qdrant store tests predates that. No behavior change.

Every PR targeting speedkick inherits the red format check until this
lands, which is why it is on its own rather than folded into one of them.

Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit c2d234b)
Signed-off-by: Haiyan Wang <[email protected]>

---------

Signed-off-by: Haiyan Wang <[email protected]>
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Edwin Yu <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants