diff --git a/README.md b/README.md index 460ebdb3..63de59af 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The following table shows the recommended `@zilliz/milvus2-sdk-node` versions fo - [Milvus](https://milvus.io/) - [Zilliz Cloud](https://cloud.zilliz.com/signup) -- Node: v12+ +- Node: v18+ ## Installation diff --git a/jest.config.js b/jest.config.js index ae544ed0..829b8cb4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,10 +6,7 @@ module.exports = { testTimeout: 60000, // because user will cause other test fail, but we still have user spec coveragePathIgnorePatterns: ['/milvus/User.ts'], - testPathIgnorePatterns: [ - 'cloud.spec.ts', - 'serverless.spec.ts', - ], // add this line + testPathIgnorePatterns: ['cloud.spec.ts', 'serverless.spec.ts'], // add this line testEnvironmentOptions: { NODE_ENV: 'production', }, diff --git a/milvus/HttpClient.ts b/milvus/HttpClient.ts index eee5fe83..68c9e43b 100644 --- a/milvus/HttpClient.ts +++ b/milvus/HttpClient.ts @@ -1,5 +1,4 @@ -import axios, { AxiosInstance } from 'axios'; -import { HttpClientConfig } from './types'; +import { HttpClientConfig, FetchOptions } from './types'; import { Collection, Vector } from './http'; import { DEFAULT_DB, @@ -7,50 +6,42 @@ import { DEFAULT_HTTP_ENDPOINT_VERSION, } from '../milvus/const'; -// base class +/** + * HttpBaseClient is a base class for making HTTP requests to a Milvus server. + * It provides basic functionality for making GET and POST requests, and handles + * configuration, headers, and timeouts. + * + * The HttpClientConfig object should contain the following properties: + * - endpoint: The URL of the Milvus server. + * - username: (Optional) The username for authentication. + * - password: (Optional) The password for authentication. + * - token: (Optional) The token for authentication. + * - fetch: (Optional) An alternative fetch API implementation, e.g., node-fetch for Node.js environments. + * - baseURL: (Optional) The base URL for the API endpoints. + * - version: (Optional) The version of the API endpoints. + * - database: (Optional) The default database to use for requests. + * - timeout: (Optional) The timeout for requests in milliseconds. + * + * Note: This is a base class and does not provide specific methods for interacting + * with Milvus entities like collections or vectors. For that, use the HttpClient class + * which extends this class and mixes in the Collection and Vector APIs. + */ export class HttpBaseClient { // The client configuration. public config: HttpClientConfig; - // axios - public client: AxiosInstance; - constructor(config: HttpClientConfig) { // Assign the configuration object. this.config = config; - // setup axios client - this.client = axios.create({ - baseURL: this.baseURL, - timeout: this.timeout, - timeoutErrorMessage: '', - withCredentials: true, - headers: { - Authorization: this.authorization, - Accept: 'application/json', - ContentType: 'application/json', - }, - }); - - // interceptors - this.client.interceptors.request.use(request => { - // if dbName is not set, using default database - // GET - if (request.params) { - request.params.dbName = request.params.dbName || this.database; - } - // POST - if (request.data) { - request.data.dbName = request.data.dbName || this.database; - request.data = JSON.stringify(request.data); - } - - // console.log('request: ', request.data); - return request; - }); - this.client.interceptors.response.use(response => { - return response.data; - }); + // The fetch method used for requests can be customized by providing a fetch property in the configuration. + // If no fetch method is provided, the global fetch method will be used if available. + // If no global fetch method is available, an error will be thrown. + if (!this.config.fetch && typeof fetch === 'undefined') { + throw new Error( + 'The Fetch API is not supported in this environment. Please provide an alternative, for example, node-fetch.' + ); + } } // baseURL @@ -76,22 +67,101 @@ export class HttpBaseClient { // database get database() { - return this.config.database || DEFAULT_DB; + return this.config.database ?? DEFAULT_DB; } // timeout get timeout() { - return this.config.timeout || DEFAULT_HTTP_TIMEOUT; + return this.config.timeout ?? DEFAULT_HTTP_TIMEOUT; + } + + // headers + get headers() { + return { + Authorization: this.authorization, + Accept: 'application/json', + ContentType: 'application/json', + }; } - get POST() { - return this.client.post; + // fetch + get fetch() { + return this.config.fetch ?? fetch; } - get GET() { - return this.client.get; + // POST API + async POST( + url: string, + data: Record = {}, + options?: FetchOptions + ): Promise { + try { + // timeout controller + const timeout = options?.timeout ?? this.timeout; + const abortController = options?.abortController ?? new AbortController(); + const id = setTimeout(() => abortController.abort(), timeout); + + // assign database + if (data) { + data.dbName = data.dbName ?? this.database; + } + + const response = await this.fetch(`${this.baseURL}${url}`, { + method: 'post', + headers: this.headers, + body: JSON.stringify(data), + signal: abortController.signal, + }); + + clearTimeout(id); + return response.json() as T; + } catch (error) { + if (error.name === 'AbortError') { + console.warn(`post ${url} request was timeout`); + } + return Promise.reject(error); + } + } + + // GET API + async GET( + url: string, + params: Record = {}, + options?: FetchOptions + ): Promise { + try { + // timeout controller + const timeout = options?.timeout ?? this.timeout; + const abortController = options?.abortController ?? new AbortController(); + const id = setTimeout(() => abortController.abort(), timeout); + + // assign database + if (params) { + params.dbName = params.dbName ?? this.database; + } + + const queryParams = new URLSearchParams(params); + + const response = await this.fetch( + `${this.baseURL}${url}?${queryParams}`, + { + method: 'get', + headers: this.headers, + signal: abortController.signal, + } + ); + + clearTimeout(id); + + return response.json() as T; + } catch (error) { + if (error.name === 'AbortError') { + console.warn(`milvus http client: request was timeout`); + } + return Promise.reject(error); + } } } -// mixin APIs +// The HttpClient class extends the functionality of the HttpBaseClient class by mixing in the Collection and Vector APIs. export class HttpClient extends Collection(Vector(HttpBaseClient)) {} diff --git a/milvus/MilvusClient.ts b/milvus/MilvusClient.ts index 8bfa1185..aaafa1e2 100644 --- a/milvus/MilvusClient.ts +++ b/milvus/MilvusClient.ts @@ -12,6 +12,9 @@ import { CreateCollectionReq, ERROR_REASONS, checkCreateCollectionCompatibility, + DEFAULT_PRIMARY_KEY_FIELD, + DEFAULT_METRIC_TYPE, + DEFAULT_VECTOR_FIELD, } from '.'; import sdkInfo from '../sdk.json'; @@ -91,10 +94,10 @@ export class MilvusClient extends GRPCClient { const { collection_name, dimension, - primary_field_name = 'id', + primary_field_name = DEFAULT_PRIMARY_KEY_FIELD, id_type = DataType.Int64, - metric_type = 'IP', - vector_field_name = 'vector', + metric_type = DEFAULT_METRIC_TYPE, + vector_field_name = DEFAULT_VECTOR_FIELD, enableDynamicField = true, enable_dynamic_field = true, auto_id = false, diff --git a/milvus/const/defaults.ts b/milvus/const/defaults.ts index 5b29f962..fd9dbf1f 100644 --- a/milvus/const/defaults.ts +++ b/milvus/const/defaults.ts @@ -4,7 +4,9 @@ export const DEFAULT_DEBUG = false; export const DEFAULT_MILVUS_PORT = 19530; // default milvus port export const DEFAULT_CONNECT_TIMEOUT = 15 * 1000; // 15s export const DEFAULT_TOPK = 100; // default topk -export const DEFAULT_METRIC_TYPE = 'L2'; +export const DEFAULT_METRIC_TYPE = 'IP'; +export const DEFAULT_VECTOR_FIELD = 'vector'; +export const DEFAULT_PRIMARY_KEY_FIELD = 'id'; export const DEFAULT_MAX_RETRIES = 3; // max retry time export const DEFAULT_RETRY_DELAY = 30; // retry delay, 30ms export const DEFAULT_PARTITIONS_NUMBER = 64; diff --git a/milvus/http/Collection.ts b/milvus/http/Collection.ts index dfbb5c31..6e702a0d 100644 --- a/milvus/http/Collection.ts +++ b/milvus/http/Collection.ts @@ -7,40 +7,74 @@ import { HttpCollectionDescribeResponse, HttpBaseResponse, HttpBaseReq, + FetchOptions, } from '../types'; +import { + DEFAULT_PRIMARY_KEY_FIELD, + DEFAULT_METRIC_TYPE, + DEFAULT_VECTOR_FIELD, +} from '../const'; +/** + * Collection is a mixin function that extends the functionality of a base class. + * It provides methods to interact with collections in a Milvus cluster. + * + * @param {Constructor} Base - The base class to be extended. + * @returns {class} - The extended class with additional methods for collection management. + * + * @method createCollection - Creates a new collection in Milvus. + * @method describeCollection - Retrieves the description of a specific collection. + * @method dropCollection - Deletes a specific collection from Milvus. + * @method listCollections - Lists all collections in the Milvus cluster. + */ export function Collection>(Base: T) { return class extends Base { // POST create collection async createCollection( - data: HttpCollectionCreateReq + data: HttpCollectionCreateReq, + options?: FetchOptions ): Promise { const url = `/vector/collections/create`; - return await this.POST(url, data); + + // if some keys not provided, using default value + data.metricType = data.metricType || DEFAULT_METRIC_TYPE; + data.primaryField = data.primaryField || DEFAULT_PRIMARY_KEY_FIELD; + data.vectorField = data.vectorField || DEFAULT_VECTOR_FIELD; + + return await this.POST(url, data, options); } // GET describe collection async describeCollection( - params: HttpBaseReq + params: HttpBaseReq, + options?: FetchOptions ): Promise { const url = `/vector/collections/describe`; - return await this.GET(url, { params }); + return await this.GET( + url, + params, + options + ); } // POST drop collection - async dropCollection(data: HttpBaseReq): Promise { + async dropCollection( + data: HttpBaseReq, + options?: FetchOptions + ): Promise { const url = `/vector/collections/drop`; - return await this.POST(url, data); + return await this.POST(url, data, options); } // GET list collections async listCollections( - params: HttpCollectionListReq = {} + params: HttpCollectionListReq = {}, + options?: FetchOptions ): Promise { const url = `/vector/collections`; - return await this.GET(url, { params }); + return await this.GET(url, params, options); } }; } diff --git a/milvus/http/Vector.ts b/milvus/http/Vector.ts index d4d05b19..a24cc0e6 100644 --- a/milvus/http/Vector.ts +++ b/milvus/http/Vector.ts @@ -10,38 +10,77 @@ import { HttpVectorDeleteReq, HttpVectorSearchResponse, HttpBaseResponse, + FetchOptions, } from '../types'; +/** + * Vector is a mixin function that extends the functionality of a base class. + * It provides methods to interact with vectors in a Milvus cluster. + * + * @param {Constructor} Base - The base class to be extended. + * @returns {class} - The extended class with additional methods for vector management. + * + * @method get - Retrieves a specific vector from Milvus. + * @method insert - Inserts a new vector into Milvus. + * @method upsert - Inserts a new vector into Milvus, or updates it if it already exists. + * @method query - Queries for vectors in Milvus. + * @method search - Searches for vectors in Milvus. + * @method delete - Deletes a specific vector from Milvus. + */ export function Vector>(Base: T) { return class extends Base { // GET get data - async get(params: HttpVectorGetReq): Promise { + async get( + params: HttpVectorGetReq, + options?: FetchOptions + ): Promise { const url = `/vector/get`; - return await this.GET(url, { params }); + return await this.GET(url, params, options); } // POST insert data - async insert(data: HttpVectorInsertReq): Promise { + async insert( + data: HttpVectorInsertReq, + options?: FetchOptions + ): Promise { const url = `/vector/insert`; - return await this.POST(url, data); + return await this.POST(url, data, options); + } + + // POST insert data + async upsert( + data: HttpVectorInsertReq, + options?: FetchOptions + ): Promise { + const url = `/vector/insert`; + return await this.POST(url, data, options); } // POST query data - async query(data: HttpVectorQueryReq): Promise { + async query( + data: HttpVectorQueryReq, + options?: FetchOptions + ): Promise { const url = `/vector/query`; - return await this.client.post(url, data); + return await this.POST(url, data, options); } // POST search data - async search(data: HttpVectorSearchReq): Promise { + async search( + data: HttpVectorSearchReq, + options?: FetchOptions + ): Promise { const url = `/vector/search`; - return await this.POST(url, data); + return await this.POST(url, data, options); } // POST delete collection - async delete(data: HttpVectorDeleteReq): Promise { + async delete( + data: HttpVectorDeleteReq, + options?: FetchOptions + ): Promise { const url = `/vector/delete`; - return await this.POST(url, data); + return await this.POST(url, data, options); } }; } diff --git a/milvus/types/Http.ts b/milvus/types/Http.ts index ef74faca..176e5168 100644 --- a/milvus/types/Http.ts +++ b/milvus/types/Http.ts @@ -1,6 +1,12 @@ import { FloatVectors } from '..'; +type Fetch = (input: any, init?: any) => Promise; + // Class types export type Constructor = new (...args: any[]) => T; +export type FetchOptions = { + abortController: AbortController; + timeout: number; +} type HttpClientConfigBase = { // database name @@ -15,6 +21,8 @@ type HttpClientConfigBase = { password?: string; // request timeout, number in milliseconds. timeout?: number; + // altenative fetch api + fetch?: Fetch; }; type HttpClientConfigAddress = HttpClientConfigBase & { @@ -48,9 +56,9 @@ export interface HttpBaseResponse { // collection operations export interface HttpCollectionCreateReq extends HttpBaseReq { dimension: number; - metricType: string; - primaryField: string; - vectorField: string; + metricType?: string; + primaryField?: string; + vectorField?: string; description?: string; } // list collection request diff --git a/package.json b/package.json index 2c6995ac..9361b8f8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "pre": "git submodule update --remote && rm -rf proto/proto/google && mkdir -p proto/proto/google/protobuf && wget https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/descriptor.proto -O proto/proto/google/protobuf/descriptor.proto", "build": "rm -rf dist && tsc --declaration && node build.js", "test": "NODE_ENV=dev jest", + "test-cloud": "NODE_ENV=dev jest test/http --testPathIgnorePatterns=none", "bench": "ts-node test/tools/bench.ts", "coverage": "NODE_ENV=dev jest --coverage=true --config jest.config.js --no-cache", "build-test": " yarn build && NODE_ENV=dev jest test/build/Collection.spec.ts --testPathIgnorePatterns=none", @@ -21,7 +22,6 @@ "dependencies": { "@grpc/grpc-js": "1.8.17", "@grpc/proto-loader": "0.7.7", - "axios": "^1.5.1", "dayjs": "^1.11.7", "lru-cache": "^9.1.2", "protobufjs": "7.2.4", @@ -30,7 +30,9 @@ "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.21.5", "@types/jest": "^29.5.1", + "@types/node-fetch": "^2.6.8", "jest": "^29.5.0", + "node-fetch": "2", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typedoc": "^0.24.7", diff --git a/test/http/api.spec.ts b/test/http/api.spec.ts deleted file mode 100644 index 97e12732..00000000 --- a/test/http/api.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { HttpClient, MilvusClient } from '../../milvus'; -import { - IP, - ENDPOINT, - genCollectionParams, - generateInsertData, - dynamicFields, -} from '../tools'; - -const milvusClient = new MilvusClient({ address: IP }); -const dbParam = { - db_name: 'HttpClient_collections', -}; - -describe(`HTTP API tests`, () => { - // Mock configuration object - const config = { - endpoint: ENDPOINT, - database: dbParam.db_name, - }; - - const createParams = { - dimension: 4, - collectionName: 'my_collection', - metricType: 'L2', - primaryField: 'id', - vectorField: 'vector', - description: 'des', - }; - - const count = 10; - const data = generateInsertData( - [ - ...genCollectionParams({ - collectionName: createParams.collectionName, - dim: createParams.dimension, - enableDynamic: true, - }).fields, - ...dynamicFields, - ], - count - ); - - // Create an instance of HttpBaseClient with the mock configuration - const client = new HttpClient(config); - - beforeAll(async () => { - await milvusClient.createDatabase(dbParam); - await milvusClient.use(dbParam); - }); - - afterAll(async () => { - // await milvusClient.dropCollection({ - // collection_name: createParams.collectionName, - // }); - await milvusClient.dropDatabase(dbParam); - }); - - it('should create collection successfully', async () => { - const create = await client.createCollection(createParams); - - const hasCollection = await milvusClient.hasCollection({ - collection_name: createParams.collectionName, - }); - - expect(create.code).toEqual(200); - expect(hasCollection.value).toEqual(true); - }); - - it('should describe collection successfully', async () => { - const describe = await client.describeCollection({ - collectionName: createParams.collectionName, - }); - - expect(describe.code).toEqual(200); - expect(describe.data.description).toEqual(createParams.description); - expect(describe.data.shardsNum).toEqual(1); - expect(describe.data.enableDynamic).toEqual(true); - expect(describe.data.fields.length).toEqual(2); - }); - - it('should list collections successfully', async () => { - const list = await client.listCollections(); - expect(list.code).toEqual(200); - expect(list.data[0]).toEqual(createParams.collectionName); - }); - - it('should insert data successfully', async () => { - const insert = await client.insert({ - collectionName: createParams.collectionName, - data: data, - }); - - expect(insert.code).toEqual(200); - expect(insert.data.insertCount).toEqual(count); - }); - - it('should query data successfully', async () => { - const query = await client.query({ - collectionName: createParams.collectionName, - outputFields: ['id'], - filter: 'id > 0', - }); - - expect(query.code).toEqual(200); - expect(query.data.length).toEqual(data.length); - }); - - it('should search data successfully', async () => { - const search = await client.search({ - collectionName: createParams.collectionName, - outputFields: ['*'], - vector: [1, 2, 3, 4], - limit: 5, - }); - - expect(search.code).toEqual(200); - expect(search.data.length).toEqual(5); - expect(typeof search.data[0].distance).toEqual('number'); - }); - - it('should drop collection successfully', async () => { - const drop = await client.dropCollection({ - collectionName: createParams.collectionName, - }); - - expect(drop.code).toEqual(200); - }); -}); diff --git a/test/http/client.spec.ts b/test/http/client.spec.ts index 254363a1..97c999e3 100644 --- a/test/http/client.spec.ts +++ b/test/http/client.spec.ts @@ -1,3 +1,4 @@ +import { default as nodeFetch } from 'node-fetch'; import { HttpClient, DEFAULT_DB, @@ -112,4 +113,19 @@ describe(`HTTP Client test`, () => { const client2 = new HttpClient(config2); expect(client2.timeout).toBe(timeout); }); + + it('should using the correct fetch', () => { + const config = { + baseURL, + fetch: nodeFetch, + }; + const client = new HttpClient(config); + expect(client.fetch).toEqual(nodeFetch); + + const config2 = { + baseURL, + }; + const client2 = new HttpClient(config2); + expect(client2.fetch).toEqual(fetch); + }); }); diff --git a/test/http/cloud.spec.ts b/test/http/cloud.spec.ts index b06c2dce..3347033b 100644 --- a/test/http/cloud.spec.ts +++ b/test/http/cloud.spec.ts @@ -1,104 +1,9 @@ -import { HttpClient } from '../../milvus'; -import { - genFloatVector, - genCollectionParams, - generateInsertData, - dynamicFields, -} from '../tools'; +import { generateTests } from './test'; -describe(`Dedicated restful API tests`, () => { - const config = { - endpoint: 'dedicated endpoint', - username: 'username', - password: 'password', - }; +const config = { + endpoint: 'dedicated endpoint', + username: 'username', + password: 'password', +}; - const createParams = { - dimension: 32, - collectionName: 'my_collection', - metricType: 'L2', - primaryField: 'id', - vectorField: 'vector', - description: 'des', - }; - - const count = 10; - const data = generateInsertData( - [ - ...genCollectionParams({ - collectionName: createParams.collectionName, - dim: createParams.dimension, - enableDynamic: true, - }).fields, - ...dynamicFields, - ], - count - ); - - // Create an instance of HttpBaseClient with the mock configuration - const client = new HttpClient(config); - - it('should create collection successfully', async () => { - const create = await client.createCollection(createParams); - expect(create.code).toEqual(200); - }); - - it('should describe collection successfully', async () => { - const describe = await client.describeCollection({ - collectionName: createParams.collectionName, - }); - - expect(describe.code).toEqual(200); - expect(describe.data.description).toEqual(createParams.description); - expect(describe.data.shardsNum).toEqual(1); - expect(describe.data.fields.length).toEqual(2); - }); - - it('should list collections successfully', async () => { - const list = await client.listCollections(); - expect(list.code).toEqual(200); - expect(list.data.indexOf(createParams.collectionName) !== -1).toEqual(true); - }); - - it('should insert data successfully', async () => { - const insert = await client.insert({ - collectionName: createParams.collectionName, - data: data, - }); - - expect(insert.code).toEqual(200); - expect(insert.data.insertCount).toEqual(count); - }); - - it('should query data successfully', async () => { - const query = await client.query({ - collectionName: createParams.collectionName, - outputFields: ['id'], - filter: 'id > 0', - }); - - expect(query.code).toEqual(200); - expect(query.data.length).toEqual(data.length); - }); - - it('should search data successfully', async () => { - const search = await client.search({ - collectionName: createParams.collectionName, - outputFields: ['*'], - vector: genFloatVector({ dim: createParams.dimension }) as number[], - limit: 5, - }); - - expect(search.code).toEqual(200); - expect(search.data.length).toEqual(5); - expect(typeof search.data[0].distance).toEqual('number'); - }); - - it('should drop collection successfully', async () => { - const drop = await client.dropCollection({ - collectionName: createParams.collectionName, - }); - - expect(drop.code).toEqual(200); - }); -}); +generateTests('cloud decidated api test', config); diff --git a/test/http/fetch.spec.ts b/test/http/fetch.spec.ts new file mode 100644 index 00000000..36d0673e --- /dev/null +++ b/test/http/fetch.spec.ts @@ -0,0 +1,7 @@ +import { ENDPOINT } from '../tools'; +import { generateTests } from './test'; + +const config = { + endpoint: ENDPOINT, +}; +generateTests('http api by native fetch test', config); diff --git a/test/http/node-fetch.spec.ts b/test/http/node-fetch.spec.ts new file mode 100644 index 00000000..90bb8e69 --- /dev/null +++ b/test/http/node-fetch.spec.ts @@ -0,0 +1,9 @@ +import fetch from 'node-fetch'; +import { ENDPOINT } from '../tools'; +import { generateTests } from './test'; + +const config = { + endpoint: ENDPOINT, + fetch: fetch, +}; +generateTests('http api by node-fetch v2 test', config); diff --git a/test/http/serverless.spec.ts b/test/http/serverless.spec.ts index 454d9338..01dea2aa 100644 --- a/test/http/serverless.spec.ts +++ b/test/http/serverless.spec.ts @@ -1,104 +1,8 @@ -import { HttpClient } from '../../milvus'; -import { - genFloatVector, - genCollectionParams, - generateInsertData, - dynamicFields, -} from '../tools'; +import { generateTests } from './test'; -describe(`Serverless restful API tests`, () => { - const config = { - endpoint: 'serverless endpoint', - token: 'serverless api key', - }; +const config = { + endpoint: 'dedicated endpoint', + token: 'serverless api key', +}; - const createParams = { - dimension: 32, - collectionName: 'my_collection', - metricType: 'L2', - primaryField: 'id', - vectorField: 'vector', - description: 'des', - }; - - const count = 10; - const data = generateInsertData( - [ - ...genCollectionParams({ - collectionName: createParams.collectionName, - dim: createParams.dimension, - enableDynamic: true, - }).fields, - ...dynamicFields, - ], - count - ); - - // Create an instance of HttpBaseClient with the mock configuration - const client = new HttpClient(config); - - it('should create collection successfully', async () => { - const create = await client.createCollection(createParams); - - expect(create.code).toEqual(200); - }); - - it('should describe collection successfully', async () => { - const describe = await client.describeCollection({ - collectionName: createParams.collectionName, - }); - - expect(describe.code).toEqual(200); - expect(describe.data.description).toEqual(createParams.description); - expect(describe.data.shardsNum).toEqual(1); - expect(describe.data.fields.length).toEqual(2); - }); - - it('should list collections successfully', async () => { - const list = await client.listCollections(); - expect(list.code).toEqual(200); - expect(list.data.indexOf(createParams.collectionName) !== -1).toEqual(true); - }); - - it('should insert data successfully', async () => { - const insert = await client.insert({ - collectionName: createParams.collectionName, - data: data, - }); - - expect(insert.code).toEqual(200); - expect(insert.data.insertCount).toEqual(count); - }); - - it('should query data successfully', async () => { - const query = await client.query({ - collectionName: createParams.collectionName, - outputFields: ['id'], - filter: 'id > 0', - }); - - expect(query.code).toEqual(200); - expect(query.data.length).toEqual(data.length); - }); - - it('should search data successfully', async () => { - const search = await client.search({ - collectionName: createParams.collectionName, - outputFields: ['*'], - vector: genFloatVector({ dim: createParams.dimension }) as number[], - limit: 5, - }); - - expect(search.code).toEqual(200); - expect(search.data.length).toEqual(5); - expect(typeof search.data[0].distance).toEqual('number'); - }); - - it('should drop collection successfully', async () => { - const drop = await client.dropCollection({ - collectionName: createParams.collectionName, - }); - - expect(drop.code).toEqual(200); - }); -}); +generateTests('serverless api test', config); diff --git a/test/http/test.ts b/test/http/test.ts new file mode 100644 index 00000000..8c4208fc --- /dev/null +++ b/test/http/test.ts @@ -0,0 +1,151 @@ +import { + HttpClient, + DEFAULT_METRIC_TYPE, + DEFAULT_VECTOR_FIELD, + HttpClientConfig, +} from '../../milvus'; +import { + genCollectionParams, + generateInsertData, + dynamicFields, +} from '../tools'; + +export function generateTests( + desc = `HTTP API tests`, + config: HttpClientConfig +) { + describe(desc, () => { + // Mock configuration object + const createParams = { + dimension: 4, + collectionName: 'my_collection', + metricType: 'L2', + primaryField: 'id', + vectorField: 'vector', + description: 'des', + }; + + const createDefaultParams = { + collectionName: 'default_collection_name', + dimension: 128, + }; + + const count = 10; + const data = generateInsertData( + [ + ...genCollectionParams({ + collectionName: createParams.collectionName, + dim: createParams.dimension, + enableDynamic: true, + }).fields, + ...dynamicFields, + ], + count + ); + + // Create an instance of HttpBaseClient with the mock configuration + const client = new HttpClient(config); + + it('should create collection successfully', async () => { + const create = await client.createCollection(createParams); + + expect(create.code).toEqual(200); + }); + + it('should create collection with only dimension successfully', async () => { + const createDefault = await client.createCollection(createDefaultParams); + + expect(createDefault.code).toEqual(200); + }); + + it('should describe collection successfully', async () => { + const describe = await client.describeCollection({ + collectionName: createParams.collectionName, + }); + + expect(describe.code).toEqual(200); + expect(describe.data.collectionName).toEqual(createParams.collectionName); + expect(describe.data.description).toEqual(createParams.description); + expect(describe.data.shardsNum).toEqual(1); + expect(describe.data.enableDynamic).toEqual(true); + expect(describe.data.fields.length).toEqual(2); + expect(describe.data.indexes[0].fieldName).toEqual( + createParams.vectorField + ); + expect(describe.data.indexes[0].metricType).toEqual( + createParams.metricType + ); + }); + + it('should describe default collection successfully', async () => { + const describe = await client.describeCollection({ + collectionName: createDefaultParams.collectionName, + }); + + expect(describe.code).toEqual(200); + expect(describe.data.collectionName).toEqual( + createDefaultParams.collectionName + ); + expect(describe.data.shardsNum).toEqual(1); + expect(describe.data.enableDynamic).toEqual(true); + expect(describe.data.fields.length).toEqual(2); + expect(describe.data.indexes[0].fieldName).toEqual(DEFAULT_VECTOR_FIELD); + expect(describe.data.indexes[0].metricType).toEqual(DEFAULT_METRIC_TYPE); + }); + + it('should list collections successfully', async () => { + const list = await client.listCollections(); + expect(list.code).toEqual(200); + expect(list.data.indexOf(createParams.collectionName) !== -1).toEqual( + true + ); + }); + + it('should insert data successfully', async () => { + const insert = await client.insert({ + collectionName: createParams.collectionName, + data: data, + }); + + expect(insert.code).toEqual(200); + expect(insert.data.insertCount).toEqual(count); + }); + + it('should query data successfully', async () => { + const query = await client.query({ + collectionName: createParams.collectionName, + outputFields: ['id'], + filter: 'id > 0', + }); + + expect(query.code).toEqual(200); + expect(query.data.length).toEqual(data.length); + }); + + it('should search data successfully', async () => { + const search = await client.search({ + collectionName: createParams.collectionName, + outputFields: ['*'], + vector: [1, 2, 3, 4], + limit: 5, + }); + + expect(search.code).toEqual(200); + expect(search.data.length).toEqual(5); + expect(typeof search.data[0].distance).toEqual('number'); + }); + + it('should drop collection successfully', async () => { + const drop = await client.dropCollection({ + collectionName: createParams.collectionName, + }); + + expect(drop.code).toEqual(200); + + const dropDefault = await client.dropCollection({ + collectionName: createDefaultParams.collectionName, + }); + expect(dropDefault.code).toEqual(200); + }); + }); +} diff --git a/test/tools/index.ts b/test/tools/index.ts index 0880eda8..0fbf01ae 100644 --- a/test/tools/index.ts +++ b/test/tools/index.ts @@ -2,7 +2,4 @@ export * from './data'; export * from './collection'; export * from './const'; export * from './utils'; - -// test IP -export const IP = '127.0.0.1:19530'; -export const ENDPOINT = `http://${IP}`; +export * from './ip'; diff --git a/test/tools/ip.ts b/test/tools/ip.ts new file mode 100644 index 00000000..2fec5d91 --- /dev/null +++ b/test/tools/ip.ts @@ -0,0 +1,3 @@ +// test IP +export const IP = '127.0.0.1:19530'; +export const ENDPOINT = `http://${IP}`; diff --git a/yarn.lock b/yarn.lock index c0902a22..c8d1b0d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -879,6 +879,14 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/node-fetch@^2.6.8": + version "2.6.8" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.8.tgz#9a2993583975849c2e1f360b6ca2f11755b2c504" + integrity sha512-nnH5lV9QCMPsbEVdTb5Y+F3GQxLSw1xQgIydrb2gSfEavRPs50FnMr+KUaa+LoPSqibm2N+ZZxH7lavZlAT4GA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": version "20.2.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.3.tgz#b31eb300610c3835ac008d690de6f87e28f9b878" @@ -987,15 +995,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f" - integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A== - dependencies: - follow-redirects "^1.15.0" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - babel-jest@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5" @@ -1449,11 +1448,6 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== - form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -2269,6 +2263,13 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +node-fetch@2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -2434,11 +2435,6 @@ protobufjs@7.2.4, protobufjs@^7.0.0: "@types/node" ">=13.7.0" long "^5.0.0" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - pure-rand@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" @@ -2688,6 +2684,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" @@ -2795,6 +2796,19 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"