# Montycat — Full Documentation > Montycat is a self-hosted, Rust-powered NoSQL + vector database with built-in AI > semantic search. Records and their vector embeddings live in one engine, with > on-device embeddings and no external vector database or embedding API — the > retrieval and memory layer for RAG, AI agents, and LLM apps, and an open-source > alternative to Pinecone, Weaviate, Chroma, and Qdrant. Clients: Python, > TypeScript/JavaScript, Dart/Flutter, and Rust. This file is the complete Montycat documentation as a single plain-text document for LLMs and agents, generated from the docs source. Code examples are shown in Python (the flagship client); the same methods exist in every client — see "Cross-language naming" at the end. Every call is asynchronous and returns `{"status": bool, "payload": , "error": }`. Default ports: 21210 (main), 21211 (subscriptions, always main + 1). Homepage: https://montygovernance.com — Docs: https://montygovernance.com/docs By use case: - Vector database (self-hosted): https://montygovernance.com/vector-database - Semantic search: https://montygovernance.com/semantic-search - RAG database: https://montygovernance.com/rag - AI agent memory: https://montygovernance.com/ai-memory - MCP server for shared agent memory: https://montygovernance.com/mcp-server - Pinecone / Weaviate / Chroma alternative: https://montygovernance.com/pinecone-alternative - Qdrant alternative (Montycat vs Qdrant): https://montygovernance.com/qdrant-alternative - NoSQL data mesh: https://montygovernance.com/data-mesh ## Installation _Read online: https://montygovernance.com/docs/installation_ ### Editions: Montycat & Montycat Semantic Montycat ships in **two editions**: - **Montycat Semantic** — the full engine **plus built-in AI semantic (vector) search**: on-device embeddings and an HNSW vector index, enabled by default. No external embedding services, no separate vector database. Pick this one for RAG, AI agents, and LLM memory. - **Montycat** — the lean core engine: persistent + in-memory keyspaces, subscriptions, snapshots, the full NoSQL data mesh, without the embedding runtime. Smallest footprint. The edition is selected at install time — a Docker **tag** (*semantic* instead of *latest*) or the apt package name ( *montycat-semantic* instead of *montycat*). Client libraries and the API are identical for both. ### Install Montycat Semantic (AI / RAG) **Recommended for AI, RAG, and agent memory.** Semantic search is **enabled by default** — no external embedding service, no separate vector database, nothing to wire up. Use the Docker tag **semantic** or the apt package **montycat-semantic**. Docker: ```bash docker pull montygovernance/montycat:semantic # Hardened (distroless): docker pull montygovernance/montycat:semantic-hardened # Apple Silicon / ARM64: docker pull montygovernance/montycat:arm64-semantic docker run -d \ -e MONTYCAT_SNAPSHOT_RATE=60 \ # default value is 0 seconds (snapshot disabled) -e MONTYCAT_SUPEROWNER=superowner \ # will setup on container startup then safely returns false -e MONTYCAT_PASSWORD=password \ # will setup on container startup then safely returns false -e MONTYCAT_EXPIRATION_RATE=10 \ # default value is 1 second -e MONTYCAT_REPORTS_ALLOWED=true \ # default value is true -p 21210:21210 \ # main port -p 21211:21211 \ # subscription notifications port is always main port + 1 (default 21211) -v ./data:/var/lib/.montycat \ # mount host data directory --name montycat_instance \ montygovernance/montycat:semantic ``` Debian / apt: ```bash # AMD64 architecture only (for ARM64 use the Docker image above) curl -fsSL https://repo-deb.montygovernance.com/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/montycat-archive-keyring.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/montycat-archive-keyring.gpg] https://repo-deb.montygovernance.com stable main" | sudo tee /etc/apt/sources.list.d/montycat.list > /dev/null # Semantic edition (built-in AI vector search, enabled by default): sudo apt update && sudo apt install -y montycat-semantic ``` ### Install Montycat (base) The lean core engine, without the embedding runtime — the smallest image. Use the Docker tag **latest** or the apt package **montycat**. Docker: ```bash docker pull montygovernance/montycat:latest # Hardened (distroless): docker pull montygovernance/montycat:hardened # Apple Silicon / ARM64: docker pull montygovernance/montycat:arm64 docker run -d \ -e MONTYCAT_SNAPSHOT_RATE=60 \ # default value is 0 seconds (snapshot disabled) -e MONTYCAT_SUPEROWNER=superowner \ # will setup on container startup then safely returns false -e MONTYCAT_PASSWORD=password \ # will setup on container startup then safely returns false -e MONTYCAT_EXPIRATION_RATE=10 \ # default value is 1 second -e MONTYCAT_REPORTS_ALLOWED=true \ # default value is true -p 21210:21210 \ # main port -p 21211:21211 \ # port for subscription notifications is always PORT + 1 (default 21211) -v ./data:/var/lib/.montycat \ # mount host data directory --name montycat_instance \ montygovernance/montycat:latest ``` Debian / apt: ```bash # AMD64 architecture only curl -fsSL https://repo-deb.montygovernance.com/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/montycat-archive-keyring.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/montycat-archive-keyring.gpg] https://repo-deb.montygovernance.com stable main" | sudo tee /etc/apt/sources.list.d/montycat.list > /dev/null sudo apt update && sudo apt install -y montycat ``` ### Hardened images > Every edition also ships a **hardened** Docker image built on **distroless** bases ( *distroless/static-debian12* for base, *distroless/base-debian13* for semantic). They contain **no shell and no package manager**, for a reduced production attack surface — the same engine, a smaller image. > Tags: **hardened** and **semantic-hardened** (plus the ARM64 variants **arm64-hardened** and **arm64-semantic-hardened**). Because there is no shell, debug with *docker logs* rather than *docker exec*. ### ARM64 / Apple Silicon > The default (unprefixed) tags — **latest**, **hardened**, **semantic**, **semantic-hardened** — are **linux/amd64**. For **ARM64 / Apple Silicon**, use the **arm64-**prefixed equivalents: **arm64**, **arm64-hardened**, **arm64-semantic**, and **arm64-semantic-hardened**. > The **apt** repository is **amd64 only** — on ARM64 hosts run the ARM64 Docker image instead. ## How to get started? _Read online: https://montygovernance.com/docs/how-to-get-started_ ### CLI To **start** Montycat run: **montycat hunt** . Command will start the Montycat process. Database engine will be initialized and available on **0.0.0.0:21210**. Subscription server is listening to **0.0.0.0:21211**. To **stop** Montycat run: **montycat home**. Command will stop the Montycat process gracefully including all running background tasks. To **get help** run: **montycat help**. ``` montycat hunt montycat home montycat help ``` Before proceeding with queries you have to **setup superowner credentials**. Otherwise the engine will decline requests. Typically returns *true* or *false* with error description. ``` montycat create-superowner username password ``` ### Setup connection First install the Montycat client library ``` #for Node based JavaScript/TypeScript clients npm install montycat --save #for Python clients pip install montycat #for Dart/Flutter clients dart pub add montycat #for Rust clients cargo add montycat ``` To setup connection instantiate a **class Engine**. The **store** is an optional property so you can define it later but make sure you define it prior to calling to database. ```python # connection.py from montycat import Engine connection = Engine( host="", port=, username="", password="", store="" ) ``` ### From uri Alternatively, you can instantiate a class using **ready-to-go method** and pass a connection string as an argument. ```python # connection.py from montycat import Engine connection = Engine.from_uri("montycat://:@:/") ``` ### Define a keyspace In Montycat, a **Keyspace** is like a namespace or logical database where your data lives. It defines how and where data is stored — either **persistent** on disk or **in memory**. Under the hood, each **Keyspace** is automatically divided into smaller logical units called **Volumes**, depending on your system parameters such as RAM, disk space, and CPU cores. Volumes are self-managing and require no manual intervention. #### Persistent Keyspace Persistent storage is based on an LSM tree and provides **WAL (Write-Ahead Logging), durability, atomicity, and consistency** . Use this when you want data to survive restarts and remain available long-term. #### In-Memory Keyspace In-Memory storage Handles data that lives entirely in memory by default (with optional snapshots for persistence). It based on lock-free thread-safe hash table with time complexity O(1) so multiple clients can have concurrent read/write access. It is extremely fast - capable of processing **over 1,000,000 structured read/write operations per second** . ```python # keyspaces.py from montycat import Keyspace from connection import connection #connection.py class Employees(Keyspace.Persistent): keyspace = "Employees" class EmployeesInMem(Keyspace.InMemory): keyspace = "EmployeesInMem" Employees.connect_engine(connection) EmployeesInMem.connect_engine(connection) ``` ### Cache and compression When defining a persistent keyspace, you can specify additional optional parameters such as cache and compression. Those options allow you to fine-tune storage performance and resource usage for persistent workloads. #### Cache Must be an *integer* value representing the cache size in megabytes (MB). The **minimum allowed value is 10 MB**. If not provided, the **default value is 10 MB**. #### Compression A *boolean* flag (*true* or *false*). Defaults to *false* if not specified. ```python from montycat import Keyspace class Employees(Keyspace.Persistent): keyspace = "Employees" cache = 100 compression = True ``` ### Define a schema Montycat is a NoSQL database with a flexible **data mesh architecture**. It offers a high degree of flexibility: you can work with schema definitions anywhere along the spectrum between a simple key-value store and a strict SQL-like engine. #### Schema Modes in Montycat - **Schemaless** - no schemas. - **Schema-like (Hybrid)** - schema defined on the client side. - **Schemaful** - schema defined on the client side and enforced on database level with relation to the particular keyspace. A schema is optional (so you can save any values such as string, integers, floats, complex types) but highly recommended to keep data well-structured and organized — especially in microservice architectures where multiple services interact with Montycat. Each Keyspace in Montycat **supports multiple schemas** (both enforced and non-enforced) and keeps track of them transparently. The base **Schema class** defines a structured schema for records stored inside a Montycat Keyspace. A schema describes the shape of the data: what fields exist, their types, and their intended usage. This ensures records remain consistent, strongly typed, and self-describing inside the database. > **Note for Rust Client:** schema definitions need **serde** with its **derive** feature alongside **montycat**, because structs derive **Serialize** and **Deserialize**: **cargo add serde --features derive**. The **RuntimeSchema** derive itself comes from **montycat** — no extra crate is required. ```python from montycat import Schema class EmployeesSchema(Schema): username: string location: string age: number ``` ### Setup runtime migrations Montycat clients support **runtime-safe migrations**, eliminating the need to execute migrations from a separate file. Migrations can be executed automatically on each application startup. If a migration has already been applied, the operation will not be repeated. Instead, the client will typically return a response with key-value pair **{status: false}**: ```python # migrations.py from keyspaces import Employees, EmployeesInMem #keyspaces.py async def migrate(): res1 = await Employees.create_keyspace() res2 = await EmployeesInMem.create_keyspace() return res1, res2 # {"status": True, "payload": None, "error": None} ``` ### Create a Keyspace using CLI Alternatively you can use **CLI** command to create a keyspace. You will be prompted to provide your credentials. Expected return *true* or *false* (with error descripton). > CLI typically requires **superowner** privileges. ``` montycat console > create-keyspace store keyspace exit ``` If you put **persistent** in the end, the keyspace you are going to create will be **persistent**. **Cache** is an optional argument. Has to be **more than 10 MB**, if set to a value less than 10 MB **will fall back to 10 MB**. The last argument is also optional. It represents **compression** and if present will set **compression** to *true*. ``` montycat console > create-keyspace store keyspace persistent cache 100 compression exit ``` ### Run first query Client's calls to Montycat are **asynchronous**, so make sure you **await the completion**. Montycat always returns structured, already deserialized and parsed results. All methods are identical across keyspaces, regardless of their type. > **Note:** If you are using **schema and ORM**, you must serialize the data before running a query by calling the built-in method **.serialize()**. > > Rust Client does not require explicit serialization and handles schemas natively with derive macro **#[derive(RuntimeSchema)]**. > **Note for Rust Client:** Struct **MontycatResponse** introduced to represent responses from the database engine. Struct has a method **.parse_response()** to handle the parsing and deserialization of responses. **MontycatResponse** can represent responses with different payload types by using Rust's generics feature, serde_json::Value or exact types. Can be used as following: > **MontycatResponse::>::parse_response(res)** > where **res** is the raw response bytes from the database and **>** is the expected payload type. > > **.parse_response()** method returns Result type so you may expect to handle potential errors during parsing. > > **This approach allows handling various response structures flexibly and type-safely** . ```python # main.py from schemas import EmployeesSchema #schemas.py from keyspaces import Employees #keyspaces.py async def query(): record = EmployeesSchema( username="Name", location="Location", age=21 ).serialize() res1 = await Employees.insert_value(record) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} res2 = await Employees.get_value(key=res1['payload']) # {"status": True, "payload": {"username": "Name", "location": "Location", "age": 21}, "error": None} ``` ### Semantic search Beyond exact lookups, Montycat can rank items by **meaning** using built-in vector search — the retrieval layer for RAG and AI-agent memory. On the **Montycat Semantic** edition it is **enabled by default**, so you can search a keyspace right away, no setup required. Full details, models, and the enable/disable switch are in the [Semantic Search](/docs/semantic-search) section. ```python from keyspaces import Employees #keyspaces.py # Rank stored items by MEANING, not keywords. Two variants: # search_values -> each hit is {__key__, __score__, __value__} # search_keys -> each hit is {__key__, __score__} (lighter; fetch a value later with get_bulk) res = await Employees.search_values(query="engineers based near the coast", limit=5) # { # "status": True, # "payload": [ # {"__key__": "128222336824100726154851618391811195396", "__score__": 0.82, # "__value__": {"username": "Name", "location": "Location", "age": 21}} # ], # "error": None # } # Keys + scores only, with a cosine-similarity floor (range [-1, 1]): keys = await Employees.search_keys(query="engineers based near the coast", limit=5, min_score=0.35) # { # "status": True, # "payload": [{"__key__": "128222336824100726154851618391811195396", "__score__": 0.82}], # "error": None # } ``` ## Store / Keyspace _Read online: https://montygovernance.com/docs/store-keyspace_ ### Create store A store is a logical container for one or more keyspaces (both persistent and not persistent). Conceptually, a store can be compared to a branch of a company, where each keyspace functions as a department within that branch. Stores are created automatically when a keyspace is initialized, so manual creation is not required in most cases. If necessary, a store can also be created manually using the CLI. During this process, you will be prompted for authentication credentials. > **Note:** Creating a store typically requires superowner privileges. ``` montycat console > create-store store exit ``` ### Remove store > **Warning:** if you remove a store all the keyspaces it contains and their indexes will be removed. > **Note:** Removing a store typically requires superowner privileges. ``` montycat console > remove-store store exit ``` #### Use clients API to create / remove stores Examples for clients: ```python from montycat import Engine, Keyspace connection = Engine( host="", port=, username="", password="", store="" ) # call in async runtime res1 = await connection.create_store() res2 = await connection.remove_store() # {"status": True, "payload": None, "error": None} ``` ### Keyspaces Keyspaces are structures within each store. A keyspace in Montycat is the top-level namespace for data storage. Think of it as: - A database in traditional SQL systems; - A bucket in object storage systems like S3; - A logical boundary that separates and organizes data under a single store. #### Volumes inside Keyspaces Within every keyspace, Montycat automatically manages volumes. Volumes are elementary data containers handling the actual physical storage of records. They are self-managing: Montycat creates, grows, and retires volumes transparently without requiring user intervention. This makes keyspaces highly scalable and efficient, since the system can split and balance data across multiple volumes as needed. #### Why Keyspaces Matter - **Isolation**: Each keyspace provides isolation, so different applications, services, or datasets can coexist within the same store without conflict. - **Organization**: Keyspaces let you group related data in a clean and structured way. - **Flexibility**: You can define one or many keyspaces depending on your workload - from simple single-namespace storage to multi-tenant systems. - **Scalability**: With volumes managed internally, keyspaces can hold extremely large datasets without requiring the user to think about partitioning. #### Example Use-Cases - **Multi-Tenant SaaS**: Each tenant gets its own keyspace. The keyspace is the namespace boundary, and credentials are scoped to it, so one tenant’s data is not reachable through another tenant’s credentials. - **Data Domains**: Analytics workloads can use different keyspaces for finance, operations, and user behavior. - **Hybrid Models**: One keyspace for fast in-memory data, another for persistent storage and so on. #### Keyspaces and the Data Mesh Philosophy Montycat is designed to fit naturally into data mesh architecture. A data mesh promotes domain-oriented decentralized data ownership and treats data as a product. Each keyspace can represent a domain in the organization (e.g., finance, sales, sensors, users), or a tenant in a multi-tenant deployment. Keyspaces act as bounded contexts: a keyspace is its own namespace, and the data inside it is reached only through credentials scoped to it. Because Montycat supports schema-flexible storage, teams can evolve their domain’s data model at their own pace, without disrupting other domains. Domains interoperate through explicit, modelled references rather than an implicit federation layer: **pointers** declared in a schema resolve records across keyspaces on request, so data stays physically separated but can be read as one hydrated object. Autonomy is delegated, not assumed. Two separate mechanisms decide what a team can do: - **Data access** — the `read`, `write`, and store-wide `all` permissions described under Credentials decide who can read and write records in a keyspace. - **Administrative authority** — governance policies decide who may provision, remove, snapshot, or reconfigure a keyspace. A superowner delegates these capabilities per store and per keyspace; they never imply data access. In short: keyspaces give teams a bounded context of their own, while credentials and governance define exactly how much autonomy each team has inside it. See [Data-mesh governance](/docs/governance) for the policy model and [Data Mesh](/data-mesh) for the architectural overview. ### Create a keyspace for in-memory data To create a keyspace use: ``` montycat console > create-keyspace store keyspace exit ``` ### Create persistent keyspace If you put **persistent** in the end keyspace you are going to create will be **persistent**. The **cache** is an optional argument, it has to be more than 10 MB, if set to a value less than 10 MB - it will fall back to 10 MB. The last argument also is optional. It represents **compression** and if present will set **compression of data** to *true*. ``` montycat console > create-keyspace store keyspace persistent cache 100 compression exit ``` #### Clients API Examples for clients: ```python from montycat import Engine, Keyspace connection = Engine.from_uri( "montycat://:@:/" ) class Employees(Keyspace.Persistent): keyspace = "Employees" cache = 100 #optional compression = True # optional class EmployeesInMem(Keyspace.InMemory): keyspace = "EmployeesInMem" Employees.connect_engine(connection) EmployeesInMem.connect_engine(connection) res1 = await Employees.create_keyspace() res2 = await EmployeesInMem.create_keyspace() # {"status": True, "payload": None, 'error': None} ``` ### Cache and compression updating You can update cache size for an existing **persistent** keyspace. Compression is selected when the keyspace is created and is immutable afterward. - **cache** - defines the cache capacity (in MB). This value controls how much memory Montycat allocates for hot data to improve query performance. - **compression** - set it when creating the keyspace; it cannot be changed later. > **Important:** cache changes require restart. ``` montycat console > update-cache-compression store keyspace cache 500 exit ``` #### Clients API Use clients to update cache capacity: ```python from montycat import Engine, Keyspace connection = Engine.from_uri("montycat://:@:/") class Employees(Keyspace.Persistent): keyspace = "Employees" Employees.connect_engine(connection) res1 = await Employees.update_cache(500) # was 100 # {"status": True, "payload": None, "error": None} ``` ### Get Available Structure Method is useful to preview all the existing stores and keyspaces. Returns a JSON object with current structure. ```python from montycat import Engine, Keyspace connection = Engine.from_uri("montycat://:@:") res1 = await connection.get_structure_available() # {"status": True, "payload": , "error": None} ``` ### Keyspace Length Get number of records within a particular keyspace. Typically returns a JSON object with number of records and volumes. ```python from keyspaces import Employees, EmployeesInMem #keyspaces.py res1 = await Employees.get_len() res2 = await EmployeesInMem.get_len() # {"status": True, "payload": , "error": None} ``` ### Remove a Keyspace Remove keyspaces. Make sure you put **persistent** if your target for removal was a persistent keyspace. > **Warning:** removing a keyspace will delete all the data and indexes associated with it. **Operation cannot be reverted**. > CLI command will require **superowner** privileges. ``` montycat console > remove-keyspace store keyspace persistent exit ``` #### Remove a Keyspace With Clients API Use clients to remove a keyspace: ```python from keyspaces import Employees, EmployeesInMem #keyspaces.py res1 = await Employees.remove_keyspace() res2 = await EmployeesInMem.remove_keyspace() # {"status": True, "payload": None, "error": None} ``` ## Schema _Read online: https://montygovernance.com/docs/schema_ ### What is Schema? Montycat is a NoSQL database with a flexible **data mesh architecture**. It offers a high degree of flexibility: you can work with schema definitions anywhere along the spectrum between a simple key-value store and a strict SQL-like engine. > No schema at all; Works like most NoSQL databases and key-value stores; You can store strings, integers, floats, arrays, key-value pairs, and other data types without restrictions. > > Data structures are defined on the client side, but are not enforced or validated by the database; Provides flexibility while keeping an organizational structure; Best suited when you want both freedom and consistency without strict enforcement. > > Data structures are defined on the client side and enforced on the database side; This is a SQL-like feature: once a schema is enforced, it cannot be altered; Any insert that does not match the schema will be rejected by the database; Guarantees consistency and reliability across records. ### Why Use Schemas? - A schema is optional but highly recommended to keep data well-structured and organized — especially in microservice architectures where multiple services interact with Montycat. Each keyspace in Montycat **supports multiple schemas** (both enforced and non-enforced) and keeps track of them transparently. - The base **Schema class** defines a structured schema for records stored inside a Montycat Keyspace. A schema describes the shape of the data: what fields exist, their types, and their intended usage. This ensures records remain consistent, strongly typed, and self-describing inside the database. > **Note for Rust Client:** Rust Client uses Rust's native structs and derives **RuntimeSchema** to detect field types **at runtime** and enforce schemas. ```python from montycat import Schema class EmployeesSchema(Schema): username: string location: string age: number ``` ### Schema enforcement Schema enforcement brings SQL-like validation to Montycat. When a schema is enforced, Montycat validates field names and types on every insert and update. A schema can only be enforced if there are no records (keys) associated with it. Once enforced, it cannot be modified — but it can be removed if unused. Both in-memory and persistent keyspaces support schema enforcement. Schema enforcement brings SQL-like validation to Montycat. > **Note for JavaScript & TypeScript**: Since these languages lack runtime type checks for class fields, you have to define a metadata object inside your schema class. This transmits field names and types to Montycat for enforcement. > **Supported metadata types**: **String**, **Number**, **Boolean**, **Array**, **Object**, **Pointer**, **Timestamp**. > **Important:** You have to define a metadata object inside your schema class. No complex types such as TypeScript specific types or complex generics are supported. > Strating from version 1.1.1 Montycat and all the clients support nullable values in schema fields when schema enforcement is enabled at the database level. > This enhancement allows you to define fields that can explicitly accept null values, providing greater flexibility in data modeling while maintaining the benefits of schema enforcement. In Python and Rust the feature is supported by default, while in JavaScript and TypeScript you will need to add a special property to the field metadata definition to indicate that the field is nullable. ```python from montycat import Schema from keyspaces import Employees #keyspaces.py class EmployeesSchema(Schema): username: string location: string | None age: number res = Employees.enforce_schema(EmployeesSchema) # {"status": True, "payload": None, "error": None} ``` ### Remove enforced schema > An enforced schema **cannot be altered**, but it can be removed **if no records exist under that schema**. > If records are present, removal will be rejected to prevent breaking data consistency. ```python from keyspaces import Employees #keyspaces.py from shemas import EmployeesSchema #schemas.py res = await Employees.remove_enforced_schema(EmployeesSchema) # {"status": True, "payload": None, "error": None} ``` ### Schemas preview Montycat allows you to inspect all schemas associated with a keyspace. This provides visibility into which schemas are defined and/or enforced, helping you manage multiple schema versions or client-side definitions. ```python from keyspaces import Employees #keyspaces.py res = await Employees.list_all_schemas_in_keyspace() # {"status": True, "payload": {"schemas": [...], "enforced": {...}}, "error": None} ``` ### Schemas lookup You can lookup values and keys by schema: ```python from keyspaces import Employees #keyspaces.py from schemas import EmployeesSchema #schemas.py res1 = await Employees.lookup_values_where( age = 21, username = "Name", schema = EmployeesSchema ) # {"status": True, "payload": [{"username": "Name", "location": "Location", "age": 21}], "error": None} res2 = await Employees.lookup_keys_where( age = 21, username = "Name", schema = EmployeesSchema ) # {"status": True, "payload": ["128222336824100726154851618391811195396"], "error": None} ``` ## Pointers _Read online: https://montygovernance.com/docs/pointers_ ### Concept of pointers Pointers in Montycat are the equivalent of foreign keys (FK) in relational databases but with additional flexibility: - Always **many-to-many**: a pointer can link records across multiple keyspaces without embedding entire documents. - **Natively resolved**: Montycat can automatically dereference pointers at query time when requested. - **Data consistency**: pointers help maintain relationships between entities stored in different keyspaces, ensuring linked data remains consistent. Pointers allow you to model relationships between keyspaces without duplicating data, while leaving you in control of whether or not to resolve them on retrieval. Example Workflow with Pointers - Create and insert a record in the target keyspace (e.g., Departments). - Store the returned key as a pointer when inserting into another keyspace (e.g., Employees). - Retrieve the record: - Without pointer resolution Montycat returns only original record. - With pointer enabled Montycat automatically dereferences and embeds the related record(s). This gives you the flexibility to either fetch lightweight references for performance, or resolve full relationships when you need rich, connected data. In the current version, pointers can be established only between the same type of keyspaces — from in-memory to in-memory or from persistent to persistent. Pointers within in-memory keyspaces cannot be established either if it was set to expire or an original key has was set to expire and will be removed by timer at some point of time. > **Performance considerations:** pointer resolution adds overhead. In highly concurrent environments with a large number of simultaneous requests, resolving deep or complex pointer chains can slow down performance. > **Note:** Pointers can be established only between the same type of keyspaces — from in-memory to in-memory or from persistent to persistent. Pointers within in-memory keyspaces cannot be established either if it was set to expire or an original key has was set to expire and will be removed by timer at some point of time. > **Important:** Always consider the lifecycle of your keys when working with pointers. > **Pointers extraction:** You can either extract pointer value or pointer metadata. ```python from montycat import Schema, Pointer from keyspaces import Employees, Departments class EmployeesSchema(Schema): username: string location: string age: number department: Pointer class DepartmentSchema(Schema): name: string employees: int department = DepartmentSchema( name='IT', employees=20 ).serialize() res1 = await Departments.insert_value(department) # {"status": True, "payload": "128345335674100726154823618391811195396", "error": None} employee = EmployeesSchema( username="Name", location="Location", age=21, department=Pointer(Departments, '128345335674100726154823618391811195396'), # pass foreign keyspace and previously returned key ).serialize() res2 = await Employees.insert_value(employee) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} res3 = await Employees.get_value(key="128222336824100726154851618391811195396") # {"status": True, "payload": {"username": "Name", "location": "Location", "age": 21}, "error": None} res4 = await Employees.get_value(key="128222336824100726154851618391811195396", with_pointers=True) # { # "status": True, # "payload": {"username": "Name", "location": "Location", "age": 21, # "department": {"name": "IT", "employees": 20} # }, # "error": None # } res5 = await Employees.get_value(key="128222336824100726154851618391811195396", with_pointers_metadata=True) # { # "status": True, # "payload": {"username": "Name", "location": "Location", "age": 21, # "department": { # "__key__": "128345335674100726154823618391811195396", # "__keyspace__": "Departments" # }, # }, # "error": None # } ``` ### Update pointers You can easily update pointers: ```python from keyspaces import Employees, Departments #keyspaces.py res1 = await Employees.update_value( key="128222336824100726154851618391811195396", age=34, department=Pointer(Departments, "128345335333100726154823618391811195454"), # the different key passed ) # {"status": True, "payload": None, "error": None} # if no updates: # {"status": False, "payload": {"no_changes": "128222336824100726154851618391811195396"}, "error": None} ``` ### Lookup using pointers You can lookup keys and values by pointers as well: ```python from keyspaces import Employees #keyspaces.py from montycat import Pointer res1 = await Employees.lookup_values_where( age = 21, username = "Name", department = Pointer(Departments, "128345335333100726154823618391811195454"), ) # {"status": True, "payload": [{"username": "Name", "location": "Location", "age": 21}], "error": None} res2 = await Employees.lookup_keys_where( age = 21, username = "Name", department = Pointer(Departments, "128345335333100726154823618391811195454"), ) # {"status": True, "payload": ["128222336824100726154851618391811195396"], "error": None} ``` ### Depending keys You can list all dependent keys and keyspaces - that is, all keys related to a specific key within a particular keyspace. ```python from keyspaces import Departments #keyspaces.py res1 = await Departments.list_all_depending_keys(key = "128345445574100726154823618391234595396") res2 = await Departments.list_all_depending_keys(custom_key = "myCustomKey") # {"status": True, "payload": {"Employees": ["128222336824100726154851618391811193333"]}, "error": None} ``` ## Timestamps overview _Read online: https://montygovernance.com/docs/timestamps_ Montycat handles timestamp indexes natively. It dynamically parses timestamps supplied by clients and supports a variety of time formats. Montycat's “preferred” default is ISO 8601. #### Date Only Formats - YYYY-MM-DD → 2021-07-15 - DD-MM-YYYY → 15-07-2021 - DD.MM.YYYY → 15.07.2021 - YYYY/MM/DD → 2021/07/15 - DD Mon YYYY → 15 Jul 2021 - DD Month YYYY → 15 July 2021 - Mon DD, YYYY → Jul 15, 2021 - DD/MM/YYYY → 15/07/2021 #### Time Only Formats - HH:MM → 14:30 - HH:MM:SS → 14:30:45 - HH:MM AM/PM → 02:30 PM (12-hour clock) - HH:MM (24-hour strict) → 23:59 - HH:MM:SS.sss… → 14:30:45.123456 (with subseconds, 3–6 digits) #### Date + Time Formats - YYYY-MM-DD HH:MM:SS → 2021-07-15 14:30:45 - YYYY-MM-DDTHH:MM:SS → 2021-07-15T14:30:45 (ISO 8601 with T) #### Week-based Formats - YYYY-Www → 2021-W29 (ISO week date) - YYYY-Www-D → 2021-W29-3 (ISO week + weekday, here 3 = Wednesday) #### RFC 3339 and ISO Extended - YYYY-MM-DDTHH:MM:SS.sssZ → 2021-07-15T14:30:45.123Z (Zulu/UTC time; fractional seconds are optional) - YYYY-MM-DDTHH:MM:SS±HH:MM → 2021-07-15T09:30:45-05:00 (UTC offsets are normalized to the same instant) ### Using timestamps To work with timestamps, use the Timestamp class. > Timestamps must always be provided as **stringified values**. > **Performance note:** Montycat timestamps parsing is highly CPU-bound. > Avoid massive bulk inserts with timestamp-heavy records, as dynamic parsing may slow down ingestion. ```python from montycat import Schema, Timestamp from keyspaces import Employees import datetime class EmployeesSchema(Schema): username: string location: string age: number dateHired: Timestamp datetime_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') record = EmployeesSchema( username="Name", location="Location", age=21, dateHired=Timestamp(datetime_str) ).serialize() res2 = await Employees.insert_value(record) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} ``` ### Lookups using timestamps Once records with timestamps are stored, Montycat enables timestamp-based lookups on both keys and values. Malformed bounds, invalid ranges, and timestamps outside the index's representable range return an error instead of an empty result. The Timestamp class supports range queries: - **before** → all records before a given timestamp - **after** → all records after a given timestamp - **range** → records between a start and stop ```python from keyspaces import Employees from montycat import Timestamp res1 = await Employees.lookup_values_where( dateHired: Timestamp(after="2025-06-10 12:00:00"), ) # { # "status": True, # "payload": [{username: 'Name', location: 'Location', age: 21, dateHired: '2025-08-10 12:00:00'}], # "error": None # } res2 = await Employees.lookup_values_where( dateHired: Timestamp(before="2025-08-24 12:00:00"), ) res3 = await Employees.lookup_values_where( dateHired: Timestamp(start="2025-06-10 12:00:00", stop="2025-08-24 12:00:00"), ) res4 = await Employees.lookup_keys_where( dateHired: Timestamp(after="2025-06-10 12:00:00"), ) # {"status": True, "payload": ["128222336824100726154851618391811195396"], "error": None} ``` ## Insertion _Read online: https://montygovernance.com/docs/insertion_ ### Insert value Values can be inserted using a **ready-to-use method**. Supported value types include: - **Integer** - **String** - **Float** - **Complex serializable data structures** Each value (record) is related to a key and can be retrieved by that key. Internally, the database stores all keys as 128-bit native unique integers. For client operations, these keys are returned as string representations. > **Note for Rust:** When working with Rust, ensure that complex data structures implement the **Serialize** trait from the serde library. Also if you are operating with no schemas use **.insert_value_no_schema()** method. ```python from schemas import EmployeesSchema #schemas.py from keyspaces import Employees #keyspaces.py res1 = await Employees.insert_value("John") res2 = await Employees.insert_value(28) res3 = await Employees.insert_value(28.8) res4 = await Employees.insert_value(["John", "Doe"]) res5 = await Employees.insert_value({"username": "John", "age": 28}) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} # or {"status": False, "payload": None, "error": "Error Text"} ``` ### Using schema and ORM When using the **ORM** approach, you define a **custom data schema** by instantiating the Montycat **Schema class**. This converts the object into a serializable data structure that can be stored in the database. The schema name is automatically tracked in the database. Data can later be queried by schema name. Each keyspace can contain multiple schemas. > Before calling the insertion method, you must invoke **.serialize()** on the schema instance. > **Note for Rust:** Rust client does not require explicit serialization. Just make sure your struct implements the **Serialize** trait from **serde** and **RuntimeSchema** from **montycat**, which re-exports the derive macro — so **cargo add montycat** and **cargo add serde --features derive** are all you need. ```python from schemas import EmployeesSchema #schemas.py from keyspaces import Employees #keyspaces.py record = EmployeesSchema( username="Name", location="Location", age=21 ).serialize() result1 = await Employees.insert_value(record) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} # or {"status": False, "payload": None, "error": "Error Text"} ``` ### Insert value with a custom key Montycat provides a method to insert a value with a custom key. The custom key must be a string. Internally, the database converts the custom key into a 128-bit integer for storage. Custom keys enable fast and efficient record lookups. Note: Montycat does not allow duplicate keys within the same keyspace. If you attempt to insert a record with an existing key, the operation will fail with the error: **Key already exists**. ```python from keyspaces import Employees #keyspaces.py res1 = await Employees.insert_custom_key_value(custom_key="myCustomKey", value="John") # {"status": True, "payload": "112424342341342", "error": None} ``` ### Insert custom key with no value It is also possible to insert a **custom key without an associated value**. This can be useful for scenarios where you need to reserve a key in advance for future use. ```python from keyspaces import Employees #keyspaces.py res1 = await Employees.insert_custom_key(custom_key="myCustomKey") # {"status": True, "payload": "112424342341342", "error": None} ``` ### Insert bulk Montycat supports **native bulk insertion of multiple values**. Bulk operations are executed using asynchronous handles and multi-threaded execution for maximum performance. The number of simultaneous operations is determined automatically by the database engine based on system parameters. This default value **can be overridden** manually if required. Custom keys are not supported in bulk insertion. Typically, bulk insertion does not return the inserted keys. If any operations fail during the bulk insert, the method will return a list of the failed values for further handling. ```python from keyspaces import Employees #keyspaces.py array = [] for num in range(1000): value = { "username": f"Name {num}", "location": f"Location {num}", "age": 21 } array.extend([value]) res1 = await Employees.insert_bulk(bulk_values=array) # {"status": True, "payload": None, "error": None} # or if all insertions failed: {"status": False, "payload": None, "error": "Error Text"} # or if some of insertions failed: # { # "status": False, # "payload": {"failed": [{"username": "Name 3", "location": "Location 3", "age": 21 }]}, # "error": None # } ``` ## Retrieval _Read online: https://montygovernance.com/docs/retrieval_ ### Single record retrieval You can retrieve a single record by it's key (id). A key can be regular ordered key or custom key. > You can embed a key in that case you will receive a **__key__** and **__value__** fields in response. ```python from keyspaces import Employees res1 = await Employees.get_value(key = "128222336824100726154851618391811195396") res2 = await Employees.get_value(custom_key = "myCustomKey") # {"status": True, "payload": {"username": "Name", "location": "Location", "age": 21}, "error": None} res3 = await Employees.get_value(key = "128222336824100726154851618391811195396", key_included = True) # { # "status": True, # "payload": { # "__key__": "128222336824100726154851618391811195396", # "__value__": {"username": "Name", "location": "Location", "age": 21} # }, # "error": None # } ``` ### Retrieve with pointers If you define **pointers** (references to another record in a different keyspace) in a **schema**, you can resolve them when querying by setting a special flag. When the flag is enabled: - **The pointer values will be automatically fetched and integrated into the original record's body** . - This allows you to work with a fully hydrated object instead of manually resolving references across keyspaces. Pointers enable Montycat to act like a data mesh join mechanism, keeping data physically distributed but logically unified. When the flag is disabled you will only see the only original record. This avoids additional fetch overhead and is useful for lightweight or high-performance reads. ```python from keyspaces import Employees res1 = await Employees.get_value(key = "128222336824100726154851618391811195396", with_pointers = True) res2 = await Employees.get_value(custom_key = "myCustomKey", with_pointers = True) # { # "status": True, # "payload": { # "username": "Name", # "location": "Location", # "age": 21, # "department": { # "name": "IT", # "employees": 20 # } # }, # "error": None # } ``` ### Multiple records retrieval Montycat allows you to retrieve multiple values in a **single request**. This improves efficiency and reduces network overhead compared to fetching keys individually. When retrieving multiple values you can: - Provide a **list of regular keys** (system-generated IDs). - Provide a **list of custom keys** (user-defined identifiers). Optionally enable pointer resolution so that referenced records from other keyspaces are automatically integrated into the result. The response will always include: - **status** → Whether the bulk operation succeeded. - **payload** → Contains succeded (records successfully retrieved) and failed (keys that could not be found or resolved). - **error** → Error message if the whole operation failed. ```python from keyspaces import Employees keys_to_get = [ '128222336824100726154851618391811195396', '254672336824100726154851618391811195333', ] custom_keys_to_get = ['myCustomKey1', 'myCustomKey2'] res1 = await Employees.get_bulk(keys=keys_to_get) res2 = await Employees.get_bulk(keys=keys_to_get, custom_keys=custom_keys_to_get, with_pointers=True) # { # "status": True, # "payload": { # "succeded": [ # {"username": "Name1", "location": "Location1", "age": 21}, # {"username": "Name1", "location": "Location1", "age": 22}, # ], # "failed": [] # }, # "error": None # } # or if all failed: {"status": False, "payload": None, "error": "Error Text"} # or if some of retrievals failed: # { # "status": True, # "payload": { # "succeded": [ # {"username": "Name1", "location": "Location1", "age": 21}, # ], # "failed": [ # "254672336824100726154851618391811195396" # ] # }, # "error": None # } ``` ### Limit output When working with large sets of keys, you may not want to fetch everything at once. Montycat provides an option that lets you control the range of records returned in a single call. This is especially useful for: - **Pagination** → Fetching results in manageable chunks (e.g., page 1 = first 50, page 2 = next 50). - **Batch processing** → Processing subsets of a large dataset iteratively. - **Performance optimization** → Preventing memory overload when working with very large key lists. Range and bulk reads also accept an `order` option. Use `ascending` to scan from oldest to newest or `descending` to scan from newest to oldest. Limits use a half-open `[start, stop)` range and are applied in the chosen direction, which makes forward and reverse pagination deterministic. > **Note:** The limit option is only available for persistent keyspaces. In-memory keyspaces always return all keys. ```python from keyspaces import Employees from montycat import ResultOrder res1 = await Employees.get_bulk( limit=[2, 4], order=ResultOrder.DESCENDING, ) # returns positions [2, 4) while scanning newest to oldest ``` ### Get keys only Montycat also allows you to fetch **keys without values**. This is useful for metadata operations, prefetching, or scanning datasets before performing full retrievals. - **In-memory keyspaces** → return all existing keys without a configurable order. **The limit option is not available**. - **Persistent keyspaces** → support the limit and order parameters. Keys are returned in deterministic order by their internal insertion timestamp; choose ascending or descending explicitly. This allows you to paginate over stored data efficiently. ```python from keyspaces import Employees, EmployeesInMem from montycat import ResultOrder res1 = await EmployeesInMem.get_keys() # In-memory keyspaces return all keys res2 = await Employees.get_keys(limit=[20, 40], order=ResultOrder.ASCENDING) # { # "status": True, # "payload": ["128222336824100726154851618391811195396", "254672336824100726154851618391811195396"], # "error": None # } ``` ### Get keys by Volume Montycat supports retrieving keys based on their associated volume. This is particularly useful for persistent keyspaces where data is sharded across multiple volumes for scalability and performance. - You can specify a particular volume number to fetch keys stored within that volume. - This allows targeted retrieval of keys from specific data shards, which can be useful for maintenance, analysis, or debugging. > **Note:** If you want to retrieve all custom keys you have to specify volume as **0**. ```python from keyspaces import Employees from connection import connection #connection.py res1 = await connection.get_structure_available() # {"status": True, "payload": , "error": None} # list of volumes will be included in response res2 = await Employees.get_keys(volumes=['32009234234234234']) res3 = await Employees.get_keys(latest_volume=True) # { # "status": True, # "payload": ["128222336824100726154851618391811195396", "254672336824100726154851618391811195396"], # "error": None # } ``` ## Lookup _Read online: https://montygovernance.com/docs/lookup_ ### Lookup overview Montycat provides efficient lookup mechanisms to retrieve records stored within keyspaces. Lookups can be performed either by keys only (identifiers) or by values (full record contents). These operations leverage Montycat's automatic indexing on structured data across in-memory and persistent storage engines. All lookups can be refined using **field-based criteria**, **timestamps**, **schemas**, and **pointer references**. Results are ordered by internal insertion timestamp. Pass `order: "ascending"` or `order: "descending"` to choose the direction explicitly. ### Lookup keys Retrieves only the unique keys of records that match the specified search criteria. This is useful when you only need record references without fetching full payload values immediately. Features: - Filter by **field values** (e.g., username, age, location). - Filter by **timestamps** (before/after/range conditions). - Filter by **schema** type to ensure type-safe lookups. - Follow **pointers** (foreign-key style references to other keyspaces). Empty results return a structured empty array. Example use case: Identify all employee records from the IT department hired after a specific date, but retrieve only their record keys (ids). ```python from keyspaces import Employees, Departments #keyspaces.py from schemas import EmployeesSchema, DepartmentSchema #schemas.py from montycat import Pointer, Timestamp import datetime department = DepartmentSchema( name='IT', employees=20 ).serialize() res1 = await Departments.insert_value(department) # {"status": True, "payload": "128345335674100726154823618391811195396", "error": None} record = EmployeesSchema( username="Name", location="Location", age=21, department=Pointer(Departments, '128345335674100726154823618391811195396'), # pass previously returned key, dateHired=Timestamp(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ).serialize() res2 = await Employees.insert_value(record) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} res3 = await Employees.lookup_keys_where( age = 21, username = "Name", ) # {"status": True, "payload": ["128222336824100726154851618391811195396"], "error": None} res4 = await Employees.lookup_keys_where( age = 21, username = "Name", dateHired: Timestamp(after='2025-06-10 12:00:00'), department=Pointer(Departments, '128345335674100726154823618391811195396'), schema=EmployeesSchema ) # {"status": True, "payload": ["128222336824100726154851618391811195396"], "error": None} ``` ### Lookup values Retrieves the full record contents matching given criteria. Unlike **lookup_keys_where**, this returns complete objects with field values. Features: - Supports the same filters as key lookups ( **field values, timestamps, schema, pointers**). - Optionally return **resolved pointer data**, so foreign-key references are automatically dereferenced into their corresponding values. Example use case: Retrieve all details of employees named "Name", aged 21, hired after a given date, including department information via pointer resolution. ```python from keyspaces import Employees, Departments #keyspaces.py from schemas import EmployeesSchema, DepartmentSchema #schemas.py from montycat import Pointer, Timestamp import datetime department = DepartmentSchema( name='IT', employees=20 ).serialize() res1 = await Departments.insert_value(department) # {"status": True, "payload": "128345335674100726154823618391811195396", "error": None} record = EmployeesSchema( username="Name", location="Location", age=21, department=Pointer(Departments, '128345335674100726154823618391811195396'), # pass previously returned key, dateHired=Timestamp(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ).serialize() res2 = await Employees.insert_value(record) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} res3 = await Employees.lookup_values_where( age = 21, username = "Name", ) # { # "status": True, # "payload": [{"username": "Name", "location": "Location", "age": 21, "dateHired": "2025-08-10 12:00:00"}], # "error": None # } res4 = await Employees.lookup_values_where( age=21, username="Name", key_included=True # embed key with key_included argument ) # { # "status": True, # "payload": [ # { # "__key__": "128222336824100726154851618391811195396", # "__value__": {"username": "Name", "location": "Location", "age": 21, "dateHired": "2025-08-10 12:00:00"} # } # ], # "error": None # } res5 = await Employees.lookup_values_where( age=21, username="Name", dateHired=Timestamp(after='2025-06-10 12:00:00'), department=Pointer(Departments, '128345335674100726154823618391811195396'), schema=EmployeesSchema ) # { # "status": True, # "payload": [{"username": "Name", "location": "Location", "age": 21, "dateHired": "2025-08-10 12:00:00"}], # "error": None # } ``` ### Lookup limit range You can limit output using a range (start, stop) to handle pagination or large datasets. This works for both in-memory and persistent keyspaces. The range is half-open (`[start, stop)`) and is applied after selecting the requested order, so reverse pagination is stable as well. Ordering applies to key and value lookups. Semantic search is different: it remains similarity-ranked and does not accept key ordering. ```python from keyspaces import Employees #keyspaces.py from montycat import ResultOrder res1 = await Employees.lookup_keys_where( age = 21, username = "Name", limit=[10, 1000], order=ResultOrder.ASCENDING, ) # {"status": True, "payload": ["128222336824100726154851618391811195396"], "error": None} res2 = await Employees.lookup_values_where( age = 21, username = "Name", limit=[10, 1000], order=ResultOrder.ASCENDING, ) # { # "status": True, # "payload": [{"username": "Name", "location": "Location", "age": 21, "dateHired": "2025-08-10 12:00:00"}], # "error": None # } ``` ### Lookup retrieve with pointers When working with relational data across keyspaces, Montycat supports pointers (foreign-key style references). By enabling pointers flag, lookups will automatically resolve and return the full referenced objects. ```python from keyspaces import Employees #keyspaces.py res1 = await Employees.lookup_values_where( age = 21, username = "Name", with_pointers = True ) # { # "status": True, # "payload": [ # { # "username": "Name", # "location": "Location", # "age": 21, # "department": { # "name": "IT", # "employees": 20 # }, # "dateHired": "2025-08-10 12:00:00" # } # ], # "error": None # } # with pointers metadata enabled res2 = await Employees.lookup_values_where( age=21, username="Name", pointers_metadata=True ) # { # "status": True, # "payload": [ # { # "username": "Name", # "location": "Location", # "age": 21, # "department": { # "__keyspace__": "departments", # "__key__": "128345335674100726154823618391811195396", # }, # "dateHired": "2025-08-10 12:00:00" # } # ], # "error": None # } ``` ## Semantic Search _Read online: https://montygovernance.com/docs/semantic-search_ ### Semantic search overview Montycat can rank stored items by **meaning** instead of exact keywords. Each value is embedded into a vector on write and indexed for approximate nearest-neighbor (kNN) search, so a query returns the items whose meaning is closest to it — the retrieval layer for RAG, AI-agent memory, and semantic search. Embeddings are computed on-device: there is no external embedding API, no API keys, and no separate vector database to run. > **Note:** Semantic search requires the **Montycat Semantic** server edition (Docker image, apt package, or prebuilt package). It is **enabled by default** there using the `bge-small` model — the lean `montycat` server does not include the embedding engine. ### Enable / disable semantic search The switch is DB-wide by default, and can be scoped to a single store or keyspace. Because the semantic edition turns it on by default, enable is mainly for enrolling a keyspace that has no configuration yet, or for setting the default model and field. The model is downloaded on first enable and cached locally; every keyspace is embedded in the background as data is written. > **Enable does not switch an enrolled keyspace.** A keyspace that already has a semantic configuration is left untouched, and asking for a different model or field is rejected. Use `reembed_semantic_search` below to replace one. ```python from montycat import SemanticModel # Enable semantic search DB-wide. Requires the Montycat Semantic server edition, # where it is ON by default — call this only to enroll a keyspace that has none. # model: MINI_LM | BGE_SMALL (default) | BGE_BASE | E5_SMALL await connection.enable_semantic_search(model=SemanticModel.BGE_SMALL) # {"status": True, "payload": None, "error": None} # The chosen model is downloaded on first enable and cached locally. Every # keyspace is embedded in the background as data is written. # Enrolling one keyspace explicitly. A keyspace that is already enrolled is left # untouched — this will not switch its model. await connection.enable_semantic_search( model=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 all stored vectors. await connection.disable_semantic_search() # {"status": True, "payload": None, "error": None} ``` ### Read status and change the model `get_semantic_status` reports what the server actually holds rather than what you assumed: the DB-wide switch, the default model and field, and for each keyspace its model, dimensions, field, storage type, and whether a backfill is still running. On a **Montycat Semantic** server **1.3.5 or newer** it also reports an `indexing` block — the live embedding queue, the backfill queue, and how many backfills are in flight. Call it with no arguments for the whole database, or pass a store and keyspace to narrow it. Changing the embedding model of a keyspace that already holds vectors is destructive — the old vectors were produced by a different model and cannot be compared against the new one. `reembed_semantic_search` does this in one atomic operation: it drops that keyspace's vectors, records the new configuration, and starts a complete backfill, reporting the previous model alongside the new one. The keyspace stays queryable throughout; results become complete once `backfill_pending` clears. ```python from montycat import SemanticModel # Read back what the server actually holds. Omit both arguments for the # whole database, or narrow to one keyspace. status = await connection.get_semantic_status(store="catalog", keyspace="products") # { # "status": True, # "payload": { # "globally_enabled": True, # "default_model": "bge-small", # "default_field": None, # "indexing": { # "live_queue": 0, # "backfill_queue": 0, # "backfill_in_flight": 0 # }, # "keyspaces": { # "catalog/products": { # "enrolled": True, # "model": "bge-small", # "dimensions": 384, # "field": None, # "persistent": True, # "backfill_pending": False # } # } # }, # "error": None # } # Replace the model of an already-enrolled keyspace. Drops its old vectors and # starts a complete backfill in one atomic operation. res = await connection.reembed_semantic_search( SemanticModel.MINI_LM, store="catalog", keyspace="products" ) # { # "status": True, # "payload": { # "scope": "catalog/products", # "changed": True, # "previous_model": "bge-small", # "model": "minilm", # "dimensions": 384, # "field": None, # "backfill_started": True # }, # "error": None # } ``` ### Search by meaning Query a keyspace with natural language and get back the most semantically similar items, ranked by cosine similarity. Use `search_values` to return the value inline with each hit, or `search_keys` for a lighter key-and-score result. Pass a `min_score` floor to drop weak matches. These replace the older `semantic_search_*` methods, which remain as semantic-only wrappers with unchanged behavior. ```python from keyspaces import Employees #keyspaces.py # Rank stored items by MEANING, not keywords. Two variants: # search_values -> each hit is {__key__, __score__, __value__} # search_keys -> each hit is {__key__, __score__} (lighter; fetch a value later with get_bulk) res = await Employees.search_values(query="engineers based near the coast", limit=5) # { # "status": True, # "payload": [ # {"__key__": "128222336824100726154851618391811195396", "__score__": 0.82, # "__value__": {"username": "Name", "location": "Location", "age": 21}} # ], # "error": None # } # Keys + scores only, with a cosine-similarity floor (range [-1, 1]): keys = await Employees.search_keys(query="engineers based near the coast", limit=5, min_score=0.35) # { # "status": True, # "payload": [{"__key__": "128222336824100726154851618391811195396", "__score__": 0.82}], # "error": None # } ``` ### Keyword and hybrid ranking Meaning is not always what you are matching on. Pass `mode` to choose how results are ranked: `semantic` (the default) ranks by vector similarity, `keyword` ranks with **BM25** over the stored text, and `hybrid` runs both and fuses them with reciprocal rank fusion. Reach for keyword when the query hinges on an exact term — an identifier, an error code, a product name — that a vector model will happily rank alongside its synonyms. Reach for hybrid when a query is both at once. > **Scores are not comparable across modes.** Cosine similarity is bounded to `[-1, 1]`, hybrid RRF is normalized to `[0, 1]`, and raw BM25 is **unbounded above** — a BM25 score of 8 is ordinary. Compare keyword scores only within one query, and set `min_score` on the scale of the mode you asked for. ```python from keyspaces import Employees #keyspaces.py from montycat import SearchMode # KEYWORD: BM25 over the stored text. The term has to actually appear, which is # what you want for an identifier, error code, or product name. exact = await Employees.search_values( query="Kubernetes", mode=SearchMode.KEYWORD, limit=5, ) # { # "status": True, # "payload": [ # {"__key__": "128222336824100726154851618391811195396", "__score__": 4.71, # "__value__": {"username": "Name", "location": "Location", "age": 21}} # ], # "error": None # } # HYBRID: vector similarity and BM25 in one request, fused with reciprocal rank # fusion. The safest default when a query mixes meaning with an exact term. mixed = await Employees.search_values( query="Kubernetes experience on the platform team", mode=SearchMode.HYBRID, limit=5, min_score=0.2, ) # Keys + scores only, same modes: keys = await Employees.search_keys(query="Kubernetes", mode=SearchMode.KEYWORD, limit=5) # Score scales differ by mode: cosine similarity is [-1, 1], hybrid RRF is # normalized to [0, 1], and raw BM25 is unbounded above — compare BM25 scores # only within one query. ``` > **Note:** keyword and hybrid ranking require a **Montycat Semantic** server **1.3.4 or newer**. Each mode is a distinct wire command, so an older engine rejects the request outright rather than silently returning vector-ranked results. Keyword indexes are built automatically for every enrolled keyspace, including external-vector profiles, and existing keyspaces are backfilled on upgrade. ### Filtered semantic search Combine ranking with structured metadata constraints by passing `filters`. The filter is a hard AND pre-filter with the same criteria shape as `lookup_keys_where`: only matching records become search candidates. It narrows *which* records are ranked in every mode and never boosts relevance scores. Filters support indexed fields, timestamps, pointers, and schemas. ```python from keyspaces import Employees #keyspaces.py # Rank among only the records that match the structured filter. The filter # narrows candidates in every mode; it never rescores. Filters use the same # criteria shape as lookup_keys_where. matching_keys = await Employees.search_keys( query="works on distributed systems", filters={"location": "Location"}, limit=5, min_score=0.35, ) # { # "status": True, # "payload": [{"__key__": "128222336824100726154851618391811195396", "__score__": 0.82}], # "error": None # } matching_values = await Employees.search_values( query="works on distributed systems", filters={"location": "Location"}, limit=5, ) # { # "status": True, # "payload": [ # {"__key__": "128222336824100726154851618391811195396", "__score__": 0.82, # "__value__": {"username": "Name", "location": "Location", "age": 21}} # ], # "error": None # } ``` ### Bring your own vectors If you already have embeddings from a compatible batch pipeline or vector store, supply them directly and the server skips embedding entirely. Every write accepts an optional `vector` alongside the value, and bulk writes accept `vectors` paired with the values **by position**. Searches accept a query `vector` too, which bypasses text embedding — so the query string may be empty when one is supplied, and no embedding model is invoked at all. > **Note:** precomputed vectors require a **Montycat Semantic** server **1.3.0 or newer**. Bulk updates take `vectors` for numeric keys and `custom_vectors` for custom keys. ```python from keyspaces import Employees #keyspaces.py # Vectors produced elsewhere — another model, a batch pipeline, an existing # vector store — are stored as-is and the server skips embedding. res = await Employees.insert_value( {"username": "Name", "location": "Location", "age": 21}, vector=my_embedding, # list[float], omit for server-side embedding ) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} # Bulk: vectors are paired with bulk_values BY POSITION. res = await Employees.insert_bulk( bulk_values=[record_one, record_two], vectors=[embedding_one, embedding_two], ) # Searching: a query vector bypasses text embedding, so the query may be empty. hits = await Employees.search_values(query="", vector=my_query_embedding, limit=10) # { # "status": True, # "payload": [ # {"__key__": "128222336824100726154851618391811195396", "__score__": 0.82, # "__value__": {"username": "Name", "location": "Location", "age": 21}} # ], # "error": None # } ``` > Every supplied record vector and query vector must come from the model enrolled for that keyspace, including the same model revision, preprocessing, pooling, and normalization. Matching dimensions alone is **not enough**. An auto-enrolled BGE-small keyspace accepts only BGE-small-compatible 384d vectors. To use another model, create the keyspace with semantic auto-enrollment disabled and enroll a matching external profile first. Montycat validates dimensions, but it cannot prove that equal-length vectors share an embedding space; mixing spaces makes cosine similarity meaningless. > A vector you supplied is **never** overwritten by background embedding. A later ordinary write to that item clears the protection and re-embeds from its text, which is exactly the point at which re-embedding is what you want. ## Update _Read online: https://montygovernance.com/docs/update_ ### Update a single record Montycat allows updating a single record by specifying its key (either system-generated or custom) and modifying **specific fields**, **including complex types like pointers and timestamps** . The update operation is atomic and ensures data consistency. Nested objects are merged recursively, so omitted sibling fields are retained. Arrays, scalar values, and `null` replace the existing value. If the provided values match the existing record, no changes are applied, and a **no_changes_detected response** is returned. ```python from keyspaces import Employees, Departments #keyspaces.py from schemas import EmployeesSchema from montycat import Pointer, Timestamp value = EmployeesSchema( username = "Name", location = "Location", age = 21, department=Pointer(Departments, '128345335674100726154823618391811195396'), # pass previously returned key, dateHired=Timestamp(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ).serialize() res1 = await Employees.insert_value(value) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} res2 = await Employees.update_value( key="128222336824100726154851618391811195396", age=34, department=Pointer(Departments, '128345335333100726154823618391811195454'), dateHired=Timestamp(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ) # {status: True, payload: None, error: None} # or if no changes detected: # { # "status": False, # "payload": {"no_changes": "128222336824100726154851618391811195396"}, # "error": "Error Text" # } ``` ### Custom keys support Montycat supports updating records using **custom keys**, which follow the same logic as system-generated keys but allow user-defined identifiers. Custom keys provide flexibility for user-defined identifiers while maintaining the same update functionality as system-generated keys. The operation is atomic and supports all schema-defined fields. ```python from keyspaces import Employees, Departments #keyspaces.py from schemas import EmployeesSchema from montycat import Pointer, Timestamp value = EmployeesSchema( username = "Name", location = "Location", age = 21, department=Pointer(Departments, '128345335674100726154823618391811195396'), # pass previously returned key, dateHired=Timestamp(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ).serialize() res1 = await Employees.insert_custom_key_value(custom_key="myCustomKey" value=value) res2 = await Employees.update_value( custom_key="myCustomKey", age=34, department=Pointer(Departments, '128345335333100726154823618391811195454'), dateHired=Timestamp(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ) # {status: True, payload: None, error: None} # or if no changes detected: # { # "status": False, # "payload": {"no_changes": "112424342341342"}, # "error": "Error Text" # } ``` ### Bulk updates Montycat supports bulk updates, allowing multiple records ( **with both regular and custom keys**) to be updated in a single operation for efficiency. Bulk updates improve performance by processing multiple updates in a single transaction. Each item uses the same recursive object-merge rules as a single update; it does not replace the complete stored document. The operation supports partial success, with detailed feedback on failed or unchanged keys. Both regular and custom keys can be combined in a single bulk update. ```python from keyspaces import Employees #keyspaces.py updates_regular_keys = { "128345335333100726154823618391811195454": {"age": 34, "username": "John"}, "128222336824100726154851618391811195396": {"age": 28, "username": "Anna"} } updates_custom_keys = { "myFirstCustomKey": {"age": 39, "username": "Mary"}, "mySecondCustomKey": {"age": 33, "username": "Alex"} } res2 = await Employees.update_bulk(bulk_keys_values=updates_regular_keys, bulk_custom_keys_values=updates_custom_keys) # {"status": True, "payload": None, "error": None} # or if all of updates failed: {"status": False, "payload": None, "error": "Error Text"} # or if some of updates failed: # { # "status": False, # "payload": {"failed": ["128222336824100726154851618391811195396"]}, # "error": "Error Text" # } # or if no changes detected: # { # "status": False, # "payload": {"no_changes": ["128222336824100726154851618391811195396"]}, # "error": None # } ``` ## Delete _Read online: https://montygovernance.com/docs/delete_ ### Delete a single record When you delete a record, Montycat automatically takes care of all the background housekeeping such as resolving pointers, updating or removing indexes, and cleaning up references. You only need to provide either a regular key or a custom key. ```python from keyspaces import Employees #keyspaces.py res1 = await Employees.delete_key(key="128222336824100726154851618391811195396") res2 = await Employees.delete_key(custom_key="myCustomKey") # {"status": True, "payload": None, "error": None} ``` ### Bulk deletion Montycat also supports deleting multiple records in a single call. You can pass a mix of regular keys and custom keys together. Behind the scenes, Montycat will handle pointer resolution, index updates, and cleanup for each record. - If all deletions succeed → returns **status: true** - If some deletions fail → returns **status: false** with a **list of failed_keys** - If all deletions fail → returns **status: false** with an **error message** ```python from keyspaces import Employees #keyspaces.py regular_keys_to_delete = ["128222336824100726154851618391811195396", "128222344424100726555851618391811195555"] custom_keys_to_delete = ["myFirstCustomKey", "mySecondCustomKey"] res1 = await Employees.delete_bulk(keys=regular_keys_to_delete, custom_keys=custom_keys_to_delete) # {"status": True, "payload": None, "error": None} # or if all of deletions failed: # {"status": False, "payload": None, "error": "Error Text"} # or if some of deletions failed: # {"status": False, "payload": {"failed": ["128222344424100726555851618391811195555"]}, "error": "Error Text"} ``` ## Expiration _Read online: https://montygovernance.com/docs/expiration_ ### In-memory keyspaces and timer Montycat maintains **in-memory** data and uses a **background timer** task to automatically remove in-memory records that are set to expire. The timer interval is set to 1 second by default. At each interval, Montycat checks for expired records and removes them from memory. If a 1-second interval is not required, the check rate can be configured to match your application's needs. ``` montycat expiration-check 10 ``` or ``` montycat console > expiration-check 10 exit ``` If you use **containerized** Montycat you can change environmental variable **MONTYCAT_EXPIRATION_RATE** to set a new value for timer check rate. That new value will be saved in the configuration file. Docker: ```bash docker pull montygovernance/montycat:latest docker run -d -e MONTYCAT_EXPIRATION_RATE=10 -p 21210:21210 -p 21211:21211 -v ./data:/var/lib/.montycat --name montycat_instance montygovernance/montycat:latest ``` Debian / apt: ```bash # N/A ``` You can also change the expiration check rate directly from the client (requires **superowner** credentials): ```python # Set how often the server scans for expired in-memory keys, in whole seconds # (rate=10 -> a scan every 10 seconds). Stored as-is, like the snapshot rate. # Defaults to 1 second. Requires superowner credentials. await connection.set_expiration_check_rate(10) # {"status": True, "payload": "Expiration-check rate updated", "error": None} ``` ### Insertion with expiration flag If you insert into in-memory keyspace, you can set optional expiration argument in seconds. Database timer will remove the record when time marker you set reached. Timer works as a non-blocking background task. ```python from keyspaces import EmployeesInMem #keyspaces.py from schemas import EmployeesSchema #schemas.py array = [ {"username": "Name1", "location": "Location1", "age": 22}, {"username": "Name2", "location": "Location2", "age": 23}, {"username": "Name3", "location": "Location3", "age": 24}, ] res1 = await EmployeesInMem.insert_bulk(bulk_values=array, expire_sec=60 * 60) res2 = await EmployeesInMem.insert_custom_key(custom_key="myCustomKey", expire_sec=60 * 60) res3 = await EmployeesInMem.insert_custom_key_value(custom_key="myCustomKey", value="John", expire_sec=60 * 60) res4 = await EmployeesInMem.insert_value({"username": "John", "age": 28}, expire_sec=60 * 60) ``` ### Updating an expiration flag You can update an expiration time for a single record for records in in-memory keyspaces. ```python #main.py from keyspaces import EmployeesInMem #keyspaces.py from schemas import EmployeesSchema value = EmployeesSchema( username = "Name", location = "Location", age = 21 ).serialize() res1 = await EmployeesInMem.insert_value(value, expire_sec=60 * 60) # {"status": True, "payload": "128222336824100726154851618391811195396", "error": None} res2 = await EmployeesInMem.update_value(key="128222336824100726154851618391811195396", age=22, expire_sec=60 * 120) # {"status": True, "payload": None, "error": None} ``` ## Subscriptions _Read online: https://montygovernance.com/docs/subscriptions_ ### Subscription overview Montycat provides a native subscription mechanism that allows clients to receive real-time notifications about changes in keyspaces. Subscriptions can be set up for both in-memory and persistent keyspaces. When a record is inserted, updated, or deleted, all clients subscribed to that keyspace are notified with the details of the change. ### Create a subscription To create a subscription, clients need to send a subscription request to Montycat, specifying the keyspace or even particular key they want to subscribe. > Subscriptions are not persistent. They will be lost if the client disconnects. Clients can re-subscribe to the same keyspace or key after reconnecting. > **Note:** Subscriptions can be created only for existing **persistent** keyspaces. ```python from keyspaces import Employees #keyspaces.py def callback_on_data(data): print("Received data:", data) sub_task, stop_event = await Employees.subscribe(callback=callback_on_data) # {"status": True, "message": "Subscription started", "payload": null, "error": null} # on every insert/update in the keyspace: { # "status": True, # "message": "Key inserted/updated", # "payload": { # "__key__": "128222336824100726154851618391811195396", # "__value__": {"username": "Name", "location": "Location", "age": 21} # }, # "error": null # } # on every removal in the keyspace: { # "status": True, # "message": "Key removed", # "payload": { # "__key__": "128222336824100726154851618391811195396", # }, # "error": null # } ``` ### Subscribe on a specific key Clients can subscribe to a specific key within a keyspace by sending a subscription request to Montycat, specifying the key they want to monitor. ```python from keyspaces import Employees #keyspaces.py async def callback_on_data(data): print("Received data:", data) subscription, stop_event = await Employees.subscribe( key="128222336824100726154851618391811195396", callback=callback_on_data ) ``` ### Cancel a subscription Clients can cancel their subscriptions at any time by sending a cancellation request. ```python from keyspaces import Employees #keyspaces.py import asyncio def callback(response): print(f"Received data: {response}") async def subscription(): sub_task, stop_event = await Employees.subscribe(callback=callback) await asyncio.sleep(100) stop_event.set() # stops the subscription after 100 seconds ``` ### Allow / restrict subscriptions Subscriptions can be governed engine-wide. Use these switches to allow or restrict clients from opening subscriptions to the database — for example to shed load or lock down a node. ```python # Engine-wide switch: allow clients to open subscriptions to this database. await connection.allow_subscriptions() # {"status": True, "payload": None, "error": None} # Restrict subscriptions engine-wide (existing and new subscription attempts): await connection.restrict_subscriptions() ``` ## Snapshots _Read online: https://montygovernance.com/docs/snapshots_ ### Snapshots overview Montycat provides **snapshotting capabilities for in-memory keyspaces**, enabling you to persist their state to disk without sacrificing performance. Snapshots are incremental and efficient—they are only triggered when changes (insertions, updates, or deletions) occur within a keyspace. Snapshots serve as periodic checkpoints: they capture the current state of a keyspace according to the interval you configure. On startup, in-memory indexes are not loaded from snapshots but reconstructed in the background, ensuring full data consistency. You control snapshotting with two levels of configuration: - **Global snapshot** rate: Defines the **system-wide interval (in seconds)** at which Montycat checks for changes and writes updated snapshots to disk. - **Per-keyspace snapshot** settings: Enable, disable, or clean snapshots on a per-keyspace basis. ``` montycat snapshot-rate 60 ``` or ``` montycat console > snapshot-rate 60 exit ``` or set it in Docker: Docker: ```bash docker pull montygovernance/montycat:latest docker run -d -e MONTYCAT_SNAPSHOT_RATE=60 -p 21210:21210 -p 21211:21211 -v ./data:/var/lib/.montycat --name montycat_instance montygovernance/montycat:latest ``` Debian / apt: ```bash # N/A ``` You can also set the snapshot rate directly from the client (requires **superowner** credentials): ```python # Set the DB-wide snapshot rate in seconds (0 disables snapshots). # Requires superowner credentials. await connection.set_snapshot_rate(60) # {"status": True, "payload": "Snapshot rate updated", "error": None} ``` ### Enable snapshots You can enable snapshotting for individual in-memory keyspaces. ``` montycat console > do-snapshots-for-keyspace store keyspace exit ``` ```python from montycat import Keyspace from connection import connection #connection.py class EmployeesInMem(Keyspace.InMemory): keyspace = "EmployeesInMem" EmployeesInMem.connect_engine(connection) res = await EmployeesInMem.do_snapshots_for_keyspace() ``` ### Disable snapshots You can selectively disable snapshotting for some keyspaces while keeping it enabled for others. When disabled, Montycat preserves the last saved snapshot but does not create new ones. ``` montycat console > stop-snapshots-for-keyspace store keyspace exit ``` ```python from montycat import Keyspace from keyspaces import EmployeesInMem res = await EmployeesInMem.stop_snapshots_for_keyspace() ``` ### Cleaning up snapshots Cleaning snapshots removes the last saved state of a keyspace from disk. This operation is destructive, so it should be used with caution. ``` montycat console > clean-snapshots-for-keyspace store keyspace exit ``` ```python from keyspaces import EmployeesInMem #keyspaces.py res = await EmployeesInMem.clean_snapshots_for_keyspace() ``` ## Credentials _Read online: https://montygovernance.com/docs/credentials_ ### How credentials work? Montycat has a two-tier user system to manage security and access control: The Superowner acts as the top-level administrator. - **Create** and manage stores and keyspaces. - **Assign** permissions (READ, WRITE, ALL) to Owners. Only one Superowner can exist per Montycat instance. - Owners are users responsible for **managing stores and keyspaces** within their assigned scope. - Permissions are restricted by default and must be explicitly granted by the superowner. Owners cannot create other owners or assign permissions beyond their granted scope. ### Superowner creation You can create a superowner using CLI. Only one Superowner per Montycat instance is allowed. Attempting to create another will fail. ``` montycat create-superowner --username --password ``` or ``` montycat console > create-superowner username password exit ``` ### Create an owner An Owner is created by the Superowner. They can manage specific stores and keyspaces: ``` montycat console > create-owner username password exit ``` ```python from montycat import Engine connection = Engine( host="", port=, username="", password="", ) res = await connection.create_owner(owner="", password="") ``` ### Owner removal Owners can be removed only by the superowner: ``` montycat console > remove-owner username exit ``` ```python from montycat import Engine connection = Engine( host="", port=, username="", password="", ) res = await connection.remove_owner(owner="") ``` ### Granting permissions Owners are restricted by default. Permissions (READ, WRITE, ALL) can be granted per store or keyspace. > **Note:** If a keyspace exists both in-memory and persistent, granting permission affects both instances. ``` montycat console > grant-to owner permission all store exit ``` ``` montycat console > grant-to owner permission all store keyspaces exit ``` #### Clients API If you use clients make sure you specified a store property. > **Note:** Clients API does not support granting permissions at keyspace level, only at store level. > **Valid permissions:** read, write, all ```python from montycat import Engine, Permission connection = Engine( host="", port=, username="", password="", store="" ) res = await connection.grant_to(owner="", permission=Permission.ALL, keyspaces=["Employees"]) # 'Permission' can be ALL, READ, WRITE # 'keyspaces' arg is optional ``` ### Revoke permissions Permissions can be revoked at store or keyspace level. As with granting, if a keyspace exists in both in-memory and persistent storage, revoking affects both. ``` montycat console > revoke_from owner permission all store exit ``` ``` montycat console > revoke_from owner permission all store keyspaces exit ``` #### Clients API If you use clients make sure you specified a **store** property. ```python from montycat import Engine, Permission connection = Engine( host="", port=, username="", password="", store="" ) # 'Permission' can be ALL, READ, WRITE # 'keyspaces' arg is optional res = await connection.revoke_from(owner="", permission=Permission.ALL, keyspaces=["Employees"]) ``` ### Listing existing owners You can list all current owners and their access levels: ``` montycat console > list-owners exit ``` #### Clients API ```python from montycat import Engine connection = Engine( host="", port=, username="", password="", ) res = await connection.list_owners() # {"status": True, "payload": {"Owner1": {"read": [...], "write": [...], "all": []}}, "error": None} ``` ## Data-mesh governance _Read online: https://montygovernance.com/docs/governance_ ### Overview Governance lets a superowner delegate administrative capabilities without giving every team full control of the database. Policies bind an owner to a store and can optionally narrow authority by keyspace, storage type, or semantic model. This supports shared and multi-tenant deployments where domain teams operate their own data products while a platform team retains central control. Governance policies control administrative operations. They complement, rather than replace, the `READ`, `WRITE`, and `ALL` data-access permissions described under Credentials. Holding a governance capability never implies data access. Policy mutations require superowner credentials, and authorization is deny-by-default: an owner can perform only what an explicit grant or its own creator rights allow. For the architectural view of how this maps onto domain-owned data products, see [Data Mesh](/data-mesh). ### Capabilities - `provision-keyspace` — Create keyspaces in a store. This is store-scoped. - `remove-keyspace` — Remove keyspaces within the delegated scope. - `manage-snapshots` — Manage snapshots. Snapshots are always in-memory. - `manage-semantic` — Enable, disable, or configure semantic search. - `manage-schema` — Create, change, preview, or remove schemas. - `manage-access` — Reserved for delegating data-access permissions. The capability is part of the policy model and can be granted, but it has no runtime enforcement yet. ### Scopes and qualifiers Every policy identifies an owner, capability, and store. A keyspace narrows applicable capabilities to one data product. The `provision-keyspace` capability is store-scoped because the target keyspace does not exist yet, so its keyspace argument is ignored. - `types` is optional for provisioning, removal, schema, access, and semantic management. - `types` is invalid for `manage-snapshots`; snapshots always apply to in-memory keyspaces. - `models` is optional and valid only for `provision-keyspace` and `manage-semantic`. - Omitting an optional qualifier grants or denies the capability across all values represented by that qualifier. ### Grants, revocations, and explicit denials Use `policyGrant` to delegate authority and `policyRevoke` to remove a grant. Use `policyDeny` when an operation must remain prohibited even when a broader grant exists, and `policyRemoveDenial` to remove that explicit denial. Preview grant and revoke operations before changing active policy with `policyPreviewGrant` and `policyPreviewRevoke`. Not every capability has to be granted. An owner that creates a keyspace automatically receives read and write access to it, snapshot management when the keyspace is in-memory, semantic management limited to the models applicable policy allows, and the right to remove it. An explicit denial suspends any of these creator capabilities for a specific resource, and outranks a broader grant. ```python from montycat import PolicyCapability, PolicyKeyspaceType, SemanticModel await connection.policy_grant( owner="alice", capability=PolicyCapability.PROVISION_KEYSPACE, store="catalog", types=[PolicyKeyspaceType.IN_MEMORY, PolicyKeyspaceType.PERSISTENT], models=[SemanticModel.BGE_SMALL], ) await connection.policy_grant( owner="alice", capability=PolicyCapability.MANAGE_SEMANTIC, store="catalog", keyspace="products", types=[PolicyKeyspaceType.IN_MEMORY], models=[SemanticModel.BGE_SMALL], ) await connection.policy_view(owner="alice", store="catalog") ``` ```python from montycat import PolicyCapability, PolicyKeyspaceType policy = { "owner": "alice", "capability": PolicyCapability.MANAGE_SCHEMA, "store": "catalog", "keyspace": "products", "types": [PolicyKeyspaceType.PERSISTENT], } preview = await connection.policy_preview_revoke(**policy) await connection.policy_revoke(**policy) await connection.policy_deny(**policy) await connection.policy_remove_denial(**policy) ``` ### Inspection, explanation, and audit - `policyView` shows the effective policy, optionally filtered by owner and store. - `policyExplain` explains whether a specific capability is allowed for an owner and scope. - `policyHistory` returns policy changes and can be filtered by owner, store, and keyspace. - `policyExport` exports the active policy as JSON or YAML. ```python from montycat import PolicyCapability, PolicyKeyspaceType, SemanticModel effective = await connection.policy_view(owner="alice", store="catalog") history = await connection.policy_history( owner="alice", store="catalog", keyspace="products" ) decision = await connection.policy_explain( capability=PolicyCapability.MANAGE_SEMANTIC, store="catalog", owner="alice", keyspace="products", keyspace_type=PolicyKeyspaceType.IN_MEMORY, model=SemanticModel.BGE_SMALL, ) ``` ### Policy manifests Policy manifests make governance repeatable and reviewable as infrastructure as code. Use `policyValidate` to check a JSON or YAML document, `policyPlan` to inspect the changes it would make, and `policyApply` to apply it. A safe deployment flow is validate, plan, review, and then apply. ```python from montycat import PolicyFormat with open("governance.yaml", encoding="utf-8") as file: document = file.read() validation = await connection.policy_validate(document, format=PolicyFormat.YAML) plan = await connection.policy_plan(document, format=PolicyFormat.YAML) # Review the returned plan before applying it. result = await connection.policy_apply(document, format=PolicyFormat.YAML) backup = await connection.policy_export(format=PolicyFormat.YAML) ``` ### Data-mesh and multi-tenant organization Treat stores as shared platform boundaries and keyspaces as domain-owned data products. Delegate only the capabilities each domain needs, constrain storage types or semantic models where platform cost and compliance require it, and use explicit denials for protected resources. Review effective policies and history as part of regular access audits. ## Advanced _Read online: https://montygovernance.com/docs/advanced_ ### Change number of default permits Montycat automatically determines the number of simultaneous operations it can handle based on system resources such as available memory, CPU cores, and disk capacity. By default, the system allocates 500 permits per thread. You can override this value if needed. > **Note:** increasing the number of permits does not guarantee better performance. Setting this value too high may exhaust system resources and degrade overall stability. For example, on a system with 8 threads, configuring 1000 permits per thread results in a total of 8000 permits. The change requires a restart. After restart, Montycat automatically redistributes permits across all background services and tasks. ``` montycat default-permits 8000 ``` or ``` montycat console > default-permits 8000 exit ``` ### Set host and port By default, Montycat binds to 0.0.0.0:21210 (and 21211 for subscription connections), which means it listens on all available network interfaces at ports 21210/21211. You can override this behavior by specifying a dedicated IP address and/or a custom port. > **Note:** Port changing will require a restart. ``` montycat setup --host 192.168.170.17 --port 21218 # subscription server will be set to 21219 by default ``` or ``` montycat console > setup host 192.168.170.17 port 21218 exit ``` ### Allow reporting / Disable reporting By default, Montycat asynchronously with no performance impact collects anonymous usage statistics to help improve the product. This data includes information about performance metrics, bugs and issues. No personal or sensitive data is collected. You can disable this feature if you prefer not to share usage data. ``` montycat disable-reports ``` or ``` montycat console > disable-reports exit ``` You can also enable reporting. ``` montycat enable-reports ``` or ``` montycat console > enable-reports exit ``` You can also set reporting flag using Docker Docker: ```bash # Pull Montycat official Docker image docker pull montygovernance/montycat:latest # Run Montycat with reporting disabled docker run -d -e MONTYCAT_REPORTS_ALLOWED=false -p 21210:21210 -p 21211:21211 -v /local/path/.montycat:/var/lib/.montycat --name montycat_instance montygovernance/montycat:latest ``` Debian / apt: ```bash # N/A ``` ### Enable wait-for-index For maximum speed and productivity, any CRUD operation does not wait for indexing Write-Ahead Log (WAL) to complete in persistent keyspaces. You can enable or disable the "wait-for-index" option based on your requirements. Then all your operations will wait for indexing to complete. Enabling the indexing wait can guarantee durability for write-heavy workloads, but may slightly slow down write operations. In case of In-Memory keyspaces wait-for-index feature will be automatically enabled as you enable snapshots. ``` montycat console > enable-wait-for-index exit ``` to disable it ``` montycat console > disable-wait-for-index exit ``` You can also toggle wait-for-index directly from the client: ```python # Make CRUD operations wait for the index to catch up (read-after-write) on # persistent keyspaces. Guarantees durability for write-heavy workloads at the # cost of slightly slower writes. await connection.enable_wait_for_index() # {"status": True, "payload": None, "error": None} # Back to fire-and-forget indexing (the default, faster writes): await connection.disable_wait_for_index() ``` Beyond the DB-wide switch, every write accepts a per-request **wait_for_index** argument that overrides the default for that single call — useful when one write needs read-after-write guarantees but the rest can stay fast. It is a no-op on in-memory keyspaces. ```python from keyspaces import Employees #keyspaces.py # Per-request override on a single persistent write: force THIS insert to wait # for indexing, regardless of the DB-wide default. (No-op on in-memory keyspaces.) await Employees.insert_value(record, wait_for_index=True) # Also available on update_value, insert_bulk, update_bulk, etc. await Employees.update_value(key, {"location": "Location"}, wait_for_index=True) ``` > **Note:** on **Montycat Semantic** **1.3.5 or newer**, `wait_for_index` also covers the semantic work a write queues — the keyword commit and reload, plus automatic embedding and vector persistence — so the write cannot return before its keyword or hybrid result is visible. ### Queue depths Montycat processes indexing, timestamps, sharding, and counting off the write hot path through internal task queues. You can inspect how much work is pending in each queue — useful for monitoring backpressure and tuning throughput on write-heavy workloads. ```python # Inspect background task queue depths — how much work is pending across the # engine's internal queues. Useful for monitoring backpressure and throughput. depths = await connection.queue_depths() # {"status": True, "payload": { ...per-queue depths... }, "error": None} ``` ### Connection pooling By default every request opens a connection, sends, reads one response, and closes it. Reuse the connection instead and the handshake disappears from every call after the first. The win scales with how much of your latency is connection setup: large for a chatty service issuing many small reads, larger over a network — where the handshake costs a full round trip before the query is even sent — and larger again with TLS. The Rust client measures **2.56x faster** on loopback against a debug engine, which is the conservative end of the range. Pooling is **opt-in**: one new argument on the engine, and no call site changes. ```python # connection.py from montycat import Engine, PoolConfig, close_all_pools connection = Engine( host="", port=, username="", password="", store="", pool=PoolConfig(), # the only new argument; omit for connect-per-request ) # Tune it if you need to. Defaults: max_idle=8, idle_timeout=30.0 seconds. # pool=PoolConfig(max_idle=4, idle_timeout=15.0) # Nothing at the call sites changes. res = await Employees.insert_value({"username": "Name", "age": 21}) # Before the process exits, and from a connectivity change if you observe one. await close_all_pools() ``` > In the Python, Node, and Dart clients pools live in a module-level registry keyed by **host, port, and TLS**, so every keyspace pointing at one server shares a single pool rather than each opening its own. TLS is part of the key because a plaintext and an encrypted connection to the same address are not interchangeable. > The Rust client is the exception: pooling is **per-engine**. Reuse one engine for the process lifetime — cloning is cheap and shares the pool, while building a fresh engine per request builds a fresh empty pool and amortises nothing. > An idle pooled connection still holds one of the engine's connection permits, so a large pool across many client processes can starve the server while mostly idle. The defaults are deliberately conservative — **8 idle connections**, dropped after **30 seconds** — and should be raised only after measuring with **queue depths** under realistic load. > Close the pool before the process exits. In Node idle sockets otherwise keep the process alive; in Rust, where `Drop` cannot be async, a TLS connection would otherwise close without **close_notify** and the server logs an error for each one. > Subscriptions are never pooled: they are long-lived, stream many responses to one request, and live on their own port. > iOS and Android close sockets when an app is backgrounded, so every pooled connection is dead on resume. The Dart client detects that before reusing one and opens a fresh connection, but the cost is a user-visible round trip — prefer a **short idleTimeout** on mobile. > Connectivity changes invalidate the pool as well: Wi-Fi to cellular kills every pooled connection. If your app already observes connectivity, call **closeAllPools()** on a change rather than discovering it one failed request at a time. Because of backgrounding, pooling helps least on mobile and most on server-side Dart. ## Security _Read online: https://montygovernance.com/docs/security_ ### Security overview Montycat combines multiple layers of security by default: safe API design, TLS, encrypted credentials, system file integrity, and enforced secret management. While other databases offer some of these features individually, Montycat integrates them seamlessly, making it easier to deploy a secure database out-of-the-box—and reducing reliance on user configuration. From eliminating raw database injections to providing native encryption in transit, Montycat ensures your data remains safe and confidential. Montycat does not allow raw queries, which eliminates the risk of SQL-style injection attacks present in many legacy databases. All operations are performed through safe, structured APIs, ensuring that user inputs cannot directly manipulate the underlying database engine. Montycat has native **TLS support**, encrypting all communication between the database server and its clients. Enabling TLS ensures that data in transit cannot be intercepted or tampered with. > **Default path to certificates:** **/var/lib/.montycat** on Linux and home directories on Windows and MacOS. > **Note:** Where a trusted certificate authority is available, use it. Self-signed certificates are the right answer for closed networks and internal deployments that no public CA can issue for — Montycat can generate one for you; see **Self-signed certificates** below. > **Note for Rust Client:** In order to use TLS with the Rust client, ensure you include the TLS feature flag in your Cargo.toml. Then call **engine.enable_tls()** for a publicly trusted certificate: > **montycat = { version = "1", features = ["tls"] }** ### Enabling TLS ``` montycat tls-paths --cert --key ``` or ``` montycat console > tls-paths cert key exit ``` Make sure the certificate and key files have the correct permissions on UNIX-based systems. Within Docker, you can set TLS paths using environment variables, and mount the certificate files as volumes. Docker: ```bash docker pull montygovernance/montycat:latest docker run -d \ --name montycat_instance \ -v ./data:/var/lib/.montycat \ -e MONTYCAT_TLS_CERT=/var/lib/.montycat/tls.crt \ -e MONTYCAT_TLS_KEY=/var/lib/.montycat/tls.key \ -p 21210:21210 \ -p 21211:21211 \ montygovernance/montycat:latest ``` Debian / apt: ```bash # N/A ``` ### Connecting clients with TLS TLS must also be enabled by the client. The setting applies to regular commands and subscriptions. ```python from montycat import Engine connection = Engine( host="db.example.com", port=21210, username="USER", password="PASSWORD", store="Company", tls=True, ) ``` ### Self-signed certificates Montycat can generate its own certificate, so an install can serve encrypted traffic without a certificate authority, an *openssl* binary, or a mounted certificate. Generation happens inside the engine and writes the key with **0600** permissions and the certificate with **0644**. There are two ways to trigger it. Set **MONTYCAT_INIT_SELF_TLS=true** and the certificate is generated at startup **before any listener binds**, so TLS is active on that same run — this is the right choice for containers. Or run **init-self-tls** in the console on a server that is already up; that reaches listeners which are already bound, so it reports **restart_required** and TLS activates on the next restart. Both are **idempotent**: existing certificate material is never replaced unless force is given explicitly ( **MONTYCAT_TLS_FORCE=true**, or **force true** in the console). Restarting a container does not churn the certificate. Docker: ```bash docker run -d \ --name montycat_instance \ -v ./data:/var/lib/.montycat \ -e MONTYCAT_INIT_SELF_TLS=true \ -e MONTYCAT_TLS_IP_ADDRESSES=192.168.1.20 \ -p 21210:21210 \ -p 21211:21211 \ montygovernance/montycat:latest # Read back what was generated, including the SHA-256 fingerprint: docker logs montycat_instance | grep -i tls ``` Debian / apt: ```bash # On an installed server, generate from the console instead. montycat console > init-self-tls ip 192.168.1.20 > init-self-tls dns db.example.com ip 192.168.1.20 days 365 > init-self-tls force true # replace existing material # Console generation reaches a server whose listeners are already bound, # so it reports restart_required. Restart to activate: montycat home montycat hunt ``` > Binding to **0.0.0.0** means "accept connections on every interface". It is not an address a client dials, so it is deliberately **not** put in the certificate, and neither is **::**. > With no configuration the certificate covers **DNS:localhost**, **IP:127.0.0.1**, and **IP:::1** for **365 days** — enough for local use. For anything else, pass the address clients actually use via **MONTYCAT_TLS_IP_ADDRESSES** (bare-IP deployments need no DNS names at all) or **MONTYCAT_TLS_DNS_NAMES**. A certificate whose identities do not include the dialed address will fail verification even though it is otherwise valid. > A self-signed certificate encrypts traffic, but nothing vouches for who is on the other end unless the client is told to trust that specific certificate. The Node, Python, and Dart clients currently accept any server certificate — traffic is encrypted but the server is **not authenticated**. > **Rust is different:** its client verifies the certificate chain and the server hostname. For a private CA or a Montycat-generated self-signed certificate, pass the generated PEM certificate explicitly: **engine.with_tls_ca_file("/path/to/tls.crt")?** . This also enables TLS. The hostname or IP used by the client must appear in the certificate's Subject Alternative Names; otherwise the connection is rejected. > Generation returns a **SHA-256 fingerprint**. Note it down and compare it out of band when distributing the certificate; verified TLS is the goal, and the fingerprint is how it will be pinned. > One more exposure to know about: **init-self-tls** sent to a server that does not yet have TLS travels over plaintext, credentials included. On an untrusted network use **MONTYCAT_INIT_SELF_TLS=true** at startup instead — it needs no network round trip. > Missing or unusable certificate material never prevents the server from starting, but it is always reported. No certificate configured logs at **info**; a certificate that is configured but broken — unparseable, half-present, or a key that does not belong to the certificate — logs at **error** with both paths and states that the listener is continuing in plaintext. Each listener also reports the mode it came up in, so check the logs after enabling TLS rather than assuming it took. ### File system All system files in Montycat are hashed and monitored for integrity. Any unauthorized access attempts or manual modifications to configuration or system parameters will be detected immediately and result in startup failures or operational errors, ensuring that tampering is prevented and the system remains secure. Montycat encrypts credential files and other sensitive system files automatically. During startup: - Montycat reads the initial secret stored locally. - Decrypts credentials and system files. - Loads them into memory for runtime usage. This default approach is suitable for testing and development but not recommended for production. ### Recommended Production Setup: - Use a dedicated environment variable with a strong secret. #### You can generate a new secret ``` montycat generate-new-secret ``` or ``` montycat console > generate-new-secret exit ``` Example output (44-character random secret): **Yp9KfAq2VWxB3j7N9GhRtUb5MzLsQeDcXo1K8nZyPwEr** #### Setting the Production Secret: ``` montycat console > set-prod-secret exit ``` This command removes the initial Montycat secret file. The new secret is temporarily stored as a local environment variable. For production, you must set it globally at the system level, otherwise Montycat will not start. ### Set secret variable with Docker. Docker: ```bash docker pull montygovernance/montycat:latest docker run -d \ -e MONTYCAT_PROD_SECRET= \ --name montycat_instance \ -p 21210:21210 \ -p 21211:21211 \ -v ./data:/var/lib/.montycat \ montygovernance/montycat:latest ``` Debian / apt: ```bash # Only YAML and Docker CLI is shown here # as they are the most common ways to set TLS environment variables. ``` ### Role-Based Access Control (RBAC) Montycat includes fine-grained role-based access controls for managing database privileges: - **Superowner** — has full control over the database, including system-level operations and ownership delegation. - **Owners** — responsible for specific resources. Owners cannot override superowner privileges but maintain full autonomy over their assigned domains. This ensures separation of duties, prevents privilege escalation, and provides administrators with enterprise-grade access control built-in by default. ### Best Practices for Maximum Security Always use TLS in production to encrypt all traffic. Use strong, randomly generated secrets for credentials encryption. Do not store secrets in plaintext; prefer environment variables or secure vault solutions. Restrict file permissions on key and certificate files to minimize exposure. Regularly rotate secrets and certificates for ongoing security compliance. Apply Montycat's RBAC model to enforce least-privilege and reduce insider risk. --- ## Cross-language naming The API is identical across clients; only casing and a few idioms differ: - **Python / Rust**: snake_case (`insert_value`, `semantic_search_get_values`, `enable_wait_for_index`). - **TypeScript / JavaScript / Dart**: camelCase (`insertValue`, `semanticSearchGetValues`, `enableWaitForIndex`); options passed as an object / named parameters (e.g. `insertValue({ value, waitForIndex: true })`). - Python/Dart/JS require `.serialize()` on schema records before writing; Rust uses `#[derive(RuntimeSchema)]` and serializes natively (needs the `montycat_serialization_derive` crate for schema definitions). ## Clients & distribution - Meow Desktop 0.1.7: https://montygovernance.com/desktop — released visual database GUI for macOS, Windows, and Linux; detects missing or outdated Montycat engines and checks for Meow updates. Downloads require user approval and open in the native operating-system installer. Linux offers DEB and portable AppImage packages. Timestamp filters support Range, Before, and After; persistent-keyspace cache settings use cache-only updates; boolean filters use a switch. Release notes: https://montygovernance.com/release-notes#meow-desktop-0-1-7 - Montycat MCP 1.1.6: https://montygovernance.com/mcp-server — 24 MCP tools for shared persistent agent memory, semantic/keyword/hybrid recall, live updates, governance, and read-only enforced-schema inspection for correctly typed structured writes and filtered retrieval. Install with `uvx montycat-mcp`. - Python 1.2.6 (PyPI): https://pypi.org/project/montycat/1.2.6/ — `pip install montycat` - Node.js / TypeScript 1.3.5 (npm): https://www.npmjs.com/package/montycat/v/1.3.5 — `npm i montycat` - Dart / Flutter 1.2.5 (pub.dev): https://pub.dev/packages/montycat/versions/1.2.5 — `dart pub add montycat` - Rust 0.4.5 (crates.io): https://crates.io/crates/montycat/0.4.5 — `cargo add montycat` - Engine 1.3.8 downloads: https://montygovernance.com/download - Engine (Docker Hub): https://hub.docker.com/r/montygovernance/montycat - MCP server 1.1.6 (PyPI): https://pypi.org/project/montycat-mcp/1.1.6/ - MCP server (Claude Desktop): https://github.com/MontyGovernance/montycat-mcp/releases/latest/download/montycat-mcp.mcpb - MCP server (generic latest.mcpb alias for automated consumers): https://github.com/MontyGovernance/montycat-mcp/releases/latest/download/latest.mcpb - Source (GitHub): https://github.com/MontyGovernance - Docs: https://montygovernance.com/docs