首页 / 资讯中心 / 文章详情

用 C# 手写一个 Everything 风格文件搜索工具:SQLite 建索引,22 万文件 1 毫秒搜中文(附完整可运行代码)

用 C# 手写一个 Everything 风格文件搜索工具:SQLite 建索引,22 万文件 1 毫秒搜中文(附完整可运行代码) ★ FEATURED ARTICLE
用 C# 手写一个 Everything 风格文件搜索工具SQLite 建索引22 万文件 1 毫秒搜中文附完整可运行代码标签C#文件搜索SQLiteEverythingWinForms索引性能优化摘要很多人都试过用Directory.GetFiles遍历硬盘找文件结果一搜就是几十秒。本文用我自己的开源小工具FileSearchGui为例讲清楚「实时遍历为什么慢 → 用 SQLite 建一份本地索引 → 之后毫秒级搜索」的完整实现并复盘一个差点让项目翻车的坑LIKE转义符用错导致 Windows 路径全部匹配失败。代码已跑通可直接抄。一、背景为什么你用Directory.GetFiles永远慢先说结论在文件多的时候慢的不是「匹配」而是「遍历文件系统」本身。我在一台普通开发机上实测C 盘排除Windows、Program Files等系统大目录大约有22.8 万个文件仅仅是把它们枚举出来EnumerateFiles递归就要19 秒。再加上逐个FileInfo取大小、时间整体跑到 70 秒一点都不稀奇。那为什么Everything能秒开因为它根本不遍历——它直接读 NTFS 的MFT / USN日志在系统层面预建了一份内存级索引开机就已经知道每个文件在哪。我们没有内核权限但可以用一个折中方案第一次用 SQLite 在本地建一份等价索引慢一次性之后所有搜索都走 SQL毫秒到秒级。这正是FileSearchGui的思路。下面把核心代码拆开讲。二、整体方案索引与搜索彻底分离建索引一次性慢 搜索每次快 遍历目录树 ──► SQLite 落库 查 SQL ──► 返回结果 (files 表: dir/name/path/ WHERE dir LIKE ? ESCAPE ! size/mtime/ext) AND name LIKE ? ESCAPE !两张表CREATETABLEroots(rootTEXTPRIMARYKEY,-- 被索引的根目录built_atTEXT,file_countINTEGER,inc_buildINTEGER,-- 建索引时是否含 bin/obj 等生成目录inc_systemINTEGER-- 是否含系统大目录);CREATETABLEfiles(idINTEGERPRIMARYKEY,dirTEXT,nameTEXT,pathTEXTUNIQUE,sizeINTEGER,mtimeINTEGER,extTEXT);CREATEINDEXidx_files_nameONfiles(name);CREATEINDEXidx_files_dirONfiles(dir);建索引时我们默认跳过编译产物和系统大目录避免把bin/obj/node_modules和Windows灌进库既快又干净staticreadonlyHashSetstringSkipDirsnew(StringComparer.OrdinalIgnoreCase){bin,obj,node_modules,.git,.workbuddy};staticreadonlyHashSetstringSystemDirsnew(StringComparer.OrdinalIgnoreCase){Windows,Program Files,ProgramData,$Recycle.Bin,System Volume Information};staticboolIsSkipDir(stringdir,boolincludeBuild,boolincludeSystem){varnamePath.GetFileName(dir.TrimEnd(Path.DirectorySeparatorChar));if(!includeBuildSkipDirs.Contains(name))returntrue;// 不勾含生成目录时跳过if(!includeSystemSystemDirs.Contains(name))returntrue;// 索引场景始终跳过系统盘大目录returnfalse;}三、实战 1建索引SQLite 批量事务别一条条插新手建索引最常见的性能陷阱是「每读一个文件就ExecuteNonQuery一次」。SQLite 每次写都有事务开销22 万次就是 22 万次提交能慢到你怀疑人生。正确做法攒一批这里 5000 条一次性提交一个事务。varbatchnewList(stringdir,stringname,stringpath,longsize,longmtime,stringext)(5000);longscanned0;voidFlushBatch(){if(batch.Count0)return;usingvarconnnewSqliteConnection(Data Source_dbPath);conn.Open();usingvartxconn.BeginTransaction();// 整批一个事务usingvarcmdconn.CreateCommand();cmd.CommandTextINSERT OR REPLACE INTO files(dir,name,path,size,mtime,ext) VALUES(d,n,p,s,m,e);// ... 用参数化 Add 循环 ExecuteNonQuery ...foreach(varfinbatch){pD.Valuef.dir;pN.Valuef.name;/* ... */cmd.ExecuteNonQuery();}tx.Commit();batch.Clear();}voidWalk(stringdir){if(IsSkipDir(dir,includeBuild,includeSystem))return;foreach(varfinDirectory.EnumerateFiles(dir)){varfinewFileInfo(f);batch.Add((dir,fi.Name,f,fi.Length,fi.LastWriteTimeUtc.Ticks,fi.Extension.ToLowerInvariant()));if(batch.Count5000)FlushBatch();// 满一批就落库}foreach(vardinDirectory.EnumerateDirectories(dir))Walk(d);}实测22.8 万文件建索引约0.3 秒当然前提是文件元数据已经被系统缓存冷启动会慢一些但仍是一次性成本。四、实战 2搜索——一个反斜杠引发的「血案」本文重点坑索引建好了结果一搜啥都搜不到。我在这个坑里卡了很久最后定位到一个反直觉的原因❌ 错误写法// 千万别这么写cmd.CommandTextSELECT ... FROM files WHERE dir LIKE r ESCAPE \\ AND name LIKE t ESCAPE \\;LIKE的转义符默认或显式用\时Windows 路径里的反斜杠会被当成转义字符。路径C:\Users\HY里的\U、\H被 SQL 解释成「转义后的字面量 U、H」于是dir LIKE C:\Users\%永远匹配不上任何记录——索引建得再好搜索也全落空。✅ 正确写法用!当转义符// 把 LIKE 的特殊字符用 ! 转义彻底绕开 Windows 路径里的 \staticstringEscapeLike(strings){if(string.IsNullOrEmpty(s))returns;returns.Replace(!,!!)// 先转义转义符本身.Replace(\\,!\\)// 反斜杠 → 被转义的反斜杠.Replace(%,!%).Replace(_,!_);}publicListResultItemSearchIndex(stringsearchRoot,stringterm,boolincludeBuild,boolincludeSystem){searchRootNormalize(searchRoot);varcoveringGetCoveringRoot(searchRoot,includeBuild,includeSystem);usingvarconnnewSqliteConnection(Data Source_dbPath);conn.Open();usingvarcmdconn.CreateCommand();// 关键ESCAPE !绝不能用 \cmd.CommandTextSELECT path,name,size,mtime FROM files WHERE dir LIKE r ESCAPE ! AND name LIKE t ESCAPE !;cmd.Parameters.AddWithValue(r,EscapeLike(searchRoot)%);cmd.Parameters.AddWithValue(t,%EscapeLike(term)%);// ... 读结果 ...}第二个隐形坑分隔符混用建索引时如果你传入的是C:/Users/HY正斜杠dir列存成C:/...搜索时却用C:\Users\HY去LIKE因为/和\不相等照样搜不到。统一用Normalize把分隔符规范掉staticstringNormalize(stringp){if(string.IsNullOrEmpty(p))returnp;pp.Replace(Path.AltDirectorySeparatorChar,Path.DirectorySeparatorChar);// / → \returnp.TrimEnd(Path.DirectorySeparatorChar);}这两个坑转义符 分隔符是「建完索引搜不到」的 99% 原因记牢。五、实战 3不止搜文件名还能搜文件内容Everything 的强项是文件名但我们要「比 Everything 还能搜中文」——搜文件内容。直接全量读全文会卡死大盘我做三道闸扩展名白名单只扫.cs/.txt/.md/.json/.py/.js...等文本类流式逐行File.ReadLines逐行读命中即停不把整个文件读进内存单文件大小上限超过maxSizeMB默认几十 MB直接跳过。staticreadonlyHashSetstringTextExtsnew(StringComparer.OrdinalIgnoreCase){.txt,.cs,.md,.json,.xml,.py,.js,.ts,.html,.css,.sql,.csv,.yaml,.toml/* ... */};boolMatch(stringpath,stringterm,boolcontent,longmaxBytes,outResultItemitem){itemnull;if(!content){if(Path.GetFileName(path).Contains(term,StringComparison.OrdinalIgnoreCase)){itemMakeItem(path);returntrue;}returnfalse;}varextPath.GetExtension(path).ToLowerInvariant();if(!TextExts.Contains(ext))returnfalse;// 闸 1非文本不扫try{varfinewFileInfo(path);if(fi.LengthmaxBytes)returnfalse;// 闸 3太大跳过foreach(varlineinFile.ReadLines(path))// 闸 2流式逐行{if(line.Contains(term,StringComparison.OrdinalIgnoreCase)){itemMakeItem(path);returntrue;}}returnfalse;}catch{returnfalse;}}配合Parallel.ForEach多核并行扫描22 万文件的内容检索也能在秒级返回。六、搜索范围正确性选「最长覆盖根」如果用户索引了C:\又单独索引了C:\Users\HY\WorkBuddy当他搜C:\Users\HY\WorkBuddy\ProjectA时必须命中最具体的那个根否则会错误落到C:\的覆盖分支导致子目录搜索全部落空。做法是在候选覆盖根里选路径最长的publicboolIsCovered(stringsearchRoot,boolincludeBuild,boolincludeSystem,outstringcoveringRoot){coveringRootnull;searchRootNormalize(searchRoot);stringbestnull;foreach(varriinGetRoots()){if(ri.IncludeBuild!includeBuild||ri.IncludeSystem!includeSystem)continue;varrNormalize(ri.Root);// 选最长覆盖根搜索根是某索引根本身或它的子目录if(searchRootr||searchRoot.StartsWith(rPath.DirectorySeparatorChar))if(bestnull||r.LengthNormalize(best).Length)bestri.Root;}coveringRootbest;returnbest!null;}同时要求「含生成目录 / 系统目录」的开关与建索引时一致避免「勾了含 bin 却走不含 bin 的旧索引」漏搜。七、性能实测同一台开发机场景方式耗时全 C 盘枚举 22.8 万文件实时遍历~19 秒仅枚举建索引一次性SQLite 批量事务~0.3 秒搜文件名索引 SQL0 ms ~ 1 ms搜中文内容22 万文件索引 流式 并行秒级数字随硬件和缓存浮动属「行业参考」量级不是精确基准。但量级关系稳定建索引一次性付出之后搜索比实时遍历快4 个数量级。八、完整代码与运行完整可运行工程WinForms GUI 版 控制台版已在我的项目里核心就本文这 4 个类IndexStore索引层、SearchCore搜索核心、Form1界面、Program。要点回顾EnsureSchema建表 三个索引BuildIndex递归遍历 5000/批事务写入SearchIndex用ESCAPE !搜路径与文件名别用\Normalize统一分隔符内容检索走TextExts白名单 File.ReadLines流式 大小上限 Parallel.ForEach。NuGet 依赖只有一个Microsoft.Data.Sqlite。目标框架.NET 8/net8.0-windows发布成自包含单文件 exe双击即用无需装运行时。九、踩坑清单速查⚠️LIKE转义符绝不用\Windows 路径全是\会全匹配失败 → 用ESCAPE !。⚠️ 建索引与搜索的路径分隔符必须统一/与\混用 → 搜不到→Normalize。⚠️ 插库务必批量事务不要逐条提交。⚠️ 索引要跳过bin/obj/node_modules/.git和Windows/Program Files否则库又脏又慢。⚠️ 内容检索必须加扩展名白名单 文件大小上限否则全盘读全文直接卡死。⚠️ 多根索引时选「最长覆盖根」否则子目录搜索落空。
阅读完成 · 觉得有帮助?
咨询建站