V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
print1024
V2EX  ›  Java

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

  •  2
     
  •   print1024 · 2024-02-05 11:21:57 +08:00 · 3234 次点击
    这是一个创建于 369 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本人遇到一个性能方法的问题,这是打标的场景,主要逻辑是三个循环,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;
        }
        
    
    30 条回复    2024-02-06 14:20:03 +08:00
    liprais
        1
    liprais  
       2024-02-05 11:27:39 +08:00
    你想的太简单了
    建议再想想
    比如你这 10w 敏感词必须一个一个判断么?
    有没有可能搞成前缀树?
    BiChengfei
        2
    BiChengfei  
       2024-02-05 11:34:28 +08:00
    试试 parallelStream ?
    感觉现在还可以啊,不算慢,资源消耗也还可以,关键实现简单,好维护
    print1024
        3
    print1024  
    OP
       2024-02-05 11:35:05 +08:00
    @liprais 这是一个打标的场景,如果关键词全部包含则打上这个标签,考虑过建树但是匹配完如何知道是哪个关键词规则命中呢
    print1024
        4
    print1024  
    OP
       2024-02-05 11:38:29 +08:00
    @BiChengfei parallelStream CPU 测试消耗比较大,我们线上 CPU 就 2 核,直接就打满了
    xtreme1
        5
    xtreme1  
       2024-02-05 11:39:05 +08:00
    ac + double array trie
    alva0
        6
    alva0  
       2024-02-05 11:52:51 +08:00
    改下设计?先将所有关键字去重,放在一个有序集合中,XXDTOList 中的关键字列表存下标,每次 YYDTO 先判断关键字集合,取出包含的所有下标,再与 XXDTOList 比较下标是否存在?这样子试下呢
    zizon
        7
    zizon  
       2024-02-05 11:52:55 +08:00
    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
        8
    zizon  
       2024-02-05 11:54:18 +08:00
    @zizon 哦,没有 early terminate.忽略.
    corningsun
        9
    corningsun  
       2024-02-05 14:27:41 +08:00   ❤️ 1
    首先推测 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
        10
    GuuJiang  
       2024-02-05 14:33:41 +08:00 via iPhone
    AC 自动机几乎是多模式匹配的不二解了,你说的“但是因为单条匹配到还要处理业务逻辑”是什么意思,不妨展开讲讲,看到底是什么原因导致了不适用 AC 自动机
    reeco
        11
    reeco  
       2024-02-05 14:44:31 +08:00
    线上 CPU 就 2 核,啥优化都没用
    soupu626
        12
    soupu626  
       2024-02-05 15:28:36 +08:00
    理论上这个关键词的命中率应该极低,可以先筛一波,比如关键词建个前缀 map ,这个前缀取多长看你们关键词的区分度,然后先用输入文本过一遍这个 map ,筛出来可能命中的关键词小集合,然后再来全量的遍历

    简单脑测了下,前面那个筛过的复杂度应该是 (N-L)*log(M) N 是输入字符串长度,M 是前缀 Map 大小 L 是前缀长度,这样筛出来的小集合理论上应该很小,再过一次 N*S 就行 S 是小集合大小
    print1024
        13
    print1024  
    OP
       2024-02-05 15:29:42 +08:00
    @GuuJiang 因为这是一个打标的场景,A 标签规则是一组关键词,B 标签也是一组关键词,如果文本完全命中这些关键词的话怎么区分是 A 标签命中还是 B 标签命中呢
    GuuJiang
        14
    GuuJiang  
       2024-02-05 15:35:45 +08:00 via iPhone   ❤️ 1
    @print1024 这个完全不是问题啊,首先 AC 自动机命中时本来就知道是哪一个关键词命中的,再额外维护一下从关键词到标签的反向关联不就行了,更简单的是在构造 AC 自动机的过程中直接把标签信息冗余到节点上
    print1024
        15
    print1024  
    OP
       2024-02-05 16:14:16 +08:00
    @GuuJiang 这个反向关联关系不太好维护,因为多个关键词同时包含才能确定一个标签,拿到了命中的关键词后,去循环查找哪个标签下的关键词组合能够匹配上?那相当于还是要从头到尾遍历一次 XXDTOList 啊
    print1024
        16
    print1024  
    OP
       2024-02-05 16:21:28 +08:00
    @corningsun 感谢,checkExistRule 是有问题的,我目前采用了你这种方式,线上 YYDTO 每条耗时差不多 150ms ,因为每次都要过 10w 条 XXDTO
    print1024
        17
    print1024  
    OP
       2024-02-05 16:22:52 +08:00
    @soupu626 这是一个不错的思路,前缀 map 能细讲一下吗
    JKeita
        18
    JKeita  
       2024-02-05 17:16:06 +08:00
    trie 树不就行了?
    JKeita
        19
    JKeita  
       2024-02-05 17:32:20 +08:00
    trie 数+bitmap ,用 trie 数匹配到的所有关键字生成一个匹配结果的 bitmap ,然后再根据每个标签的关键字独有的 bitmap 一个个比对,成的话就执行相关策略
    yicixin
        20
    yicixin  
       2024-02-05 18:16:10 +08:00
    不知道说的对不对,
    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 进行匹配
    lrjia
        21
    lrjia  
       2024-02-05 23:31:24 +08:00
    @print1024 #15 不用循环查找的,做一个倒排索引就行了
    lrjia
        22
    lrjia  
       2024-02-06 01:54:36 +08:00
    ac 自动机 + trie 。记 ac 自动机匹配到的关键字个数为 n ,最终匹配到的规则数为 m 。复杂度最差应该是 O(min(2^n, 10w * n)),一般情况应该是 O(nm) https://pastebin.ubuntu.com/p/JbcMYQqHfp/
    wanqiangcrack
        23
    wanqiangcrack  
       2024-02-06 08:52:44 +08:00
    你这个优化思路应该是对数据集做索引,做完索引再去匹配。 这样单次需要处理的数据就少很多了。
    crazyweeds
        24
    crazyweeds  
       2024-02-06 09:07:59 +08:00
    DFA 了解一下
    gongxuanzhang
        25
    gongxuanzhang  
       2024-02-06 09:18:30 +08:00
    前缀树正解。。 而且你这个 try catch 莫名其妙
    missya
        26
    missya  
       2024-02-06 09:31:32 +08:00
    试试 AI 打标
    vivisidea
        27
    vivisidea  
       2024-02-06 09:53:29 +08:00   ❤️ 1
    AC 自动机肯定适用。。AC 自动机只是把词库快速匹配出来,你说的逻辑是放在匹配后的处理逻辑,跟 AC 无关

    最直接的就是 Trie 树,把所有的词都建一个 trie 树,匹配出一堆词之后再看这堆词属于哪个 label

    不好理解的话就试个简单的,先做一道粗筛
    包含所有词,意味着也包含所有字

    把输入字符串 变成字符 hashset ,kewordrule 也变成多个 hashset ,把原来的多重循环变成 hashset 的交集运算,速度应该会比字符串循环快,粗筛出候选,再走原来的逻辑走精确匹配
    print1024
        28
    print1024  
    OP
       2024-02-06 10:26:12 +08:00
    @vivisidea @corningsun @GuuJiang @soupu626 @lrjia 目前采用了 @vivisidea 所说的这种方式,不过我是 AC 自动机+ HashMap<keyword,Set<XXDTO>>这种形式,在数据量大的情况下较 @corningsun 方式速度能提升 10 倍。感谢感谢
    wysnxzm
        29
    wysnxzm  
       2024-02-06 11:16:19 +08:00
    @crazyweeds #24 DFA+1
    MoYi123
        30
    MoYi123  
       2024-02-06 14:20:03 +08:00
    1. 把关键词列表放到一个 list, 去重排序, 并且把原先的关键词列表替换为这里的 rank, 关键词列表变成 list<list<rank>> 即把 string 离散化.

    2. 把上面的 list<string> 变成 ac 自动机, 在文本中搜索. 得到一个 list<int>

    3. 在 list<list<rank>>里搜索有哪几个 list<rank>是 list<int>的子序列, 在这里面抄一个最快的算法 https://leetcode.cn/problems/number-of-matching-subsequences/description/
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2782 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 11:48 · PVG 19:48 · LAX 03:48 · JFK 06:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.