/* * # 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 expr import ( "testing" "github.com/apache/arrow/go/v17/arrow" "github.com/apache/arrow/go/v17/arrow/array" "github.com/apache/arrow/go/v17/arrow/memory" "github.com/stretchr/testify/suite" "github.com/milvus-io/milvus-proto/go-api/v3/schemapb" "github.com/milvus-io/milvus/internal/util/function/chain/types" ) type RoundDecimalExprTestSuite struct { suite.Suite pool *memory.CheckedAllocator } func (s *RoundDecimalExprTestSuite) SetupTest() { s.pool = memory.NewCheckedAllocator(memory.NewGoAllocator()) } func (s *RoundDecimalExprTestSuite) TearDownTest() { s.pool.AssertSize(s.T(), 0) } func TestRoundDecimalExprTestSuite(t *testing.T) { suite.Run(t, new(RoundDecimalExprTestSuite)) } func (s *RoundDecimalExprTestSuite) buildScoreInput(scores []float32) *arrow.Chunked { builder := array.NewFloat32Builder(s.pool) defer builder.Release() builder.AppendValues(scores, nil) arr := builder.NewArray() chunked := arrow.NewChunked(arrow.PrimitiveTypes.Float32, []arrow.Array{arr}) arr.Release() return chunked } func (s *RoundDecimalExprTestSuite) TestNewRoundDecimalExpr_ValidDecimals() { for _, d := range []int64{0, 1, 2, 3, 4, 5, 6} { expr, err := NewRoundDecimalExpr(d) s.Require().NoError(err) s.Equal(RoundDecimalFuncName, expr.Name()) s.Equal([]arrow.DataType{arrow.PrimitiveTypes.Float32}, expr.OutputDataTypes()) s.True(expr.IsRunnable(types.StageL2Rerank)) } } func (s *RoundDecimalExprTestSuite) TestNewRoundDecimalExpr_InvalidDecimals() { _, err := NewRoundDecimalExpr(-1) s.Error(err) _, err = NewRoundDecimalExpr(7) s.Error(err) } func (s *RoundDecimalExprTestSuite) TestNewRoundDecimalExprFromParams() { expr, err := NewRoundDecimalExprFromParams(types.FunctionBuildContext{}, types.FunctionConfig{Params: map[string]*schemapb.FunctionParamValue{"decimal": intParam(2)}}) s.Require().NoError(err) s.Equal(RoundDecimalFuncName, expr.Name()) _, err = NewRoundDecimalExprFromParams(types.FunctionBuildContext{}, types.FunctionConfig{Params: map[string]*schemapb.FunctionParamValue{}}) s.ErrorContains(err, "missing required parameter") _, err = NewRoundDecimalExprFromParams(types.FunctionBuildContext{}, types.FunctionConfig{Params: map[string]*schemapb.FunctionParamValue{"decimal": doubleParam(1.5)}}) s.ErrorContains(err, "must be an integer") _, err = NewRoundDecimalExprFromParams(types.FunctionBuildContext{}, types.FunctionConfig{Params: map[string]*schemapb.FunctionParamValue{"decimal": intParam(7)}}) s.ErrorContains(err, "decimal must be in range") } func (s *RoundDecimalExprTestSuite) TestExecute_RoundTo2Decimals() { expr, err := NewRoundDecimalExpr(2) s.Require().NoError(err) ctx := types.NewFuncContext(s.pool) input := s.buildScoreInput([]float32{0.956, 0.834, 0.715, 0.123, 0.999}) defer input.Release() results, err := expr.Execute(ctx, []*arrow.Chunked{input}) s.Require().NoError(err) s.Len(results, 1) defer results[0].Release() chunk := results[0].Chunk(0).(*array.Float32) s.Equal(5, chunk.Len()) s.InDelta(float32(0.96), chunk.Value(0), 0.001) s.InDelta(float32(0.83), chunk.Value(1), 0.001) // 0.715 as float32 is ~0.71499997, so floor(71.499997+0.5)/100 = 0.71 s.InDelta(float32(0.71), chunk.Value(2), 0.001) s.InDelta(float32(0.12), chunk.Value(3), 0.001) s.InDelta(float32(1.00), chunk.Value(4), 0.001) } func (s *RoundDecimalExprTestSuite) TestExecute_RoundTo0Decimals() { expr, err := NewRoundDecimalExpr(0) s.Require().NoError(err) ctx := types.NewFuncContext(s.pool) input := s.buildScoreInput([]float32{0.5, 1.4, 2.5, 3.6}) defer input.Release() results, err := expr.Execute(ctx, []*arrow.Chunked{input}) s.Require().NoError(err) defer results[0].Release() chunk := results[0].Chunk(0).(*array.Float32) s.InDelta(float32(1.0), chunk.Value(0), 0.001) // 0.5 -> 1 s.InDelta(float32(1.0), chunk.Value(1), 0.001) // 1.4 -> 1 s.InDelta(float32(3.0), chunk.Value(2), 0.001) // 2.5 -> 3 s.InDelta(float32(4.0), chunk.Value(3), 0.001) // 3.6 -> 4 } func (s *RoundDecimalExprTestSuite) TestExecute_MultipleChunks() { expr, err := NewRoundDecimalExpr(1) s.Require().NoError(err) ctx := types.NewFuncContext(s.pool) b1 := array.NewFloat32Builder(s.pool) b1.AppendValues([]float32{0.96, 0.84}, nil) arr1 := b1.NewArray() b1.Release() b2 := array.NewFloat32Builder(s.pool) b2.AppendValues([]float32{0.71}, nil) arr2 := b2.NewArray() b2.Release() input := arrow.NewChunked(arrow.PrimitiveTypes.Float32, []arrow.Array{arr1, arr2}) arr1.Release() arr2.Release() defer input.Release() results, err := expr.Execute(ctx, []*arrow.Chunked{input}) s.Require().NoError(err) defer results[0].Release() s.Equal(2, len(results[0].Chunks())) chunk0 := results[0].Chunk(0).(*array.Float32) s.InDelta(float32(1.0), chunk0.Value(0), 0.001) s.InDelta(float32(0.8), chunk0.Value(1), 0.001) chunk1 := results[0].Chunk(1).(*array.Float32) s.InDelta(float32(0.7), chunk1.Value(0), 0.001) } func (s *RoundDecimalExprTestSuite) TestExecute_WrongInputCount() { expr, err := NewRoundDecimalExpr(2) s.Require().NoError(err) ctx := types.NewFuncContext(s.pool) input1 := s.buildScoreInput([]float32{0.5}) defer input1.Release() input2 := s.buildScoreInput([]float32{0.5}) defer input2.Release() _, err = expr.Execute(ctx, []*arrow.Chunked{input1, input2}) s.Error(err) s.Contains(err.Error(), "expected 1 input") } func (s *RoundDecimalExprTestSuite) TestExecute_MatchesOldFormula() { // Verify the formula matches the old rerank implementation: // score = floor(score * 10^decimal + 0.5) / 10^decimal expr, err := NewRoundDecimalExpr(3) s.Require().NoError(err) ctx := types.NewFuncContext(s.pool) input := s.buildScoreInput([]float32{0.12345, 0.98765, 0.55555}) defer input.Release() results, err := expr.Execute(ctx, []*arrow.Chunked{input}) s.Require().NoError(err) defer results[0].Release() chunk := results[0].Chunk(0).(*array.Float32) s.InDelta(float32(0.123), chunk.Value(0), 0.0001) s.InDelta(float32(0.988), chunk.Value(1), 0.0001) s.InDelta(float32(0.556), chunk.Value(2), 0.0001) }