diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f21cd..f82860b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index fcb318b..1b2ba50 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/montycat/core/engine.py b/montycat/core/engine.py index 1532136..26e9280 100644 --- a/montycat/core/engine.py +++ b/montycat/core/engine.py @@ -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") @@ -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). @@ -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: diff --git a/montycat/store_classes/inmemory.py b/montycat/store_classes/inmemory.py index b644578..e8675a7 100644 --- a/montycat/store_classes/inmemory.py +++ b/montycat/store_classes/inmemory.py @@ -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. @@ -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): """ diff --git a/setup.py b/setup.py index b061bf4..8b4ad62 100644 --- a/setup.py +++ b/setup.py @@ -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 ' diff --git a/tests/test_engine_commands.py b/tests/test_engine_commands.py index 538b079..d2b708c 100644 --- a/tests/test_engine_commands.py +++ b/tests/test_engine_commands.py @@ -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(