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

746 lines
25 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 chain
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/apache/arrow/go/v17/arrow/memory"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/util/function/chain/expr"
"github.com/milvus-io/milvus/internal/util/function/chain/types"
"github.com/milvus-io/milvus/internal/util/function/models"
"github.com/milvus-io/milvus/internal/util/function/rerank"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
)
// =============================================================================
// Constants
// =============================================================================
const (
// Reranker names
DecayRerankerName = "decay"
ModelRerankerName = "model"
RRFRerankerName = "rrf"
WeightedRerankerName = "weighted"
// Parameter keys
rerankerKey = "reranker"
rrfKKey = "k"
weightsKey = "weights"
normScoreKey = "norm_score"
scoreModeKey = "score_mode"
functionKey = "function"
originKey = "origin"
scaleKey = "scale"
offsetKey = "offset"
decayKey = "decay"
// Legacy parameter keys
legacyRankTypeKey = "strategy"
legacyRankParamsKey = "params"
// Model parameter keys
queryKeyName = "queries"
// Default values
defaultRRFK = 60.0
defaultDecay = 0.5
defaultScoreMode = "max"
)
// =============================================================================
// SearchParams
// =============================================================================
// SearchParams contains search parameters for reranking.
type SearchParams struct {
Nq int64
Limit int64
Offset int64
RoundDecimal int64
// Grouping parameters
GroupByField string // Field to group by (empty means no grouping)
GroupSize int64 // Maximum rows per group
GroupScorer GroupScorer // How to compute group score ("max", "sum", "avg")
// Model reranker extra info (can be nil for non-model rerankers)
ModelExtraInfo *models.ModelExtraInfo
}
// NewSearchParams creates a new SearchParams.
func NewSearchParams(nq, limit, offset, roundDecimal int64) *SearchParams {
return &SearchParams{
Nq: nq,
Limit: limit,
Offset: offset,
RoundDecimal: roundDecimal,
}
}
// NewSearchParamsWithGrouping creates a new SearchParams with grouping support.
func NewSearchParamsWithGrouping(nq, limit, offset, roundDecimal int64, groupByField string, groupSize int64) *SearchParams {
return NewSearchParamsWithGroupingAndScorer(nq, limit, offset, roundDecimal, groupByField, groupSize, GroupScorerMax)
}
// NewSearchParamsWithGroupingAndScorer creates a new SearchParams with grouping and scorer support.
func NewSearchParamsWithGroupingAndScorer(nq, limit, offset, roundDecimal int64, groupByField string, groupSize int64, scorer GroupScorer) *SearchParams {
return &SearchParams{
Nq: nq,
Limit: limit,
Offset: offset,
RoundDecimal: roundDecimal,
GroupByField: groupByField,
GroupSize: groupSize,
GroupScorer: scorer,
}
}
// HasGrouping returns true if grouping is enabled.
func (sp *SearchParams) HasGrouping() bool {
return sp.GroupByField != "" && sp.GroupSize > 0
}
// =============================================================================
// BuildRerankChain
// =============================================================================
// BuildRerankChain builds a FuncChain for reranking from FunctionScore schema.
func BuildRerankChain(
collSchema *schemapb.CollectionSchema,
funcScoreSchema *schemapb.FunctionScore,
searchMetrics []string,
searchParams *SearchParams,
alloc memory.Allocator,
) (*FuncChain, error) {
if funcScoreSchema == nil || len(funcScoreSchema.Functions) == 0 {
return nil, merr.WrapErrParameterInvalidMsg("rerank_builder: no rerank functions specified")
}
// Filter out boost functions — boost is pushed down to QueryNode, proxy doesn't handle it
var proxyFunctions []*schemapb.FunctionSchema
for _, f := range funcScoreSchema.Functions {
if rerank.GetRerankName(f) != rerank.BoostName {
proxyFunctions = append(proxyFunctions, f)
}
}
// All functions are boost, proxy has nothing to rerank
if len(proxyFunctions) == 0 {
return nil, nil
}
// Currently only support single proxy rerank function
if len(proxyFunctions) > 1 {
return nil, merr.WrapErrParameterInvalidMsg("rerank_builder: currently only supports one rerank function")
}
return buildRerankChainInternal(collSchema, proxyFunctions[0], searchMetrics, searchParams, alloc)
}
// BuildRerankChainWithLegacy builds a FuncChain from legacy rank parameters.
func BuildRerankChainWithLegacy(
collSchema *schemapb.CollectionSchema,
rankParams []*commonpb.KeyValuePair,
searchMetrics []string,
searchParams *SearchParams,
alloc memory.Allocator,
) (*FuncChain, error) {
funcSchema, err := convertLegacyParams(rankParams)
if err != nil {
return nil, merr.WrapErrParameterInvalidMsg("rerank_builder: %v", err)
}
return buildRerankChainInternal(collSchema, funcSchema, searchMetrics, searchParams, alloc)
}
// convertLegacyParams converts legacy rank params to FunctionSchema.
func convertLegacyParams(rankParams []*commonpb.KeyValuePair) (*schemapb.FunctionSchema, error) {
var params map[string]interface{}
rankTypeStr := "rrf"
for _, kv := range rankParams {
switch strings.ToLower(kv.Key) {
case legacyRankTypeKey:
rankTypeStr = strings.ToLower(kv.Value)
case legacyRankParamsKey:
if err := json.Unmarshal([]byte(kv.Value), &params); err != nil {
return nil, merr.WrapErrParameterInvalidMsg("parse rerank params failed: %v", err)
}
}
}
fSchema := &schemapb.FunctionSchema{
Type: schemapb.FunctionType_Rerank,
InputFieldNames: []string{},
OutputFieldNames: []string{},
Params: []*commonpb.KeyValuePair{},
}
switch rankTypeStr {
case "rrf":
fSchema.Params = append(fSchema.Params, &commonpb.KeyValuePair{Key: rerankerKey, Value: RRFRerankerName})
if v, ok := params[rrfKKey]; ok {
if k, ok := v.(float64); ok {
fSchema.Params = append(fSchema.Params, &commonpb.KeyValuePair{Key: rrfKKey, Value: strconv.FormatFloat(k, 'f', -1, 64)})
} else {
return nil, merr.WrapErrParameterInvalidMsg("the type of rank param k should be float")
}
}
case "weighted":
fSchema.Params = append(fSchema.Params, &commonpb.KeyValuePair{Key: rerankerKey, Value: WeightedRerankerName})
if v, ok := params[weightsKey]; ok {
if d, err := json.Marshal(v); err != nil {
return nil, merr.WrapErrParameterInvalidMsg("the weights param should be an array")
} else {
fSchema.Params = append(fSchema.Params, &commonpb.KeyValuePair{Key: weightsKey, Value: string(d)})
}
}
if normScore, ok := params[normScoreKey]; ok {
switch ns := normScore.(type) {
case bool:
fSchema.Params = append(fSchema.Params, &commonpb.KeyValuePair{Key: normScoreKey, Value: strconv.FormatBool(ns)})
case string:
if parsed, err := strconv.ParseBool(ns); err != nil {
return nil, merr.WrapErrParameterInvalidMsg("the type of rank param norm_score should be bool")
} else {
fSchema.Params = append(fSchema.Params, &commonpb.KeyValuePair{Key: normScoreKey, Value: strconv.FormatBool(parsed)})
}
default:
return nil, merr.WrapErrParameterInvalidMsg("the type of rank param norm_score should be bool")
}
}
default:
return nil, merr.WrapErrParameterInvalidMsg("unsupported rank type %s", rankTypeStr)
}
return fSchema, nil
}
// buildRerankChainInternal builds a FuncChain from FunctionSchema.
//
// It produces 4 kinds of chains depending on the reranker, sharing a common tail:
//
// 1. RRF:
// Merge(RRF) → Sort/GroupBy → [RoundDecimal] → Select
//
// 2. Weighted:
// Merge(Weighted) → Sort/GroupBy → [RoundDecimal] → Select
//
// 3. Decay:
// Merge(Max|Sum|Avg) → Map(DecayExpr→_decay_score) → Map(NumCombine: $score*_decay_score→$score) → Sort/GroupBy → [RoundDecimal] → Select
//
// 4. Model:
// Merge(Max) → Map(RerankModelExpr) → Sort/GroupBy → [RoundDecimal] → Select
//
// Common tail behavior:
// - Without grouping: Sort($score, DESC) → Limit(limit, offset)
// - With grouping: GroupBy(field, groupSize, limit, offset, scorer)
// - RoundDecimal >= 0: Map(RoundDecimalExpr) rounds $score after ordering
// - Select: keeps only $id, $score (plus groupByField, $group_score if grouping)
func buildRerankChainInternal(
collSchema *schemapb.CollectionSchema,
funcSchema *schemapb.FunctionSchema,
searchMetrics []string,
searchParams *SearchParams,
alloc memory.Allocator,
) (*FuncChain, error) {
rerankerName := rerank.GetRerankName(funcSchema)
if rerankerName == "" {
return nil, merr.WrapErrParameterMissingMsg("rerank_builder: reranker name not specified")
}
if alloc == nil {
alloc = memory.DefaultAllocator
}
fc := NewFuncChainWithAllocator(alloc).
SetStage(types.StageL2Rerank).
SetName("rerank_chain")
sortDescending := true // default: larger score = better match
switch rerankerName {
case RRFRerankerName:
if err := buildRRFChain(fc, funcSchema, searchMetrics); err != nil {
return nil, err
}
case WeightedRerankerName:
var err error
sortDescending, err = buildWeightedChain(fc, funcSchema, searchMetrics)
if err != nil {
return nil, err
}
case DecayRerankerName:
if err := buildDecayChain(fc, collSchema, funcSchema, searchMetrics); err != nil {
return nil, err
}
case ModelRerankerName:
if err := buildModelChain(fc, collSchema, funcSchema, searchMetrics, searchParams); err != nil {
return nil, err
}
default:
return nil, merr.WrapErrParameterInvalidMsg("rerank_builder: unsupported reranker %s", rerankerName)
}
// Add grouping or sort+limit
if searchParams.HasGrouping() {
// Use GroupBy for grouping search
scorer := searchParams.GroupScorer
if scorer == "" {
scorer = GroupScorerMax
}
// Construct the GroupByOp directly so we can propagate the sort
// direction from the merge stage. fc.GroupByWithScorer would default
// to DESC, which is wrong for distance metrics + no normalization
// (e.g., weighted reranker on raw L2 distances).
groupByOp := NewGroupByOpWithScorer(
searchParams.GroupByField,
searchParams.GroupSize,
searchParams.Limit,
searchParams.Offset,
scorer,
).SetSortDescending(sortDescending)
fc.Add(groupByOp)
} else {
// Non-grouping: sort by score and apply limit
fc.Sort(types.ScoreFieldName, sortDescending, types.IDFieldName)
if searchParams.Limit > 0 {
fc.LimitWithOffset(searchParams.Limit, searchParams.Offset)
}
}
// Apply score rounding after sort/limit (rounding should not affect ordering)
if searchParams.RoundDecimal >= 0 {
roundExpr, err := expr.NewRoundDecimalExpr(searchParams.RoundDecimal)
if err != nil {
return nil, merr.WrapErrParameterInvalidMsg("rerank_builder: %v", err)
}
fc.Map(roundExpr,
[]string{types.ScoreFieldName},
[]string{types.ScoreFieldName})
}
// Only keep $id and $score (plus group-by and $group_score if present) in the output.
// Other field columns are no longer needed — downstream pipeline stages
// (organize/requery) re-fetch fields from segments.
selectCols := []string{types.IDFieldName, types.ScoreFieldName}
if searchParams.HasGrouping() {
selectCols = append(selectCols, searchParams.GroupByField, GroupScoreFieldName)
}
fc.Select(selectCols...)
return fc, nil
}
// =============================================================================
// RRF Builder
// =============================================================================
func buildRRFChain(fc *FuncChain, funcSchema *schemapb.FunctionSchema, searchMetrics []string) error {
rrfK, err := parseRRFK(funcSchema)
if err != nil {
return err
}
// RRF scores are computed purely from rank position, not from original scores,
// so metricTypes and normalize are not needed.
fc.Merge(MergeStrategyRRF,
WithRRFK(rrfK))
return nil
}
func parseRRFK(funcSchema *schemapb.FunctionSchema) (float64, error) {
for _, param := range funcSchema.Params {
if strings.ToLower(param.Key) == rrfKKey {
k, err := strconv.ParseFloat(param.Value, 64)
if err != nil {
return 0, merr.WrapErrParameterInvalidMsg("%s is not a number", param.Value)
}
if k <= 0 || k >= 16384 {
return 0, merr.WrapErrParameterInvalidMsg("The rank params k should be in range (0, %d)", 16384)
}
return k, nil
}
}
return defaultRRFK, nil
}
// =============================================================================
// Weighted Builder
// =============================================================================
func buildWeightedChain(fc *FuncChain, funcSchema *schemapb.FunctionSchema, searchMetrics []string) (sortDescending bool, err error) {
weights, normalize, err := parseWeightedParams(funcSchema)
if err != nil {
return true, err
}
if len(weights) == 0 {
return true, merr.WrapErrParameterInvalidMsg("rerank_builder: weighted reranker requires weights parameter")
}
if len(weights) != len(searchMetrics) {
return true, merr.WrapErrParameterInvalid(fmt.Sprint(len(searchMetrics)), fmt.Sprint(len(weights)), "the length of weights param mismatch with ann search requests")
}
mergeOp := NewMergeOp(MergeStrategyWeighted,
WithWeights(weights),
WithMetricTypes(searchMetrics),
WithNormalize(normalize))
fc.Add(mergeOp)
return mergeOp.SortDescending(), nil
}
func parseWeightedParams(funcSchema *schemapb.FunctionSchema) ([]float64, bool, error) {
var weights []float64
normalize := false
for _, param := range funcSchema.Params {
switch strings.ToLower(param.Key) {
case weightsKey:
var ws []float64
if err := json.Unmarshal([]byte(param.Value), &ws); err != nil {
return nil, false, merr.WrapErrParameterInvalidMsg("failed to parse weights: %v", err)
}
for _, w := range ws {
if w < 0 || w > 1 {
return nil, false, merr.WrapErrParameterInvalidMsg("rank param weight should be in range [0, 1]")
}
}
weights = ws
case normScoreKey:
if ns, err := strconv.ParseBool(param.Value); err != nil {
return nil, false, merr.WrapErrParameterInvalidMsg("failed to parse norm_score: %v", err)
} else {
normalize = ns
}
}
}
return weights, normalize, nil
}
// =============================================================================
// Decay Builder
// =============================================================================
func buildDecayChain(fc *FuncChain, collSchema *schemapb.CollectionSchema, funcSchema *schemapb.FunctionSchema, searchMetrics []string) error {
// Parse decay parameters
strategy, normalize, decayParams, err := parseDecayParams(funcSchema)
if err != nil {
return err
}
// Get input field name
if len(funcSchema.InputFieldNames) != 1 {
return merr.WrapErrParameterInvalidMsg("rerank_builder: decay reranker requires exactly 1 input field, got %d", len(funcSchema.InputFieldNames))
}
inputField := funcSchema.InputFieldNames[0]
// Validate input field exists in collection schema
if err := validateInputField(collSchema, inputField); err != nil {
return err
}
// Add MergeOp with score_mode strategy.
//
// Decay multiplies $score by a decay factor in [0, 1] and assumes
// "higher = more relevant" semantics. WithForceDescending(true) ensures
// distance metrics like L2 are flipped via 1 - 2·atan(d)/π before the
// multiplication, even when norm_score=false. Without this, raw L2
// distances would be combined with the decay factor and the worst
// vector match would rank first after the outer DESC sort.
//
// WithNormalize(true) (full range normalization) is still honored when
// the user opts in via norm_score=true; the two flags compose without
// overlap because the normalize branch returns first.
fc.Merge(strategy,
WithMetricTypes(searchMetrics),
WithNormalize(normalize),
WithForceDescending(true))
// DecayExpr only computes the decay factor (0~1) into an intermediate column,
// then NumCombineExpr multiplies it with $score.
decayScoreCol := "_decay_score"
decayExpr, err := expr.NewDecayExpr(
decayParams.function,
decayParams.origin,
decayParams.scale,
decayParams.offset,
decayParams.decay,
)
if err != nil {
return merr.WrapErrParameterInvalidMsg("rerank_builder: %v", err)
}
fc.Map(decayExpr,
[]string{inputField},
[]string{decayScoreCol})
combineExpr, err := expr.NewNumCombineExpr(expr.ModeMultiply, nil, expr.WithNullPolicy(expr.NumCombineNullAsZero))
if err != nil {
return merr.WrapErrParameterInvalidMsg("rerank_builder: %v", err)
}
fc.Map(combineExpr,
[]string{types.ScoreFieldName, decayScoreCol},
[]string{types.ScoreFieldName})
return nil
}
type decayParams struct {
function string
origin float64
scale float64
offset float64
decay float64
}
func parseDecayParams(funcSchema *schemapb.FunctionSchema) (MergeStrategy, bool, *decayParams, error) {
params := &decayParams{
offset: 0,
decay: defaultDecay,
}
scoreMode := defaultScoreMode
normalize := false
originSet := false
scaleSet := false
functionSet := false
for _, param := range funcSchema.Params {
switch strings.ToLower(param.Key) {
case functionKey:
params.function = strings.ToLower(param.Value)
functionSet = true
case originKey:
if v, err := strconv.ParseFloat(param.Value, 64); err == nil {
params.origin = v
originSet = true
} else {
return "", false, nil, merr.WrapErrParameterInvalidMsg("decay param origin: %s is not a number", param.Value)
}
case scaleKey:
if v, err := strconv.ParseFloat(param.Value, 64); err == nil {
params.scale = v
scaleSet = true
} else {
return "", false, nil, merr.WrapErrParameterInvalidMsg("decay param scale: %s is not a number", param.Value)
}
case offsetKey:
if v, err := strconv.ParseFloat(param.Value, 64); err == nil {
params.offset = v
} else {
return "", false, nil, merr.WrapErrParameterInvalidMsg("decay param offset: %s is not a number", param.Value)
}
case decayKey:
if v, err := strconv.ParseFloat(param.Value, 64); err == nil {
params.decay = v
} else {
return "", false, nil, merr.WrapErrParameterInvalidMsg("decay param decay: %s is not a number", param.Value)
}
case normScoreKey:
if ns, err := strconv.ParseBool(param.Value); err == nil {
normalize = ns
}
case scoreModeKey:
scoreMode = strings.ToLower(param.Value)
}
}
if !functionSet {
return "", false, nil, merr.WrapErrParameterMissingMsg("decay function not specified")
}
if !originSet {
return "", false, nil, merr.WrapErrParameterMissingMsg("decay origin not specified")
}
if !scaleSet {
return "", false, nil, merr.WrapErrParameterMissingMsg("decay scale not specified")
}
// Convert score_mode to MergeStrategy
var strategy MergeStrategy
switch scoreMode {
case "max":
strategy = MergeStrategyMax
case "sum":
strategy = MergeStrategySum
case "avg":
strategy = MergeStrategyAvg
default:
return "", false, nil, merr.WrapErrParameterInvalidMsg("unsupported score_mode: %s, only supports [max, sum, avg]", scoreMode)
}
return strategy, normalize, params, nil
}
// =============================================================================
// Model Builder
// =============================================================================
func buildModelChain(fc *FuncChain, collSchema *schemapb.CollectionSchema, funcSchema *schemapb.FunctionSchema, searchMetrics []string, searchParams *SearchParams) error {
// Validate input field
if len(funcSchema.InputFieldNames) != 1 {
return merr.WrapErrParameterInvalidMsg("rerank_builder: model reranker requires exactly 1 input field, got %d", len(funcSchema.InputFieldNames))
}
inputField := funcSchema.InputFieldNames[0]
if err := validateVarcharInputField(collSchema, inputField); err != nil {
return err
}
// Parse queries from params
queries, err := parseModelQueries(funcSchema)
if err != nil {
return err
}
// Create provider
extraInfo := searchParams.ModelExtraInfo
if extraInfo == nil {
extraInfo = &models.ModelExtraInfo{}
}
provider, err := rerank.NewModelProvider(funcSchema.Params, extraInfo)
if err != nil {
return merr.WrapErrParameterInvalidMsg("rerank_builder: %v", err)
}
// Add Merge to combine and deduplicate results from multiple search paths.
// No metricTypes or normalization needed: the model will overwrite all scores,
// so any score processing here (normalization, direction conversion) is wasted work.
fc.Merge(MergeStrategyMax)
// Create and add RerankModelExpr
modelExpr, err := expr.NewRerankModelExpr(provider, queries)
if err != nil {
return merr.WrapErrParameterInvalidMsg("rerank_builder: %v", err)
}
fc.Map(modelExpr,
[]string{inputField},
[]string{types.ScoreFieldName})
return nil
}
func parseModelQueries(funcSchema *schemapb.FunctionSchema) ([]string, error) {
for _, param := range funcSchema.Params {
if param.Key == queryKeyName {
var queries []string
if err := json.Unmarshal([]byte(param.Value), &queries); err != nil {
return nil, merr.WrapErrParameterInvalidMsg("rerank_builder: parse rerank params [queries] failed: %v", err)
}
if len(queries) == 0 {
return nil, merr.WrapErrParameterMissingMsg("rerank_builder: rerank queries must not be empty")
}
return queries, nil
}
}
return nil, merr.WrapErrParameterInvalidMsg("rerank_builder: rerank function missing required param: queries")
}
func validateVarcharInputField(collSchema *schemapb.CollectionSchema, fieldName string) error {
for _, field := range collSchema.Fields {
if field.Name == fieldName {
if field.DataType == schemapb.DataType_VarChar {
return nil
}
return merr.WrapErrParameterInvalidMsg("rerank_builder: model input field %s must be VarChar, got %s", fieldName, field.DataType.String())
}
}
return merr.WrapErrParameterInvalidMsg("rerank_builder: input field %s not found in collection schema", fieldName)
}
// GetInputFieldNamesFromFuncScore returns input field names from a FunctionScore schema.
// RRF/Weighted have no input fields; Decay has input fields from InputFieldNames.
func GetInputFieldNamesFromFuncScore(funcScore *schemapb.FunctionScore) []string {
if funcScore == nil || len(funcScore.Functions) == 0 {
return nil
}
funcSchema := funcScore.Functions[0]
return funcSchema.GetInputFieldNames()
}
// GetInputFieldIDsFromSchema resolves input field IDs from the collection schema using the function score's input field names.
func GetInputFieldIDsFromSchema(collSchema *schemapb.CollectionSchema, funcScore *schemapb.FunctionScore) []int64 {
names := GetInputFieldNamesFromFuncScore(funcScore)
if len(names) == 0 {
return nil
}
nameToID := make(map[string]int64, len(collSchema.GetFields()))
for _, field := range collSchema.GetFields() {
nameToID[field.GetName()] = field.GetFieldID()
}
ids := make([]int64, 0, len(names))
for _, name := range names {
if id, ok := nameToID[name]; ok {
ids = append(ids, id)
}
}
return ids
}
// GetRerankNameFromFuncScore returns the reranker name from a FunctionScore schema.
func GetRerankNameFromFuncScore(funcScore *schemapb.FunctionScore) string {
if funcScore == nil || len(funcScore.Functions) == 0 {
return ""
}
return rerank.GetRerankName(funcScore.Functions[0])
}
func validateInputField(collSchema *schemapb.CollectionSchema, fieldName string) error {
for _, field := range collSchema.Fields {
if field.Name == fieldName {
// Check if field type is numeric.
//
// Timestamptz is intentionally excluded: although the legacy
// rerank/decay code listed it in its type-dispatch switch, no
// production path or test ever exercised it end-to-end (the
// Arrow converter and GetNumericValue have no Timestamptz
// support). Reject it here so the failure is reported at
// validation time with a clear message rather than at runtime.
switch field.DataType {
case schemapb.DataType_Int8, schemapb.DataType_Int16, schemapb.DataType_Int32,
schemapb.DataType_Int64, schemapb.DataType_Float, schemapb.DataType_Double:
return nil
default:
return merr.WrapErrParameterInvalidMsg("rerank_builder: decay input field %s must be numeric, got %s", fieldName, field.DataType.String())
}
}
}
return merr.WrapErrParameterInvalidMsg("rerank_builder: input field %s not found in collection schema", fieldName)
}