@zilliz/milvus2-sdk-node
Version:
Official Node.js SDK for [Milvus](https://github.com/milvus-io/milvus) vector database. Provides gRPC and HTTP clients for vector similarity search, metadata filtering, and full collection/index/user management.
634 lines • 31.3 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GRPCClient = exports.LOADER_OPTIONS = void 0;
const grpc_js_1 = require("@grpc/grpc-js");
const dayjs_1 = __importDefault(require("dayjs"));
const generic_pool_1 = require("generic-pool");
const __1 = require("../");
const User_1 = require("./User");
// default loader options
exports.LOADER_OPTIONS = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true, // populate oneof fields
};
/**
* A client for interacting with the Milvus server via gRPC.
*/
class GRPCClient extends User_1.User {
/**
* Creates a new instance of MilvusClient.
* @param configOrAddress The Milvus server's address or client configuration object.
* @param ssl Whether to use SSL or not.
* @param username The username for authentication.
* @param password The password for authentication.
* @param channelOptions Additional channel options for gRPC.
*/
constructor(configOrAddress, ssl, username, password, channelOptions) {
// setup the configuration
super(configOrAddress, ssl, username, password, channelOptions);
// Incremented whenever the live telemetry endpoint changes. The telemetry
// manager uses this epoch to discard a response that arrives from an old
// endpoint after a successful global-cluster failover.
this.telemetryEndpointEpoch = 0;
// Async topology discovery and candidate validation can complete after close(). Fence
// their publication so a released candidate can never resurrect a closed client.
this.lifecycleGeneration = 0;
this.closed = false;
// Store sdkVersion for reconnection
this._sdkVersion = '';
// alias
this.useDatabase = this.use;
// Get the gRPC service for Milvus
this._MilvusService = (0, __1.getGRPCService)({
serviceName: this.protoInternalPath.serviceName, // the name of the Milvus service
}, Object.assign(Object.assign({}, exports.LOADER_OPTIONS), this.config.loaderOptions));
this._ClientTelemetryService = (0, __1.getGRPCService)({
serviceName: 'milvus.proto.milvus.ClientTelemetryService',
}, Object.assign(Object.assign({}, exports.LOADER_OPTIONS), this.config.loaderOptions));
this.telemetryDatabaseExplicit = Boolean(this.config.database);
this.telemetry = new __1.ClientTelemetryManager({
sender: request => this.sendTelemetryHeartbeat(request),
config: this.config.telemetry,
userProvider: () => this.config.username || '',
databaseProvider: () => this.telemetryDatabaseExplicit
? this.metadata.get(__1.METADATA.DATABASE) || __1.DEFAULT_DB
: '',
configProvider: () => ({
address: this.config.address,
username: this.config.username,
database: this.metadata.get(__1.METADATA.DATABASE) || __1.DEFAULT_DB,
ssl: this.config.ssl,
timeout: this.config.timeout,
}),
senderEpochProvider: () => this.telemetryEndpointEpoch,
});
// setup auth if necessary
const auth = (0, __1.getAuthString)(this.config);
if (auth.length > 0) {
this.metadata.set(__1.METADATA.AUTH, auth);
}
// setup database
this.metadata.set(__1.METADATA.DATABASE, this.config.database || __1.DEFAULT_DB);
// meta interceptor, add the injector
const metaInterceptor = (0, __1.getMetaInterceptor)(this.metadataListener.bind(this));
// retry interceptor
const retryInterceptor = (0, __1.getRetryInterceptor)({
maxRetries: typeof this.config.maxRetries === 'undefined'
? __1.DEFAULT_MAX_RETRIES
: this.config.maxRetries,
retryDelay: typeof this.config.retryDelay === 'undefined'
? __1.DEFAULT_RETRY_DELAY
: this.config.retryDelay,
clientId: this.clientId,
});
// interceptors
const interceptors = [metaInterceptor];
// add request metadata interceptor (adds client-request-unixmsec)
interceptors.push((0, __1.getRequestMetadataInterceptor)());
// add trace if necessary
if (this.config.trace) {
// add trace interceptor
interceptors.push((0, __1.getTraceInterceptor)());
}
// Heartbeats are best-effort: keep auth/request metadata/trace propagation, but do not
// let the ordinary RPC retry interceptor turn one heartbeat into a retry burst. The
// telemetry manager owns its own next-heartbeat/backoff policy.
this.telemetryChannelOptions = Object.assign(Object.assign({}, this.channelOptions), { interceptors: [...interceptors] });
// Ordinary Milvus RPCs retain the configured retry behavior.
this.channelOptions.interceptors = [...interceptors, retryInterceptor];
// For global cluster, skip pool creation here — pool will be created
// in connect() after topology is fetched and primary endpoint is resolved.
if (!this.isGlobal) {
this.channelPool = this.createChannelPool();
}
}
// create a grpc service client(connect)
connect(sdkVersion) {
if (this.closed) {
return;
}
this.lifecycleGeneration += 1;
const lifecycleGeneration = this.lifecycleGeneration;
this._sdkVersion = sdkVersion;
this.telemetry.setSdkVersion(sdkVersion);
if (this.isGlobal) {
// For global cluster: fetch topology → create pool → connect
this.connectPromise = this._initGlobalConnection(sdkVersion, lifecycleGeneration);
}
else {
// Normal connection
this.replaceTelemetryClient();
this.connectPromise = this._getServerInfo(sdkVersion, this.channelPool, true, lifecycleGeneration);
}
}
/**
* Initializes a global cluster connection.
* Fetches topology, resolves primary endpoint, creates pool, starts refresher.
*/
_initGlobalConnection(sdkVersion, lifecycleGeneration = this.lifecycleGeneration) {
return __awaiter(this, void 0, void 0, function* () {
const token = this.config.token || '';
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Initializing global cluster connection to ${this.globalEndpoint}`);
// Fetch topology to discover primary cluster
const topology = yield (0, __1.fetchTopology)(this.globalEndpoint, token);
if (!this.isLifecycleCurrent(lifecycleGeneration)) {
return;
}
this.globalTopology = topology;
// Resolve primary endpoint and create pool
const primary = (0, __1.getPrimaryCluster)(topology);
this.config.address = primary.endpoint;
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Resolved primary: ${primary.endpoint} (cluster=${primary.clusterId}), creating channel pool`);
this.channelPool = this.createChannelPool();
this.replaceTelemetryClient();
this._attachFailoverHandler();
// Start background topology refresher
this.topologyRefresher = new __1.TopologyRefresher({
globalEndpoint: this.globalEndpoint,
token,
topology,
onTopologyChange: newTopology => {
this.globalTopology = newTopology;
},
});
this.topologyRefresher.start();
// Now connect to the primary. Validate without side effects first because close may
// run while the RPC is awaiting a response.
const serverInfo = yield this._getServerInfo(sdkVersion, this.channelPool, false);
if (!this.isLifecycleCurrent(lifecycleGeneration)) {
return serverInfo;
}
this.applyServerInfo(serverInfo);
return serverInfo;
});
}
/**
* Reconnects to a new primary cluster after failover.
* Creates a new pool for the new primary, then drains the old pool.
* @returns true if primary changed and reconnection happened, false if primary unchanged
*/
reconnectToPrimary() {
return __awaiter(this, void 0, void 0, function* () {
if (this.closed) {
return false;
}
// Serialize concurrent failover attempts
if (this.isReconnecting) {
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Reconnect already in progress, waiting for completion`);
if (this.reconnectingPromise) {
return this.reconnectingPromise;
}
return false;
}
let primaryChanged = false;
const lifecycleGeneration = this.lifecycleGeneration;
this.isReconnecting = true;
this.reconnectingPromise = (() => __awaiter(this, void 0, void 0, function* () {
var _a;
let candidatePool;
let candidateTelemetryClient;
let candidateRefresher;
try {
const token = this.config.token || '';
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Attempting reconnect, fetching fresh topology`);
// Fetch fresh topology
const newTopology = yield (0, __1.fetchTopology)(this.globalEndpoint, token);
if (!this.isLifecycleCurrent(lifecycleGeneration)) {
return false;
}
const newPrimary = (0, __1.getPrimaryCluster)(newTopology);
// Check if primary actually changed
if (newPrimary.endpoint === this.config.address) {
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Primary unchanged (${this.config.address}), no reconnect needed`);
this.globalTopology = newTopology;
return false; // Primary hasn't changed, no reconnect needed
}
__1.logger.info(`Global cluster failover: ${this.config.address} -> ${newPrimary.endpoint}`);
// Build and validate a complete candidate lifecycle without mutating the live
// address, pool, telemetry transport, topology, or connection status. In
// particular, pool factories must capture the candidate address rather than
// reading this.config.address later when generic-pool creates a client.
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Creating new channel pool for ${newPrimary.endpoint}`);
candidatePool = this.createChannelPool(newPrimary.endpoint);
candidateTelemetryClient = this.createTelemetryClient(newPrimary.endpoint);
const candidateServerInfo = yield this._getServerInfo(this._sdkVersion, candidatePool, false);
if (!this.isLifecycleCurrent(lifecycleGeneration)) {
try {
candidateTelemetryClient.close();
}
catch (_b) {
// ignore cleanup errors on an unpublished stale candidate
}
candidateTelemetryClient = undefined;
yield this.disposeChannelPool(candidatePool);
candidatePool = undefined;
return false;
}
candidateRefresher = new __1.TopologyRefresher({
globalEndpoint: this.globalEndpoint,
token,
topology: newTopology,
onTopologyChange: t => {
this.globalTopology = t;
},
});
// Attaching the handler only mutates the isolated candidate pool and makes it
// ready for publication without exposing it to ordinary operations yet.
this._attachFailoverHandler(candidatePool);
const oldAddress = this.config.address;
const oldPool = this.channelPool;
const oldTelemetryClient = this.telemetryClient;
const oldRefresher = this.topologyRefresher;
// JavaScript runs this assignment block without interleaving another promise
// continuation. Advance the epoch before publishing the candidate telemetry
// client so any late response from the old endpoint is ignored as a whole.
this.telemetryEndpointEpoch += 1;
this.config.address = newPrimary.endpoint;
this.channelPool = candidatePool;
this.telemetryClient = candidateTelemetryClient;
this.applyServerInfo(candidateServerInfo, false);
this.globalTopology = newTopology;
this.topologyRefresher = candidateRefresher;
primaryChanged = true;
// Candidate resources are now owned by the live lifecycle and must not be
// cleaned up by the failure path.
candidatePool = undefined;
candidateTelemetryClient = undefined;
candidateRefresher = undefined;
// Everything below is post-commit cleanup/startup. None of it may enter the
// candidate-validation catch path and pretend the already-published lifecycle
// was rolled back.
try {
(_a = this.topologyRefresher) === null || _a === void 0 ? void 0 : _a.start();
}
catch (error) {
__1.logger.warn(`Failed to start topology refresher: ${error.message}`);
}
if (candidateServerInfo === null || candidateServerInfo === void 0 ? void 0 : candidateServerInfo.identifier) {
try {
this.telemetry.start();
}
catch (error) {
__1.logger.warn(`Failed to start client telemetry: ${error.message}`);
}
}
try {
oldRefresher === null || oldRefresher === void 0 ? void 0 : oldRefresher.stop();
}
catch (error) {
__1.logger.warn(`Failed to stop old topology refresher: ${error.message}`);
}
try {
oldTelemetryClient === null || oldTelemetryClient === void 0 ? void 0 : oldTelemetryClient.close();
}
catch (error) {
__1.logger.warn(`Failed to close old telemetry client: ${error.message}`);
}
// Existing operations may still hold a client from the old pool. drain() waits
// for those borrowers before clear() closes the channels, so late operations can
// finish while all new operations use the newly published pool.
if (oldPool) {
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Draining old channel pool for ${oldAddress}`);
yield this.disposeChannelPool(oldPool);
}
}
catch (e) {
if (primaryChanged) {
// Publication is the commit point. An unexpected post-commit cleanup error
// must not close the new live resources or report a rollback that did not
// happen.
__1.logger.warn(`Global cluster failover committed with a cleanup error: ${e.message}`);
return true;
}
__1.logger.warn(`Global cluster failover failed: ${e.message}`);
// The live lifecycle was not touched unless the synchronous publication block
// completed. Candidate validation failures therefore leave the old address,
// pool, telemetry manager/client/state, topology refresher, and status usable.
try {
candidateRefresher === null || candidateRefresher === void 0 ? void 0 : candidateRefresher.stop();
}
catch (_c) {
// ignore cleanup errors on an unpublished candidate
}
try {
candidateTelemetryClient === null || candidateTelemetryClient === void 0 ? void 0 : candidateTelemetryClient.close();
}
catch (_d) {
// ignore cleanup errors on an unpublished candidate
}
if (candidatePool) {
yield this.disposeChannelPool(candidatePool);
}
throw e;
}
return primaryChanged;
}))();
try {
return yield this.reconnectingPromise;
}
finally {
this.isReconnecting = false;
this.reconnectingPromise = null;
}
});
}
/**
* Attaches a failover handler to the channel pool for global cluster support.
* When promisify encounters a gRPC UNAVAILABLE error after all retries,
* this handler triggers topology refresh and pool rebuild.
*/
_attachFailoverHandler(pool = this.channelPool) {
(0, __1.setPoolFailoverHandler)(pool, () => __awaiter(this, void 0, void 0, function* () {
// Trigger topology refresh
if (this.topologyRefresher) {
this.topologyRefresher.triggerRefresh();
}
yield this.reconnectToPrimary();
return this.channelPool;
}));
}
/**
* Creates a pool of gRPC service clients.
* @returns {Pool} - A pool of gRPC service clients.
*/
createChannelPool(address = this.config.address) {
var _a;
const ServiceClientConstructor = this._MilvusService;
const formattedAddress = (0, __1.formatAddress)(address);
const pool = (0, generic_pool_1.createPool)({
create: () => __awaiter(this, void 0, void 0, function* () {
// Create a new gRPC service client
return new ServiceClientConstructor(formattedAddress, this.creds, this.channelOptions);
}),
destroy: (client) => __awaiter(this, void 0, void 0, function* () {
// Close the gRPC service client
return new Promise((resolve, reject) => {
client.close();
resolve(client.getChannel().getConnectivityState(true));
});
}),
}, (_a = this.config.pool) !== null && _a !== void 0 ? _a : {
min: __1.DEFAULT_POOL_MIN,
max: __1.DEFAULT_POOL_MAX,
});
(0, __1.setPoolTelemetryManager)(pool, this.telemetry);
return pool;
}
createTelemetryClient(address) {
return new this._ClientTelemetryService((0, __1.formatAddress)(address), this.creds, this.telemetryChannelOptions);
}
replaceTelemetryClient(address = this.config.address) {
const replacement = this.createTelemetryClient(address);
const previous = this.telemetryClient;
this.telemetryEndpointEpoch += 1;
this.telemetryClient = replacement;
previous === null || previous === void 0 ? void 0 : previous.close();
}
disposeChannelPool(pool) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield pool.drain();
yield pool.clear();
}
catch (_a) {
// Pool cleanup is best-effort. A cleanup failure must neither roll back a
// successful publication nor hide the original candidate validation error.
}
});
}
sendTelemetryHeartbeat(request) {
return new Promise((resolve, reject) => {
if (!this.telemetryClient) {
reject(new Error('telemetry client is not connected'));
return;
}
this.telemetryClient.ClientHeartbeat(request, new grpc_js_1.Metadata(), { deadline: new Date(Date.now() + 10000) }, (error, response) => error ? reject(error) : resolve(response));
});
}
/** Returns the telemetry manager for inspection and custom command handlers. */
getTelemetry() {
return this.telemetry;
}
/**
* Injects client metadata into the metadata of the gRPC client.
* @param metadata The metadata object of the gRPC client.
* @returns The updated metadata object.
*/
metadataListener(metadata) {
// inject client metadata into the metadata of the grpc client
for (var [key, value] of this.metadata) {
metadata.add(key, value);
}
return metadata;
}
/**
* Sets the active database for the gRPC client.
* @param data An optional object containing the name of the database to use.
* @returns A Promise that resolves with a `ResStatus` object.
*/
use(data) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => {
if (!data || data.db_name === '') {
__1.logger.info(`No database name provided, using default database: ${__1.DEFAULT_DB}`);
}
this.telemetryDatabaseExplicit = Boolean(data === null || data === void 0 ? void 0 : data.db_name);
// update database
this.metadata.set(__1.METADATA.DATABASE, (data && data.db_name) || __1.DEFAULT_DB);
resolve({ error_code: __1.ErrorCode.SUCCESS, reason: '' });
});
});
}
/**
* Retrieves server information from the Milvus server.
* @param {string} sdkVersion - The version of the SDK being used.
* @returns {Promise<void>} - A Promise that resolves when the server information has been retrieved.
*/
_getServerInfo(sdkVersion, pool = this.channelPool, apply = true, lifecycleGeneration = this.lifecycleGeneration) {
return __awaiter(this, void 0, void 0, function* () {
// build user info
const userInfo = {
client_info: {
sdk_type: 'nodejs',
sdk_version: sdkVersion,
local_time: (0, dayjs_1.default)().format(`YYYY-MM-DD HH:mm:ss.SSS`),
user: this.config.username,
reserved: this.config.option || {},
},
};
if (apply) {
this.connectStatus = __1.CONNECT_STATUS.CONNECTING;
}
const response = yield (0, __1.promisify)(pool, 'Connect', userInfo, this.timeout);
if (apply && this.isLifecycleCurrent(lifecycleGeneration)) {
this.applyServerInfo(response);
}
return response;
});
}
applyServerInfo(response, startTelemetry = true) {
if (response === null || response === void 0 ? void 0 : response.identifier) {
this.metadata.set(__1.METADATA.CLIENT_ID, response.identifier);
this.serverInfo = response.server_info;
}
this.connectStatus = (response === null || response === void 0 ? void 0 : response.identifier)
? __1.CONNECT_STATUS.CONNECTED
: __1.CONNECT_STATUS.UNIMPLEMENTED;
if ((response === null || response === void 0 ? void 0 : response.identifier) && startTelemetry) {
this.telemetry.start();
}
}
/**
* Closes the connection to the Milvus server.
* This method drains and clears the connection pool, and updates the connection status to SHUTDOWN.
* @returns {Promise<CONNECT_STATUS>} The updated connection status.
*/
closeConnection() {
return __awaiter(this, void 0, void 0, function* () {
if (this.closePromise) {
return this.closePromise;
}
// Publish the close fence before touching resources. Any topology/candidate promise
// released from this point on observes a different generation and may only clean up.
this.closed = true;
this.lifecycleGeneration += 1;
this.telemetryEndpointEpoch += 1;
this.connectStatus = __1.CONNECT_STATUS.SHUTDOWN;
const telemetryClient = this.telemetryClient;
this.telemetryClient = undefined;
const topologyRefresher = this.topologyRefresher;
this.topologyRefresher = null;
const channelPool = this.channelPool;
this.closePromise = (() => __awaiter(this, void 0, void 0, function* () {
try {
this.telemetry.stop();
}
catch (_a) {
// best-effort telemetry shutdown must not prevent channel cleanup
}
try {
telemetryClient === null || telemetryClient === void 0 ? void 0 : telemetryClient.close();
}
catch (_b) {
// best-effort cleanup
}
if (topologyRefresher) {
__1.logger.debug(`\x1b[36m[Global]\x1b[0m Stopping topology refresher on connection close`);
try {
topologyRefresher.stop();
}
catch (_c) {
// best-effort cleanup
}
}
if (channelPool) {
yield this.disposeChannelPool(channelPool);
}
return this.connectStatus;
}))();
return this.closePromise;
});
}
isLifecycleCurrent(generation) {
return !this.closed && generation === this.lifecycleGeneration;
}
/**
* Returns version information for the Milvus server.
* This method returns a Promise that resolves with a `GetVersionResponse` object.
*/
getVersion() {
return __awaiter(this, void 0, void 0, function* () {
// wait until connecting finished
yield this.connectPromise;
return yield (0, __1.promisify)(this.channelPool, 'GetVersion', {}, this.timeout);
});
}
/**
* Checks the health of the Milvus server.
* This method returns a Promise that resolves with a `CheckHealthResponse` object.
*/
checkHealth() {
return __awaiter(this, void 0, void 0, function* () {
// wait until connecting finished
yield this.connectPromise;
return yield (0, __1.promisify)(this.channelPool, 'CheckHealth', {}, this.timeout);
});
}
/**
* Runs an analyzer on the provided text.
* @param {RunAnalyzerRequest} data - The request object containing analyzer parameters and text.
* @returns {Promise<RunAnalyzerResponse>} - A Promise that resolves with the analyzer response.
*/
runAnalyzer(data) {
return __awaiter(this, void 0, void 0, function* () {
return (0, __1.withTelemetryLogicalOperation)(this.channelPool, 'RunAnalyzer', data, () => __awaiter(this, void 0, void 0, function* () {
return (0, __1.promisify)(this.channelPool, 'RunAnalyzer', {
analyzer_params: data.analyzer_params
? JSON.stringify(data.analyzer_params)
: '',
placeholder: (Array.isArray(data.text)
? data.text
: [data.text]).map(d => new TextEncoder().encode(String(d))),
with_detail: data.with_detail,
with_hash: data.with_hash,
db_name: data.db_name,
collection_name: data.collection_name,
field_name: data.field_name,
analyzer_names: data.analyzer_names,
}, this.timeout);
}));
});
}
/**
* Adds a file resource to Milvus metadata.
* @param {AddFileResourceReq} data - The file resource name and server-visible path.
* @returns {Promise<ResStatus>} - A Promise that resolves with the operation status.
*/
addFileResource(data) {
return __awaiter(this, void 0, void 0, function* () {
return yield (0, __1.promisify)(this.channelPool, 'AddFileResource', {
name: data.name,
path: data.path,
}, data.timeout || this.timeout);
});
}
/**
* Removes a file resource from Milvus metadata.
* @param {RemoveFileResourceReq} data - The file resource name.
* @returns {Promise<ResStatus>} - A Promise that resolves with the operation status.
*/
removeFileResource(data) {
return __awaiter(this, void 0, void 0, function* () {
return yield (0, __1.promisify)(this.channelPool, 'RemoveFileResource', {
name: data.name,
}, data.timeout || this.timeout);
});
}
/**
* Lists file resources registered in Milvus metadata.
* @param {ListFileResourcesReq} [data] - Optional request parameters.
* @returns {Promise<ListFileResourcesResponse>} - A Promise that resolves with file resources.
*/
listFileResources(data = {}) {
return __awaiter(this, void 0, void 0, function* () {
return yield (0, __1.promisify)(this.channelPool, 'ListFileResources', {}, data.timeout || this.timeout);
});
}
}
exports.GRPCClient = GRPCClient;
//# sourceMappingURL=GrpcClient.js.map