192 lines
4.4 KiB
Go
192 lines
4.4 KiB
Go
package face
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"lone-services/pkg/utils"
|
|
|
|
"github.com/milvus-io/milvus-sdk-go/v2/client"
|
|
"github.com/milvus-io/milvus-sdk-go/v2/entity"
|
|
"github.com/zeromicro/go-zero/core/logx"
|
|
)
|
|
|
|
const (
|
|
collectionName = "face_vectors"
|
|
faceVectorField = "face_vector"
|
|
faceUserIDField = "user_id"
|
|
searchTopK = 1
|
|
similarityMin = float32(0.55)
|
|
hnswM = 16
|
|
hnswEfConstruct = 200
|
|
hnswEf = 64
|
|
collectionShards = 2
|
|
)
|
|
|
|
var (
|
|
milvusMu sync.RWMutex
|
|
milvusClient client.Client
|
|
)
|
|
|
|
func InitMilvus() error {
|
|
ctx := context.Background()
|
|
host := utils.GetConfigString("milvus.milvus_host")
|
|
port := utils.GetConfigString("milvus.milvus_grpc_port")
|
|
if host == utils.StringEmpty || port == utils.StringEmpty {
|
|
return fmt.Errorf("milvus config empty")
|
|
}
|
|
address := fmt.Sprintf("%s:%s", host, port)
|
|
c, err := client.NewGrpcClient(ctx, address)
|
|
if err != nil {
|
|
return fmt.Errorf("connect milvus %s: %w", address, err)
|
|
}
|
|
if err = ensureCollection(ctx, c); err != nil {
|
|
_ = c.Close()
|
|
return err
|
|
}
|
|
if err = ensureIndex(ctx, c); err != nil {
|
|
_ = c.Close()
|
|
return err
|
|
}
|
|
if err = c.LoadCollection(ctx, collectionName, false); err != nil {
|
|
_ = c.Close()
|
|
return err
|
|
}
|
|
|
|
milvusMu.Lock()
|
|
if milvusClient != nil {
|
|
_ = milvusClient.Close()
|
|
}
|
|
milvusClient = c
|
|
milvusMu.Unlock()
|
|
logx.Infof("milvus connected: %s", address)
|
|
return nil
|
|
}
|
|
|
|
func CloseMilvus() {
|
|
milvusMu.Lock()
|
|
defer milvusMu.Unlock()
|
|
if milvusClient != nil {
|
|
_ = milvusClient.Close()
|
|
milvusClient = nil
|
|
}
|
|
}
|
|
|
|
func getMilvus() client.Client {
|
|
milvusMu.RLock()
|
|
defer milvusMu.RUnlock()
|
|
return milvusClient
|
|
}
|
|
|
|
func ensureCollection(ctx context.Context, c client.Client) error {
|
|
has, err := c.HasCollection(ctx, collectionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if has {
|
|
return nil
|
|
}
|
|
schema := &entity.Schema{
|
|
CollectionName: collectionName,
|
|
Description: "user face vectors (ArcFace 512d)",
|
|
AutoID: false,
|
|
Fields: []*entity.Field{
|
|
{
|
|
Name: faceUserIDField,
|
|
DataType: entity.FieldTypeInt64,
|
|
PrimaryKey: true,
|
|
},
|
|
{
|
|
Name: faceVectorField,
|
|
DataType: entity.FieldTypeFloatVector,
|
|
TypeParams: map[string]string{
|
|
"dim": fmt.Sprintf("%d", faceEmbeddingDim),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return c.CreateCollection(ctx, schema, int32(collectionShards))
|
|
}
|
|
|
|
func ensureIndex(ctx context.Context, c client.Client) error {
|
|
indexes, err := c.DescribeIndex(ctx, collectionName, faceVectorField)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "index not found") {
|
|
return err
|
|
}
|
|
} else if len(indexes) > utils.NumberZero {
|
|
return nil
|
|
}
|
|
idx, err := entity.NewIndexHNSW(entity.IP, hnswM, hnswEfConstruct)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.CreateIndex(ctx, collectionName, faceVectorField, idx, false)
|
|
}
|
|
|
|
func UpsertFace(userID int64, vector []float32) error {
|
|
c := getMilvus()
|
|
if c == nil {
|
|
return errors.New("milvus client is nil")
|
|
}
|
|
if userID < utils.NumberOne || len(vector) != faceEmbeddingDim {
|
|
return errors.New("invalid face upsert args")
|
|
}
|
|
ctx := context.Background()
|
|
_ = c.Delete(ctx, collectionName, "", fmt.Sprintf("%s == %d", faceUserIDField, userID))
|
|
_, err := c.Insert(ctx, collectionName, "",
|
|
entity.NewColumnInt64(faceUserIDField, []int64{userID}),
|
|
entity.NewColumnFloatVector(faceVectorField, faceEmbeddingDim, [][]float32{vector}),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func SearchUser(vector []float32) (int64, float32, error) {
|
|
c := getMilvus()
|
|
if c == nil {
|
|
return 0, 0, errors.New("milvus client is nil")
|
|
}
|
|
if len(vector) != faceEmbeddingDim {
|
|
return 0, 0, errors.New("invalid face vector")
|
|
}
|
|
ctx := context.Background()
|
|
searchParam, err := entity.NewIndexHNSWSearchParam(hnswEf)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
result, err := c.Search(
|
|
ctx,
|
|
collectionName,
|
|
nil,
|
|
"",
|
|
[]string{faceUserIDField},
|
|
[]entity.Vector{entity.FloatVector(vector)},
|
|
faceVectorField,
|
|
entity.IP,
|
|
searchTopK,
|
|
searchParam,
|
|
)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if len(result) == utils.NumberZero || result[0].IDs.Len() == utils.NumberZero {
|
|
return 0, 0, errors.New("face not found")
|
|
}
|
|
ids, ok := result[0].IDs.(*entity.ColumnInt64)
|
|
if !ok {
|
|
return 0, 0, errors.New("invalid milvus id column")
|
|
}
|
|
userID, err := ids.ValueByIdx(0)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
score := result[0].Scores[0]
|
|
if score < similarityMin {
|
|
return 0, score, errors.New("face similarity too low")
|
|
}
|
|
return userID, score, nil
|
|
}
|