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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:17 +08:00
commit 498b235461
5446 changed files with 2748612 additions and 0 deletions
@@ -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)
})
}