Java 多字符串同时匹配文本,消耗 CPU 过高,如何优化?

93 天前
 print1024

本人遇到一个性能方法的问题,这是打标的场景,主要逻辑是三个循环,YYDTO 中有段文本,XXDTO 中有关键词列表,XXDTOList 量级大约 10 万条必须执行,判断关键词列表是否全部在文本中存在,如果存在则执行业务逻辑; 目前测试了 stream 、parallelStream 、正则和原生的 for 循环,发现下面 checkExistRule 是最快的,大约 50ms ,但是会一直消耗大量 CPU (串行下一直占用 100%)。之前还考虑使用 AC 自动机,但是因为单条匹配到还要处理业务逻辑,所以不太合适。

text 举例 :
"#DIESEL 大牌好友# @宋雨琦_G-I-DLE 演绎#DIESEL2023 秋冬系列# 牛仔坠饰 D-VINA 包袋。渐变丹宁渲染不羁格调,另类包型注解无畏想象"


keywordRule 中举例:
[鼎赛龙, 男士春夏, D-FINING, 深灰色, 锥形牛仔裤] string[]
[DIESEL, 男士春夏, DFINING, 深灰色, 锥形牛仔裤] string[]
上面这是关键词列表中的一个,总共 10 万个,也就是 10 万个 List 中每个 List 包含 N  个字符串数组

请问有什么方式进行优化?能不能做到时间复杂度 O(1) 或者 O(m + n)级别?


    for (YYDTO yydto : YYDTOList) { //2000
        String text = yydto.getText();
        for (XXDTO xxdto : XXDTOList) {//10w
          List<String[]> keywordRule = xxdto.getKeywordRule();
            if (checkExistRule(keywordRule, text)) {
                // 处理业务逻辑
                // yydto.set(xxdto.getName());
            }
        }
    }

    private boolean checkExistRule(List<String[]> keywordRule, String text) {
        try {
            for (String[] strings : keywordRule) {
                for (String string : strings) {
                    if (!text.contains(string)) {
                        return false;
                    }
                }
                return true;
            }
        } catch (Exception e) {
            
        }
        return false;
    }
    
2792 次点击
所在节点    Java
30 条回复
liprais
93 天前
你想的太简单了
建议再想想
比如你这 10w 敏感词必须一个一个判断么?
有没有可能搞成前缀树?
BiChengfei
93 天前
试试 parallelStream ?
感觉现在还可以啊,不算慢,资源消耗也还可以,关键实现简单,好维护
print1024
93 天前
@liprais 这是一个打标的场景,如果关键词全部包含则打上这个标签,考虑过建树但是匹配完如何知道是哪个关键词规则命中呢
print1024
93 天前
@BiChengfei parallelStream CPU 测试消耗比较大,我们线上 CPU 就 2 核,直接就打满了
xtreme1
93 天前
ac + double array trie
alva0
93 天前
改下设计?先将所有关键字去重,放在一个有序集合中,XXDTOList 中的关键字列表存下标,每次 YYDTO 先判断关键字集合,取出包含的所有下标,再与 XXDTOList 比较下标是否存在?这样子试下呢
zizon
93 天前
YYDTOList.steram().flatmap((yydto)=>{
XXDTOList.stream().flatmap(::getKeywordRule()::stream()).flatmap((keyword)=>{
return Map.Entry(keyword,yydto)
})
}).filter((entry)=>{
return entry.values.contains(keyword)
}).foreach()

如果不改数据结构/算法本身的话.
zizon
93 天前
@zizon 哦,没有 early terminate.忽略.
corningsun
93 天前
首先推测 checkExistRule(List<String[]> keywordRule, String text) 的实现有问题,OP 的实现只会用到第一条规则。
其次 String 的 contains 匹配是数组下标查找,也是一层循环,效率不高。
建议是先分词,得到 哈希数组,然后再比较,示例代码如下:


public static boolean checkExistRuleV2(List<Set<String>> keywordRule, Set<String> textSet) {
return keywordRule.stream().anyMatch(textSet::containsAll);
}


Benchmark 压测结果如下:循环 10000 次的时间,从 13 ms 降低到 0.5 ms

Benchmark Mode Cnt Score Error Units
Print1024Test.checkExistRule avgt 6 13.877 ± 0.148 ms/op
Print1024Test.checkExistRuleV2 avgt 6 0.528 ± 0.035 ms/op

单元测试代码

package org.corning.v2ex.year2024;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.junit.jupiter.api.Test;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.TimeValue;

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class Print1024Test {

public static List<String[]> keywordRule = Lists.newArrayList(
new String[]{"鼎赛龙", "男士春夏", "D-FINING", "深灰色", "锥形牛仔裤"},
new String[]{"DIESEL", "男士春夏", "DFINING", "深灰色", "锥形牛仔裤"}
);
public static String text = "#DIESEL 大牌好友# @宋雨琦_G-I-DLE 演绎#DIESEL2023 秋冬系列# 牛仔坠饰 D-VINA 包袋。渐变丹宁渲染不羁格调,另类包型注解无畏想象";
public static List<Set<String>> keywordRuleSet = keywordRule.stream()
.map(Sets::newHashSet)
.collect(Collectors.toList());

// 这里可能需要更好的分词手法,trim / 去掉逗号等
public static Set<String> textSet = Arrays.stream(text.split(" ")).collect(Collectors.toSet());

@Test
public void runBenchmarks() throws Exception {
Options options = new OptionsBuilder()
.include(this.getClass().getName() + ".*")
.mode(Mode.AverageTime)
.warmupTime(TimeValue.seconds(1))
.warmupIterations(6)
.threads(1)
.measurementIterations(6)
.forks(1)
.shouldFailOnError(true)
.shouldDoGC(true)
.build();
new Runner(options).run();
}


@Benchmark
@Test
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void checkExistRule() {
for (int i = 0; i < 10000; i++) {
Print1024.checkExistRule(keywordRule, text);
}
}

@Benchmark
@Test
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void checkExistRuleV2() {
for (int i = 0; i < 10000; i++) {
Print1024.checkExistRuleV2(keywordRuleSet, textSet);
}
}

}
GuuJiang
93 天前
AC 自动机几乎是多模式匹配的不二解了,你说的“但是因为单条匹配到还要处理业务逻辑”是什么意思,不妨展开讲讲,看到底是什么原因导致了不适用 AC 自动机
reeco
93 天前
线上 CPU 就 2 核,啥优化都没用
soupu626
93 天前
理论上这个关键词的命中率应该极低,可以先筛一波,比如关键词建个前缀 map ,这个前缀取多长看你们关键词的区分度,然后先用输入文本过一遍这个 map ,筛出来可能命中的关键词小集合,然后再来全量的遍历

简单脑测了下,前面那个筛过的复杂度应该是 (N-L)*log(M) N 是输入字符串长度,M 是前缀 Map 大小 L 是前缀长度,这样筛出来的小集合理论上应该很小,再过一次 N*S 就行 S 是小集合大小
print1024
93 天前
@GuuJiang 因为这是一个打标的场景,A 标签规则是一组关键词,B 标签也是一组关键词,如果文本完全命中这些关键词的话怎么区分是 A 标签命中还是 B 标签命中呢
GuuJiang
93 天前
@print1024 这个完全不是问题啊,首先 AC 自动机命中时本来就知道是哪一个关键词命中的,再额外维护一下从关键词到标签的反向关联不就行了,更简单的是在构造 AC 自动机的过程中直接把标签信息冗余到节点上
print1024
93 天前
@GuuJiang 这个反向关联关系不太好维护,因为多个关键词同时包含才能确定一个标签,拿到了命中的关键词后,去循环查找哪个标签下的关键词组合能够匹配上?那相当于还是要从头到尾遍历一次 XXDTOList 啊
print1024
93 天前
@corningsun 感谢,checkExistRule 是有问题的,我目前采用了你这种方式,线上 YYDTO 每条耗时差不多 150ms ,因为每次都要过 10w 条 XXDTO
print1024
93 天前
@soupu626 这是一个不错的思路,前缀 map 能细讲一下吗
JKeita
93 天前
trie 树不就行了?
JKeita
93 天前
trie 数+bitmap ,用 trie 数匹配到的所有关键字生成一个匹配结果的 bitmap ,然后再根据每个标签的关键字独有的 bitmap 一个个比对,成的话就执行相关策略
yicixin
93 天前
不知道说的对不对,
1.首先看了下示例代码,一个 rule 是一个二维字符串数组,一段文本匹配该 rule 的条件是该文本中出现了 rule 中所有的关键词,既然如此,一个 rule 是否可以简化成一个一维字符串数组,即把二维数组中所有的关键词去重后放入一个一维数组,这样问题可以稍微简化一下。

2.接着,换个思路,把 10w 个 rule 中的所有关键词去重后构造前缀树,用 text 去进行匹配这个 10wKeyTrie ,拿到该 text 匹配到的所有关键词,将匹配到的所有关键词排序后拼接,最终得到一个字符串,例如 text 匹配了关键词 深灰色、锥形牛仔裤和 DIESEL ,假设排序拼接后是 DIESEL 深灰色锥形牛仔裤,记这个最终字符串为 matchText

3.上文说了把一个 rule 简化成一个一维字符串数组,那么每个 rule 也可以进行类似的排序拼接,那么最终 10w 个 rule 就是 10w 个字符串,把这 10w 个字符串构造前缀树,叫 10wRuleTrie 吧,最后用 2.中最后得到的 matchText 放进这个 10wRuleTrie 里进行匹配,如果成功匹配上即说明满足该条 rule 。

其中的 10wKeyTrie 和 10wRuleTrie 都是可以在初始化的时候就可以构造好,每次循环内要做的就是对进行 text 与 10wKeyTrie 的匹配-->得到多个关键词,排序后拼接--> 与 10wRuleTrie 进行匹配

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1014344

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX