ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

关键词匹配优化:从小时级到分钟级的工程实战

关键词匹配优化:从小时级到分钟级的工程实战 先说下项目的背景。我接手这套匹配系统的时候线上关键词量级已经跑到百万级每天要拿这些关键词去跟海量文本数据做交集判断。当时单批任务跑完基本要一个多小时遇到晚高峰数据积压直接奔着三小时去。业务方天天催领导给的指标很简单粗暴单个批处理任务必须压到 15 分钟以内最好能到个位数分钟。这种优化任务最忌讳上来就改代码。我先把整个数据链路的耗时拆了一轮结论很清晰瓶颈不在磁盘也不在数据库纯粹是匹配算法太原始——那时候线上用的是最朴素的“拿每个关键词对每段文本跑一遍 contains”一百万关键词对十万条文本算下来就是千亿次字符串扫描不慢才怪。这篇文章就完整复盘一下我是怎么一步步把匹配任务从小时级干到分钟级的涉及数据结构选型、并行化改造、内存控制和一些工程层面的取舍。如果你也在折腾关键词匹配、内容过滤、敏感词扫描这类场景这篇应该能直接帮上忙。1. 先搞清楚慢在哪儿盲目优化是大忌任何性能优化项目第一步都不是选什么高级算法而是把现有系统的耗时构成摸清楚。这个道理听起来简单但很多人一上来就急着换数据结构、上并发结果折腾半天收益甚微就是因为没对准真正的瓶颈。1.1 需求场景还原我这边核心流程是这样的每天会收到一批需要打标的文本数据同时有一份不断更新的关键词库。关键词库里的词分了好几类有品牌词、品类词、竞品词、敏感词等等总数在一百二十万左右。匹配任务要做的事情是把每条文本里命中的关键词全部找出来按优先级打好标签输出到下游。从数据形态上看是标准的“多关键词 - 多文本”的笛卡尔积匹配问题。一百万关键词十万条文本最坏情况下要做 10^11 次子串比较。如果用单纯的 contains 判断单次比较的复杂度是 O(m*n)m 是文本长度n 是关键词长度这还不算字符串函数本身的常数开销。数据量一大耗时立刻爆炸。1.2 先量化再动手我没有凭感觉猜而是写了个简单的压测脚本抽取线上真实数据的一小部分做了基准测试。测试环境跟生产一致32 核 CPU机器内存 64G。样本是 1 万条文本、10 万关键词用 Python 的朴遍历写法跑了一遍耗时接近 40 秒。线性外推到 10 万条文本和 120 万关键词的线上规模单线程大概就是 8000 秒即 2.2 小时左右。这里有个很重要的分析思路优化之前一定要记录每个环节的耗时占比。我把任务拆成了四段数据加载读文本、读词库、预处理匹配计算结果聚合结果写回实测下来匹配计算占了 94% 以上的耗时其他环节加起来还不到 6%。所以这轮优化的核心战场非常清晰把匹配计算的复杂度降下来其他环节顺手优化一下就行。1.3 为什么 contains 方案撑不住contains 方案的致命问题在于它没有利用“关键词之间的共同前缀”或者“文本只需要扫描一遍”这些信息。每次 contains 都是一次独立的字符串搜索哪怕两个关键词有同样的前缀前面的搜索结果也无法复用。这种模式下计算量随关键词数量线性增长是完全没有优化空间的。另外还有一个隐藏问题CPU 缓存命中率极低。循环里反复调用字符串匹配函数文本内容基本都在内存里随机访问加上 Python 这类动态语言的对象开销实际效率比 C 实现还要再打一个折扣。所以做优化的时候我不仅仅是换算法连实现语言和数据结构一起做了调整。2. 方案选型我为什么选了 Aho-Corasick 自动机这一节聊聊数据结构的选型过程。很多人在关键词匹配场景先想到的是 Trie 树前缀树也有人会想用正则表达式的拼接方式。我最终选的是 Aho-Corasick 自动机简称 AC 自动机但这不是唯一答案选型一定要结合自己的数据特征。2.1 Trie 树解决了什么问题Trie 树的核心价值是共享前缀。比如关键词库里有“苹果”、“苹果手机”、“苹果电脑”三个词普通方案要存三份“苹果”的匹配逻辑但 Trie 树只需要存储一份公共前缀路径后面的分支各自展开。Trie 树的匹配过程就是把文本从头到尾过一遍顺着树的边往下走。走到某个节点发现它恰好是一个关键词的结尾就说明命中了。这个过程是 O(文本长度) 的建树查询复杂度不再跟关键词数量挂钩。但是裸的 Trie 树有一个致命缺陷当文本中出现“匹配失败”的情况时它需要回溯到文本的下一个字符重新从根节点开始匹配。这意味着在匹配失败率高的场景下文本会被反复扫描实际效率依然不理想。比如文本是“三星手机”而词库里只有“苹果”、“华为”那每次从根节点走过去又走不通只能回退效率就下来了。2.2 AC 自动机在 Trie 树上加了失败指针AC 自动机做的事情很聪明它在构建 Trie 的基础上给每个节点增加了一条fail 指针失败指针。当当前路径匹配失败时不回溯文本而是跳到 fail 指针指向的节点继续尝试匹配。这个 fail 指针指向的是“当前已匹配字符串的最长真后缀对应的节点”也就是说它能保留下已经扫过部分的有效信息。举个例子文本扫到“abcdef”当前匹配到了“abcde”下一个字符不匹配。这时候不需要回退到“bcdef”重新从根开始而是跳到某个能匹配“bcde”后缀的位置继续走。这种机制保证了整个文本只需要扫描一遍匹配复杂度稳定在 O(文本长度 关键词总数)跟关键词数量基本解耦。AC 自动机还有另一个巨大优势它天然支持“多关键词同时匹配”。你不需要循环遍历关键词库文本扫一遍所有可能命中的词全都能在一个流程里被检出。这一点直接打破了原来 O(关键词数量 * 文本长度) 的乘法关系降到了加法关系。2.3 选型时我考虑的对比项我自己做选型的时候画过一张对比表贴在这里供参考方案匹配复杂度构建复杂度内存占用主要缺点暴力 containsO(K * L)O(1)极小关键词量一大就崩正则拼接O(L * 表达式复杂度)O(K)小长正则回溯多、易灾难裸 TrieO(L * 失败回溯)O(total_len)中等失败场景退化AC 自动机O(L K 命中数)O(total_len)较大构建细节繁琐表中的 K 是关键词数量L 是文本总长度。从这张表能清楚看到为啥 AC 自动机在大关键词量场景下几乎是唯一合理的选择。正则拼接方案在关键词只有几十个的时候挺好用构建简单也不需要额外的数据结构但关键词一多正则引擎的回溯复杂度是不可控的特殊字符还要做转义很容易踩坑。我后来在实际压测里也验证了同样十万关键词AC 自动机的匹配速度比正则拼接快至少一个数量级。3. 第一步优化把匹配算法换成 AC 自动机选型定了之后就是实际的编码和落地。AC 自动机的实现细节比想象中多尤其是构建阶段的层序处理、失败指针的生成顺序、以及中文关键词的特殊处理。我踩过不少坑下面把这些细节一次性讲清楚。3.1 AC 自动机的构建过程构建 AC 自动机分两步第一步建 Trie 树第二步补 fail 指针。建 Trie 树没什么特别就是把每个关键词拆成字符序列按照公共前缀合并路径在结尾节点标记关键词 ID 和优先级。这里我直接用了数组模拟指针的方式避免 Python 对象引用带来的内存膨胀。每个节点就存三个字段子节点字典、失败指针、输出列表在当前节点结束的关键词 ID 列表。第二步是 BFS 构建 fail 指针。根节点的所有直接子节点fail 指针都指向根节点。其他节点的 fail 指针要从它父节点的 fail 指针开始逐层找有没有对应的子节点。如果找到就把当前节点的 fail 指过去找不到就继续往上跳直到根节点为止。这里有个一辈子不会忘的教训构建 fail 指针必须按层序遍历不能递归不然后面深层次节点的 fail 会指向还没构建好的节点。代码核心逻辑长这样Python 版本方便演示from collections import deque class ACNode: __slots__ (next, fail, output, depth) def __init__(self): self.next {} self.fail None self.output [] # 存储终止于当前节点的关键词ID self.depth 0 def build_ac(keywords): root ACNode() # 1. 建 Trie for kid, kw in enumerate(keywords): cur root for ch in kw: if ch not in cur.next: cur.next[ch] ACNode() cur cur.next[ch] cur.depth cur.depth or 0 cur.output.append(kid) # 2. BFS 构建 fail 指针 q deque() for node in root.next.values(): node.fail root q.append(node) while q: cur q.popleft() for ch, nxt in cur.next.items(): q.append(nxt) f cur.fail while f is not None and ch not in f.next: f f.fail nxt.fail f.next[ch] if f and ch in f.next else root # 把 fail 节点的输出合并过来保证后缀命中也能检出 if nxt.fail: nxt.output.extend(nxt.fail.output) return root注意最后一步把 fail 指针指向节点的 output 列表合并到当前节点的 output。这样才能保证匹配到“苹果手机”的时候同时也能检出“手机”这个后缀关键词。不合并的话你会漏掉很多后缀型命中结果。3.2 匹配过程的细节优化构建完 AC 自动机匹配过程就简单了。逐字符读取文本不断往下走走不通就跳 fail 指针。每走到一个节点把该节点的 output 列表全部取出记录命中。整段代码不超过 30 行def match_text(root, text): cur root hits [] for i, ch in enumerate(text): while cur is not root and ch not in cur.next: cur cur.fail if ch in cur.next: cur cur.next[ch] else: continue for kid in cur.output: hits.append((i, kid)) return hits这里的核心优化点在于当字符走不通时直接用 while 循环跳 fail而不是递归回退。循环跳 fail 的操作均摊下来是 O(1)因为每个字符最多跳一轮 depth 深度的 fail而 AC 自动机保证了总跳转次数是线性的。实测中这个函数即便用 Python 写单线程也能跑到每秒几百万字符的处理速度。3.3 中文关键词的特殊处理这个项目里的关键词绝大多数是中文。AC 自动机理论上天然支持 unicode 字符但我想提醒几个容易被忽略的点是否要做分词如果关键词都是完整短语比如“苹果手机”直接按字符建 Trie 没问题。但如果关键词里有单字比如“苹”、“华”匹配时会跟其他更长的关键词产生重叠命中需要业务层决定是取最长匹配还是全量命中。我们是全量命中后按优先级聚合所以没做额外限制。英文大小写中文没有大小写问题但词库里混了品牌英文词比如“iPhone”。我在预处理阶段统一把文本和关键词都转成小写避免因为大小写导致漏匹配。这一步必须在构建之前完成不然自动机里存的就是大小写两套路径内存直接翻倍。全半角符号中文文本里经常混着全角逗号、全角括号。关键词里要是带符号建树前必须做统一归一化否则“苹果官方”和“苹果(官方)”会被当成两个不同的词。我写了个简单的映射表把所有全角字符转半角。3.4 换算法后的第一轮收益用 AC 自动机替换掉原来的 contains 方案后我先在 1 万文本、10 万关键词的样本上做了验证。耗时从原来的 40 秒降到了 0.8 秒接近 50 倍提升。线性外推到 10 万文本、120 万关键词的规模单线程约 80 秒左右。这个数字已经大幅超过预期但距离 15 分钟的目标还有距离。别急瓶颈不再单纯是匹配算法了。接下来是并行化和工程层面的调优目标是把 80 秒再往下压一个量级。4. 第二步优化并行化与分片策略单机 32 核只用一个线程跑这是巨大的浪费。AC 自动机的匹配过程本身是无状态的天然适合并行。难点在于怎么分片——分片分得不好要么负载不均要么结果合并的时候麻烦。4.1 文本分片还是关键词分片两种思路都能并行按文本分片每台线程处理一批文本共享同一份 AC 自动机。这种方案适合文本条数远大于 CPU 核数的场景均摊效果好。我们最终用的就是这种。按关键词分片关键词库拆成多份每份建一个 AC 自动机然后每个线程处理全量文本。这种方案问题很大每份关键词都要构建独立的自动机内存翻好几倍而且每条文本都要被多个线程各扫一遍总扫描量翻倍。除非关键词量小到能拆分否则不推荐。选文本分片还有一个隐性好处自动机只需要构建一次构建好的对象在多个线程间共享即可不会产生重复构建的开销。Python 的多线程受 GIL 限制所以我这里直接用了多进程。如果你们是 Java 或者 C 环境多线程就行。4.2 任务切分与进程池我用的是 multiprocessing 的进程池配合一个简单的 chunk 切分函数import multiprocessing as mp def worker(args): text_list, auto args local_hits [] for text in text_list: hits match_text(auto, text) local_hits.append((text_id, hits)) return local_hits def parallel_match(texts, auto, num_workers32): chunk_size max(1, len(texts) // num_workers) chunks [texts[i:ichunk_size] for i in range(0, len(texts), chunk_size)] with mp.Pool(processesnum_workers) as pool: results pool.map(worker, [(c, auto) for c in chunks]) # 汇总 merged [] for r in results: merged.extend(r) return merged这里有个经验不要逐条文本提交给进程池。进程间通信、任务队列、上下文切换的开销是很大的任务粒度太细反而比单线程更慢。把文本切成与 CPU 核数相同数量级的 chunk让每个进程处理一整块通信开销降到最低。实测下来32 进程跑 10 万条文本耗时从 80 秒降到 5 秒左右扩展倍数接近线性。4.3 并行时的内存控制进程池的一个副作用是每个进程都会复制一份 AC 自动机对象。如果直接传递自动机作为参数Python 的进程池默认用 pickle 序列化然后复制到子进程。百万关键词的自动机序列化一次要好几十秒内存占用直接乘以进程数。解决方案是让子进程在 fork 之后继承父进程的内存而不是通过参数传递。Unix 系统下 multiprocessing 默认使用 fork子进程会继承父进程已构建好的自动机对象不需要重新序列化。实现上有一个关键点在进入进程池之前先构建好全局自动机让子进程 fork 时自动继承。我写了这样一个结构auto None # 全局变量fork后子进程自动继承 def init_worker(auto_obj): global auto auto auto_obj if __name__ __main__: auto build_ac(all_keywords) pool mp.Pool(processes32, initializerinit_worker, initargs(auto,))注意一段踩坑经历在 macOS 上 multiprocessing 默认使用 spawn 而不是 fork子进程不会自动继承内存必须手动传 auto。为了兼顾跨平台我在代码里做了判断if sys.platform darwin: results pool.map(worker, [(chunk, auto) for chunk in chunks]) else: results pool.map(worker, [(chunk, auto) for chunk in chunks])严格说 macOS 下直接传 auto 也没有致命问题就是每次提交任务都会多一次序列化开销。生产环境跑在 Linux 上用 fork 模式收益更大。4.4 共赢线程池方案Java 侧参考项目主体虽然是 Python但调研阶段我在 Java 侧也做了原型验证。Java 里做法更优雅直接把 AC 自动机构建为不可变对象使用 ExecutorService 做固定线程池ExecutorService executor Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); ListFutureListHit futures texts.stream() .map(text - executor.submit(new MatchTask(ac, text))) .collect(Collectors.toList());Java 的共享对象在只读场景下没有并发问题多线程读同一个 AC 自动机实例是完全安全的。注意不要在匹配过程中修改自动机的任何字段否则会出现诡异的数据竞争。我们的一些子任务为了省内存直接把匹配结果记录在节点上这种写法在多线程下就是灾难。5. 第三步优化从算法到工程细节决定最终收益AC 自动机加并行化之后耗时就真的从小时级到了分钟级。但这不代表优化工作结束了。工程层面还有很多可以挤压的空间数据加载、结果合并、对象复用、甚至进程的启动开销。每一步看似不起眼叠在一起又能把耗时砍掉一截。5.1 数据加载与预处理提速前面提到匹配耗时占 94%但那是相对原始方案。换用 AC 自动机后匹配耗时的绝对占比降下来了数据加载和预处理占比反而升高。观察线上任务后我发现加载 10 万条文本、解析 CSV、做全半角转换、小写化这些步骤单线程跑也要十几秒。虽然不算瓶颈但既然目标是压总时长这些也得并行化。我的做法是把数据解析也放进进程池里每块文本直接解析完再进入匹配阶段。核心思想是边加载边匹配不再等所有数据都读进内存才开始处理而是按行流式读取每读满一个 chunk 就丢给进程池。另一个容易忽略的点是IO 缓冲区大小。默认的 readline 方法在磁盘 IO 频繁时效率一般。我测试了不同缓冲区大小最终选了 1MB 的缓冲加载速度提升了 20% 左右。代码很简单with open(file_path, r, buffering1024*1024, encodingutf-8) as f: for line in f: # 处理 pass如果你处理的是 GB 级文件建议直接用上 memory-mapped 文件进一步减少用户态和内核态的拷贝次数。不过我们这批任务的数据集规模还没到那个量级暂时没上 mmap。5.2 结果聚合与去重的细节AC 自动机匹配出来的是“文本位置 - 关键词ID”的扁平列表规模是命中次数级别的可能几百万到几千万条。聚合阶段要把这些数据按文本组织好并根据优先级排序去重。这个环节如果写得不好也会拖慢整体任务。我的做法是避免构建大型中间字典。在 worker 进程内部直接使用 Python 的字典局部聚合再把聚合后的结果传给合并阶段。这样合并阶段只需要做字典合并不需要处理海量原始命中记录。合并逻辑如下def merge_results(partial_results): final defaultdict(list) for text_id, hits in partial_results: final[text_id].extend(hits) return final其实这里还能做一层优化既然每个 worker 处理的是固定文本块可以让 worker 直接输出“文本ID - 去重排序后的关键词列表”这样最终合并阶段连排序都不用做只需要把多个 worker 的结果按文本 ID 拼接起来。实测这步改完聚合阶段耗时降到了秒级。5.3 构建面向增量更新的自动机关键词库不是一成不变的业务方每天都会新增关键词。如果每次更新都要全量重建 AC 自动机百万关键词的构建时间大概是 20 秒左右对于每日更新来说完全能接受。但有一种情况很烦关键词库中途更新已经跑了一半的任务怎么办。我的做法是双缓冲机制当前任务使用旧自动机继续跑新关键词到来时在后台线程构建新的自动机等新自动机构建完成再做一次原子切换。这样既不会停顿当前任务也不会丢失新增关键词。实现核心就是加锁的引用替换auto_ref {auto: old_auto} def rebuild(new_kws): new_auto build_ac(new_kws) auto_ref[auto] new_auto def current(): return auto_ref[auto]你的进程池 worker 在每次处理新 chunk 前重新读取current()这样就能平滑完成版本切换。这个思路来自线上系统平滑升级的做法在这个关键词匹配场景里同样适用。5.4 把匹配结果落库的优化优化做完了还有个经常被忽视的坑结果写回数据库的速度。我们的下游是 MySQL每条文本对应多条标签单批任务要写入百万行记录。一开始逐条 INSERT写入耗时快赶上匹配耗时了。后来改成批量插入每次 500 条用一条带多条 VALUES 的 INSERT 语句提交写入速度提升了一个量级。如果是更高吞吐的场景可以考虑 CSV 批量导入或者走消息队列异步落库。这段虽然不算“关键词匹配”本身但既然整个链路的耗时目标是分钟级任何环节的瓶颈都会拉高总时长一个负责任的优化项目必须端到端看全局。6. 常见问题与排查实录优化过程中踩的坑不少有些问题光看报错信息根本定位不到。我挑了几个典型问题写成速查表包括问题原因和排查方法希望帮大家少走弯路。6.1 匹配结果莫名遗漏这是最让我头疼的一个问题。换了 AC 自动机之后样例测试一切正常但一跑线上数据就发现某些长文本里的关键词没被检出来。排查思路先检查关键词本身是否带了隐藏字符比如全角空格、零宽字符。文本里的“苹果”和词库里的“苹果”肉眼看起来一样但编码不同直接匹配不上。用repr()把字符串打出来看最容易发现这类问题。再检查 fail 指针构建是否正确。我最初写递归版本时深度大的节点 fail 指向了尚未构建完成的节点导致部分后缀命中丢失。解决方式就是上文提到的层序遍历。还要检查 output 合并逻辑。如果漏了nxt.output.extend(nxt.fail.output)以“手机”结尾的短关键词在匹配“苹果手机”时不会被检出。这是最经典的遗漏原因。6.2 自动机构建时间过长有一版暴力实现中每插入一个关键词都要从根节点重新遍历一遍路径导致 120 万关键词的构建耗时到了 5 分钟。这显然不能接受。优化方案是在插入时维护一个当前节点的游标不要每次都从根开始。另外构建 fail 指针时用的是 BFS 队列这是线性复杂度不会成为瓶颈。如果构建时间还是太慢可以考虑换用更紧凑的数据结构用有序数组二分查找代替字典或者直接上编译型语言C/Rust。对于纯 Python 项目可以用array模块存 child 索引牺牲部分可读性换内存和速度。6.3 内存占用飙升百万关键词的 AC 自动机如果用 Python 字典存子节点节点数量是关键词总字符数的量级粗算一下要 3-5GB 内存。这在 64G 机器上还能承受但如果部署到小内存容器就麻烦了。内存优化方向有三个用__slots__限定类的实例属性省掉__dict__的内存开销子节点不要用普通 dict用dict但有大量短字符串时考虑换成trie的连续存储结构比如双数组 Trie如果关键词中有大量长尾词可以设置“只保留关键词结尾标记”的模式不对每个字符都存完整信息。双数组 Trie 的实现复杂度高一些但内存能从几 GB 降到几百 MB适合部署资源紧张的环境。6.4 SQL 层面的配套问题这个项目蹭了一个热搜词“慢SQL优化”因为关键词匹配结果的写入确实踩了慢 SQL 的坑。除了批量插入之外还有一个索引设计问题结果表按文本ID建了唯一索引批量插入时遇到主键冲突会直接回滚。后来我改成INSERT ... ON DUPLICATE KEY UPDATE重复文本的标签更新变得很平滑没有中断整个批任务。如果你的写入表有多个索引批量插入时 MySQL 的索引维护开销也不小。建议把结果表建成分区表按日期或批次分区避免单表过大导致的索引膨胀和锁竞争。7. 最终效果与复盘不卖关子先说最终结果。优化前单批任务耗时约 8000 秒优化分三步走换用 AC 自动机后压到 800 秒左右并行化后压到 60 秒左右工程层面数据加载、结果聚合、批量写入再压到 25 秒。整体提速超过 300 倍完全满足“分钟级”的目标。这个结果其实不是单一优化点的功劳而是“算法选型 并行化 IO 优化”三个层面叠加的效果。很多优化项目之所以失败是因为只做了一步就停了没有把整条链路上的瓶颈逐一消除。AC 自动机解决的是“匹配速度”并行化解决的是“CPU 利用率”IO 优化解决的是“数据搬运”三者缺一不可。7.1 关键收益复盘优化阶段主要操作耗时变化提速倍数原始方案暴力 contains8000 秒1x换 AC 自动机多模式匹配框架800 秒10x并行化32 进程文本分片60 秒13x工程优化加载、聚合、写库25 秒2.4x数据很直白最大头收益来自算法替换把指数级的笛卡尔积比较变成了线性扫描并行化在算法基础上线性扩展工程优化则是压死骆驼的最后一根稻草把剩余零零散散的耗时全部收敛。7.2 一点复盘心得我在实际项目中体会最深的一点是性能优化不是一次性的动作而是一个持续监控的过程。自动机上线后我还是会定期跑基准测试用线上真实数据作为输入样本看耗时有没有劣化。关键词库规模从 120 万涨到 150 万、200 万构建时间和匹配时间都可能有小幅变化但算法本身不会有数量级上的退化。只要数据量没有指数级增长AC 自动机这个方案能撑很久。另外我把整个匹配模块做了封装对外暴露接口很简单输入文本关键词库输出命中标签。这样业务方不需要关心底层是 AC 自动机还是别的方案后续如果换用 Rust 或者 C 的实现接口保持不变对上游完全透明。7.3 后续还能往哪些方向延展这套方案目前的扩展方向我列一下供有类似需求的朋友参考流式匹配如果文本是实时到达的可以考虑把 AC 自动机放进流处理框架里每条消息到达时直接做匹配不用等批处理。Automatons 本身是无状态的非常适合部署在 Kafka Streams 或者 Flink 里。多级关键词分层百万级关键词先粗筛再用小型自动机细筛能进一步降低内存占用。比如先用几千个高频词粗筛掉 90% 的文本剩下的 10% 再走全量匹配。这个思路在处理超大关键词库时尤其管用。支持命中的上下文提取现在匹配结果只有关键词 ID如果业务需要展示上下文片段可以在匹配过程中记录命中位置前后若干字符拼一个高亮摘要。这部分我已经在自己的分支里做了原型效率影响不大。最后再说一个实际工程里的注意点上线前一定要做灰度验证。我拿线上历史数据跑了回归测试对比新旧方案命中结果是否一致确保 AC 自动机没有引入漏报和误报。这一步不能省算法替换最怕的就是结果对不上。这个项目做完以后我对“优化”这事的态度变了不少——真正的优化不是找到某个银弹算法而是把合适的技术用在合适的位置再把工程细节打磨到位。如果你正面临着类似的性能瓶颈不妨也把当前方案的耗时构成先拆出来看看往往最糙的地方就是最容易暴利的地方。这一套打下来你的任务也能从小时级干到分钟级。
返回列表