Skip to content

Commit 21a3076

Browse files
committed
fix(vector-store): address Milvus review feedback
1 parent a7b3793 commit 21a3076

13 files changed

Lines changed: 58 additions & 57 deletions

File tree

.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ VECTOR_STORE_MIGRATED=false
331331
# VECTOR_STORE_MILVUS_URI=./milvus.db
332332
# VECTOR_STORE_MILVUS_TOKEN=
333333
# VECTOR_STORE_MILVUS_DB_NAME=
334-
# VECTOR_STORE_MILVUS_CONSISTENCY_LEVEL=
334+
# VECTOR_STORE_MILVUS_CONSISTENCY_LEVEL=Session
335335

336336
# Reconciliation interval for background sync (default: 5 minutes)
337337
# VECTOR_STORE_RECONCILIATION_INTERVAL_SECONDS=300

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
99

1010
### Added
1111

12+
- Milvus vector store backend with Milvus Lite defaults, configurable server or Zilliz Cloud connectivity, and session-consistent reads (#876)
1213
- `api_request_duration_seconds` Prometheus histogram tracking per-route request latency, labeled by method and endpoint (#837)
1314
- LLM `provider_params` passthroughs (`extra_body` / `extra_headers` / `extra_query`) are now forwarded to the underlying provider transport across all backends, with shape validation that rejects non-mapping values (#821)
1415
- `structured_output_mode` model-config option to use `json_object` mode for OpenAI-compatible providers that lack native Structured Outputs support (used by the deriver) (#820)

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ src/
244244
├── vector_store/ # Optional external vector stores (pgvector is default,
245245
│ │ # implemented via MessageEmbedding/Document in models+crud)
246246
│ ├── lancedb.py
247+
│ ├── milvus.py
247248
│ └── turbopuffer.py
248249
├── telemetry/ # Observability
249250
│ ├── emitter.py # CloudEvents emitter
@@ -286,7 +287,7 @@ src/
286287
5. **Provider-agnostic LLM layer** (`src/llm/`): all model calls go through `honcho_llm_call()`. Backends (`anthropic`, `gemini`, `openai`) sit behind a registry; per-agent `MODEL_CONFIG` with fallback chains is resolved at call time.
287288
6. **Dialectic reasoning tiers**: 5 levels (`minimal``max`); each level has its own model config and tool set (`minimal` uses a reduced toolset).
288289
7. **Hybrid search**: Postgres FTS (GIN index on `to_tsvector('english', content)`) + vector similarity (HNSW on `MessageEmbedding.embedding`). `MessageEmbedding` is a separate table from `Message` with its own `sync_state` so embedding is decoupled from message creation.
289-
8. **Pluggable external vector stores**: defaults to pgvector inline; can swap to turbopuffer or lancedb (`VECTOR_STORE_*` config; `src/vector_store/`).
290+
8. **Pluggable external vector stores**: defaults to pgvector inline; can swap to turbopuffer, lancedb, or milvus (`VECTOR_STORE_*` config; `src/vector_store/`).
290291
9. **Composite-FK multi-tenancy**: `workspace_name` participates in nearly every composite FK. Cross-workspace data leakage is structurally impossible at the schema level.
291292
10. **Scoped Authentication**: JWTs can be scoped to workspace, peer, or session level.
292293
11. **Batch Operations**: Bulk message creation up to 100 messages per request.

config.toml.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,5 +287,5 @@ LANCEDB_PATH = "./lancedb_data"
287287
MILVUS_URI = "./milvus.db"
288288
# MILVUS_TOKEN = ""
289289
# MILVUS_DB_NAME = ""
290-
# MILVUS_CONSISTENCY_LEVEL = "Session"
290+
MILVUS_CONSISTENCY_LEVEL = "Session"
291291
RECONCILIATION_INTERVAL_SECONDS = 300

docs/changelog/introduction.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Welcome to the Honcho changelog! This section documents all notable changes to t
3030
<Update label="v3.0.11 (Current)">
3131
### Added
3232

33+
- Milvus vector store backend with Milvus Lite defaults, configurable server or Zilliz Cloud connectivity, and session-consistent reads (#876)
3334
- `api_request_duration_seconds` Prometheus histogram tracking per-route request latency, labeled by method and endpoint (#837)
3435
- LLM `provider_params` passthroughs (`extra_body` / `extra_headers` / `extra_query`) are now forwarded to the underlying provider transport across all backends, with shape validation that rejects non-mapping values (#821)
3536
- `structured_output_mode` model-config option to use `json_object` mode for OpenAI-compatible providers that lack native Structured Outputs support (used by the deriver) (#820)

docs/v3/contributing/changing-embeddings.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ If you are using `text-embedding-3-large` but truncating to 1536 (the default),
5757

5858
For this case, either set `EMBEDDING_VECTOR_DIMENSIONS=1536` explicitly (so `auto` knows the operator opted in), or set `EMBEDDING_MODEL_CONFIG__DIMENSIONS_MODE=always`.
5959

60-
## Backend swap (turbopuffer ↔ lancedb ↔ pgvector) is a different operation
60+
## Backend swap (turbopuffer ↔ lancedb ↔ milvus ↔ pgvector) is a different operation
6161

6262
Switching the *storage backend* at constant dim/model — for example moving from pgvector to Turbopuffer — is supported via `src/reconciler/sync_vectors.py` and `VECTOR_STORE_MIGRATED`. That flow is unchanged by the embedding-pipeline work and is documented separately. It is **not** the destroy + rebuild path described above.

docs/v3/contributing/configuration.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ Existing deployments at 1536 with `text-embedding-3-small` need no action — st
282282

283283
The script refuses to ALTER tables that already contain non-null embeddings. To switch dim or model on a populated deployment, stand up a new deployment at the new configuration and migrate data out of band; there is no in-place re-embedding affordance. See [Changing Embeddings](./changing-embeddings) for the destroy + rebuild recipe and the same-dim model-swap caveat.
284284

285-
External vector stores (Turbopuffer, LanceDB) do not need bootstrap setup. Namespaces are per-workspace and lazy-created on first write at whatever dim the embedding client returns. Use `--report` to inventory the existing namespaces against the configured dim:
285+
External vector stores (Turbopuffer, LanceDB, Milvus) do not need bootstrap setup. Namespaces are per-workspace and lazy-created on first write at whatever dim the embedding client returns. Use `--report` to inventory the existing namespaces against the configured dim:
286286

287287
```bash
288288
uv run python scripts/configure_embeddings.py --report
@@ -292,7 +292,7 @@ The startup validator at `src/startup/embedding_validator.py` enforces the dim i
292292

293293
`VECTOR_STORE_DIMENSIONS` is **deprecated**. `EMBEDDING_VECTOR_DIMENSIONS` is the single source of truth; setting `VECTOR_STORE_DIMENSIONS` explicitly emits a startup warning and is otherwise ignored. The field will be removed in a future release; drop it from your `.env` to silence the warning.
294294

295-
The `VECTOR_STORE_MIGRATED` flag still exists and still controls dual-write / cutover semantics for legacy tenants moving between storage backends (pgvector ↔ turbopuffer ↔ lancedb). It is unrelated to dimension configuration after this release.
295+
The `VECTOR_STORE_MIGRATED` flag still exists and still controls dual-write / cutover semantics for legacy tenants moving between storage backends (pgvector ↔ turbopuffer ↔ lancedb ↔ milvus). It is unrelated to dimension configuration after this release.
296296

297297
### Feature-Specific Model Configuration
298298

@@ -565,7 +565,7 @@ VECTOR_STORE_LANCEDB_PATH=./lancedb_data
565565
VECTOR_STORE_MILVUS_URI=./milvus.db
566566
VECTOR_STORE_MILVUS_TOKEN=
567567
VECTOR_STORE_MILVUS_DB_NAME=
568-
VECTOR_STORE_MILVUS_CONSISTENCY_LEVEL=
568+
VECTOR_STORE_MILVUS_CONSISTENCY_LEVEL=Session
569569
```
570570

571571
## Monitoring
@@ -711,6 +711,7 @@ ENABLED = false
711711
[vector_store]
712712
TYPE = "pgvector"
713713
MILVUS_URI = "./milvus.db"
714+
MILVUS_CONSISTENCY_LEVEL = "Session"
714715

715716
[sentry]
716717
ENABLED = false

scripts/configure_embeddings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
Existing 1536 deployments need no action — step 2 is a no-op when settings
1717
already match the schema.
1818
19-
This script never creates or modifies external-store namespaces. Turbopuffer
20-
and LanceDB namespaces are per-workspace and lazy-created on first write by
21-
application code; their dim is implicitly pinned at that point. Use
19+
This script never creates or modifies external-store namespaces. Turbopuffer,
20+
LanceDB, and Milvus namespaces are per-workspace and lazy-created on first write
21+
by application code; their dim is implicitly pinned at that point. Use
2222
``--report`` to enumerate existing namespaces against the configured dim.
2323
"""
2424

src/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,7 @@ class VectorStoreSettings(HonchoSettings):
13781378
MILVUS_DB_NAME: str | None = None
13791379
MILVUS_CONSISTENCY_LEVEL: (
13801380
Literal["Strong", "Session", "Bounded", "Eventually"] | None
1381-
) = None
1381+
) = "Session"
13821382

13831383
RECONCILIATION_INTERVAL_SECONDS: Annotated[int, Field(default=300, gt=0)] = (
13841384
300 # 5 minutes

src/vector_store/milvus.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import logging
1212
import re
1313
from collections.abc import Callable, Sequence
14+
from importlib.metadata import PackageNotFoundError
15+
from importlib.metadata import version as package_version
1416
from typing import Any, ParamSpec, TypeVar, cast
1517

1618
from pymilvus import DataType, MilvusClient
@@ -46,6 +48,16 @@
4648
_UNSAFE_COLLECTION_CHARS = re.compile(r"[^A-Za-z0-9_]")
4749

4850

51+
def _uses_lite_3_cosine_distance(uri: str) -> bool:
52+
"""Return whether the client uses Milvus Lite 3.0 distance semantics."""
53+
if "://" in uri:
54+
return False
55+
try:
56+
return package_version("milvus-lite") in {"3.0", "3.0.0"}
57+
except PackageNotFoundError:
58+
return False
59+
60+
4961
class MilvusVectorStore(VectorStore):
5062
"""
5163
Milvus implementation of the VectorStore interface.
@@ -364,6 +376,10 @@ async def query(
364376
"limit": top_k,
365377
"search_params": {"metric_type": DISTANCE_METRIC},
366378
}
379+
if settings.VECTOR_STORE.MILVUS_CONSISTENCY_LEVEL:
380+
search_kwargs["consistency_level"] = (
381+
settings.VECTOR_STORE.MILVUS_CONSISTENCY_LEVEL
382+
)
367383
if output_fields is not None:
368384
search_kwargs["output_fields"] = output_fields
369385

@@ -399,11 +415,16 @@ async def query(
399415

400416
def _hit_cosine_distance(self, hit: dict[str, Any]) -> float:
401417
"""Return Honcho cosine distance from a Milvus search hit."""
402-
if "distance" in hit:
403-
return float(hit["distance"])
404-
if "score" in hit:
405-
return 1.0 - float(hit["score"])
406-
return 0.0
418+
if "distance" not in hit:
419+
return 0.0
420+
421+
raw_distance = float(hit["distance"])
422+
# Milvus Lite 3.0 reports COSINE distance while Milvus server and
423+
# Zilliz Cloud report COSINE similarity. Keep this workaround scoped
424+
# to the affected Lite release: https://github.com/milvus-io/milvus-lite/issues/343
425+
if _uses_lite_3_cosine_distance(settings.VECTOR_STORE.MILVUS_URI):
426+
return raw_distance
427+
return 1.0 - raw_distance
407428

408429
def _output_fields(self, include_attributes: bool | list[str]) -> list[str] | None:
409430
"""Translate Honcho projection settings to Milvus output fields."""

0 commit comments

Comments
 (0)