chore: import upstream snapshot with attribution
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"github.com/milvus-io/milvus/internal/proxy"
|
||||
"github.com/milvus-io/milvus/pkg/v3/common"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/metric"
|
||||
)
|
||||
|
||||
// v2
|
||||
const (
|
||||
// --- category ---
|
||||
DataBaseCategory = "/databases/"
|
||||
CollectionCategory = "/collections/"
|
||||
EntityCategory = "/entities/"
|
||||
PartitionCategory = "/partitions/"
|
||||
UserCategory = "/users/"
|
||||
RoleCategory = "/roles/"
|
||||
IndexCategory = "/indexes/"
|
||||
AliasCategory = "/aliases/"
|
||||
ImportJobCategory = "/jobs/import/"
|
||||
SnapshotJobCategory = "/jobs/snapshot/"
|
||||
ExternalCollectionJobCategory = "/jobs/external_collection/"
|
||||
PrivilegeGroupCategory = "/privilege_groups/"
|
||||
CollectionFieldCategory = "/collections/fields/"
|
||||
CollectionStructFieldCategory = "/collections/struct_fields/"
|
||||
ResourceGroupCategory = "/resource_groups/"
|
||||
SegmentCategory = "/segments/"
|
||||
QuotaCenterCategory = "/quotacenter/"
|
||||
CommonCategory = "/common/"
|
||||
|
||||
ListAction = "list"
|
||||
HasAction = "has"
|
||||
DescribeAction = "describe"
|
||||
CreateAction = "create"
|
||||
DropAction = "drop"
|
||||
StatsAction = "get_stats"
|
||||
LoadStateAction = "get_load_state"
|
||||
RenameAction = "rename"
|
||||
LoadAction = "load"
|
||||
RefreshLoadAction = "refresh_load"
|
||||
ReleaseAction = "release"
|
||||
QueryAction = "query"
|
||||
GetAction = "get"
|
||||
DeleteAction = "delete"
|
||||
InsertAction = "insert"
|
||||
UpsertAction = "upsert"
|
||||
SearchAction = "search"
|
||||
AdvancedSearchAction = "advanced_search"
|
||||
HybridSearchAction = "hybrid_search"
|
||||
|
||||
UpdatePasswordAction = "update_password"
|
||||
GrantRoleAction = "grant_role"
|
||||
RevokeRoleAction = "revoke_role"
|
||||
GrantPrivilegeAction = "grant_privilege"
|
||||
RevokePrivilegeAction = "revoke_privilege"
|
||||
GrantPrivilegeActionV2 = "grant_privilege_v2"
|
||||
RevokePrivilegeActionV2 = "revoke_privilege_v2"
|
||||
AlterAction = "alter"
|
||||
AlterPropertiesAction = "alter_properties"
|
||||
AddFunctionAction = "add_function"
|
||||
AlterFunctionAction = "alter_function"
|
||||
DropFunctionAction = "drop_function"
|
||||
AddAction = `add`
|
||||
DropPropertiesAction = "drop_properties"
|
||||
CompactAction = "compact"
|
||||
CompactionStateAction = "get_compaction_state"
|
||||
FlushAction = "flush"
|
||||
TruncateAction = "truncate"
|
||||
GetProgressAction = "get_progress" // deprecated, keep it for compatibility, use `/v2/vectordb/jobs/import/describe` instead
|
||||
RestoreExternalAction = "restore_external"
|
||||
ExportAction = "export"
|
||||
RefreshAction = "refresh"
|
||||
AddPrivilegesToGroupAction = "add_privileges_to_group"
|
||||
RemovePrivilegesFromGroupAction = "remove_privileges_from_group"
|
||||
TransferReplicaAction = "transfer_replica"
|
||||
|
||||
RunAnalyzerAction = "run_analyzer"
|
||||
|
||||
CommitAction = "commit"
|
||||
AbortAction = "abort"
|
||||
)
|
||||
|
||||
const (
|
||||
ContextRequest = "request"
|
||||
ContextResponse = "response"
|
||||
ContextUsername = "username"
|
||||
ContextToken = "token"
|
||||
VectorCollectionsPath = "/vector/collections"
|
||||
VectorCollectionsCreatePath = "/vector/collections/create"
|
||||
VectorCollectionsDescribePath = "/vector/collections/describe"
|
||||
VectorCollectionsDropPath = "/vector/collections/drop"
|
||||
VectorInsertPath = "/vector/insert"
|
||||
VectorUpsertPath = "/vector/upsert"
|
||||
VectorSearchPath = "/vector/search"
|
||||
VectorGetPath = "/vector/get"
|
||||
VectorQueryPath = "/vector/query"
|
||||
VectorDeletePath = "/vector/delete"
|
||||
|
||||
ShardNumDefault = 1
|
||||
|
||||
EnableDynamic = true
|
||||
EnableAutoID = true
|
||||
DisableAutoID = false
|
||||
|
||||
HTTPCollectionName = "collectionName"
|
||||
HTTPCollectionID = "collectionID"
|
||||
HTTPDbName = "dbName"
|
||||
HTTPDbID = "dbID"
|
||||
HTTPProperties = "properties"
|
||||
HTTPPartitionName = "partitionName"
|
||||
HTTPPartitionNames = "partitionNames"
|
||||
HTTPUserName = "userName"
|
||||
HTTPRoleName = "roleName"
|
||||
HTTPIndexName = "indexName"
|
||||
HTTPIndexField = "fieldName"
|
||||
HTTPAliasName = "aliasName"
|
||||
HTTPRequestData = "data"
|
||||
HTTPRequestDefaultValue = "defaultValue"
|
||||
DefaultDbName = "default"
|
||||
DefaultIndexName = "vector_idx"
|
||||
DefaultAliasName = "the_alias"
|
||||
DefaultOutputFields = "*"
|
||||
HTTPHeaderAllowInt64 = "Accept-Type-Allow-Int64"
|
||||
HTTPHeaderDBName = "DB-Name"
|
||||
HTTPHeaderRequestTimeout = "Request-Timeout"
|
||||
HTTPHeaderMilvusTraceID = "X-Milvus-Trace-Id"
|
||||
HTTPReturnCode = "code"
|
||||
HTTPReturnMessage = "message"
|
||||
HTTPReturnData = "data"
|
||||
HTTPReturnCost = "cost"
|
||||
HTTPReturnRecalls = "recalls"
|
||||
HTTPReturnLoadState = "loadState"
|
||||
HTTPReturnLoadProgress = "loadProgress"
|
||||
HTTPReturnTopks = "topks"
|
||||
HTTPReturnAggTopks = "aggTopks"
|
||||
|
||||
HTTPReturnHas = "has"
|
||||
|
||||
HTTPReturnScannedRemoteBytes = "scanned_remote_bytes"
|
||||
HTTPReturnScannedTotalBytes = "scanned_total_bytes"
|
||||
HTTPReturnCacheHitRatio = "cache_hit_ratio"
|
||||
|
||||
HTTPReturnFieldName = "name"
|
||||
HTTPReturnFieldID = "id"
|
||||
HTTPReturnFieldType = "type"
|
||||
HTTPReturnFieldPrimaryKey = "primaryKey"
|
||||
HTTPReturnFieldPartitionKey = "partitionKey"
|
||||
HTTPReturnFieldClusteringKey = "clusteringKey"
|
||||
HTTPReturnFieldNullable = "nullable"
|
||||
HTTPReturnFieldDefaultValue = "defaultValue"
|
||||
HTTPReturnFieldAutoID = "autoId"
|
||||
HTTPReturnFieldElementType = "elementType"
|
||||
HTTPReturnDescription = "description"
|
||||
HTTPReturnFieldIsFunctionOutput = "isFunctionOutput"
|
||||
|
||||
HTTPReturnFunctionName = "name"
|
||||
HTTPReturnFunctionID = "id"
|
||||
HTTPReturnFunctionType = "type"
|
||||
HTTPReturnFunctionInputFieldNames = "inputFieldNames"
|
||||
HTTPReturnFunctionOutputFieldNames = "outputFieldNames"
|
||||
HTTPReturnFunctionParams = "params"
|
||||
|
||||
HTTPReturnIndexMetricType = "metricType"
|
||||
HTTPReturnIndexType = "indexType"
|
||||
HTTPIndexOffsetCacheEnabledKey = "indexoffsetcache.enabled"
|
||||
HTTPMmapEnabledKey = "mmap.enabled"
|
||||
HTTPReturnIndexTotalRows = "totalRows"
|
||||
HTTPReturnIndexPendingRows = "pendingRows"
|
||||
HTTPReturnIndexIndexedRows = "indexedRows"
|
||||
HTTPReturnIndexState = "indexState"
|
||||
HTTPReturnIndexFailReason = "failReason"
|
||||
|
||||
HTTPReturnMinIndexVersion = "minIndexVersion"
|
||||
HTTPReturnMaxIndexVersion = "maxIndexVersion"
|
||||
HTTPReturnIndexParams = "indexParams"
|
||||
|
||||
HTTPReturnDistance = "distance"
|
||||
|
||||
HTTPReturnRowCount = "rowCount"
|
||||
|
||||
HTTPReturnObjectType = "objectType"
|
||||
HTTPReturnObjectName = "objectName"
|
||||
HTTPReturnPrivilege = "privilege"
|
||||
HTTPReturnGrantor = "grantor"
|
||||
HTTPReturnDbName = "dbName"
|
||||
HTTPReturnPrivilegeGroupName = "privilegeGroupName"
|
||||
HTTPReturnPrivileges = "privileges"
|
||||
HTTPReturnPrivilegeGroups = "privilegeGroups"
|
||||
|
||||
DefaultMetricType = metric.COSINE
|
||||
DefaultPrimaryFieldName = "id"
|
||||
DefaultVectorFieldName = "vector"
|
||||
|
||||
HTTPWarmupKey = common.WarmupKey
|
||||
|
||||
Dim = "dim"
|
||||
)
|
||||
|
||||
const (
|
||||
Params = proxy.ParamsKey
|
||||
ParamRoundDecimal = proxy.RoundDecimalKey
|
||||
ParamRadius = "radius"
|
||||
ParamRangeFilter = "range_filter"
|
||||
ParamGroupByField = "group_by_field"
|
||||
ParamGroupSize = "group_size"
|
||||
ParamStrictGroupSize = "strict_group_size"
|
||||
BoundedTimestamp = 2
|
||||
)
|
||||
@@ -0,0 +1,566 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
|
||||
"github.com/milvus-io/milvus/internal/types"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
)
|
||||
|
||||
// Handlers handles http requests
|
||||
type Handlers struct {
|
||||
proxy types.ProxyComponent
|
||||
}
|
||||
|
||||
// NewHandlers creates a new Handlers
|
||||
func NewHandlers(proxy types.ProxyComponent) *Handlers {
|
||||
return &Handlers{
|
||||
proxy: proxy,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRouters registers routes to given router
|
||||
func (h *Handlers) RegisterRoutesTo(router gin.IRouter) {
|
||||
router.GET("/health", wrapHandler(h.handleGetHealth))
|
||||
router.POST("/dummy", wrapHandler(h.handleDummy))
|
||||
|
||||
router.POST("/collection", wrapHandler(h.handleCreateCollection))
|
||||
router.DELETE("/collection", wrapHandler(h.handleDropCollection))
|
||||
router.GET("/collection/existence", wrapHandler(h.handleHasCollection))
|
||||
router.GET("/collection", wrapHandler(h.handleDescribeCollection))
|
||||
router.POST("/collection/load", wrapHandler(h.handleLoadCollection))
|
||||
router.DELETE("/collection/load", wrapHandler(h.handleReleaseCollection))
|
||||
router.GET("/collection/statistics", wrapHandler(h.handleGetCollectionStatistics))
|
||||
router.GET("/collections", wrapHandler(h.handleShowCollections))
|
||||
|
||||
router.POST("/partition", wrapHandler(h.handleCreatePartition))
|
||||
router.DELETE("/partition", wrapHandler(h.handleDropPartition))
|
||||
router.GET("/partition/existence", wrapHandler(h.handleHasPartition))
|
||||
router.POST("/partitions/load", wrapHandler(h.handleLoadPartitions))
|
||||
router.DELETE("/partitions/load", wrapHandler(h.handleReleasePartitions))
|
||||
router.GET("/partition/statistics", wrapHandler(h.handleGetPartitionStatistics))
|
||||
router.GET("/partitions", wrapHandler(h.handleShowPartitions))
|
||||
|
||||
router.POST("/alias", wrapHandler(h.handleCreateAlias))
|
||||
router.DELETE("/alias", wrapHandler(h.handleDropAlias))
|
||||
router.PATCH("/alias", wrapHandler(h.handleAlterAlias))
|
||||
|
||||
router.POST("/index", wrapHandler(h.handleCreateIndex))
|
||||
router.GET("/index", wrapHandler(h.handleDescribeIndex))
|
||||
router.GET("/index/state", wrapHandler(h.handleGetIndexState))
|
||||
router.GET("/index/progress", wrapHandler(h.handleGetIndexBuildProgress))
|
||||
router.DELETE("/index", wrapHandler(h.handleDropIndex))
|
||||
|
||||
router.POST("/entities", wrapHandler(h.handleInsert))
|
||||
router.DELETE("/entities", wrapHandler(h.handleDelete))
|
||||
router.POST("/search", wrapHandler(h.handleSearch))
|
||||
router.POST("/query", wrapHandler(h.handleQuery))
|
||||
|
||||
router.POST("/persist", wrapHandler(h.handleFlush))
|
||||
router.GET("/distance", wrapHandler(h.handleCalcDistance))
|
||||
router.GET("/persist/state", wrapHandler(h.handleGetFlushState))
|
||||
router.GET("/persist/segment-info", wrapHandler(h.handleGetPersistentSegmentInfo))
|
||||
router.GET("/query-segment-info", wrapHandler(h.handleGetQuerySegmentInfo))
|
||||
router.GET("/replicas", wrapHandler(h.handleGetReplicas))
|
||||
|
||||
router.GET("/metrics", wrapHandler(h.handleGetMetrics))
|
||||
router.POST("/load-balance", wrapHandler(h.handleLoadBalance))
|
||||
router.GET("/compaction/state", wrapHandler(h.handleGetCompactionState))
|
||||
router.GET("/compaction/plans", wrapHandler(h.handleGetCompactionStateWithPlans))
|
||||
router.POST("/compaction", wrapHandler(h.handleManualCompaction))
|
||||
|
||||
router.POST("/import", wrapHandler(h.handleImport))
|
||||
router.GET("/import/state", wrapHandler(h.handleGetImportState))
|
||||
router.GET("/import/tasks", wrapHandler(h.handleListImportTasks))
|
||||
|
||||
router.POST("/credential", wrapHandler(h.handleCreateCredential))
|
||||
router.PATCH("/credential", wrapHandler(h.handleUpdateCredential))
|
||||
router.DELETE("/credential", wrapHandler(h.handleDeleteCredential))
|
||||
router.GET("/credential/users", wrapHandler(h.handleListCredUsers))
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetHealth(c *gin.Context) (interface{}, error) {
|
||||
return gin.H{"status": "ok"}, nil
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDummy(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DummyRequest{}
|
||||
// use ShouldBind to supports binding JSON, XML, YAML, and protobuf.
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.Dummy(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleCreateCollection(c *gin.Context) (interface{}, error) {
|
||||
wrappedReq := WrappedCreateCollectionRequest{}
|
||||
err := shouldBind(c, &wrappedReq)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
schemaProto, err := proto.Marshal(&wrappedReq.Schema)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "marshal schema failed")
|
||||
}
|
||||
req := &milvuspb.CreateCollectionRequest{
|
||||
Base: wrappedReq.Base,
|
||||
DbName: wrappedReq.DbName,
|
||||
CollectionName: wrappedReq.CollectionName,
|
||||
Schema: schemaProto,
|
||||
ShardsNum: wrappedReq.ShardsNum,
|
||||
ConsistencyLevel: wrappedReq.ConsistencyLevel,
|
||||
Properties: wrappedReq.Properties,
|
||||
}
|
||||
return h.proxy.CreateCollection(c, req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDropCollection(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DropCollectionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.DropCollection(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleHasCollection(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.HasCollectionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.HasCollection(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDescribeCollection(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DescribeCollectionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.DescribeCollection(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleLoadCollection(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.LoadCollectionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.LoadCollection(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleReleaseCollection(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ReleaseCollectionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.ReleaseCollection(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetCollectionStatistics(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetCollectionStatisticsRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetCollectionStatistics(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleShowCollections(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ShowCollectionsRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.ShowCollections(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleCreatePartition(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.CreatePartitionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.CreatePartition(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDropPartition(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DropPartitionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.DropPartition(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleHasPartition(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.HasPartitionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.HasPartition(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleLoadPartitions(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.LoadPartitionsRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.LoadPartitions(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleReleasePartitions(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ReleasePartitionsRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.ReleasePartitions(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetPartitionStatistics(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetPartitionStatisticsRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetPartitionStatistics(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleShowPartitions(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ShowPartitionsRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.ShowPartitions(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleCreateAlias(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.CreateAliasRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.CreateAlias(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDropAlias(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DropAliasRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.DropAlias(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleAlterAlias(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.AlterAliasRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.AlterAlias(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleCreateIndex(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.CreateIndexRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.CreateIndex(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDescribeIndex(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DescribeIndexRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.DescribeIndex(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetIndexState(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetIndexStateRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetIndexState(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetIndexBuildProgress(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetIndexBuildProgressRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetIndexBuildProgress(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDropIndex(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DropIndexRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.DropIndex(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleInsert(c *gin.Context) (interface{}, error) {
|
||||
wrappedReq := WrappedInsertRequest{}
|
||||
err := shouldBind(c, &wrappedReq)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
req, err := wrappedReq.AsInsertRequest()
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "convert body to pb failed")
|
||||
}
|
||||
return h.proxy.Insert(c, req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDelete(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DeleteRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.Delete(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleSearch(c *gin.Context) (interface{}, error) {
|
||||
wrappedReq := SearchRequest{}
|
||||
err := shouldBind(c, &wrappedReq)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
if wrappedReq.HasSearchAggregation() {
|
||||
return nil, badRequestf(merr.WrapErrParameterInvalidMsg("searchAggregation is not supported for low-level REST search"), "invalid request")
|
||||
}
|
||||
req := milvuspb.SearchRequest{
|
||||
Base: wrappedReq.Base,
|
||||
DbName: wrappedReq.DbName,
|
||||
CollectionName: wrappedReq.CollectionName,
|
||||
PartitionNames: wrappedReq.PartitionNames,
|
||||
Dsl: wrappedReq.Dsl,
|
||||
DslType: wrappedReq.DslType,
|
||||
OutputFields: wrappedReq.OutputFields,
|
||||
SearchParams: wrappedReq.SearchParams,
|
||||
TravelTimestamp: wrappedReq.TravelTimestamp,
|
||||
GuaranteeTimestamp: wrappedReq.GuaranteeTimestamp,
|
||||
Nq: wrappedReq.Nq,
|
||||
}
|
||||
if len(wrappedReq.BinaryVectors) > 0 {
|
||||
req.SearchInput = &milvuspb.SearchRequest_PlaceholderGroup{
|
||||
PlaceholderGroup: binaryVector2Bytes(wrappedReq.BinaryVectors),
|
||||
}
|
||||
} else {
|
||||
req.SearchInput = &milvuspb.SearchRequest_PlaceholderGroup{
|
||||
PlaceholderGroup: vector2Bytes(wrappedReq.Vectors),
|
||||
}
|
||||
}
|
||||
return h.proxy.Search(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleQuery(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.QueryRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.Query(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleFlush(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.FlushRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.Flush(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleCalcDistance(c *gin.Context) (interface{}, error) {
|
||||
wrappedReq := WrappedCalcDistanceRequest{}
|
||||
err := shouldBind(c, &wrappedReq)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
|
||||
req := milvuspb.CalcDistanceRequest{
|
||||
Base: wrappedReq.Base,
|
||||
Params: wrappedReq.Params,
|
||||
OpLeft: wrappedReq.OpLeft.AsPbVectorArray(),
|
||||
OpRight: wrappedReq.OpRight.AsPbVectorArray(),
|
||||
}
|
||||
return h.proxy.CalcDistance(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetFlushState(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetFlushStateRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetFlushState(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetPersistentSegmentInfo(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetPersistentSegmentInfoRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetPersistentSegmentInfo(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetQuerySegmentInfo(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetQuerySegmentInfoRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetQuerySegmentInfo(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetReplicas(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetReplicasRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetReplicas(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetMetrics(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetMetricsRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetMetrics(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleLoadBalance(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.LoadBalanceRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.LoadBalance(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetCompactionState(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetCompactionStateRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetCompactionState(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetCompactionStateWithPlans(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetCompactionPlansRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetCompactionStateWithPlans(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleManualCompaction(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ManualCompactionRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.ManualCompaction(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleImport(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ImportRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.Import(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleGetImportState(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.GetImportStateRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.GetImportState(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleListImportTasks(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ListImportTasksRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.ListImportTasks(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleCreateCredential(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.CreateCredentialRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.CreateCredential(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleUpdateCredential(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.UpdateCredentialRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.UpdateCredential(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleDeleteCredential(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.DeleteCredentialRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.DeleteCredential(c, &req)
|
||||
}
|
||||
|
||||
func (h *Handlers) handleListCredUsers(c *gin.Context) (interface{}, error) {
|
||||
req := milvuspb.ListCredUsersRequest{}
|
||||
err := shouldBind(c, &req)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "parse body failed")
|
||||
}
|
||||
return h.proxy.ListCredUsers(c, &req)
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
||||
"github.com/milvus-io/milvus/internal/json"
|
||||
"github.com/milvus-io/milvus/internal/types"
|
||||
)
|
||||
|
||||
func Test_WrappedInsertRequest_JSONMarshal_AsInsertRequest(t *testing.T) {
|
||||
// https://github.com/milvus-io/milvus/issues/20415
|
||||
insertRaw := []byte(`{
|
||||
"collection_name": "seller_tag",
|
||||
"fields_data": [{ "field_name":"kid","type":5,"field":[9999999999999999] },{ "field_name":"seller_id","type":5,"field":[5625300123280813090] },{ "field_name":"vector","type":101,"field":[[0.08090433, 0.19154754, 0.16858263, 0.027101958, 0.07229418, 0.15223257, -0.024227709, 0.13302892, 0.05951315, 0.03572949, -0.015721956, -0.21992287, 0.08134472, 0.18640009, -0.09814235, -0.11117617, 0.10464557, -0.092037976, -0.19489805, -0.069008306, -0.039415136, -0.17841195, 0.076126315, 0.031378396, 0.22680397, 0.045089707, 0.12307317, 0.06711619, 0.15067382, -0.213569, 0.066602595, -0.021743167, -0.2727193, -0.112709574, 0.09504322, 0.02386695, 0.04574049, -0.055642836, -0.16812482, -0.051256, -0.11399734, 0.29519975, 0.109542266, 0.18452083, 0.05543076, -0.064969495, -0.14457555, -0.034600936, 0.045484997, -0.15677887, -0.12983392, 0.20921704, -0.049788076, 0.050687622, -0.23369887, -0.022488454, 0.06089106, 0.14699098, -0.08140416, -0.008949298, -0.14867777, 0.07415456, -0.0027948048, 0.0060837376]] }],
|
||||
"num_rows": 1
|
||||
}`)
|
||||
wrappedInsert := new(WrappedInsertRequest)
|
||||
err := json.Unmarshal(insertRaw, &wrappedInsert)
|
||||
assert.NoError(t, err)
|
||||
insertReq, err := wrappedInsert.AsInsertRequest()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(9999999999999999), insertReq.FieldsData[0].GetScalars().GetLongData().Data[0])
|
||||
}
|
||||
|
||||
type mockProxyComponent struct {
|
||||
// wrap the interface to avoid implement not used func.
|
||||
// and to let not implemented call panics
|
||||
// implement the method you want to mock
|
||||
types.ProxyComponent
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) Dummy(ctx context.Context, request *milvuspb.DummyRequest) (*milvuspb.DummyResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
emptyBody = &gin.H{}
|
||||
testStatus = &commonpb.Status{Reason: "ok"}
|
||||
)
|
||||
|
||||
func (m *mockProxyComponent) CreateDatabase(ctx context.Context, in *milvuspb.CreateDatabaseRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DropDatabase(ctx context.Context, in *milvuspb.DropDatabaseRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ListDatabases(ctx context.Context, in *milvuspb.ListDatabasesRequest) (*milvuspb.ListDatabasesResponse, error) {
|
||||
return &milvuspb.ListDatabasesResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) CreateCollection(ctx context.Context, request *milvuspb.CreateCollectionRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DropCollection(ctx context.Context, request *milvuspb.DropCollectionRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) HasCollection(ctx context.Context, request *milvuspb.HasCollectionRequest) (*milvuspb.BoolResponse, error) {
|
||||
return &milvuspb.BoolResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) LoadCollection(ctx context.Context, request *milvuspb.LoadCollectionRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ReleaseCollection(ctx context.Context, request *milvuspb.ReleaseCollectionRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DescribeCollection(ctx context.Context, request *milvuspb.DescribeCollectionRequest) (*milvuspb.DescribeCollectionResponse, error) {
|
||||
return &milvuspb.DescribeCollectionResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetCollectionStatistics(ctx context.Context, request *milvuspb.GetCollectionStatisticsRequest) (*milvuspb.GetCollectionStatisticsResponse, error) {
|
||||
return &milvuspb.GetCollectionStatisticsResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ShowCollections(ctx context.Context, request *milvuspb.ShowCollectionsRequest) (*milvuspb.ShowCollectionsResponse, error) {
|
||||
return &milvuspb.ShowCollectionsResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) AlterCollection(ctx context.Context, request *milvuspb.AlterCollectionRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) CreatePartition(ctx context.Context, request *milvuspb.CreatePartitionRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DropPartition(ctx context.Context, request *milvuspb.DropPartitionRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) HasPartition(ctx context.Context, request *milvuspb.HasPartitionRequest) (*milvuspb.BoolResponse, error) {
|
||||
return &milvuspb.BoolResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) LoadPartitions(ctx context.Context, request *milvuspb.LoadPartitionsRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ReleasePartitions(ctx context.Context, request *milvuspb.ReleasePartitionsRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetPartitionStatistics(ctx context.Context, request *milvuspb.GetPartitionStatisticsRequest) (*milvuspb.GetPartitionStatisticsResponse, error) {
|
||||
return &milvuspb.GetPartitionStatisticsResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ShowPartitions(ctx context.Context, request *milvuspb.ShowPartitionsRequest) (*milvuspb.ShowPartitionsResponse, error) {
|
||||
return &milvuspb.ShowPartitionsResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) CreateAlias(ctx context.Context, request *milvuspb.CreateAliasRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DropAlias(ctx context.Context, request *milvuspb.DropAliasRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) AlterAlias(ctx context.Context, request *milvuspb.AlterAliasRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DescribeAlias(ctx context.Context, request *milvuspb.DescribeAliasRequest) (*milvuspb.DescribeAliasResponse, error) {
|
||||
return &milvuspb.DescribeAliasResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ListAliases(ctx context.Context, request *milvuspb.ListAliasesRequest) (*milvuspb.ListAliasesResponse, error) {
|
||||
return &milvuspb.ListAliasesResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) CreateIndex(ctx context.Context, request *milvuspb.CreateIndexRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DescribeIndex(ctx context.Context, request *milvuspb.DescribeIndexRequest) (*milvuspb.DescribeIndexResponse, error) {
|
||||
return &milvuspb.DescribeIndexResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetIndexStatistics(ctx context.Context, request *milvuspb.GetIndexStatisticsRequest) (*milvuspb.GetIndexStatisticsResponse, error) {
|
||||
return &milvuspb.GetIndexStatisticsResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetIndexState(ctx context.Context, request *milvuspb.GetIndexStateRequest) (*milvuspb.GetIndexStateResponse, error) {
|
||||
return &milvuspb.GetIndexStateResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetIndexBuildProgress(ctx context.Context, request *milvuspb.GetIndexBuildProgressRequest) (*milvuspb.GetIndexBuildProgressResponse, error) {
|
||||
return &milvuspb.GetIndexBuildProgressResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DropIndex(ctx context.Context, request *milvuspb.DropIndexRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) Insert(ctx context.Context, request *milvuspb.InsertRequest) (*milvuspb.MutationResult, error) {
|
||||
if request.CollectionName == "" {
|
||||
return nil, errors.New("body parse err")
|
||||
}
|
||||
return &milvuspb.MutationResult{Acknowledged: true}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) Delete(ctx context.Context, request *milvuspb.DeleteRequest) (*milvuspb.MutationResult, error) {
|
||||
if request.Expr == "" {
|
||||
return nil, errors.New("body parse err")
|
||||
}
|
||||
return &milvuspb.MutationResult{Acknowledged: true}, nil
|
||||
}
|
||||
|
||||
var searchResult = milvuspb.SearchResults{
|
||||
Results: &schemapb.SearchResultData{
|
||||
TopK: 10,
|
||||
},
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) Search(ctx context.Context, request *milvuspb.SearchRequest) (*milvuspb.SearchResults, error) {
|
||||
if request.Dsl == "" {
|
||||
return nil, errors.New("body parse err")
|
||||
}
|
||||
return &searchResult, nil
|
||||
}
|
||||
|
||||
var queryResult = milvuspb.QueryResults{
|
||||
CollectionName: "test",
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) Query(ctx context.Context, request *milvuspb.QueryRequest) (*milvuspb.QueryResults, error) {
|
||||
if request.Expr == "" {
|
||||
return nil, errors.New("body parse err")
|
||||
}
|
||||
return &queryResult, nil
|
||||
}
|
||||
|
||||
var flushResult = &milvuspb.FlushResponse{
|
||||
DbName: "default",
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) Flush(ctx context.Context, request *milvuspb.FlushRequest) (*milvuspb.FlushResponse, error) {
|
||||
if len(request.CollectionNames) < 1 {
|
||||
return nil, errors.New("body parse err")
|
||||
}
|
||||
return flushResult, nil
|
||||
}
|
||||
|
||||
var calcDistanceResult = &milvuspb.CalcDistanceResults{
|
||||
Array: &milvuspb.CalcDistanceResults_IntDist{
|
||||
IntDist: &schemapb.IntArray{
|
||||
Data: []int32{1, 2, 3},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) CalcDistance(ctx context.Context, request *milvuspb.CalcDistanceRequest) (*milvuspb.CalcDistanceResults, error) {
|
||||
if len(request.Params) < 1 {
|
||||
return nil, errors.New("body parse err")
|
||||
}
|
||||
return calcDistanceResult, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetFlushState(ctx context.Context, request *milvuspb.GetFlushStateRequest) (*milvuspb.GetFlushStateResponse, error) {
|
||||
return &milvuspb.GetFlushStateResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetPersistentSegmentInfo(ctx context.Context, request *milvuspb.GetPersistentSegmentInfoRequest) (*milvuspb.GetPersistentSegmentInfoResponse, error) {
|
||||
return &milvuspb.GetPersistentSegmentInfoResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetQuerySegmentInfo(ctx context.Context, request *milvuspb.GetQuerySegmentInfoRequest) (*milvuspb.GetQuerySegmentInfoResponse, error) {
|
||||
return &milvuspb.GetQuerySegmentInfoResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetReplicas(ctx context.Context, request *milvuspb.GetReplicasRequest) (*milvuspb.GetReplicasResponse, error) {
|
||||
return &milvuspb.GetReplicasResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetMetrics(ctx context.Context, request *milvuspb.GetMetricsRequest) (*milvuspb.GetMetricsResponse, error) {
|
||||
return &milvuspb.GetMetricsResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) LoadBalance(ctx context.Context, request *milvuspb.LoadBalanceRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetCompactionState(ctx context.Context, request *milvuspb.GetCompactionStateRequest) (*milvuspb.GetCompactionStateResponse, error) {
|
||||
return &milvuspb.GetCompactionStateResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetCompactionStateWithPlans(ctx context.Context, request *milvuspb.GetCompactionPlansRequest) (*milvuspb.GetCompactionPlansResponse, error) {
|
||||
return &milvuspb.GetCompactionPlansResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ManualCompaction(ctx context.Context, request *milvuspb.ManualCompactionRequest) (*milvuspb.ManualCompactionResponse, error) {
|
||||
return &milvuspb.ManualCompactionResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) Import(ctx context.Context, request *milvuspb.ImportRequest) (*milvuspb.ImportResponse, error) {
|
||||
return &milvuspb.ImportResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) GetImportState(ctx context.Context, request *milvuspb.GetImportStateRequest) (*milvuspb.GetImportStateResponse, error) {
|
||||
return &milvuspb.GetImportStateResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ListImportTasks(ctx context.Context, request *milvuspb.ListImportTasksRequest) (*milvuspb.ListImportTasksResponse, error) {
|
||||
return &milvuspb.ListImportTasksResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) CreateCredential(ctx context.Context, request *milvuspb.CreateCredentialRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) UpdateCredential(ctx context.Context, request *milvuspb.UpdateCredentialRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) DeleteCredential(ctx context.Context, request *milvuspb.DeleteCredentialRequest) (*commonpb.Status, error) {
|
||||
return testStatus, nil
|
||||
}
|
||||
|
||||
func (m *mockProxyComponent) ListCredUsers(ctx context.Context, request *milvuspb.ListCredUsersRequest) (*milvuspb.ListCredUsersResponse, error) {
|
||||
return &milvuspb.ListCredUsersResponse{Status: testStatus}, nil
|
||||
}
|
||||
|
||||
func TestHandlers(t *testing.T) {
|
||||
mockProxy := &mockProxyComponent{}
|
||||
h := NewHandlers(mockProxy)
|
||||
testEngine := gin.New()
|
||||
h.RegisterRoutesTo(testEngine)
|
||||
|
||||
t.Run("handleGetHealth default json ok", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, w.Body.Bytes(), []byte(`{"status":"ok"}`))
|
||||
})
|
||||
t.Run("handleGetHealth accept yaml ok", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
req.Header = http.Header{
|
||||
"Accept": []string{binding.MIMEYAML},
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, w.Body.Bytes(), []byte("status: ok\n"))
|
||||
})
|
||||
t.Run("handlePostDummy parsejson failed 400", func(t *testing.T) {
|
||||
bodyBytes := []byte("---")
|
||||
req := httptest.NewRequest(http.MethodPost, "/dummy", bytes.NewReader(bodyBytes))
|
||||
req.Header = http.Header{
|
||||
"Content-Type": []string{binding.MIMEJSON},
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
})
|
||||
t.Run("handlePostDummy parseyaml failed 400", func(t *testing.T) {
|
||||
bodyBytes := []byte("{")
|
||||
req := httptest.NewRequest(http.MethodPost, "/dummy", bytes.NewReader(bodyBytes))
|
||||
req.Header = http.Header{
|
||||
"Content-Type": []string{binding.MIMEYAML},
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
})
|
||||
t.Run("handlePostDummy default json ok", func(t *testing.T) {
|
||||
bodyBytes := []byte("")
|
||||
req := httptest.NewRequest(http.MethodPost, "/dummy", bytes.NewReader(bodyBytes))
|
||||
req.Header = http.Header{
|
||||
"Content-Type": []string{binding.MIMEJSON},
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
t.Run("handlePostDummy yaml ok", func(t *testing.T) {
|
||||
bodyBytes := []byte("")
|
||||
req := httptest.NewRequest(http.MethodPost, "/dummy", bytes.NewReader(bodyBytes))
|
||||
req.Header = http.Header{
|
||||
"Content-Type": []string{binding.MIMEYAML},
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
|
||||
type testCase struct {
|
||||
httpMethod string
|
||||
path string
|
||||
body interface{}
|
||||
expectedStatus int
|
||||
expectedBody interface{}
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
http.MethodPost, "/collection", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/collection", emptyBody,
|
||||
http.StatusOK, &testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/collection/existence", emptyBody,
|
||||
http.StatusOK, &milvuspb.BoolResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/collection", emptyBody,
|
||||
http.StatusOK, &milvuspb.DescribeCollectionResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/collection/load", emptyBody,
|
||||
http.StatusOK, &testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/collection/load", emptyBody,
|
||||
http.StatusOK, &testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/collection/statistics", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetCollectionStatisticsResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/collections", emptyBody,
|
||||
http.StatusOK, &milvuspb.ShowCollectionsResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/partition", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/partition", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/partition/existence", emptyBody,
|
||||
http.StatusOK, &milvuspb.BoolResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/partitions/load", emptyBody,
|
||||
http.StatusOK, &testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/partitions/load", emptyBody,
|
||||
http.StatusOK, &testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/partition/statistics", emptyBody,
|
||||
http.StatusOK,
|
||||
milvuspb.GetPartitionStatisticsResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/partitions", emptyBody,
|
||||
http.StatusOK, &milvuspb.ShowPartitionsResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/alias", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/alias", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodPatch, "/alias", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/index", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/index", emptyBody,
|
||||
http.StatusOK, &milvuspb.DescribeIndexResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/index/state", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetIndexStateResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/index/progress", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetIndexBuildProgressResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/index", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/entities", &milvuspb.InsertRequest{CollectionName: "c1"},
|
||||
http.StatusOK, &milvuspb.MutationResult{Acknowledged: true},
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/entities",
|
||||
milvuspb.DeleteRequest{Expr: "some expr"},
|
||||
http.StatusOK, &milvuspb.MutationResult{Acknowledged: true},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/search",
|
||||
milvuspb.SearchRequest{Dsl: "some dsl"},
|
||||
http.StatusOK, &searchResult,
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/query",
|
||||
milvuspb.QueryRequest{Expr: "some expr"},
|
||||
http.StatusOK, &queryResult,
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/persist",
|
||||
milvuspb.FlushRequest{CollectionNames: []string{"c1"}},
|
||||
http.StatusOK, flushResult,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/distance",
|
||||
milvuspb.CalcDistanceRequest{
|
||||
Params: []*commonpb.KeyValuePair{
|
||||
{Key: "key", Value: "val"},
|
||||
},
|
||||
},
|
||||
http.StatusOK, calcDistanceResult,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/persist/state", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetFlushStateResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/persist/segment-info", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetPersistentSegmentInfoResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/query-segment-info", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetQuerySegmentInfoResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/replicas", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetReplicasResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/metrics", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetMetricsResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/load-balance", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/compaction/state", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetCompactionStateResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/compaction/plans", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetCompactionPlansResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/compaction", emptyBody,
|
||||
http.StatusOK, &milvuspb.ManualCompactionResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/import", emptyBody,
|
||||
http.StatusOK, &milvuspb.ImportResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/import/state", emptyBody,
|
||||
http.StatusOK, &milvuspb.GetImportStateResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/import/tasks", emptyBody,
|
||||
http.StatusOK, &milvuspb.ListImportTasksResponse{Status: testStatus},
|
||||
},
|
||||
{
|
||||
http.MethodPost, "/credential", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodPatch, "/credential", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodDelete, "/credential", emptyBody,
|
||||
http.StatusOK, testStatus,
|
||||
},
|
||||
{
|
||||
http.MethodGet, "/credential/users", emptyBody,
|
||||
http.StatusOK, &milvuspb.ListCredUsersResponse{Status: testStatus},
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(fmt.Sprintf("%s %s %d", tt.httpMethod, tt.path, tt.expectedStatus), func(t *testing.T) {
|
||||
body := []byte{}
|
||||
if tt.body != nil {
|
||||
body, _ = json.Marshal(tt.body)
|
||||
}
|
||||
req := httptest.NewRequest(tt.httpMethod, tt.path, bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
if tt.expectedBody != nil {
|
||||
bodyBytes, err := json.Marshal(tt.expectedBody)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, bodyBytes, w.Body.Bytes())
|
||||
}
|
||||
// test marshal failed
|
||||
req = httptest.NewRequest(tt.httpMethod, tt.path, bytes.NewReader([]byte("bad request")))
|
||||
w = httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowLevelSearchRejectSearchAggregation(t *testing.T) {
|
||||
h := NewHandlers(&mockProxyComponent{})
|
||||
testEngine := gin.New()
|
||||
h.RegisterRoutesTo(testEngine)
|
||||
|
||||
for _, body := range [][]byte{
|
||||
[]byte(`{"collection_name":"book","searchAggregation":{"fields":["brand"],"size":1}}`),
|
||||
[]byte(`{"collection_name":"book","search_aggregation":{"fields":["brand"],"size":1}}`),
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodPost, "/search", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "searchAggregation is not supported for low-level REST search")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/milvus-io/milvus/internal/json"
|
||||
)
|
||||
|
||||
var jsonContentType = []string{"application/json; charset=utf-8"}
|
||||
|
||||
type jsonRender struct {
|
||||
Data any
|
||||
}
|
||||
|
||||
// Render writes data with custom ContentType.
|
||||
func (r jsonRender) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
encoder := json.NewEncoder(w)
|
||||
return encoder.Encode(r.Data)
|
||||
}
|
||||
|
||||
// WriteContentType writes JSON ContentType.
|
||||
func (r jsonRender) WriteContentType(w http.ResponseWriter) {
|
||||
header := w.Header()
|
||||
if val := header["Content-Type"]; len(val) == 0 {
|
||||
header["Content-Type"] = jsonContentType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var testResult = gin.H{
|
||||
"message": "This is a test message",
|
||||
"data": make(map[string]interface{}),
|
||||
}
|
||||
|
||||
func init() {
|
||||
const chunkSize = 1024
|
||||
rs := randomString(chunkSize)
|
||||
var sb strings.Builder
|
||||
for sb.Len() < 10*1024*1024 {
|
||||
sb.WriteString(rs)
|
||||
}
|
||||
testResult["data"] = sb.String()
|
||||
}
|
||||
|
||||
func randomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var result strings.Builder
|
||||
for i := 0; i < length; i++ {
|
||||
result.WriteByte(charset[rand.Intn(len(charset))])
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func BenchmarkHTTPReturn(b *testing.B) {
|
||||
// Set Gin to test mode to prevent output to stdout
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
b.Run("test HTTPReturn", func(b *testing.B) {
|
||||
router := gin.New()
|
||||
router.GET("/test1", func(c *gin.Context) {
|
||||
HTTPReturn(c, http.StatusOK, testResult)
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := httptest.NewRequest("GET", "/test1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
})
|
||||
|
||||
b.Run("test HTTPReturnStream", func(b *testing.B) {
|
||||
router := gin.New()
|
||||
router.GET("/test2", func(c *gin.Context) {
|
||||
HTTPReturnStream(c, http.StatusOK, testResult)
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
req := httptest.NewRequest("GET", "/test2", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
})
|
||||
}
|
||||
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/milvus-io/milvus/internal/distributed/proxy/httpserver
|
||||
// cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
|
||||
// BenchmarkHTTPReturn
|
||||
// BenchmarkHTTPReturn/test_HTTPReturn
|
||||
// BenchmarkHTTPReturn/test_HTTPReturn-12 87 13127452 ns/op 27992718 B/op 34 allocs/op
|
||||
// BenchmarkHTTPReturn/test_HTTPReturnStream
|
||||
// BenchmarkHTTPReturn/test_HTTPReturnStream-12 87 12804875 ns/op 14361636 B/op 31 allocs/op
|
||||
// PASS
|
||||
//
|
||||
// Process finished with the exit code 0
|
||||
@@ -0,0 +1,96 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
type CreateCollectionReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
Dimension int32 `json:"dimension" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
MetricType string `json:"metricType"`
|
||||
PrimaryField string `json:"primaryField"`
|
||||
VectorField string `json:"vectorField"`
|
||||
EnableDynamicField bool `json:"enableDynamicField"`
|
||||
}
|
||||
|
||||
type DropCollectionReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
}
|
||||
|
||||
type QueryReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
OutputFields []string `json:"outputFields"`
|
||||
Filter string `json:"filter" validate:"required"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type GetReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
OutputFields []string `json:"outputFields"`
|
||||
ID interface{} `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type DeleteReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
ID interface{} `json:"id"`
|
||||
Filter string `json:"filter"`
|
||||
}
|
||||
|
||||
type InsertReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
Data []map[string]interface{} `json:"data" validate:"required"`
|
||||
}
|
||||
|
||||
type SingleInsertReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
Data map[string]interface{} `json:"data" validate:"required"`
|
||||
}
|
||||
|
||||
type UpsertReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
Data []map[string]interface{} `json:"data" validate:"required"`
|
||||
PartialUpdate bool `json:"partialUpdate"`
|
||||
FieldOps []FieldPartialUpdateOpReq `json:"fieldOps"`
|
||||
}
|
||||
|
||||
type SingleUpsertReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
Data map[string]interface{} `json:"data" validate:"required"`
|
||||
PartialUpdate bool `json:"partialUpdate"`
|
||||
FieldOps []FieldPartialUpdateOpReq `json:"fieldOps"`
|
||||
}
|
||||
|
||||
type SearchReq struct {
|
||||
DbName string `json:"dbName"`
|
||||
CollectionName string `json:"collectionName" validate:"required"`
|
||||
Filter string `json:"filter"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
OutputFields []string `json:"outputFields"`
|
||||
Vector []float32 `json:"vector"`
|
||||
Params map[string]float64 `json:"params"`
|
||||
SearchAggregation *SearchAggregationReq `json:"searchAggregation"`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/requestutil"
|
||||
)
|
||||
|
||||
func TestRequestV2_GetCollectionName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req requestutil.CollectionNameGetter
|
||||
want string
|
||||
}{
|
||||
{"RenameCollectionReq", &RenameCollectionReq{CollectionName: "col1"}, "col1"},
|
||||
{"QueryReqV2", &QueryReqV2{CollectionName: "col2"}, "col2"},
|
||||
{"CollectionIDReq", &CollectionIDReq{CollectionName: "col3"}, "col3"},
|
||||
{"CollectionFilterReq", &CollectionFilterReq{CollectionName: "col4"}, "col4"},
|
||||
{"CollectionDataReq", &CollectionDataReq{CollectionName: "col5"}, "col5"},
|
||||
{"SearchReqV2", &SearchReqV2{CollectionName: "col6"}, "col6"},
|
||||
{"HybridSearchReq", &HybridSearchReq{CollectionName: "col7"}, "col7"},
|
||||
{"PartitionsReq", &PartitionsReq{CollectionName: "col8"}, "col8"},
|
||||
{"GrantV2Req", &GrantV2Req{CollectionName: "col9"}, "col9"},
|
||||
{"IndexParamReq", &IndexParamReq{CollectionName: "col10"}, "col10"},
|
||||
{"CollectionReq", &CollectionReq{CollectionName: "col11"}, "col11"},
|
||||
{"RunAnalyzerReq", &RunAnalyzerReq{CollectionName: "col12"}, "col12"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, tt.req.GetCollectionName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFieldPartialUpdateOps(t *testing.T) {
|
||||
ops, err := buildFieldPartialUpdateOps([]FieldPartialUpdateOpReq{
|
||||
{FieldName: "tags", Op: "ARRAY_APPEND"},
|
||||
{FieldName: "scores", Op: "ARRAY_REMOVE"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, ops, 2)
|
||||
assert.Equal(t, "tags", ops[0].GetFieldName())
|
||||
assert.Equal(t, schemapb.FieldPartialUpdateOp_ARRAY_APPEND, ops[0].GetOp())
|
||||
assert.Equal(t, "scores", ops[1].GetFieldName())
|
||||
assert.Equal(t, schemapb.FieldPartialUpdateOp_ARRAY_REMOVE, ops[1].GetOp())
|
||||
}
|
||||
|
||||
func TestBuildFieldPartialUpdateOps_RejectsUnknownOp(t *testing.T) {
|
||||
_, err := buildFieldPartialUpdateOps([]FieldPartialUpdateOpReq{
|
||||
{FieldName: "tags", Op: "ARRAY_EXTEND"},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unsupported partial update op")
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/rgpb"
|
||||
)
|
||||
|
||||
func toKVPair(data map[string]string) []*commonpb.KeyValuePair {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pairs []*commonpb.KeyValuePair
|
||||
for k, v := range data {
|
||||
pairs = append(pairs, &commonpb.KeyValuePair{
|
||||
Key: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
func toPbRGNodeFilter(nodeFilter *ResourceGroupNodeFilter) *rgpb.ResourceGroupNodeFilter {
|
||||
if nodeFilter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &rgpb.ResourceGroupNodeFilter{
|
||||
NodeLabels: toKVPair(nodeFilter.GetNodeLabels()),
|
||||
}
|
||||
}
|
||||
|
||||
func toPbResourceGroupConfig(config *ResourceGroupConfig) *rgpb.ResourceGroupConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &rgpb.ResourceGroupConfig{
|
||||
Requests: &rgpb.ResourceGroupLimit{
|
||||
NodeNum: config.GetRequests().GetNodeNum(),
|
||||
},
|
||||
Limits: &rgpb.ResourceGroupLimit{
|
||||
NodeNum: config.GetLimits().GetNodeNum(),
|
||||
},
|
||||
TransferFrom: lo.Map(config.GetTransferFrom(), func(t *ResourceGroupTransfer, _ int) *rgpb.ResourceGroupTransfer {
|
||||
return &rgpb.ResourceGroupTransfer{
|
||||
ResourceGroup: t.GetResourceGroup(),
|
||||
}
|
||||
}),
|
||||
TransferTo: lo.Map(config.GetTransferTo(), func(t *ResourceGroupTransfer, _ int) *rgpb.ResourceGroupTransfer {
|
||||
return &rgpb.ResourceGroupTransfer{
|
||||
ResourceGroup: t.GetResourceGroup(),
|
||||
}
|
||||
}),
|
||||
NodeFilter: toPbRGNodeFilter(config.GetNodeFilter()),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HandlersV2) createResourceGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
|
||||
httpReq := anyReq.(*ResourceGroupReq)
|
||||
req := &milvuspb.CreateResourceGroupRequest{
|
||||
ResourceGroup: httpReq.GetName(),
|
||||
Config: toPbResourceGroupConfig(httpReq.GetConfig()),
|
||||
}
|
||||
resp, err := wrapperProxyWithLimit(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/CreateResourceGroup", true, h.proxy, func(reqCtx context.Context, req any) (interface{}, error) {
|
||||
return h.proxy.CreateResourceGroup(reqCtx, req.(*milvuspb.CreateResourceGroupRequest))
|
||||
})
|
||||
if err == nil {
|
||||
HTTPReturn(c, http.StatusOK, wrapperReturnDefault())
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (h *HandlersV2) dropResourceGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
|
||||
httpReq := anyReq.(*ResourceGroupReq)
|
||||
req := &milvuspb.DropResourceGroupRequest{
|
||||
ResourceGroup: httpReq.GetName(),
|
||||
}
|
||||
resp, err := wrapperProxyWithLimit(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/DropResourceGroup", true, h.proxy, func(reqCtx context.Context, req any) (interface{}, error) {
|
||||
return h.proxy.DropResourceGroup(reqCtx, req.(*milvuspb.DropResourceGroupRequest))
|
||||
})
|
||||
if err == nil {
|
||||
HTTPReturn(c, http.StatusOK, wrapperReturnDefault())
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (h *HandlersV2) listResourceGroups(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
|
||||
req := &milvuspb.ListResourceGroupsRequest{}
|
||||
resp, err := wrapperProxyWithLimit(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/ListResourceGroups", true, h.proxy, func(reqCtx context.Context, req any) (interface{}, error) {
|
||||
return h.proxy.ListResourceGroups(reqCtx, req.(*milvuspb.ListResourceGroupsRequest))
|
||||
})
|
||||
if err == nil {
|
||||
HTTPReturn(c, http.StatusOK, wrapperReturnList(resp.(*milvuspb.ListResourceGroupsResponse).GetResourceGroups()))
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (h *HandlersV2) describeResourceGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
|
||||
httpReq := anyReq.(*ResourceGroupReq)
|
||||
req := &milvuspb.DescribeResourceGroupRequest{
|
||||
ResourceGroup: httpReq.GetName(),
|
||||
}
|
||||
resp, err := wrapperProxyWithLimit(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/DescribeResourceGroup", true, h.proxy, func(reqCtx context.Context, req any) (interface{}, error) {
|
||||
return h.proxy.DescribeResourceGroup(reqCtx, req.(*milvuspb.DescribeResourceGroupRequest))
|
||||
})
|
||||
if err == nil {
|
||||
response := resp.(*milvuspb.DescribeResourceGroupResponse)
|
||||
HTTPReturn(c, http.StatusOK, gin.H{
|
||||
"resource_group": response.GetResourceGroup(),
|
||||
})
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (h *HandlersV2) updateResourceGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
|
||||
httpReq := anyReq.(*UpdateResourceGroupReq)
|
||||
req := &milvuspb.UpdateResourceGroupsRequest{
|
||||
ResourceGroups: lo.MapValues(httpReq.GetResourceGroups(), func(config *ResourceGroupConfig, name string) *rgpb.ResourceGroupConfig {
|
||||
return toPbResourceGroupConfig(config)
|
||||
}),
|
||||
}
|
||||
resp, err := wrapperProxyWithLimit(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/UpdateResourceGroups", true, h.proxy, func(reqCtx context.Context, req any) (interface{}, error) {
|
||||
return h.proxy.UpdateResourceGroups(reqCtx, req.(*milvuspb.UpdateResourceGroupsRequest))
|
||||
})
|
||||
if err == nil {
|
||||
HTTPReturn(c, http.StatusOK, wrapperReturnDefault())
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (h *HandlersV2) transferReplica(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
|
||||
httpReq := anyReq.(*TransferReplicaReq)
|
||||
req := &milvuspb.TransferReplicaRequest{
|
||||
SourceResourceGroup: httpReq.GetSourceRgName(),
|
||||
TargetResourceGroup: httpReq.GetTargetRgName(),
|
||||
CollectionName: httpReq.GetCollectionName(),
|
||||
NumReplica: httpReq.GetReplicaNum(),
|
||||
}
|
||||
resp, err := wrapperProxyWithLimit(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/TransferMaster", true, h.proxy, func(reqCtx context.Context, req any) (interface{}, error) {
|
||||
return h.proxy.TransferReplica(reqCtx, req.(*milvuspb.TransferReplicaRequest))
|
||||
})
|
||||
if err == nil {
|
||||
HTTPReturn(c, http.StatusOK, wrapperReturnDefault())
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
|
||||
"github.com/milvus-io/milvus/internal/json"
|
||||
"github.com/milvus-io/milvus/internal/mocks"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
|
||||
)
|
||||
|
||||
func validateRequestBodyTestCases(t *testing.T, testEngine *gin.Engine, queryTestCases []requestBodyTestCase, allowInt64 bool) {
|
||||
for i, testcase := range queryTestCases {
|
||||
t.Run(testcase.path, func(t *testing.T) {
|
||||
bodyReader := bytes.NewReader(testcase.requestBody)
|
||||
req := httptest.NewRequest(http.MethodPost, testcase.path, bodyReader)
|
||||
if allowInt64 {
|
||||
req.Header.Set(HTTPHeaderAllowInt64, "true")
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code, "case %d: ", i, string(testcase.requestBody))
|
||||
returnBody := &ReturnErrMsg{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), returnBody)
|
||||
assert.Nil(t, err, "case %d: ", i)
|
||||
assert.Equal(t, testcase.errCode, returnBody.Code, "case %d: ", i, string(testcase.requestBody))
|
||||
if testcase.errCode != 0 {
|
||||
assert.Equal(t, testcase.errMsg, returnBody.Message, "case %d: ", i, string(testcase.requestBody))
|
||||
}
|
||||
fmt.Println(w.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceGroupHandlerV2(t *testing.T) {
|
||||
// init params
|
||||
paramtable.Init()
|
||||
|
||||
// disable rate limit
|
||||
paramtable.Get().Save(paramtable.Get().QuotaConfig.QuotaAndLimitsEnabled.Key, "false")
|
||||
defer paramtable.Get().Reset(paramtable.Get().QuotaConfig.QuotaAndLimitsEnabled.Key)
|
||||
|
||||
// mock proxy
|
||||
mockProxy := mocks.NewMockProxy(t)
|
||||
mockProxy.EXPECT().CreateResourceGroup(mock.Anything, mock.Anything).Return(merr.Success(), nil)
|
||||
mockProxy.EXPECT().DescribeResourceGroup(mock.Anything, mock.Anything).Return(&milvuspb.DescribeResourceGroupResponse{}, nil)
|
||||
mockProxy.EXPECT().DropResourceGroup(mock.Anything, mock.Anything).Return(merr.Success(), nil)
|
||||
mockProxy.EXPECT().ListResourceGroups(mock.Anything, mock.Anything).Return(&milvuspb.ListResourceGroupsResponse{}, nil)
|
||||
mockProxy.EXPECT().UpdateResourceGroups(mock.Anything, mock.Anything).Return(merr.Success(), nil)
|
||||
mockProxy.EXPECT().TransferReplica(mock.Anything, mock.Anything).Return(merr.Success(), nil)
|
||||
|
||||
// setup test server
|
||||
testServer := initHTTPServerV2(mockProxy, false)
|
||||
|
||||
// setup test case
|
||||
testCases := make([]requestBodyTestCase, 0)
|
||||
|
||||
// test case: create resource group with empty request body
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, CreateAction),
|
||||
requestBody: []byte(`{}`),
|
||||
errCode: 1802,
|
||||
errMsg: "missing required parameters, error: Key: 'ResourceGroupReq.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
||||
})
|
||||
|
||||
// test case: create resource group with name
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, CreateAction),
|
||||
requestBody: []byte(`{"name":"test"}`),
|
||||
})
|
||||
|
||||
// test case: create resource group with name and config
|
||||
requestBodyInJSON := `{"name":"test","config":{"requests":{"node_num":1},"limits":{"node_num":1},"transfer_from":[{"resource_group":"__default_resource_group"}],"transfer_to":[{"resource_group":"__default_resource_group"}]}}`
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, CreateAction),
|
||||
requestBody: []byte(requestBodyInJSON),
|
||||
})
|
||||
|
||||
// test case: describe resource group with empty request body
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, DescribeAction),
|
||||
requestBody: []byte(`{}`),
|
||||
errCode: 1802,
|
||||
errMsg: "missing required parameters, error: Key: 'ResourceGroupReq.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
||||
})
|
||||
// test case: describe resource group with name
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, DescribeAction),
|
||||
requestBody: []byte(`{"name":"test"}`),
|
||||
})
|
||||
|
||||
// test case: drop resource group with empty request body
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, DropAction),
|
||||
requestBody: []byte(`{}`),
|
||||
errCode: 1802,
|
||||
errMsg: "missing required parameters, error: Key: 'ResourceGroupReq.Name' Error:Field validation for 'Name' failed on the 'required' tag",
|
||||
})
|
||||
// test case: drop resource group with name
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, DropAction),
|
||||
requestBody: []byte(`{"name":"test"}`),
|
||||
})
|
||||
|
||||
// test case: list resource groups
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, ListAction),
|
||||
requestBody: []byte(`{}`),
|
||||
})
|
||||
|
||||
// test case: update resource group with empty request body
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, AlterAction),
|
||||
requestBody: []byte(`{}`),
|
||||
errCode: 1802,
|
||||
errMsg: "missing required parameters, error: Key: 'UpdateResourceGroupReq.ResourceGroups' Error:Field validation for 'ResourceGroups' failed on the 'required' tag",
|
||||
})
|
||||
|
||||
// test case: update resource group with resource groups
|
||||
requestBodyInJSON = `{"resource_groups":{"test":{"requests":{"node_num":1},"limits":{"node_num":1},"transfer_from":[{"resource_group":"__default_resource_group"}],"transfer_to":[{"resource_group":"__default_resource_group"}]}}}`
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, AlterAction),
|
||||
requestBody: []byte(requestBodyInJSON),
|
||||
})
|
||||
|
||||
// test case: transfer replica with empty request body
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, TransferReplicaAction),
|
||||
requestBody: []byte(`{}`),
|
||||
errCode: 1802,
|
||||
errMsg: "missing required parameters, error: Key: 'TransferReplicaReq.SourceRgName' Error:Field validation for 'SourceRgName' failed on the 'required' tag\nKey: 'TransferReplicaReq.TargetRgName' Error:Field validation for 'TargetRgName' failed on the 'required' tag\nKey: 'TransferReplicaReq.CollectionName' Error:Field validation for 'CollectionName' failed on the 'required' tag\nKey: 'TransferReplicaReq.ReplicaNum' Error:Field validation for 'ReplicaNum' failed on the 'required' tag",
|
||||
})
|
||||
|
||||
// test case: transfer replica
|
||||
requestBodyInJSON = `{"sourceRgName":"rg1","targetRgName":"rg2","collectionName":"hello_milvus","replicaNum":1}`
|
||||
testCases = append(testCases, requestBodyTestCase{
|
||||
path: versionalV2(ResourceGroupCategory, TransferReplicaAction),
|
||||
requestBody: []byte(requestBodyInJSON),
|
||||
})
|
||||
|
||||
// verify test case
|
||||
validateRequestBodyTestCases(t, testServer, testCases, false)
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
mhttp "github.com/milvus-io/milvus/internal/http"
|
||||
"github.com/milvus-io/milvus/internal/json"
|
||||
"github.com/milvus-io/milvus/pkg/v3/mlog"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
|
||||
)
|
||||
|
||||
// BufferPool represents a pool of buffers.
|
||||
type BufferPool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
// Get returns a buffer from the buffer pool.
|
||||
// If the pool is empty, a new buffer is created and returned.
|
||||
func (p *BufferPool) Get() *bytes.Buffer {
|
||||
buf := p.pool.Get()
|
||||
if buf == nil {
|
||||
return &bytes.Buffer{}
|
||||
}
|
||||
return buf.(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// Put adds a buffer back to the pool.
|
||||
func (p *BufferPool) Put(buf *bytes.Buffer) {
|
||||
p.pool.Put(buf)
|
||||
}
|
||||
|
||||
// Timeout struct
|
||||
type Timeout struct {
|
||||
handler gin.HandlerFunc
|
||||
}
|
||||
|
||||
const timeoutRecorderNoWritten = -1
|
||||
|
||||
type timeoutResponseRecorder struct {
|
||||
body *bytes.Buffer
|
||||
headers http.Header
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
status int
|
||||
size int
|
||||
closeNotify chan bool
|
||||
}
|
||||
|
||||
func newTimeoutResponseRecorder(buf *bytes.Buffer) *timeoutResponseRecorder {
|
||||
return &timeoutResponseRecorder{
|
||||
body: buf,
|
||||
headers: make(http.Header),
|
||||
status: http.StatusOK,
|
||||
size: timeoutRecorderNoWritten,
|
||||
closeNotify: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Header() http.Header {
|
||||
return w.headers
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Write(data []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed || w.body == nil {
|
||||
return 0, merr.WrapErrServiceInternalMsg("response writer closed")
|
||||
}
|
||||
if !w.written() {
|
||||
w.size = 0
|
||||
}
|
||||
n, err := w.body.Write(data)
|
||||
w.size += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) WriteString(s string) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed || w.body == nil {
|
||||
return 0, merr.WrapErrServiceInternalMsg("response writer closed")
|
||||
}
|
||||
if !w.written() {
|
||||
w.size = 0
|
||||
}
|
||||
n, err := w.body.WriteString(s)
|
||||
w.size += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) WriteHeader(code int) {
|
||||
if code == -1 {
|
||||
return
|
||||
}
|
||||
checkWriteHeaderCode(code)
|
||||
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed || w.written() {
|
||||
return
|
||||
}
|
||||
w.status = code
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) WriteHeaderNow() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed || w.written() {
|
||||
return
|
||||
}
|
||||
w.size = 0
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Status() int {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.status
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Size() int {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.size
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Written() bool {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.written()
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Flush() {}
|
||||
|
||||
func (w *timeoutResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return nil, nil, merr.WrapErrServiceInternalMsg("response writer does not support hijack")
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) CloseNotify() <-chan bool {
|
||||
return w.closeNotify
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Pusher() http.Pusher {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) CommitTo(realWriter gin.ResponseWriter) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.closed || w.body == nil {
|
||||
return merr.WrapErrServiceInternalMsg("response writer closed")
|
||||
}
|
||||
|
||||
dst := realWriter.Header()
|
||||
for k, vv := range w.headers {
|
||||
dst[k] = append([]string(nil), vv...)
|
||||
}
|
||||
realWriter.WriteHeader(w.status)
|
||||
if w.body.Len() == 0 {
|
||||
if w.written() || w.status != http.StatusOK {
|
||||
realWriter.WriteHeaderNow()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, err := realWriter.Write(w.body.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) Close() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.body != nil {
|
||||
w.body.Reset()
|
||||
w.body = nil
|
||||
}
|
||||
w.closed = true
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) CloseForTimeout() {
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func (w *timeoutResponseRecorder) written() bool {
|
||||
return w.size != timeoutRecorderNoWritten
|
||||
}
|
||||
|
||||
func checkWriteHeaderCode(code int) {
|
||||
if code < 100 || code > 999 {
|
||||
panic(fmt.Sprintf("invalid http status code: %d", code))
|
||||
}
|
||||
}
|
||||
|
||||
var timeoutContextKeysToPropagate = []string{
|
||||
HTTPReturnCode,
|
||||
HTTPReturnMessage,
|
||||
ContextRequest,
|
||||
ContextResponse,
|
||||
"traceID",
|
||||
}
|
||||
|
||||
func propagateTimeoutContextKeys(dst *gin.Context, src *gin.Context) {
|
||||
for _, key := range timeoutContextKeysToPropagate {
|
||||
if value, ok := src.Get(key); ok {
|
||||
dst.Set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func timeoutMiddleware(handler gin.HandlerFunc) gin.HandlerFunc {
|
||||
timeoutHandler := &Timeout{
|
||||
handler: handler,
|
||||
}
|
||||
bufPool := &BufferPool{}
|
||||
return func(gCtx *gin.Context) {
|
||||
timeout := paramtable.Get().HTTPCfg.RequestTimeoutMs.GetAsDuration(time.Millisecond)
|
||||
requestTimeout := gCtx.Request.Header.Get(mhttp.HTTPHeaderRequestTimeout)
|
||||
if requestTimeout != "" {
|
||||
timeoutSecond, err := strconv.ParseInt(requestTimeout, 10, 64)
|
||||
if err != nil {
|
||||
HTTPAbortReturn(gCtx, http.StatusOK, gin.H{
|
||||
mhttp.HTTPReturnCode: merr.Code(merr.ErrParameterInvalid),
|
||||
mhttp.HTTPReturnMessage: merr.WrapErrParameterInvalidMsg(
|
||||
"%s parse failed, err: %s",
|
||||
mhttp.HTTPHeaderRequestTimeout,
|
||||
err.Error(),
|
||||
).Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
timeout = time.Duration(timeoutSecond) * time.Second
|
||||
}
|
||||
topCtx, cancel := context.WithTimeout(gCtx.Request.Context(), timeout)
|
||||
defer cancel()
|
||||
req := gCtx.Request.WithContext(topCtx)
|
||||
gCtx.Request = req
|
||||
|
||||
finish := make(chan struct{}, 1)
|
||||
panicChan := make(chan interface{}, 1)
|
||||
|
||||
realWriter := gCtx.Writer
|
||||
buffer := bufPool.Get()
|
||||
buffer.Reset()
|
||||
recorder := newTimeoutResponseRecorder(buffer)
|
||||
handlerCtx := gCtx.Copy()
|
||||
handlerCtx.Request = req
|
||||
handlerCtx.Writer = recorder
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
panicChan <- p
|
||||
}
|
||||
}()
|
||||
timeoutHandler.handler(handlerCtx)
|
||||
finish <- struct{}{}
|
||||
}()
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case p := <-panicChan:
|
||||
recorder.Close()
|
||||
bufPool.Put(buffer)
|
||||
gCtx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{mhttp.HTTPReturnCode: http.StatusInternalServerError})
|
||||
panic(p)
|
||||
|
||||
case <-finish:
|
||||
propagateTimeoutContextKeys(gCtx, handlerCtx)
|
||||
if handlerCtx.IsAborted() {
|
||||
gCtx.Abort()
|
||||
}
|
||||
gCtx.Next()
|
||||
if err := recorder.CommitTo(realWriter); err != nil {
|
||||
mlog.Warn(context.TODO(), "failed to write response body", mlog.Err(err))
|
||||
recorder.Close()
|
||||
bufPool.Put(buffer)
|
||||
return
|
||||
}
|
||||
recorder.Close()
|
||||
bufPool.Put(buffer)
|
||||
|
||||
case <-timer.C:
|
||||
cancel()
|
||||
gCtx.Abort()
|
||||
gCtx.Set(HTTPReturnCode, merr.TimeoutCode)
|
||||
gCtx.Set(HTTPReturnMessage, "request timeout")
|
||||
recorder.CloseForTimeout()
|
||||
bufPool.Put(buffer)
|
||||
|
||||
realWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if traceID, ok := getTraceID(gCtx); ok {
|
||||
setTraceIDHeaderTo(realWriter.Header(), traceID)
|
||||
}
|
||||
realWriter.WriteHeader(http.StatusRequestTimeout)
|
||||
body, _ := json.Marshal(gin.H{HTTPReturnCode: merr.TimeoutCode, HTTPReturnMessage: "request timeout"})
|
||||
realWriter.Write(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
|
||||
)
|
||||
|
||||
const testTraceID = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
var traceIDPattern = regexp.MustCompile(`^[0-9a-f]{32}$`)
|
||||
|
||||
func withTestTracerProvider(t *testing.T, opts ...sdktrace.TracerProviderOption) {
|
||||
t.Helper()
|
||||
oldProvider := otel.GetTracerProvider()
|
||||
tp := sdktrace.NewTracerProvider(opts...)
|
||||
otel.SetTracerProvider(tp)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, tp.Shutdown(context.Background()))
|
||||
otel.SetTracerProvider(oldProvider)
|
||||
})
|
||||
}
|
||||
|
||||
func withTraceContextPropagator(t *testing.T) {
|
||||
t.Helper()
|
||||
oldPropagator := otel.GetTextMapPropagator()
|
||||
otel.SetTextMapPropagator(propagation.TraceContext{})
|
||||
t.Cleanup(func() {
|
||||
otel.SetTextMapPropagator(oldPropagator)
|
||||
})
|
||||
}
|
||||
|
||||
func newTraceIDTestContext() (*gin.Context, *httptest.ResponseRecorder) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/traceid", nil)
|
||||
c.Set("traceID", testTraceID)
|
||||
return c, w
|
||||
}
|
||||
|
||||
func assertTraceIDHeader(t *testing.T, headers http.Header, expected string) {
|
||||
t.Helper()
|
||||
assert.Equal(t, expected, headers.Get(HTTPHeaderMilvusTraceID))
|
||||
}
|
||||
|
||||
func assertValidTraceIDHeader(t *testing.T, headers http.Header) string {
|
||||
t.Helper()
|
||||
traceID := headers.Get(HTTPHeaderMilvusTraceID)
|
||||
require.Regexp(t, traceIDPattern, traceID)
|
||||
assertTraceIDHeader(t, headers, traceID)
|
||||
return traceID
|
||||
}
|
||||
|
||||
func TestHTTPReturnWritesTraceIDHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
write func(*gin.Context)
|
||||
}{
|
||||
{
|
||||
name: "json",
|
||||
write: func(c *gin.Context) {
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stream",
|
||||
write: func(c *gin.Context) {
|
||||
HTTPReturnStream(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "abort",
|
||||
write: func(c *gin.Context) {
|
||||
HTTPAbortReturn(c, http.StatusUnauthorized, gin.H{
|
||||
HTTPReturnCode: merr.Code(merr.ErrNeedAuthenticate),
|
||||
HTTPReturnMessage: merr.ErrNeedAuthenticate.Error(),
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c, w := newTraceIDTestContext()
|
||||
|
||||
test.write(c)
|
||||
|
||||
assertTraceIDHeader(t, w.Header(), testTraceID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPReturnSkipsTraceIDHeaderWhenMissing(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/traceid", nil)
|
||||
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
|
||||
assert.Empty(t, w.Header().Get(HTTPHeaderMilvusTraceID))
|
||||
}
|
||||
|
||||
func TestTraceIDHandlerFuncCreatesRequestTraceID(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
|
||||
var ctxTraceID string
|
||||
app.POST("/v2/vectordb/entities/search", func(c *gin.Context) {
|
||||
spanCtx := oteltrace.SpanFromContext(c.Request.Context()).SpanContext()
|
||||
require.True(t, spanCtx.TraceID().IsValid())
|
||||
ctxTraceID = spanCtx.TraceID().String()
|
||||
|
||||
traceID, ok := c.Get("traceID")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, ctxTraceID, traceID)
|
||||
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/vectordb/entities/search", nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
returnedTraceID := assertValidTraceIDHeader(t, w.Header())
|
||||
assert.Equal(t, ctxTraceID, returnedTraceID)
|
||||
}
|
||||
|
||||
func TestTraceIDHandlerFuncContinuesIncomingTrace(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
withTraceContextPropagator(t)
|
||||
|
||||
parentTraceID, err := oteltrace.TraceIDFromHex("11111111111111111111111111111111")
|
||||
require.NoError(t, err)
|
||||
parentSpanID, err := oteltrace.SpanIDFromHex("2222222222222222")
|
||||
require.NoError(t, err)
|
||||
parentSpan := oteltrace.NewSpanContext(oteltrace.SpanContextConfig{
|
||||
TraceID: parentTraceID,
|
||||
SpanID: parentSpanID,
|
||||
TraceFlags: oteltrace.FlagsSampled,
|
||||
Remote: true,
|
||||
})
|
||||
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.POST("/v2/vectordb/entities/search", func(c *gin.Context) {
|
||||
spanCtx := oteltrace.SpanFromContext(c.Request.Context()).SpanContext()
|
||||
assert.Equal(t, parentTraceID, spanCtx.TraceID())
|
||||
assert.NotEqual(t, parentSpanID, spanCtx.SpanID())
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/vectordb/entities/search", nil)
|
||||
otel.GetTextMapPropagator().Inject(
|
||||
oteltrace.ContextWithRemoteSpanContext(context.Background(), parentSpan),
|
||||
propagation.HeaderCarrier(req.Header),
|
||||
)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assertTraceIDHeader(t, w.Header(), parentTraceID.String())
|
||||
}
|
||||
|
||||
func TestTraceIDHandlerFuncCoversV1AndV2RestRoutes(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "v1",
|
||||
method: http.MethodGet,
|
||||
path: "/v1/vector/collections",
|
||||
},
|
||||
{
|
||||
name: "v2",
|
||||
method: http.MethodPost,
|
||||
path: "/v2/vectordb/collections/list",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.Handle(test.method, test.path, func(c *gin.Context) {
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(test.method, test.path, nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assertValidTraceIDHeader(t, w.Header())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceIDHandlerFuncCreatesTraceIDWhenUnsampled(t *testing.T) {
|
||||
withTestTracerProvider(t, sdktrace.WithSampler(sdktrace.NeverSample()))
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
|
||||
var sampled bool
|
||||
app.POST("/v2/vectordb/entities/search", func(c *gin.Context) {
|
||||
spanCtx := oteltrace.SpanFromContext(c.Request.Context()).SpanContext()
|
||||
require.True(t, spanCtx.TraceID().IsValid())
|
||||
sampled = spanCtx.IsSampled()
|
||||
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/vectordb/entities/search", nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.False(t, sampled)
|
||||
assertValidTraceIDHeader(t, w.Header())
|
||||
}
|
||||
|
||||
func TestTraceIDHandlerFuncReturnsUniqueTraceID(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.POST("/v2/vectordb/entities/search", func(c *gin.Context) {
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
})
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
for i := 0; i < 20; i++ {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/vectordb/entities/search", nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
traceID := assertValidTraceIDHeader(t, w.Header())
|
||||
require.NotContains(t, seen, traceID)
|
||||
seen[traceID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceIDHandlerFuncCoversEarlyAbort(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.GET("/auth", func(c *gin.Context) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
HTTPReturnCode: merr.Code(merr.ErrNeedAuthenticate),
|
||||
HTTPReturnMessage: merr.ErrNeedAuthenticate.Error(),
|
||||
})
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth", nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assertValidTraceIDHeader(t, w.Header())
|
||||
}
|
||||
|
||||
func TestWrapperPostBadJSONReturnsTraceIDHeader(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.POST("/v2/vectordb/entities/search", wrapperPost(func() any { return &DefaultReq{} },
|
||||
func(ctx context.Context, c *gin.Context, req any, dbName string) (interface{}, error) {
|
||||
return nil, nil
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/vectordb/entities/search", strings.NewReader(`{"dbName"}`))
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assertValidTraceIDHeader(t, w.Header())
|
||||
}
|
||||
|
||||
func TestWrapperPostKeepsRequestTraceID(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.Use(func(c *gin.Context) {
|
||||
c.Set(ContextUsername, "")
|
||||
c.Next()
|
||||
})
|
||||
|
||||
var handlerTraceID string
|
||||
app.POST("/v2/vectordb/entities/search", wrapperPost(func() any { return &DefaultReq{} },
|
||||
func(ctx context.Context, c *gin.Context, req any, dbName string) (interface{}, error) {
|
||||
handlerTraceID = oteltrace.SpanFromContext(ctx).SpanContext().TraceID().String()
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
return nil, nil
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/vectordb/entities/search", strings.NewReader(`{}`))
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
returnedTraceID := assertValidTraceIDHeader(t, w.Header())
|
||||
assert.Equal(t, handlerTraceID, returnedTraceID)
|
||||
}
|
||||
|
||||
func TestWrapperPostUsesRequestSpan(t *testing.T) {
|
||||
recorder := tracetest.NewSpanRecorder()
|
||||
withTestTracerProvider(t, sdktrace.WithSpanProcessor(recorder))
|
||||
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.Use(func(c *gin.Context) {
|
||||
c.Set(ContextUsername, "")
|
||||
c.Next()
|
||||
})
|
||||
app.POST("/v2/vectordb/entities/search", wrapperPost(func() any { return &DefaultReq{} },
|
||||
func(ctx context.Context, c *gin.Context, req any, dbName string) (interface{}, error) {
|
||||
HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil)})
|
||||
return nil, nil
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/vectordb/entities/search", strings.NewReader(`{}`))
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
spans := recorder.Ended()
|
||||
require.Len(t, spans, 1)
|
||||
assert.Equal(t, "/v2/vectordb/entities/search", spans[0].Name())
|
||||
}
|
||||
|
||||
func TestTimeoutMiddlewareReturnsTraceIDHeader(t *testing.T) {
|
||||
withTestTracerProvider(t)
|
||||
paramtable.Init()
|
||||
paramtable.Get().Save(paramtable.Get().HTTPCfg.RequestTimeoutMs.Key, "10")
|
||||
t.Cleanup(func() {
|
||||
paramtable.Get().Reset(paramtable.Get().HTTPCfg.RequestTimeoutMs.Key)
|
||||
})
|
||||
|
||||
app := gin.New()
|
||||
app.Use(TraceIDHandlerFunc)
|
||||
app.POST("/timeout", timeoutMiddleware(func(c *gin.Context) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/timeout", nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusRequestTimeout, w.Code)
|
||||
assertValidTraceIDHeader(t, w.Header())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
||||
"github.com/milvus-io/milvus/internal/json"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
||||
)
|
||||
|
||||
/*
|
||||
* Benchmarkings for different serialization implementations
|
||||
* See results: https://github.com/milvus-io/milvus/pull/37556#issuecomment-2491668743
|
||||
*/
|
||||
|
||||
// serializeFloatVectorsBaseline uses []gjson.Result as input and calls json.Unmarshal in multiple times,
|
||||
// which downgrades the performance
|
||||
func serializeFloatVectorsBaseline(vectors []gjson.Result, dataType schemapb.DataType, dimension, bytesLen int64) ([][]byte, error) {
|
||||
values := make([][]byte, 0)
|
||||
for _, vector := range vectors {
|
||||
var vectorArray []float32
|
||||
err := json.Unmarshal([]byte(vector.String()), &vectorArray)
|
||||
if err != nil {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], vector.String(), err.Error())
|
||||
}
|
||||
if int64(len(vectorArray)) != dimension {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], vector.String(),
|
||||
fmt.Sprintf("dimension: %d, but length of []float: %d", dimension, len(vectorArray)))
|
||||
}
|
||||
vectorBytes := typeutil.Float32ArrayToBytes(vectorArray)
|
||||
values = append(values, vectorBytes)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// serializeFloatOrByteVectorsBaseline calls json.Unmarshal in multiple times, which downgrades the performance
|
||||
func serializeFloatOrByteVectorsBaseline(jsonResult gjson.Result, dataType schemapb.DataType, dimension int64, fpArrayToBytesFunc func([]float32) []byte) ([][]byte, error) {
|
||||
values := make([][]byte, 0)
|
||||
for _, vector := range jsonResult.Array() {
|
||||
if vector.IsArray() {
|
||||
var vectorArray []float32
|
||||
err := json.Unmarshal([]byte(vector.Raw), &vectorArray)
|
||||
if err != nil {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], vector.String(), err.Error())
|
||||
}
|
||||
if int64(len(vectorArray)) != dimension {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], vector.String(),
|
||||
fmt.Sprintf("dimension: %d, but length of []float: %d", dimension, len(vectorArray)))
|
||||
}
|
||||
vectorBytes := fpArrayToBytesFunc(vectorArray)
|
||||
values = append(values, vectorBytes)
|
||||
} else if vector.Type == gjson.String {
|
||||
var vectorArray []byte
|
||||
err := json.Unmarshal([]byte(vector.Raw), &vectorArray)
|
||||
if err != nil {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], vector.String(), err.Error())
|
||||
}
|
||||
if int64(len(vectorArray)) != dimension*2 {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], string(vectorArray),
|
||||
fmt.Sprintf("dimension: %d, but length of []byte: %d", dimension, len(vectorArray)))
|
||||
}
|
||||
values = append(values, vectorArray)
|
||||
} else {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], vector.String(), "invalid type")
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// serializeFloatOrByteVectorsUnmarshalTwice calls Unmarshal twice, which downgrades the performance
|
||||
// See: https://github.com/milvus-io/milvus/pull/37556#discussion_r1849672721
|
||||
func serializeFloatOrByteVectorsUnmarshalTwice(vectorStr string, dataType schemapb.DataType, dimension int64, serializeFunc func([]float32) []byte) ([][]byte, error) {
|
||||
// try to unmarshal as [][]float32 first to make sure `[[3, 3]]` is [][]float32 instead of [][]byte
|
||||
fp32Values := make([][]float32, 0)
|
||||
err := json.Unmarshal([]byte(vectorStr), &fp32Values)
|
||||
if err == nil {
|
||||
values := make([][]byte, 0)
|
||||
for _, vectorArray := range fp32Values {
|
||||
if int64(len(vectorArray)) != dimension {
|
||||
return nil, merr.WrapErrParameterInvalid(schemapb.DataType_name[int32(dataType)], fmt.Sprintf("%v", vectorArray),
|
||||
fmt.Sprintf("dimension: %d, but length of []float: %d", dimension, len(vectorArray)))
|
||||
}
|
||||
vectorBytes := serializeFunc(vectorArray)
|
||||
values = append(values, vectorBytes)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
return serializeByteVectors(vectorStr, dataType, dimension, dimension*2)
|
||||
}
|
||||
|
||||
func generateVectorsStr() string {
|
||||
vectors := make([][]float32, 0, 10_000)
|
||||
for i := 0; i < 10_000; i++ {
|
||||
vector := make([]float32, 0, 128)
|
||||
for j := 0; j < 128; j++ {
|
||||
vector = append(vector, rand.Float32())
|
||||
}
|
||||
vectors = append(vectors, vector)
|
||||
}
|
||||
vectorJSON, _ := json.Marshal(vectors)
|
||||
return string(vectorJSON)
|
||||
}
|
||||
|
||||
func generateVectorsJSON() gjson.Result {
|
||||
vectorJSON := generateVectorsStr()
|
||||
return gjson.Parse(vectorJSON)
|
||||
}
|
||||
|
||||
func generateByteVectorsStr() string {
|
||||
vectors := make([][]byte, 0, 10_000)
|
||||
for i := 0; i < 10_000; i++ {
|
||||
vector := make([]byte, 0, 128*4)
|
||||
for j := 0; j < 128*4; j++ {
|
||||
vector = append(vector, byte(rand.Intn(256)))
|
||||
}
|
||||
vectors = append(vectors, vector)
|
||||
}
|
||||
vectorJSON, _ := json.Marshal(vectors)
|
||||
return string(vectorJSON)
|
||||
}
|
||||
|
||||
func generateByteVectorsJSON() gjson.Result {
|
||||
vectorJSON := generateByteVectorsStr()
|
||||
return gjson.Parse(vectorJSON)
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_FloatVectors_Baseline(b *testing.B) {
|
||||
vectorsJSON := generateVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeFloatVectorsBaseline(vectorsJSON.Array(), schemapb.DataType_FloatVector, 128, -1)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_FloatVectors(b *testing.B) {
|
||||
vectorsJSON := generateVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeFloatVectors(vectorsJSON.Raw, schemapb.DataType_FloatVector, 128, -1, typeutil.Float32ArrayToBytes)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_FloatVectors_Float16(b *testing.B) {
|
||||
vectorsJSON := generateVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeFloatVectors(vectorsJSON.Raw, schemapb.DataType_Float16Vector, 128, -1, typeutil.Float32ArrayToFloat16Bytes)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_FloatOrByteVectors_Fp32(b *testing.B) {
|
||||
vectorsJSON := generateVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeFloatOrByteVectors(vectorsJSON, schemapb.DataType_Float16Vector, 128, typeutil.Float32ArrayToFloat16Bytes)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_FloatOrByteVectors_Byte(b *testing.B) {
|
||||
vectorsJSON := generateByteVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeFloatOrByteVectors(vectorsJSON, schemapb.DataType_Float16Vector, 256, typeutil.Float32ArrayToFloat16Bytes)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_FloatOrByteVectors_Fp32_UnmashalTwice(b *testing.B) {
|
||||
vectorsJSON := generateVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeFloatOrByteVectorsUnmarshalTwice(vectorsJSON.Raw, schemapb.DataType_Float16Vector, 128, typeutil.Float32ArrayToFloat16Bytes)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_FloatOrByteVectors_Byte_UnmashalTwice(b *testing.B) {
|
||||
vectorsJSON := generateByteVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeFloatOrByteVectorsUnmarshalTwice(vectorsJSON.Raw, schemapb.DataType_Float16Vector, 256, typeutil.Float32ArrayToFloat16Bytes)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSerialize_ByteVectors(b *testing.B) {
|
||||
vectorsJSON := generateByteVectorsJSON()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := serializeByteVectors(vectorsJSON.Raw, schemapb.DataType_BinaryVector, -1, 512)
|
||||
assert.Nil(b, err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,518 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
||||
"github.com/milvus-io/milvus/internal/json"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
||||
)
|
||||
|
||||
// We wrap original protobuf structure for 2 reasons:
|
||||
// 1. Milvus uses `bytes` as the type of `schema` field,
|
||||
// while the bytes has to be serialized by proto.Marshal.
|
||||
// It's very inconvenient for an HTTP clien to do this,
|
||||
// so we change the type to a struct,
|
||||
// and does the conversion for user.
|
||||
// 2. Some fields uses proto.oneof, does not supported directly json marshal
|
||||
// so we have to implements the marshal procedure. example: InsertReqeust
|
||||
|
||||
// WrappedCreateCollectionRequest wraps CreateCollectionRequest
|
||||
type WrappedCreateCollectionRequest struct {
|
||||
// Not useful for now
|
||||
Base *commonpb.MsgBase `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
// Not useful for now
|
||||
DbName string `protobuf:"bytes,2,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"`
|
||||
// The unique collection name in milvus.(Required)
|
||||
CollectionName string `protobuf:"bytes,3,opt,name=collection_name,json=collectionName,proto3" json:"collection_name,omitempty"`
|
||||
// The serialized `schema.CollectionSchema`(Required)
|
||||
Schema schemapb.CollectionSchema `protobuf:"bytes,4,opt,name=schema,proto3" json:"schema,omitempty"`
|
||||
// Once set, no modification is allowed (Optional)
|
||||
// https://github.com/milvus-io/milvus/issues/6690
|
||||
ShardsNum int32 `protobuf:"varint,5,opt,name=shards_num,json=shardsNum,proto3" json:"shards_num,omitempty"`
|
||||
// The consistency level that the collection used, modification is not supported now.
|
||||
ConsistencyLevel commonpb.ConsistencyLevel `protobuf:"varint,6,opt,name=consistency_level,json=consistencyLevel,proto3,enum=milvus.proto.common.ConsistencyLevel" json:"consistency_level,omitempty"`
|
||||
Properties []*commonpb.KeyValuePair `protobuf:"bytes,13,rep,name=properties,proto3" json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// WrappedInsertRequest is the InsertRequest wrapped for RESTful request
|
||||
type WrappedInsertRequest struct {
|
||||
Base *commonpb.MsgBase `json:"base,omitempty"`
|
||||
DbName string `json:"db_name,omitempty"`
|
||||
CollectionName string `json:"collection_name,omitempty"`
|
||||
PartitionName string `json:"partition_name,omitempty"`
|
||||
FieldsData []*FieldData `json:"fields_data,omitempty"`
|
||||
HashKeys []uint32 `json:"hash_keys,omitempty"`
|
||||
NumRows uint32 `json:"num_rows,omitempty"`
|
||||
}
|
||||
|
||||
func (w *WrappedInsertRequest) AsInsertRequest() (*milvuspb.InsertRequest, error) {
|
||||
fieldData, err := convertFieldDataArray(w.FieldsData)
|
||||
if err != nil {
|
||||
return nil, badRequestf(err, "convert field data failed")
|
||||
}
|
||||
return &milvuspb.InsertRequest{
|
||||
Base: w.Base,
|
||||
DbName: w.DbName,
|
||||
CollectionName: w.CollectionName,
|
||||
PartitionName: w.PartitionName,
|
||||
FieldsData: fieldData,
|
||||
HashKeys: w.HashKeys,
|
||||
NumRows: w.NumRows,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FieldData is the field data in RESTful request that can be convertd to schemapb.FieldData
|
||||
type FieldData struct {
|
||||
Type schemapb.DataType `json:"type,omitempty"`
|
||||
FieldName string `json:"field_name,omitempty"`
|
||||
Field json.RawMessage `json:"field,omitempty"` // we use postpone the unmarshal until we know the type
|
||||
FieldID int64 `json:"field_id,omitempty"`
|
||||
}
|
||||
|
||||
func (f *FieldData) makePbFloat16OrBfloat16Array(raw json.RawMessage, serializeFunc func([]float32) []byte) ([]byte, int64, error) {
|
||||
wrappedData := [][]float32{}
|
||||
err := json.Unmarshal(raw, &wrappedData)
|
||||
if err != nil {
|
||||
return nil, 0, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
if len(wrappedData) < 1 {
|
||||
return nil, 0, merr.WrapErrParameterInvalidMsg("at least one row for insert")
|
||||
}
|
||||
array0 := wrappedData[0]
|
||||
dim := len(array0)
|
||||
if dim < 1 {
|
||||
return nil, 0, merr.WrapErrParameterInvalidMsg("dim must >= 1")
|
||||
}
|
||||
data := make([]byte, 0, len(wrappedData)*dim*2)
|
||||
for _, fp32Array := range wrappedData {
|
||||
data = append(data, serializeFunc(fp32Array)...)
|
||||
}
|
||||
return data, int64(dim), nil
|
||||
}
|
||||
|
||||
// AsSchemapb converts the FieldData to schemapb.FieldData
|
||||
func (f *FieldData) AsSchemapb() (*schemapb.FieldData, error) {
|
||||
// is scarlar
|
||||
ret := schemapb.FieldData{
|
||||
Type: f.Type,
|
||||
FieldName: f.FieldName,
|
||||
FieldId: f.FieldID,
|
||||
}
|
||||
|
||||
raw := f.Field
|
||||
switch f.Type {
|
||||
case schemapb.DataType_Bool:
|
||||
data := []bool{}
|
||||
err := json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_BoolData{
|
||||
BoolData: &schemapb.BoolArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_VarChar:
|
||||
data := []string{}
|
||||
err := json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_StringData{
|
||||
StringData: &schemapb.StringArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_Int8, schemapb.DataType_Int16, schemapb.DataType_Int32:
|
||||
data := []int32{}
|
||||
err := json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_IntData{
|
||||
IntData: &schemapb.IntArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_Int64:
|
||||
data := []int64{}
|
||||
err := json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_LongData{
|
||||
LongData: &schemapb.LongArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_Float:
|
||||
data := []float32{}
|
||||
err := json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_FloatData{
|
||||
FloatData: &schemapb.FloatArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
case schemapb.DataType_Double:
|
||||
data := []float64{}
|
||||
err := json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_DoubleData{
|
||||
DoubleData: &schemapb.DoubleArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
case schemapb.DataType_Timestamptz:
|
||||
data := []int64{}
|
||||
err := json.Unmarshal(raw, &data)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_TimestamptzData{
|
||||
TimestamptzData: &schemapb.TimestamptzArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
case schemapb.DataType_FloatVector:
|
||||
wrappedData := [][]float32{}
|
||||
err := json.Unmarshal(raw, &wrappedData)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
if len(wrappedData) < 1 {
|
||||
return nil, merr.WrapErrParameterInvalidMsg("at least one row for insert")
|
||||
}
|
||||
array0 := wrappedData[0]
|
||||
dim := len(array0)
|
||||
if dim < 1 {
|
||||
return nil, merr.WrapErrParameterInvalidMsg("dim must >= 1")
|
||||
}
|
||||
data := make([]float32, len(wrappedData)*dim)
|
||||
|
||||
var i int
|
||||
for _, dataArray := range wrappedData {
|
||||
for _, v := range dataArray {
|
||||
data[i] = v
|
||||
i++
|
||||
}
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Vectors{
|
||||
Vectors: &schemapb.VectorField{
|
||||
Dim: int64(dim),
|
||||
Data: &schemapb.VectorField_FloatVector{
|
||||
FloatVector: &schemapb.FloatArray{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_Float16Vector:
|
||||
// only support float32 conversion right now
|
||||
data, dim, err := f.makePbFloat16OrBfloat16Array(raw, typeutil.Float32ArrayToFloat16Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Vectors{
|
||||
Vectors: &schemapb.VectorField{
|
||||
Dim: dim,
|
||||
Data: &schemapb.VectorField_Float16Vector{
|
||||
Float16Vector: data,
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_BFloat16Vector:
|
||||
// only support float32 conversion right now
|
||||
data, dim, err := f.makePbFloat16OrBfloat16Array(raw, typeutil.Float32ArrayToBFloat16Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Vectors{
|
||||
Vectors: &schemapb.VectorField{
|
||||
Dim: dim,
|
||||
Data: &schemapb.VectorField_Bfloat16Vector{
|
||||
Bfloat16Vector: data,
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_SparseFloatVector:
|
||||
var wrappedData []map[string]interface{}
|
||||
err := json.Unmarshal(raw, &wrappedData)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
if len(wrappedData) < 1 {
|
||||
return nil, merr.WrapErrParameterInvalidMsg("at least one row for insert")
|
||||
}
|
||||
data := make([][]byte, len(wrappedData))
|
||||
dim := int64(0)
|
||||
for _, row := range wrappedData {
|
||||
rowData, err := typeutil.CreateSparseFloatRowFromMap(row)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
data = append(data, rowData)
|
||||
rowDim := typeutil.SparseFloatRowDim(rowData)
|
||||
if rowDim > dim {
|
||||
dim = rowDim
|
||||
}
|
||||
}
|
||||
|
||||
ret.Field = &schemapb.FieldData_Vectors{
|
||||
Vectors: &schemapb.VectorField{
|
||||
Dim: dim,
|
||||
Data: &schemapb.VectorField_SparseFloatVector{
|
||||
SparseFloatVector: &schemapb.SparseFloatArray{
|
||||
Dim: dim,
|
||||
Contents: data,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case schemapb.DataType_Int8Vector:
|
||||
wrappedData := [][]int8{}
|
||||
err := json.Unmarshal(raw, &wrappedData)
|
||||
if err != nil {
|
||||
return nil, newFieldDataError(f.FieldName, err)
|
||||
}
|
||||
if len(wrappedData) < 1 {
|
||||
return nil, merr.WrapErrParameterInvalidMsg("at least one row for insert")
|
||||
}
|
||||
array0 := wrappedData[0]
|
||||
dim := len(array0)
|
||||
if dim < 1 {
|
||||
return nil, merr.WrapErrParameterInvalidMsg("dim must >= 1")
|
||||
}
|
||||
data := make([]byte, len(wrappedData)*dim)
|
||||
|
||||
var i int
|
||||
for _, dataArray := range wrappedData {
|
||||
for _, v := range dataArray {
|
||||
data[i] = byte(v)
|
||||
i++
|
||||
}
|
||||
}
|
||||
ret.Field = &schemapb.FieldData_Vectors{
|
||||
Vectors: &schemapb.VectorField{
|
||||
Dim: int64(dim),
|
||||
Data: &schemapb.VectorField_Int8Vector{
|
||||
Int8Vector: data,
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
return nil, merr.WrapErrParameterInvalidMsg("unsupported data type")
|
||||
}
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func newFieldDataError(field string, err error) error {
|
||||
return merr.WrapErrParameterInvalidErr(err, "parse field[%s]", field)
|
||||
}
|
||||
|
||||
func convertFieldDataArray(input []*FieldData) ([]*schemapb.FieldData, error) {
|
||||
ret := make([]*schemapb.FieldData, len(input))
|
||||
for i, v := range input {
|
||||
fieldData, err := v.AsSchemapb()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[i] = fieldData
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// SearchRequest is the RESTful request body for search
|
||||
type SearchRequest struct {
|
||||
Base *commonpb.MsgBase `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
DbName string `protobuf:"bytes,2,opt,name=db_name,json=dbName,proto3" json:"db_name,omitempty"`
|
||||
CollectionName string `protobuf:"bytes,3,opt,name=collection_name,json=collectionName,proto3" json:"collection_name,omitempty"`
|
||||
PartitionNames []string `protobuf:"bytes,4,rep,name=partition_names,json=partitionNames,proto3" json:"partition_names,omitempty"`
|
||||
Dsl string `protobuf:"bytes,5,opt,name=dsl,proto3" json:"dsl,omitempty"`
|
||||
DslType commonpb.DslType `protobuf:"varint,7,opt,name=dsl_type,json=dslType,proto3,enum=milvus.proto.common.DslType" json:"dsl_type,omitempty"`
|
||||
BinaryVectors [][]byte `json:"binary_vectors,omitempty"`
|
||||
Vectors [][]float32 `json:"vectors,omitempty"`
|
||||
OutputFields []string `protobuf:"bytes,8,rep,name=output_fields,json=outputFields,proto3" json:"output_fields,omitempty"`
|
||||
SearchParams []*commonpb.KeyValuePair `protobuf:"bytes,9,rep,name=search_params,json=searchParams,proto3" json:"search_params,omitempty"`
|
||||
TravelTimestamp uint64 `protobuf:"varint,10,opt,name=travel_timestamp,json=travelTimestamp,proto3" json:"travel_timestamp,omitempty"`
|
||||
GuaranteeTimestamp uint64 `protobuf:"varint,11,opt,name=guarantee_timestamp,json=guaranteeTimestamp,proto3" json:"guarantee_timestamp,omitempty"`
|
||||
Nq int64 `protobuf:"varint,12,opt,name=nq,proto3" json:"nq,omitempty"`
|
||||
SearchAggregation *SearchAggregationReq `json:"searchAggregation,omitempty"`
|
||||
SearchAggregationSnake *SearchAggregationReq `json:"search_aggregation,omitempty"`
|
||||
}
|
||||
|
||||
func (r *SearchRequest) HasSearchAggregation() bool {
|
||||
return r.SearchAggregation != nil || r.SearchAggregationSnake != nil
|
||||
}
|
||||
|
||||
func binaryVector2Bytes(vectors [][]byte) []byte {
|
||||
ph := &commonpb.PlaceholderValue{
|
||||
Tag: "$0",
|
||||
Type: commonpb.PlaceholderType_BinaryVector,
|
||||
Values: make([][]byte, 0, len(vectors)),
|
||||
}
|
||||
ph.Values = append(ph.Values, vectors...)
|
||||
phg := &commonpb.PlaceholderGroup{
|
||||
Placeholders: []*commonpb.PlaceholderValue{
|
||||
ph,
|
||||
},
|
||||
}
|
||||
ret, _ := proto.Marshal(phg)
|
||||
return ret
|
||||
}
|
||||
|
||||
func vector2Bytes(vectors [][]float32) []byte {
|
||||
ph := &commonpb.PlaceholderValue{
|
||||
Tag: "$0",
|
||||
Type: commonpb.PlaceholderType_FloatVector,
|
||||
Values: make([][]byte, 0, len(vectors)),
|
||||
}
|
||||
for _, vector := range vectors {
|
||||
ph.Values = append(ph.Values, typeutil.Float32ArrayToBytes(vector))
|
||||
}
|
||||
phg := &commonpb.PlaceholderGroup{
|
||||
Placeholders: []*commonpb.PlaceholderValue{
|
||||
ph,
|
||||
},
|
||||
}
|
||||
ret, _ := proto.Marshal(phg)
|
||||
return ret
|
||||
}
|
||||
|
||||
// WrappedCalcDistanceRequest is the RESTful request body for calc distance
|
||||
type WrappedCalcDistanceRequest struct {
|
||||
Base *commonpb.MsgBase `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
|
||||
OpLeft VectorsArray `json:"op_left,omitempty"`
|
||||
OpRight VectorsArray `json:"op_right,omitempty"`
|
||||
|
||||
Params []*commonpb.KeyValuePair `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// VectorsArray is vector array, assigned by vectors or ids
|
||||
type VectorsArray struct {
|
||||
// Dim of vectors or binary_vectors, not needed when use ids
|
||||
Dim int64 `json:"dim,omitempty"`
|
||||
// Vectors is an array of vector divided by given dim. Disabled when ids or binary_vectors is set
|
||||
Vectors []float32 `json:"vectors,omitempty"`
|
||||
// Vectors is an array of binary vector divided by given dim. Disabled when IDs is set
|
||||
BinaryVectors []byte `json:"binary_vectors,omitempty"`
|
||||
// IDs of vector field in milvus, if not nil, vectors will be ignored
|
||||
IDs *VectorIDs `json:"ids,omitempty"`
|
||||
}
|
||||
|
||||
func (v *VectorsArray) isIDs() bool {
|
||||
return v.IDs != nil
|
||||
}
|
||||
|
||||
func (v *VectorsArray) isBinaryVector() bool {
|
||||
return v.IDs == nil && len(v.BinaryVectors) > 0
|
||||
}
|
||||
|
||||
// AsPbVectorArray convert as milvuspb.VectorArray
|
||||
func (v *VectorsArray) AsPbVectorArray() *milvuspb.VectorsArray {
|
||||
ret := &milvuspb.VectorsArray{}
|
||||
switch {
|
||||
case v.isIDs():
|
||||
ids := &milvuspb.VectorsArray_IdArray{}
|
||||
ids.IdArray = &milvuspb.VectorIDs{
|
||||
CollectionName: v.IDs.CollectionName,
|
||||
FieldName: v.IDs.FieldName,
|
||||
}
|
||||
ids.IdArray.PartitionNames = v.IDs.PartitionNames
|
||||
ids.IdArray.IdArray = &schemapb.IDs{}
|
||||
ids.IdArray.IdArray.IdField = &schemapb.IDs_IntId{
|
||||
IntId: &schemapb.LongArray{
|
||||
Data: v.IDs.IDArray,
|
||||
},
|
||||
}
|
||||
ret.Array = ids
|
||||
case v.isBinaryVector():
|
||||
vf := &schemapb.VectorField{
|
||||
Dim: v.Dim,
|
||||
}
|
||||
vf.Data = &schemapb.VectorField_BinaryVector{
|
||||
BinaryVector: v.BinaryVectors,
|
||||
}
|
||||
ret.Array = &milvuspb.VectorsArray_DataArray{
|
||||
DataArray: vf,
|
||||
}
|
||||
default:
|
||||
// take it as ordinary vectors
|
||||
vf := &schemapb.VectorField{
|
||||
Dim: v.Dim,
|
||||
}
|
||||
vf.Data = &schemapb.VectorField_FloatVector{
|
||||
FloatVector: &schemapb.FloatArray{
|
||||
Data: v.Vectors,
|
||||
},
|
||||
}
|
||||
ret.Array = &milvuspb.VectorsArray_DataArray{
|
||||
DataArray: vf,
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// VectorIDs is an array of id reference in milvus
|
||||
type VectorIDs struct {
|
||||
CollectionName string `protobuf:"bytes,1,opt,name=collection_name,json=collectionName,proto3" json:"collection_name,omitempty"`
|
||||
FieldName string `protobuf:"bytes,2,opt,name=field_name,json=fieldName,proto3" json:"field_name,omitempty"`
|
||||
PartitionNames []string `json:"partition_names"`
|
||||
IDArray []int64 `json:"id_array,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
||||
"github.com/milvus-io/milvus/internal/json"
|
||||
)
|
||||
|
||||
func TestFieldData_AsSchemapb(t *testing.T) {
|
||||
t.Run("varchar_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_VarChar,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("varchar_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_VarChar,
|
||||
Field: []byte("[1, 2, 3]"),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("bool_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Bool,
|
||||
Field: []byte("[true, true, false]"),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("bool_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Bool,
|
||||
Field: []byte("[1, 2, 3]"),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("int8_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int8,
|
||||
Field: []byte("[1, 2, 3]"),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("int8_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int8,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("int32_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int32,
|
||||
Field: []byte("[1, 2, 3]"),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("int32_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int32,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("int64_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int64,
|
||||
Field: []byte("[1, 2, 3]"),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("int64_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int64,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("float_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Float,
|
||||
Field: []byte(`[1.1, 2.1, 3.1]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("float_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Float,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("double_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Double,
|
||||
Field: []byte(`[1.1, 2.1, 3.1]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("double_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Double,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("string_not_support", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_String,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
// vectors
|
||||
testcases := []struct {
|
||||
name string
|
||||
dataType schemapb.DataType
|
||||
}{
|
||||
{
|
||||
"float", schemapb.DataType_FloatVector,
|
||||
},
|
||||
{
|
||||
"float16", schemapb.DataType_Float16Vector,
|
||||
},
|
||||
{
|
||||
"bfloat16", schemapb.DataType_BFloat16Vector,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name+"vector_ok", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: tc.dataType,
|
||||
Field: []byte(`[
|
||||
[1.1, 2.2, 3.1],
|
||||
[1.1, 2.2, 3.1],
|
||||
[1.1, 2.2, 3.1]
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run(tc.name+"vector_empty_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: tc.dataType,
|
||||
Field: []byte(""),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run(tc.name+"vector_dim=0_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: tc.dataType,
|
||||
Field: []byte(`[]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run(tc.name+"vector_vectorTypeError_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: tc.dataType,
|
||||
Field: []byte(`["1"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run(tc.name+"vector_error", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: tc.dataType,
|
||||
Field: []byte(`["a", "b", "c"]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("sparsefloatvector_ok_1", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_SparseFloatVector,
|
||||
Field: []byte(`[
|
||||
{"1": 0.1, "2": 0.2},
|
||||
{"3": 0.1, "5": 0.2},
|
||||
{"4": 0.1, "6": 0.2}
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("sparsefloatvector_ok_2", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_SparseFloatVector,
|
||||
Field: []byte(`[
|
||||
{"indices": [1, 2], "values": [0.1, 0.2]},
|
||||
{"indices": [3, 5], "values": [0.1, 0.2]},
|
||||
{"indices": [4, 6], "values": [0.1, 0.2]}
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("sparsefloatvector_ok_3", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_SparseFloatVector,
|
||||
Field: []byte(`[
|
||||
{"indices": [1, 2], "values": [0.1, 0.2]},
|
||||
{"3": 0.1, "5": 0.2},
|
||||
{"indices": [4, 6], "values": [0.1, 0.2]}
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("sparsefloatvector_empty_err", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_SparseFloatVector,
|
||||
Field: []byte(`[]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("sparsefloatvector_invalid_json_err", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_SparseFloatVector,
|
||||
Field: []byte(`[
|
||||
{"3": 0.1, : 0.2}
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("sparsefloatvector_invalid_row_1_err", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_SparseFloatVector,
|
||||
Field: []byte(`[
|
||||
{"indices": [1, 2], "values": [-0.1, 0.2]},
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("sparsefloatvector_invalid_row_2_err", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_SparseFloatVector,
|
||||
Field: []byte(`[
|
||||
{"indices": [1, -2], "values": [0.1, 0.2]},
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("int8vector_ok_1", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int8Vector,
|
||||
Field: []byte(`[
|
||||
[1, 2, 3, 4],
|
||||
[-11, -52, 37, 121],
|
||||
[-128, -35, 31, 127]
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("int8vector_ok_1", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int8Vector,
|
||||
Field: []byte(`[
|
||||
[-200, 141]
|
||||
]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("int8vector_empty_err", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int8Vector,
|
||||
Field: []byte(""),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("int8vector_dim0_err", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int8Vector,
|
||||
Field: []byte(`[]`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("int8vector_datatype_err", func(t *testing.T) {
|
||||
fieldData := FieldData{
|
||||
Type: schemapb.DataType_Int8Vector,
|
||||
Field: []byte(`['a', 'b', 'c']`),
|
||||
}
|
||||
raw, _ := json.Marshal(fieldData)
|
||||
json.Unmarshal(raw, &fieldData)
|
||||
_, err := fieldData.AsSchemapb()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_vector2Bytes(t *testing.T) {
|
||||
ret := vector2Bytes([][]float32{{1.1, 1.2}})
|
||||
assert.NotEmpty(t, ret)
|
||||
}
|
||||
|
||||
func Test_binaryVector2Bytes(t *testing.T) {
|
||||
ret := binaryVector2Bytes([][]byte{
|
||||
[]byte("somebytes"),
|
||||
})
|
||||
assert.NotEmpty(t, ret)
|
||||
}
|
||||
|
||||
func TestVectorsArray_AsPbVectorArray(t *testing.T) {
|
||||
dim := int64(1)
|
||||
t.Run("vector_ok", func(t *testing.T) {
|
||||
vector := []float32{1, 2}
|
||||
v := VectorsArray{
|
||||
Dim: dim,
|
||||
Vectors: vector,
|
||||
}
|
||||
ret := v.AsPbVectorArray()
|
||||
da, ok := ret.Array.(*milvuspb.VectorsArray_DataArray)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, dim, da.DataArray.Dim)
|
||||
assert.Equal(t, vector, da.DataArray.GetFloatVector().Data)
|
||||
})
|
||||
t.Run("binary_vector_ok", func(t *testing.T) {
|
||||
bv := []byte("somebytes")
|
||||
v := VectorsArray{
|
||||
// IDs: ,
|
||||
Dim: dim,
|
||||
BinaryVectors: bv,
|
||||
}
|
||||
ret := v.AsPbVectorArray()
|
||||
da, ok := ret.Array.(*milvuspb.VectorsArray_DataArray)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, dim, da.DataArray.Dim)
|
||||
assert.Equal(t, bv, da.DataArray.GetBinaryVector())
|
||||
})
|
||||
t.Run("ids_ok", func(t *testing.T) {
|
||||
ids := []int64{1, 2, 3}
|
||||
cn := "collection"
|
||||
paritions := []string{"p1", "p2"}
|
||||
field := "field"
|
||||
v := VectorsArray{
|
||||
IDs: &VectorIDs{
|
||||
CollectionName: cn,
|
||||
PartitionNames: paritions,
|
||||
FieldName: field,
|
||||
IDArray: ids,
|
||||
},
|
||||
}
|
||||
ret := v.AsPbVectorArray()
|
||||
ia, ok := ret.Array.(*milvuspb.VectorsArray_IdArray)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, cn, ia.IdArray.CollectionName)
|
||||
assert.Equal(t, paritions, ia.IdArray.PartitionNames)
|
||||
assert.Equal(t, field, ia.IdArray.FieldName)
|
||||
ints, ok := ia.IdArray.IdArray.IdField.(*schemapb.IDs_IntId)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, ids, ints.IntId.Data)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
)
|
||||
|
||||
var errBadRequest = errors.New("bad request")
|
||||
|
||||
// badRequestf wraps err with the package-internal errBadRequest sentinel and
|
||||
// a contextual message, preserving the inner error chain. wrapHandler maps
|
||||
// any error that matches errBadRequest to HTTP 400.
|
||||
func badRequestf(err error, format string, args ...any) error {
|
||||
return merr.Mark(merr.Wrapf(err, format, args...), errBadRequest)
|
||||
}
|
||||
|
||||
// handlerFunc handles http request with gin context
|
||||
type handlerFunc func(c *gin.Context) (interface{}, error)
|
||||
|
||||
// ErrResponse of server
|
||||
type ErrResponse = commonpb.Status
|
||||
|
||||
// wrapHandler wraps a handlerFunc into a gin.HandlerFunc
|
||||
func wrapHandler(handle handlerFunc) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
data, err := handle(c)
|
||||
// format body by accept header, protobuf marshal not supported by gin by default
|
||||
// TODO: add marshal handler to support protobuf response
|
||||
formatOffered := []string{binding.MIMEJSON, binding.MIMEYAML}
|
||||
bodyFormatNegotiate := gin.Negotiate{
|
||||
Offered: formatOffered,
|
||||
Data: data,
|
||||
}
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, errBadRequest):
|
||||
bodyFormatNegotiate.Data = ErrResponse{
|
||||
ErrorCode: commonpb.ErrorCode_IllegalArgument,
|
||||
Reason: err.Error(),
|
||||
}
|
||||
c.Negotiate(http.StatusBadRequest, bodyFormatNegotiate)
|
||||
return
|
||||
default:
|
||||
bodyFormatNegotiate.Data = ErrResponse{
|
||||
ErrorCode: commonpb.ErrorCode_UnexpectedError,
|
||||
Reason: err.Error(),
|
||||
}
|
||||
c.Negotiate(http.StatusInternalServerError, bodyFormatNegotiate)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Negotiate(http.StatusOK, bodyFormatNegotiate)
|
||||
}
|
||||
}
|
||||
|
||||
// gin.ShouldBind() default as `form`, but we want JSON
|
||||
func shouldBind(c *gin.Context, obj interface{}) error {
|
||||
b := getBinding(c.ContentType())
|
||||
err := c.ShouldBindWith(obj, b)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getBinding(contentType string) binding.Binding {
|
||||
// ref: binding.Default
|
||||
switch contentType {
|
||||
case binding.MIMEYAML:
|
||||
return binding.YAML
|
||||
default:
|
||||
return binding.JSON
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Licensed to the LF AI & Data foundation under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWrapHandler(t *testing.T) {
|
||||
testWrapFunc := func(c *gin.Context) (interface{}, error) {
|
||||
Case := c.Param("case")
|
||||
switch Case {
|
||||
case "0":
|
||||
return gin.H{"status": "ok"}, nil
|
||||
case "1":
|
||||
return nil, errBadRequest
|
||||
case "2":
|
||||
return nil, errors.New("internal err")
|
||||
}
|
||||
panic("shall not reach")
|
||||
}
|
||||
wrappedHandler := wrapHandler(testWrapFunc)
|
||||
testEngine := gin.New()
|
||||
testEngine.GET("/test/:case", wrappedHandler)
|
||||
|
||||
t.Run("status ok", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test/0?verbose=false", nil)
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
|
||||
t.Run("err notfound", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
|
||||
t.Run("err bad request", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test/1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
})
|
||||
|
||||
t.Run("err internal", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test/2", nil)
|
||||
w := httptest.NewRecorder()
|
||||
testEngine.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user