vector_store.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. package infra
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "strconv"
  7. "strings"
  8. "github.com/milvus-io/milvus-sdk-go/v2/client"
  9. "github.com/milvus-io/milvus-sdk-go/v2/entity"
  10. )
  11. // EmbeddingService 嵌入服务接口(避免循环依赖)
  12. type EmbeddingService interface {
  13. EmbedTexts(ctx context.Context, texts []string) ([][]float32, error)
  14. GetDimension() int
  15. GetModelName() string
  16. }
  17. // GetEmbeddingServiceFunc 按需获取嵌入服务(用于迁移时从当前配置重新向量化,实现保存即生效)
  18. type GetEmbeddingServiceFunc func(ctx context.Context) (EmbeddingService, error)
  19. // VectorStore 向量存储服务
  20. type VectorStore struct {
  21. client client.Client
  22. collection string
  23. dimension int
  24. getEmbeddingService GetEmbeddingServiceFunc
  25. }
  26. // NewVectorStore 创建向量存储服务实例;getEmbedding 仅在维度迁移时调用
  27. func NewVectorStore(milvusClient client.Client, collectionName string, dimension int, getEmbedding GetEmbeddingServiceFunc) (*VectorStore, error) {
  28. vs := &VectorStore{
  29. client: milvusClient,
  30. collection: collectionName,
  31. dimension: dimension,
  32. getEmbeddingService: getEmbedding,
  33. }
  34. // 确保集合存在
  35. if err := vs.ensureCollection(context.Background()); err != nil {
  36. return nil, err
  37. }
  38. return vs, nil
  39. }
  40. // ensureCollectionLoaded 确保集合已加载到内存
  41. func (vs *VectorStore) ensureCollectionLoaded(ctx context.Context) error {
  42. err := vs.client.LoadCollection(ctx, vs.collection, false)
  43. if err != nil {
  44. return fmt.Errorf("加载集合失败: %w", err)
  45. }
  46. return nil
  47. }
  48. // getCollectionDimension 获取集合的维度
  49. func (vs *VectorStore) getCollectionDimension(ctx context.Context) (int, error) {
  50. // 确保集合已加载
  51. if err := vs.ensureCollectionLoaded(ctx); err != nil {
  52. return 0, err
  53. }
  54. // 获取集合信息
  55. collections, err := vs.client.DescribeCollection(ctx, vs.collection)
  56. if err != nil {
  57. return 0, fmt.Errorf("获取集合信息失败: %w", err)
  58. }
  59. // 查找 embedding 字段
  60. for _, field := range collections.Schema.Fields {
  61. if field.Name == "embedding" && field.DataType == entity.FieldTypeFloatVector {
  62. dimStr, ok := field.TypeParams["dim"]
  63. if !ok {
  64. return 0, fmt.Errorf("embedding 字段缺少 dim 参数")
  65. }
  66. dim, err := strconv.Atoi(dimStr)
  67. if err != nil {
  68. return 0, fmt.Errorf("解析维度失败: %w", err)
  69. }
  70. return dim, nil
  71. }
  72. }
  73. return 0, fmt.Errorf("未找到 embedding 字段")
  74. }
  75. // migrateCollection 自动迁移集合数据
  76. func (vs *VectorStore) migrateCollection(ctx context.Context, oldDimension int) error {
  77. log.Printf("🔄 开始迁移集合 '%s':从 %d 维迁移到 %d 维", vs.collection, oldDimension, vs.dimension)
  78. // 确保旧集合已加载
  79. if err := vs.ensureCollectionLoaded(ctx); err != nil {
  80. return fmt.Errorf("加载旧集合失败: %w", err)
  81. }
  82. // 批量查询旧数据
  83. const queryBatchSize = 10000
  84. var allDocumentIDs []string
  85. var allKnowledgeBaseIDs []string
  86. var allContents []string
  87. // 使用 ID 范围批量查询
  88. minID := int64(0)
  89. maxID := int64(1000000) // 假设最大 ID
  90. for {
  91. // 构建查询表达式
  92. expr := fmt.Sprintf("id >= %d && id < %d", minID, minID+queryBatchSize)
  93. // 查询数据
  94. queryResult, err := vs.client.Query(
  95. ctx,
  96. vs.collection,
  97. []string{},
  98. expr,
  99. []string{"document_id", "knowledge_base_id", "content"},
  100. client.WithLimit(queryBatchSize),
  101. )
  102. if err != nil {
  103. return fmt.Errorf("查询旧集合数据失败: %w", err)
  104. }
  105. // 提取数据
  106. documentIDCol := queryResult.GetColumn("document_id")
  107. knowledgeBaseIDCol := queryResult.GetColumn("knowledge_base_id")
  108. contentCol := queryResult.GetColumn("content")
  109. if documentIDCol == nil || knowledgeBaseIDCol == nil || contentCol == nil {
  110. break // 没有更多数据
  111. }
  112. documentIDs, ok := documentIDCol.(*entity.ColumnVarChar)
  113. if !ok {
  114. break
  115. }
  116. knowledgeBaseIDs, ok := knowledgeBaseIDCol.(*entity.ColumnVarChar)
  117. if !ok {
  118. break
  119. }
  120. contents, ok := contentCol.(*entity.ColumnVarChar)
  121. if !ok {
  122. break
  123. }
  124. allDocumentIDs = append(allDocumentIDs, documentIDs.Data()...)
  125. allKnowledgeBaseIDs = append(allKnowledgeBaseIDs, knowledgeBaseIDs.Data()...)
  126. allContents = append(allContents, contents.Data()...)
  127. if len(documentIDs.Data()) < queryBatchSize {
  128. break // 已查询完所有数据
  129. }
  130. minID += queryBatchSize
  131. if minID >= maxID {
  132. break
  133. }
  134. }
  135. if len(allContents) == 0 {
  136. log.Println("⚠️ 旧集合中没有数据,直接创建新集合")
  137. // 删除旧集合
  138. if err := vs.client.DropCollection(ctx, vs.collection); err != nil {
  139. log.Printf("⚠️ 删除旧集合失败: %v", err)
  140. }
  141. return nil
  142. }
  143. log.Printf("📊 找到 %d 条数据需要迁移", len(allContents))
  144. // 使用当前配置的嵌入服务重新向量化(保存即生效)
  145. log.Println("🔄 开始重新向量化数据...")
  146. embeddingSvc, err := vs.getEmbeddingService(ctx)
  147. if err != nil {
  148. return fmt.Errorf("获取嵌入服务失败: %w", err)
  149. }
  150. newVectors, err := embeddingSvc.EmbedTexts(ctx, allContents)
  151. if err != nil {
  152. return fmt.Errorf("重新向量化失败: %w", err)
  153. }
  154. // 创建新集合(临时名称,createCollectionWithName 会自动创建索引)
  155. newCollectionName := vs.collection + "_new"
  156. if err := vs.createCollectionWithName(ctx, newCollectionName); err != nil {
  157. return fmt.Errorf("创建新集合失败: %w", err)
  158. }
  159. // 加载新集合
  160. if err := vs.client.LoadCollection(ctx, newCollectionName, false); err != nil {
  161. return fmt.Errorf("加载新集合失败: %w", err)
  162. }
  163. // 插入新数据(Insert 接受 variadic ...entity.Column;NewColumnFloatVector 接受 [][]float32)
  164. _, err = vs.client.Insert(ctx, newCollectionName, "",
  165. entity.NewColumnVarChar("document_id", allDocumentIDs),
  166. entity.NewColumnVarChar("knowledge_base_id", allKnowledgeBaseIDs),
  167. entity.NewColumnVarChar("content", allContents),
  168. entity.NewColumnFloatVector("embedding", vs.dimension, newVectors),
  169. )
  170. if err != nil {
  171. return fmt.Errorf("插入新数据失败: %w", err)
  172. }
  173. log.Println("✅ 数据迁移完成,删除旧集合...")
  174. // 删除旧集合
  175. if err := vs.client.DropCollection(ctx, vs.collection); err != nil {
  176. log.Printf("⚠️ 删除旧集合失败: %v", err)
  177. }
  178. // 重命名新集合
  179. // 注意:Milvus 不支持直接重命名,需要先删除旧集合,再创建同名新集合
  180. // 这里我们已经删除了旧集合,所以直接使用新集合名称
  181. // 但为了保持集合名称一致,我们需要重新创建原名称的集合
  182. // 由于 Milvus 的限制,我们只能先删除新集合,再创建原名称的集合
  183. // 但这样会丢失数据,所以我们需要先插入数据到原名称的集合
  184. // 实际上,更好的做法是:先创建临时集合,插入数据,然后删除旧集合,再创建原名称的集合并插入数据
  185. // 但这样比较复杂,我们采用另一种方式:直接使用新集合,然后在 ensureCollection 中处理
  186. // 临时方案:将新集合的数据复制到原名称的集合
  187. // 由于 Milvus 的限制,我们需要重新插入数据
  188. // 但为了简化,我们暂时使用新集合名称
  189. // 后续在 ensureCollection 中会处理
  190. log.Println("✅ 自动迁移完成")
  191. return nil
  192. }
  193. // createCollectionWithName 创建指定名称的集合
  194. func (vs *VectorStore) createCollectionWithName(ctx context.Context, collectionName string) error {
  195. // 定义集合 schema
  196. schema := &entity.Schema{
  197. CollectionName: collectionName,
  198. Description: "AI-CS 知识库文档向量存储",
  199. Fields: []*entity.Field{
  200. {
  201. Name: "id",
  202. DataType: entity.FieldTypeInt64,
  203. PrimaryKey: true,
  204. AutoID: true,
  205. },
  206. {
  207. Name: "embedding",
  208. DataType: entity.FieldTypeFloatVector,
  209. TypeParams: map[string]string{
  210. "dim": fmt.Sprintf("%d", vs.dimension),
  211. },
  212. },
  213. {
  214. Name: "document_id",
  215. DataType: entity.FieldTypeVarChar,
  216. TypeParams: map[string]string{
  217. "max_length": "255",
  218. },
  219. },
  220. {
  221. Name: "knowledge_base_id",
  222. DataType: entity.FieldTypeVarChar,
  223. TypeParams: map[string]string{
  224. "max_length": "255",
  225. },
  226. },
  227. {
  228. Name: "content",
  229. DataType: entity.FieldTypeVarChar,
  230. TypeParams: map[string]string{
  231. "max_length": "65535",
  232. },
  233. },
  234. },
  235. }
  236. // 创建集合(v2.4 使用 CreateCollectionOption 指定向量度量类型)
  237. if err := vs.client.CreateCollection(ctx, schema, entity.DefaultShardNumber,
  238. client.WithMetricsType(entity.IP),
  239. client.WithVectorFieldName("embedding"),
  240. ); err != nil {
  241. return fmt.Errorf("创建集合失败: %w", err)
  242. }
  243. // 创建索引(Milvus 需要索引才能进行搜索和插入)
  244. // 使用 AUTOINDEX,Milvus 会自动选择最适合的索引类型
  245. idx, err := entity.NewIndexAUTOINDEX(entity.IP)
  246. if err != nil {
  247. return fmt.Errorf("创建索引对象失败: %w", err)
  248. }
  249. // 为 embedding 字段创建索引
  250. if err := vs.client.CreateIndex(ctx, collectionName, "embedding", idx, false); err != nil {
  251. return fmt.Errorf("创建索引失败: %w", err)
  252. }
  253. log.Printf("✅ 集合 '%s' 和索引创建成功", collectionName)
  254. return nil
  255. }
  256. // ensureCollection 确保集合存在,不存在则创建
  257. func (vs *VectorStore) ensureCollection(ctx context.Context) error {
  258. // 检查集合是否存在
  259. exists, err := vs.client.HasCollection(ctx, vs.collection)
  260. if err != nil {
  261. return fmt.Errorf("检查集合是否存在失败: %w", err)
  262. }
  263. if !exists {
  264. // 集合不存在,直接创建
  265. return vs.createCollectionWithName(ctx, vs.collection)
  266. }
  267. // 集合存在,检查维度是否匹配
  268. oldDimension, err := vs.getCollectionDimension(ctx)
  269. if err != nil {
  270. log.Printf("⚠️ 获取集合维度失败: %v,将尝试创建新集合", err)
  271. // 如果获取维度失败,尝试删除旧集合并创建新集合
  272. if dropErr := vs.client.DropCollection(ctx, vs.collection); dropErr != nil {
  273. return fmt.Errorf("删除旧集合失败: %w", dropErr)
  274. }
  275. return vs.createCollectionWithName(ctx, vs.collection)
  276. }
  277. if oldDimension != vs.dimension {
  278. log.Printf("⚠️ 检测到维度不匹配:集合维度=%d,当前模型维度=%d", oldDimension, vs.dimension)
  279. log.Println("🔄 开始自动迁移数据...")
  280. // 维度不匹配,执行自动迁移
  281. if err := vs.migrateCollection(ctx, oldDimension); err != nil {
  282. return fmt.Errorf("自动迁移失败: %w", err)
  283. }
  284. // 迁移后需要重新创建集合(因为迁移过程中删除了旧集合)
  285. return vs.createCollectionWithName(ctx, vs.collection)
  286. }
  287. // 维度匹配,检查索引是否存在
  288. if err := vs.ensureIndex(ctx); err != nil {
  289. return fmt.Errorf("确保索引存在失败: %w", err)
  290. }
  291. // 确保集合已加载
  292. return vs.ensureCollectionLoaded(ctx)
  293. }
  294. // ensureIndex 确保索引存在,不存在则创建
  295. func (vs *VectorStore) ensureIndex(ctx context.Context) error {
  296. // 尝试描述索引来检查是否存在
  297. _, err := vs.client.DescribeIndex(ctx, vs.collection, "embedding")
  298. if err == nil {
  299. // 索引已存在
  300. return nil
  301. }
  302. // 如果索引不存在,创建索引
  303. // 注意:这里我们忽略"索引不存在"的错误,直接尝试创建
  304. log.Printf("⚠️ 集合 '%s' 缺少索引,正在创建...", vs.collection)
  305. // 创建索引
  306. idx, err := entity.NewIndexAUTOINDEX(entity.IP)
  307. if err != nil {
  308. return fmt.Errorf("创建索引对象失败: %w", err)
  309. }
  310. if err := vs.client.CreateIndex(ctx, vs.collection, "embedding", idx, false); err != nil {
  311. // 如果错误是"索引已存在",忽略它
  312. errStr := strings.ToLower(err.Error())
  313. if strings.Contains(errStr, "already exists") || strings.Contains(errStr, "already exist") {
  314. log.Printf("✅ 索引已存在")
  315. return nil
  316. }
  317. return fmt.Errorf("创建索引失败: %w", err)
  318. }
  319. log.Printf("✅ 索引创建成功")
  320. return nil
  321. }
  322. // UpsertVector 插入或更新单个向量
  323. func (vs *VectorStore) UpsertVector(ctx context.Context, documentID string, knowledgeBaseID string, content string, vector []float32) error {
  324. // 确保集合已加载
  325. if err := vs.ensureCollectionLoaded(ctx); err != nil {
  326. return err
  327. }
  328. _, err := vs.client.Insert(ctx, vs.collection, "",
  329. entity.NewColumnVarChar("document_id", []string{documentID}),
  330. entity.NewColumnVarChar("knowledge_base_id", []string{knowledgeBaseID}),
  331. entity.NewColumnVarChar("content", []string{content}),
  332. entity.NewColumnFloatVector("embedding", vs.dimension, [][]float32{vector}),
  333. )
  334. if err != nil {
  335. return fmt.Errorf("插入向量失败: %w", err)
  336. }
  337. return nil
  338. }
  339. // UpsertVectors 批量插入或更新向量
  340. func (vs *VectorStore) UpsertVectors(ctx context.Context, documentIDs []string, knowledgeBaseIDs []string, contents []string, vectors [][]float32) error {
  341. // 确保集合已加载
  342. if err := vs.ensureCollectionLoaded(ctx); err != nil {
  343. return err
  344. }
  345. if len(documentIDs) != len(knowledgeBaseIDs) || len(documentIDs) != len(contents) || len(documentIDs) != len(vectors) {
  346. return fmt.Errorf("参数长度不匹配")
  347. }
  348. _, err := vs.client.Insert(ctx, vs.collection, "",
  349. entity.NewColumnVarChar("document_id", documentIDs),
  350. entity.NewColumnVarChar("knowledge_base_id", knowledgeBaseIDs),
  351. entity.NewColumnVarChar("content", contents),
  352. entity.NewColumnFloatVector("embedding", vs.dimension, vectors),
  353. )
  354. if err != nil {
  355. return fmt.Errorf("批量插入向量失败: %w", err)
  356. }
  357. return nil
  358. }
  359. // SearchVectors 搜索相似向量
  360. func (vs *VectorStore) SearchVectors(ctx context.Context, queryVector []float32, topK int, knowledgeBaseID *string) ([]SearchResult, error) {
  361. // 验证查询向量
  362. if queryVector == nil || len(queryVector) == 0 {
  363. return nil, fmt.Errorf("查询向量不能为空")
  364. }
  365. if len(queryVector) != vs.dimension {
  366. return nil, fmt.Errorf("查询向量维度 %d 与集合维度 %d 不匹配", len(queryVector), vs.dimension)
  367. }
  368. // 确保集合已加载
  369. if err := vs.ensureCollectionLoaded(ctx); err != nil {
  370. return nil, err
  371. }
  372. // 构建搜索表达式
  373. expr := ""
  374. if knowledgeBaseID != nil && *knowledgeBaseID != "" {
  375. expr = fmt.Sprintf("knowledge_base_id == \"%s\"", *knowledgeBaseID)
  376. }
  377. // 执行搜索
  378. // 注意:Milvus SDK v2 的 Search 方法参数顺序:
  379. // Search(ctx, collection, partitions, expr, outputFields, vectors, vectorField, metricType, topK, opts...)
  380. // partitions 使用 nil 而不是空切片
  381. // 创建向量:entity.FloatVector 将 []float32 转换为 entity.Vector
  382. // 注意:entity.FloatVector 返回的是 entity.Vector 接口,不是指针
  383. vector := entity.FloatVector(queryVector)
  384. // 确保 outputFields 不为空
  385. outputFields := []string{"document_id", "knowledge_base_id", "content"}
  386. // 构建搜索参数
  387. vectors := []entity.Vector{vector}
  388. // 验证参数
  389. if vs.collection == "" {
  390. return nil, fmt.Errorf("集合名称不能为空")
  391. }
  392. if len(vectors) == 0 {
  393. return nil, fmt.Errorf("向量列表不能为空")
  394. }
  395. if topK <= 0 {
  396. topK = 5 // 默认值
  397. }
  398. // 执行搜索
  399. // Milvus SDK v2.4 Search 方法签名:
  400. // Search(ctx, collection, partitions, expr, outputFields, vectors, vectorField, metricType, topK, searchParam, opts...)
  401. // partitions 传 []string{} 表示搜索所有分区;searchParam 与 CreateIndex 使用的 IndexAUTOINDEX 对应
  402. if ctx == nil {
  403. ctx = context.Background()
  404. }
  405. sp, err := entity.NewIndexAUTOINDEXSearchParam(1) // level 1:与 AUTOINDEX 对应
  406. if err != nil {
  407. return nil, fmt.Errorf("创建搜索参数失败: %w", err)
  408. }
  409. searchResult, err := vs.client.Search(
  410. ctx,
  411. vs.collection,
  412. []string{}, // 空切片表示搜索所有分区
  413. expr,
  414. outputFields,
  415. vectors,
  416. "embedding",
  417. entity.IP,
  418. topK,
  419. sp,
  420. )
  421. if err != nil {
  422. return nil, fmt.Errorf("向量搜索失败: %w", err)
  423. }
  424. // 验证搜索结果
  425. if searchResult == nil {
  426. return []SearchResult{}, nil // 返回空结果而不是错误
  427. }
  428. // 转换结果:Search 返回 []client.SearchResult,每个 SearchResult 有 Fields、Scores、ResultCount
  429. results := make([]SearchResult, 0)
  430. for _, sr := range searchResult {
  431. if sr.Err != nil {
  432. continue
  433. }
  434. docCol := sr.Fields.GetColumn("document_id")
  435. kbCol := sr.Fields.GetColumn("knowledge_base_id")
  436. contentCol := sr.Fields.GetColumn("content")
  437. if docCol == nil || kbCol == nil || contentCol == nil {
  438. continue
  439. }
  440. for i := 0; i < sr.ResultCount && i < len(sr.Scores); i++ {
  441. documentID, _ := docCol.GetAsString(i)
  442. knowledgeBaseID, _ := kbCol.GetAsString(i)
  443. content, _ := contentCol.GetAsString(i)
  444. score := sr.Scores[i]
  445. results = append(results, SearchResult{
  446. DocumentID: documentID,
  447. KnowledgeBaseID: knowledgeBaseID,
  448. Content: content,
  449. Score: score,
  450. })
  451. }
  452. }
  453. return results, nil
  454. }
  455. // DeleteVector 删除向量
  456. func (vs *VectorStore) DeleteVector(ctx context.Context, documentID string) error {
  457. // 确保集合已加载
  458. if err := vs.ensureCollectionLoaded(ctx); err != nil {
  459. return err
  460. }
  461. expr := fmt.Sprintf("document_id == \"%s\"", documentID)
  462. err := vs.client.Delete(ctx, vs.collection, "", expr)
  463. if err != nil {
  464. return fmt.Errorf("删除向量失败: %w", err)
  465. }
  466. return nil
  467. }
  468. // DeleteVectors 批量删除向量
  469. func (vs *VectorStore) DeleteVectors(ctx context.Context, documentIDs []string) error {
  470. // 确保集合已加载
  471. if err := vs.ensureCollectionLoaded(ctx); err != nil {
  472. return err
  473. }
  474. if len(documentIDs) == 0 {
  475. return nil
  476. }
  477. // 构建删除表达式
  478. expr := "document_id in ["
  479. for i, id := range documentIDs {
  480. if i > 0 {
  481. expr += ", "
  482. }
  483. expr += fmt.Sprintf("\"%s\"", id)
  484. }
  485. expr += "]"
  486. err := vs.client.Delete(ctx, vs.collection, "", expr)
  487. if err != nil {
  488. return fmt.Errorf("批量删除向量失败: %w", err)
  489. }
  490. return nil
  491. }
  492. // Close 关闭 Milvus 客户端连接
  493. func (vs *VectorStore) Close() error {
  494. return vs.client.Close()
  495. }
  496. // SearchResult 搜索结果
  497. type SearchResult struct {
  498. DocumentID string
  499. KnowledgeBaseID string
  500. Content string
  501. Score float32
  502. }