In-memory VectorBackend and the brute-force similarity it searches with.
Split out of :mod:symfonic.memory.backends.in_memory (378 lines against the
300-line budget). That module implemented two unrelated protocols in one file:
GraphBackend, which stores nodes and edges, and VectorBackend, which
stores embeddings. Only the vector half ever used _cosine_similarity, so
the two leave with nothing shared between them.
in_memory re-exports both names, so existing imports are unchanged.
InMemoryVectorBackend
In-memory implementation of the VectorBackend protocol.
Stores embeddings in a list and uses brute-force cosine similarity
for search. Suitable for testing only.
Source code in src/symfonic/memory/backends/in_memory_vector.py
| def __init__(self) -> None:
self._store: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
add
async
add(scope: StorageScope, ids: list[str], embeddings: list[list[float]], metadatas: list[dict[str, Any]], documents: list[str]) -> None
Add vectors with metadata to the store.
v8.0: stamps the materialised scope_path into the metadata bag so
the prefix-isolation filter (§5.d) can run on every search.
Source code in src/symfonic/memory/backends/in_memory_vector.py
| async def add(
self,
scope: StorageScope,
ids: list[str],
embeddings: list[list[float]],
metadatas: list[dict[str, Any]],
documents: list[str],
) -> None:
"""Add vectors with metadata to the store.
v8.0: stamps the materialised ``scope_path`` into the metadata bag so
the prefix-isolation filter (§5.d) can run on every search.
"""
scope_path = materialise_scope_path(scope)
from symfonic.memory.backends.in_memory_ownership import native_owner
native = _is_native_scope(scope)
wanted = set(ids)
for entry in self._store.get(scope.tenant_id, []):
owner = metadata_scope_path(entry["metadata"], scope.tenant_id, scope=scope)
if entry["id"] in wanted and (
owner != scope_path if native else native_owner(owner)
):
from symfonic.memory.errors import VectorOwnershipConflict
raise VectorOwnershipConflict("Vector ID already belongs to a different scope")
if not native and any(native_owner(meta.get(SCOPE_PATH_KEY)) for meta in metadatas):
raise ValueError("Legacy metadata cannot declare a native scope owner")
added = []
for i, doc_id in enumerate(ids):
meta = dict(metadatas[i]) if i < len(metadatas) else {}
if native:
meta[SCOPE_PATH_KEY] = scope_path
else:
meta.setdefault(SCOPE_PATH_KEY, scope_path)
entries = added if native else self._store[scope.tenant_id]
entries.append({
"id": doc_id,
"embedding": embeddings[i],
"metadata": meta,
"document": documents[i] if i < len(documents) else "",
})
if native:
replacements = {row["id"]: row for row in added}
self._store[scope.tenant_id] = [
row for row in self._store.get(scope.tenant_id, ())
if row["id"] not in replacements
] + list(replacements.values())
|
add_owned_batch
async
add_owned_batch(entries: list[tuple[Any, str, list[float], dict, str]]) -> None
Validate every native owner before changing vectors; never awaits.
Source code in src/symfonic/memory/backends/in_memory_vector.py
| async def add_owned_batch(self, entries: list[tuple[Any, str, list[float], dict, str]]) -> None:
"""Validate every native owner before changing vectors; never awaits."""
from symfonic.memory.errors import VectorOwnershipConflict
proposed: dict[str, list[dict[str, Any]]] = defaultdict(list)
for scope, identifier, embedding, metadata, document in entries:
if not _is_native_scope(scope):
raise ValueError("owned vector batch requires native scopes")
path = materialise_scope_path(scope)
for row in (*self._store.get(scope.tenant_id, ()), *proposed[scope.tenant_id]):
if row["id"] == identifier and metadata_scope_path(
row["metadata"], scope.tenant_id, scope=scope,
) != path:
raise VectorOwnershipConflict("Vector ID already belongs to a different scope")
proposed[scope.tenant_id].append({
"id": identifier, "embedding": embedding,
"metadata": {**metadata, SCOPE_PATH_KEY: path}, "document": document,
})
for tenant, rows in proposed.items():
replacements = {row["id"]: row for row in rows}
self._store[tenant] = [
row for row in self._store.get(tenant, ())
if row["id"] not in replacements
] + list(replacements.values())
|
count
async
count(scope: StorageScope) -> int
Return the total number of stored vectors for the tenant.
Source code in src/symfonic/memory/backends/in_memory_vector.py
| async def count(self, scope: StorageScope) -> int:
"""Return the total number of stored vectors for the tenant."""
entries = self._store.get(scope.tenant_id, [])
if not _is_native_scope(scope):
from symfonic.memory.backends.in_memory_ownership import native_owner
return sum(not native_owner(entry["metadata"].get(SCOPE_PATH_KEY))
for entry in entries)
return sum(is_visible(
metadata_scope_path(entry["metadata"], scope.tenant_id, scope=scope),
scope.ancestor_prefix_paths(),
) for entry in entries)
|
delete
async
delete(scope: StorageScope, ids: list[str]) -> None
Delete vectors by their IDs.
Source code in src/symfonic/memory/backends/in_memory_vector.py
| async def delete(self, scope: StorageScope, ids: list[str]) -> None:
"""Delete vectors by their IDs."""
from symfonic.memory.backends.in_memory_ownership import native_owner
id_set = set(ids)
if scope.tenant_id in self._store:
self._store[scope.tenant_id] = [
e for e in self._store[scope.tenant_id] if e["id"] not in id_set
or (not _is_native_scope(scope) and native_owner(
e["metadata"].get(SCOPE_PATH_KEY),
))
or (_is_native_scope(scope) and not is_visible(
metadata_scope_path(e["metadata"], scope.tenant_id, scope=scope),
scope.ancestor_prefix_paths(),
))
]
|
search
async
search(scope: StorageScope, query_embedding: list[float], top_k: int = 5, *, allowed_scope_paths: tuple[str, ...] | None = None) -> list[dict[str, Any]]
Brute-force cosine similarity search.
v8.0: ALWAYS-ON prefix-isolation (§5.d) — only entries whose stored
scope_path is a prefix of the query path are scored. Dual-reads
pre-v8.0 metadata (no scope_path) as a 1-level root path.
Source code in src/symfonic/memory/backends/in_memory_vector.py
| async def search(
self,
scope: StorageScope,
query_embedding: list[float],
top_k: int = 5,
*,
allowed_scope_paths: tuple[str, ...] | None = None,
) -> list[dict[str, Any]]:
"""Brute-force cosine similarity search.
v8.0: ALWAYS-ON prefix-isolation (§5.d) — only entries whose stored
``scope_path`` is a prefix of the query path are scored. Dual-reads
pre-v8.0 metadata (no ``scope_path``) as a 1-level root path.
"""
entries = self._store.get(scope.tenant_id, [])
prefixes = scope.ancestor_prefix_paths()
from symfonic.memory.backends.in_memory_ownership import native_owner
scored = []
native = _is_native_scope(scope)
for entry in entries:
entry_path = metadata_scope_path(entry["metadata"], scope.tenant_id, scope=scope)
if not native and native_owner(entry_path):
continue
if (native or allowed_scope_paths is None) and not is_visible(entry_path, prefixes):
continue
if allowed_scope_paths is not None and entry_path not in allowed_scope_paths:
continue
sim = _cosine_similarity(query_embedding, entry["embedding"])
scored.append({
"id": entry["id"],
"score": sim,
"metadata": entry["metadata"],
"document": entry["document"],
})
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k]
|