使用sfsDb高并发创建大量文件全文索引

5 阅读8分钟

使用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:

  1. 减小goroutine池大小
  2. 实现文件内容流式处理
  3. 及时释放不再需要的资源

Q: 如何提高索引速度?

A:

  1. 调整goroutine池大小
  2. 使用SSD存储提升IO性能
  3. 实现批量插入机制
  4. 分批处理超大目录

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集成:轻量级数据库,支持全文索引
✅ 中文优化:专门针对中文文本的句子分割
✅ 幂等设计:支持重复安全运行
✅ 资源管理:完善的资源释放机制
✅ 错误处理:健壮的并发错误收集

通过合理配置和优化,这个方案可以处理数万甚至更多文件的索引创建任务,为构建搜索引擎和文档管理系统提供坚实的基础。