chunk_service.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log"
  7. "strings"
  8. "unicode/utf8"
  9. "github.com/2930134478/AI-CS/backend/models"
  10. "github.com/2930134478/AI-CS/backend/repository"
  11. "github.com/2930134478/AI-CS/backend/service/rag"
  12. )
  13. // ChunkService 文档分段服务
  14. type ChunkService struct {
  15. docRepo *repository.DocumentRepository
  16. kbRepo *repository.KnowledgeBaseRepository
  17. chunkRepo *repository.DocumentChunkRepository
  18. embeddingSvc *rag.DocumentEmbeddingService
  19. vectorStore *rag.VectorStoreService
  20. }
  21. // NewChunkService 创建分段服务实例
  22. func NewChunkService(
  23. docRepo *repository.DocumentRepository,
  24. kbRepo *repository.KnowledgeBaseRepository,
  25. chunkRepo *repository.DocumentChunkRepository,
  26. embeddingSvc *rag.DocumentEmbeddingService,
  27. vectorStore *rag.VectorStoreService,
  28. ) *ChunkService {
  29. return &ChunkService{
  30. docRepo: docRepo,
  31. kbRepo: kbRepo,
  32. chunkRepo: chunkRepo,
  33. embeddingSvc: embeddingSvc,
  34. vectorStore: vectorStore,
  35. }
  36. }
  37. // ChunkRequest 分段请求参数
  38. type ChunkRequest struct {
  39. Method string `json:"method"` // "char_count" | "separator"
  40. ChunkSize int `json:"chunk_size,omitempty"` // 按字数时的每段字数
  41. Separator string `json:"separator,omitempty"` // 按分隔符时的分隔符
  42. }
  43. // ChunkByCharCount 按字数分段(不重叠)
  44. func ChunkByCharCount(text string, chunkSize int) []string {
  45. if chunkSize <= 0 {
  46. chunkSize = 500
  47. }
  48. runes := []rune(text)
  49. if len(runes) <= chunkSize {
  50. return []string{text}
  51. }
  52. var chunks []string
  53. for i := 0; i < len(runes); i += chunkSize {
  54. end := i + chunkSize
  55. if end > len(runes) {
  56. end = len(runes)
  57. }
  58. chunks = append(chunks, string(runes[i:end]))
  59. }
  60. return chunks
  61. }
  62. // ChunkBySeparator 按分隔符分段
  63. func ChunkBySeparator(text string, sep string) []string {
  64. if sep == "" {
  65. return []string{text}
  66. }
  67. parts := strings.Split(text, sep)
  68. var chunks []string
  69. for _, part := range parts {
  70. trimmed := strings.TrimSpace(part)
  71. if trimmed != "" {
  72. chunks = append(chunks, trimmed)
  73. }
  74. }
  75. if len(chunks) == 0 {
  76. return []string{text}
  77. }
  78. return chunks
  79. }
  80. // ExecuteChunking 执行分段:切分文本 → 写入 MySQL → 删除旧 Milvus 向量 → 逐段向量化写入 Milvus
  81. func (s *ChunkService) ExecuteChunking(ctx context.Context, documentID uint, req ChunkRequest) ([]models.DocumentChunk, error) {
  82. doc, err := s.docRepo.GetByID(documentID)
  83. if err != nil {
  84. return nil, fmt.Errorf("文档不存在: %w", err)
  85. }
  86. if strings.TrimSpace(doc.Content) == "" {
  87. return nil, errors.New("文档内容为空,无法分段")
  88. }
  89. var chunkTexts []string
  90. switch req.Method {
  91. case "char_count":
  92. size := req.ChunkSize
  93. if size <= 0 {
  94. size = 500
  95. }
  96. chunkTexts = ChunkByCharCount(doc.Content, size)
  97. case "separator":
  98. if req.Separator == "" {
  99. return nil, errors.New("分隔符不能为空")
  100. }
  101. chunkTexts = ChunkBySeparator(doc.Content, req.Separator)
  102. default:
  103. return nil, fmt.Errorf("不支持的分段方式: %s(支持 char_count 或 separator)", req.Method)
  104. }
  105. if len(chunkTexts) == 0 {
  106. return nil, errors.New("分段结果为空")
  107. }
  108. if err := s.deleteExistingChunks(ctx, documentID); err != nil {
  109. return nil, fmt.Errorf("删除旧分段失败: %w", err)
  110. }
  111. chunks := make([]*models.DocumentChunk, len(chunkTexts))
  112. for i, text := range chunkTexts {
  113. chunks[i] = &models.DocumentChunk{
  114. DocumentID: documentID,
  115. KnowledgeBaseID: doc.KnowledgeBaseID,
  116. ChunkIndex: i,
  117. Content: text,
  118. EmbeddingStatus: "pending",
  119. }
  120. }
  121. if err := s.chunkRepo.BatchCreate(chunks); err != nil {
  122. return nil, fmt.Errorf("保存分段失败: %w", err)
  123. }
  124. go s.embedChunks(documentID, doc.KnowledgeBaseID, chunks)
  125. result := make([]models.DocumentChunk, len(chunks))
  126. for i, c := range chunks {
  127. result[i] = *c
  128. }
  129. return result, nil
  130. }
  131. // GetChunks 获取文档的分段列表
  132. func (s *ChunkService) GetChunks(documentID uint, page, pageSize int) ([]models.DocumentChunk, int64, error) {
  133. if _, err := s.docRepo.GetByID(documentID); err != nil {
  134. return nil, 0, fmt.Errorf("文档不存在: %w", err)
  135. }
  136. if page < 1 {
  137. page = 1
  138. }
  139. if pageSize < 1 || pageSize > 100 {
  140. pageSize = 20
  141. }
  142. offset := (page - 1) * pageSize
  143. return s.chunkRepo.GetByDocumentIDPaginated(documentID, offset, pageSize)
  144. }
  145. // UpdateChunk 更新单个分段内容,仅重新向量化该段
  146. func (s *ChunkService) UpdateChunk(ctx context.Context, chunkID uint, content string) (*models.DocumentChunk, error) {
  147. chunk, err := s.chunkRepo.GetByID(chunkID)
  148. if err != nil {
  149. return nil, fmt.Errorf("分段不存在: %w", err)
  150. }
  151. if strings.TrimSpace(content) == "" {
  152. return nil, errors.New("分段内容不能为空")
  153. }
  154. chunk.Content = content
  155. chunk.EmbeddingStatus = "pending"
  156. if err := s.chunkRepo.Update(chunk); err != nil {
  157. return nil, fmt.Errorf("更新分段失败: %w", err)
  158. }
  159. go s.reEmbedSingleChunk(chunk)
  160. return chunk, nil
  161. }
  162. // reEmbedSingleChunk 仅重新向量化单个分段
  163. func (s *ChunkService) reEmbedSingleChunk(chunk *models.DocumentChunk) {
  164. ctx := context.Background()
  165. svc, err := s.embeddingSvc.GetEmbeddingService(ctx)
  166. if err != nil {
  167. log.Printf("[分段] 获取嵌入服务失败 (chunk=%d): %v", chunk.ID, err)
  168. _ = s.chunkRepo.UpdateEmbeddingStatus(chunk.ID, "failed")
  169. return
  170. }
  171. vectors, err := svc.EmbedTexts(ctx, []string{chunk.Content})
  172. if err != nil || len(vectors) == 0 {
  173. log.Printf("[分段] 单段向量化失败 (chunk=%d): %v", chunk.ID, err)
  174. _ = s.chunkRepo.UpdateEmbeddingStatus(chunk.ID, "failed")
  175. return
  176. }
  177. chunkIDStr := rag.ConvertDocumentID(chunk.ID)
  178. _ = s.vectorStore.DeleteVectorByChunkID(ctx, chunkIDStr)
  179. docIDStr := rag.ConvertDocumentID(chunk.DocumentID)
  180. kbIDStr := rag.ConvertKnowledgeBaseID(chunk.KnowledgeBaseID)
  181. if err := s.vectorStore.UpsertVector(ctx, docIDStr, kbIDStr, chunk.Content, chunkIDStr, vectors[0]); err != nil {
  182. log.Printf("[分段] 单段向量写入失败 (chunk=%d): %v", chunk.ID, err)
  183. _ = s.chunkRepo.UpdateEmbeddingStatus(chunk.ID, "failed")
  184. return
  185. }
  186. _ = s.chunkRepo.UpdateEmbeddingStatus(chunk.ID, "completed")
  187. log.Printf("[分段] 单段向量化完成 (chunk=%d, 长度=%d)", chunk.ID, len([]rune(chunk.Content)))
  188. }
  189. // DeleteChunks 删除文档的所有分段(MySQL + Milvus)
  190. func (s *ChunkService) DeleteChunks(ctx context.Context, documentID uint) error {
  191. return s.deleteExistingChunks(ctx, documentID)
  192. }
  193. // deleteExistingChunks 删除文档的旧分段(MySQL + Milvus)
  194. func (s *ChunkService) deleteExistingChunks(ctx context.Context, documentID uint) error {
  195. if err := s.chunkRepo.DeleteByDocumentID(documentID); err != nil {
  196. return err
  197. }
  198. docIDStr := rag.ConvertDocumentID(documentID)
  199. if err := s.vectorStore.DeleteVector(ctx, docIDStr); err != nil {
  200. log.Printf("[分段] 删除旧向量失败(可能本就没有向量): %v", err)
  201. }
  202. return nil
  203. }
  204. // embedChunks 批量向量化所有分段并写入 Milvus(异步执行)
  205. func (s *ChunkService) embedChunks(documentID uint, knowledgeBaseID uint, chunks []*models.DocumentChunk) {
  206. ctx := context.Background()
  207. s.embedChunkList(ctx, documentID, knowledgeBaseID, chunks)
  208. }
  209. // embedChunkList 批量向量化分段列表
  210. func (s *ChunkService) embedChunkList(ctx context.Context, documentID uint, knowledgeBaseID uint, chunks []*models.DocumentChunk) {
  211. for _, c := range chunks {
  212. _ = s.chunkRepo.UpdateEmbeddingStatus(c.ID, "processing")
  213. }
  214. contents := make([]string, len(chunks))
  215. for i, c := range chunks {
  216. contents[i] = c.Content
  217. }
  218. docIDs := make([]uint, len(chunks))
  219. kbIDs := make([]uint, len(chunks))
  220. chunkIDs := make([]string, len(chunks))
  221. for i, c := range chunks {
  222. docIDs[i] = documentID
  223. kbIDs[i] = knowledgeBaseID
  224. chunkIDs[i] = rag.ConvertDocumentID(c.ID)
  225. }
  226. if err := s.embeddingSvc.EmbedDocuments(ctx, docIDs, kbIDs, contents, chunkIDs); err != nil {
  227. log.Printf("[分段] 批量向量化失败 (doc=%d): %v", documentID, err)
  228. for _, c := range chunks {
  229. _ = s.chunkRepo.UpdateEmbeddingStatus(c.ID, "failed")
  230. }
  231. return
  232. }
  233. for _, c := range chunks {
  234. _ = s.chunkRepo.UpdateEmbeddingStatus(c.ID, "completed")
  235. }
  236. log.Printf("[分段] 批量向量化完成 (doc=%d, chunks=%d)", documentID, len(chunks))
  237. }
  238. // ensure utf8 package is used
  239. var _ = utf8.RuneCountInString