mirror of
https://gitee.com/milvus-io/milvus.git
synced 2025-12-08 01:58:34 +08:00
[test]Add restful api test (#21336)
Signed-off-by: zhuwenxing <wenxing.zhu@zilliz.com>
This commit is contained in:
parent
396a85c926
commit
19387754dc
17
tests/restful_client/api/alias.py
Normal file
17
tests/restful_client/api/alias.py
Normal 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
|
||||||
|
|
||||||
62
tests/restful_client/api/collection.py
Normal file
62
tests/restful_client/api/collection.py
Normal 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)
|
||||||
19
tests/restful_client/api/credential.py
Normal file
19
tests/restful_client/api/credential.py
Normal 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
|
||||||
63
tests/restful_client/api/entity.py
Normal file
63
tests/restful_client/api/entity.py
Normal 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"""
|
||||||
|
|
||||||
18
tests/restful_client/api/import.py
Normal file
18
tests/restful_client/api/import.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
38
tests/restful_client/api/index.py
Normal file
38
tests/restful_client/api/index.py
Normal 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"""
|
||||||
11
tests/restful_client/api/metrics.py
Normal file
11
tests/restful_client/api/metrics.py
Normal 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
|
||||||
|
|
||||||
21
tests/restful_client/api/ops.py
Normal file
21
tests/restful_client/api/ops.py
Normal 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
|
||||||
27
tests/restful_client/api/partition.py
Normal file
27
tests/restful_client/api/partition.py
Normal 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
|
||||||
43
tests/restful_client/api/pydantic_demo.py
Normal file
43
tests/restful_client/api/pydantic_demo.py
Normal 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)
|
||||||
|
|
||||||
0
tests/restful_client/base/alias_service.py
Normal file
0
tests/restful_client/base/alias_service.py
Normal file
72
tests/restful_client/base/client_base.py
Normal file
72
tests/restful_client/base/client_base.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
96
tests/restful_client/base/collection_service.py
Normal file
96
tests/restful_client/base/collection_service.py
Normal 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)
|
||||||
0
tests/restful_client/base/credential_service.py
Normal file
0
tests/restful_client/base/credential_service.py
Normal file
182
tests/restful_client/base/entity_service.py
Normal file
182
tests/restful_client/base/entity_service.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
tests/restful_client/base/import_service.py
Normal file
0
tests/restful_client/base/import_service.py
Normal file
57
tests/restful_client/base/index_service.py
Normal file
57
tests/restful_client/base/index_service.py
Normal 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
tests/restful_client/base/metrics_service.py
Normal file
0
tests/restful_client/base/metrics_service.py
Normal file
0
tests/restful_client/base/ops_service.py
Normal file
0
tests/restful_client/base/ops_service.py
Normal file
0
tests/restful_client/base/partition_service.py
Normal file
0
tests/restful_client/base/partition_service.py
Normal file
27
tests/restful_client/check/func_check.py
Normal file
27
tests/restful_client/check/func_check.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
21
tests/restful_client/check/param_check.py
Normal file
21
tests/restful_client/check/param_check.py
Normal 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
|
||||||
292
tests/restful_client/common/common_func.py
Normal file
292
tests/restful_client/common/common_func.py
Normal 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)
|
||||||
80
tests/restful_client/common/common_type.py
Normal file
80
tests/restful_client/common/common_type.py
Normal 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"}
|
||||||
|
}
|
||||||
44
tests/restful_client/config/log_config.py
Normal file
44
tests/restful_client/config/log_config.py
Normal 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()
|
||||||
49
tests/restful_client/conftest.py
Normal file
49
tests/restful_client/conftest.py
Normal 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)
|
||||||
3
tests/restful_client/models/__init__.py
Normal file
3
tests/restful_client/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: openapi.json
|
||||||
|
# timestamp: 2022-12-08T02:46:08+00:00
|
||||||
28
tests/restful_client/models/common.py
Normal file
28
tests/restful_client/models/common.py
Normal 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
|
||||||
138
tests/restful_client/models/milvus.py
Normal file
138
tests/restful_client/models/milvus.py
Normal 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,
|
||||||
|
)
|
||||||
72
tests/restful_client/models/schema.py
Normal file
72
tests/restful_client/models/schema.py
Normal 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')
|
||||||
587
tests/restful_client/models/server.py
Normal file
587
tests/restful_client/models/server.py
Normal 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
|
||||||
12
tests/restful_client/pytest.ini
Normal file
12
tests/restful_client/pytest.ini
Normal 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
|
||||||
2
tests/restful_client/requirements.txt
Normal file
2
tests/restful_client/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
decorest~=0.1.0
|
||||||
|
pydantic~=1.10.2
|
||||||
49
tests/restful_client/testcases/test_e2e.py
Normal file
49
tests/restful_client/testcases/test_e2e.py
Normal 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}")
|
||||||
|
|
||||||
|
|
||||||
57
tests/restful_client/utils/util_log.py
Normal file
57
tests/restful_client/utils/util_log.py
Normal 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
|
||||||
38
tests/restful_client/utils/util_wrapper.py
Normal file
38
tests/restful_client/utils/util_wrapper.py
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user