Skip to content

Python: add Milvus vector store (agent-framework-milvus) - #9346

Open
Nantha kumar (Nanduu24) wants to merge 20 commits into
microsoft:mainfrom
Nanduu24:python-milvus-vector-store
Open

Nantha kumar (Nanduu24) wants to merge 20 commits into
microsoft:mainfrom
Nanduu24:python-milvus-vector-store

Conversation

@Nanduu24

@Nanduu24 Nantha kumar (Nanduu24) commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a new agent-framework-milvus package providing a Milvus vector store for Agent Framework, implementing the BaseVectorStore / BaseVectorCollection / BaseVectorSearch interfaces. This makes Milvus — one of the most widely used open-source vector databases — available out of the box alongside the existing Postgres, Qdrant, Redis, MongoDB, SQL Server and other vector-store packages.

It follows the structure of the existing agent-framework-qdrant package (settings via load_settings, a store that mints collection clients, borrowed-vs-managed client ownership, operation_options validation, native filtering).

What's included

  • MilvusStore / MilvusCollection / MilvusSettings (+ py.typed).
  • Schema from the data model — the key field becomes the primary key (INT64 for int keys, VARCHAR otherwise), data fields become typed scalar fields, and vector fields become FLOAT_VECTOR fields, each with its own index.
  • Mappings — DistanceFunction → Milvus metric types (cosine_similarity/cosine_distance → COSINE, dot_prod → IP, euclidean_distance/euclidean_squared_distance → L2); IndexKind → Milvus index types (hnsw, flat, ivf_flat, disk_ann, default → AUTOINDEX).
  • Full surface — ensure_collection_exists / collection_exists / ensure_collection_deleted, upsert, get (by key or portable filter), delete, and vector search.
  • Portable filters translated to Milvus boolean expressions: eq, ne, gt, gte, lt, lte, between, in, not_in, and and/or/not groups. Operators that Milvus can't implement with correct null/collection semantics raise NotImplementedError rather than silently returning wrong results.
  • Connector-side score_threshold that respects each metric's comparison direction (larger-is-closer for COSINE/IP, smaller-is-closer for L2), since core does not post-filter.
  • Uses the native AsyncMilvusClient throughout, presenting the framework's async interface without wrapping a synchronous client.
  • README, a runnable sample (samples/milvus_vectors.py), and the package registered in the workspace [tool.uv.sources].

Review guide — what to focus on

The areas most worth a careful read, since they are where a vector-store connector most easily goes subtly wrong:

  • Existing-collection validation (_validate_existing_collection): when a collection already exists, the connector verifies it matches the data model — key presence and primary flag, INT64-vs-VARCHAR key type, each scalar field's DataType, and each vector field's FLOAT_VECTOR type, dimension, metric (via describe_index), and that it is not left on AUTOINDEX — rather than trusting it blindly.
  • Filter translation (eq/ne/gt/gte/lt/lte/between/in/not_in + and/or/not), and the deliberate NotImplementedError for operators Milvus can't express with correct null/collection semantics — the design choice is to fail loudly instead of returning wrong results.
  • Dense-vector validation (_validate_dense_vector): rejects non-numeric, bool, NaN/inf, and float32-overflow values before they reach the server.
  • score_threshold direction handling per metric, given core does not post-filter results.
  • Dependency floor and integration coverage (see Testing): the pymilvus>=2.6.0 floor and the new disposable-Milvus integration job.

Testing

  • Unit tests (tests/milvus/): filter translation, settings resolution/precedence, and collection behavior (upsert/get/delete/search, schema building, serialization, lifecycle, client ownership) — hermetic, no live service.
  • Integration tests gated behind MILVUS_TEST_URI for a real server, now run in CI against a disposable single-node Milvus (embedded etcd + local storage, no cloud credentials) via a new Python Integration Tests - Milvus job that mirrors the existing Qdrant and MongoDB jobs and feeds the aggregate report and the integration gate.
  • A dependency-bounds job installs the declared floor (pymilvus==2.6.0) and runs the unit tests + Pyright against it, so the minimum supported version stays covered.
  • Verified end-to-end against a live Milvus database (create → index → upsert → semantic search → portable-filter search → get with vectors → list → delete → lifecycle), all passing.
  • ruff check, ruff format --check, and Pyright (strict) are clean on the package; uv lock --locked succeeds.

Motivation

Milvus is a top-tier open-source vector database with no first-party Agent Framework integration. This PR implements the accepted feature request in #3488, with a connector that matches the conventions of the other vector-store packages.

Related issue

Closes #3488 — the agreed Milvus VectorStore connector feature request.

Adds a new agent-framework-milvus package providing MilvusStore, MilvusCollection
and MilvusSettings on the BaseVectorStore / BaseVectorCollection interfaces, so
Milvus is available as a vector store alongside the Postgres, Qdrant, Redis,
MongoDB and other connectors.

- The data model definition drives the collection schema: the key field becomes
  the primary key (INT64/VARCHAR), data fields become typed scalar fields, and
  vector fields become FLOAT_VECTOR fields, each with an index.
- Distance functions map to Milvus metrics (COSINE/IP/L2) and index kinds to
  Milvus index types (HNSW/FLAT/IVF_FLAT/DISKANN/AUTOINDEX).
- Portable filters (eq, ne, gt, gte, lt, lte, between, in, not_in and and/or/not
  groups) are translated to Milvus boolean expressions; connector-side score
  thresholds respect each metric's comparison direction.
- Upsert, get (by key or filter), delete, vector search and collection lifecycle,
  with the sync MilvusClient wrapped in asyncio.to_thread.
- README, a runnable sample, and unit tests (filters, settings, collection) plus
  integration tests gated behind MILVUS_TEST_URI. Registered in the workspace.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Connector correctness, package registration, CI coverage, integration testing, and documentation issues remain unresolved.

9 open findings
What changed in this PR

Adds a first-party Milvus vector-store connector implementing Agent Framework’s collection, store, filtering, lifecycle, and vector-search abstractions.

Changes:

  • Adds Milvus schema, CRUD, filtering, search, settings, and client ownership support.
  • Adds package documentation, samples, and unit-test scaffolding.
  • Registers and locks the new workspace package and dependencies.
File Description
python/​uv.lock Locks Milvus and transitive dependencies.
python/​pyproject.toml Registers the workspace source.
python/​packages/​milvus/​tests/​milvus/​test_settings.py Tests settings resolution.
python/​packages/​milvus/​tests/​milvus/​test_filters.py Tests filter translation.
python/​packages/​milvus/​tests/​milvus/​test_collection.py Tests collection behavior.
python/​packages/​milvus/​tests/​milvus/​conftest.py Defines test fixtures.
python/​packages/​milvus/​samples/​README.md Documents the sample.
python/​packages/​milvus/​samples/​milvus_vectors.py Demonstrates Milvus usage.
python/​packages/​milvus/​README.md Documents installation and APIs.
python/​packages/​milvus/​pyproject.toml Defines package metadata and tasks.
python/​packages/​milvus/​LICENSE Adds the MIT license.
python/​packages/​milvus/​agent_framework_milvus/​py.typed Marks typed-package support.
python/​packages/​milvus/​agent_framework_milvus/​_vector_store.py Implements the connector.
python/​packages/​milvus/​agent_framework_milvus/​__init__.py Exports public APIs.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/packages/milvus/pyproject.toml Resolved
Comment thread python/packages/milvus/pyproject.toml Resolved
Comment thread python/packages/milvus/README.md Resolved
Comment thread python/packages/milvus/pyproject.toml Resolved
- ensure_collection_exists: handle concurrent creation (treat an already-created
  collection as success instead of failing the ensure operation).
- Filter string literals are serialized with json.dumps so quotes, backslashes and
  control characters cannot break out of the Milvus expression.
- Reject is_indexed / is_full_text_indexed on data fields until scalar/full-text
  index creation is implemented, rather than silently ignoring them.
- Validate score_threshold as a finite, non-boolean number before searching.
- Qualify the Milvus Lite (local .db) docs: it requires the pymilvus[milvus_lite]
  extra; fix the README usage snippet (sequence upsert, generate_vectors, subscript
  SearchResponse fields).
- Add live integration tests exercising server_collection (CRUD, search, filters),
  selected by -m integration.
- Add a Milvus PR unit-test job and path filter so regressions are caught on PRs.
- Register the package in PACKAGE_STATUS.md (alpha / no).
@allyson-tcesp

Copy link
Copy Markdown

Great! Thx

@eavanvalkenburg

Copy link
Copy Markdown
Member

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (2 commit(s)): d121cfe0e10a, 73796abec852
Model: gpt-5.6-sol

Overview

The review found 2 verified inline finding(s).

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (2 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: python/packages/milvus/agent_framework_milvus/_vector_store.py

Addresses review feedback on microsoft#9346:

- ensure_collection_exists now validates an existing collection's fields, vector
  dimensions and metric types against the data model definition (and does the same
  after an already-exists race) instead of accepting a stale or unrelated schema.
- Portable filter operands are validated against the declared field type: a
  boolean operand on a numeric field (or any type mismatch) is rejected with a
  clear error rather than emitting an invalid comparison such as 'price == true';
  ordered comparisons require a numeric field.
- Added unit tests for schema mismatch (dimension, metric) and operand-type
  rejection.
…on_exists

Per review on microsoft#9346: create_collection builds the collection and then separately
creates indexes and loads it. Treating any exception as success when the collection
exists afterwards could mask an index or load failure, leaving an unindexed/unloaded
collection. Now only a genuine already-exists/concurrent-create MilvusException is
swallowed (then the existing collection is validated); all other create/index/load
failures are re-raised. Added tests for the index/load-failure and non-Milvus cases.
@Nanduu24

Copy link
Copy Markdown
Contributor Author

Heads-up on the red Public API Compatibility check — it isn't from this connector. griffe raises AliasResolutionError on agent_framework._workflows._edge.EdgeGroup.builtin_type while loading the base API, because that builtins.type alias is imported inside the EdgeGroup class body (so griffe sees it as a class member). It fails on every PR against main. I opened #9614 with a one-line fix (move the alias to module scope, matching _request_info_mixin.py); once it lands and I re-sync, this check should go green here. The connector's own checks pass.

@Nanduu24

Copy link
Copy Markdown
Contributor Author

Hi Eduard van Valkenburg (@eavanvalkenburg) — thanks for merging #9614. With that fix on main and the branch re-synced, the Public API Compatibility check (and all of CI) is now green here: 40 passing / 16 skipped, 0 failing.

The code-review items from your last pass are all addressed and pushed — schema safety (strict validation of an existing collection's key/primary/data/vector field types, dims, and metric), dense-vector validation (rejecting NaN/inf/overflow/non-numeric), and the concurrent-create handling.

The one item still awaiting your steer is live integration coverage. Milvus Lite can't be driven through MILVUS_URI (pymilvus reserves that env var for an http(s) server address and crashes at import on a local .db path), so the cleanest option looks like a docker run standalone Milvus service in the integration job, mirroring the existing Qdrant/MongoDB jobs. Happy to wire that up — just let me know if that approach works for you or if you'd prefer something else.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nantha kumar (@Nanduu24) Please address the inline dependency-floor finding, implement the integration-test direction in the existing thread, and add the missing reviewer-focus answer to the PR description.

Comment thread python/packages/milvus/pyproject.toml Resolved
- Raise the pymilvus dependency floor to >=2.6.0,<3 and update uv.lock
  metadata. 2.5.x AsyncMilvusClient lacks has_collection, list_collections,
  describe_collection, describe_index, and (in 2.5.3) prepare_index_params,
  so valid installs at the old 2.5.3 floor failed basic collection
  operations with AttributeError.
- Add a dependency-bounds workflow that installs the pymilvus==2.6.0 floor
  and runs the unit tests + Pyright against it, keeping the minimum
  supported version covered.
- Add a Milvus integration job to python-merge-tests.yml: a disposable
  single-node Milvus (embedded etcd + local storage, no credentials)
  started with docker, mirroring the Qdrant and MongoDB jobs, running
  poe test-integration and feeding the aggregate report and the gate.
- Gate integration tests on MILVUS_TEST_URI instead of MILVUS_URI so the
  suite does not collide with the env var pymilvus reserves for its
  default client.
Milvus 2.6 standalone panics with "embedded etcd can not be used under
distributed mode" when started with ETCD_USE_EMBED=true, because a
package-level init reads the config before `milvus run standalone` sets
the role, so the server never became healthy and the integration job
timed out waiting on :9091/healthz.

Replace the single embedded-etcd container with the official Milvus
standalone stack (external etcd + MinIO + Milvus) brought up via docker
compose, pinned to the image versions from the v2.6.20 release compose
(etcd v3.5.25, MinIO RELEASE.2024-12-18, milvus v2.6.20, MQ_TYPE
woodpecker). Still disposable and credential-free; the readiness loop and
test steps are unchanged.
The standalone stack failed to start because MinIO removed its public
Docker Hub image (`minio/minio` now 404s: "pull access denied ...
repository does not exist"), so `docker compose up` aborted before Milvus
started.

Milvus standalone does not need object storage over the network for this
test: set COMMON_STORAGETYPE=local on the standalone service and remove the
MinIO service entirely. The stack is now just external etcd + Milvus
(local-disk object storage, woodpecker WAL), which keeps the fix for the
embedded-etcd panic while removing the fragile image dependency.
…rage)

The standalone container started and reported healthy, but every integration
test hung at fixture setup and was killed by pytest-timeout (>120s), with no
Milvus error — a silent data-plane stall.

Cause: the job paired COMMON_STORAGETYPE=local with MQ_TYPE=woodpecker.
Woodpecker (Milvus 2.6's WAL) persists its segments to remote object storage,
so with local-disk storage and no MinIO/S3 it never acknowledges writes and
collection/insert operations block indefinitely. Milvus's own
standalone_embed.sh uses local storage without setting MQ_TYPE, letting
standalone fall back to the local-disk message queue.

Remove MQ_TYPE so local storage uses its matching default MQ.
@eavanvalkenburg

Copy link
Copy Markdown
Member

Nantha kumar (@Nanduu24) I found two additional current-head blockers:

  1. In python/packages/milvus/agent_framework_milvus/_vector_store.py:153, _format_scalar() uses json.dumps() with its default ensure_ascii=True, so filtering for a non-BMP string such as "😀" emits UTF-16 surrogate escapes. Milvus parses string literals through Go's strconv.Unquote, which rejects those escapes. Valid stored strings containing emoji or other supplementary-plane characters therefore fail in eq, ne, in, and not_in filters. Please use json.dumps(value, ensure_ascii=False) and add a non-BMP regression while retaining the existing quote/control-character coverage.

  2. In _vector_store.py:176-178, _check_scalar_operand() accepts floats for declared integer fields, but the generated expression preserves floating literals. Milvus's INT64 comparison and membership parser cannot cast them, so portable filters such as Filter("id", "eq", 1.0) and Filter("id", "in", [1.0]) fail instead of matching key 1; fractional ordered comparisons also fail. Please normalize integral floats and translate fractional comparisons exactly, or explicitly reject cases the connector cannot represent, and add integer-field tests for integral and fractional float operands.

Please address both before approval.

…elds; strong read-after-write

Addresses review findings and the integration read-after-write failures:

- Non-BMP string literals: _format_scalar now uses json.dumps(ensure_ascii=False).
  The default ensure_ascii=True emits 😀-style surrogate escapes for
  emoji / supplementary-plane characters, which Milvus's Go strconv.Unquote
  rejects, so eq/ne/in/not_in on such strings failed. Quote/backslash/control
  escaping is unchanged.

- Integer-field float operands: operands on INT64 fields are now normalized.
  Integral floats (1.0) emit integer literals so eq/ne/in match the key;
  fractional ordered comparisons are translated exactly (id > 1.5 -> id >= 2,
  id < 1.5 -> id <= 1, likewise for between bounds); fractional equality and
  membership are rejected since no integer key can equal them. Float fields are
  unaffected.

- Read-after-write: the collection is created with consistency_level=Strong and
  get/query/search default to Strong (both overridable via operation_options),
  so a record is visible immediately after upsert on a real Milvus server.
  Milvus Lite was already strongly consistent; the server default (bounded) made
  the integration tests' immediate reads intermittently miss fresh data.

Adds filter tests for non-BMP strings and integral/fractional float operands on
integer fields.
@Nanduu24

Copy link
Copy Markdown
Contributor Author

Thanks Eduard van Valkenburg (@eavanvalkenburg) — both addressed in d04a8b9:

  1. Non-BMP strings: _format_scalar now uses json.dumps(value, ensure_ascii=False), so emoji / supplementary-plane characters stay literal UTF-8 instead of 😀 surrogate escapes (which Go's strconv.Unquote rejects). Quote/backslash/control-character escaping is unchanged. Added a non-BMP regression covering eq and in.

  2. Integer-field float operands: operands on INT64 fields are now normalized. Integral floats (1.0) emit integer literals so eq/ne/in/not_in match the key; fractional ordered comparisons are translated exactly (id > 1.5 → id >= 2, id < 1.5 → id <= 1, and likewise for between bounds); fractional equality/membership is rejected, since no integer key can equal a fractional value. Float-declared fields are unaffected. Added integer-field tests for both integral and fractional operands.

Separately, now that the integration job runs against a real server it surfaced a third issue: immediate read-after-write missed fresh data under Milvus's default bounded consistency (Milvus Lite is strongly consistent, so it only showed on the server). The collection is now created with consistency_level=Strong and get/query/search default to Strong — both overridable via operation_options — which targets exactly the reads the integration tests do right after upsert.

65 unit tests pass; ruff and Pyright (strict) are clean.

…tory

The release-manifest validation (test_inventory_covers_every_workspace_distribution_
with_a_public_export) requires every workspace distribution to appear in
prepare_release.SMOKE_IMPORTS with a reviewed public module:export entry point.
Add agent-framework-milvus -> agent_framework_milvus:MilvusStore so the new
package passes release validation, matching the other vector-store connectors.
@Nanduu24

Copy link
Copy Markdown
Contributor Author

Status after syncing main into the branch and addressing the new validation checks:

  • Python Integration Tests - Milvus and Milvus pymilvus 2.6 floor are green — the disposable standalone-Milvus integration job runs the connector end-to-end, and the strong read-after-write fix resolved the earlier empty-read failures.
  • Validate Python release manifest is addressed: registered agent-framework-milvus → agent_framework_milvus:MilvusStore in prepare_release.py SMOKE_IMPORTS (the inventory check requires every workspace distribution to have a reviewed public entry point). Release-validation tests pass locally (88 passed).

One red check, Dependency Bounds Validation, is unrelated to this PR: it fails inside packages/azure-documentdb (a Pyright error at its dependency lower bound, _vector_store.py:902), which this PR does not touch. The repo-wide validate-dependency-bounds-test --package "*" surfaces it on any PR. The Milvus package itself passes its own lower-bound validation (pymilvus==2.6.0 floor job is green). Happy to help if useful, but it looked like a pre-existing issue in that package rather than something this change should carry.

@eavanvalkenburg

Copy link
Copy Markdown
Member

Nantha kumar (@Nanduu24) Independently created connector-owned AsyncMilvusClient instances can reuse the same global connection alias at the supported pymilvus==2.6.0 floor. Each store then believes it owns the client, but closing one removes the shared connection and breaks the other store’s later operations. Please allocate a unique SDK connection alias for each connector-owned client, or raise the dependency floor to a verified version with reference-counted connection ownership. Add a regression proving that closing one independently created store leaves another store usable.

…lias

At the pymilvus==2.6.0 floor, AsyncMilvusClient derives its connection alias
from the uri/auth/db_name and reuses any global connection already registered
under that alias (pymilvus.milvus_client._utils.create_connection). Two
connector-created stores pointing at the same server therefore shared one
connection, so closing one store (connections.async_remove_connection) tore
down the connection the other store still depended on.

Pass a unique alias for every connector-created client so each owns an
independent connection; caller-supplied clients are untouched. Add regression
tests proving two independently created stores get distinct aliases and that
closing one leaves the other connected.
@Nanduu24

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 45f2619.

Confirmed against the 2.6.0 source: AsyncMilvusClient derives its connection alias from the uri/auth/db_name and reuses any global connection already registered under it (pymilvus.milvus_client._utils.create_connection), so two connector-owned stores at the same server shared one connection — and close() → connections.async_remove_connection(alias) on one tore it down for the other.

I took the unique-alias route rather than raising the floor: each connector-created client now gets alias="agent-framework-milvus-<uuid4>", so it owns an independent connection. Caller-supplied clients are untouched and keep managing their own alias.

Regression added in test_settings.py:

  • test_connector_clients_get_unique_connection_alias — two stores get distinct aliases.
  • test_closing_one_store_leaves_independent_store_usable — closing one independently created store leaves the other connected (fails without the fix: both collapse to async-http://localhost:19530).

67 unit tests pass, ruff + pyright-strict clean.

This branch was previously deployed

1 inactive deployment
github-app-auth — 45f26195 Deployed Nov 3, 2026 by Nanduu24 via add_label #26985
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: MilvusDB Out-of-the-box VectorStore connector

5 participants