[test]Add restful api test (#21336)

Signed-off-by: zhuwenxing <wenxing.zhu@zilliz.com>
This commit is contained in:
zhuwenxing 2022-12-21 17:47:27 +08:00 committed by GitHub
parent 396a85c926
commit 19387754dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2225 additions and 0 deletions

View File

@ -0,0 +1,17 @@
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Alias(RestClient):
def drop_alias():
pass
def alter_alias():
pass
def create_alias():
pass

View File

@ -0,0 +1,62 @@
import json
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Collection(RestClient):
@DELETE("collection")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def drop_collection(self, payload):
"""Drop a collection"""
@GET("collection")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def describe_collection(self, payload):
"""Describe a collection"""
@POST("collection")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def create_collection(self, payload):
"""Create a collection"""
@GET("collection/existence")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def has_collection(self, payload):
"""Check if a collection exists"""
@DELETE("collection/load")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def release_collection(self, payload):
"""Release a collection"""
@POST("collection/load")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def load_collection(self, payload):
"""Load a collection"""
@GET("collection/statistics")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def get_collection_statistics(self, payload):
"""Get collection statistics"""
@GET("collections")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def show_collections(self, payload):
"""Show collections"""
if __name__ == '__main__':
client = Collection("http://localhost:19121/api/v1")
print(client)

View File

@ -0,0 +1,19 @@
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Credential(RestClient):
def delete_credential():
pass
def update_credential():
pass
def create_credential():
pass
def list_credentials():
pass

View File

@ -0,0 +1,63 @@
import json
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Entity(RestClient):
@POST("distance")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def calc_distance(self, payload):
""" Calculate distance between two points """
@DELETE("entities")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def delete(self, payload):
"""delete entities"""
@POST("entities")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def insert(self, payload):
"""insert entities"""
@POST("persist")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def flush(self, payload):
"""flush entities"""
@POST("persist/segment-info")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def get_persistent_segment_info(self, payload):
"""get persistent segment info"""
@POST("persist/state")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def get_flush_state(self, payload):
"""get flush state"""
@POST("query")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def query(self, payload):
"""query entities"""
@POST("query-segment-info")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def get_query_segment_info(self, payload):
"""get query segment info"""
@POST("search")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def search(self, payload):
"""search entities"""

View File

@ -0,0 +1,18 @@
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Import(RestClient):
def list_import_tasks():
pass
def exec_import():
pass
def get_import_state():
pass

View File

@ -0,0 +1,38 @@
import json
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Index(RestClient):
@DELETE("/index")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def drop_index(self, payload):
"""Drop an index"""
@GET("/index")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def describe_index(self, payload):
"""Describe an index"""
@POST("index")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def create_index(self, payload):
"""create index"""
@GET("index/progress")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def get_index_build_progress(self, payload):
"""get index build progress"""
@GET("index/state")
@body("payload", lambda p: json.dumps(p))
@on(200, lambda r: r.json())
def get_index_state(self, payload):
"""get index state"""

View File

@ -0,0 +1,11 @@
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Metrics(RestClient):
def get_metrics():
pass

View File

@ -0,0 +1,21 @@
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Ops(RestClient):
def manual_compaction():
pass
def get_compaction_plans():
pass
def get_compaction_state():
pass
def load_balance():
pass
def get_replicas():
pass

View File

@ -0,0 +1,27 @@
from decorest import GET, POST, DELETE
from decorest import HttpStatus, RestClient
from decorest import accept, body, content, endpoint, form
from decorest import header, multipart, on, query, stream, timeout
class Partition(RestClient):
def drop_partition():
pass
def create_partition():
pass
def has_partition():
pass
def get_partition_statistics():
pass
def show_partitions():
pass
def release_partition():
pass
def load_partition():
pass

View File

@ -0,0 +1,43 @@
from datetime import date, datetime
from typing import List, Union, Optional
from pydantic import BaseModel, UUID4, conlist
from pydantic_factories import ModelFactory
class Person(BaseModel):
def __init__(self, length):
super().__init__()
self.len = length
id: UUID4
name: str
hobbies: List[str]
age: Union[float, int]
birthday: Union[datetime, date]
class Pet(BaseModel):
name: str
age: int
class PetFactory(BaseModel):
name: str
pet: Pet
age: Optional[int] = None
sample = {
"name": "John",
"pet": {
"name": "Fido",
"age": 3
}
}
result = PetFactory(**sample)
print(result)

View File

@ -0,0 +1,72 @@
from time import sleep
from decorest import HttpStatus, RestClient
from models.schema import CollectionSchema
from base.collection_service import CollectionService
from base.index_service import IndexService
from base.entity_service import EntityService
from utils.util_log import test_log as log
from common import common_func as cf
from common import common_type as ct
class Base:
"""init base class"""
endpoint = None
collection_service = None
index_service = None
entity_service = None
collection_name = None
collection_object_list = []
def setup_class(self):
log.info("setup class")
def teardown_class(self):
log.info("teardown class")
def setup_method(self, method):
log.info(("*" * 35) + " setup " + ("*" * 35))
log.info("[setup_method] Start setup test case %s." % method.__name__)
host = cf.param_info.param_host
port = cf.param_info.param_port
self.endpoint = "http://" + host + ":" + str(port) + "/api/v1"
self.collection_service = CollectionService(self.endpoint)
self.index_service = IndexService(self.endpoint)
self.entity_service = EntityService(self.endpoint)
def teardown_method(self, method):
res = self.collection_service.has_collection(collection_name=self.collection_name)
log.info(f"collection {self.collection_name} exists: {res}")
if res["value"] is True:
res = self.collection_service.drop_collection(self.collection_name)
log.info(f"drop collection {self.collection_name} res: {res}")
res = self.collection_service.show_collections()
all_collections = res["collection_names"]
union_collections = set(all_collections) & set(self.collection_object_list)
for collection in union_collections:
res = self.collection_service.drop_collection(collection)
log.info(f"drop collection {collection} res: {res}")
log.info("[teardown_method] Start teardown test case %s." % method.__name__)
log.info(("*" * 35) + " teardown " + ("*" * 35))
class TestBase(Base):
"""init test base class"""
def init_collection(self, name=None, schema=None):
collection_name = cf.gen_unique_str("test") if name is None else name
self.collection_name = collection_name
self.collection_object_list.append(collection_name)
if schema is None:
schema = cf.gen_default_schema(collection_name=collection_name)
# create collection
res = self.collection_service.create_collection(collection_name=collection_name, schema=schema)
log.info(f"create collection name: {collection_name} with schema: {schema}")
return collection_name, schema

View File

@ -0,0 +1,96 @@
from api.collection import Collection
from utils.util_log import test_log as log
from models import milvus
TIMEOUT = 30
class CollectionService:
def __init__(self, endpoint=None, timeout=None):
if timeout is None:
timeout = TIMEOUT
if endpoint is None:
endpoint = "http://localhost:9091/api/v1"
self._collection = Collection(endpoint=endpoint)
def create_collection(self, collection_name, consistency_level=1, schema=None, shards_num=2):
payload = {
"collection_name": collection_name,
"consistency_level": consistency_level,
"schema": schema,
"shards_num": shards_num
}
log.info(f"payload: {payload}")
# payload = milvus.CreateCollectionRequest(collection_name=collection_name,
# consistency_level=consistency_level,
# schema=schema,
# shards_num=shards_num)
# payload = payload.dict()
rsp = self._collection.create_collection(payload)
return rsp
def has_collection(self, collection_name=None, time_stamp=0):
payload = {
"collection_name": collection_name,
"time_stamp": time_stamp
}
# payload = milvus.HasCollectionRequest(collection_name=collection_name, time_stamp=time_stamp)
# payload = payload.dict()
return self._collection.has_collection(payload)
def drop_collection(self, collection_name):
payload = {
"collection_name": collection_name
}
# payload = milvus.DropCollectionRequest(collection_name=collection_name)
# payload = payload.dict()
return self._collection.drop_collection(payload)
def describe_collection(self, collection_name, collection_id=None, time_stamp=0):
payload = {
"collection_name": collection_name,
"collection_id": collection_id,
"time_stamp": time_stamp
}
# payload = milvus.DescribeCollectionRequest(collection_name=collection_name,
# collectionID=collection_id,
# time_stamp=time_stamp)
# payload = payload.dict()
return self._collection.describe_collection(payload)
def load_collection(self, collection_name, replica_number=1):
payload = {
"collection_name": collection_name,
"replica_number": replica_number
}
# payload = milvus.LoadCollectionRequest(collection_name=collection_name, replica_number=replica_number)
# payload = payload.dict()
return self._collection.load_collection(payload)
def release_collection(self, collection_name):
payload = {
"collection_name": collection_name
}
# payload = milvus.ReleaseCollectionRequest(collection_name=collection_name)
# payload = payload.dict()
return self._collection.release_collection(payload)
def get_collection_statistics(self, collection_name):
payload = {
"collection_name": collection_name
}
# payload = milvus.GetCollectionStatisticsRequest(collection_name=collection_name)
# payload = payload.dict()
return self._collection.get_collection_statistics(payload)
def show_collections(self, collection_names=None, type=0):
payload = {
"collection_names": collection_names,
"type": type
}
# payload = milvus.ShowCollectionsRequest(collection_names=collection_names, type=type)
# payload = payload.dict()
return self._collection.show_collections(payload)

View File

@ -0,0 +1,182 @@
from api.entity import Entity
from common import common_type as ct
from utils.util_log import test_log as log
from models import common, schema, milvus, server
TIMEOUT = 30
class EntityService:
def __init__(self, endpoint=None, timeout=None):
if timeout is None:
timeout = TIMEOUT
if endpoint is None:
endpoint = "http://localhost:9091/api/v1"
self._entity = Entity(endpoint=endpoint)
def calc_distance(self, base=None, op_left=None, op_right=None, params=None):
payload = {
"base": base,
"op_left": op_left,
"op_right": op_right,
"params": params
}
# payload = milvus.CalcDistanceRequest(base=base, op_left=op_left, op_right=op_right, params=params)
# payload = payload.dict()
return self._entity.calc_distance(payload)
def delete(self, base=None, collection_name=None, db_name=None, expr=None, hash_keys=None, partition_name=None):
payload = {
"base": base,
"collection_name": collection_name,
"db_name": db_name,
"expr": expr,
"hash_keys": hash_keys,
"partition_name": partition_name
}
# payload = server.DeleteRequest(base=base,
# collection_name=collection_name,
# db_name=db_name,
# expr=expr,
# hash_keys=hash_keys,
# partition_name=partition_name)
# payload = payload.dict()
return self._entity.delete(payload)
def insert(self, base=None, collection_name=None, db_name=None, fields_data=None, hash_keys=None, num_rows=None,
partition_name=None, check_task=True):
payload = {
"base": base,
"collection_name": collection_name,
"db_name": db_name,
"fields_data": fields_data,
"hash_keys": hash_keys,
"num_rows": num_rows,
"partition_name": partition_name
}
# payload = milvus.InsertRequest(base=base,
# collection_name=collection_name,
# db_name=db_name,
# fields_data=fields_data,
# hash_keys=hash_keys,
# num_rows=num_rows,
# partition_name=partition_name)
# payload = payload.dict()
rsp = self._entity.insert(payload)
if check_task:
assert rsp["status"] == {}
assert rsp["insert_cnt"] == num_rows
return rsp
def flush(self, base=None, collection_names=None, db_name=None, check_task=True):
payload = {
"base": base,
"collection_names": collection_names,
"db_name": db_name
}
# payload = server.FlushRequest(base=base,
# collection_names=collection_names,
# db_name=db_name)
# payload = payload.dict()
rsp = self._entity.flush(payload)
if check_task:
assert rsp["status"] == {}
def get_persistent_segment_info(self, base=None, collection_name=None, db_name=None):
payload = {
"base": base,
"collection_name": collection_name,
"db_name": db_name
}
# payload = server.GetPersistentSegmentInfoRequest(base=base,
# collection_name=collection_name,
# db_name=db_name)
# payload = payload.dict()
return self._entity.get_persistent_segment_info(payload)
def get_flush_state(self, segment_ids=None):
payload = {
"segment_ids": segment_ids
}
# payload = server.GetFlushStateRequest(segment_ids=segment_ids)
# payload = payload.dict()
return self._entity.get_flush_state(payload)
def query(self, base=None, collection_name=None, db_name=None, expr=None,
guarantee_timestamp=None, output_fields=None, partition_names=None, travel_timestamp=None,
check_task=True):
payload = {
"base": base,
"collection_name": collection_name,
"db_name": db_name,
"expr": expr,
"guarantee_timestamp": guarantee_timestamp,
"output_fields": output_fields,
"partition_names": partition_names,
"travel_timestamp": travel_timestamp
}
#
# payload = server.QueryRequest(base=base, collection_name=collection_name, db_name=db_name, expr=expr,
# guarantee_timestamp=guarantee_timestamp, output_fields=output_fields,
# partition_names=partition_names, travel_timestamp=travel_timestamp)
# payload = payload.dict()
rsp = self._entity.query(payload)
if check_task:
fields_data = rsp["fields_data"]
for field_data in fields_data:
if field_data["field_name"] in expr:
data = field_data["Field"]["Scalars"]["Data"]["LongData"]["data"]
for d in data:
s = expr.replace(field_data["field_name"], str(d))
assert eval(s) is True
return rsp
def get_query_segment_info(self, base=None, collection_name=None, db_name=None):
payload = {
"base": base,
"collection_name": collection_name,
"db_name": db_name
}
# payload = server.GetQuerySegmentInfoRequest(base=base,
# collection_name=collection_name,
# db_name=db_name)
# payload = payload.dict()
return self._entity.get_query_segment_info(payload)
def search(self, base=None, collection_name=None, vectors=None, db_name=None, dsl=None,
output_fields=None, dsl_type=1,
guarantee_timestamp=None, partition_names=None, placeholder_group=None,
search_params=None, travel_timestamp=None, check_task=True):
payload = {
"base": base,
"collection_name": collection_name,
"output_fields": output_fields,
"vectors": vectors,
"db_name": db_name,
"dsl": dsl,
"dsl_type": dsl_type,
"guarantee_timestamp": guarantee_timestamp,
"partition_names": partition_names,
"placeholder_group": placeholder_group,
"search_params": search_params,
"travel_timestamp": travel_timestamp
}
# payload = server.SearchRequest(base=base, collection_name=collection_name, db_name=db_name, dsl=dsl,
# dsl_type=dsl_type, guarantee_timestamp=guarantee_timestamp,
# partition_names=partition_names, placeholder_group=placeholder_group,
# search_params=search_params, travel_timestamp=travel_timestamp)
# payload = payload.dict()
rsp = self._entity.search(payload)
if check_task:
assert rsp["status"] == {}
assert rsp["results"]["num_queries"] == len(vectors)
assert len(rsp["results"]["ids"]["IdField"]["IntId"]["data"]) == sum(rsp["results"]["topks"])
return rsp

View File

@ -0,0 +1,57 @@
from api.index import Index
from models import common, schema, milvus, server
TIMEOUT = 30
class IndexService:
def __init__(self, endpoint=None, timeout=None):
if timeout is None:
timeout = TIMEOUT
if endpoint is None:
endpoint = "http://localhost:9091/api/v1"
self._index = Index(endpoint=endpoint)
def drop_index(self, base, collection_name, db_name, field_name, index_name):
payload = server.DropIndexRequest(base=base, collection_name=collection_name,
db_name=db_name, field_name=field_name, index_name=index_name)
payload = payload.dict()
return self._index.drop_index(payload)
def describe_index(self, base, collection_name, db_name, field_name, index_name):
payload = server.DescribeIndexRequest(base=base, collection_name=collection_name,
db_name=db_name, field_name=field_name, index_name=index_name)
payload = payload.dict()
return self._index.describe_index(payload)
def create_index(self, base=None, collection_name=None, db_name=None, extra_params=None,
field_name=None, index_name=None):
payload = {
"base": base,
"collection_name": collection_name,
"db_name": db_name,
"extra_params": extra_params,
"field_name": field_name,
"index_name": index_name
}
# payload = server.CreateIndexRequest(base=base, collection_name=collection_name, db_name=db_name,
# extra_params=extra_params, field_name=field_name, index_name=index_name)
# payload = payload.dict()
return self._index.create_index(payload)
def get_index_build_progress(self, base, collection_name, db_name, field_name, index_name):
payload = server.GetIndexBuildProgressRequest(base=base, collection_name=collection_name,
db_name=db_name, field_name=field_name, index_name=index_name)
payload = payload.dict()
return self._index.get_index_build_progress(payload)
def get_index_state(self, base, collection_name, db_name, field_name, index_name):
payload = server.GetIndexStateRequest(base=base, collection_name=collection_name,
db_name=db_name, field_name=field_name, index_name=index_name)
payload = payload.dict()
return self._index.get_index_state(payload)

View File

View File

@ -0,0 +1,27 @@
class CheckTasks:
""" The name of the method used to check the result """
check_nothing = "check_nothing"
err_res = "error_response"
ccr = "check_connection_result"
check_collection_property = "check_collection_property"
check_partition_property = "check_partition_property"
check_search_results = "check_search_results"
check_query_results = "check_query_results"
check_query_empty = "check_query_empty" # verify that query result is empty
check_query_not_empty = "check_query_not_empty"
check_distance = "check_distance"
check_delete_compact = "check_delete_compact"
check_merge_compact = "check_merge_compact"
check_role_property = "check_role_property"
check_permission_deny = "check_permission_deny"
check_value_equal = "check_value_equal"
class ResponseChecker:
def __init__(self, check_task, check_items):
self.check_task = check_task
self.check_items = check_items

View File

@ -0,0 +1,21 @@
from utils.util_log import test_log as log
def ip_check(ip):
if ip == "localhost":
return True
if not isinstance(ip, str):
log.error("[IP_CHECK] IP(%s) is not a string." % ip)
return False
return True
def number_check(num):
if str(num).isdigit():
return True
else:
log.error("[NUMBER_CHECK] Number(%s) is not a numbers." % num)
return False

View File

@ -0,0 +1,292 @@
import json
import os
import random
import string
import numpy as np
from enum import Enum
from common import common_type as ct
from utils.util_log import test_log as log
class ParamInfo:
def __init__(self):
self.param_host = ""
self.param_port = ""
def prepare_param_info(self, host, http_port):
self.param_host = host
self.param_port = http_port
param_info = ParamInfo()
class DataType(Enum):
Bool: 1
Int8: 2
Int16: 3
Int32: 4
Int64: 5
Float: 10
Double: 11
String: 20
VarChar: 21
BinaryVector: 100
FloatVector: 101
def gen_unique_str(str_value=None):
prefix = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
return "test_" + prefix if str_value is None else str_value + "_" + prefix
def gen_field(name=ct.default_bool_field_name, description=ct.default_desc, type_params=None, index_params=None,
data_type="Int64", is_primary_key=False, auto_id=False, dim=128, max_length=256):
data_type_map = {
"Bool": 1,
"Int8": 2,
"Int16": 3,
"Int32": 4,
"Int64": 5,
"Float": 10,
"Double": 11,
"String": 20,
"VarChar": 21,
"BinaryVector": 100,
"FloatVector": 101,
}
if data_type == "Int64":
is_primary_key = True
auto_id = True
if type_params is None:
type_params = []
if index_params is None:
index_params = []
if data_type in ["FloatVector", "BinaryVector"]:
type_params = [{"key": "dim", "value": str(dim)}]
if data_type in ["String", "VarChar"]:
type_params = [{"key": "max_length", "value": str(dim)}]
return {
"name": name,
"description": description,
"data_type": data_type_map.get(data_type, 0),
"type_params": type_params,
"index_params": index_params,
"is_primary_key": is_primary_key,
"auto_id": auto_id,
}
def gen_schema(name, fields, description=ct.default_desc, auto_id=False):
return {
"name": name,
"description": description,
"auto_id": auto_id,
"fields": fields,
}
def gen_default_schema(data_types=None, dim=ct.default_dim, collection_name=None):
if data_types is None:
data_types = ["Int64", "Float", "VarChar", "FloatVector"]
fields = []
for data_type in data_types:
if data_type in ["FloatVector", "BinaryVector"]:
fields.append(gen_field(name=data_type, data_type=data_type, type_params=[{"key": "dim", "value": dim}]))
else:
fields.append(gen_field(name=data_type, data_type=data_type))
return {
"autoID": True,
"fields": fields,
"description": ct.default_desc,
"name": collection_name,
}
def gen_fields_data(schema=None, nb=ct.default_nb,):
if schema is None:
schema = gen_default_schema()
fields = schema["fields"]
fields_data = []
for field in fields:
if field["data_type"] == 1:
fields_data.append([random.choice([True, False]) for i in range(nb)])
elif field["data_type"] == 2:
fields_data.append([i for i in range(nb)])
elif field["data_type"] == 3:
fields_data.append([i for i in range(nb)])
elif field["data_type"] == 4:
fields_data.append([i for i in range(nb)])
elif field["data_type"] == 5:
fields_data.append([i for i in range(nb)])
elif field["data_type"] == 10:
fields_data.append([np.float64(i) for i in range(nb)]) # json not support float32
elif field["data_type"] == 11:
fields_data.append([np.float64(i) for i in range(nb)])
elif field["data_type"] == 20:
fields_data.append([gen_unique_str((str(i))) for i in range(nb)])
elif field["data_type"] == 21:
fields_data.append([gen_unique_str(str(i)) for i in range(nb)])
elif field["data_type"] == 100:
dim = ct.default_dim
for k, v in field["type_params"]:
if k == "dim":
dim = int(v)
break
fields_data.append(gen_binary_vectors(nb, dim))
elif field["data_type"] == 101:
dim = ct.default_dim
for k, v in field["type_params"]:
if k == "dim":
dim = int(v)
break
fields_data.append(gen_float_vectors(nb, dim))
else:
log.error("Unknown data type.")
fields_data_body = []
for i, field in enumerate(fields):
fields_data_body.append({
"field_name": field["name"],
"type": field["data_type"],
"field": fields_data[i],
})
return fields_data_body
def get_vector_field(schema):
for field in schema["fields"]:
if field["data_type"] in [100, 101]:
return field["name"]
return None
def get_varchar_field(schema):
for field in schema["fields"]:
if field["data_type"] == 21:
return field["name"]
return None
def gen_vectors(nq=None, schema=None):
if nq is None:
nq = ct.default_nq
dim = ct.default_dim
data_type = 101
for field in schema["fields"]:
if field["data_type"] in [100, 101]:
dim = ct.default_dim
data_type = field["data_type"]
for k, v in field["type_params"]:
if k == "dim":
dim = int(v)
break
if data_type == 100:
return gen_binary_vectors(nq, dim)
if data_type == 101:
return gen_float_vectors(nq, dim)
def gen_float_vectors(nb, dim):
return [[np.float64(random.uniform(-1.0, 1.0)) for _ in range(dim)] for _ in range(nb)] # json not support float32
def gen_binary_vectors(nb, dim):
raw_vectors = []
binary_vectors = []
for _ in range(nb):
raw_vector = [random.randint(0, 1) for _ in range(dim)]
raw_vectors.append(raw_vector)
# packs a binary-valued array into bits in a unit8 array, and bytes array_of_ints
binary_vectors.append(bytes(np.packbits(raw_vector, axis=-1).tolist()))
return binary_vectors
def gen_index_params(index_type=None):
if index_type is None:
index_params = ct.default_index_params
else:
index_params = ct.all_index_params_map[index_type]
extra_params = []
for k, v in index_params.items():
item = {"key": k, "value": json.dumps(v) if isinstance(v, dict) else str(v)}
extra_params.append(item)
return extra_params
def gen_search_param_by_index_type(index_type, metric_type="L2"):
search_params = []
if index_type in ["FLAT", "IVF_FLAT", "IVF_SQ8", "IVF_PQ"]:
for nprobe in [10]:
ivf_search_params = {"metric_type": metric_type, "params": {"nprobe": nprobe}}
search_params.append(ivf_search_params)
elif index_type in ["BIN_FLAT", "BIN_IVF_FLAT"]:
for nprobe in [10]:
bin_search_params = {"metric_type": "HAMMING", "params": {"nprobe": nprobe}}
search_params.append(bin_search_params)
elif index_type in ["HNSW"]:
for ef in [64]:
hnsw_search_param = {"metric_type": metric_type, "params": {"ef": ef}}
search_params.append(hnsw_search_param)
elif index_type == "ANNOY":
for search_k in [1000]:
annoy_search_param = {"metric_type": metric_type, "params": {"search_k": search_k}}
search_params.append(annoy_search_param)
else:
log.info("Invalid index_type.")
raise Exception("Invalid index_type.")
return search_params
def gen_search_params(index_type=None, anns_field=ct.default_float_vec_field_name,
topk=ct.default_top_k):
if index_type is None:
search_params = gen_search_param_by_index_type(ct.default_index_type)[0]
else:
search_params = gen_search_param_by_index_type(index_type)[0]
extra_params = []
for k, v in search_params.items():
item = {"key": k, "value": json.dumps(v) if isinstance(v, dict) else str(v)}
extra_params.append(item)
extra_params.append({"key": "anns_field", "value": anns_field})
extra_params.append({"key": "topk", "value": str(topk)})
return extra_params
def gen_search_vectors(dim, nb, is_binary=False):
if is_binary:
return gen_binary_vectors(nb, dim)
return gen_float_vectors(nb, dim)
def modify_file(file_path_list, is_modify=False, input_content=""):
"""
file_path_list : file list -> list[<file_path>]
is_modify : does the file need to be reset
input_content the content that need to insert to the file
"""
if not isinstance(file_path_list, list):
log.error("[modify_file] file is not a list.")
for file_path in file_path_list:
folder_path, file_name = os.path.split(file_path)
if not os.path.isdir(folder_path):
log.debug("[modify_file] folder(%s) is not exist." % folder_path)
os.makedirs(folder_path)
if not os.path.isfile(file_path):
log.error("[modify_file] file(%s) is not exist." % file_path)
else:
if is_modify is True:
log.debug("[modify_file] start modifying file(%s)..." % file_path)
with open(file_path, "r+") as f:
f.seek(0)
f.truncate()
f.write(input_content)
f.close()
log.info("[modify_file] file(%s) modification is complete." % file_path_list)
if __name__ == '__main__':
a = gen_binary_vectors(10, 128)
print(a)

View File

@ -0,0 +1,80 @@
""" Initialized parameters """
port = 19530
epsilon = 0.000001
namespace = "milvus"
default_flush_interval = 1
big_flush_interval = 1000
default_drop_interval = 3
default_dim = 128
default_nb = 3000
default_nb_medium = 5000
default_top_k = 10
default_nq = 2
default_limit = 10
default_search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
default_search_ip_params = {"metric_type": "IP", "params": {"nprobe": 10}}
default_search_binary_params = {"metric_type": "JACCARD", "params": {"nprobe": 10}}
default_index_type = "HNSW"
default_index_params = {"index_type": "HNSW", "params": {"M": 48, "efConstruction": 500}, "metric_type": "L2"}
default_varchar_index = {}
default_binary_index = {"index_type": "BIN_IVF_FLAT", "params": {"nlist": 128}, "metric_type": "JACCARD"}
default_diskann_index = {"index_type": "DISKANN", "metric_type": "L2", "params": {}}
default_diskann_search_params = {"metric_type": "L2", "params": {"search_list": 30}}
max_top_k = 16384
max_partition_num = 4096 # 256
default_segment_row_limit = 1000
default_server_segment_row_limit = 1024 * 512
default_alias = "default"
default_user = "root"
default_password = "Milvus"
default_bool_field_name = "Bool"
default_int8_field_name = "Int8"
default_int16_field_name = "Int16"
default_int32_field_name = "Int32"
default_int64_field_name = "Int64"
default_float_field_name = "Float"
default_double_field_name = "Double"
default_string_field_name = "Varchar"
default_float_vec_field_name = "FloatVector"
another_float_vec_field_name = "FloatVector1"
default_binary_vec_field_name = "BinaryVector"
default_partition_name = "_default"
default_tag = "1970_01_01"
row_count = "row_count"
default_length = 65535
default_desc = ""
default_collection_desc = "default collection"
default_index_name = "default_index_name"
default_binary_desc = "default binary collection"
collection_desc = "collection"
int_field_desc = "int64 type field"
float_field_desc = "float type field"
float_vec_field_desc = "float vector type field"
binary_vec_field_desc = "binary vector type field"
max_dim = 32768
min_dim = 1
gracefulTime = 1
default_nlist = 128
compact_segment_num_threshold = 4
compact_delta_ratio_reciprocal = 5 # compact_delta_binlog_ratio is 0.2
compact_retention_duration = 40 # compaction travel time retention range 20s
max_compaction_interval = 60 # the max time interval (s) from the last compaction
max_field_num = 256 # Maximum number of fields in a collection
default_dsl = f"{default_int64_field_name} in [2,4,6,8]"
default_expr = f"{default_int64_field_name} in [2,4,6,8]"
metric_types = []
all_index_types = ["FLAT", "IVF_FLAT", "IVF_SQ8", "IVF_PQ", "HNSW", "ANNOY", "DISKANN", "BIN_FLAT", "BIN_IVF_FLAT"]
all_index_params_map = {"FLAT": {"index_type": "FLAT", "params": {"nlist": 128}, "metric_type": "L2"},
"IVF_FLAT": {"index_type": "IVF_FLAT", "params": {"nlist": 128}, "metric_type": "L2"},
"IVF_SQ8": {"index_type": "IVF_SQ8", "params": {"nlist": 128}, "metric_type": "L2"},
"IVF_PQ": {"index_type": "IVF_PQ", "params": {"nlist": 128, "m": 16, "nbits": 8},
"metric_type": "L2"},
"HNSW": {"index_type": "HNSW", "params": {"M": 48, "efConstruction": 500}, "metric_type": "L2"},
"ANNOY": {"index_type": "ANNOY", "params": {"n_trees": 50}, "metric_type": "L2"},
"DISKANN": {"index_type": "DISKANN", "params": {}, "metric_type": "L2"},
"BIN_FLAT": {"index_type": "BIN_FLAT", "params": {"nlist": 128}, "metric_type": "JACCARD"},
"BIN_IVF_FLAT": {"index_type": "BIN_IVF_FLAT", "params": {"nlist": 128},
"metric_type": "JACCARD"}
}

View File

@ -0,0 +1,44 @@
import os
class LogConfig:
def __init__(self):
self.log_debug = ""
self.log_err = ""
self.log_info = ""
self.log_worker = ""
self.get_default_config()
@staticmethod
def get_env_variable(var="CI_LOG_PATH"):
""" get log path for testing """
try:
log_path = os.environ[var]
return str(log_path)
except Exception as e:
# now = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
log_path = f"/tmp/ci_logs"
print("[get_env_variable] failed to get environment variables : %s, use default path : %s" % (str(e), log_path))
return log_path
@staticmethod
def create_path(log_path):
if not os.path.isdir(str(log_path)):
print("[create_path] folder(%s) is not exist." % log_path)
print("[create_path] create path now...")
os.makedirs(log_path)
def get_default_config(self):
""" Make sure the path exists """
log_dir = self.get_env_variable()
self.log_debug = "%s/ci_test_log.debug" % log_dir
self.log_info = "%s/ci_test_log.log" % log_dir
self.log_err = "%s/ci_test_log.err" % log_dir
work_log = os.environ.get('PYTEST_XDIST_WORKER')
if work_log is not None:
self.log_worker = f'{log_dir}/{work_log}.log'
self.create_path(log_dir)
log_config = LogConfig()

View File

@ -0,0 +1,49 @@
import pytest
import common.common_func as cf
from check.param_check import ip_check, number_check
from config.log_config import log_config
from utils.util_log import test_log as log
from common.common_func import param_info
def pytest_addoption(parser):
parser.addoption("--host", action="store", default="127.0.0.1", help="Milvus host")
parser.addoption("--port", action="store", default="9091", help="Milvus http port")
parser.addoption('--clean_log', action='store_true', default=False, help="clean log before testing")
@pytest.fixture
def host(request):
return request.config.getoption("--host")
@pytest.fixture
def port(request):
return request.config.getoption("--port")
@pytest.fixture
def clean_log(request):
return request.config.getoption("--clean_log")
@pytest.fixture(scope="session", autouse=True)
def initialize_env(request):
""" clean log before testing """
host = request.config.getoption("--host")
port = request.config.getoption("--port")
clean_log = request.config.getoption("--clean_log")
""" params check """
assert ip_check(host) and number_check(port)
""" modify log files """
file_path_list = [log_config.log_debug, log_config.log_info, log_config.log_err]
if log_config.log_worker != "":
file_path_list.append(log_config.log_worker)
cf.modify_file(file_path_list=file_path_list, is_modify=clean_log)
log.info("#" * 80)
log.info("[initialize_milvus] Log cleaned up, start testing...")
param_info.prepare_param_info(host, port)

View File

@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: openapi.json
# timestamp: 2022-12-08T02:46:08+00:00

View File

@ -0,0 +1,28 @@
# generated by datamodel-codegen:
# filename: openapi.json
# timestamp: 2022-12-08T02:46:08+00:00
from __future__ import annotations
from typing import List, Optional
from pydantic import BaseModel, Field
class KeyDataPair(BaseModel):
data: Optional[List[int]] = None
key: Optional[str] = None
class KeyValuePair(BaseModel):
key: Optional[str] = Field(None, example='dim')
value: Optional[str] = Field(None, example='128')
class MsgBase(BaseModel):
msg_type: Optional[int] = Field(None, description='Not useful for now')
class Status(BaseModel):
error_code: Optional[int] = None
reason: Optional[str] = None

View File

@ -0,0 +1,138 @@
# generated by datamodel-codegen:
# filename: openapi.json
# timestamp: 2022-12-08T02:46:08+00:00
from __future__ import annotations
from typing import List, Optional
from pydantic import BaseModel, Field
from models import common, schema
class DescribeCollectionRequest(BaseModel):
collection_name: Optional[str] = None
collectionID: Optional[int] = Field(
None, description='The collection ID you want to describe'
)
time_stamp: Optional[int] = Field(
None,
description='If time_stamp is not zero, will describe collection success when time_stamp >= created collection timestamp, otherwise will throw error.',
)
class DropCollectionRequest(BaseModel):
collection_name: Optional[str] = Field(
None, description='The unique collection name in milvus.(Required)'
)
class FieldData(BaseModel):
field: Optional[List] = None
field_id: Optional[int] = None
field_name: Optional[str] = None
type: Optional[int] = Field(
None,
description='0: "None",\n1: "Bool",\n2: "Int8",\n3: "Int16",\n4: "Int32",\n5: "Int64",\n10: "Float",\n11: "Double",\n20: "String",\n21: "VarChar",\n100: "BinaryVector",\n101: "FloatVector",',
)
class GetCollectionStatisticsRequest(BaseModel):
collection_name: Optional[str] = Field(
None, description='The collection name you want get statistics'
)
class HasCollectionRequest(BaseModel):
collection_name: Optional[str] = Field(
None, description='The unique collection name in milvus.(Required)'
)
time_stamp: Optional[int] = Field(
None,
description='If time_stamp is not zero, will return true when time_stamp >= created collection timestamp, otherwise will return false.',
)
class InsertRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = None
db_name: Optional[str] = None
fields_data: Optional[List[FieldData]] = None
hash_keys: Optional[List[int]] = None
num_rows: Optional[int] = None
partition_name: Optional[str] = None
class LoadCollectionRequest(BaseModel):
collection_name: Optional[str] = Field(
None, description='The collection name you want to load'
)
replica_number: Optional[int] = Field(
None, description='The replica number to load, default by 1'
)
class ReleaseCollectionRequest(BaseModel):
collection_name: Optional[str] = Field(
None, description='The collection name you want to release'
)
class ShowCollectionsRequest(BaseModel):
collection_names: Optional[List[str]] = Field(
None,
description="When type is InMemory, will return these collection's inMemory_percentages.(Optional)",
)
type: Optional[int] = Field(
None,
description='Decide return Loaded collections or All collections(Optional)',
)
class VectorIDs(BaseModel):
collection_name: Optional[str] = None
field_name: Optional[str] = None
id_array: Optional[List[int]] = None
partition_names: Optional[List[str]] = None
class VectorsArray(BaseModel):
binary_vectors: Optional[List[int]] = Field(
None,
description='Vectors is an array of binary vector divided by given dim. Disabled when IDs is set',
)
dim: Optional[int] = Field(
None, description='Dim of vectors or binary_vectors, not needed when use ids'
)
ids: Optional[VectorIDs] = None
vectors: Optional[List[float]] = Field(
None,
description='Vectors is an array of vector divided by given dim. Disabled when ids or binary_vectors is set',
)
class CalcDistanceRequest(BaseModel):
base: Optional[common.MsgBase] = None
op_left: Optional[VectorsArray] = None
op_right: Optional[VectorsArray] = None
params: Optional[List[common.KeyValuePair]] = None
class CreateCollectionRequest(BaseModel):
collection_name: str = Field(
...,
description='The unique collection name in milvus.(Required)',
example='book',
)
consistency_level: int = Field(
...,
description='The consistency level that the collection used, modification is not supported now.\n"Strong": 0,\n"Session": 1,\n"Bounded": 2,\n"Eventually": 3,\n"Customized": 4,',
example=1,
)
schema_: schema.CollectionSchema = Field(..., alias='schema')
shards_num: Optional[int] = Field(
None,
description='Once set, no modification is allowed (Optional)\nhttps://github.com/milvus-io/milvus/issues/6690',
example=1,
)

View File

@ -0,0 +1,72 @@
# generated by datamodel-codegen:
# filename: openapi.json
# timestamp: 2022-12-08T02:46:08+00:00
from __future__ import annotations
from typing import Any, List, Optional
from pydantic import BaseModel, Field
from models import common
class FieldData(BaseModel):
field: Optional[Any] = Field(
None,
description='Types that are assignable to Field:\n\t*FieldData_Scalars\n\t*FieldData_Vectors',
)
field_id: Optional[int] = None
field_name: Optional[str] = None
type: Optional[int] = Field(
None,
description='0: "None",\n1: "Bool",\n2: "Int8",\n3: "Int16",\n4: "Int32",\n5: "Int64",\n10: "Float",\n11: "Double",\n20: "String",\n21: "VarChar",\n100: "BinaryVector",\n101: "FloatVector",',
)
class FieldSchema(BaseModel):
autoID: Optional[bool] = None
data_type: int = Field(
...,
description='0: "None",\n1: "Bool",\n2: "Int8",\n3: "Int16",\n4: "Int32",\n5: "Int64",\n10: "Float",\n11: "Double",\n20: "String",\n21: "VarChar",\n100: "BinaryVector",\n101: "FloatVector",',
example=101,
)
description: Optional[str] = Field(
None, example='embedded vector of book introduction'
)
fieldID: Optional[int] = None
index_params: Optional[List[common.KeyValuePair]] = None
is_primary_key: Optional[bool] = Field(None, example=False)
name: str = Field(..., example='book_intro')
type_params: Optional[List[common.KeyValuePair]] = None
class IDs(BaseModel):
idField: Optional[Any] = Field(
None,
description='Types that are assignable to IdField:\n\t*IDs_IntId\n\t*IDs_StrId',
)
class LongArray(BaseModel):
data: Optional[List[int]] = None
class SearchResultData(BaseModel):
fields_data: Optional[List[FieldData]] = None
ids: Optional[IDs] = None
num_queries: Optional[int] = None
scores: Optional[List[float]] = None
top_k: Optional[int] = None
topks: Optional[List[int]] = None
class CollectionSchema(BaseModel):
autoID: Optional[bool] = Field(
None,
description='deprecated later, keep compatible with c++ part now',
example=False,
)
description: Optional[str] = Field(None, example='Test book search')
fields: Optional[List[FieldSchema]] = None
name: str = Field(..., example='book')

View File

@ -0,0 +1,587 @@
# generated by datamodel-codegen:
# filename: openapi.json
# timestamp: 2022-12-08T02:46:08+00:00
from __future__ import annotations
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
from models import common, schema
class AlterAliasRequest(BaseModel):
alias: Optional[str] = None
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = None
db_name: Optional[str] = None
class BoolResponse(BaseModel):
status: Optional[common.Status] = None
value: Optional[bool] = None
class CalcDistanceResults(BaseModel):
array: Optional[Any] = Field(
None,
description='num(op_left)*num(op_right) distance values, "HAMMIN" return integer distance\n\nTypes that are assignable to Array:\n\t*CalcDistanceResults_IntDist\n\t*CalcDistanceResults_FloatDist',
)
status: Optional[common.Status] = None
class CompactionMergeInfo(BaseModel):
sources: Optional[List[int]] = None
target: Optional[int] = None
class CreateAliasRequest(BaseModel):
alias: Optional[str] = None
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = None
db_name: Optional[str] = None
class CreateCredentialRequest(BaseModel):
base: Optional[common.MsgBase] = None
created_utc_timestamps: Optional[int] = Field(None, description='create time')
modified_utc_timestamps: Optional[int] = Field(None, description='modify time')
password: Optional[str] = Field(None, description='ciphertext password')
username: Optional[str] = Field(None, description='username')
class CreateIndexRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The particular collection name you want to create index.'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
extra_params: Optional[List[common.KeyValuePair]] = Field(
None,
description='Support keys: index_type,metric_type, params. Different index_type may has different params.',
)
field_name: Optional[str] = Field(
None, description='The vector field name in this particular collection'
)
index_name: Optional[str] = Field(
None,
description="Version before 2.0.2 doesn't contain index_name, we use default index name.",
)
class CreatePartitionRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The collection name in milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
partition_name: Optional[str] = Field(
None, description='The partition name you want to create.'
)
class DeleteCredentialRequest(BaseModel):
base: Optional[common.MsgBase] = None
username: Optional[str] = Field(None, description='Not useful for now')
class DeleteRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = None
db_name: Optional[str] = None
expr: Optional[str] = None
hash_keys: Optional[List[int]] = None
partition_name: Optional[str] = None
class DescribeIndexRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The particular collection name in Milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
field_name: Optional[str] = Field(
None, description='The vector field name in this particular collection'
)
index_name: Optional[str] = Field(
None, description='No need to set up for now @2021.06.30'
)
class DropAliasRequest(BaseModel):
alias: Optional[str] = None
base: Optional[common.MsgBase] = None
db_name: Optional[str] = None
class DropIndexRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(None, description='must')
db_name: Optional[str] = None
field_name: Optional[str] = None
index_name: Optional[str] = Field(
None, description='No need to set up for now @2021.06.30'
)
class DropPartitionRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The collection name in milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
partition_name: Optional[str] = Field(
None, description='The partition name you want to drop'
)
class FlushRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_names: Optional[List[str]] = None
db_name: Optional[str] = None
class FlushResponse(BaseModel):
coll_segIDs: Optional[Dict[str, schema.LongArray]] = None
db_name: Optional[str] = None
status: Optional[common.Status] = None
class GetCollectionStatisticsResponse(BaseModel):
stats: Optional[List[common.KeyValuePair]] = Field(
None, description='Collection statistics data'
)
status: Optional[common.Status] = None
class GetCompactionPlansRequest(BaseModel):
compactionID: Optional[int] = None
class GetCompactionPlansResponse(BaseModel):
mergeInfos: Optional[List[CompactionMergeInfo]] = None
state: Optional[int] = None
status: Optional[common.Status] = None
class GetCompactionStateRequest(BaseModel):
compactionID: Optional[int] = None
class GetCompactionStateResponse(BaseModel):
completedPlanNo: Optional[int] = None
executingPlanNo: Optional[int] = None
state: Optional[int] = None
status: Optional[common.Status] = None
timeoutPlanNo: Optional[int] = None
class GetFlushStateRequest(BaseModel):
segmentIDs: Optional[List[int]] = Field(None, alias='segment_ids')
class GetFlushStateResponse(BaseModel):
flushed: Optional[bool] = None
status: Optional[common.Status] = None
class GetImportStateRequest(BaseModel):
task: Optional[int] = Field(None, description='id of an import task')
class GetImportStateResponse(BaseModel):
id: Optional[int] = Field(None, description='id of an import task')
id_list: Optional[List[int]] = Field(
None, description='auto generated ids if the primary key is autoid'
)
infos: Optional[List[common.KeyValuePair]] = Field(
None,
description='more informations about the task, progress percent, file path, failed reason, etc.',
)
row_count: Optional[int] = Field(
None,
description='if the task is finished, this value is how many rows are imported. if the task is not finished, this value is how many rows are parsed. return 0 if failed.',
)
state: Optional[int] = Field(
None, description='is this import task finished or not'
)
status: Optional[common.Status] = None
class GetIndexBuildProgressRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The collection name in milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
field_name: Optional[str] = Field(
None, description='The vector field name in this collection'
)
index_name: Optional[str] = Field(None, description='Not useful for now')
class GetIndexBuildProgressResponse(BaseModel):
indexed_rows: Optional[int] = None
status: Optional[common.Status] = None
total_rows: Optional[int] = None
class GetIndexStateRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(None, description='must')
db_name: Optional[str] = None
field_name: Optional[str] = None
index_name: Optional[str] = Field(
None, description='No need to set up for now @2021.06.30'
)
class GetIndexStateResponse(BaseModel):
fail_reason: Optional[str] = None
state: Optional[int] = None
status: Optional[common.Status] = None
class GetMetricsRequest(BaseModel):
base: Optional[common.MsgBase] = None
request: Optional[str] = Field(None, description='request is of jsonic format')
class GetMetricsResponse(BaseModel):
component_name: Optional[str] = Field(
None, description='metrics from which component'
)
response: Optional[str] = Field(None, description='response is of jsonic format')
status: Optional[common.Status] = None
class GetPartitionStatisticsRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The collection name in milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
partition_name: Optional[str] = Field(
None, description='The partition name you want to collect statistics'
)
class GetPartitionStatisticsResponse(BaseModel):
stats: Optional[List[common.KeyValuePair]] = None
status: Optional[common.Status] = None
class GetPersistentSegmentInfoRequest(BaseModel):
base: Optional[common.MsgBase] = None
collectionName: Optional[str] = Field(None, alias="collection_name", description='must')
dbName: Optional[str] = Field(None, alias="db_name")
class GetQuerySegmentInfoRequest(BaseModel):
base: Optional[common.MsgBase] = None
collectionName: Optional[str] = Field(None, alias="collection_name", description='must')
dbName: Optional[str] = Field(None, alias="db_name")
class GetReplicasRequest(BaseModel):
base: Optional[common.MsgBase] = None
collectionID: Optional[int] = Field(None, alias="collection_id")
with_shard_nodes: Optional[bool] = None
class HasPartitionRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The collection name in milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
partition_name: Optional[str] = Field(
None, description='The partition name you want to check'
)
class ImportRequest(BaseModel):
channel_names: Optional[List[str]] = Field(
None, description='channel names for the collection'
)
collection_name: Optional[str] = Field(None, description='target collection')
files: Optional[List[str]] = Field(None, description='file paths to be imported')
options: Optional[List[common.KeyValuePair]] = Field(
None, description='import options, bucket, etc.'
)
partition_name: Optional[str] = Field(None, description='target partition')
row_based: Optional[bool] = Field(
None, description='the file is row-based or column-based'
)
class ImportResponse(BaseModel):
status: Optional[common.Status] = None
tasks: Optional[List[int]] = Field(None, description='id array of import tasks')
class IndexDescription(BaseModel):
field_name: Optional[str] = Field(None, description='The vector field name')
index_name: Optional[str] = Field(None, description='Index name')
indexID: Optional[int] = Field(None, description='Index id')
params: Optional[List[common.KeyValuePair]] = Field(
None, description='Will return index_type, metric_type, params(like nlist).'
)
class ListCredUsersRequest(BaseModel):
base: Optional[common.MsgBase] = None
class ListCredUsersResponse(BaseModel):
status: Optional[common.Status] = None
usernames: Optional[List[str]] = Field(None, description='username array')
class ListImportTasksRequest(BaseModel):
pass
class ListImportTasksResponse(BaseModel):
status: Optional[common.Status] = None
tasks: Optional[List[GetImportStateResponse]] = Field(
None, description='list of all import tasks'
)
class LoadBalanceRequest(BaseModel):
base: Optional[common.MsgBase] = None
collectionName: Optional[str] = None
dst_nodeIDs: Optional[List[int]] = None
sealed_segmentIDs: Optional[List[int]] = None
src_nodeID: Optional[int] = None
class LoadPartitionsRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The collection name in milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
partition_names: Optional[List[str]] = Field(
None, description='The partition names you want to load'
)
replica_number: Optional[int] = Field(
None, description='The replicas number you would load, 1 by default'
)
class ManualCompactionRequest(BaseModel):
collectionID: Optional[int] = None
timetravel: Optional[int] = None
class ManualCompactionResponse(BaseModel):
compactionID: Optional[int] = None
status: Optional[common.Status] = None
class PersistentSegmentInfo(BaseModel):
collectionID: Optional[int] = None
num_rows: Optional[int] = None
partitionID: Optional[int] = None
segmentID: Optional[int] = None
state: Optional[int] = None
class QueryRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = None
db_name: Optional[str] = None
expr: Optional[str] = None
guarantee_timestamp: Optional[int] = Field(None, description='guarantee_timestamp')
output_fields: Optional[List[str]] = None
partition_names: Optional[List[str]] = None
travel_timestamp: Optional[int] = None
class QueryResults(BaseModel):
collection_name: Optional[str] = None
fields_data: Optional[List[schema.FieldData]] = None
status: Optional[common.Status] = None
class QuerySegmentInfo(BaseModel):
collectionID: Optional[int] = None
index_name: Optional[str] = None
indexID: Optional[int] = None
mem_size: Optional[int] = None
nodeID: Optional[int] = None
num_rows: Optional[int] = None
partitionID: Optional[int] = None
segmentID: Optional[int] = None
state: Optional[int] = None
class ReleasePartitionsRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None, description='The collection name in milvus'
)
db_name: Optional[str] = Field(None, description='Not useful for now')
partition_names: Optional[List[str]] = Field(
None, description='The partition names you want to release'
)
class SearchRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(None, description='must')
db_name: Optional[str] = None
dsl: Optional[str] = Field(None, description='must')
dsl_type: Optional[int] = Field(None, description='must')
guarantee_timestamp: Optional[int] = Field(None, description='guarantee_timestamp')
output_fields: Optional[List[str]] = None
partition_names: Optional[List[str]] = Field(None, description='must')
placeholder_group: Optional[List[int]] = Field(
None, description='serialized `PlaceholderGroup`'
)
search_params: Optional[List[common.KeyValuePair]] = Field(None, description='must')
travel_timestamp: Optional[int] = None
class SearchResults(BaseModel):
collection_name: Optional[str] = None
results: Optional[schema.SearchResultData] = None
status: Optional[common.Status] = None
class ShardReplica(BaseModel):
dm_channel_name: Optional[str] = None
leader_addr: Optional[str] = Field(None, description='IP:port')
leaderID: Optional[int] = None
node_ids: Optional[List[int]] = Field(
None,
description='optional, DO NOT save it in meta, set it only for GetReplicas()\nif with_shard_nodes is true',
)
class ShowCollectionsResponse(BaseModel):
collection_ids: Optional[List[int]] = Field(None, description='Collection Id array')
collection_names: Optional[List[str]] = Field(
None, description='Collection name array'
)
created_timestamps: Optional[List[int]] = Field(
None, description='Hybrid timestamps in milvus'
)
created_utc_timestamps: Optional[List[int]] = Field(
None, description='The utc timestamp calculated by created_timestamp'
)
inMemory_percentages: Optional[List[int]] = Field(
None, description='Load percentage on querynode when type is InMemory'
)
status: Optional[common.Status] = None
class ShowPartitionsRequest(BaseModel):
base: Optional[common.MsgBase] = None
collection_name: Optional[str] = Field(
None,
description='The collection name you want to describe, you can pass collection_name or collectionID',
)
collectionID: Optional[int] = Field(None, description='The collection id in milvus')
db_name: Optional[str] = Field(None, description='Not useful for now')
partition_names: Optional[List[str]] = Field(
None,
description="When type is InMemory, will return these patitions' inMemory_percentages.(Optional)",
)
type: Optional[int] = Field(
None, description='Decide return Loaded partitions or All partitions(Optional)'
)
class ShowPartitionsResponse(BaseModel):
created_timestamps: Optional[List[int]] = Field(
None, description='All hybrid timestamps'
)
created_utc_timestamps: Optional[List[int]] = Field(
None, description='All utc timestamps calculated by created_timestamps'
)
inMemory_percentages: Optional[List[int]] = Field(
None, description='Load percentage on querynode'
)
partition_names: Optional[List[str]] = Field(
None, description='All partition names for this collection'
)
partitionIDs: Optional[List[int]] = Field(
None, description='All partition ids for this collection'
)
status: Optional[common.Status] = None
class UpdateCredentialRequest(BaseModel):
base: Optional[common.MsgBase] = None
created_utc_timestamps: Optional[int] = Field(None, description='create time')
modified_utc_timestamps: Optional[int] = Field(None, description='modify time')
newPassword: Optional[str] = Field(None, description='new password')
oldPassword: Optional[str] = Field(None, description='old password')
username: Optional[str] = Field(None, description='username')
class DescribeCollectionResponse(BaseModel):
aliases: Optional[List[str]] = Field(
None, description='The aliases of this collection'
)
collection_name: Optional[str] = Field(None, description='The collection name')
collectionID: Optional[int] = Field(None, description='The collection id')
consistency_level: Optional[int] = Field(
None,
description='The consistency level that the collection used, modification is not supported now.',
)
created_timestamp: Optional[int] = Field(
None, description='Hybrid timestamp in milvus'
)
created_utc_timestamp: Optional[int] = Field(
None, description='The utc timestamp calculated by created_timestamp'
)
physical_channel_names: Optional[List[str]] = Field(
None, description='System design related, users should not perceive'
)
schema_: Optional[schema.CollectionSchema] = Field(None, alias='schema')
shards_num: Optional[int] = Field(None, description='The shards number you set.')
start_positions: Optional[List[common.KeyDataPair]] = Field(
None, description='The message ID/posititon when collection is created'
)
status: Optional[common.Status] = None
virtual_channel_names: Optional[List[str]] = Field(
None, description='System design related, users should not perceive'
)
class DescribeIndexResponse(BaseModel):
index_descriptions: Optional[List[IndexDescription]] = Field(
None,
description='All index informations, for now only return tha latest index you created for the collection.',
)
status: Optional[common.Status] = None
class GetPersistentSegmentInfoResponse(BaseModel):
infos: Optional[List[PersistentSegmentInfo]] = None
status: Optional[common.Status] = None
class GetQuerySegmentInfoResponse(BaseModel):
infos: Optional[List[QuerySegmentInfo]] = None
status: Optional[common.Status] = None
class ReplicaInfo(BaseModel):
collectionID: Optional[int] = None
node_ids: Optional[List[int]] = Field(None, description='include leaders')
partition_ids: Optional[List[int]] = Field(
None, description='empty indicates to load collection'
)
replicaID: Optional[int] = None
shard_replicas: Optional[List[ShardReplica]] = None
class GetReplicasResponse(BaseModel):
replicas: Optional[List[ReplicaInfo]] = None
status: Optional[common.Status] = None

View File

@ -0,0 +1,12 @@
[pytest]
addopts = --host 10.101.178.131 --html=/tmp/ci_logs/report.html --self-contained-html -v
# python3 -W ignore -m pytest
log_format = [%(asctime)s - %(levelname)s - %(name)s]: %(message)s (%(filename)s:%(lineno)s)
log_date_format = %Y-%m-%d %H:%M:%S
filterwarnings =
ignore::DeprecationWarning

View File

@ -0,0 +1,2 @@
decorest~=0.1.0
pydantic~=1.10.2

View File

@ -0,0 +1,49 @@
from time import sleep
from common import common_type as ct
from common import common_func as cf
from base.client_base import TestBase
from utils.util_log import test_log as log
class TestDefault(TestBase):
def test_e2e(self):
collection_name, schema = self.init_collection()
nb = ct.default_nb
# insert
res = self.entity_service.insert(collection_name=collection_name, fields_data=cf.gen_fields_data(schema, nb=nb),
num_rows=nb)
log.info(f"insert {nb} rows into collection {collection_name}, response: {res}")
# flush
res = self.entity_service.flush(collection_names=[collection_name])
log.info(f"flush collection {collection_name}, response: {res}")
# create index for vector field
vector_field_name = cf.get_vector_field(schema)
vector_index_params = cf.gen_index_params(index_type="HNSW")
res = self.index_service.create_index(collection_name=collection_name, field_name=vector_field_name,
extra_params=vector_index_params)
log.info(f"create index for vector field {vector_field_name}, response: {res}")
# load
res = self.collection_service.load_collection(collection_name=collection_name)
log.info(f"load collection {collection_name}, response: {res}")
sleep(5)
# search
vectors = cf.gen_vectors(nq=ct.default_nq, schema=schema)
res = self.entity_service.search(collection_name=collection_name, vectors=vectors,
output_fields=[ct.default_int64_field_name],
search_params=cf.gen_search_params())
log.info(f"search collection {collection_name}, response: {res}")
# hybrid search
res = self.entity_service.search(collection_name=collection_name, vectors=vectors,
output_fields=[ct.default_int64_field_name],
search_params=cf.gen_search_params(),
dsl=ct.default_dsl)
log.info(f"hybrid search collection {collection_name}, response: {res}")
# query
res = self.entity_service.query(collection_name=collection_name, expr=ct.default_expr)
log.info(f"query collection {collection_name}, response: {res}")

View File

@ -0,0 +1,57 @@
import logging
import sys
from config.log_config import log_config
class TestLog:
def __init__(self, logger, log_debug, log_file, log_err, log_worker):
self.logger = logger
self.log_debug = log_debug
self.log_file = log_file
self.log_err = log_err
self.log_worker = log_worker
self.log = logging.getLogger(self.logger)
self.log.setLevel(logging.DEBUG)
try:
formatter = logging.Formatter("[%(asctime)s - %(levelname)s - %(name)s]: "
"%(message)s (%(filename)s:%(lineno)s)")
# [%(process)s] process NO.
dh = logging.FileHandler(self.log_debug)
dh.setLevel(logging.DEBUG)
dh.setFormatter(formatter)
self.log.addHandler(dh)
fh = logging.FileHandler(self.log_file)
fh.setLevel(logging.INFO)
fh.setFormatter(formatter)
self.log.addHandler(fh)
eh = logging.FileHandler(self.log_err)
eh.setLevel(logging.ERROR)
eh.setFormatter(formatter)
self.log.addHandler(eh)
if self.log_worker != "":
wh = logging.FileHandler(self.log_worker)
wh.setLevel(logging.DEBUG)
wh.setFormatter(formatter)
self.log.addHandler(wh)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
# self.log.addHandler(ch)
except Exception as e:
print("Can not use %s or %s or %s to log. error : %s" % (log_debug, log_file, log_err, str(e)))
"""All modules share this unified log"""
log_debug = log_config.log_debug
log_info = log_config.log_info
log_err = log_config.log_err
log_worker = log_config.log_worker
test_log = TestLog('ci_test', log_debug, log_info, log_err, log_worker).log

View File

@ -0,0 +1,38 @@
import time
from datetime import datetime
import functools
from utils.util_log import test_log as log
DEFAULT_FMT = '[{start_time}] [{elapsed:0.8f}s] {collection_name} {func_name} -> {res!r}'
def trace(fmt=DEFAULT_FMT, prefix='test', flag=True):
def decorate(func):
@functools.wraps(func)
def inner_wrapper(*args, **kwargs):
# args[0] is an instance of ApiCollectionWrapper class
flag = args[0].active_trace
if flag:
start_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
t0 = time.perf_counter()
res, result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
end_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
func_name = func.__name__
collection_name = args[0].collection.name
# arg_lst = [repr(arg) for arg in args[1:]][:100]
# arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
# arg_str = ', '.join(arg_lst)[:200]
log_str = f"[{prefix}]" + fmt.format(**locals())
# TODO: add report function in this place, like uploading to influxdb
# it is better a async way to do this, in case of blocking the request processing
log.info(log_str)
return res, result
else:
res, result = func(*args, **kwargs)
return res, result
return inner_wrapper
return decorate