-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: add Milvus vector index provider #404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # ADR 0003: External Vector Index Alongside Metadata Store | ||
|
|
||
| - Status: Accepted | ||
| - Date: 2026-04-15 | ||
|
|
||
| ## Context | ||
|
|
||
| ADR 0002 locked in repository-based storage with backend-aware vector search: the `inmemory` and `sqlite` backends do brute-force cosine, and `postgres` can use pgvector. Two footprints are not well served by that design: | ||
|
|
||
| - Multi-million-vector workloads, where brute-force is too slow and pgvector requires keeping Postgres hot enough to hold the index. | ||
| - Managed deployments that already standardise on a dedicated vector service (Milvus, Zilliz Cloud) and want memU to reuse it. | ||
|
|
||
| `settings.py` already separates `metadata_store` from `vector_index` in configuration, but every shipped backend kept vectors inside its own tables. The separation was not realised in code. | ||
|
|
||
| ## Decision | ||
|
|
||
| Introduce a `VectorIndex` protocol that memory repositories can delegate to when an external index is configured. The first implementation is `MilvusVectorIndex`, targeting Milvus Lite (zero-dep local), self-hosted Milvus, and Zilliz Cloud behind a single `uri`/`token` config. | ||
|
|
||
| - `VectorIndex` lives in `memu/database/vector_index/` and exposes `upsert`, `delete`, `delete_many`, `search`, `close`. | ||
| - `build_vector_index` in the factory constructs the provider based on `VectorIndexConfig.provider`. | ||
| - Metadata backends receive the vector index through their builder and mirror mutations to it on create / update / delete / clear. | ||
| - Search routes through the vector index when present; salience ranking stays local because it needs per-item reinforcement and recency factors the index does not persist. | ||
|
|
||
| Initial scope: the `inmemory` metadata backend is wired to `MilvusVectorIndex`. `sqlite` and `postgres` are planned follow-ups; their existing (brute-force / pgvector) paths remain the default so the change is additive. | ||
|
|
||
| ## Consequences | ||
|
|
||
| Positive: | ||
|
|
||
| - memU can scale vector similarity independently of the metadata store. | ||
| - Users on Zilliz Cloud or a shared Milvus cluster can reuse that infrastructure. | ||
| - Milvus Lite keeps the zero-setup local story (single file, no service) that `inmemory` already promises. | ||
|
|
||
| Negative: | ||
|
|
||
| - Two systems of record mean write amplification and a consistency window between the metadata store and the vector index. | ||
| - Scope fields are replicated into Milvus as dynamic fields, increasing storage vs. a single SQL column. | ||
| - `pymilvus` / `milvus-lite` are optional and gated behind the `milvus` extra; users picking this path accept that dependency. | ||
| - `milvus-lite <=2.5.1` still imports `pkg_resources`, so Python 3.13 users need `setuptools<81` until upstream switches to `importlib.metadata`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Milvus Vector Index | ||
|
|
||
| memU can route similarity search to [Milvus](https://milvus.io/) as an external vector index, decoupled from the metadata store. This is useful when: | ||
|
|
||
| - **Scale**: you have more vectors than the brute-force fallback can handle at latency budget. | ||
| - **Managed deployments**: you want to offload vector infrastructure to Zilliz Cloud or a self-hosted Milvus cluster. | ||
| - **Zero-setup local dev**: Milvus Lite runs as a local file (`./milvus.db`) with no external service. | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| uv sync --extra milvus | ||
| # or: uv pip install "memu-py[milvus]" | ||
| ``` | ||
|
|
||
| > The `milvus` extra pulls `pymilvus`, `milvus-lite` and β on Python 3.13 β a `setuptools<81` pin to work around [milvus-lite's use of the removed `pkg_resources` API](https://github.com/milvus-io/milvus-lite/issues). | ||
|
|
||
| ## Quick Start (Milvus Lite) | ||
|
|
||
| ```python | ||
| from memu.app import MemoryService | ||
|
|
||
| service = MemoryService( | ||
| llm_profiles={"default": {"api_key": "your-api-key"}}, | ||
| database_config={ | ||
| "metadata_store": {"provider": "inmemory"}, | ||
| "vector_index": {"provider": "milvus"}, | ||
| }, | ||
| ) | ||
| ``` | ||
|
|
||
| With the default configuration the index is persisted to `./milvus.db` using Milvus Lite β no Docker, no separate process. | ||
|
|
||
| ## Targeting a Milvus Server | ||
|
|
||
| ```python | ||
| database_config = { | ||
| "metadata_store": {"provider": "sqlite", "dsn": "sqlite:///memu.db"}, | ||
| "vector_index": { | ||
| "provider": "milvus", | ||
| "uri": "http://localhost:19530", | ||
| "collection_name": "memu_prod", | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| ## Targeting Zilliz Cloud | ||
|
|
||
| ```python | ||
| database_config = { | ||
| "metadata_store": {"provider": "postgres", "dsn": os.environ["POSTGRES_DSN"]}, | ||
| "vector_index": { | ||
| "provider": "milvus", | ||
| "uri": os.environ["ZILLIZ_URI"], | ||
| "token": os.environ["ZILLIZ_TOKEN"], | ||
| "collection_name": "memu_prod", | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| | Field | Default | Notes | | ||
| | --- | --- | --- | | ||
| | `provider` | β | Must be `"milvus"` to enable this index. | | ||
| | `uri` | `"./milvus.db"` | File path runs Milvus Lite; `http(s)://host:port` targets a Milvus server; a Zilliz Cloud endpoint targets the managed service. | | ||
| | `token` | `None` | Auth token for Zilliz Cloud or a secured Milvus server. | | ||
| | `collection_name` | `"memu_memory_items"` | Name of the Milvus collection that holds memory vectors. | | ||
| | `dim` | `None` | Embedding dimension. Inferred from the first upsert when omitted. | | ||
|
|
||
| ## Supported Combinations | ||
|
|
||
| The Milvus vector index can be paired with any metadata store. memU keeps the metadata record (summary, categories, scope, reinforcement stats, ...) in the metadata store and mirrors embeddings into Milvus on create / update / delete. | ||
|
|
||
| - **Available now**: `inmemory` metadata store + Milvus vector index (feature-complete). | ||
| - **Planned**: `sqlite` and `postgres` metadata stores wired to Milvus. See `docs/adr/0003-external-vector-index.md` for the rollout plan. | ||
|
Comment on lines
+71
to
+76
|
||
|
|
||
| ## How Search Works | ||
|
|
||
| 1. `create_item` / `update_item` mirror the embedding (and scope fields such as `user_id`, `agent_id`) into the configured Milvus collection. | ||
| 2. `vector_search_items` forwards the query vector to Milvus using a COSINE AUTOINDEX. Scope filters (e.g. `where={"user_id": "u1"}`) are translated into Milvus boolean expressions on dynamic fields. | ||
| 3. Milvus returns `(id, score)` pairs; the metadata store resolves them back to full memory records. | ||
| 4. Salience ranking (`ranking="salience"`) still runs locally because it needs per-item reinforcement and recency factors that the vector index does not store. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||||||||||||
| """Minimal end-to-end example: memU backed by a Milvus vector index. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Uses the ``inmemory`` metadata store and Milvus Lite (a single ``milvus.db`` | ||||||||||||||||||||||
| file) so it runs with zero external services. Point ``uri`` at a Milvus server | ||||||||||||||||||||||
| URL or a Zilliz Cloud endpoint to scale to production. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Run: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| uv sync --extra milvus | ||||||||||||||||||||||
| export OPENAI_API_KEY=sk-... | ||||||||||||||||||||||
| uv run python examples/milvus_vector_index.py | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||
| import os | ||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from memu.app import MemoryService | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async def main() -> None: | ||||||||||||||||||||||
| api_key = os.environ.get("OPENAI_API_KEY") | ||||||||||||||||||||||
| if not api_key: | ||||||||||||||||||||||
| msg = "Set OPENAI_API_KEY before running this example." | ||||||||||||||||||||||
| raise SystemExit(msg) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| file_path = os.path.abspath("example/example_conversation.json") | ||||||||||||||||||||||
| if not Path(file_path).exists(): | ||||||||||||||||||||||
| msg = ( | ||||||||||||||||||||||
| f"Example conversation not found at {file_path}. " | ||||||||||||||||||||||
| "Run from the repository root so the 'example/' folder is visible." | ||||||||||||||||||||||
|
Comment on lines
+29
to
+33
|
||||||||||||||||||||||
| file_path = os.path.abspath("example/example_conversation.json") | |
| if not Path(file_path).exists(): | |
| msg = ( | |
| f"Example conversation not found at {file_path}. " | |
| "Run from the repository root so the 'example/' folder is visible." | |
| file_path = os.path.abspath("tests/example/example_conversation.json") | |
| if not Path(file_path).exists(): | |
| msg = ( | |
| f"Example conversation not found at {file_path}. " | |
| "Run from the repository root so 'tests/example/example_conversation.json' is available." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
milvusbullet implies all metadata backends mirror writes and route searches throughVectorIndex, but in code only theinmemorybackend is currently wired to receive aVectorIndexinstance;sqlite/postgresignore it today. Consider tightening this wording (e.g., βcurrently only inmemory mirrorsβ¦β) to avoid suggesting functionality that isnβt implemented yet.