1. 向量数据库Milvus

Milvus是开源向量数据库中功能最全面的,由Zilliz公司开发维护。它支持多种 ANN索引(HNSW、IVF_FLAT、IVF_SQ8等),支持标量过滤(比如"只在category=技术的文档中检索"),有完善的Go SDK,而且Eino框架官方提供了Milvus 的Indexer 和Retriever适配。社区活跃,文档丰富,是Go开发者做RAG的首选。

2. Milvus 安装

docker compose 安装

wget https://github.com/milvus-io/milvus/releases/download/v3.0-beta/milvus-standalone-docker-compose.yml -O docker-compose.yml
sudo docker compose up -d

docker desktop 安装
*注意:windows下需要使用 powershell 管理员执行以下命令;docker 下载镜像慢,请使用国内镜像源 https://docker.1ms.run *

Invoke-WebRequest https://raw.githubusercontent.com/milvus-io/milvus/refs/heads/master/scripts/standalone_embed.bat -OutFile standalone.bat

standalone.bat start

在这里插入图片描述
启动后,milvus 默认端口为: 19530

3. Eino 操作Milvus示例

通过

package main

import (
	"context"
	"fmt"
	"math"
	"time"

	"github.com/milvus-io/milvus/client/v2/column"
	"github.com/milvus-io/milvus/client/v2/entity"
	"github.com/milvus-io/milvus/client/v2/index"
	"github.com/milvus-io/milvus/client/v2/milvusclient"
	"github.com/sashabaranov/go-openai"
)

const (
	collectionName = "go_knowledge"
	embeddingDim   = 1024 // text-embedding-v3 默认输出 1024 维
)

// 知识库文档
var documents = []string{
	"Go语言的并发模型基于CSP理论,goroutine是轻量级协程,初始栈空间仅2KB,通过channel进行类型安全的通信。",
	"Go语言的GC采用三色标记清除算法,从1.5版本开始STW时间控制在毫秒级,可通过GOGC环境变量调整触发频率。",
	"Go语言的接口是隐式实现的,只要类型实现了接口的所有方法就自动满足该接口,空接口interface{}可用any代替。",
	"Go Module是官方依赖管理方案,go.mod记录模块路径和依赖版本,go.sum保存哈希校验值,go mod tidy清理依赖。",
	"Go语言的错误处理采用显式返回error的方式,errors.Is和errors.As用于判断错误类型,支持%w格式化动词包装错误。",
}

func main() {
	ctx := context.Background()

	// ====== 1. 初始化客户端 ======
	// 初始化 Milvus 客户端
	milClient, err := milvusclient.New(ctx, &milvusclient.ClientConfig{
		Address: "localhost:19530",
	})
	if err != nil {
		fmt.Printf("连接 Milvus 失败: %v\n", err)
		return
	}
	defer milClient.Close(ctx)

	// 创建大模型基础
	aiConfig := openai.DefaultConfig("xxx")  // 魔塔社区API KEY
	aiConfig.BaseURL = "https://api-inference.modelscope.cn/v1/"
	aiClient := openai.NewClientWithConfig(aiConfig)

	// ====== 2. 创建 Collection ======
	// 先检查是否已存在,存在则删除(方便反复测试)
	has, _ := milClient.HasCollection(ctx, milvusclient.NewHasCollectionOption(collectionName))
	if has {
		err := milClient.DropCollection(ctx, milvusclient.NewDropCollectionOption(collectionName))
		if err != nil {
			return
		}
	}

	// 定义 Collection 的 Schema
	schema := entity.NewSchema().WithName(collectionName).WithDescription("Go语言知识库")
	schema.
		WithField(entity.NewField().WithName("id").WithDataType(entity.FieldTypeInt64).WithIsPrimaryKey(true).WithIsAutoID(true)).
		WithField(entity.NewField().WithName("content").WithDataType(entity.FieldTypeVarChar).WithMaxLength(2000)).
		WithField(entity.NewField().WithName("embedding").WithDataType(entity.FieldTypeFloatVector).WithDim(embeddingDim))

	// 创建 Collection
	err = milClient.CreateCollection(ctx, milvusclient.NewCreateCollectionOption(collectionName, schema))
	if err != nil {
		fmt.Printf("创建 Collection 失败: %v\n", err)
		return
	}
	fmt.Println("✅ Collection 创建成功")
	// ====== 3. 生成 Embedding 并插入数据 ======
	fmt.Println("\n📚 正在向量化文档并插入...")
	contents := make([]string, len(documents))
	embedding := make([][]float32, len(documents))
	for i, doc := range documents {
		contents[i] = doc

		// 请求之间添加间隔,避免触发限流
		if i > 0 {
			time.Sleep(500 * time.Millisecond)
		}

		emb, err := getEmbedding(ctx, aiClient, doc)
		if err != nil {
			fmt.Printf("文档 %d 向量化失败: %v\n", i, err)
			return
		}
		embedding[i] = emb
		fmt.Printf("  文档 %d 已向量化(%d 维)\n", i+1, len(emb))
	}

	// 批量插入
	contentColumn := column.NewColumnVarChar("content", contents)
	embeddingColumn := column.NewColumnFloatVector("embedding", embeddingDim, embedding)
	_, err = milClient.Insert(ctx, milvusclient.NewColumnBasedInsertOption(collectionName).WithColumns(contentColumn, embeddingColumn))
	if err != nil {
		fmt.Printf("插入数据失败: %v\n", err)
		return
	}
	fmt.Printf("✅ 成功插入 %d 条文档\n", len(documents))

	// ====== 4. 创建索引 ======
	// 在 embedding 字段上创建 HNSW 索引
	hnswIndex := index.NewHNSWIndex(entity.COSINE, 16, 200)
	createIndexTask, err := milClient.CreateIndex(ctx, milvusclient.NewCreateIndexOption(collectionName, "embedding", hnswIndex))
	if err != nil {
		fmt.Printf("创建索引失败: %v\n", err)
		return
	}
	err = createIndexTask.Await(ctx)
	if err != nil {
		fmt.Printf("创建索引失败: %v\n", err)
		return
	}
	fmt.Println("✅ HNSW 索引创建成功")

	// ====== 5. 加载 Collection 到内存 ======
	loadTask, err := milClient.LoadCollection(ctx, milvusclient.NewLoadCollectionOption(collectionName))
	if err != nil {
		fmt.Printf("加载 Collection 失败: %v\n", err)
		return
	}
	err = loadTask.Await(ctx)
	if err != nil {
		fmt.Printf("加载 Collection 失败: %v\n", err)
		return
	}
	fmt.Println("✅ Collection 已加载到内存")

	// ====== 6. 执行向量检索 ======
	query := "Go语言怎么做并发编程?"
	fmt.Printf("\n🔍 查询: %s\n\n", query)

	queryEmbedding, err := getEmbedding(ctx, aiClient, query)
	if err != nil {
		fmt.Printf("查询向量化失败: %v\n", err)
		return
	}
	// 搜索 Top-3
	searchTask, err := milClient.Search(ctx, milvusclient.NewSearchOption(collectionName, 3, []entity.Vector{entity.FloatVector(queryEmbedding)}).WithOutputFields("content"))
	if err != nil {
		fmt.Printf("检索失败: %v\n", err)
		return
	}
	fmt.Println("📋 检索结果:")
	for _, result := range searchTask {
		for i := 0; i < result.ResultCount; i++ {
			content, _ := result.GetColumn("content").GetAsString(i)
			score := result.Scores[i]
			fmt.Printf("  [%d] 相似度: %.4f\n      内容: %s\n\n", i+1, score, content)
		}
	}
}

func getEmbedding(ctx context.Context, client *openai.Client, text string) ([]float32, error) {
	var lastErr error
	maxRetries := 3

	for attempt := 0; attempt < maxRetries; attempt++ {
		if attempt > 0 {
			// 指数退避: 1s, 2s, 4s
			wait := time.Duration(math.Pow(2, float64(attempt-1))) * time.Second
			fmt.Printf("  ⏳ 等待 %.0f 秒后重试 (%d/%d)...\n", wait.Seconds(), attempt, maxRetries-1)
			time.Sleep(wait)
		}

		resp, err := client.CreateEmbeddings(ctx, openai.EmbeddingRequest{
			Model:          "Qwen/Qwen3-Embedding-0.6B",
			Input:          []string{text},
			EncodingFormat: openai.EmbeddingEncodingFormatBase64,
		})
		if err != nil {
			lastErr = err
			// 检查是否是429错误
			if isRateLimitError(err) {
				continue
			}
			return nil, err
		}
		return resp.Data[0].Embedding, nil
	}

	return nil, fmt.Errorf("达到最大重试次数 (%d): %v", maxRetries, lastErr)
}

// isRateLimitError 检查错误是否为 429 限流错误
func isRateLimitError(err error) bool {
	if err == nil {
		return false
	}
	// 检查错误消息中是否包含 429
	return true
}

由于魔塔社区的API 调用存在限流,代码中增加了延时处理。

运行结果,如下:

✅ Collection 创建成功

📚 正在向量化文档并插入...
  文档 1 已向量化(1024 维)
  文档 2 已向量化(1024 维)
  文档 3 已向量化(1024 维)
  文档 4 已向量化(1024 维)
  文档 5 已向量化(1024 维)
✅ 成功插入 5 条文档
✅ HNSW 索引创建成功
✅ Collection 已加载到内存

🔍 查询: Go语言怎么做并发编程?

📋 检索结果:
  [1] 相似度: 0.7386
      内容: Go语言的并发模型基于CSP理论,goroutine是轻量级协程,初始栈空间仅2KB,通过channel进行类型安全的通信。

  [2] 相似度: 0.6472
      内容: Go语言的GC采用三色标记清除算法,从1.5版本开始STW时间控制在毫秒级,可通过GOGC环境变量调整触发频率。

  [3] 相似度: 0.5871
      内容: Go语言的错误处理采用显式返回error的方式,errors.Is和errors.As用于判断错误类型,支持%w格式化动词包装错误。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐