Files
wehub-resource-sync 498b235461
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
chore: import upstream snapshot with attribution
2026-07-13 12:31:17 +08:00

1514 lines
50 KiB
Go

// 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 delegator
import (
"context"
"fmt"
"runtime"
"time"
"github.com/cockroachdb/errors"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
"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/msgpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/querynodev2/cluster"
"github.com/milvus-io/milvus/internal/querynodev2/delegator/deletebuffer"
"github.com/milvus-io/milvus/internal/querynodev2/pkoracle"
"github.com/milvus-io/milvus/internal/querynodev2/segments"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
"github.com/milvus-io/milvus/internal/util/function"
"github.com/milvus-io/milvus/internal/util/grpcclient"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/metrics"
"github.com/milvus-io/milvus/pkg/v3/mlog"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/proto/internalpb"
"github.com/milvus-io/milvus/pkg/v3/proto/querypb"
"github.com/milvus-io/milvus/pkg/v3/proto/segcorepb"
"github.com/milvus-io/milvus/pkg/v3/util/commonpbutil"
"github.com/milvus-io/milvus/pkg/v3/util/conc"
"github.com/milvus-io/milvus/pkg/v3/util/funcutil"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/retry"
"github.com/milvus-io/milvus/pkg/v3/util/timerecord"
"github.com/milvus-io/milvus/pkg/v3/util/tsoutil"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
// delegator data related part
const defaultAnalyzerName = "default"
func normalizeAnalyzerNames(analyzerNames []string, textNum int) ([]string, error) {
if textNum == 0 {
return []string{}, nil
}
switch len(analyzerNames) {
case 0:
names := make([]string, textNum)
for i := range names {
names[i] = defaultAnalyzerName
}
return names, nil
case 1:
name := analyzerNames[0]
if name == "" {
name = defaultAnalyzerName
}
names := make([]string, textNum)
for i := range names {
names[i] = name
}
return names, nil
case textNum:
names := append([]string(nil), analyzerNames...)
for i, name := range names {
if name == "" {
names[i] = defaultAnalyzerName
}
}
return names, nil
default:
return nil, merr.WrapErrParameterInvalidMsg("analyzer names size must be 0, 1, or equal to text size, got analyzer names size [%d], text size [%d]", len(analyzerNames), textNum)
}
}
func normalizeHighlightAnalyzerNames(analyzerNames []string, textNum int) ([]string, error) {
if len(analyzerNames) != textNum {
return nil, merr.WrapErrServiceInternalMsg("highlight analyzer names size must equal text size, got analyzer names size [%d], text size [%d]", len(analyzerNames), textNum)
}
names := append([]string(nil), analyzerNames...)
for i, name := range names {
if name == "" {
names[i] = defaultAnalyzerName
}
}
return names, nil
}
// segmentEffectiveTs returns the timestamp for delete-buffer pin/ListAfter.
// For import segments with commit_timestamp, only deletes from T_commit onwards
// are applied via the buffer (pre-commit deletes are in the delta log or L0 path).
func segmentEffectiveTs(info *querypb.SegmentLoadInfo) uint64 {
if ts := info.GetCommitTimestamp(); ts != 0 {
return ts
}
return info.GetStartPosition().GetTimestamp()
}
// InsertData
type InsertData struct {
RowIDs []int64
PrimaryKeys []storage.PrimaryKey
Timestamps []uint64
InsertRecord *segcorepb.InsertRecord
BM25Stats map[int64]*storage.BM25Stats
StartPosition *msgpb.MsgPosition
PartitionID int64
}
type DeleteData struct {
PartitionID int64
PrimaryKeys []storage.PrimaryKey
Timestamps []uint64
RowCount int64
}
type DeleteBatch struct {
Ts uint64
Data []*DeleteData
}
// Append appends another delete data into this one.
func (d *DeleteData) Append(ad DeleteData) {
d.PrimaryKeys = append(d.PrimaryKeys, ad.PrimaryKeys...)
d.Timestamps = append(d.Timestamps, ad.Timestamps...)
d.RowCount += ad.RowCount
}
// ProcessInsert handles insert data in delegator.
func (sd *shardDelegator) ProcessInsert(insertRecords map[int64]*InsertData) {
method := "ProcessInsert"
tr := timerecord.NewTimeRecorder(method)
log := sd.getLogger(context.Background())
for segmentID, insertData := range insertRecords {
growing := sd.segmentManager.GetGrowing(segmentID)
newGrowingSegment := false
if growing == nil {
var err error
// TODO: It's a wired implementation that growing segment have load info.
// we should separate the growing segment and sealed segment by type system.
growing, err = segments.NewSegment(
context.Background(),
sd.collection,
sd.segmentManager,
segments.SegmentTypeGrowing,
0,
&querypb.SegmentLoadInfo{
SegmentID: segmentID,
PartitionID: insertData.PartitionID,
CollectionID: sd.collectionID,
InsertChannel: sd.vchannelName,
StartPosition: insertData.StartPosition,
DeltaPosition: insertData.StartPosition,
Level: datapb.SegmentLevel_L1,
},
)
if err != nil {
log.Error(context.TODO(), "failed to create new segment",
mlog.FieldSegmentID(segmentID),
mlog.Err(err))
panic(err)
}
newGrowingSegment = true
}
err := growing.Insert(context.Background(), insertData.RowIDs, insertData.Timestamps, insertData.InsertRecord)
if err != nil {
log.Error(context.TODO(), "failed to insert data into growing segment",
mlog.FieldSegmentID(segmentID),
mlog.Err(err),
)
if errors.IsAny(err, merr.ErrSegmentNotLoaded, merr.ErrSegmentNotFound) {
log.Warn(context.TODO(), "try to insert data into released segment, skip it", mlog.Err(err))
continue
}
// panic here, insert failure
panic(err)
}
growing.UpdatePkCandidate(insertData.PrimaryKeys)
if newGrowingSegment {
sd.growingSegmentLock.Lock()
// Forbid create growing segment in excluded segment
// (Now excluded ts may not worked for growing segment with multiple partition.
// Because use checkpoint ts as excluded ts when add excluded, but it may less than last message ts.
// And cause some invalid message not filtered out and create growing again.
// So we forbid all segment in excluded segment create here.)
// TODO:
// Use right ts when add excluded segment. And Verify with insert ts here.
if ok := sd.VerifyExcludedSegments(segmentID, 0); !ok {
log.Warn(context.TODO(), "try to insert data into released segment, skip it", mlog.FieldSegmentID(segmentID))
sd.growingSegmentLock.Unlock()
growing.Release(context.Background())
continue
}
if !sd.distribution.GrowingSegmentExists(segmentID) {
// register created growing segment after insert, avoid to add empty growing to delegator
if idfOracle := sd.getIDFOracle(); idfOracle != nil {
idfOracle.RegisterGrowing(segmentID, insertData.BM25Stats)
}
sd.segmentManager.Put(context.Background(), segments.SegmentTypeGrowing, growing)
sd.addGrowing(SegmentEntry{
NodeID: paramtable.GetNodeID(),
SegmentID: segmentID,
PartitionID: insertData.PartitionID,
Version: 0,
TargetVersion: initialTargetVersion,
Candidate: growing, // growing segment itself is the Candidate
})
}
sd.growingSegmentLock.Unlock()
} else if idfOracle := sd.getIDFOracle(); idfOracle != nil {
idfOracle.UpdateGrowing(growing.ID(), insertData.BM25Stats)
}
log.Info(context.TODO(), "insert into growing segment",
mlog.FieldCollectionID(growing.Collection()),
mlog.FieldSegmentID(segmentID),
mlog.Int("rowCount", len(insertData.RowIDs)),
mlog.Uint64("maxTimestamp", insertData.Timestamps[len(insertData.Timestamps)-1]),
)
}
metrics.QueryNodeProcessCost.WithLabelValues(paramtable.GetStringNodeID(), metrics.InsertLabel).
Observe(float64(tr.ElapseSpan().Milliseconds()))
}
// ProcessDelete handles delete data in delegator.
// delegator puts deleteData into buffer first,
// then dispatch data to segments according to the result of bloom filter check.
func (sd *shardDelegator) ProcessDelete(deleteData []*DeleteData, ts uint64) {
sd.ProcessDeleteBatches([]DeleteBatch{{Ts: ts, Data: deleteData}})
}
func (sd *shardDelegator) ProcessDeleteBatches(batches []DeleteBatch) {
// Early return if delegator is stopped - ProcessDelete becomes a no-op
// This prevents unnecessary processing and side effects during shutdown
if sd.Stopped() {
return
}
if len(batches) == 0 {
return
}
method := "ProcessDelete"
tr := timerecord.NewTimeRecorder(method)
// block load segment handle delete buffer
sd.deleteMut.Lock()
defer sd.deleteMut.Unlock()
log := sd.getLogger(context.Background())
log.Debug(context.TODO(), "start to process delete batches", mlog.Int("batchNum", len(batches)))
allDeleteData := make([]*DeleteData, 0, len(batches))
for _, batch := range batches {
if len(batch.Data) == 0 {
continue
}
cacheItems := make([]deletebuffer.BufferItem, 0, len(batch.Data))
for _, entry := range batch.Data {
cacheItems = append(cacheItems, deletebuffer.BufferItem{
PartitionID: entry.PartitionID,
DeleteData: storage.DeleteData{
Pks: entry.PrimaryKeys,
Tss: entry.Timestamps,
RowCount: entry.RowCount,
},
})
}
sd.deleteBuffer.Put(&deletebuffer.Item{
Ts: batch.Ts,
Data: cacheItems,
})
allDeleteData = append(allDeleteData, batch.Data...)
}
if len(allDeleteData) > 0 {
sd.forwardStreamingDeletion(context.Background(), allDeleteData)
}
metrics.QueryNodeProcessCost.WithLabelValues(paramtable.GetStringNodeID(), metrics.DeleteLabel).
Observe(float64(tr.ElapseSpan().Milliseconds()))
}
type BatchApplyRet = struct {
DeleteDataIdx int
StartIdx int
Segment2Hits map[int64][]bool
}
// applyBFInParallel applies bloom filter check in parallel on the provided pinned segments.
// Using pinned segments ensures consistency between BF check and delete application,
// preventing race conditions where new segments could be added between PinOnlineSegments
// and this call.
func (sd *shardDelegator) applyBFInParallel(deleteDatas []*DeleteData, pool *conc.Pool[any], sealed []SnapshotItem, growing []SegmentEntry) *typeutil.ConcurrentMap[int, *BatchApplyRet] {
retIdx := 0
retMap := typeutil.NewConcurrentMap[int, *BatchApplyRet]()
batchSize := paramtable.Get().CommonCfg.BloomFilterApplyBatchSize.GetAsInt()
var futures []*conc.Future[any]
for didx, data := range deleteDatas {
pks := data.PrimaryKeys
for idx := 0; idx < len(pks); idx += batchSize {
startIdx := idx
endIdx := startIdx + batchSize
if endIdx > len(pks) {
endIdx = len(pks)
}
retIdx += 1
tmpRetIndex := retIdx
deleteDataId := didx
partitionID := data.PartitionID
future := pool.Submit(func() (any, error) {
ret := BatchGetFromSegments(pks[startIdx:endIdx], partitionID, sealed, growing)
retMap.Insert(tmpRetIndex, &BatchApplyRet{
DeleteDataIdx: deleteDataId,
StartIdx: startIdx,
Segment2Hits: ret,
})
return nil, nil
})
futures = append(futures, future)
}
}
conc.AwaitAll(futures...)
return retMap
}
// applyDelete handles delete record and apply them to corresponding workers.
func (sd *shardDelegator) applyDelete(ctx context.Context,
nodeID int64,
worker cluster.Worker,
delRecords func(segmentID int64) (DeleteData, bool),
entries []SegmentEntry,
scope querypb.DataScope,
) []int64 {
offlineSegments := typeutil.NewConcurrentSet[int64]()
log := sd.getLogger(ctx)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
pool := conc.NewPool[struct{}](runtime.GOMAXPROCS(0) * 4)
defer pool.Release()
var futures []*conc.Future[struct{}]
for _, segmentEntry := range entries {
segmentEntry := segmentEntry
delRecord, ok := delRecords(segmentEntry.SegmentID)
log := log.With(
mlog.FieldSegmentID(segmentEntry.SegmentID),
mlog.Int64("workerID", nodeID),
mlog.Int("forwardRowCount", len(delRecord.PrimaryKeys)),
)
if ok {
future := pool.Submit(func() (struct{}, error) {
log.Debug(ctx, "delegator plan to applyDelete via worker")
err := retry.Handle(ctx, func() (bool, error) {
if sd.Stopped() {
return false, merr.WrapErrChannelNotAvailable(sd.vchannelName, "channel is unsubscribing")
}
err := worker.Delete(ctx, &querypb.DeleteRequest{
Base: commonpbutil.NewMsgBase(commonpbutil.WithTargetID(nodeID)),
CollectionId: sd.collectionID,
PartitionId: segmentEntry.PartitionID,
VchannelName: sd.vchannelName,
SegmentId: segmentEntry.SegmentID,
PrimaryKeys: storage.ParsePrimaryKeys2IDs(delRecord.PrimaryKeys),
Timestamps: delRecord.Timestamps,
Scope: scope,
})
if errors.Is(err, merr.ErrNodeNotFound) {
log.Warn(ctx, "try to delete data on non-exist node")
// cancel other request
cancel()
return false, err
} else if grpcclient.IsServerIDMismatchErr(err) {
log.Warn(ctx, "try to delete data on mismatched node, node has been replaced", mlog.Err(err))
cancel()
return false, err
} else if errors.IsAny(err, merr.ErrSegmentNotFound, merr.ErrSegmentNotLoaded) {
log.Warn(ctx, "try to delete data of released segment")
return false, nil
} else if err != nil {
log.Warn(ctx, "worker failed to delete on segment", mlog.Err(err))
return true, err
}
return false, nil
}, retry.Attempts(10))
if err != nil {
log.Warn(ctx, "apply delete for segment failed, marking it offline")
offlineSegments.Insert(segmentEntry.SegmentID)
}
return struct{}{}, err
})
futures = append(futures, future)
}
}
conc.AwaitAll(futures...)
return offlineSegments.Collect()
}
// markSegmentOffline makes segment go offline and waits for QueryCoord to fix.
func (sd *shardDelegator) markSegmentOffline(segmentIDs ...int64) {
sd.distribution.MarkOfflineSegments(segmentIDs...)
}
// addGrowing add growing segment record for delegator.
func (sd *shardDelegator) addGrowing(entries ...SegmentEntry) {
log := sd.getLogger(context.Background())
log.Info(context.TODO(), "add growing segments to delegator", mlog.Int64s("segmentIDs", lo.Map(entries, func(entry SegmentEntry, _ int) int64 {
return entry.SegmentID
})))
sd.distribution.AddGrowing(entries...)
}
// LoadGrowing load growing segments locally.
func (sd *shardDelegator) LoadGrowing(ctx context.Context, infos []*querypb.SegmentLoadInfo, version int64) error {
log := sd.getLogger(ctx)
segmentIDs := lo.Map(infos, func(info *querypb.SegmentLoadInfo, _ int) int64 { return info.GetSegmentID() })
log.Info(ctx, "loading growing segments...", mlog.Int64s("segmentIDs", segmentIDs))
loaded, err := sd.loader.Load(ctx, sd.collectionID, segments.SegmentTypeGrowing, version, infos...)
if err != nil {
log.Warn(ctx, "failed to load growing segment", mlog.Err(err))
return err
}
for _, segment := range loaded {
err = sd.addL0ForGrowing(ctx, segment)
if err != nil {
log.Warn(ctx, "failed to forward L0 deletions to growing segment",
mlog.Err(err),
)
// clear loaded growing segments
for _, segment := range loaded {
segment.Release(ctx)
}
return err
}
}
segmentIDs = lo.Map(loaded, func(segment segments.Segment, _ int) int64 { return segment.ID() })
log.Info(ctx, "load growing segments done", mlog.Int64s("segmentIDs", segmentIDs))
if idfOracle := sd.getIDFOracle(); idfOracle != nil {
for _, segment := range loaded {
idfOracle.RegisterGrowing(segment.ID(), segment.GetBM25Stats())
}
}
sd.addGrowing(lo.Map(loaded, func(segment segments.Segment, _ int) SegmentEntry {
return SegmentEntry{
NodeID: paramtable.GetNodeID(),
SegmentID: segment.ID(),
PartitionID: segment.Partition(),
Version: version,
TargetVersion: sd.distribution.getTargetVersion(),
Candidate: segment, // growing segment itself is the Candidate
}
})...)
return nil
}
// load bm25 stats for sealed segments.
// idf oracle owns the full lifecycle: download, disk write, register, cleanup.
func (sd *shardDelegator) loadBM25Stats(ctx context.Context, infos []*querypb.SegmentLoadInfo, req *querypb.LoadSegmentsRequest) error {
idfOracle := sd.getIDFOracle()
if idfOracle == nil {
return nil
}
pool := segments.GetLoadPool()
cm := sd.loader.GetChunkManager()
futures := make([]*conc.Future[any], 0, len(infos))
for _, info := range infos {
info := info
futures = append(futures, pool.Submit(func() (any, error) {
if err := idfOracle.LoadSealed(ctx, info.GetSegmentID(), info, cm); err != nil {
mlog.Warn(ctx, "failed to load bm25 stats for segment",
mlog.FieldCollectionID(req.GetCollectionID()),
mlog.FieldSegmentID(info.GetSegmentID()),
mlog.Err(err))
return nil, err
}
return nil, nil
}))
}
err := conc.BlockOnAll(futures...)
if err != nil {
mlog.Warn(ctx, "failed to load bm25 stats", mlog.Err(err))
return err
}
return nil
}
func (sd *shardDelegator) handleReopenPostLoad(ctx context.Context, req *querypb.LoadSegmentsRequest) error {
log := sd.getLogger(ctx).With(
mlog.Int64s("segments", lo.Map(req.GetInfos(), func(info *querypb.SegmentLoadInfo, _ int) int64 { return info.GetSegmentID() })),
mlog.String("loadScope", req.GetLoadScope().String()),
)
infosWithBM25Stats := make([]*querypb.SegmentLoadInfo, 0, len(req.GetInfos()))
for _, info := range req.GetInfos() {
bm25Paths, err := packed.NewStatsResolverFromLoadInfo(info).BM25StatsPaths()
if err != nil {
log.Warn(ctx, "resolve reopened bm25 stats failed", mlog.FieldSegmentID(info.GetSegmentID()), mlog.Err(err))
return err
}
if len(bm25Paths) > 0 {
infosWithBM25Stats = append(infosWithBM25Stats, info)
}
}
if len(infosWithBM25Stats) == 0 {
return nil
}
return sd.loadBM25StatsForReopen(ctx, infosWithBM25Stats, req)
}
func (sd *shardDelegator) loadBM25StatsForReopen(ctx context.Context, infos []*querypb.SegmentLoadInfo, req *querypb.LoadSegmentsRequest) error {
idfOracle := sd.getIDFOracle()
if idfOracle == nil {
return merr.WrapErrServiceInternal("reopen contains BM25 stats before delegator BM25 oracle is initialized")
}
pool := segments.GetLoadPool()
cm := sd.loader.GetChunkManager()
futures := make([]*conc.Future[any], 0, len(infos))
for _, info := range infos {
info := info
futures = append(futures, pool.Submit(func() (any, error) {
activateIfReadable := sd.distribution.IsReadableSealedSegment(info.GetSegmentID())
if err := idfOracle.LoadSealedForReopen(ctx, info.GetSegmentID(), info, cm, activateIfReadable); err != nil {
mlog.Warn(ctx, "failed to load reopened bm25 stats for segment",
mlog.FieldCollectionID(req.GetCollectionID()),
mlog.FieldSegmentID(info.GetSegmentID()),
mlog.Bool("activateIfReadable", activateIfReadable),
mlog.Err(err))
return nil, err
}
return nil, nil
}))
}
if err := conc.BlockOnAll(futures...); err != nil {
mlog.Warn(ctx, "failed to load reopened bm25 stats", mlog.Err(err))
return err
}
return nil
}
// syncCollectionIndexMeta refreshes the delegator node's CCollection IndexMeta after a
// forwarded worker load. Worker LoadSegments already updates IndexMeta on the target
// worker, but the delegator (which executes growing search locally) must stay in sync.
func (sd *shardDelegator) syncCollectionIndexMeta(ctx context.Context, req *querypb.LoadSegmentsRequest) error {
if len(req.GetIndexInfoList()) == 0 {
return nil
}
schema := req.GetSchema()
if schema == nil {
schema = sd.collection.Schema()
}
loadMeta := req.GetLoadMeta()
if loadMeta == nil {
loadMeta = &querypb.LoadMetaInfo{
CollectionID: req.GetCollectionID(),
}
}
meta := segments.ComposeIndexMeta(ctx, req.GetIndexInfoList(), schema)
if err := sd.collectionManager.PutOrRef(req.GetCollectionID(), schema, meta, loadMeta); err != nil {
return err
}
sd.collectionManager.Unref(req.GetCollectionID(), 1)
return nil
}
// LoadSegments load segments local or remotely depends on the target node.
func (sd *shardDelegator) LoadSegments(ctx context.Context, req *querypb.LoadSegmentsRequest) error {
if len(req.GetInfos()) == 0 {
return nil
}
log := sd.getLogger(ctx)
targetNodeID := req.GetDstNodeID()
// add common log fields
log = log.With(
mlog.Int64("workID", targetNodeID),
mlog.Int64s("segments", lo.Map(req.GetInfos(), func(info *querypb.SegmentLoadInfo, _ int) int64 { return info.GetSegmentID() })),
)
if req.GetInfos()[0].GetLevel() == datapb.SegmentLevel_L0 {
return merr.WrapErrServiceInternal("load L0 segment is not supported, l0 segment should only be loaded by watchChannel")
}
// pin all segments to prevent delete buffer has been cleaned up during worker load segments
// Note: if delete records is pinned, it will skip cleanup during SyncTargetVersion
// which means after segment is loaded, then delete buffer will be cleaned up by next SyncTargetVersion call
for _, info := range req.GetInfos() {
sd.deleteBuffer.Pin(segmentEffectiveTs(info), info.GetSegmentID())
}
defer func() {
for _, info := range req.GetInfos() {
sd.deleteBuffer.Unpin(segmentEffectiveTs(info), info.GetSegmentID())
}
}()
worker, err := sd.workerManager.GetWorker(ctx, targetNodeID)
if err != nil {
log.Warn(ctx, "delegator failed to find worker", mlog.Err(err))
return err
}
req.Base.TargetID = targetNodeID
log.Debug(ctx, "worker loads segments...")
sLoad := func(ctx context.Context, req *querypb.LoadSegmentsRequest) error {
segmentID := req.GetInfos()[0].GetSegmentID()
nodeID := req.GetDstNodeID()
_, err, _ := sd.sf.Do(fmt.Sprintf("%d-%d", nodeID, segmentID), func() (struct{}, error) {
err := worker.LoadSegments(ctx, req)
return struct{}{}, err
})
return err
}
// separate infos into different load task
if len(req.GetInfos()) > 1 {
var reqs []*querypb.LoadSegmentsRequest
for _, info := range req.GetInfos() {
newReq := typeutil.Clone(req)
newReq.Infos = []*querypb.SegmentLoadInfo{info}
reqs = append(reqs, newReq)
}
group, ctx := errgroup.WithContext(ctx)
for _, req := range reqs {
req := req
group.Go(func() error {
return sLoad(ctx, req)
})
}
err = group.Wait()
} else {
err = sLoad(ctx, req)
}
if err != nil {
log.Warn(ctx, "worker failed to load segments", mlog.Err(err))
return err
}
log.Debug(ctx, "work loads segments done")
if err := sd.syncCollectionIndexMeta(ctx, req); err != nil {
log.Warn(ctx, "failed to sync collection index meta on delegator", mlog.Err(err))
return err
}
// load index segment need no stream delete and distribution change
if req.GetLoadScope() == querypb.LoadScope_Index {
return nil
}
if req.GetLoadScope() == querypb.LoadScope_Reopen {
return sd.handleReopenPostLoad(ctx, req)
}
return sd.withPostLoadLimit(ctx, func() error {
infos := lo.Filter(req.GetInfos(), func(info *querypb.SegmentLoadInfo, _ int) bool {
return !sd.distribution.SealedSegmentExistsOnNode(info.GetSegmentID(), targetNodeID)
})
candidates, err := sd.loader.LoadBloomFilterSet(ctx, req.GetCollectionID(), infos...)
if err != nil {
log.Warn(ctx, "failed to load bloom filter set for segment", mlog.Err(err))
return err
}
// Load BM25 stats BEFORE loadStreamDelete so stats are ready before segment becomes visible
err = sd.loadBM25Stats(ctx, infos, req)
if err != nil {
log.Warn(ctx, "failed to load BM25 stats", mlog.Err(err))
return err
}
// Build a map from segmentID to BloomFilterSet
bfMap := make(map[int64]pkoracle.Candidate)
for _, candidate := range candidates {
log.Info(ctx, "loaded bloom filter set for sealed segment",
mlog.FieldSegmentID(candidate.ID()),
)
bfMap[candidate.ID()] = candidate
}
// Build entries with Candidate before loadStreamDelete, which will atomically add them to distribution
entries := make([]SegmentEntry, 0, len(infos))
for _, info := range infos {
entries = append(entries, SegmentEntry{
SegmentID: info.GetSegmentID(),
PartitionID: info.GetPartitionID(),
NodeID: req.GetDstNodeID(),
Version: req.GetVersion(),
Level: info.GetLevel(),
Candidate: bfMap[info.GetSegmentID()],
})
}
log.Debug(ctx, "load delete...")
// loadStreamDelete now handles distribution add atomically in Phase 3
err = sd.loadStreamDelete(ctx, candidates, infos, req, targetNodeID, worker,
entries, req.GetLoadMeta().GetSchemaBarrierTs())
if err != nil {
log.Warn(ctx, "load stream delete failed", mlog.Err(err))
// BM25 stats already loaded into idf oracle will be cleaned up
// automatically by SyncDistribution when the segment is not in target.
return err
}
log.Debug(ctx, "load stream delete done")
return nil
})
}
func (sd *shardDelegator) withPostLoadLimit(ctx context.Context, fn func() error) error {
if sd.postLoadSem == nil {
return fn()
}
start := time.Now()
if err := sd.postLoadSem.Acquire(ctx); err != nil {
return err
}
defer sd.postLoadSem.Release()
mlog.Debug(ctx, "delegator acquired post-load slot",
mlog.Duration("wait", time.Since(start)),
mlog.Int("capacity", sd.postLoadSem.Cap()),
mlog.Int("current", sd.postLoadSem.Current()))
return fn()
}
func (sd *shardDelegator) addDistributionIfSchemaBarrierOK(schemaBarrierTs uint64, entries ...SegmentEntry) error {
sd.schemaChangeMutex.RLock()
defer sd.schemaChangeMutex.RUnlock()
if schemaBarrierTs < sd.schemaBarrierTs {
return merr.WrapErrServiceInternal("schema barrier changed")
}
// alter distribution
sd.distribution.AddDistributions(entries...)
return nil
}
// LoadGrowing load growing segments locally.
func (sd *shardDelegator) LoadL0(ctx context.Context, infos []*querypb.SegmentLoadInfo, version int64) error {
log := sd.getLogger(ctx)
segmentIDs := lo.Map(infos, func(info *querypb.SegmentLoadInfo, _ int) int64 { return info.GetSegmentID() })
log.Info(ctx, "loading l0 segments...", mlog.Int64s("segmentIDs", segmentIDs))
loaded := make([]segments.Segment, 0)
if sd.l0ForwardPolicy == L0ForwardPolicyRemoteLoad {
for _, info := range infos {
l0Seg, err := segments.NewL0Segment(sd.collection, segments.SegmentTypeSealed, version, info)
if err != nil {
return err
}
loaded = append(loaded, l0Seg)
}
} else {
var err error
loaded, err = sd.loader.Load(ctx, sd.collectionID, segments.SegmentTypeSealed, version, infos...)
if err != nil {
log.Warn(ctx, "failed to load l0 segment", mlog.Err(err))
return err
}
}
segmentIDs = lo.Map(loaded, func(segment segments.Segment, _ int) int64 { return segment.ID() })
log.Info(ctx, "load l0 segments done", mlog.Int64s("segmentIDs", segmentIDs))
sd.deleteBuffer.RegisterL0(loaded...)
// register l0 segment
sd.RefreshLevel0DeletionStats()
return nil
}
func (sd *shardDelegator) rangeHitL0Deletions(partitionID int64, candidate pkoracle.Candidate, fn func(pk storage.PrimaryKey, ts uint64) error) error {
level0Segments := sd.deleteBuffer.ListL0()
if len(level0Segments) == 0 {
return nil
}
log := sd.getLogger(context.Background())
start := time.Now()
totalL0Rows := 0
totalForwardRows := int64(0)
processedL0Count := 0
for _, segment := range level0Segments {
segment := segment.(*segments.L0Segment)
if segment.Partition() == partitionID || segment.Partition() == common.AllPartitionsID {
segmentPks, segmentTss := segment.DeleteRecords()
totalL0Rows += len(segmentPks)
processedL0Count++
batchSize := paramtable.Get().CommonCfg.BloomFilterApplyBatchSize.GetAsInt()
for idx := 0; idx < len(segmentPks); idx += batchSize {
endIdx := idx + batchSize
if endIdx > len(segmentPks) {
endIdx = len(segmentPks)
}
if !candidate.PkCandidateExist() {
for i := idx; i < endIdx; i++ {
totalForwardRows += 1
if err := fn(segmentPks[i], segmentTss[i]); err != nil {
return err
}
}
continue
}
lc := storage.NewBatchLocationsCache(segmentPks[idx:endIdx])
hits := candidate.BatchPkExist(lc)
for i, hit := range hits {
if !hit {
continue
}
totalForwardRows += 1
if err := fn(segmentPks[idx+i], segmentTss[idx+i]); err != nil {
return err
}
}
}
}
}
log.Info(context.TODO(), "forward delete from L0 segments to worker",
mlog.Int64("targetSegmentID", candidate.ID()),
mlog.String("channel", sd.vchannelName),
mlog.Bool("broadcast", !candidate.PkCandidateExist()),
mlog.Int("l0SegmentCount", processedL0Count),
mlog.Int("totalDeleteRowsInL0", totalL0Rows),
mlog.Int64("totalForwardRows", totalForwardRows),
mlog.Int64("totalCost", time.Since(start).Milliseconds()),
)
return nil
}
func (sd *shardDelegator) GetLevel0Deletions(partitionID int64, candidate pkoracle.Candidate) (storage.PrimaryKeys, []storage.Timestamp) {
deltaData := storage.NewDeltaData(0)
sd.rangeHitL0Deletions(partitionID, candidate, func(pk storage.PrimaryKey, ts uint64) error {
deltaData.Append(pk, ts)
return nil
})
return deltaData.DeletePks(), deltaData.DeleteTimestamps()
}
func (sd *shardDelegator) StreamForwardLevel0Deletions(bufferedForwarder *BufferForwarder, partitionID int64, candidate pkoracle.Candidate) error {
err := sd.rangeHitL0Deletions(partitionID, candidate, func(pk storage.PrimaryKey, ts uint64) error {
return bufferedForwarder.Buffer(pk, ts)
})
if err != nil {
return err
}
return bufferedForwarder.Flush()
}
func (sd *shardDelegator) RefreshLevel0DeletionStats() {
level0Segments := sd.deleteBuffer.ListL0()
totalSize := int64(0)
for _, segment := range level0Segments {
segment := segment.(*segments.L0Segment)
pks, tss := segment.DeleteRecords()
totalSize += lo.SumBy(pks, func(pk storage.PrimaryKey) int64 { return pk.Size() }) + int64(len(tss)*8)
}
metrics.QueryNodeNumSegments.WithLabelValues(
paramtable.GetStringNodeID(),
fmt.Sprint(sd.Collection()),
commonpb.SegmentState_Sealed.String(),
datapb.SegmentLevel_L0.String(),
).Set(float64(len(level0Segments)))
metrics.QueryNodeLevelZeroSize.WithLabelValues(
paramtable.GetStringNodeID(),
fmt.Sprint(sd.collectionID),
sd.vchannelName,
).Set(float64(totalSize))
}
// processDeleteRecords performs BF checks on delete buffer records and forwards matching deletes
// via the buffered forwarder. Does NOT require any lock to be held.
// When candidate has no stats (PkCandidateExist() == false), all deletes are forwarded (broadcast mode).
// Returns the number of timestamp-hit and bloom-filter-hit rows.
func (sd *shardDelegator) processDeleteRecords(
candidate *pkoracle.BloomFilterSet,
records []*deletebuffer.Item,
forwarder *BufferForwarder,
) (tsHit, bfHit int64, err error) {
for _, entry := range records {
for _, record := range entry.Data {
tsHit += int64(len(record.DeleteData.Pks))
if record.PartitionID != common.AllPartitionsID && candidate.Partition() != record.PartitionID {
continue
}
pks := record.DeleteData.Pks
batchSize := paramtable.Get().CommonCfg.BloomFilterApplyBatchSize.GetAsInt()
for idx := 0; idx < len(pks); idx += batchSize {
endIdx := idx + batchSize
if endIdx > len(pks) {
endIdx = len(pks)
}
// When BF not initialized (bloom filter disabled), forward all deletes
if !candidate.PkCandidateExist() {
for i := idx; i < endIdx; i++ {
bfHit++
if err = forwarder.Buffer(pks[i], record.DeleteData.Tss[i]); err != nil {
return tsHit, bfHit, err
}
}
continue
}
lc := storage.NewBatchLocationsCache(pks[idx:endIdx])
hits := candidate.BatchPkExist(lc)
for i, hit := range hits {
if hit {
bfHit++
if err = forwarder.Buffer(pks[idx+i], record.DeleteData.Tss[idx+i]); err != nil {
return tsHit, bfHit, err
}
}
}
}
}
}
return tsHit, bfHit, nil
}
// segDeleteSnapshot holds the snapshotted delete buffer entries for a segment,
// captured under RLock in Phase 1 of loadStreamDelete.
type segDeleteSnapshot struct {
records []*deletebuffer.Item // copied slice of delete buffer entries
snapshotMaxTs uint64 // max Item.Ts in snapshot, used for timestamp-based catch-up
}
func (sd *shardDelegator) loadStreamDelete(ctx context.Context,
candidates []*pkoracle.BloomFilterSet,
infos []*querypb.SegmentLoadInfo,
req *querypb.LoadSegmentsRequest,
targetNodeID int64,
worker cluster.Worker,
entries []SegmentEntry,
schemaBarrierTs uint64,
) error {
log := sd.getLogger(ctx)
idCandidates := lo.SliceToMap(candidates, func(candidate *pkoracle.BloomFilterSet) (int64, *pkoracle.BloomFilterSet) {
return candidate.ID(), candidate
})
// Phase 0: Forward L0 deletions (no lock needed, unchanged)
for _, info := range infos {
candidate := idCandidates[info.GetSegmentID()]
err := sd.forwardL0Deletion(ctx, info, req, candidate, targetNodeID, worker)
if err != nil {
return err
}
}
// === Phase 1: Snapshot delete buffer entries under RLock (fast — microseconds) ===
sd.deleteMut.RLock()
snapshots := make([]segDeleteSnapshot, len(infos))
for i, info := range infos {
records := sd.deleteBuffer.ListAfter(segmentEffectiveTs(info))
// Copy the slice to safely use outside lock scope.
// ListAfter returns a new slice from doubleCacheBuffer, but we copy to
// ensure no dependency on internal buffer state that may change after unlock.
copied := make([]*deletebuffer.Item, len(records))
copy(copied, records)
var maxTs uint64
if len(records) > 0 {
maxTs = records[len(records)-1].Ts
}
snapshots[i] = segDeleteSnapshot{
records: copied,
snapshotMaxTs: maxTs,
}
}
sd.deleteMut.RUnlock()
// RLock released — WAL pipeline (ProcessDelete) is now unblocked
// Create one forwarder per segment, shared across Phase 2 and Phase 3, flushed once at the end.
forwarders := make([]*BufferForwarder, len(infos))
for i, info := range infos {
candidate := idCandidates[info.GetSegmentID()]
deleteScope := querypb.DataScope_All
switch candidate.Type() {
case commonpb.SegmentState_Sealed:
deleteScope = querypb.DataScope_Historical
case commonpb.SegmentState_Growing:
deleteScope = querypb.DataScope_Streaming
}
forwarders[i] = NewBufferedForwarder(paramtable.Get().QueryNodeCfg.ForwardBatchSize.GetAsInt64(),
deleteViaWorker(ctx, worker, targetNodeID, info, deleteScope))
}
// === Phase 2: Process snapshot WITHOUT lock (expensive — seconds) ===
for i, info := range infos {
candidate := idCandidates[info.GetSegmentID()]
start := time.Now()
tsHit, bfHit, err := sd.processDeleteRecords(candidate, snapshots[i].records, forwarders[i])
if err != nil {
return err
}
log.Info(ctx, "forward delete to worker (phase 2: snapshot)...",
mlog.String("channel", info.InsertChannel),
mlog.FieldSegmentID(info.GetSegmentID()),
mlog.Time("startPosition", tsoutil.PhysicalTime(info.GetStartPosition().GetTimestamp())),
mlog.Int64("tsHitDeleteRowNum", tsHit),
mlog.Int64("bfHitDeleteRowNum", bfHit),
mlog.Int64("bfCost", time.Since(start).Milliseconds()),
)
}
// === Phase 3: Catch-up new entries + flush + add distribution under RLock (fast — milliseconds) ===
sd.deleteMut.RLock()
defer sd.deleteMut.RUnlock()
for i, info := range infos {
candidate := idCandidates[info.GetSegmentID()]
// Use timestamp-based catch-up: fetch records added after the snapshot's max timestamp.
// This is robust against delete buffer eviction (Put → evict discards old tail during Phase 2).
// Index-based approach (allRecords[snapshotLen:]) would panic or miss data if eviction occurs.
// Item.Ts comes from WAL TSO, monotonically increasing and unique per ProcessDelete call,
// so ListAfter(snapshotMaxTs + 1) precisely captures only new records.
catchUpTs := segmentEffectiveTs(info)
if snapshots[i].snapshotMaxTs > 0 {
catchUpTs = snapshots[i].snapshotMaxTs + 1
}
newRecords := sd.deleteBuffer.ListAfter(catchUpTs)
if len(newRecords) > 0 {
start := time.Now()
tsHit, bfHit, err := sd.processDeleteRecords(candidate, newRecords, forwarders[i])
if err != nil {
return err
}
log.Info(ctx, "forward delete to worker (phase 3: catch-up)...",
mlog.String("channel", info.InsertChannel),
mlog.FieldSegmentID(info.GetSegmentID()),
mlog.Int64("tsHitDeleteRowNum", tsHit),
mlog.Int64("bfHitDeleteRowNum", bfHit),
mlog.Int64("bfCost", time.Since(start).Milliseconds()),
)
}
// Flush once per segment after both phases are done
if err := forwarders[i].Flush(); err != nil {
return err
}
}
// Atomically add to distribution while still holding RLock.
// This guarantees no ProcessDelete can run between catch-up and distribution update,
// so there is no gap between "deletes applied" and "segment visible".
if err := sd.addDistributionIfSchemaBarrierOK(schemaBarrierTs, entries...); err != nil {
return err
}
log.Info(ctx, "load stream delete done")
return nil
}
// ReleaseSegments releases segments local or remotely depending on the target node.
func (sd *shardDelegator) ReleaseSegments(ctx context.Context, req *querypb.ReleaseSegmentsRequest, force bool) error {
log := sd.getLogger(ctx)
targetNodeID := req.GetNodeID()
level0Segments := typeutil.NewSet(lo.Map(sd.deleteBuffer.ListL0(), func(segment segments.Segment, _ int) int64 {
return segment.ID()
})...)
hasLevel0 := false
for _, segmentID := range req.GetSegmentIDs() {
hasLevel0 = level0Segments.Contain(segmentID)
if hasLevel0 {
return merr.WrapErrServiceInternal("release L0 segment is not supported, l0 segment should only be released by unSubChannel/SyncDataDistribution")
}
}
// add common log fields
log = log.With(
mlog.Int64s("segmentIDs", req.GetSegmentIDs()),
mlog.FieldNodeID(req.GetNodeID()),
mlog.String("scope", req.GetScope().String()),
mlog.Bool("force", force))
log.Info(ctx, "delegator start to release segments")
// alter distribution first
var sealed, growing []SegmentEntry
convertSealed := func(segmentID int64, _ int) SegmentEntry {
return SegmentEntry{
SegmentID: segmentID,
NodeID: targetNodeID,
}
}
convertGrowing := func(segmentID int64, _ int) SegmentEntry {
return SegmentEntry{
SegmentID: segmentID,
}
}
switch req.GetScope() {
case querypb.DataScope_All:
sealed = lo.Map(req.GetSegmentIDs(), convertSealed)
growing = lo.Map(req.GetSegmentIDs(), convertGrowing)
case querypb.DataScope_Streaming:
growing = lo.Map(req.GetSegmentIDs(), convertGrowing)
case querypb.DataScope_Historical:
sealed = lo.Map(req.GetSegmentIDs(), convertSealed)
}
signal := sd.distribution.RemoveDistributions(sealed, growing)
// wait cleared signal
<-signal
if len(growing) > 0 {
sd.growingSegmentLock.Lock()
}
// when we try to release a segment, add it to pipeline's exclude list first
// in case of consumed it's growing segment again
droppedInfos := lo.SliceToMap(req.GetSegmentIDs(), func(id int64) (int64, uint64) {
if req.GetCheckpoint() == nil {
return id, typeutil.MaxTimestamp
}
return id, req.GetCheckpoint().GetTimestamp()
})
sd.AddExcludedSegments(droppedInfos)
var releaseErr error
if !force {
worker, err := sd.workerManager.GetWorker(ctx, targetNodeID)
if err != nil {
log.Warn(ctx, "delegator failed to find worker", mlog.Err(err))
releaseErr = err
}
req.Base.TargetID = targetNodeID
err = worker.ReleaseSegments(ctx, req)
if err != nil {
log.Warn(ctx, "worker failed to release segments", mlog.Err(err))
releaseErr = err
}
}
if len(growing) > 0 {
sd.growingSegmentLock.Unlock()
}
if releaseErr != nil {
return releaseErr
}
if len(growing) > 0 && sd.growingSourceProvider != nil {
for _, entry := range growing {
sd.growingSourceProvider.ClearReleasePrepared(entry.SegmentID)
}
}
return nil
}
func (sd *shardDelegator) SyncTargetVersion(action *querypb.SyncAction, partitions []int64) {
sd.distribution.SyncTargetVersion(action, partitions)
// clean delete buffer after distribution becomes serviceable
if sd.distribution.queryView.Serviceable() {
checkpoint := action.GetCheckpoint()
deleteSeekPos := action.GetDeleteCP()
if deleteSeekPos == nil {
// for compatible with 2.4, we use checkpoint as deleteCP when deleteCP is nil
deleteSeekPos = checkpoint
mlog.Info(context.TODO(), "use checkpoint as deleteCP",
mlog.String("channelName", sd.vchannelName),
mlog.Time("deleteSeekPos", tsoutil.PhysicalTime(action.GetCheckpoint().GetTimestamp())))
}
start := time.Now()
sizeBeforeClean, _ := sd.deleteBuffer.Size()
l0NumBeforeClean := len(sd.deleteBuffer.ListL0())
sd.deleteBuffer.UnRegister(deleteSeekPos.GetTimestamp())
sizeAfterClean, _ := sd.deleteBuffer.Size()
l0NumAfterClean := len(sd.deleteBuffer.ListL0())
if sizeAfterClean < sizeBeforeClean || l0NumAfterClean < l0NumBeforeClean {
mlog.Info(context.TODO(), "clean delete buffer",
mlog.String("channel", sd.vchannelName),
mlog.Time("deleteSeekPos", tsoutil.PhysicalTime(deleteSeekPos.GetTimestamp())),
mlog.Time("channelCP", tsoutil.PhysicalTime(checkpoint.GetTimestamp())),
mlog.Int64("sizeBeforeClean", sizeBeforeClean),
mlog.Int64("sizeAfterClean", sizeAfterClean),
mlog.Int("l0NumBeforeClean", l0NumBeforeClean),
mlog.Int("l0NumAfterClean", l0NumAfterClean),
mlog.Duration("cost", time.Since(start)),
)
}
sd.RefreshLevel0DeletionStats()
}
}
func (sd *shardDelegator) GetChannelQueryView() *channelQueryView {
return sd.distribution.GetQueryView()
}
func (sd *shardDelegator) AddExcludedSegments(excludeInfo map[int64]uint64) {
sd.excludedSegments.Insert(excludeInfo)
}
func (sd *shardDelegator) VerifyExcludedSegments(segmentID int64, ts uint64) bool {
return sd.excludedSegments.Verify(segmentID, ts)
}
func (sd *shardDelegator) TryCleanExcludedSegments(ts uint64) {
if sd.excludedSegments.ShouldClean() {
sd.excludedSegments.CleanInvalid(ts)
}
}
func (sd *shardDelegator) buildBM25IDF(ctx context.Context, req *internalpb.SearchRequest) (float64, error) {
idfOracle := sd.getIDFOracle()
if idfOracle == nil {
return 0, merr.WrapErrServiceInternal("bm25 oracle is not initialized")
}
pb := &commonpb.PlaceholderGroup{}
if err := proto.Unmarshal(req.GetPlaceholderGroup(), pb); err != nil {
return 0, merr.WrapErrParameterInvalidMsg("failed to unmarshal BM25 IDF placeholder group: %v", err)
}
if len(pb.Placeholders) != 1 || len(pb.Placeholders[0].Values) == 0 {
return 0, merr.WrapErrParameterInvalidMsg("please provide varchar/text for BM25 Function based search")
}
holder := pb.Placeholders[0]
if holder.Type != commonpb.PlaceholderType_VarChar {
return 0, merr.WrapErrParameterInvalidMsg("please provide varchar/text for BM25 Function based search, got %s", holder.Type.String())
}
texts := funcutil.GetVarCharFromPlaceholder(holder)
var tfArray *schemapb.SparseFloatArray
schemaVersion := sd.collection.Schema().GetVersion()
ok, err := function.RunWithRunner(ctx, sd.collectionID, schemaVersion, req.GetFieldId(), func(functionType schemapb.FunctionType, functionRunner function.FunctionRunner) error {
if functionType != schemapb.FunctionType_BM25 {
return merr.WrapErrServiceInternalMsg("functionRunner not found for field: %d", req.GetFieldId())
}
datas := []any{texts}
if len(functionRunner.GetInputFields()) == 2 {
analyzerName := "default"
if name := req.GetAnalyzerName(); name != "" {
// use user provided analyzer name
analyzerName = name
}
analyzers := make([]string, len(texts))
for i := range texts {
analyzers[i] = analyzerName
}
datas = append(datas, analyzers)
}
// get search text term frequency
output, err := functionRunner.BatchRun(datas...)
if err != nil {
return err
}
if len(output) == 0 {
return merr.WrapErrFunctionFailedMsg("BM25 embedding failed: runner returned empty output")
}
var ok bool
tfArray, ok = output[0].(*schemapb.SparseFloatArray)
if !ok {
return merr.WrapErrFunctionFailedMsg("functionRunner return unknown data")
}
return nil
})
if err != nil {
return 0, err
}
if !ok {
// internal invariant: runners are populated with the schema, never by
// the request — classified system, keeps cross-replica failover
return 0, merr.WrapErrServiceInternalMsg("functionRunner not found for field: %d", req.GetFieldId())
}
idfSparseVector, avgdl, err := idfOracle.BuildIDF(req.GetFieldId(), tfArray)
if err != nil {
return 0, err
}
if avgdl <= 0 {
return 0, nil
}
for _, idf := range idfSparseVector {
metrics.QueryNodeSearchFTSNumTokens.WithLabelValues(paramtable.GetStringNodeID(), fmt.Sprint(sd.collectionID), fmt.Sprint(req.GetFieldId())).Observe(float64(typeutil.SparseFloatRowElementCount(idf)))
}
err = SetBM25Params(req, avgdl)
if err != nil {
return 0, err
}
req.PlaceholderGroup = funcutil.SparseVectorDataToPlaceholderGroupBytes(idfSparseVector)
return avgdl, nil
}
func (sd *shardDelegator) parseMinHash(ctx context.Context, req *internalpb.SearchRequest) error {
pb := &commonpb.PlaceholderGroup{}
if err := proto.Unmarshal(req.GetPlaceholderGroup(), pb); err != nil {
return merr.WrapErrParameterInvalidMsg("failed to unmarshal MinHash placeholder group: %v", err)
}
if len(pb.Placeholders) != 1 || len(pb.Placeholders[0].Values) == 0 {
return merr.WrapErrParameterInvalidMsg("please provide varchar/text for MinHash Function based search")
}
holder := pb.Placeholders[0]
if holder.Type != commonpb.PlaceholderType_VarChar {
return merr.WrapErrParameterInvalidMsg("please provide varchar/text for MinHash Function based search, got %s", holder.Type.String())
}
texts := funcutil.GetVarCharFromPlaceholder(holder)
var fieldData *schemapb.FieldData
schemaVersion := sd.collection.Schema().GetVersion()
ok, err := function.RunWithRunner(ctx, sd.collectionID, schemaVersion, req.GetFieldId(), func(functionType schemapb.FunctionType, functionRunner function.FunctionRunner) error {
if functionType != schemapb.FunctionType_MinHash {
return merr.WrapErrServiceInternalMsg("functionRunner not found for field: %d", req.GetFieldId())
}
output, err := functionRunner.BatchRun(texts)
if err != nil {
return err
}
if len(output) == 0 {
return merr.WrapErrFunctionFailedMsg("MinHash embedding failed: runner returned empty output")
}
var ok bool
fieldData, ok = output[0].(*schemapb.FieldData)
if !ok {
return merr.WrapErrFunctionFailedMsg("MinHash embedding failed: MinHash functionRunner return unknown data")
}
return nil
})
if err != nil {
return err
}
if !ok {
return merr.WrapErrServiceInternalMsg("functionRunner not found for field: %d", req.GetFieldId())
}
vectorField := fieldData.GetVectors()
if vectorField == nil {
return merr.WrapErrFunctionFailedMsg("MinHash embedding failed: output is not a vector field")
}
binaryVector := vectorField.GetBinaryVector()
if binaryVector == nil {
return merr.WrapErrFunctionFailedMsg("MinHash embedding failed: output is not a binary vector")
}
req.PlaceholderGroup, err = funcutil.FieldDataToPlaceholderGroupBytes(fieldData)
if err != nil {
return err
}
return nil
}
func (sd *shardDelegator) DropIndex(ctx context.Context, req *querypb.DropIndexRequest) error {
workers := sd.workerManager.GetAllWorkers()
for _, worker := range workers {
if err := worker.DropIndex(ctx, req); err != nil {
return err
}
}
return nil
}
func (sd *shardDelegator) GetHighlight(ctx context.Context, req *querypb.GetHighlightRequest) ([]*querypb.HighlightResult, error) {
result := []*querypb.HighlightResult{}
for _, task := range req.GetTasks() {
if len(task.GetTexts()) != int(task.GetSearchTextNum()+task.GetCorpusTextNum())+len(task.GetQueries()) {
return nil, merr.WrapErrServiceInternalMsg("package highlight texts error, num of texts not equal the expected num %d:%d", len(task.GetTexts()), int(task.GetSearchTextNum()+task.GetCorpusTextNum())+len(task.GetQueries()))
}
topks := req.GetTopks()
var results [][]*milvuspb.AnalyzerToken
var analyzeErr error
ok, err := sd.runWithAnalyzer(ctx, task.GetFieldId(), func(analyzer function.Analyzer) error {
if len(analyzer.GetInputFields()) == 1 {
results, analyzeErr = analyzer.BatchAnalyze(true, false, task.GetTexts())
return analyzeErr
}
if len(analyzer.GetInputFields()) == 2 {
analyzerNames, err := normalizeHighlightAnalyzerNames(task.GetAnalyzerNames(), len(task.GetTexts()))
if err != nil {
return err
}
results, analyzeErr = analyzer.BatchAnalyze(true, false, task.GetTexts(), analyzerNames)
return analyzeErr
}
return nil
})
if err != nil {
return nil, err
}
if !ok {
return nil, merr.WrapErrParameterInvalidMsg("get highlight failed, the highlight field not found, %s:%d", task.GetFieldName(), task.GetFieldId())
}
// analyze result of search text
searchResults := results[0:task.SearchTextNum]
// analyze result of query text
queryResults := results[task.SearchTextNum : task.SearchTextNum+int64(len(task.Queries))]
// analyze result of corpus text
corpusStartOffset := int(task.SearchTextNum) + len(task.Queries)
corpusResults := results[corpusStartOffset:]
// query for all corpus texts
// only support text match now
// build match set for all analyze result of query text
// TODO: support more query types
queryTokenSet := typeutil.NewSet[string]()
for _, tokens := range queryResults {
for _, token := range tokens {
queryTokenSet.Insert(token.GetToken())
}
}
corpusIdx := 0
for i := range len(topks) {
tokenSet := typeutil.NewSet[string]()
if len(searchResults) > i {
for _, token := range searchResults[i] {
tokenSet.Insert(token.GetToken())
}
}
for j := 0; j < int(topks[i]); j++ {
spans := SpanList{}
for _, token := range corpusResults[corpusIdx] {
if tokenSet.Contain(token.GetToken()) || queryTokenSet.Contain(token.GetToken()) {
spans = append(spans, Span{token.GetStartOffset(), token.GetEndOffset()})
}
}
spans = mergeOffsets(spans)
// Convert byte offsets from analyzer to rune (character) offsets
corpusText := task.Texts[corpusStartOffset+corpusIdx]
err := bytesOffsetToRuneOffset(corpusText, spans)
if err != nil {
return nil, err
}
frags := fetchFragmentsFromOffsets(corpusText, spans,
task.GetOptions().GetFragmentOffset(),
task.GetOptions().GetFragmentSize(),
task.GetOptions().GetNumOfFragments())
result = append(result, &querypb.HighlightResult{Fragments: frags})
corpusIdx++
}
}
}
return result, nil
}