112 lines
3.5 KiB
Go
112 lines
3.5 KiB
Go
|
|
package embed
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"math"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestNormalizeProducesUnitVectors(t *testing.T) {
|
||
|
|
got := Normalize([]float32{3, 4})
|
||
|
|
if math.Abs(float64(got[0])-0.6) > 1e-6 || math.Abs(float64(got[1])-0.8) > 1e-6 {
|
||
|
|
t.Errorf("Normalize([3,4]) = %v, want [0.6 0.8]", got)
|
||
|
|
}
|
||
|
|
// A zero vector must survive untouched — dividing by its length would
|
||
|
|
// put NaN into every later comparison.
|
||
|
|
zero := []float32{0, 0, 0}
|
||
|
|
if out := Normalize(zero); out[0] != 0 || math.IsNaN(float64(out[0])) {
|
||
|
|
t.Errorf("Normalize(zero) = %v, want zeros", out)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSimilarity(t *testing.T) {
|
||
|
|
a := Normalize([]float32{1, 0})
|
||
|
|
if s := Similarity(a, a); math.Abs(s-1) > 1e-6 {
|
||
|
|
t.Errorf("self-similarity = %v, want 1", s)
|
||
|
|
}
|
||
|
|
if s := Similarity(a, Normalize([]float32{0, 1})); math.Abs(s) > 1e-6 {
|
||
|
|
t.Errorf("orthogonal similarity = %v, want 0", s)
|
||
|
|
}
|
||
|
|
// A vector from a different model must never outrank a real hit.
|
||
|
|
if s := Similarity(a, []float32{1, 0, 0}); s != 0 {
|
||
|
|
t.Errorf("mismatched dimensions scored %v, want 0", s)
|
||
|
|
}
|
||
|
|
if s := Similarity(nil, nil); s != 0 {
|
||
|
|
t.Errorf("empty vectors scored %v, want 0", s)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEncodeDecodeRoundTrip(t *testing.T) {
|
||
|
|
in := []float32{0.5, -0.25, 1e-8, 12345.75}
|
||
|
|
out := Decode(Encode(in))
|
||
|
|
if len(out) != len(in) {
|
||
|
|
t.Fatalf("round trip changed length: %d → %d", len(in), len(out))
|
||
|
|
}
|
||
|
|
for i := range in {
|
||
|
|
if in[i] != out[i] {
|
||
|
|
t.Errorf("round trip [%d]: %v → %v", i, in[i], out[i])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got := Decode([]byte{1, 2, 3}); got != nil {
|
||
|
|
t.Errorf("Decode of a truncated blob = %v, want nil", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The API returns an `index` per item rather than promising input order, so
|
||
|
|
// the client must reorder — otherwise chunk N gets chunk M's vector and every
|
||
|
|
// later search is quietly wrong.
|
||
|
|
func TestEmbedReordersByIndexAndNormalizes(t *testing.T) {
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req embedRequest
|
||
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
||
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||
|
|
"data": []map[string]any{
|
||
|
|
{"index": 1, "embedding": []float32{0, 5}},
|
||
|
|
{"index": 0, "embedding": []float32{3, 4}},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}))
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
vecs, err := New(Config{BaseURL: srv.URL}).Embed(context.Background(), []string{"first", "second"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(vecs) != 2 {
|
||
|
|
t.Fatalf("got %d vectors, want 2", len(vecs))
|
||
|
|
}
|
||
|
|
if math.Abs(float64(vecs[0][0])-0.6) > 1e-6 {
|
||
|
|
t.Errorf("vecs[0] = %v, want the index-0 item normalized ([0.6 0.8])", vecs[0])
|
||
|
|
}
|
||
|
|
if math.Abs(float64(vecs[1][1])-1) > 1e-6 {
|
||
|
|
t.Errorf("vecs[1] = %v, want the index-1 item normalized ([0 1])", vecs[1])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEmbedRejectsShortResponse(t *testing.T) {
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||
|
|
"data": []map[string]any{{"index": 0, "embedding": []float32{1, 0}}},
|
||
|
|
})
|
||
|
|
}))
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
if _, err := New(Config{BaseURL: srv.URL}).Embed(context.Background(), []string{"a", "b"}); err == nil {
|
||
|
|
t.Fatal("want an error when the endpoint returns fewer vectors than inputs")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEmbedSurfacesHTTPError(t *testing.T) {
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
http.Error(w, "model not loaded", http.StatusServiceUnavailable)
|
||
|
|
}))
|
||
|
|
defer srv.Close()
|
||
|
|
|
||
|
|
if _, err := New(Config{BaseURL: srv.URL}).Embed(context.Background(), []string{"a"}); err == nil {
|
||
|
|
t.Fatal("want an error on a 503")
|
||
|
|
}
|
||
|
|
}
|