Repository: milvus-io/milvus-sdk-go (go)

Issue:
[Bug]: Search with binary vector
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

`vector2Placeholder` considers `entity.Vector` as `FloatVector`, and uses ` server.PlaceholderType_FloatVector` as its type in placeholder. 
But it may be a `BinaryVector`.

``` go
func vector2Placeholder(vectors []entity.Vector) *server.PlaceholderValue {
	ph := &server.PlaceholderValue{
		Tag:    "$0",
		Type:   server.PlaceholderType_FloatVector,
		Values: make([][]byte, 0, len(vectors)),
	}
	for _, vector := range vectors {
		ph.Values = append(ph.Values, vector.Serialize())
	}
	return ph
}

```

### Expected Behavior

_No response_

### Steps To Reproduce

_No response_

### Environment

_No response_

### Anything else?

_No response_

Pull request:
Fix PlaceHolderValue type is wrong when searching binary vector
Fix #278

/kind bug

Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>

Patch:
diff --git a/client/client_grpc_data.go b/client/client_grpc_data.go
index 534f7e4..c625a44 100644
--- a/client/client_grpc_data.go
+++ b/client/client_grpc_data.go
@@ -266,11 +266,17 @@ func (c *grpcClient) Search(ctx context.Context, collName string, partitions []s
 	}
 	dimStr := vfDef.TypeParams[entity.TypeParamDim]
 	for _, vector := range vectors {
+		if vector.FieldType() != vfDef.DataType {
+			return nil, fmt.Errorf("vector %s shall be type of %s, but input found type:%s", vectorField,
+				vfDef.DataType.String(), vector.FieldType().String())
+		}
+
 		if fmt.Sprintf("%d", vector.Dim()) != dimStr {
 			return nil, fmt.Errorf("vector %s has dim of %s while found search vector with dim %d", vectorField,
 				dimStr, vector.Dim())
 		}
 	}
+
 	switch vfDef.DataType {
 	case entity.FieldTypeFloatVector:
 		if metricType != entity.IP && metricType != entity.L2 {
@@ -288,7 +294,7 @@ func (c *grpcClient) Search(ctx context.Context, collName string, partitions []s
 	}
 
 	// 2. Request milvus service
-	reqs := splitSearchRequest(coll.Schema, partitions, expr, outputFields, vectors, vectorField, metricType, topK, sp, option)
+	reqs := splitSearchRequest(coll.Schema, partitions, expr, outputFields, vectors, vfDef.DataType, vectorField, metricType, topK, sp, option)
 	if len(reqs) == 0 {
 		return nil, errors.New("empty request generated")
 	}
@@ -451,7 +457,7 @@ func getPKField(schema *entity.Schema) *entity.Field {
 }
 
 func splitSearchRequest(sch *entity.Schema, partitions []string,
-	expr string, outputFields []string, vectors []entity.Vector, vectorField string,
+	expr string, outputFields []string, vectors []entity.Vector, fieldType entity.FieldType, vectorField string,
 	metricType entity.MetricType, topK int, sp entity.SearchParam, opt *SearchQueryOption) []*server.SearchRequest {
 	params := sp.Params()
 	bs, _ := json.Marshal(params)
@@ -478,7 +484,7 @@ func splitSearchRequest(sch *entity.Schema, partitions []string,
 			CollectionName:     sch.CollectionName,
 			PartitionNames:     partitions,
 			Dsl:                expr,
-			PlaceholderGroup:   vector2PlaceholderGroupBytes(batchVectors),
+			PlaceholderGroup:   vector2PlaceholderGroupBytes(batchVectors, fieldType),
 			DslType:            common.DslType_BoolExprV1,
 			OutputFields:       outputFields,
 			SearchParams:       searchParams,
@@ -684,10 +690,10 @@ func columnToVectorsArray(collName string, partitions []string, column entity.Co
 	return result
 }
 
-func vector2PlaceholderGroupBytes(vectors []entity.Vector) []byte {
+func vector2PlaceholderGroupBytes(vectors []entity.Vector, fieldType entity.FieldType) []byte {
 	phg := &server.PlaceholderGroup{
 		Placeholders: []*server.PlaceholderValue{
-			vector2Placeholder(vectors),
+			vector2Placeholder(vectors, fieldType),
 		},
 	}
 
@@ -695,10 +701,17 @@ func vector2PlaceholderGroupBytes(vectors []entity.Vector) []byte {
 	return bs
 }
 
-func vector2Placeholder(vectors []entity.Vector) *server.PlaceholderValue {
+func vector2Placeholder(vectors []entity.Vector, fieldType entity.FieldType) *server.PlaceholderValue {
+	var placeHolderType server.PlaceholderType
+	switch fieldType {
+	case entity.FieldTypeFloatVector:
+		placeHolderType = server.PlaceholderType_FloatVector
+	case entity.FieldTypeBinaryVector:
+		placeHolderType = server.PlaceholderType_BinaryVector
+	}
 	ph := &server.PlaceholderValue{
 		Tag:    "$0",
-		Type:   server.PlaceholderType_FloatVector,
+		Type:   placeHolderType,
 		Values: make([][]byte, 0, len(vectors)),
 	}
 	for _, vector := range vectors {
diff --git a/entity/columns.go b/entity/columns.go
index b79a276..fed71ea 100644
--- a/entity/columns.go
+++ b/entity/columns.go
@@ -35,16 +35,22 @@ type Column interface {
 type Vector interface {
 	Dim() int
 	Serialize() []byte
+	FieldType() FieldType
 }
 
-// FloatVector float32 vector wrapper
+// FloatVector float32 vector wrapper.
 type FloatVector []float32
 
-// Dim returns vector dimension
+// Dim returns vector dimension.
 func (fv FloatVector) Dim() int {
 	return len(fv)
 }
 
+// FieldType returns coresponding field type.
+func (fv FloatVector) FieldType() FieldType {
+	return FieldTypeFloatVector
+}
+
 // Serialize serializes vector into byte slice, used in search placeholder
 // LittleEndian is used for convention
 func (fv FloatVector) Serialize() []byte {
@@ -70,6 +76,11 @@ func (bv BinaryVector) Serialize() []byte {
 	return bv
 }
 
+// FieldType returns coresponding field type.
+func (bv BinaryVector) FieldType() FieldType {
+	return FieldTypeBinaryVector
+}
+
 var errFieldDataTypeNotMatch = errors.New("FieldData type not matched")
 
 // IDColumns converts schema.IDs to corresponding column


Test patch:
diff --git a/client/client_grpc_data_test.go b/client/client_grpc_data_test.go
index 934bf71..f2d205d 100644
--- a/client/client_grpc_data_test.go
+++ b/client/client_grpc_data_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/milvus-io/milvus-sdk-go/v2/internal/proto/schema"
 	"github.com/milvus-io/milvus-sdk-go/v2/internal/proto/server"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestGrpcClientInsert(t *testing.T) {
@@ -301,6 +302,13 @@ func TestGrpcSearch(t *testing.T) {
 		assert.Nil(t, r)
 		assert.NotNil(t, err)
 
+		// wrong vector type
+		binaryVector := generateBinaryVector(1, testVectorDim)
+		r, err = c.Search(ctx, testCollectionName, []string{}, "", []string{"int64"}, []entity.Vector{entity.BinaryVector(binaryVector[0])}, "vector",
+			entity.L2, 5, sp)
+		assert.Nil(t, r)
+		assert.Error(t, err)
+
 		// metric type
 		r, err = c.Search(ctx, testCollectionName, []string{}, "", []string{"int64"}, []entity.Vector{entity.FloatVector(vectors[0])}, "vector",
 			entity.HAMMING, 5, sp)
@@ -1056,3 +1064,37 @@ func generateBinaryVector(num, dim int) [][]byte {
 	}
 	return r
 }
+
+func TestVector2PlaceHolder(t *testing.T) {
+	t.Run("FloatVector", func(t *testing.T) {
+		data := generateFloatVector(10, 32)
+		vectors := make([]entity.Vector, 0, len(data))
+		for _, row := range data {
+			vectors = append(vectors, entity.FloatVector(row))
+		}
+
+		phv := vector2Placeholder(vectors, entity.FieldTypeFloatVector)
+		assert.Equal(t, "$0", phv.Tag)
+		assert.Equal(t, server.PlaceholderType_FloatVector, phv.Type)
+		require.Equal(t, len(vectors), len(phv.Values))
+		for idx, line := range phv.Values {
+			assert.Equal(t, vectors[idx].Serialize(), line)
+		}
+	})
+
+	t.Run("BinaryVector", func(t *testing.T) {
+		data := generateBinaryVector(10, 32)
+		vectors := make([]entity.Vector, 0, len(data))
+		for _, row := range data {
+			vectors = append(vectors, entity.BinaryVector(row))
+		}
+
+		phv := vector2Placeholder(vectors, entity.FieldTypeBinaryVector)
+		assert.Equal(t, "$0", phv.Tag)
+		assert.Equal(t, server.PlaceholderType_BinaryVector, phv.Type)
+		require.Equal(t, len(vectors), len(phv.Values))
+		for idx, line := range phv.Values {
+			assert.Equal(t, vectors[idx].Serialize(), line)
+		}
+	})
+}