package memory import ( "context" "hash/fnv" "math" "os" "strings" "testing" ) // detEmbedder:确定性伪嵌入(同文本同向量;含指定关键词的文本在对应维度更高)。 // 只为端到端验证「本包代码路径」——Upsert 存向量 → Get 算余弦重排 —— 对真 PG, // 不打真 embedding 网络(那是 rag 包已验证的复用基建)。 type detEmbedder struct{} func (detEmbedder) Embed(_ context.Context, texts []string) ([][]float32, error) { const dim = 16 kw := []string{"咖啡", "coffee", "运动", "健身", "音乐"} out := make([][]float32, len(texts)) for i, t := range texts { v := make([]float32, dim) for _, w := range kw { if strings.Contains(t, w) { h := fnv.New32a() _, _ = h.Write([]byte(w)) v[h.Sum32()%dim] += 1 } } var n float64 for _, x := range v { n += float64(x) * float64(x) } if n > 0 { for j := range v { v[j] = float32(float64(v[j]) / math.Sqrt(n)) } } out[i] = v } return out, nil } // 端到端(真 PG):写两条记忆(咖啡 importance=3 / 运动 importance=8)→ // 无 query 时运动(重要度高)在前;query="咖啡"时咖啡(语义相关)翻到前面。 func TestRelevance_EndToEnd(t *testing.T) { dsn := os.Getenv("MEMORY_TEST_DSN") if dsn == "" { t.Skip("设 MEMORY_TEST_DSN 启用 Postgres 端到端测试") } s := Open(dsn) if s.db == nil { t.Fatal("Store 不应降级") } s.SetEmbedder(detEmbedder{}) ctx := context.Background() uid := "memtest-relevance-e2e" defer func() { _ = s.Delete(ctx, uid, "饮品偏好") _ = s.Delete(ctx, uid, "运动习惯") }() if err := s.Upsert(ctx, uid, "饮品偏好", "喜欢手冲咖啡 coffee 不加糖", 3); err != nil { t.Fatalf("upsert 咖啡: %v", err) } if err := s.Upsert(ctx, uid, "运动习惯", "每天健身运动一小时", 8); err != nil { t.Fatalf("upsert 运动: %v", err) } // 确认写入即向量化:embedding 列非空。 var rows []Profile s.db.WithContext(ctx).Where("user_id = ?", uid).Find(&rows) for _, r := range rows { if len(r.Embedding) == 0 { t.Errorf("%s 应已向量化(embedding 非空)", r.Key) } } // 无 query:两项打分,运动(importance 8)在前。 noQ, _ := s.Get(ctx, uid, "") if !strings.HasPrefix(noQ, "- 运动习惯") { t.Errorf("无 query 应重要度优先(运动在前),得:\n%s", noQ) } // query 咖啡:三项打分,咖啡语义相关翻到前面(尽管重要度更低)。 withQ, _ := s.Get(ctx, uid, "推荐一款好喝的咖啡 coffee") if !strings.HasPrefix(withQ, "- 饮品偏好") { t.Errorf("query=咖啡 应相关性优先(咖啡在前),得:\n%s", withQ) } }