Skip to content

Commit a65437b

Browse files
committed
fix(memory): persist resolved index backend so auto+usearch snapshots reload correctly
1 parent cba1b7c commit a65437b

3 files changed

Lines changed: 47 additions & 1 deletion

File tree

livekit-memory/livekit/memory/_index.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
class VectorIndex(Protocol):
4444
"""Minimal index interface used by `MemoryStore`."""
4545

46+
# Concrete backend tag ("bruteforce" | "usearch"); persisted in snapshots so a
47+
# store created with backend="auto" reloads the same index type it resolved to.
48+
kind: str
49+
4650
@property
4751
def dims(self) -> int: ...
4852

@@ -66,6 +70,8 @@ class BruteForceIndex:
6670
partial top-k selection. Removal is O(1) swap-with-last to keep the matrix dense.
6771
"""
6872

73+
kind = "bruteforce"
74+
6975
def __init__(self, dims: int, *, initial_capacity: int = 1024) -> None:
7076
self._dims = int(dims)
7177
self._count = 0
@@ -157,6 +163,8 @@ class UsearchIndex:
157163
as similarity (1 - distance) to match `BruteForceIndex`.
158164
"""
159165

166+
kind = "usearch"
167+
160168
def __init__(
161169
self,
162170
dims: int,

livekit-memory/livekit/memory/store.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,13 @@ def __init__(
7575
"""
7676
self._embedder = coerce_embedder(embedder, dims)
7777
self._dims = self._embedder.dims
78-
self._backend_name = backend
7978
self._lock = threading.RLock()
8079

8180
self._collection: VectorIndex = make_index(self._dims, backend, expected_size=expected_size)
81+
# Record the *resolved* concrete backend ("bruteforce"|"usearch"), not the
82+
# configured one — "auto" depends on expected_size, which we don't persist, so
83+
# snapshots and clear() must rebuild from what was actually selected.
84+
self._backend_name = self._collection.kind
8285
# Facts are always exact and fully scanned — keep them brute-force regardless.
8386
self._facts: VectorIndex = BruteForceIndex(self._dims)
8487

tests/memory/test_store.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from __future__ import annotations
55

6+
import json
7+
68
import numpy as np
79
import pytest
810

@@ -11,6 +13,7 @@
1113
BruteForceIndex,
1214
HashingEmbedder,
1315
MemoryStore,
16+
UsearchIndex,
1417
)
1518

1619

@@ -116,6 +119,38 @@ def test_save_and_load(tmp_path):
116119
assert hits[0].metadata == {"k": 1}
117120

118121

122+
def test_snapshot_records_resolved_backend_not_auto(tmp_path):
123+
# backend="auto" must persist the concrete backend it resolved to, never "auto",
124+
# so load() rebuilds the right index type.
125+
store = make_store(backend="auto")
126+
store.save(tmp_path / "snap")
127+
with open(tmp_path / "snap" / "meta.json") as f:
128+
meta = json.load(f)
129+
assert meta["backend"] in ("bruteforce", "usearch")
130+
assert meta["backend"] != "auto"
131+
132+
133+
def test_usearch_auto_save_load_roundtrip(tmp_path):
134+
# Regression: backend="auto" + large expected_size selects UsearchIndex; the
135+
# snapshot must reload as UsearchIndex rather than crashing in BruteForceIndex.load.
136+
pytest.importorskip("usearch")
137+
emb = HashingEmbedder(dims=64)
138+
store = MemoryStore(embedder=emb, backend="auto", expected_size=200_000)
139+
assert isinstance(store._collection, UsearchIndex)
140+
141+
store.upsert("name", "user is Ada") # fact -> brute force
142+
for i in range(20):
143+
store.add(f"memory number {i} about topic {i % 5}")
144+
store.save(tmp_path / "snap")
145+
146+
loaded = MemoryStore.load(tmp_path / "snap", embedder=HashingEmbedder(dims=64))
147+
assert isinstance(loaded._collection, UsearchIndex)
148+
assert len(loaded) == 21
149+
assert loaded.all(namespace=FACTS_NAMESPACE)[0].text == "user is Ada"
150+
hits = loaded.search("memory number 7 about topic 2", limit=1)
151+
assert hits and "number 7" in hits[0].text
152+
153+
119154
def test_dimension_mismatch_rejected():
120155
store = make_store() # dims=64
121156
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)