mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-20 09:45:43 +00:00
enhance: add delete/shard by namespace (#50153)
Shard data by namespace. issue: #50154 --------- Signed-off-by: sunby <sunbingyi1992@gmail.com>
This commit is contained in:
@@ -289,6 +289,10 @@ cd ../../../
|
||||
make unittest
|
||||
```
|
||||
|
||||
The Apple Silicon compose file uses the Milvus Harbor Docker Hub proxy for
|
||||
Pulsar by default. To pull Pulsar directly from Docker Hub instead, set
|
||||
`PULSAR_IMAGE=milvusdb/pulsar:v2.8.2-m1`.
|
||||
|
||||
For others:
|
||||
|
||||
```shell
|
||||
|
||||
@@ -70,6 +70,11 @@ func (opt *searchIteratorOption) WithPartitions(partitionNames ...string) *searc
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *searchIteratorOption) WithNamespace(namespace string) *searchIteratorOption {
|
||||
opt.searchOption.WithNamespace(namespace)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *searchIteratorOption) WithFilter(expr string) *searchIteratorOption {
|
||||
opt.annRequest.WithFilter(expr)
|
||||
return opt
|
||||
@@ -166,6 +171,7 @@ type QueryIteratorOption interface {
|
||||
type queryIteratorOption struct {
|
||||
collectionName string
|
||||
partitionNames []string
|
||||
namespace *string
|
||||
outputFields []string
|
||||
expr string
|
||||
batchSize int
|
||||
@@ -178,6 +184,7 @@ func (opt *queryIteratorOption) Request() (*milvuspb.QueryRequest, error) {
|
||||
return &milvuspb.QueryRequest{
|
||||
CollectionName: opt.collectionName,
|
||||
PartitionNames: opt.partitionNames,
|
||||
Namespace: opt.namespace,
|
||||
OutputFields: opt.outputFields,
|
||||
Expr: opt.expr,
|
||||
ConsistencyLevel: opt.consistencyLevel.CommonConsistencyLevel(),
|
||||
@@ -211,6 +218,11 @@ func (opt *queryIteratorOption) WithPartitions(partitionNames ...string) *queryI
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *queryIteratorOption) WithNamespace(namespace string) *queryIteratorOption {
|
||||
opt.namespace = &namespace
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *queryIteratorOption) WithFilter(expr string) *queryIteratorOption {
|
||||
opt.expr = expr
|
||||
return opt
|
||||
|
||||
@@ -47,6 +47,18 @@ func (s *SearchIteratorSuite) SetupSuite() {
|
||||
WithField(entity.NewField().WithName("Vector").WithDataType(entity.FieldTypeFloatVector).WithDim(128))
|
||||
}
|
||||
|
||||
func (s *SearchIteratorSuite) TestSearchIteratorOptionWithNamespace() {
|
||||
namespace := "tenant_a"
|
||||
|
||||
opt := NewSearchIteratorOption("coll", entity.FloatVector(lo.RepeatBy(128, func(_ int) float32 {
|
||||
return rand.Float32()
|
||||
}))).WithNamespace(namespace)
|
||||
req, err := opt.SearchOption().Request()
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Equal(namespace, req.GetNamespace())
|
||||
}
|
||||
|
||||
func (s *SearchIteratorSuite) TestSearchIteratorInit() {
|
||||
ctx := context.Background()
|
||||
s.Run("success", func() {
|
||||
@@ -431,6 +443,15 @@ func (s *QueryIteratorSuite) SetupSuite() {
|
||||
WithField(entity.NewField().WithName("Name").WithDataType(entity.FieldTypeVarChar).WithMaxLength(256))
|
||||
}
|
||||
|
||||
func (s *QueryIteratorSuite) TestQueryIteratorOptionWithNamespace() {
|
||||
namespace := "tenant_a"
|
||||
|
||||
req, err := NewQueryIteratorOption("coll").WithNamespace(namespace).Request()
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Equal(namespace, req.GetNamespace())
|
||||
}
|
||||
|
||||
func (s *QueryIteratorSuite) TestQueryIteratorInit() {
|
||||
ctx := context.Background()
|
||||
s.Run("success", func() {
|
||||
|
||||
@@ -90,6 +90,29 @@ func (s *SearchOptionSuite) TestBasic() {
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
func (s *SearchOptionSuite) TestWithNamespace() {
|
||||
collName := "namespace_read_option"
|
||||
namespace := "tenant_a"
|
||||
|
||||
searchReq, err := NewSearchOption(collName, 10, []entity.Vector{entity.FloatVector([]float32{0.1, 0.2})}).
|
||||
WithNamespace(namespace).
|
||||
Request()
|
||||
s.Require().NoError(err)
|
||||
s.Equal(namespace, searchReq.GetNamespace())
|
||||
|
||||
queryReq, err := NewQueryOption(collName).
|
||||
WithNamespace(namespace).
|
||||
Request()
|
||||
s.Require().NoError(err)
|
||||
s.Equal(namespace, queryReq.GetNamespace())
|
||||
|
||||
hybridReq, err := NewHybridSearchOption(collName, 10, NewAnnRequest("vector", 10, entity.FloatVector([]float32{0.1, 0.2}))).
|
||||
WithNamespace(namespace).
|
||||
HybridRequest()
|
||||
s.Require().NoError(err)
|
||||
s.Equal(namespace, hybridReq.GetNamespace())
|
||||
}
|
||||
|
||||
func (s *SearchOptionSuite) TestPlaceHolder() {
|
||||
type testCase struct {
|
||||
tag string
|
||||
|
||||
@@ -59,6 +59,7 @@ type searchOption struct {
|
||||
annRequest *AnnRequest
|
||||
collectionName string
|
||||
partitionNames []string
|
||||
namespace *string
|
||||
outputFields []string
|
||||
searchAggregation *SearchAggregation
|
||||
consistencyLevel entity.ConsistencyLevel
|
||||
@@ -333,6 +334,7 @@ func (opt *searchOption) Request() (*milvuspb.SearchRequest, error) {
|
||||
|
||||
request.CollectionName = opt.collectionName
|
||||
request.PartitionNames = opt.partitionNames
|
||||
request.Namespace = opt.namespace
|
||||
request.ConsistencyLevel = commonpb.ConsistencyLevel(opt.consistencyLevel)
|
||||
request.UseDefaultConsistency = opt.useDefaultConsistencyLevel
|
||||
request.OutputFields = opt.outputFields
|
||||
@@ -367,6 +369,11 @@ func (opt *searchOption) WithPartitions(partitionNames ...string) *searchOption
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *searchOption) WithNamespace(namespace string) *searchOption {
|
||||
opt.namespace = &namespace
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *searchOption) WithFilter(expr string) *searchOption {
|
||||
opt.annRequest.WithFilter(expr)
|
||||
return opt
|
||||
@@ -525,6 +532,7 @@ type HybridSearchOption interface {
|
||||
type hybridSearchOption struct {
|
||||
collectionName string
|
||||
partitionNames []string
|
||||
namespace *string
|
||||
|
||||
reqs []*AnnRequest
|
||||
|
||||
@@ -554,6 +562,11 @@ func (opt *hybridSearchOption) WithPartitions(partitions ...string) *hybridSearc
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *hybridSearchOption) WithNamespace(namespace string) *hybridSearchOption {
|
||||
opt.namespace = &namespace
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *hybridSearchOption) WithOutputFields(outputFields ...string) *hybridSearchOption {
|
||||
opt.outputFields = outputFields
|
||||
return opt
|
||||
@@ -596,6 +609,7 @@ func (opt *hybridSearchOption) HybridRequest() (*milvuspb.HybridSearchRequest, e
|
||||
r := &milvuspb.HybridSearchRequest{
|
||||
CollectionName: opt.collectionName,
|
||||
PartitionNames: opt.partitionNames,
|
||||
Namespace: opt.namespace,
|
||||
Requests: requests,
|
||||
UseDefaultConsistency: opt.useDefaultConsistency,
|
||||
ConsistencyLevel: commonpb.ConsistencyLevel(opt.consistencyLevel),
|
||||
@@ -629,6 +643,7 @@ type QueryOption interface {
|
||||
type queryOption struct {
|
||||
collectionName string
|
||||
partitionNames []string
|
||||
namespace *string
|
||||
queryParams map[string]string
|
||||
outputFields []string
|
||||
consistencyLevel entity.ConsistencyLevel
|
||||
@@ -641,6 +656,7 @@ func (opt *queryOption) Request() (*milvuspb.QueryRequest, error) {
|
||||
req := &milvuspb.QueryRequest{
|
||||
CollectionName: opt.collectionName,
|
||||
PartitionNames: opt.partitionNames,
|
||||
Namespace: opt.namespace,
|
||||
OutputFields: opt.outputFields,
|
||||
|
||||
Expr: opt.expr,
|
||||
@@ -703,6 +719,11 @@ func (opt *queryOption) WithPartitions(partitionNames ...string) *queryOption {
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *queryOption) WithNamespace(namespace string) *queryOption {
|
||||
opt.namespace = &namespace
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *queryOption) WithIDs(ids column.Column) *queryOption {
|
||||
opt.expr = pks2Expr(ids)
|
||||
return opt
|
||||
|
||||
@@ -195,6 +195,59 @@ func (s *ColumnBasedDataOptionSuite) TestNewStructSubColumnErrors() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ColumnBasedDataOptionSuite) TestWithNamespace() {
|
||||
collName := "namespace_write_option"
|
||||
namespace := "tenant_a"
|
||||
coll := &entity.Collection{
|
||||
Schema: entity.NewSchema().WithName(collName).
|
||||
WithField(entity.NewField().WithName("id").WithDataType(entity.FieldTypeInt64)),
|
||||
}
|
||||
|
||||
insertOpt := NewColumnBasedInsertOption(collName, column.NewColumnInt64("id", []int64{1})).
|
||||
WithNamespace(namespace)
|
||||
insertReq, err := insertOpt.InsertRequest(coll)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(namespace, insertReq.GetNamespace())
|
||||
|
||||
upsertOpt := NewColumnBasedInsertOption(collName, column.NewColumnInt64("id", []int64{1})).
|
||||
WithNamespace(namespace)
|
||||
upsertReq, err := upsertOpt.UpsertRequest(coll)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(namespace, upsertReq.GetNamespace())
|
||||
}
|
||||
|
||||
func (s *ColumnBasedDataOptionSuite) TestRowBasedWithNamespaceKeepsRows() {
|
||||
collName := "namespace_row_write_option"
|
||||
namespace := "tenant_a"
|
||||
partition := "partition_a"
|
||||
coll := &entity.Collection{
|
||||
Schema: entity.NewSchema().WithName(collName).
|
||||
WithField(entity.NewField().WithName("id").WithDataType(entity.FieldTypeInt64).WithIsPrimaryKey(true)).
|
||||
WithField(entity.NewField().WithName("name").WithDataType(entity.FieldTypeVarChar).WithMaxLength(64)),
|
||||
}
|
||||
rows := []any{map[string]any{"id": int64(1), "name": "alice"}}
|
||||
|
||||
var insertOpt InsertOption = NewRowBasedInsertOption(collName, rows...).
|
||||
WithPartition(partition).
|
||||
WithNamespace(namespace)
|
||||
insertReq, err := insertOpt.InsertRequest(coll)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(partition, insertReq.GetPartitionName())
|
||||
s.Equal(namespace, insertReq.GetNamespace())
|
||||
s.EqualValues(1, insertReq.GetNumRows())
|
||||
s.Len(insertReq.GetFieldsData(), 2)
|
||||
|
||||
var upsertOpt UpsertOption = NewRowBasedInsertOption(collName, rows...).
|
||||
WithPartition(partition).
|
||||
WithNamespace(namespace)
|
||||
upsertReq, err := upsertOpt.UpsertRequest(coll)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(partition, upsertReq.GetPartitionName())
|
||||
s.Equal(namespace, upsertReq.GetNamespace())
|
||||
s.EqualValues(1, upsertReq.GetNumRows())
|
||||
s.Len(upsertReq.GetFieldsData(), 2)
|
||||
}
|
||||
|
||||
func TestRowBasedDataOption(t *testing.T) {
|
||||
suite.Run(t, new(ColumnBasedDataOptionSuite))
|
||||
}
|
||||
@@ -210,6 +263,14 @@ func (s *DeleteOptionSuite) TestBasic() {
|
||||
s.Equal(collectionName, opt.Request().GetCollectionName())
|
||||
}
|
||||
|
||||
func (s *DeleteOptionSuite) TestWithNamespace() {
|
||||
collectionName := fmt.Sprintf("coll_%s", s.randString(6))
|
||||
namespace := "tenant_a"
|
||||
|
||||
req := NewDeleteOption(collectionName).WithNamespace(namespace).Request()
|
||||
s.Equal(namespace, req.GetNamespace())
|
||||
}
|
||||
|
||||
func TestDeleteOption(t *testing.T) {
|
||||
suite.Run(t, new(DeleteOptionSuite))
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ var (
|
||||
type columnBasedDataOption struct {
|
||||
collName string
|
||||
partitionName string
|
||||
namespace *string
|
||||
columns []column.Column
|
||||
partialUpdate bool
|
||||
|
||||
@@ -379,6 +380,14 @@ func (opt *columnBasedDataOption) WithPartition(partitionName string) *columnBas
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithNamespace scopes the write to a collection namespace. Primary keys are
|
||||
// still collection-scoped for delete/upsert tombstones, so callers must keep
|
||||
// primary keys unique across namespaces in the same collection.
|
||||
func (opt *columnBasedDataOption) WithNamespace(namespace string) *columnBasedDataOption {
|
||||
opt.namespace = &namespace
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *columnBasedDataOption) WithPartialUpdate(partialUpdate bool) *columnBasedDataOption {
|
||||
opt.partialUpdate = partialUpdate
|
||||
return opt
|
||||
@@ -452,6 +461,7 @@ func (opt *columnBasedDataOption) InsertRequest(coll *entity.Collection) (*milvu
|
||||
return &milvuspb.InsertRequest{
|
||||
CollectionName: opt.collName,
|
||||
PartitionName: opt.partitionName,
|
||||
Namespace: opt.namespace,
|
||||
FieldsData: fieldsData,
|
||||
NumRows: uint32(rowNum),
|
||||
SchemaTimestamp: coll.UpdateTimestamp,
|
||||
@@ -477,6 +487,7 @@ func (opt *columnBasedDataOption) UpsertRequest(coll *entity.Collection) (*milvu
|
||||
return &milvuspb.UpsertRequest{
|
||||
CollectionName: opt.collName,
|
||||
PartitionName: opt.partitionName,
|
||||
Namespace: opt.namespace,
|
||||
FieldsData: fieldsData,
|
||||
NumRows: uint32(rowNum),
|
||||
SchemaTimestamp: coll.UpdateTimestamp,
|
||||
@@ -509,6 +520,36 @@ func NewRowBasedInsertOption(collName string, rows ...any) *rowBasedDataOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (opt *rowBasedDataOption) WithPartition(partitionName string) *rowBasedDataOption {
|
||||
opt.columnBasedDataOption.WithPartition(partitionName)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *rowBasedDataOption) WithNamespace(namespace string) *rowBasedDataOption {
|
||||
opt.columnBasedDataOption.WithNamespace(namespace)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *rowBasedDataOption) WithPartialUpdate(partialUpdate bool) *rowBasedDataOption {
|
||||
opt.columnBasedDataOption.WithPartialUpdate(partialUpdate)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *rowBasedDataOption) WithArrayAppend(fieldName string) *rowBasedDataOption {
|
||||
opt.columnBasedDataOption.WithArrayAppend(fieldName)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *rowBasedDataOption) WithArrayRemove(fieldName string) *rowBasedDataOption {
|
||||
opt.columnBasedDataOption.WithArrayRemove(fieldName)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *rowBasedDataOption) WithFieldPartialOp(fieldName string, op schemapb.FieldPartialUpdateOp_OpType) *rowBasedDataOption {
|
||||
opt.columnBasedDataOption.WithFieldPartialOp(fieldName, op)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *rowBasedDataOption) InsertRequest(coll *entity.Collection) (*milvuspb.InsertRequest, error) {
|
||||
columns, err := row.AnyToColumns(opt.rows, opt.keepAutoIDPk, coll.Schema)
|
||||
if err != nil {
|
||||
@@ -522,6 +563,7 @@ func (opt *rowBasedDataOption) InsertRequest(coll *entity.Collection) (*milvuspb
|
||||
return &milvuspb.InsertRequest{
|
||||
CollectionName: opt.collName,
|
||||
PartitionName: opt.partitionName,
|
||||
Namespace: opt.namespace,
|
||||
FieldsData: fieldsData,
|
||||
NumRows: uint32(rowNum),
|
||||
}, nil
|
||||
@@ -545,6 +587,7 @@ func (opt *rowBasedDataOption) UpsertRequest(coll *entity.Collection) (*milvuspb
|
||||
return &milvuspb.UpsertRequest{
|
||||
CollectionName: opt.collName,
|
||||
PartitionName: opt.partitionName,
|
||||
Namespace: opt.namespace,
|
||||
FieldsData: fieldsData,
|
||||
NumRows: uint32(rowNum),
|
||||
PartialUpdate: partialUpdate,
|
||||
@@ -586,6 +629,7 @@ type DeleteOption interface {
|
||||
type deleteOption struct {
|
||||
collectionName string
|
||||
partitionName string
|
||||
namespace *string
|
||||
expr string
|
||||
}
|
||||
|
||||
@@ -593,6 +637,7 @@ func (opt *deleteOption) Request() *milvuspb.DeleteRequest {
|
||||
return &milvuspb.DeleteRequest{
|
||||
CollectionName: opt.collectionName,
|
||||
PartitionName: opt.partitionName,
|
||||
Namespace: opt.namespace,
|
||||
Expr: opt.expr,
|
||||
}
|
||||
}
|
||||
@@ -617,6 +662,14 @@ func (opt *deleteOption) WithPartition(partitionName string) *deleteOption {
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithNamespace scopes the delete request to a collection namespace. Delete
|
||||
// tombstones are primary-key based, so callers must keep primary keys unique
|
||||
// across namespaces in the same collection.
|
||||
func (opt *deleteOption) WithNamespace(namespace string) *deleteOption {
|
||||
opt.namespace = &namespace
|
||||
return opt
|
||||
}
|
||||
|
||||
func NewDeleteOption(collectionName string) *deleteOption {
|
||||
return &deleteOption{collectionName: collectionName}
|
||||
}
|
||||
|
||||
@@ -153,11 +153,12 @@ func TestRowBasedUpsertEmitsFieldOps(t *testing.T) {
|
||||
map[string]any{"id": int64(1), "tags": []int64{10}},
|
||||
map[string]any{"id": int64(2), "tags": []int64{20, 30}},
|
||||
}
|
||||
opt := NewRowBasedInsertOption(coll.Name, rows...)
|
||||
opt.WithArrayAppend("tags")
|
||||
opt := NewRowBasedInsertOption(coll.Name, rows...).
|
||||
WithArrayAppend("tags")
|
||||
|
||||
req, err := opt.UpsertRequest(coll)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, len(rows), req.GetNumRows())
|
||||
assert.True(t, req.GetPartialUpdate())
|
||||
tagsOp := findOp(req.GetFieldOps(), "tags")
|
||||
require.NotNil(t, tagsOp)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
etcd:
|
||||
image: quay.io/coreos/etcd:v3.5.25
|
||||
@@ -17,7 +15,7 @@ services:
|
||||
- "4001:4001"
|
||||
|
||||
pulsar:
|
||||
image: milvusdb/pulsar:v2.8.2-m1
|
||||
image: ${PULSAR_IMAGE:-harbor.milvus.io/dockerhub/milvusdb/pulsar:v2.8.2-m1}
|
||||
volumes:
|
||||
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/pulsar:/pulsar/data
|
||||
environment:
|
||||
|
||||
@@ -3027,6 +3027,7 @@ func (node *Proxy) search(ctx context.Context, request *milvuspb.SearchRequest,
|
||||
shardClientMgr: node.shardMgr,
|
||||
enableMaterializedView: node.enableMaterializedView,
|
||||
mustUsePartitionKey: Params.ProxyCfg.MustUsePartitionKey.GetAsBool(),
|
||||
chMgr: node.chMgr,
|
||||
}
|
||||
|
||||
succeeded := false
|
||||
@@ -3254,6 +3255,7 @@ func (node *Proxy) hybridSearch(ctx context.Context, request *milvuspb.HybridSea
|
||||
lb: node.lbPolicy,
|
||||
shardClientMgr: node.shardMgr,
|
||||
mustUsePartitionKey: Params.ProxyCfg.MustUsePartitionKey.GetAsBool(),
|
||||
chMgr: node.chMgr,
|
||||
}
|
||||
|
||||
succeeded := false
|
||||
@@ -3529,6 +3531,7 @@ func (node *Proxy) handleIfSearchByPK(ctx context.Context, request *milvuspb.Sea
|
||||
GuaranteeTimestamp: request.GuaranteeTimestamp,
|
||||
ConsistencyLevel: request.ConsistencyLevel,
|
||||
UseDefaultConsistency: request.UseDefaultConsistency,
|
||||
Namespace: request.Namespace,
|
||||
}
|
||||
|
||||
// Create queryTask to execute the retrieval
|
||||
@@ -3552,6 +3555,7 @@ func (node *Proxy) handleIfSearchByPK(ctx context.Context, request *milvuspb.Sea
|
||||
mustUsePartitionKey: Params.ProxyCfg.MustUsePartitionKey.GetAsBool(),
|
||||
// reQuery defaults to false - we need full query processing:
|
||||
// partition conversion, struct field reconstruction, timestamp handling etc
|
||||
chMgr: node.chMgr,
|
||||
}
|
||||
|
||||
// Execute query
|
||||
@@ -3828,6 +3832,7 @@ func (node *Proxy) Query(ctx context.Context, request *milvuspb.QueryRequest) (*
|
||||
lb: node.lbPolicy,
|
||||
shardclientMgr: node.shardMgr,
|
||||
mustUsePartitionKey: Params.ProxyCfg.MustUsePartitionKey.GetAsBool(),
|
||||
chMgr: node.chMgr,
|
||||
}
|
||||
|
||||
subLabel := GetCollectionRateSubLabel(request)
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -46,6 +47,7 @@ import (
|
||||
"github.com/milvus-io/milvus/internal/mocks/distributed/mock_streaming"
|
||||
"github.com/milvus-io/milvus/internal/proxy/shardclient"
|
||||
"github.com/milvus-io/milvus/internal/util/dependency"
|
||||
"github.com/milvus-io/milvus/internal/util/segcore"
|
||||
"github.com/milvus-io/milvus/internal/util/sessionutil"
|
||||
"github.com/milvus-io/milvus/pkg/v3/common"
|
||||
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
|
||||
@@ -2207,6 +2209,88 @@ func TestHandleIfSearchByPK_BM25Detection(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleIfSearchByPK_PreservesNamespaceInInternalQuery(t *testing.T) {
|
||||
mockey.PatchConvey("TestHandleIfSearchByPK_PreservesNamespaceInInternalQuery", t, func() {
|
||||
paramtable.Init()
|
||||
originalCache := globalMetaCache
|
||||
defer func() { globalMetaCache = originalCache }()
|
||||
|
||||
namespace := "tenant_a"
|
||||
schema := &schemapb.CollectionSchema{
|
||||
Name: "test_collection",
|
||||
EnableNamespace: true,
|
||||
Fields: []*schemapb.FieldSchema{
|
||||
{FieldID: 100, Name: "id", IsPrimaryKey: true, DataType: schemapb.DataType_Int64},
|
||||
{
|
||||
FieldID: 101,
|
||||
Name: "vec",
|
||||
DataType: schemapb.DataType_FloatVector,
|
||||
TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "2"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewMockCache(t)
|
||||
cache.EXPECT().
|
||||
GetCollectionInfo(mock.Anything, "default", "test_collection", int64(0)).
|
||||
Return(&collectionInfo{schema: newSchemaInfo(schema)}, nil)
|
||||
globalMetaCache = cache
|
||||
|
||||
var capturedNamespace *string
|
||||
mockey.Mock((*Proxy).query).To(func(_ *Proxy, _ context.Context, qt *queryTask, _ trace.Span) (*milvuspb.QueryResults, segcore.StorageCost, error) {
|
||||
capturedNamespace = qt.request.Namespace
|
||||
return &milvuspb.QueryResults{
|
||||
Status: merr.Success(),
|
||||
FieldsData: []*schemapb.FieldData{
|
||||
{
|
||||
FieldName: "id",
|
||||
FieldId: 100,
|
||||
Type: schemapb.DataType_Int64,
|
||||
Field: &schemapb.FieldData_Scalars{
|
||||
Scalars: &schemapb.ScalarField{
|
||||
Data: &schemapb.ScalarField_LongData{
|
||||
LongData: &schemapb.LongArray{Data: []int64{1}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
FieldName: "vec",
|
||||
FieldId: 101,
|
||||
Type: schemapb.DataType_FloatVector,
|
||||
Field: &schemapb.FieldData_Vectors{
|
||||
Vectors: &schemapb.VectorField{
|
||||
Dim: 2,
|
||||
Data: &schemapb.VectorField_FloatVector{
|
||||
FloatVector: &schemapb.FloatArray{Data: []float32{0.1, 0.2}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, segcore.StorageCost{}, nil
|
||||
}).Build()
|
||||
|
||||
node := &Proxy{}
|
||||
req := &milvuspb.SearchRequest{
|
||||
DbName: "default",
|
||||
CollectionName: "test_collection",
|
||||
Namespace: &namespace,
|
||||
SearchInput: &milvuspb.SearchRequest_Ids{
|
||||
Ids: &schemapb.IDs{
|
||||
IdField: &schemapb.IDs_IntId{IntId: &schemapb.LongArray{Data: []int64{1}}},
|
||||
},
|
||||
},
|
||||
SearchParams: []*commonpb.KeyValuePair{{Key: AnnsFieldKey, Value: "vec"}},
|
||||
}
|
||||
|
||||
_, err := node.handleIfSearchByPK(context.Background(), req)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, capturedNamespace)
|
||||
assert.Equal(t, namespace, *capturedNamespace)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProxy_ManualCompaction_ExternalCollection(t *testing.T) {
|
||||
// Save and restore globalMetaCache
|
||||
cache := globalMetaCache
|
||||
|
||||
@@ -84,6 +84,8 @@ func TestRepackDeleteMsgByHashPreservesModuloRouting(t *testing.T) {
|
||||
2,
|
||||
"partition",
|
||||
"default",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -125,6 +127,8 @@ func TestRepackDeleteMsgByHashReturnsRoutingErrorWithoutChannels(t *testing.T) {
|
||||
2,
|
||||
"partition",
|
||||
"default",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
assert.ErrorIs(t, err, common.ErrRoutingTableNoValues)
|
||||
|
||||
@@ -1478,6 +1478,7 @@ func (op *requeryOperator) requery(ctx context.Context, span trace.Span, ids *sc
|
||||
preferredNodes: preferredNodes,
|
||||
fastSkip: true,
|
||||
reQuery: true,
|
||||
chMgr: op.node.(*Proxy).chMgr,
|
||||
}
|
||||
queryResult, storageCost, err := op.node.(*Proxy).query(op.traceCtx, qt, span)
|
||||
if err != nil {
|
||||
|
||||
@@ -1070,6 +1070,7 @@ func convertHybridSearchToSearch(req *milvuspb.HybridSearchRequest) *milvuspb.Se
|
||||
PartitionNames: req.GetPartitionNames(),
|
||||
OutputFields: req.GetOutputFields(),
|
||||
SearchParams: req.GetRankParams(),
|
||||
Namespace: req.Namespace,
|
||||
TravelTimestamp: req.GetTravelTimestamp(),
|
||||
GuaranteeTimestamp: req.GetGuaranteeTimestamp(),
|
||||
Nq: 0,
|
||||
@@ -1079,7 +1080,6 @@ func convertHybridSearchToSearch(req *milvuspb.HybridSearchRequest) *milvuspb.Se
|
||||
SearchByPrimaryKeys: false,
|
||||
SubReqs: nil,
|
||||
FunctionScore: req.FunctionScore,
|
||||
Namespace: req.Namespace,
|
||||
}
|
||||
|
||||
for _, sub := range req.GetRequests() {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 proxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
|
||||
)
|
||||
|
||||
func TestConvertHybridSearchToSearchKeepsNamespace(t *testing.T) {
|
||||
namespace := "tenant_a"
|
||||
|
||||
searchReq := convertHybridSearchToSearch(&milvuspb.HybridSearchRequest{
|
||||
CollectionName: "coll",
|
||||
Namespace: &namespace,
|
||||
Requests: []*milvuspb.SearchRequest{
|
||||
{Nq: 1},
|
||||
},
|
||||
})
|
||||
|
||||
assert.Equal(t, namespace, searchReq.GetNamespace())
|
||||
}
|
||||
@@ -208,6 +208,7 @@ func (node *CachedProxyServiceProvider) DescribeCollection(ctx context.Context,
|
||||
}),
|
||||
StructArrayFields: cloneStructArrayFields(c.schema.StructArrayFields),
|
||||
EnableDynamicField: c.schema.EnableDynamicField,
|
||||
EnableNamespace: c.schema.EnableNamespace,
|
||||
Properties: c.schema.Properties,
|
||||
Functions: c.schema.Functions,
|
||||
DbName: c.schema.DbName,
|
||||
|
||||
@@ -91,6 +91,7 @@ func TestCachedProxyServiceProvider_DescribeCollection_FilterNamespaceField(t *t
|
||||
schema := &schemapb.CollectionSchema{
|
||||
Name: collectionName,
|
||||
EnableDynamicField: true,
|
||||
EnableNamespace: true,
|
||||
Fields: []*schemapb.FieldSchema{
|
||||
{
|
||||
FieldID: common.StartOfUserFieldID,
|
||||
@@ -128,6 +129,7 @@ func TestCachedProxyServiceProvider_DescribeCollection_FilterNamespaceField(t *t
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, commonpb.ErrorCode_Success, resp.GetStatus().GetErrorCode())
|
||||
assert.True(t, resp.GetSchema().GetEnableNamespace())
|
||||
|
||||
fieldNames := make(map[string]struct{})
|
||||
for _, f := range resp.GetSchema().GetFields() {
|
||||
|
||||
@@ -128,7 +128,14 @@ func (dt *deleteTask) getChannels() []pChan {
|
||||
}
|
||||
|
||||
func (dt *deleteTask) PreExecute(ctx context.Context) error {
|
||||
return nil
|
||||
if dt.req.Namespace == nil {
|
||||
return nil
|
||||
}
|
||||
schema, err := globalMetaCache.GetCollectionSchema(ctx, dt.req.GetDbName(), dt.req.GetCollectionName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return common.CheckNamespace(schema.CollectionSchema, dt.req.Namespace)
|
||||
}
|
||||
|
||||
func (dt *deleteTask) PostExecute(ctx context.Context) error {
|
||||
@@ -151,12 +158,30 @@ func repackDeleteMsgByHash(
|
||||
partitionID int64,
|
||||
partitionName string,
|
||||
dbName string,
|
||||
namespace *string,
|
||||
schema *schemapb.CollectionSchema,
|
||||
) (map[uint32][]*msgstream.DeleteMsg, int64, error) {
|
||||
maxSize := Params.PulsarCfg.MaxMessageSize.GetAsInt()
|
||||
hashValues, err := typeutil.HashPK2Channels(primaryKeys, vChannels)
|
||||
var hashValues []uint32
|
||||
// Delete tombstones are PK+timestamp based. Namespace can narrow routing,
|
||||
// but it is not part of the tombstone identity; PKs must stay unique across
|
||||
// namespaces in the same collection.
|
||||
channelID, ok, err := namespaceShardingChannelID(schema, namespace, vChannels)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if ok {
|
||||
size := typeutil.GetSizeOfIDs(primaryKeys)
|
||||
hashValues = make([]uint32, size)
|
||||
for i := 0; i < size; i++ {
|
||||
hashValues[i] = channelID
|
||||
}
|
||||
} else {
|
||||
hashValues, err = typeutil.HashPK2Channels(primaryKeys, vChannels)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
// repack delete msg by dmChannel
|
||||
result := make(map[uint32][]*msgstream.DeleteMsg)
|
||||
lastMessageSize := map[uint32]int{}
|
||||
@@ -294,11 +319,11 @@ func (dr *deleteRunner) Init(ctx context.Context) error {
|
||||
if err := validateTextStorageV3Enabled(dr.schema.CollectionSchema); err != nil {
|
||||
return ErrWithLog(log, "TEXT field requires StorageV3", err)
|
||||
}
|
||||
if namespacePartitionModeEnabled(dr.schema.CollectionSchema) {
|
||||
partitionName, _, err := resolveNamespacePartitionName(dr.schema.CollectionSchema, dr.req.Namespace, dr.req.GetPartitionName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
partitionName, namespaceAsPartition, err := resolveNamespacePartitionName(dr.schema.CollectionSchema, dr.req.Namespace, dr.req.GetPartitionName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if namespaceAsPartition {
|
||||
dr.req.PartitionName = partitionName
|
||||
}
|
||||
|
||||
@@ -321,9 +346,22 @@ func (dr *deleteRunner) Init(ctx context.Context) error {
|
||||
return merr.WrapErrAsInputError(merr.WrapErrParameterInvalidMsg("delete plan can't be empty or always true : %s", dr.req.GetExpr()))
|
||||
}
|
||||
|
||||
dr.plan.Namespace = namespaceForPlan(dr.schema.CollectionSchema, dr.req.Namespace)
|
||||
// Set partitionIDs, could be empty if no partition name specified and no partition key
|
||||
partName := dr.req.GetPartitionName()
|
||||
if dr.schema.IsPartitionKeyCollection() {
|
||||
if namespacePartitionKeyMode(dr.schema.CollectionSchema) && dr.req.Namespace != nil {
|
||||
if len(partName) > 0 {
|
||||
return merr.WrapErrParameterInvalidMsg("not support manually specifying the partition names if namespace is used")
|
||||
}
|
||||
hashedPartitionNames, err := assignNamespacePartitionKey(ctx, dr.req.GetDbName(), dr.req.GetCollectionName(), dr.req.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dr.partitionIDs, err = getPartitionIDs(ctx, dr.req.GetDbName(), dr.req.GetCollectionName(), hashedPartitionNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if dr.schema.IsPartitionKeyCollection() {
|
||||
if len(partName) > 0 {
|
||||
return merr.WrapErrParameterInvalidMsg("not support manually specifying the partition names if partition key mode is used")
|
||||
}
|
||||
@@ -561,13 +599,28 @@ func (dr *deleteRunner) complexDelete(ctx context.Context, plan *planpb.PlanNode
|
||||
return err
|
||||
}
|
||||
|
||||
err = dr.lb.Execute(ctx, shardclient.CollectionWorkLoad{
|
||||
Db: dr.req.GetDbName(),
|
||||
CollectionName: dr.req.GetCollectionName(),
|
||||
CollectionID: dr.collectionID,
|
||||
Nq: 1,
|
||||
Exec: dr.getStreamingQueryAndDelteFunc(plan),
|
||||
})
|
||||
channelName, useNamespaceChannel, err := namespaceShardingChannel(dr.schema.CollectionSchema, dr.req.Namespace, dr.vChannels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if useNamespaceChannel {
|
||||
err = dr.lb.ExecuteWithRetry(ctx, shardclient.ChannelWorkload{
|
||||
Db: dr.req.GetDbName(),
|
||||
CollectionName: dr.req.GetCollectionName(),
|
||||
CollectionID: dr.collectionID,
|
||||
Channel: channelName,
|
||||
Nq: 1,
|
||||
Exec: dr.getStreamingQueryAndDelteFunc(plan),
|
||||
})
|
||||
} else {
|
||||
err = dr.lb.Execute(ctx, shardclient.CollectionWorkLoad{
|
||||
Db: dr.req.GetDbName(),
|
||||
CollectionName: dr.req.GetCollectionName(),
|
||||
CollectionID: dr.collectionID,
|
||||
Nq: 1,
|
||||
Exec: dr.getStreamingQueryAndDelteFunc(plan),
|
||||
})
|
||||
}
|
||||
dr.result.DeleteCnt = dr.count.Load()
|
||||
dr.result.Timestamp = dr.sessionTS.Load()
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
|
||||
"github.com/milvus-io/milvus/internal/distributed/streaming"
|
||||
"github.com/milvus-io/milvus/internal/util/hookutil"
|
||||
"github.com/milvus-io/milvus/pkg/v3/mlog"
|
||||
@@ -26,6 +27,17 @@ func (dt *deleteTask) Execute(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
dt.tr = timerecord.NewTimeRecorder(fmt.Sprintf("proxy execute delete %d", dt.ID()))
|
||||
|
||||
var collectionSchema *schemapb.CollectionSchema
|
||||
if dt.req.Namespace != nil || hookutil.IsClusterEncryptionEnabled() {
|
||||
schema, err := globalMetaCache.GetCollectionSchema(ctx, dt.req.GetDbName(), dt.req.GetCollectionName())
|
||||
if err != nil {
|
||||
mlog.Warn(ctx, "get collection schema from global meta cache failed", mlog.String("collectionName", dt.req.GetCollectionName()), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
collectionSchema = schema.CollectionSchema
|
||||
}
|
||||
|
||||
result, numRows, err := repackDeleteMsgByHash(
|
||||
ctx, dt.primaryKeys,
|
||||
dt.vChannels, dt.idAllocator,
|
||||
@@ -33,6 +45,8 @@ func (dt *deleteTask) Execute(ctx context.Context) (err error) {
|
||||
dt.req.GetCollectionName(),
|
||||
dt.partitionID, dt.req.GetPartitionName(),
|
||||
dt.req.GetDbName(),
|
||||
dt.req.Namespace,
|
||||
collectionSchema,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -40,13 +54,7 @@ func (dt *deleteTask) Execute(ctx context.Context) (err error) {
|
||||
|
||||
var ez *message.CipherConfig
|
||||
if hookutil.IsClusterEncryptionEnabled() {
|
||||
schema, err := globalMetaCache.GetCollectionSchema(ctx, dt.req.GetDbName(), dt.req.GetCollectionName())
|
||||
if err != nil {
|
||||
mlog.Warn(ctx, "get collection schema from global meta cache failed", mlog.String("collectionName", dt.req.GetCollectionName()), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
|
||||
ez = hookutil.GetEzByCollProperties(schema.GetProperties(), dt.collectionID).AsMessageConfig()
|
||||
ez = hookutil.GetEzByCollProperties(collectionSchema.GetProperties(), dt.collectionID).AsMessageConfig()
|
||||
}
|
||||
|
||||
var msgs []message.MutableMessage
|
||||
|
||||
@@ -138,6 +138,19 @@ func TestDeleteTask_GetChannels(t *testing.T) {
|
||||
assert.ElementsMatch(t, channels, dt.pChannels)
|
||||
}
|
||||
|
||||
func TestDeleteTask_PreExecuteSkipsNamespaceValidationWhenUnset(t *testing.T) {
|
||||
cache := globalMetaCache
|
||||
globalMetaCache = nil
|
||||
defer func() {
|
||||
globalMetaCache = cache
|
||||
}()
|
||||
|
||||
dt := deleteTask{
|
||||
req: &milvuspb.DeleteRequest{},
|
||||
}
|
||||
assert.NoError(t, dt.PreExecute(context.Background()))
|
||||
}
|
||||
|
||||
func TestDeleteTask_Execute(t *testing.T) {
|
||||
collectionName := "test_delete"
|
||||
collectionID := int64(111)
|
||||
@@ -334,6 +347,38 @@ func (s *DeleteRunnerSuite) TestInitSuccess() {
|
||||
s.Require().Equal(0, len(dr.partitionIDs))
|
||||
})
|
||||
|
||||
s.Run("namespace assigns partition", func() {
|
||||
mockChMgr := NewMockChannelsMgr(s.T())
|
||||
namespace := "ns-1"
|
||||
partitionNames := []string{"_default_0", "_default_1"}
|
||||
partitionIDs := map[string]int64{"_default_0": 100, "_default_1": 101}
|
||||
schema := namespaceEnabledSchema(
|
||||
&schemapb.FieldSchema{FieldID: common.StartOfUserFieldID, Name: "pk", IsPrimaryKey: true, DataType: schemapb.DataType_Int64},
|
||||
&schemapb.FieldSchema{FieldID: common.StartOfUserFieldID + 1, Name: "non_pk", DataType: schemapb.DataType_Int64},
|
||||
)
|
||||
s.schema = newSchemaInfo(schema)
|
||||
dr := deleteRunner{
|
||||
req: &milvuspb.DeleteRequest{
|
||||
CollectionName: s.collectionName,
|
||||
Expr: "pk == 1",
|
||||
Namespace: &namespace,
|
||||
},
|
||||
chMgr: mockChMgr,
|
||||
}
|
||||
s.mockCache.EXPECT().GetDatabaseInfo(mock.Anything, mock.Anything).Return(&databaseInfo{dbID: 0}, nil)
|
||||
s.mockCache.EXPECT().GetCollectionID(mock.Anything, mock.Anything, mock.Anything).Return(s.collectionID, nil)
|
||||
s.mockCache.EXPECT().GetCollectionInfo(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&collectionInfo{}, nil)
|
||||
s.mockCache.EXPECT().GetCollectionSchema(mock.Anything, mock.Anything, mock.Anything).Return(s.schema, nil).Twice()
|
||||
s.mockCache.EXPECT().GetPartitionsIndex(mock.Anything, mock.Anything, mock.Anything).Return(partitionNames, nil)
|
||||
s.mockCache.EXPECT().GetPartitions(mock.Anything, mock.Anything, mock.Anything).Return(partitionIDs, nil)
|
||||
mockChMgr.EXPECT().getVChannels(mock.Anything).Return([]string{"vchan1"}, nil)
|
||||
|
||||
globalMetaCache = s.mockCache
|
||||
s.NoError(dr.Init(context.Background()))
|
||||
|
||||
s.Equal([]int64{expectedNamespacePartitionID(namespace, partitionNames, partitionIDs)}, dr.partitionIDs)
|
||||
})
|
||||
|
||||
s.Run("pk == 1, no partition name", func() {
|
||||
mockChMgr := NewMockChannelsMgr(s.T())
|
||||
dr := deleteRunner{
|
||||
@@ -471,6 +516,37 @@ func (s *DeleteRunnerSuite) TestInitFailure() {
|
||||
s.Error(dr.Init(context.Background()))
|
||||
})
|
||||
|
||||
s.Run("namespace disabled", func() {
|
||||
mockChMgr := NewMockChannelsMgr(s.T())
|
||||
namespace := "ns-1"
|
||||
dr := deleteRunner{
|
||||
req: &milvuspb.DeleteRequest{
|
||||
CollectionName: s.collectionName,
|
||||
Expr: "pk == 1",
|
||||
Namespace: &namespace,
|
||||
},
|
||||
chMgr: mockChMgr,
|
||||
}
|
||||
schema := &schemapb.CollectionSchema{
|
||||
Name: s.collectionName,
|
||||
Fields: []*schemapb.FieldSchema{
|
||||
{
|
||||
FieldID: common.StartOfUserFieldID,
|
||||
Name: "pk",
|
||||
IsPrimaryKey: true,
|
||||
DataType: schemapb.DataType_Int64,
|
||||
},
|
||||
},
|
||||
}
|
||||
s.schema = newSchemaInfo(schema)
|
||||
s.mockCache.EXPECT().GetDatabaseInfo(mock.Anything, mock.Anything).Return(&databaseInfo{dbID: 0}, nil)
|
||||
s.mockCache.EXPECT().GetCollectionID(mock.Anything, mock.Anything, mock.Anything).Return(s.collectionID, nil)
|
||||
s.mockCache.EXPECT().GetCollectionSchema(mock.Anything, mock.Anything, mock.Anything).Return(s.schema, nil)
|
||||
|
||||
globalMetaCache = s.mockCache
|
||||
s.Error(dr.Init(context.Background()))
|
||||
})
|
||||
|
||||
s.Run("fail to get database info", func() {
|
||||
dr := deleteRunner{
|
||||
req: &milvuspb.DeleteRequest{
|
||||
|
||||
@@ -65,7 +65,7 @@ func (it *insertTask) Execute(ctx context.Context) error {
|
||||
if it.partitionKeys == nil {
|
||||
msgs, err = repackInsertDataForStreamingService(it.TraceCtx(), channelNames, it.insertMsg, it.result, ez, it.schemaVersion)
|
||||
} else {
|
||||
msgs, err = repackInsertDataWithPartitionKeyForStreamingService(it.TraceCtx(), channelNames, it.insertMsg, it.result, it.partitionKeys, ez, it.schemaVersion)
|
||||
msgs, err = repackInsertDataWithPartitionKeyForStreamingService(it.TraceCtx(), channelNames, it.insertMsg, it.result, it.partitionKeys, ez, it.schema, it.schemaVersion)
|
||||
}
|
||||
if err != nil {
|
||||
mlog.Warn(ctx, "assign segmentID and repack insert data failed", mlog.Err(err))
|
||||
@@ -146,11 +146,18 @@ func repackInsertDataWithPartitionKeyForStreamingService(
|
||||
result *milvuspb.MutationResult,
|
||||
partitionKeys *schemapb.FieldData,
|
||||
ez *message.CipherConfig,
|
||||
schema *schemapb.CollectionSchema,
|
||||
schemaVersion int32,
|
||||
) ([]message.MutableMessage, error) {
|
||||
messages := make([]message.MutableMessage, 0)
|
||||
|
||||
channel2RowOffsets, err := assignChannelsByPK(result.IDs, channelNames, insertMsg)
|
||||
var channel2RowOffsets map[string][]int
|
||||
var err error
|
||||
if namespacePartitionKeyModeEnabled(schema) && insertMsg.Namespace != nil {
|
||||
channel2RowOffsets, err = assignChannelsByNamespace(*insertMsg.Namespace, channelNames, insertMsg)
|
||||
} else {
|
||||
channel2RowOffsets, err = assignChannelsByPK(result.IDs, channelNames, insertMsg)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ type queryTask struct {
|
||||
resolvedTimezoneStr string
|
||||
storageCost segcore.StorageCost
|
||||
aggregationFieldMap *agg.AggregationFieldMap
|
||||
chMgr channelsMgr
|
||||
}
|
||||
|
||||
func (t *queryTask) getQueryLabel() string {
|
||||
@@ -805,7 +806,14 @@ func (t *queryTask) PreExecute(ctx context.Context) error {
|
||||
// convert partition names only when requery is false
|
||||
if !t.reQuery {
|
||||
partitionNames := t.request.GetPartitionNames()
|
||||
if t.partitionKeyMode {
|
||||
if namespacePartitionKeyMode(t.schema.CollectionSchema) && t.request.Namespace != nil {
|
||||
hashedPartitionNames, err := assignNamespacePartitionKey(ctx, t.request.GetDbName(), t.request.CollectionName, t.request.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
partitionNames = append(partitionNames, hashedPartitionNames...)
|
||||
} else if t.partitionKeyMode {
|
||||
expr, err := exprutil.ParseExprFromPlan(t.plan)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -919,6 +927,34 @@ func (t *queryTask) Execute(ctx context.Context) error {
|
||||
mlog.String("requestType", t.getQueryLabel()))
|
||||
|
||||
t.resultBuf = typeutil.NewConcurrentSet[*internalpb.RetrieveResults]()
|
||||
if namespacePartitionKeyModeEnabled(t.schema.CollectionSchema) && t.request.Namespace != nil {
|
||||
channelNames, err := t.chMgr.getVChannels(t.CollectionID)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "get vChannels failed", mlog.Int64("collectionID", t.CollectionID), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
channelName, ok, err := namespaceShardingChannel(t.schema.CollectionSchema, t.request.Namespace, channelNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
if err := t.lb.ExecuteWithRetry(ctx, shardclient.ChannelWorkload{
|
||||
Db: t.request.GetDbName(),
|
||||
CollectionName: t.collectionName,
|
||||
CollectionID: t.CollectionID,
|
||||
Channel: channelName,
|
||||
Nq: 1,
|
||||
Exec: t.queryShard,
|
||||
PreferredNodeID: preferredNodeForChannel(t.preferredNodes, channelName),
|
||||
}); err != nil {
|
||||
log.Warn(ctx, "fail to execute query", mlog.Err(err))
|
||||
return errors.Wrap(err, "failed to query")
|
||||
}
|
||||
|
||||
log.Debug(ctx, "Query Execute done.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err := t.lb.Execute(ctx, shardclient.CollectionWorkLoad{
|
||||
Db: t.request.GetDbName(),
|
||||
CollectionID: t.CollectionID,
|
||||
|
||||
@@ -11,11 +11,46 @@ import (
|
||||
"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/parser/planparserv2"
|
||||
"github.com/milvus-io/milvus/pkg/v3/common"
|
||||
"github.com/milvus-io/milvus/pkg/v3/proto/internalpb"
|
||||
"github.com/milvus-io/milvus/pkg/v3/proto/planpb"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/merr"
|
||||
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
|
||||
)
|
||||
|
||||
func namespaceEnabledSchema(fields ...*schemapb.FieldSchema) *schemapb.CollectionSchema {
|
||||
return &schemapb.CollectionSchema{
|
||||
Name: "test_collection",
|
||||
Fields: append(fields, &schemapb.FieldSchema{
|
||||
FieldID: 999,
|
||||
Name: common.NamespaceFieldName,
|
||||
IsPartitionKey: true,
|
||||
DataType: schemapb.DataType_VarChar,
|
||||
}),
|
||||
EnableNamespace: true,
|
||||
}
|
||||
}
|
||||
|
||||
func nonPartitionKeyPredicate(fieldID int64, dataType schemapb.DataType) *planpb.Expr {
|
||||
return &planpb.Expr{
|
||||
Expr: &planpb.Expr_UnaryRangeExpr{
|
||||
UnaryRangeExpr: &planpb.UnaryRangeExpr{
|
||||
ColumnInfo: &planpb.ColumnInfo{
|
||||
FieldId: fieldID,
|
||||
DataType: dataType,
|
||||
},
|
||||
Op: planpb.OpType_GreaterThan,
|
||||
Value: &planpb.GenericValue{Val: &planpb.GenericValue_Int64Val{Int64Val: 0}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedNamespacePartitionID(namespace string, partitionNames []string, partitionIDs map[string]int64) int64 {
|
||||
idx := typeutil.HashString2Uint32(namespace) % uint32(len(partitionNames))
|
||||
return partitionIDs[partitionNames[idx]]
|
||||
}
|
||||
|
||||
func TestQueryTask_PlanNamespace_AfterPreExecute(t *testing.T) {
|
||||
mockey.PatchConvey("TestQueryTask_PlanNamespace_AfterPreExecute", t, func() {
|
||||
// Setup global meta cache and common mocks
|
||||
@@ -77,3 +112,53 @@ func TestQueryTask_PlanNamespace_AfterPreExecute(t *testing.T) {
|
||||
assert.Equal(t, *task.request.Namespace, *capturedPlan.Namespace)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryTask_NamespaceSetsPartitionIDs(t *testing.T) {
|
||||
mockey.PatchConvey("TestQueryTask_NamespaceSetsPartitionIDs", t, func() {
|
||||
globalMetaCache = &MetaCache{}
|
||||
|
||||
partitionNames := []string{"_default_0", "_default_1"}
|
||||
partitionIDs := map[string]int64{"_default_0": 101, "_default_1": 102}
|
||||
namespaces := []string{"ns-0", "ns-1", "ns-2", "ns-3", "ns-4", "ns-5", "ns-6", "ns-7"}
|
||||
schema := namespaceEnabledSchema(
|
||||
&schemapb.FieldSchema{FieldID: 100, Name: "id", IsPrimaryKey: true, DataType: schemapb.DataType_Int64},
|
||||
&schemapb.FieldSchema{FieldID: 101, Name: "value", DataType: schemapb.DataType_Int64},
|
||||
)
|
||||
|
||||
mockey.Mock((*MetaCache).GetCollectionID).Return(int64(1001), nil).Build()
|
||||
mockey.Mock((*MetaCache).GetCollectionInfo).Return(&collectionInfo{updateTimestamp: 12345, consistencyLevel: commonpb.ConsistencyLevel_Strong}, nil).Build()
|
||||
mockey.Mock((*MetaCache).GetCollectionSchema).Return(newSchemaInfo(schema), nil).Build()
|
||||
mockey.Mock((*MetaCache).GetPartitionsIndex).Return(partitionNames, nil).Build()
|
||||
mockey.Mock((*MetaCache).GetPartitions).Return(partitionIDs, nil).Build()
|
||||
mockey.Mock(validatePartitionTag).Return(nil).Build()
|
||||
mockey.Mock(isIgnoreGrowing).Return(false, nil).Build()
|
||||
|
||||
mockey.Mock((*queryTask).createPlanArgs).To(func(q *queryTask, ctx context.Context, visitorArgs *planparserv2.ParserVisitorArgs) error {
|
||||
q.plan = &planpb.PlanNode{
|
||||
Node: &planpb.PlanNode_Query{
|
||||
Query: &planpb.QueryPlanNode{
|
||||
Predicates: nonPartitionKeyPredicate(101, schemapb.DataType_Int64),
|
||||
},
|
||||
},
|
||||
}
|
||||
q.translatedOutputFields = []string{"id"}
|
||||
q.userOutputFields = []string{"id"}
|
||||
return nil
|
||||
}).Build()
|
||||
|
||||
for _, ns := range namespaces {
|
||||
namespace := ns
|
||||
task := &queryTask{
|
||||
Condition: NewTaskCondition(context.Background()),
|
||||
RetrieveRequest: &internalpb.RetrieveRequest{QueryLabel: "query", Base: &commonpb.MsgBase{MsgType: commonpb.MsgType_Retrieve}},
|
||||
ctx: context.Background(),
|
||||
request: &milvuspb.QueryRequest{CollectionName: "test_collection", Expr: "value > 0", Namespace: &namespace},
|
||||
result: &milvuspb.QueryResults{Status: merr.Success()},
|
||||
}
|
||||
|
||||
err := task.PreExecute(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{expectedNamespacePartitionID(namespace, partitionNames, partitionIDs)}, task.GetPartitionIDs())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,6 +125,8 @@ type searchTask struct {
|
||||
|
||||
hybridSubSearchInfos []hybridSubSearchInfo
|
||||
hybridElementLevel bool
|
||||
|
||||
chMgr channelsMgr
|
||||
}
|
||||
|
||||
func (t *searchTask) CanSkipAllocTimestamp() bool {
|
||||
@@ -1179,6 +1181,23 @@ func (t *searchTask) tryGeneratePlan(params []*commonpb.KeyValuePair, dsl string
|
||||
}
|
||||
|
||||
func (t *searchTask) tryParsePartitionIDsFromPlan(plan *planpb.PlanNode) ([]int64, error) {
|
||||
if namespacePartitionKeyMode(t.schema.CollectionSchema) && t.request.Namespace != nil {
|
||||
hashedPartitionNames, err := assignNamespacePartitionKey(t.ctx, t.request.GetDbName(), t.collectionName, t.request.Namespace)
|
||||
if err != nil {
|
||||
mlog.Warn(t.ctx, "failed to assign namespace partition key", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
if len(hashedPartitionNames) > 0 {
|
||||
PartitionIDs, err2 := getPartitionIDs(t.ctx, t.request.GetDbName(), t.collectionName, hashedPartitionNames)
|
||||
if err2 != nil {
|
||||
mlog.Warn(t.ctx, "failed to get namespace partition ids", mlog.Err(err2))
|
||||
return nil, err2
|
||||
}
|
||||
return PartitionIDs, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
expr, err := exprutil.ParseExprFromPlan(plan)
|
||||
if err != nil {
|
||||
mlog.Warn(t.ctx, "failed to parse expr", mlog.Err(err))
|
||||
@@ -1212,6 +1231,36 @@ func (t *searchTask) Execute(ctx context.Context) error {
|
||||
defer tr.CtxElapse(ctx, "done")
|
||||
|
||||
t.queryChannelsNode = typeutil.NewConcurrentMap[string, int64]()
|
||||
if namespacePartitionKeyModeEnabled(t.schema.CollectionSchema) && t.request.Namespace != nil {
|
||||
channelNames, err := t.chMgr.getVChannels(t.CollectionID)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "get vChannels failed", mlog.Int64("collectionID", t.CollectionID), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
channelName, ok, err := namespaceShardingChannel(t.schema.CollectionSchema, t.request.Namespace, channelNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
if err := t.lb.ExecuteWithRetry(ctx, shardclient.ChannelWorkload{
|
||||
Db: t.request.GetDbName(),
|
||||
CollectionName: t.collectionName,
|
||||
CollectionID: t.CollectionID,
|
||||
Channel: channelName,
|
||||
Nq: t.Nq,
|
||||
Exec: t.searchShard,
|
||||
PreferredNodeID: preferredNodeFromConcurrentMap(t.queryChannelsNode, channelName),
|
||||
}); err != nil {
|
||||
log.Warn(ctx, "search execute failed", mlog.Err(err))
|
||||
return errors.Wrap(err, "failed to search")
|
||||
}
|
||||
|
||||
log.Debug(ctx, "Search Execute done.",
|
||||
mlog.Int64("collection", t.GetCollectionID()),
|
||||
mlog.Int64s("partitionIDs", t.GetPartitionIDs()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err := t.lb.Execute(ctx, shardclient.CollectionWorkLoad{
|
||||
Db: t.request.GetDbName(),
|
||||
CollectionID: t.CollectionID,
|
||||
|
||||
@@ -70,6 +70,55 @@ func TestSearchTask_PlanNamespace_AfterPreExecute(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchTask_NamespaceSetsPartitionIDs(t *testing.T) {
|
||||
mockey.PatchConvey("TestSearchTask_NamespaceSetsPartitionIDs", t, func() {
|
||||
globalMetaCache = &MetaCache{}
|
||||
|
||||
partitionNames := []string{"_default_0", "_default_1"}
|
||||
partitionIDs := map[string]int64{"_default_0": 101, "_default_1": 102}
|
||||
namespaces := []string{"ns-0", "ns-1", "ns-2", "ns-3", "ns-4", "ns-5", "ns-6", "ns-7"}
|
||||
schema := namespaceEnabledSchema(
|
||||
&schemapb.FieldSchema{FieldID: 100, Name: "id", IsPrimaryKey: true, DataType: schemapb.DataType_Int64},
|
||||
&schemapb.FieldSchema{FieldID: 101, Name: "vec", DataType: schemapb.DataType_FloatVector, TypeParams: []*commonpb.KeyValuePair{{Key: "dim", Value: "4"}}},
|
||||
&schemapb.FieldSchema{FieldID: 102, Name: "value", DataType: schemapb.DataType_Int64},
|
||||
)
|
||||
|
||||
mockey.Mock((*MetaCache).GetCollectionID).Return(int64(1001), nil).Build()
|
||||
mockey.Mock((*MetaCache).GetCollectionInfo).Return(&collectionInfo{updateTimestamp: 12345, consistencyLevel: commonpb.ConsistencyLevel_Strong}, nil).Build()
|
||||
mockey.Mock((*MetaCache).GetCollectionSchema).Return(newSchemaInfo(schema), nil).Build()
|
||||
mockey.Mock((*MetaCache).GetPartitionsIndex).Return(partitionNames, nil).Build()
|
||||
mockey.Mock((*MetaCache).GetPartitions).Return(partitionIDs, nil).Build()
|
||||
mockey.Mock(isIgnoreGrowing).Return(false, nil).Build()
|
||||
mockey.Mock((*searchTask).checkNq).Return(int64(1), nil).Build()
|
||||
mockey.Mock((*searchTask).tryGeneratePlan).To(func(_ *searchTask, _ []*commonpb.KeyValuePair, _ string, _ map[string]*schemapb.TemplateValue) (*planpb.PlanNode, *planpb.QueryInfo, int64, bool, []OrderByField, internalpb.SearchType, error) {
|
||||
plan := &planpb.PlanNode{
|
||||
Node: &planpb.PlanNode_VectorAnns{
|
||||
VectorAnns: &planpb.VectorANNS{
|
||||
Predicates: nonPartitionKeyPredicate(102, schemapb.DataType_Int64),
|
||||
},
|
||||
},
|
||||
}
|
||||
qi := &planpb.QueryInfo{Topk: 10, MetricType: "L2", QueryFieldId: 101, GroupByFieldId: -1}
|
||||
return plan, qi, 0, false, nil, internalpb.SearchType_DEFAULT, nil
|
||||
}).Build()
|
||||
|
||||
for _, ns := range namespaces {
|
||||
namespace := ns
|
||||
task := &searchTask{
|
||||
Condition: NewTaskCondition(context.Background()),
|
||||
SearchRequest: &internalpb.SearchRequest{Base: &commonpb.MsgBase{MsgType: commonpb.MsgType_Search}},
|
||||
ctx: context.Background(),
|
||||
request: &milvuspb.SearchRequest{CollectionName: "test_collection", Namespace: &namespace},
|
||||
result: &milvuspb.SearchResults{Status: merr.Success()},
|
||||
}
|
||||
|
||||
err := task.PreExecute(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{expectedNamespacePartitionID(namespace, partitionNames, partitionIDs)}, task.GetPartitionIDs())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchTask_RequeryPlanNamespace(t *testing.T) {
|
||||
mockey.PatchConvey("TestSearchTask_RequeryPlanNamespace", t, func() {
|
||||
// Minimal searchTask with schema and namespace
|
||||
|
||||
@@ -222,6 +222,7 @@ func retrieveByPKs(ctx context.Context, t *upsertTask, ids *schemapb.IDs, output
|
||||
mixCoord: t.node.(*Proxy).mixCoord,
|
||||
lb: t.node.(*Proxy).lbPolicy,
|
||||
shardclientMgr: t.node.(*Proxy).shardMgr,
|
||||
chMgr: t.node.(*Proxy).chMgr,
|
||||
}
|
||||
|
||||
ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-Upsert-retrieveByPKs")
|
||||
|
||||
@@ -88,7 +88,7 @@ func (ut *upsertTask) packInsertMessage(ctx context.Context, ez *message.CipherC
|
||||
if ut.partitionKeys == nil {
|
||||
msgs, err = repackInsertDataForStreamingService(ut.TraceCtx(), channelNames, ut.upsertMsg.InsertMsg, ut.result, ez, ut.schemaVersion)
|
||||
} else {
|
||||
msgs, err = repackInsertDataWithPartitionKeyForStreamingService(ut.TraceCtx(), channelNames, ut.upsertMsg.InsertMsg, ut.result, ut.partitionKeys, ez, ut.schemaVersion)
|
||||
msgs, err = repackInsertDataWithPartitionKeyForStreamingService(ut.TraceCtx(), channelNames, ut.upsertMsg.InsertMsg, ut.result, ut.partitionKeys, ez, ut.schema.CollectionSchema, ut.schemaVersion)
|
||||
}
|
||||
if err != nil {
|
||||
log.Warn(ctx, "assign segmentID and repack insert data failed", mlog.Err(err))
|
||||
@@ -122,6 +122,8 @@ func (ut *upsertTask) packDeleteMessage(ctx context.Context, ez *message.CipherC
|
||||
ut.upsertMsg.DeleteMsg.CollectionID, ut.upsertMsg.DeleteMsg.CollectionName,
|
||||
ut.upsertMsg.DeleteMsg.PartitionID, ut.upsertMsg.DeleteMsg.PartitionName,
|
||||
ut.req.GetDbName(),
|
||||
ut.req.Namespace,
|
||||
ut.schema.CollectionSchema,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+91
-1
@@ -2648,6 +2648,30 @@ func assignChannelsByPK(pks *schemapb.IDs, channelNames []string, insertMsg *msg
|
||||
return channel2RowOffsets, nil
|
||||
}
|
||||
|
||||
func assignChannelsByNamespace(namespace string, channelNames []string, insertMsg *msgstream.InsertMsg) (map[string][]int, error) {
|
||||
if len(channelNames) == 0 {
|
||||
return nil, merr.WrapErrServiceInternalMsg("no virtual channels available for namespace sharding")
|
||||
}
|
||||
channelID := typeutil.HashNamespace2Channels(namespace, channelNames)
|
||||
return assignChannelsByChannel(channelID, channelNames, insertMsg), nil
|
||||
}
|
||||
|
||||
func assignChannelsByChannel(channelID uint32, channelNames []string, insertMsg *msgstream.InsertMsg) map[string][]int {
|
||||
insertMsg.HashValues = make([]uint32, insertMsg.NumRows)
|
||||
for i := range insertMsg.HashValues {
|
||||
insertMsg.HashValues[i] = channelID
|
||||
}
|
||||
|
||||
channelName := channelNames[channelID]
|
||||
channel2RowOffsets := map[string][]int{
|
||||
channelName: make([]int, 0, insertMsg.NRows()),
|
||||
}
|
||||
for i := range insertMsg.HashValues {
|
||||
channel2RowOffsets[channelName] = append(channel2RowOffsets[channelName], i)
|
||||
}
|
||||
return channel2RowOffsets
|
||||
}
|
||||
|
||||
func assignPartitionKeys(ctx context.Context, dbName string, collName string, keys []*planpb.GenericValue) ([]string, error) {
|
||||
partitionNames, err := globalMetaCache.GetPartitionsIndex(ctx, dbName, collName)
|
||||
if err != nil {
|
||||
@@ -2668,6 +2692,16 @@ func assignPartitionKeys(ctx context.Context, dbName string, collName string, ke
|
||||
return hashedPartitionNames, err
|
||||
}
|
||||
|
||||
func assignNamespacePartitionKey(ctx context.Context, dbName string, collName string, namespace *string) ([]string, error) {
|
||||
if namespace == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return assignPartitionKeys(ctx, dbName, collName, []*planpb.GenericValue{
|
||||
{Val: &planpb.GenericValue_StringVal{StringVal: *namespace}},
|
||||
})
|
||||
}
|
||||
|
||||
func ErrWithLog(logger *mlog.Logger, msg string, err error) error {
|
||||
wrapErr := errors.Wrap(err, msg)
|
||||
if logger != nil {
|
||||
@@ -2745,8 +2779,64 @@ func checkDynamicFieldDataForPartialUpdate(schema *schemapb.CollectionSchema, in
|
||||
return doCheckDynamicFieldData(schema, insertMsg, true)
|
||||
}
|
||||
|
||||
func namespaceShardingEnabled(schema *schemapb.CollectionSchema) bool {
|
||||
if schema == nil || !schema.GetEnableNamespace() {
|
||||
return false
|
||||
}
|
||||
enabled, err := common.IsNamespaceShardingEnabled(schema.GetProperties()...)
|
||||
return err == nil && enabled
|
||||
}
|
||||
|
||||
func namespacePartitionKeyMode(schema *schemapb.CollectionSchema) bool {
|
||||
return schema != nil && schema.GetEnableNamespace() && common.IsNamespaceModePartitionKey(schema.GetProperties()...)
|
||||
}
|
||||
|
||||
func namespacePartitionKeyModeEnabled(schema *schemapb.CollectionSchema) bool {
|
||||
return namespaceShardingEnabled(schema) && namespacePartitionKeyMode(schema)
|
||||
}
|
||||
|
||||
func namespacePartitionModeEnabled(schema *schemapb.CollectionSchema) bool {
|
||||
return schema.GetEnableNamespace() && common.IsNamespaceModePartition(schema.GetProperties()...)
|
||||
return schema != nil && schema.GetEnableNamespace() && common.IsNamespaceModePartition(schema.GetProperties()...)
|
||||
}
|
||||
|
||||
func namespaceShardingChannelID(schema *schemapb.CollectionSchema, namespace *string, channelNames []string) (uint32, bool, error) {
|
||||
if namespace == nil || !namespacePartitionKeyModeEnabled(schema) {
|
||||
return 0, false, nil
|
||||
}
|
||||
if len(channelNames) == 0 {
|
||||
return 0, false, merr.WrapErrServiceInternalMsg("no virtual channels available for namespace sharding")
|
||||
}
|
||||
return typeutil.HashNamespace2Channels(*namespace, channelNames), true, nil
|
||||
}
|
||||
|
||||
func namespaceShardingChannel(schema *schemapb.CollectionSchema, namespace *string, channelNames []string) (string, bool, error) {
|
||||
channelID, ok, err := namespaceShardingChannelID(schema, namespace, channelNames)
|
||||
if !ok || err != nil {
|
||||
return "", ok, err
|
||||
}
|
||||
return channelNames[channelID], true, nil
|
||||
}
|
||||
|
||||
func preferredNodeForChannel(preferredNodes map[string]int64, channel string) int64 {
|
||||
if preferredNodes == nil {
|
||||
return 0
|
||||
}
|
||||
preferredNodeID, ok := preferredNodes[channel]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return preferredNodeID
|
||||
}
|
||||
|
||||
func preferredNodeFromConcurrentMap(preferredNodes *typeutil.ConcurrentMap[string, int64], channel string) int64 {
|
||||
if preferredNodes == nil {
|
||||
return 0
|
||||
}
|
||||
preferredNodeID, ok := preferredNodes.Get(channel)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return preferredNodeID
|
||||
}
|
||||
|
||||
func resolveNamespacePartitionName(schema *schemapb.CollectionSchema, namespace *string, partitionName string) (string, bool, error) {
|
||||
|
||||
@@ -187,3 +187,9 @@ func RearrangePartitionsForPartitionKey(partitions map[string]int64) ([]string,
|
||||
|
||||
return partitionNames, partitionIDs, nil
|
||||
}
|
||||
|
||||
func HashNamespace2Channels(namespace string, shardNames []string) uint32 {
|
||||
numShard := uint32(len(shardNames))
|
||||
hashValue := HashString2Uint32(namespace)
|
||||
return hashValue % numShard
|
||||
}
|
||||
|
||||
+32
-8
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 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
|
||||
@@ -14,12 +16,34 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
echo "Stopping milvus..."
|
||||
PROCESS=$(ps -e | grep milvus | grep -v grep | awk '{print $1}')
|
||||
if [ -z "$PROCESS" ]; then
|
||||
echo "No milvus process"
|
||||
exit 0
|
||||
fi
|
||||
kill -15 $PROCESS
|
||||
echo "Milvus stopped"
|
||||
function filter_milvus_process() {
|
||||
awk '
|
||||
{
|
||||
pid = $1
|
||||
$1 = ""
|
||||
sub(/^[[:space:]]+/, "", $0)
|
||||
# Match Milvus commands, not tools/editors that mention the repo name.
|
||||
if ($0 ~ /^([^[:space:]]*\/)?milvus[[:space:]]+run[[:space:]]/) {
|
||||
print pid
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
function get_milvus_process() {
|
||||
ps -e -o pid= -o args= | filter_milvus_process
|
||||
}
|
||||
|
||||
function main() {
|
||||
echo "Stopping milvus..."
|
||||
PROCESS=$(get_milvus_process)
|
||||
if [ -z "$PROCESS" ]; then
|
||||
echo "No milvus process"
|
||||
exit 0
|
||||
fi
|
||||
kill -15 $PROCESS
|
||||
echo "Milvus stopped"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
|
||||
@@ -805,6 +805,68 @@ class TestMilvusClientQueryValid(TestMilvusClientV2Base):
|
||||
)
|
||||
self.drop_collection(client, collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_milvus_client_query_with_multiple_namespaces(self):
|
||||
"""
|
||||
target: test query with namespace enabled
|
||||
method: create namespace-enabled collection with multiple shards, insert rows into multiple namespaces, then query by namespace
|
||||
expected: query only returns rows from the requested namespace
|
||||
"""
|
||||
client = self._client()
|
||||
collection_name = cf.gen_collection_name_by_testcase_name()
|
||||
shards_num = 4
|
||||
self.create_collection(
|
||||
client,
|
||||
collection_name,
|
||||
default_dim,
|
||||
consistency_level="Strong",
|
||||
enable_namespace=True,
|
||||
shards_num=shards_num,
|
||||
)
|
||||
|
||||
desc = self.describe_collection(client, collection_name)[0]
|
||||
assert desc["enable_namespace"] is True
|
||||
assert desc["num_shards"] == shards_num
|
||||
|
||||
rng = np.random.default_rng(seed=19530)
|
||||
namespaces = [f"namespace_{i}" for i in range(8)]
|
||||
nb_per_namespace = 10
|
||||
ids_by_namespace = {
|
||||
namespace: [idx * 1000 + i for i in range(nb_per_namespace)]
|
||||
for idx, namespace in enumerate(namespaces)
|
||||
}
|
||||
for namespace, ids in ids_by_namespace.items():
|
||||
rows = [
|
||||
{
|
||||
default_primary_key_field_name: pk,
|
||||
default_vector_field_name: list(rng.random((1, default_dim))[0]),
|
||||
}
|
||||
for pk in ids
|
||||
]
|
||||
self.insert(client, collection_name, rows, namespace=namespace)
|
||||
|
||||
for namespace, expected_ids in ids_by_namespace.items():
|
||||
res = self.query(
|
||||
client,
|
||||
collection_name,
|
||||
filter=default_search_exp,
|
||||
output_fields=[default_primary_key_field_name],
|
||||
namespace=namespace,
|
||||
)[0]
|
||||
assert {r[default_primary_key_field_name] for r in res} == set(expected_ids)
|
||||
|
||||
first_id_per_namespace = [ids[0] for ids in ids_by_namespace.values()]
|
||||
for namespace, expected_ids in ids_by_namespace.items():
|
||||
filtered = self.query(
|
||||
client,
|
||||
collection_name,
|
||||
filter=f"{default_primary_key_field_name} in {first_id_per_namespace}",
|
||||
output_fields=[default_primary_key_field_name],
|
||||
namespace=namespace,
|
||||
)[0]
|
||||
assert {r[default_primary_key_field_name] for r in filtered} == {expected_ids[0]}
|
||||
self.drop_collection(client, collection_name)
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_milvus_client_query_output_fields(self):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user