milvus_dart

pub.dev Dart SDK License: MIT Platform

The first unofficial Dart/Flutter client for Milvus — the open-source, high-performance vector database built for AI applications.

Targets Milvus 2.5.x / 2.6.x. Ships with two transports:

  • REST (MilvusClient) — HTTP/2 + JSON, works on every Flutter target including Web.
  • gRPC (MilvusClient.grpc) — binary protobuf, lower latency, native platforms only.

Features

Category What's included
Collections create, drop, describe, list, load, release, stats, load state, rename, compact, alter properties
Indexes create, drop, describe, list — HNSW, IVF_FLAT, IVF_SQ8, FLAT, DiskANN, sparse, binary, GPU, scalar
Partitions create, drop, has, list, load, release, stats
Entities insert, upsert, delete (by IDs or filter), get by IDs
Search & Query ANN vector search, scalar query, range search, grouping search, hybrid multi-vector search with WeightedRanker / RRFRanker
Databases create, drop, list, describe, alter properties
Aliases create, drop, alter, list, describe
Bulk import submit, get state, list jobs
Users (RBAC) create, drop, update password, list, describe, grant/revoke role
Roles (RBAC) create, drop, list, describe, grant/revoke privilege
Resource groups create, drop, describe, list, transfer node/replica
Vector types FloatVector, BinaryVector, Float16Vector, BFloat16Vector, SparseFloatVector
Transport REST (all platforms, including Web) or gRPC (native platforms) — same API, swap one constructor
Iterators SearchIterator, QueryIterator — cursor-based paging that works around the 16,384 offset ceiling

Installation

dependencies:
  milvus_dart: ^0.1.0
dart pub get

Quick start

import 'package:milvus_dart/milvus_dart.dart';

void main() async {
  final client = MilvusClient(
    MilvusConfig(host: 'localhost', port: 19530),
  );

  final names = await client.collections.listCollections();
  print(names);

  await client.close();
}

Switch to gRPC with a single constructor change — the rest of the API is identical:

// gRPC transport — lower latency, native platforms only
final client = MilvusClient.grpc(
  MilvusConfig(host: 'localhost', port: 19530),
);

For Zilliz Cloud or token-secured self-hosted Milvus:

final client = MilvusClient(
  MilvusConfig(
    host: 'your-cluster.zillizcloud.com',
    port: 443,
    useTls: true,
    token: 'your-api-key',         // Zilliz Cloud API key
    // token: 'user:password',      // or base64-encode user:password for self-hosted
  ),
);

Usage examples

Create a collection

await client.collections.createCollection(
  CreateCollectionRequest(
    collectionName: 'articles',
    schema: CollectionSchema(
      enableDynamicField: true,
      fields: [
        FieldSchema(
          fieldName: 'id',
          dataType: DataType.int64,
          isPrimaryKey: true,
        ),
        FieldSchema(
          fieldName: 'title',
          dataType: DataType.varChar,
          elementTypeParams: {'max_length': 512},
        ),
        FieldSchema(
          fieldName: 'embedding',
          dataType: DataType.floatVector,
          elementTypeParams: {'dim': 768},
        ),
      ],
    ),
    // Supplying indexParams auto-loads the collection after creation.
    indexParams: [
      IndexParams(
        fieldName: 'embedding',
        indexName: 'embedding_hnsw',
        metricType: MetricType.cosine,
        indexType: IndexType.hnsw,
        params: {'M': 16, 'efConstruction': 200},
      ),
    ],
  ),
);

Insert vectors

final result = await client.entities.insert(
  InsertRequest(
    collectionName: 'articles',
    data: [
      {
        'id': 1,
        'title': 'Introduction to vector databases',
        'embedding': List.generate(768, (i) => i * 0.001),
      },
      {
        'id': 2,
        'title': 'Building RAG applications with Flutter',
        'embedding': List.generate(768, (i) => i * 0.002),
      },
    ],
  ),
);
print('Inserted ${result.insertCount} rows');
final queryVector = List.generate(768, (i) => i * 0.001);

final results = await client.search.search(
  SearchRequest(
    collectionName: 'articles',
    vectors: [queryVector],
    annsField: 'embedding',
    limit: 5,
    outputFields: ['id', 'title'],
    searchParams: {
      'metricType': 'COSINE',
      'params': {'ef': 64},
    },
  ),
);

for (final hit in results.first) {
  print('id=${hit.id}  distance=${hit.distance}  title=${hit.entity['title']}');
}

Scalar query (filter without vectors)

final rows = await client.search.query(
  QueryRequest(
    collectionName: 'articles',
    filter: 'id in [1, 2]',
    outputFields: ['id', 'title'],
    limit: 10,
  ),
);

Hybrid search (dense + sparse with reranking)

Hybrid search lets you combine a dense-vector ANN search with a sparse BM25 search and merge results using a reranker — a common pattern for retrieval-augmented generation (RAG).

final hits = await client.search.hybridSearch(
  HybridSearchRequest(
    collectionName: 'articles',
    searches: [
      // Dense vector leg
      AnnSearchRequest(
        data: [denseQueryVector],
        annsField: 'dense_embedding',
        limit: 20,
        searchParams: {'metricType': 'COSINE', 'params': {'ef': 64}},
      ),
      // Sparse vector leg (BM25)
      AnnSearchRequest(
        data: [{'3': 0.8, '17': 0.2, '42': 0.5}],
        annsField: 'sparse_embedding',
        limit: 20,
        searchParams: {'metricType': 'IP', 'params': {'drop_ratio_search': 0.2}},
      ),
    ],
    reranker: RRFRanker(),          // or WeightedRanker([0.6, 0.4])
    limit: 10,
    outputFields: ['id', 'title'],
  ),
);

Paginate with iterators

Milvus caps offset + limit at 16,384. Use SearchIterator or QueryIterator to page past that ceiling transparently:

// Stream all matching query rows, 200 at a time
final iterator = QueryIterator(
  transport: client.transport,  // via MilvusClient.withTransport or direct access
  request: QueryRequest(
    collectionName: 'articles',
    filter: 'year > 2020',
    outputFields: ['id', 'title'],
  ),
  pageSize: 200,
);

await for (final page in iterator.pages()) {
  for (final row in page) {
    print(row['title']);
  }
}
// Stream ANN search results page by page
final iterator = SearchIterator(
  transport: transport,
  request: SearchRequest(
    collectionName: 'articles',
    vectors: [queryVector],
    annsField: 'embedding',
    limit: 1000,
  ),
  pageSize: 100,
);

await for (final page in iterator.pages()) {
  for (final hit in page.first) {  // page.first = hits for the first query vector
    print('${hit.id}  ${hit.distance}');
  }
}

Delete entities

// By primary key list:
await client.entities.delete(
  DeleteRequest(collectionName: 'articles', ids: [1, 2]),
);

// Or by filter expression:
await client.entities.delete(
  DeleteRequest(collectionName: 'articles', filter: 'id > 100'),
);

Database multi-tenancy

await client.databases.createDatabase('tenant_a');

// Scope all operations to a specific database:
final config = MilvusConfig(host: 'localhost', database: 'tenant_a');
final tenantClient = MilvusClient(config);

User & role management (RBAC)

await client.users.createUser('alice', 'secret123');
await client.roles.createRole('read_only');
await client.roles.grantPrivilege('read_only', 'collection', 'Query', '*');
await client.users.grantRole('alice', 'read_only');

Error handling

Milvus always returns HTTP 200; errors are encoded in the response body. The client always inspects the response code and throws typed exceptions:

try {
  await client.collections.describeCollection('nonexistent');
} on CollectionException catch (e) {
  print('Collection error ${e.code}: ${e.message}');
} on AuthException catch (e) {
  print('Auth error: ${e.message}');
} on MilvusException catch (e) {
  // Base class — covers all other Milvus error codes.
  print('Milvus error ${e.code}: ${e.message}');
}
Exception class When thrown
AuthException Code 80 — bad credentials or missing token
CollectionException Codes 1000–1099 — collection-level errors
IndexException Codes 1100–1199 — index-level errors
EntityException Codes 1200–1299 — insert/delete/get errors
SearchException Codes 1300–1399 — search/query errors
RateLimitException Code 8 — server overloaded, retry with backoff
ServerException Codes 5000–5099 — internal server errors
MilvusException Base class for all other codes

Platform support

Platform REST gRPC Notes
Android
iOS
Web gRPC uses dart:io, unavailable on Web
macOS
Linux
Windows
Server-side Dart Pure Dart, no Flutter dependency

Running Milvus locally (Docker)

# Standalone mode — fastest way to get started
docker run -d \
  --name milvus-standalone \
  -p 19530:19530 \
  -p 9091:9091 \
  milvusdb/milvus:v2.5.0 \
  milvus run standalone

Then connect with MilvusConfig(host: 'localhost'). No token needed for a fresh local instance.


Testing your own code

Inject FakeTransport to write unit tests without a live Milvus server:

import 'package:milvus_dart/milvus_dart.dart';
import 'package:milvus_dart/src/transport/transport.dart'; // Transport interface

// See test/helpers/fake_transport.dart in the source for a ready-made helper.
final client = MilvusClient.withTransport(myFakeTransport);

Roadmap

Phase Status Scope
Phase 1 — Core MVP ✅ Done Collections, indexes, partitions, entities, search
Phase 2 — Full REST parity ✅ Done Databases, aliases, bulk import, users, roles, resource groups, hybrid search
Phase 3 — gRPC + iterators ✅ Done gRPC transport (MilvusClient.grpc), SearchIterator, QueryIterator
pub.dev publish Planned After example and coverage gates pass

Contributing

Issues and pull requests are welcome at github.com/behradkhadem/milvus_dart.

dart analyze          # must be zero-issue before any PR
dart test             # unit tests (no live Milvus needed)
MILVUS_HOST=localhost dart test test/integration/  # integration tests

License

MIT — see LICENSE.

Libraries

milvus_dart
Dart/Flutter client for the Milvus vector database.