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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ Try a prompt:
and Homebrew installs; `bm update` triggers a manual check.
- **Semantic vector search.** Find notes by meaning, not just keywords.
Hybrid full-text + vector ranking with FastEmbed embeddings, on SQLite or
Postgres.
Postgres, with optional Milvus vector storage.
- **Schema system.** Infer, validate, and diff the structure of your
knowledge base with `schema_infer`, `schema_validate`, `schema_diff`.
- **Per-project cloud routing.** Route individual projects through the cloud
Expand Down
63 changes: 63 additions & 0 deletions docs/semantic-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ All settings are fields on `BasicMemoryConfig` and can be set via environment va
| `semantic_embedding_document_prefix` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_DOCUMENT_PREFIX` | Unset | Optional literal text prefix prepended to indexed document chunks before embedding. |
| `semantic_embedding_query_prefix` | `BASIC_MEMORY_SEMANTIC_EMBEDDING_QUERY_PREFIX` | Unset | Optional literal text prefix prepended to search queries before embedding. |
| `semantic_vector_k` | `BASIC_MEMORY_SEMANTIC_VECTOR_K` | `100` | Candidate count for vector nearest-neighbour retrieval. Higher values improve recall at the cost of latency. |
| `semantic_vector_backend` | `BASIC_MEMORY_SEMANTIC_VECTOR_BACKEND` | `"database"` | Vector storage backend. `"database"` uses sqlite-vec or pgvector; `"milvus"` stores vectors in Milvus while keeping metadata in SQLite/Postgres. |
| `milvus_uri` | `BASIC_MEMORY_MILVUS_URI` or `MILVUS_URI` | `<config dir>/milvus.db` | Milvus URI when `semantic_vector_backend="milvus"`. Use a local `.db` path for Milvus Lite, `http://localhost:19530` for Milvus server, or a Zilliz Cloud endpoint. |
| `milvus_token` | `BASIC_MEMORY_MILVUS_TOKEN` or `MILVUS_TOKEN` | unset | Optional Milvus/Zilliz Cloud token. |
| `milvus_collection_name` | `BASIC_MEMORY_MILVUS_COLLECTION_NAME` | `"basic_memory_vectors"` | Milvus collection used for semantic chunks. |

## Embedding Providers

Expand Down Expand Up @@ -282,6 +286,47 @@ rebuild embeddings:
bm reindex --embeddings
```

## Milvus Vector Backend

By default, Basic Memory stores vectors in the active database: sqlite-vec for
SQLite and pgvector for Postgres. Advanced users can store semantic vectors in
Milvus while keeping full-text search, entity data, and chunk metadata in the
existing SQL backend.

Install the optional Milvus extra:

```bash
pip install "basic-memory[milvus]"
```

Enable the backend:

```bash
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true
export BASIC_MEMORY_SEMANTIC_VECTOR_BACKEND=milvus
```

The default `milvus_uri` points to a Milvus Lite database under Basic Memory's
config directory, so no server is required. To target another Milvus deployment:

```bash
# Milvus server
export MILVUS_URI=http://localhost:19530

# Zilliz Cloud
export MILVUS_URI=https://<cluster-endpoint>
export MILVUS_TOKEN=<api-key>
```

Milvus uses the same embedding provider settings as the database-backed vector
stores. After changing `semantic_vector_backend`, `milvus_uri`,
`milvus_collection_name`, model, dimensions, or provider-specific embedding
settings, rebuild embeddings:

```bash
bm reindex --embeddings --full
```

## Search Modes

### `text` (default)
Expand Down Expand Up @@ -412,3 +457,21 @@ The sqlite-vec extension is loaded per-connection. Vector tables are created laz
- **Index**: HNSW index on the embedding column for fast approximate nearest-neighbour queries

The Alembic migration creates the dimension-independent chunks table. The embeddings table and HNSW index are deferred to runtime because they depend on the configured vector dimensions.

### Milvus (optional)

- **Vector storage**: Milvus via `pymilvus.MilvusClient`, using `AUTOINDEX` and
COSINE scoring
- **Default local mode**: Milvus Lite at `<Basic Memory config dir>/milvus.db`
- **Server/cloud mode**: set `MILVUS_URI` and optionally `MILVUS_TOKEN`
- **SQL metadata**: `search_vector_chunks` stores chunk text and invalidation
metadata; `search_vector_embeddings` is a lightweight marker table used for
embedding coverage and incremental reindex checks
- **Collection schema**: Basic Memory creates a collection with `id`,
`project_id`, `chunk_id`, `entity_id`, `chunk_key`, `chunk_text`, and
`embedding` fields

If an existing Milvus collection has a different embedding dimension or is
missing required fields, Basic Memory fails fast. Use a new
`milvus_collection_name` or recreate the collection, then run
`bm reindex --embeddings --full`.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ Documentation = "https://github.com/basicmachines-co/basic-memory#readme"
basic-memory = "basic_memory.cli.main:app"
bm = "basic_memory.cli.main:app"

[project.optional-dependencies]
milvus = [
"pymilvus[milvus-lite]>=3.0.0,<4.0.0",
]

[build-system]
requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
build-backend = "hatchling.build"
Expand Down
53 changes: 51 additions & 2 deletions src/basic_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
WATCH_STATUS_JSON = "watch-status.json"
CONFIG_DIR_MODE = 0o700
CONFIG_FILE_MODE = 0o600
UNPREFIXED_ENV_ALIASES: dict[str, tuple[str, ...]] = {
"milvus_uri": ("MILVUS_URI",),
"milvus_token": ("MILVUS_TOKEN",),
}

Environment = Literal["test", "dev", "user"]

Expand Down Expand Up @@ -57,6 +61,13 @@ class DatabaseBackend(str, Enum):
POSTGRES = "postgres"


class SemanticVectorBackend(str, Enum):
"""Supported semantic vector storage backends."""

DATABASE = "database"
MILVUS = "milvus"


def _default_semantic_search_enabled() -> bool:
"""Enable semantic search by default when required local semantic dependencies exist."""
required_modules = ("fastembed", "sqlite_vec")
Expand Down Expand Up @@ -106,6 +117,13 @@ def default_fastembed_cache_dir() -> str:
return str(resolve_data_dir() / "fastembed_cache")


def default_milvus_uri() -> str:
"""Return the default Milvus Lite URI for the semantic Milvus backend."""
if env_override := os.getenv("MILVUS_URI"):
return env_override
return str(resolve_data_dir() / "milvus.db")


@dataclass
class ProjectConfig:
"""Configuration for a specific basic-memory project."""
Expand Down Expand Up @@ -375,6 +393,34 @@ def __init__(self, **data: Any) -> None: ...
"Valid values: text, vector, hybrid. "
"When unset, defaults to 'hybrid' if semantic search is enabled, otherwise 'text'.",
)
semantic_vector_backend: SemanticVectorBackend = Field(
default=SemanticVectorBackend.DATABASE,
description=(
"Semantic vector storage backend. 'database' uses sqlite-vec or pgvector "
"with the active database backend; 'milvus' stores vectors in Milvus while "
"keeping search metadata in the active database."
),
)
milvus_uri: str = Field(
default_factory=default_milvus_uri,
description=(
"Milvus URI for semantic_vector_backend='milvus'. Defaults to a Milvus Lite "
"database under the Basic Memory config directory. Can also be set with "
"MILVUS_URI."
),
)
milvus_token: str | None = Field(
default_factory=lambda: os.getenv("MILVUS_TOKEN"),
description=(
"Optional Milvus/Zilliz Cloud token for semantic_vector_backend='milvus'. "
"Can also be set with MILVUS_TOKEN."
),
)
milvus_collection_name: str = Field(
default="basic_memory_vectors",
description="Milvus collection name for semantic vector chunks.",
min_length=1,
)

# Database connection pool configuration (Postgres only)
db_pool_size: int = Field(
Expand Down Expand Up @@ -1070,8 +1116,11 @@ def load_config(self) -> BasicMemoryConfig:

merged_data = file_data.copy()
for field_name in BasicMemoryConfig.model_fields.keys():
env_var_name = f"BASIC_MEMORY_{field_name.upper()}"
if env_var_name in os.environ:
env_var_names = (
f"BASIC_MEMORY_{field_name.upper()}",
*UNPREFIXED_ENV_ALIASES.get(field_name, ()),
)
if any(env_var_name in os.environ for env_var_name in env_var_names):
# Trigger: the file and environment both configure this field.
# Why: BaseSettings only gives environment values precedence when the
# field is absent from constructor data. Removing the file value lets
Expand Down
Loading