feat: analysis admin
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user