Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@ All notable changes to the Montycat Python client are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.3] - 2026-07-31

Adds a way to read the server's real semantic configuration, and a safe way to
change an enrolled keyspace's embedding model. Additive — upgrading from 1.1.2
requires no code changes.

### Added

- `Engine.get_semantic_status(store, keyspace)` returns the server's actual
semantic settings rather than what the caller assumed: the DB-wide switch and
default model, plus each enrolled keyspace's model, dimensions, field,
storage type, and whether a backfill is still pending.
- `Engine.reembed_semantic_search(model, store, keyspace, field)` atomically
drops one keyspace's vectors, records the new configuration, and starts a
complete backfill. It reports the previous model alongside the new one, so a
caller can confirm what it replaced.
- **`Keyspace.InMemory.do_snapshots_for_keyspace()`** — the method was spelled
`do_snaphots_for_keyspace` (missing `s`), the only client of the four to
misspell it. The correct spelling now exists; the old name is kept as a
deprecated alias so existing callers keep working.

### Changed

- Documented that `enable_semantic_search` leaves an already-enrolled keyspace
alone. It was never a way to switch models; `reembed_semantic_search` is.
Behavior is unchanged — only the documentation was misleading.
- Corrected the `disable_semantic_search` docs: `drop_vectors` is not "required
before switching to a different embedding model". Use
`reembed_semantic_search`, which does not leave the keyspace unsearchable in
between.

### Deprecated

- `Keyspace.InMemory.do_snaphots_for_keyspace()`. It forwards to
`do_snapshots_for_keyspace()` and will be removed in a future major release.

## [1.1.2] - 2026-07-29

Fixes a hang that affects any request whose payload contains the word
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,24 @@ keys = await Sales.semantic_search_get_keys("Show all Bluetooth devices", limit=
strong = await Sales.semantic_search_get_keys("Show all Bluetooth devices", limit=5, min_score=0.35)

# Control the DB-wide switch (optional — it's already on):
# switch the embedding model: 'minilm' | 'bge-small' (default) | 'bge-base' | 'e5-small'
await connection.enable_semantic_search(model=SemanticModel.BGE_BASE)
# Read back the model and backfill state actually assigned to a keyspace.
status = await connection.get_semantic_status(
store="catalog", keyspace="products"
)

# Enable an unenrolled keyspace with an explicit model.
await connection.enable_semantic_search(
model=SemanticModel.BGE_BASE,
store="catalog",
keyspace="products",
)

# Changing an enrolled keyspace is destructive and starts a full backfill.
await connection.reembed_semantic_search(
SemanticModel.BGE_BASE,
store="catalog",
keyspace="products",
)

# turn it off (vectors are kept so re-enabling resumes instantly;
# pass drop_vectors=True to also clear stored vectors)
Expand Down
41 changes: 37 additions & 4 deletions montycat/core/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,11 @@ async def enable_semantic_search(self, model: Optional[SemanticModel] = None, fi
None (DB-wide). If the DB-wide switch is off, a scoped
enable enrolls but nothing embeds until a DB-wide enable.

Returns:
Any: The server's response describing the enabled model and enrolled keyspaces.
Returns:
Any: The server's response describing the enabled model and enrolled keyspaces.

An already-enrolled keyspace is not modified. Use
``reembed_semantic_search`` to change its model or field.
"""
if keyspace and not store:
raise ValueError("A store is required when keyspace is specified")
Expand Down Expand Up @@ -270,8 +273,9 @@ async def disable_semantic_search(self, drop_vectors: bool = False, store: Union
Args:
drop_vectors (bool, optional): If True, also clear stored vectors — every
keyspace's DB-wide, or the scoped store's when
`store` is set. Required before switching to a
different embedding model. Default False.
`store` is set. Use
``reembed_semantic_search`` for model
replacement. Default False.
store (str, optional): Restrict the disable to this store only. Default None
(DB-wide).

Expand All @@ -290,6 +294,35 @@ async def disable_semantic_search(self, drop_vectors: bool = False, store: Union

return await self._execute_query_with_credentials(command)

async def get_semantic_status(
self,
store: Union[str, None] = None,
keyspace: Union[str, None] = None,
) -> Any:
"""Return actual global and per-keyspace semantic configuration."""
if keyspace and not store:
raise ValueError("A store is required when keyspace is specified")
command = ["get-semantic-status"]
if store:
command.extend(["store", store])
if keyspace:
command.extend(["keyspace", keyspace])
return await self._execute_query_with_credentials(command)

async def reembed_semantic_search(
self,
model: SemanticModel,
store: str,
keyspace: str,
field: Union[str, None] = None,
) -> Any:
"""Atomically replace a keyspace's vectors and start a full backfill."""
command = ["reembed-semantic-search", "model", model.value]
if field:
command.extend(["field", field])
command.extend(["store", store, "keyspace", keyspace])
return await self._execute_query_with_credentials(command)

async def policy_view(self, owner: Optional[str] = None, store: Optional[str] = None) -> Any:
command = ['policy-view']
if owner:
Expand Down
11 changes: 10 additions & 1 deletion montycat/store_classes/inmemory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class inmemory_kv:
persistent: bool = False

@classmethod
async def do_snaphots_for_keyspace(cls):
async def do_snapshots_for_keyspace(cls):
"""
Returns:
True if the snapshot operation was successful. Class 'str' if the snapshot operation failed.
Expand All @@ -26,6 +26,15 @@ async def do_snaphots_for_keyspace(cls):

return await cls._run_query(query)

@classmethod
async def do_snaphots_for_keyspace(cls):
"""Deprecated misspelled alias of :meth:`do_snapshots_for_keyspace`.

Kept so existing callers keep working; use the correctly spelled name.
"""

return await cls.do_snapshots_for_keyspace()

@classmethod
async def clean_snapshots_for_keyspace(cls):
"""
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='montycat',
version='1.1.2',
version='1.1.3',
description=(
'Self-hosted vector database + NoSQL with built-in AI semantic search — the async '
'Python client for Montycat. A Rust-powered, AI-native Pinecone / Weaviate / Chroma '
Expand Down
33 changes: 33 additions & 0 deletions tests/test_engine_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,43 @@ async def test_semantic_commands_and_scope_validation(self):
"products",
],
)
await self.assert_command(
self.engine.get_semantic_status(
store="catalog", keyspace="products"
),
[
"get-semantic-status",
"store",
"catalog",
"keyspace",
"products",
],
)
await self.assert_command(
self.engine.reembed_semantic_search(
SemanticModel.BGE_BASE,
store="catalog",
keyspace="products",
field="description",
),
[
"reembed-semantic-search",
"model",
"bge-base",
"field",
"description",
"store",
"catalog",
"keyspace",
"products",
],
)
with self.assertRaisesRegex(ValueError, "store is required"):
await self.engine.enable_semantic_search(keyspace="products")
with self.assertRaisesRegex(ValueError, "store is required"):
await self.engine.disable_semantic_search(keyspace="products")
with self.assertRaisesRegex(ValueError, "store is required"):
await self.engine.get_semantic_status(keyspace="products")

async def test_policy_read_commands(self):
await self.assert_command(
Expand Down
Loading