Skip to content

Commit 10c7640

Browse files
Fix mixed numeric types in embedding column (#1222)
1 parent bfc2ab9 commit 10c7640

3 files changed

Lines changed: 145 additions & 8 deletions

File tree

logics/item_to_item.go

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package logics
1717
import (
1818
"context"
1919
"errors"
20+
"fmt"
2021
"slices"
2122
"strings"
2223
"sync"
@@ -199,10 +200,11 @@ func (e *embeddingItemToItem) Push(item *data.Item, _ []int32) {
199200
zap.Any("item", item), zap.Error(err))
200201
return
201202
}
202-
// Check column type
203-
v, ok := result.([]float32)
204-
if !ok {
205-
log.Logger().Error("invalid column type", zap.Any("column", result))
203+
// Convert column to []float32
204+
v, err := toFloat32Slice(result)
205+
if err != nil {
206+
log.Logger().Error("failed to convert column to float32 slice",
207+
zap.Any("column", result), zap.Error(err))
206208
return
207209
}
208210
// Check dimension
@@ -218,6 +220,60 @@ func (e *embeddingItemToItem) Push(item *data.Item, _ []int32) {
218220
e.pushItem(item, v)
219221
}
220222

223+
// toFloat32Slice converts an any to []float32, handling mixed numeric types
224+
// that may occur when reading from MongoDB (e.g., 0 stored as int instead of float32)
225+
func toFloat32Slice(v any) ([]float32, error) {
226+
switch val := v.(type) {
227+
case []float32:
228+
return val, nil
229+
case []float64:
230+
result := make([]float32, len(val))
231+
for i, e := range val {
232+
result[i] = float32(e)
233+
}
234+
return result, nil
235+
case []int:
236+
result := make([]float32, len(val))
237+
for i, e := range val {
238+
result[i] = float32(e)
239+
}
240+
return result, nil
241+
case []int32:
242+
result := make([]float32, len(val))
243+
for i, e := range val {
244+
result[i] = float32(e)
245+
}
246+
return result, nil
247+
case []int64:
248+
result := make([]float32, len(val))
249+
for i, e := range val {
250+
result[i] = float32(e)
251+
}
252+
return result, nil
253+
case []any:
254+
result := make([]float32, len(val))
255+
for i, elem := range val {
256+
switch e := elem.(type) {
257+
case float32:
258+
result[i] = e
259+
case float64:
260+
result[i] = float32(e)
261+
case int:
262+
result[i] = float32(e)
263+
case int32:
264+
result[i] = float32(e)
265+
case int64:
266+
result[i] = float32(e)
267+
default:
268+
return nil, fmt.Errorf("invalid element type %T in slice", e)
269+
}
270+
}
271+
return result, nil
272+
default:
273+
return nil, fmt.Errorf("invalid column type %T", v)
274+
}
275+
}
276+
221277
type tagsItemToItem struct {
222278
baseItemToItem[[]dataset.ID]
223279
IDF[dataset.ID]
@@ -425,9 +481,10 @@ func (g *chatItemToItem) PopAll(i int) []cache.Score {
425481
zap.Any("item", item), zap.Error(err))
426482
return nil
427483
}
428-
embedding0, ok := result.([]float32)
429-
if !ok {
430-
log.Logger().Error("invalid column type", zap.Any("column", result))
484+
embedding0, err := toFloat32Slice(result)
485+
if err != nil {
486+
log.Logger().Error("failed to convert column to float32 slice",
487+
zap.Any("column", result), zap.Error(err))
431488
return nil
432489
}
433490
// render template

logics/item_to_item_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/gorse-io/gorse/config"
2525
"github.com/gorse-io/gorse/dataset"
2626
"github.com/gorse-io/gorse/storage/data"
27+
"github.com/stretchr/testify/assert"
2728
"github.com/stretchr/testify/suite"
2829
)
2930

@@ -282,3 +283,82 @@ func (suite *ItemToItemTestSuite) TestChat() {
282283
func TestItemToItem(t *testing.T) {
283284
suite.Run(t, new(ItemToItemTestSuite))
284285
}
286+
287+
func TestToFloat32Slice(t *testing.T) {
288+
floatSlice := []float32{0.1, 0.2, 0.3}
289+
testCases := []struct {
290+
name string
291+
input any
292+
expected []float32
293+
errContains string
294+
}{
295+
{
296+
name: "float32 slice",
297+
input: floatSlice,
298+
expected: []float32{0.1, 0.2, 0.3},
299+
},
300+
{
301+
name: "float64 slice",
302+
input: []float64{0.1, 0.2, 0.3},
303+
expected: []float32{0.1, 0.2, 0.3},
304+
},
305+
{
306+
name: "int slice",
307+
input: []int{-1, 0, 2},
308+
expected: []float32{-1, 0, 2},
309+
},
310+
{
311+
name: "int32 slice",
312+
input: []int32{-1, 0, 2},
313+
expected: []float32{-1, 0, 2},
314+
},
315+
{
316+
name: "int64 slice",
317+
input: []int64{-1, 0, 2},
318+
expected: []float32{-1, 0, 2},
319+
},
320+
{
321+
name: "mixed any slice",
322+
input: []any{float32(0.1), float64(0.2), int(0), int32(1), int64(2)},
323+
expected: []float32{0.1, 0.2, 0.0, 1.0, 2.0},
324+
},
325+
{
326+
name: "empty any slice",
327+
input: []any{},
328+
expected: []float32{},
329+
},
330+
{
331+
name: "invalid element in any slice",
332+
input: []any{float32(0.1), "string"},
333+
errContains: "invalid element type",
334+
},
335+
{
336+
name: "nil element in any slice",
337+
input: []any{float32(0.1), nil},
338+
errContains: "invalid element type",
339+
},
340+
{
341+
name: "invalid input type",
342+
input: "string",
343+
errContains: "invalid column type",
344+
},
345+
{
346+
name: "nil input",
347+
input: nil,
348+
errContains: "invalid column type",
349+
},
350+
}
351+
352+
for _, tc := range testCases {
353+
t.Run(tc.name, func(t *testing.T) {
354+
result, err := toFloat32Slice(tc.input)
355+
if tc.errContains != "" {
356+
assert.Error(t, err)
357+
assert.ErrorContains(t, err, tc.errContains)
358+
return
359+
}
360+
assert.NoError(t, err)
361+
assert.Equal(t, tc.expected, result)
362+
})
363+
}
364+
}

model/params.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const (
4949
)
5050

5151
// Params stores hyper-parameters for an model. It is a map between strings
52-
// (names) and interface{}s (values). For example, hyper-parameters for SVD
52+
// (names) and anys (values). For example, hyper-parameters for SVD
5353
// is given by:
5454
//
5555
// base.Params{

0 commit comments

Comments
 (0)