fix(knowledge): 将 category 存储到 fault_source 字段

## 问题

数据库表 api_document 没有独立的 category 字段,导致 frontmatter 的 category 信息无法正确存储。

### 表结构分析

```sql
CREATE TABLE api_document (
    fault_category VARCHAR(32) DEFAULT 'EXTERNAL_API',  -- 固定枚举,不合适存储自定义分类
    fault_source VARCHAR(128),                           -- 可以存储自定义分类
    ...
)
```

## 解决方案

使用 `fault_source` 字段存储 frontmatter 的 category:

```java
// 保存时
document.setFaultSource(category);  // api, infrastructure, domain, troubleshooting

// 统计时
Map<String, Long> categoryCount = apiDocumentRepository.findAll().stream()
    .collect(Collectors.groupingBy(
        doc -> doc.getFaultSource() != null ? doc.getFaultSource() : "general",
        Collectors.counting()
    ));
```

## 字段映射关系

| Frontmatter | 数据库字段 | 示例值 |
|-------------|-----------|--------|
| `title` | `api_name` | "支付网关错误码定义" |
| `category` | `fault_source` | "api" / "infrastructure" |
| `keywords` | `metadata` (JSON) | ["ERR_TIMEOUT","超时"] |
| `summary` | `metadata` (JSON) | "记录了..." |

## 优势

1. **充分利用现有字段**:fault_source (VARCHAR 128) 足够存储分类
2. **避免枚举限制**:不受 FaultCategory 枚举约束
3. **查询方便**:直接通过 fault_source 字段查询和统计
4. **向后兼容**:metadata 中仍保留完整的 frontmatter 信息

## 验证

```bash
# 初始化
curl -X POST http://localhost:9900/api/knowledge/init

# 查询统计
curl http://localhost:9900/api/knowledge/stats

# 响应
{
  "categories": {
    "api": 1,
    "infrastructure": 3,
    "domain": 1,
    "troubleshooting": 1
  }
}
```

## 数据库查询

```sql
-- 按分类统计
SELECT fault_source, COUNT(*)
FROM api_document
GROUP BY fault_source;

-- 结果
api             | 1
infrastructure  | 3
domain          | 1
troubleshooting | 1
```
This commit is contained in:
zhuyongxin
2026-06-25 14:08:50 +08:00
parent b01f133efb
commit 4e3502a51b
@@ -19,6 +19,7 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Collectors;
/**
* 知识库初始化服务
@@ -193,12 +194,12 @@ public class KnowledgeBaseInitService {
int indexSize = knowledgeIndexService.getIndexSize();
logger.debug("L0 索引大小: {}", indexSize);
// 按分类统计(从 metadata JSON 中提取 category)
Map<String, Long> categoryCount = new HashMap<>();
apiDocumentRepository.findAll().forEach(doc -> {
String category = extractCategoryFromMetadata(doc.getMetadata());
categoryCount.merge(category, 1L, Long::sum);
});
// 按分类统计(从 fault_source 字段读取)
Map<String, Long> categoryCount = apiDocumentRepository.findAll().stream()
.collect(Collectors.groupingBy(
doc -> doc.getFaultSource() != null ? doc.getFaultSource() : "general",
Collectors.counting()
));
stats.setCategoryCount(categoryCount);
// Milvus 中的向量数量(需要实现)
@@ -250,10 +251,9 @@ public class KnowledgeBaseInitService {
document.setApiName(title); // 使用 title 作为 apiName
document.setStatus("PENDING"); // 初始状态为 PENDING,索引成功后更新为 INDEXED
// 尝试映射 category 到 FaultCategory(如果匹配的话)
// 否则保持默认值 EXTERNAL_API
// 注意:frontmatter 的 category(api, infrastructure, domain)与 FaultCategory 枚举不完全匹配
// 真实的分类信息保存在 metadata 中
// 直接使用 fault_source 字段存储 category(api, infrastructure, domain 等)
// fault_category 字段名虽然不太贴切,但可以用来存储文档分类
document.setFaultSource(category);
// 将 frontmatter 信息保存到 metadata(JSON 格式)
String metadataJson = String.format(
@@ -283,34 +283,6 @@ public class KnowledgeBaseInitService {
.replace("\r", "\\r");
}
/**
* 从 metadata JSON 中提取 category
*/
private String extractCategoryFromMetadata(String metadata) {
if (metadata == null || metadata.isEmpty()) {
return "general";
}
try {
// 简单的 JSON 解析(提取 "category":"xxx")
int categoryIndex = metadata.indexOf("\"category\":\"");
if (categoryIndex == -1) {
return "general";
}
int startIndex = categoryIndex + "\"category\":\"".length();
int endIndex = metadata.indexOf("\"", startIndex);
if (endIndex == -1) {
return "general";
}
return metadata.substring(startIndex, endIndex);
} catch (Exception e) {
logger.warn("解析 metadata 失败: {}", metadata, e);
return "general";
}
}
/**
* 提取文档正文(去除 frontmatter)
*/