Files

287 lines
7.7 KiB
Go
Raw Permalink Normal View History

package gorse
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
gorseio "github.com/gorse-io/gorse-go"
)
// FeedbackType 反馈类型
type FeedbackType string
const (
FeedbackTypeLike FeedbackType = "like" // 点赞
FeedbackTypeStar FeedbackType = "star" // 收藏
FeedbackTypeComment FeedbackType = "comment" // 评论
FeedbackTypeRead FeedbackType = "read" // 浏览
)
// Score 非个性化推荐返回的评分项
type Score struct {
Id string `json:"Id"`
Score float64 `json:"Score"`
}
// Client Gorse客户端接口
type Client interface {
// InsertFeedback 插入用户反馈
InsertFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error
// DeleteFeedback 删除用户反馈
DeleteFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error
// GetRecommend 获取个性化推荐列表
GetRecommend(ctx context.Context, userID string, n int, offset int) ([]string, error)
// GetNonPersonalized 获取非个性化推荐(通过名称)
GetNonPersonalized(ctx context.Context, name string, n int, offset int, userID string) ([]string, error)
// UpsertItem 插入或更新物品无embedding
UpsertItem(ctx context.Context, itemID string, categories []string, comment string) error
// UpsertItemWithEmbedding 插入或更新物品带embedding
UpsertItemWithEmbedding(ctx context.Context, itemID string, categories []string, comment string, textToEmbed string) error
// DeleteItem 删除物品
DeleteItem(ctx context.Context, itemID string) error
// UpsertUser 插入或更新用户
UpsertUser(ctx context.Context, userID string, labels map[string]any) error
// IsEnabled 检查是否启用
IsEnabled() bool
}
// client Gorse客户端实现
type client struct {
config Config
gorse *gorseio.GorseClient
httpClient *http.Client
}
// NewClient 创建新的Gorse客户端
func NewClient(cfg Config) Client {
if !cfg.Enabled {
return &noopClient{}
}
gorse := gorseio.NewGorseClient(cfg.Address, cfg.APIKey)
return &client{
config: cfg,
gorse: gorse,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// IsEnabled 检查是否启用
func (c *client) IsEnabled() bool {
return c.config.Enabled
}
// InsertFeedback 插入用户反馈
func (c *client) InsertFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.InsertFeedback(ctx, []gorseio.Feedback{
{
FeedbackType: string(feedbackType),
UserId: userID,
ItemId: itemID,
Timestamp: time.Now().UTC().Truncate(time.Second),
},
})
return err
}
// DeleteFeedback 删除用户反馈
func (c *client) DeleteFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.DeleteFeedback(ctx, string(feedbackType), userID, itemID)
return err
}
// GetRecommend 获取个性化推荐列表
func (c *client) GetRecommend(ctx context.Context, userID string, n int, offset int) ([]string, error) {
if !c.config.Enabled {
return nil, nil
}
result, err := c.gorse.GetRecommend(ctx, userID, "", n, offset)
if err != nil {
return nil, err
}
return result, nil
}
// GetNonPersonalized 获取非个性化推荐
// name: 推荐器名称,如 "most_liked_weekly"
// n: 返回数量
// offset: 偏移量
// userID: 可选,用于排除用户已读物品
func (c *client) GetNonPersonalized(ctx context.Context, name string, n int, offset int, userID string) ([]string, error) {
if !c.config.Enabled {
return nil, nil
}
// 构建URL
url := fmt.Sprintf("%s/api/non-personalized/%s?n=%d&offset=%d", c.config.Address, name, n, offset)
if userID != "" {
url += fmt.Sprintf("&user-id=%s", userID)
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// 设置API Key
if c.config.APIKey != "" {
req.Header.Set("X-API-Key", c.config.APIKey)
}
// 发送请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("gorse api error: status=%d, body=%s", resp.StatusCode, string(body))
}
// 解析响应
var scores []Score
if err := json.Unmarshal(body, &scores); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// 提取ID
ids := make([]string, len(scores))
for i, score := range scores {
ids[i] = score.Id
}
return ids, nil
}
// UpsertItem 插入或更新物品
func (c *client) UpsertItem(ctx context.Context, itemID string, categories []string, comment string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.InsertItem(ctx, gorseio.Item{
ItemId: itemID,
IsHidden: false,
Categories: categories,
Comment: comment,
Timestamp: time.Now().UTC().Truncate(time.Second),
})
return err
}
// UpsertItemWithEmbedding 插入或更新物品带embedding
func (c *client) UpsertItemWithEmbedding(ctx context.Context, itemID string, categories []string, comment string, textToEmbed string) error {
if !c.config.Enabled {
return nil
}
// 生成embedding
var embedding []float64
if textToEmbed != "" {
var err error
embedding, err = GetEmbedding(textToEmbed)
if err != nil {
log.Printf("[WARN] Failed to get embedding for item %s: %v, using zero vector", itemID, err)
embedding = make([]float64, 1024)
}
} else {
embedding = make([]float64, 1024)
}
_, err := c.gorse.InsertItem(ctx, gorseio.Item{
ItemId: itemID,
IsHidden: false,
Categories: categories,
Comment: comment,
Timestamp: time.Now().UTC().Truncate(time.Second),
Labels: map[string]any{
"embedding": embedding,
},
})
return err
}
// DeleteItem 删除物品
func (c *client) DeleteItem(ctx context.Context, itemID string) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.DeleteItem(ctx, itemID)
return err
}
// UpsertUser 插入或更新用户
func (c *client) UpsertUser(ctx context.Context, userID string, labels map[string]any) error {
if !c.config.Enabled {
return nil
}
_, err := c.gorse.InsertUser(ctx, gorseio.User{
UserId: userID,
Labels: labels,
})
return err
}
// noopClient 空操作客户端(用于未启用推荐功能时)
type noopClient struct{}
func (c *noopClient) IsEnabled() bool { return false }
func (c *noopClient) InsertFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
return nil
}
func (c *noopClient) DeleteFeedback(ctx context.Context, feedbackType FeedbackType, userID, itemID string) error {
return nil
}
func (c *noopClient) GetRecommend(ctx context.Context, userID string, n int, offset int) ([]string, error) {
return nil, nil
}
func (c *noopClient) GetNonPersonalized(ctx context.Context, name string, n int, offset int, userID string) ([]string, error) {
return nil, nil
}
func (c *noopClient) UpsertItem(ctx context.Context, itemID string, categories []string, comment string) error {
return nil
}
func (c *noopClient) UpsertItemWithEmbedding(ctx context.Context, itemID string, categories []string, comment string, textToEmbed string) error {
return nil
}
func (c *noopClient) DeleteItem(ctx context.Context, itemID string) error { return nil }
func (c *noopClient) UpsertUser(ctx context.Context, userID string, labels map[string]any) error {
return nil
}
// 确保实现了接口
var _ Client = (*client)(nil)
var _ Client = (*noopClient)(nil)
// log 用于内部日志
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}