使用sfsDb高并发创建大量文件全文索引
在现代信息时代,面对海量的文档和文件资源,快速、高效地构建全文索引是搜索引擎和文档管理系统的核心需求。本文将详细介绍如何使用sfsDb数据库结合Go语言的高并发特性,实现对大量文件的全文索引创建。
概述
本文将通过ReSearch项目中的TraversePathAndReadFiles函数,展示如何:
- 使用sfsDb作为底层数据库存储索引数据
- 利用Go语言的goroutine池实现高并发文件处理
- 支持中文文本的句子分割与索引
- 实现幂等性设计,支持重复安全运行
sfsDb简介
sfsDb是一个轻量级的Go语言数据库引擎,提供了以下核心功能:
- 表结构管理与数据存储
- 多种索引类型(主键索引、普通索引、全文索引)
- 高效的查询与搜索功能
- 内存优化的索引匹配缓存
ReSearch中的sfsDb表结构
ReSearch项目使用两张核心表:
1. dir 表(目录表)
存储文件和目录的元数据信息:
id- 目录/文件的唯一标识符(主键)name- 目录/文件名称url- 完整路径ext- 文件扩展名
2. senc 表(句子表,含全文索引)
存储文档的句子内容,支持全文搜索:
did- 目录ID(外键,关联 dir 表的 id)secNo- 句子序号(在文档中的顺序)content- 句子内容(全文索引字段)
主键: did + secNo 组合主键
核心实现
1. 初始化与准备
在开始索引之前,需要初始化goroutine池和sfsDb相关资源:
// 全局Pool实例
var globalPool *pool.Pool
func init() {
// 初始化全局Pool,数据库插入属于IO密集型任务
globalPool = pool.NewPoolForIO()
currentTime = time.Now().Format("2006-01-02 15:04:05")
}
2. 目录信息管理
使用sfsDb管理目录信息,确保数据一致性:
// 检测目录是否存在于sfsDb中
dirurliter, _ := db.Tables["dir"].Search(&map[string]any{
"url": path,
}, util.Equal)
defer dirurliter.Release()
if dirurliter.Exist() {
// 目录已存在,获取现有ID
record := dirurliter.GetRecords(true, 1).Select("id")
defer record.Release()
if record[0]["id"] != nil {
currentID = record[0]["id"].(int)
}
} else {
// 目录不存在,插入新记录到sfsDb
currentID, err = db.Tables["dir"].Insert(&map[string]any{
"name": info.Name(),
"url": path,
"ext": ext,
})
}
3. 高并发文件处理
使用goroutine池实现高并发文件内容处理,这是处理大量文件的关键:
// 捕获外部变量,避免闭包陷阱
localCurrentID := currentID
localContent := content
localName := name
// 提交任务到goroutine池
globalPool.Submit(func() {
defer wg.Done()
if err := AddArticle(localCurrentID, localContent); err != nil {
fmt.Printf("插入文章 %s 失败: %v\n", localName, err)
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
})
高并发关键技术:
sync.WaitGroup:等待所有并发任务完成sync.Mutex:保护共享资源的并发访问- goroutine池:控制并发数量,避免资源耗尽
- 变量捕获:确保每个任务获得正确的参数
4. sfsDb全文索引创建
4.1 全文索引表结构
在ReSearch中,senc表的全文索引创建方式如下:
func CreateTable_Senc() (*engine.Table, error) {
// 创建表
table, err := engine.TableNew("senc")
if err != nil {
return nil, err
}
// 定义字段
fields := map[string]any{
"did": 0, // 目录ID(整数)
"secNo": 0, // 句子序号(整数)
"content": "", // 句子内容(字符串)
}
table.SetFields(fields)
// 创建组合主键(did + secNo)
primaryKey, err := engine.DefaultPrimaryKeyNew("pk")
primaryKey.AddFields("did", "secNo")
table.CreateIndex(primaryKey)
// 创建全文索引(content 字段)
FullTextIndex, err := engine.DefaultFullTextIndexNew("ft")
// ⚠️ 重要:全文索引必须包含主键字段,否则关键词会被覆盖
FullTextIndex.AddFields("content", "did", "secNo")
// 指定 content 为全文索引字段,索引长度为 5
err = FullTextIndex.SetFullField("content", 5)
table.CreateIndex(FullTextIndex)
return table, nil
}
全文索引关键要点:
- 必须包含主键字段(did和secNo),否则关键词会被覆盖
- 索引长度设为5,平衡查询效率和存储空间
- 支持字符级全文搜索,无需分词
4.2 文本内容处理与全文索引
将文件内容分割成句子并存储到sfsDb,构建全文索引:
const spstr = "。!??!;;" // 支持中文标点符号
func AddArticle(did int, content string) error {
// 保留原始换行符
content = strings.ReplaceAll(content, "\n", currentTime+"\n")
// 在标点符号后添加换行符,实现句子分割
for _, sep := range spstr {
content = strings.ReplaceAll(content, string(sep), string(sep)+"\n")
}
parts := strings.Split(content, "\n")
secNo := 0
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
continue
}
// 恢复原始换行符
trimmed = strings.ReplaceAll(trimmed, currentTime, "\n")
secNo++
// 插入句子到sfsDb,构建全文索引
err := addSentence(did, secNo, trimmed)
if err != nil {
return err
}
}
return nil
}
5. 数据存储到sfsDb
将处理后的句子数据插入sfsDb:
func addSentence(did, secNo int, content string) error {
id, err := db.Tables["senc"].Insert(&map[string]any{
"did": did, // 文章目录ID
"secNo": secNo, // 文章句子序号
"content": content, // 包含分隔符的完整句子
})
if err != nil {
fmt.Printf("插入句子失败: %v, did: %v, secNo: %v\n", err, did, secNo)
return err
}
if id == -1 {
return fmt.Errorf("插入句子失败: 插入ID为 -1")
}
return nil
}
完整执行流程
1. 初始化阶段
├─ 初始化goroutine池
├─ 初始化统计结构
└─ 准备sfsDb表连接
2. 遍历阶段
└─ 对每个文件/目录:
├─ 在sfsDb中检查是否已存在
├─ 不存在则插入新记录
├─ 文件读取内容并预处理
└─ 提交到goroutine池处理
3. 并发处理阶段
└─ goroutine池执行:
├─ 文本分割为句子
├─ 句子内容处理
└─ 批量插入sfsDb
4. 等待与完成
├─ 等待所有并发任务完成
└─ 返回统计结果
关键技术解析
1. 幂等性设计
为了支持重复安全运行,采用先查询再插入的策略:
// 先检查记录是否存在
iter, _ := db.Tables["dir"].Search(&map[string]any{
"url": path,
}, util.Equal)
if iter.Exist() {
// 已存在,跳过插入
} else {
// 不存在,插入新记录
}
2. 资源管理
使用defer确保sfsDb迭代器等资源被正确释放:
dirurliter, _ := db.Tables["dir"].Search(...)
defer dirurliter.Release()
3. 中文文本处理
专门针对中文文本优化的句子分割:
- 支持中文标点符号(
。!??!;;) - 保留原始换行符
- 智能过滤空白内容
使用示例
基础使用
import "ReSearch/files"
// 对指定目录创建全文索引
fileTypes, unsupportedFiles, err := files.TraversePathAndReadFiles("/path/to/documents")
if err != nil {
log.Fatalf("索引创建失败: %v", err)
}
fmt.Printf("处理完成!\n")
fmt.Printf("文件类型统计: %v\n", fileTypes)
fmt.Printf("不支持的文件: %v\n", unsupportedFiles)
与Web服务集成
func IndexHandler(c *gin.Context) {
path := c.PostForm("path")
// 异步创建索引
go func() {
fileTypes, unsupportedFiles, err := files.TraversePathAndReadFiles(path)
// 处理结果...
}()
c.JSON(200, gin.H{"status": "started"})
}
性能优化建议
1. 调整goroutine池大小
根据硬件配置调整并发数:
// CPU密集型任务
globalPool = pool.NewPoolWithSize(runtime.NumCPU())
// IO密集型任务
globalPool = pool.NewPoolWithSize(runtime.NumCPU() * 4)
2. 批量插入优化
对于大量句子,考虑实现批量插入:
// 收集多个句子后批量插入
// 减少sfsDb操作次数
3. 内存使用优化
对于大文件,采用流式处理:
// 使用bufio.Scanner逐行读取
// 避免一次性加载大文件到内存
4. 索引缓存利用
sfsDb提供了索引匹配缓存,可以监控其使用情况:
indexStats := engine.GetIndexMatchCacheStats()
fmt.Printf("缓存命中率: %.2f%%\n", indexStats.HitRate*100)
常见问题
Q: 如何避免重复索引同一文件?
A: 函数设计为幂等的,会先在sfsDb中检查记录是否存在,已存在则跳过。
Q: 支持哪些文件类型?
A: 支持文本文件、代码文件、以及通过ReadFileContent可读取的文档格式。无法读取的文件会被统计。
Q: 索引大量文件时内存占用过高怎么办?
A:
- 减小goroutine池大小
- 实现文件内容流式处理
- 及时释放不再需要的资源
Q: 如何提高索引速度?
A:
- 调整goroutine池大小
- 使用SSD存储提升IO性能
- 实现批量插入机制
- 分批处理超大目录
Q: sfsDb的全文索引如何查询?
A: 可以通过sfsDb的全文索引API进行查询,支持关键词搜索、模糊匹配等功能。
全文索引搜索使用
基础全文搜索
// 全文搜索关键词
searchKeyword := "搜索内容"
// 使用全文索引搜索
iter := db.Tables["senc"].Search(&map[string]any{
"content": searchKeyword,
})
defer iter.Release()
// 获取搜索结果
records := iter.GetRecords(true, 100).Select("did", "secNo", "content")
// 处理搜索结果
for _, record := range records {
did := record["did"].(int)
secNo := record["secNo"].(int)
content := record["content"].(string)
fmt.Printf("找到句子: [文档%d-句子%d] %s\n", did, secNo, content)
}
关联查询文件信息
通过did关联查询dir表,获取完整的文件信息:
// 1. 先搜索句子
iter := db.Tables["senc"].Search(&map[string]any{
"content": searchKeyword,
})
defer iter.Release()
records := iter.GetRecords(true, 100).Select("did", "secNo", "content")
// 2. 对每个结果,查询对应的文件信息
for _, record := range records {
did := record["did"].(int)
// 查询dir表获取文件信息
dirIter := db.Tables["dir"].Search(&map[string]any{
"id": did,
}, util.Equal)
defer dirIter.Release()
if dirIter.Exist() {
dirRecords := dirIter.GetRecords(true, 1).Select("name", "url")
if len(dirRecords) > 0 {
fileName := dirRecords[0]["name"].(string)
filePath := dirRecords[0]["url"].(string)
content := record["content"].(string)
fmt.Printf("文件: %s (%s)\n句子: %s\n\n", fileName, filePath, content)
}
}
}
按文件名搜索
利用内容前缀的<文件名>标签,可以按文件名搜索:
// 搜索包含特定文件名的内容
searchKeyword := "<文档名称"
iter := db.Tables["senc"].Search(&map[string]any{
"content": searchKeyword,
})
defer iter.Release()
// 这样可以找到文件名包含"文档名称"的所有相关句子
总结
使用sfsDb结合Go语言的高并发特性,可以高效地为大量文件创建全文索引。本文介绍的方案具有以下优势:
✅ 高并发处理:利用goroutine池充分利用多核CPU
✅ sfsDb集成:轻量级数据库,支持全文索引
✅ 中文优化:专门针对中文文本的句子分割
✅ 幂等设计:支持重复安全运行
✅ 资源管理:完善的资源释放机制
✅ 错误处理:健壮的并发错误收集
通过合理配置和优化,这个方案可以处理数万甚至更多文件的索引创建任务,为构建搜索引擎和文档管理系统提供坚实的基础。