Go-Redis × 向量检索实战用 HNSW 在 Redis 中索引与查询文本 Embedding(Hash & JSON 双版本)

1. 场景与思路

  • 痛点:把“文本内容”转成向量后,如何在本地 Redis 里做近似向量搜索(KNN),而不依赖外部向量数据库?

  • 方案

    1. 利用 HuggingFace 模型 sentence-transformers/all-MiniLM-L6-v2 生成 384 维 Float32 向量;
    2. 借助 RediSearchHNSW 索引能力,在 Hash 或 JSON 文档里存储 & 查询向量;
    3. go-redis v9 的高阶 API(FTCreate / FTSearch / FTAggregate)完成端到端流程。

2. 环境准备

# 初始化 Go Module
go mod init vecexample

# 安装依赖
go get github.com/redis/go-redis/v9
go get github.com/henomis/lingoose/embedder/huggingface

⚠️ Redis 服务器

  • 启动模块:redis-stackredis-stack-server 或自行加载 redisearch.so + rejson.so
  • go-redis 建议 Protocol: 2 (RESP2),避免 RESP3 生僻结构的解析开销。

3. 连接 Redis

ctx := context.Background()

rdb := redis.NewClient(&redis.Options{
	Addr:     "localhost:6379",
	Protocol: 2, // RESP2 更省心
})

4. 创建索引(Hash 版)

_, _ = rdb.FTDropIndexWithArgs(ctx,
	"vector_idx",
	&redis.FTDropIndexOptions{DeleteDocs: true},
) // 防止重复

_, err := rdb.FTCreate(ctx, "vector_idx",
	&redis.FTCreateOptions{
		OnHash: true,
		Prefix: []any{"doc:"}, // 监听 doc:* 键
	},
	&redis.FieldSchema{FieldName: "content", FieldType: redis.SearchFieldTypeText},
	&redis.FieldSchema{FieldName: "genre",   FieldType: redis.SearchFieldTypeTag},
	&redis.FieldSchema{
		FieldName: "embedding",
		FieldType: redis.SearchFieldTypeVector,
		VectorArgs: &redis.FTVectorArgs{
			HNSWOptions: &redis.FTHNSWOptions{
				Dim: 384, Type: "FLOAT32", DistanceMetric: "L2",
			},
		},
	},
).Result()
if err != nil { panic(err) }

5. 生成文本 Embedding

hf := huggingfaceembedder.New().
	WithToken(os.Getenv("HF_TOKEN")).                 // HuggingFace 访问令牌
	WithModel("sentence-transformers/all-MiniLM-L6-v2")

sentences := []string{
	"That is a very happy person",
	"That is a happy dog",
	"Today is a sunny day",
}
tags := []string{"persons", "pets", "weather"}

embs, _ := hf.Embed(ctx, sentences)

将 []float32 转成字节串(Hash 专用)

func floatsToBytes(fs []float32) []byte {
	buf := make([]byte, len(fs)*4)
	for i, f := range fs {
		binary.LittleEndian.PutUint32(buf[i*4:], math.Float32bits(f))
	}
	return buf
}

6. 写入 Hash 文档并自动索引

for i, emb := range embs {
	_, _ = rdb.HSet(ctx, fmt.Sprintf("doc:%d", i), map[string]any{
		"content":   sentences[i],
		"genre":     tags[i],
		"embedding": floatsToBytes(emb.ToFloat32()),
	}).Result()
}

7. KNN 查询

qEmb, _ := hf.Embed(ctx, []string{"That is a happy person"})
buf := floatsToBytes(qEmb[0].ToFloat32())

res, _ := rdb.FTSearchWithArgs(ctx,
	"vector_idx",
	"*=>[KNN 3 @embedding $vec AS score]",
	&redis.FTSearchOptions{
		Return: []redis.FTSearchReturn{
			{FieldName: "score"},
			{FieldName: "content"},
		},
		Params: map[string]any{"vec": buf},
		DialectVersion: 2,
	},
).Result()

for _, d := range res.Docs {
	fmt.Printf("%s\t%v\n", d.Fields["content"], d.Fields["score"])
}

输出示例

That is a very happy person     0.114...
That is a happy dog             0.610...
Today is a sunny day            1.486...

8. 聚合统计:每类文本数量

agg, _ := rdb.FTAggregateWithArgs(ctx,
	"vector_idx", "*",
	&redis.FTAggregateOptions{
		GroupBy: []redis.FTAggregateGroupBy{
			{
				Fields: []any{"@genre"},
				Reduce: []redis.FTAggregateReducer{
					{Reducer: redis.SearchCount, As: "cnt"},
				},
			},
		},
	},
).Result()

for _, row := range agg.Rows {
	fmt.Printf("%s : %v\n", row.Fields["genre"], row.Fields["cnt"])
}

9. 切换 JSON 存储的关键差异

步骤 Hash JSON
FTCreate OnHash:true OnJSON:true;字段用 $.path + As
写入方式 HSet("doc:*", ...) + 字节串 JSONSet("jdoc:*", "$", ...) + []float32 原样
查询参数 依旧传字节串 依旧传字节串(参数统一编码)
结果字段 字段直接展开 字段在 Fields["$"] 内或按别名返回

JSON 写入示例

_, _ = rdb.FTCreate(ctx, "vector_json_idx",
	&redis.FTCreateOptions{OnJSON: true, Prefix: []any{"jdoc:"}},
	&redis.FieldSchema{FieldName: "$.content", As: "content", FieldType: redis.SearchFieldTypeText},
	&redis.FieldSchema{FieldName: "$.genre",   As: "genre",   FieldType: redis.SearchFieldTypeTag},
	&redis.FieldSchema{FieldName: "$.embedding", As: "embedding",
		FieldType: redis.SearchFieldTypeVector,
		VectorArgs: &redis.FTVectorArgs{
			HNSWOptions: &redis.FTHNSWOptions{Dim: 384, Type: "FLOAT32", DistanceMetric: "L2"},
		},
	},
).Result()

for i, emb := range embs {
	_, _ = rdb.JSONSet(ctx, fmt.Sprintf("jdoc:%d", i), "$", map[string]any{
		"content":   sentences[i],
		"genre":     tags[i],
		"embedding": emb.ToFloat32(), // 直接存数组!
	}).Result()
}

10. 常见问题 & 排错

问题 可能原因 / 解决
ERR unknown index name 忘记先 FT.CREATE 或 Index 名写错
查询报 Property is not a vector 向量字段未被识别:检查 FieldType、Dim、Type
向量搜索速度慢 调整 HNSW 参数 EF_CONSTRUCTION / M,或增加内存
RESP3 解析困难 使用 Protocol:2,或调用 RawResult() 自行解析

11. 结语

通过 go-redis + RediSearch,你可以在本地 Redis 轻松实现:

  1. 文本 Embedding 近似搜索(KNN & 距离排名)
  2. 多条件过滤、聚合分析 与传统全文搜索混合使用
  3. Hash 与 JSON 双存储的无缝切换

这使得 Redis 成为“小而全”的 实时语义检索引擎。复制本文代码,即刻在你的业务中解锁 AI 搜索能力吧!

你可能感兴趣的:(数据库,运维,缓存技术,golang,redis,embedding)