searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

ACDAT(AhoCorasickDoubleArrayTrie)关键词匹配

2024-06-04 09:06:51
14
0

一、实现方式简要说明

1.1、词源存储

便于测试,本地使用中文+英文随机生成100k的词量,根据要求词的平均长度为15。

使用txt文件存储到本地,文件大小:2.64M,也可以使用其他文件格式,如csv文件。

1.2、预加载

由于读取词文本并构建词树需要消耗一定的时间(本地读取txt文件并构建词树耗时:10~15秒之间),我们在项目启动进行关键词树预加载,并通过本地+redis进行缓存,提高词树的读取速度。

1.3、缓存时间设置

为了测试,缓存有效时间设置为60秒(本地随机生成词在redis存储大小:22.07M),实际应用建议持久化存储,添加缓存刷新机制。

1.4、关键词检验流程

1、接口接收检验文本
2、本地缓存获取词树,不存在则从redis获取,获取成功之后缓存到本地
3、redis不存在,则读取词文本文件,构建词树,缓存到本地及redis
4、调用acdat的parseText方法进行关键词匹配

1.5、刷新缓存

一般情况下,词文本变动的情况比较少,但是不排除更新的情况,为了缓存能和词文本内容及时保持一致,提供如下两种方式将缓存进行同步更新:

  1. 添加监听器监听词文本状态,当词文本内容发生变化则读取词文本内容,更新本地及redis缓存(推荐方式)

  2. 添加定时任务(此方案允许存在一定的延时性)检测词文本状态,如果进行了更新,则读取词文本内容,更新本地及redis缓存

1.6、Jmeter测试

测试参数 测试值  
检验文本 2K字  
并发数 200  
持续时间 120秒  

二、代码实现

1、项目引入依赖

<dependency>
  <groupId>com.hankcs</groupId>
  <artifactId>aho-corasick-double-array-trie</artifactId>
  <version>1.2.3</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.8.4</version>
</dependency>

2、配置文件添加 redis配置(略)

  redis:
  host: redis服务IP
  port: redis端口
  password: 密码
  timeout: 6000ms
  database: 2

3、添加缓存时间配置

keywords:
time-out: 60
words-key: "keyWords"
chinese-chars: “生成词的中文”
english-chars: “生成词的英文”

4、添加本地缓存配置

@Configuration
public class CaffeineConfig {

  @Value("${keywords.time-out}")
  private Integer keyWorksTimeOut;
   
  @Bean
  @Primary
  public CacheManager caffeineCacheManager() {
      SimpleCacheManager cacheManager = new SimpleCacheManager();
      ArrayList<CaffeineCache> caches = Lists.newArrayList();
      Map<String, Object> map = getCacheType();
      for (String name : map.keySet()) {
          caches.add(new CaffeineCache(name, (Cache<Object, Object>) map.get(name)));
      }
      cacheManager.setCaches(caches);
      return cacheManager;
  }
   
  private Map<String, Object> getCacheType() {
      Map<String, Object> map = new ConcurrentHashMap<>();
      map.put("key-words", Caffeine.newBuilder().recordStats()
              .expireAfterWrite(keyWorksTimeOut, TimeUnit.SECONDS)
              .maximumSize(100)
              .build());
      return map;
  }
}

5、添加redis配置

@Configuration
public class RedisConfig {
  @Bean
  @SuppressWarnings(value = { "unchecked", "rawtypes" })
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
  {
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(connectionFactory);
      template.setKeySerializer(new StringRedisSerializer());
      template.setHashKeySerializer(new StringRedisSerializer());
      template.afterPropertiesSet();
      return template;
  }
}

6、添加监听器,项目启动执行

@Component
public class KeyWordsListener {

  @Autowired
  public KeywordService keywordService;

  @Value("${keywords.words-key}")
  private String WORDS_KEY;

  @EventListener(classes = {ContextRefreshedEvent.class})
  public void onApplicationEvent() {
      keywordService.readKeyWords(“randomKeWords.txt”, WORDS_KEY);
  }
}

7、添加redisService

@Service
public class RedisService {

  @Autowired
  public RedisTemplate redisTemplate;

  /**
    * 缓存基本的对象,Integer、String、实体类等
    *
    * @param key 缓存的键值
    * @param value 缓存的值
    */
  public <T> void setCacheObject(final String key, final T value){
      redisTemplate.opsForValue().set(key, value);
  }
   
  /**
    * 获得缓存的基本对象。
    *
    * @param key 缓存键值
    * @return 缓存键值对应的数据
    */
  public <T> T getCacheObject(final String key){
      ValueOperations<String, T> operation = redisTemplate.opsForValue();
      return operation.get(key);
  }
  /**
    * 是否存在 Key
    * @param key
    * @return
    */
  public boolean hasKey(String key){
      return redisTemplate.hasKey(key);
  }

8、添加关键词处理service

@Service
public class KeywordService {

  @Resource
  private RedisService redisService;

  // 关键词缓存过期时间,单位:秒
  @Value("${keywords.time-out}")
  private Integer keyWorksTimeOut;
  @Value("${keywords.words-key}")
  private String WORDS_KEY;
  @Value("${keywords.chinese-chars}")
  private String CHINESE_CHARS;
  @Value("${keywords.english-chars}")
  private String ENGLISH_CHARS;


  /**
    * 随机生成词,保存到文本
    * @return boolean,成功或失败
    */
  public boolean randomKeyWords() {

      Random random = new Random();
      List<String> keywords = new ArrayList<>();
      int totalKeywords = 100000; // 100k关键词
      for (int i = 0; i < totalKeywords; i++) {
          // 每个词的平均长度为15,但这里我们直接指定长度为15
          String keyword = generateRandomKeyword(15, random);
          keywords.add(keyword);
      }

      try (BufferedWriter writer = new BufferedWriter(
              new OutputStreamWriter(
                      new FileOutputStream(“randomKeWords.txt”),
                      StandardCharsets.UTF_8
              )
      )) {
          for (String s : keywords) {
              writer.write(s);
              writer.newLine();
          }
          return true;
      } catch (IOException e) {
          e.printStackTrace();
          System.out.println("在保存文件时发生错误");
      }
      return false;
  }

  /**
    * 随机生成词(中文和英文混合)
    * @param length
    * @param random
    * @return StringBuilder,词拼成的字符串
    */
  public String generateRandomKeyword(int length, Random random) {
      StringBuilder sb = new StringBuilder(length);
      for (int i = 0; i < length; i++) {
          if (random.nextBoolean()) {
              if (i > 0 && random.nextFloat() < 0.2) {
                  // 20% 的概率插入全角空格(可选)
                  sb.append(""); // 全角空格
              } else {
                  // 插入随机中文字符
                  int index = random.nextInt(CHINESE_CHARS.length());
                  sb.append(CHINESE_CHARS.charAt(index));
              }
          } else {
              // 插入一个英文字符(包括字母和数字)
              int index = random.nextInt(ENGLISH_CHARS.length());
              sb.append(ENGLISH_CHARS.charAt(index));
          }
      }
      return sb.toString();
  }

  /**
    * 读取关键词
    * @param filePath
    * @return ArrayTrie<String>
    */
  @Cacheable(value = "key-words", key = "#key", sync = true)
  public AhoCorasickDoubleArrayTrie<String> readKeyWords(String filePath, String key) {
      AhoCorasickDoubleArrayTrie<String> arrayTrie = null;
      // 二级redis缓存
      if(redisService.hasKey(WORDS_KEY)){
          arrayTrie = redisService.getCacheObject(WORDS_KEY);
          // redis 存在,进行本地一级缓存
          putKeyWords(arrayTrie,false);
          return arrayTrie;
      }
      // 一级、二级缓存都不存在,先读取文本词,再进行缓存
      arrayTrie = getArrayTrie(filePath);
      // 进行一级、二级缓存
      putKeyWords(arrayTrie, true);
      return arrayTrie;
  }

  /**
    *
    * @param arrayTrie 词树
    * @param isPutRedis 是否更新redis
    */
  @CachePut(value = "key-words", key = "#arrayTrie")
  public void putKeyWords(AhoCorasickDoubleArrayTrie<String> arrayTrie,Boolean isPutRedis){
      if(isPutRedis){
          // redis缓存
          //redisService.setCacheObject(WORDS_KEY, arrayTrie, keyWorksTimeOut, TimeUnit.SECONDS);
          redisService.setCacheObject(WORDS_KEY, arrayTrie);
      }
  }

  /**
    * 从保存的文本获取词
    * @param filePath
    * @return ArrayTrie<String>
    */
  private AhoCorasickDoubleArrayTrie<String> getArrayTrie(String filePath){
      Map<String,String> keyWordsMap = new HashMap<>();
      boolean hasWords = false;
      try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
          String line;
          while ((line = reader.readLine()) != null) {
              keyWordsMap.put(line.trim(),line.trim());
              hasWords = true;
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
      if(hasWords) {
          AhoCorasickDoubleArrayTrie<String> arrayTrie = new AhoCorasickDoubleArrayTrie<String>();
          arrayTrie.build(keyWordsMap);
          return arrayTrie;
      }else {
          return null;
      }
  }
}

9、添加controller

@RestController
@RequestMapping("/checkWords")
public class CheckWords {
  @Autowired
  private KeywordService keywordService;

  @Value("${keywords.words-key}")
  private String WORDS_KEY;

  @GetMapping("/checkFormKeyWords")
  public String checkFormKeyWords(@RequestParam("sourceStr") String sourceStr){
      Date start = new Date();
      // 获取关键词
      AhoCorasickDoubleArrayTrie<String> acdat = keywordService.readKeyWords(“randomKeWords.txt”, WORDS_KEY);

      List<AhoCorasickDoubleArrayTrie.Hit<String>> wordList = acdat.parseText(sourceStr);

      String timeStr = "startTime:"+start.getTime()+",nedTime:"+new Date().getTime();
      if(null != wordList && wordList.size() > 0){
          StringBuilder result = new StringBuilder(timeStr+",检验不通过,包含关键词:");
          for (AhoCorasickDoubleArrayTrie.Hit<String> word: wordList
                ) {
              result.append(word).append("、");
          }
          return result.toString();
      }else {
          return timeStr+",校验通过,无关键词";
      }
  }

  /**
    * 随机生成关键词
    */
  @PostMapping("/randomKeyWords")
  public String randomKeyWords(){
      // 保存随机生成的字符到 txt
      if(keywordService.randomKeyWords()){
          return "关键词生成成功";
      }
      return "关键词生成失败";
  }
}

10、添加监听器监听词文本更新状态

自实现

11、启动类添加缓存配置

@SpringBootApplication
@EnableCaching // Caffeine本地缓存
public class ServiceAcApplication {
  public static void main(String[] args) {
      SpringApplication.run(ServiceAcApplication.class, args);
  }
}
0条评论
0 / 1000
刘****海
1文章数
0粉丝数
刘****海
1 文章 | 0 粉丝
刘****海
1文章数
0粉丝数
刘****海
1 文章 | 0 粉丝
原创

ACDAT(AhoCorasickDoubleArrayTrie)关键词匹配

2024-06-04 09:06:51
14
0

一、实现方式简要说明

1.1、词源存储

便于测试,本地使用中文+英文随机生成100k的词量,根据要求词的平均长度为15。

使用txt文件存储到本地,文件大小:2.64M,也可以使用其他文件格式,如csv文件。

1.2、预加载

由于读取词文本并构建词树需要消耗一定的时间(本地读取txt文件并构建词树耗时:10~15秒之间),我们在项目启动进行关键词树预加载,并通过本地+redis进行缓存,提高词树的读取速度。

1.3、缓存时间设置

为了测试,缓存有效时间设置为60秒(本地随机生成词在redis存储大小:22.07M),实际应用建议持久化存储,添加缓存刷新机制。

1.4、关键词检验流程

1、接口接收检验文本
2、本地缓存获取词树,不存在则从redis获取,获取成功之后缓存到本地
3、redis不存在,则读取词文本文件,构建词树,缓存到本地及redis
4、调用acdat的parseText方法进行关键词匹配

1.5、刷新缓存

一般情况下,词文本变动的情况比较少,但是不排除更新的情况,为了缓存能和词文本内容及时保持一致,提供如下两种方式将缓存进行同步更新:

  1. 添加监听器监听词文本状态,当词文本内容发生变化则读取词文本内容,更新本地及redis缓存(推荐方式)

  2. 添加定时任务(此方案允许存在一定的延时性)检测词文本状态,如果进行了更新,则读取词文本内容,更新本地及redis缓存

1.6、Jmeter测试

测试参数 测试值  
检验文本 2K字  
并发数 200  
持续时间 120秒  

二、代码实现

1、项目引入依赖

<dependency>
  <groupId>com.hankcs</groupId>
  <artifactId>aho-corasick-double-array-trie</artifactId>
  <version>1.2.3</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.8.4</version>
</dependency>

2、配置文件添加 redis配置(略)

  redis:
  host: redis服务IP
  port: redis端口
  password: 密码
  timeout: 6000ms
  database: 2

3、添加缓存时间配置

keywords:
time-out: 60
words-key: "keyWords"
chinese-chars: “生成词的中文”
english-chars: “生成词的英文”

4、添加本地缓存配置

@Configuration
public class CaffeineConfig {

  @Value("${keywords.time-out}")
  private Integer keyWorksTimeOut;
   
  @Bean
  @Primary
  public CacheManager caffeineCacheManager() {
      SimpleCacheManager cacheManager = new SimpleCacheManager();
      ArrayList<CaffeineCache> caches = Lists.newArrayList();
      Map<String, Object> map = getCacheType();
      for (String name : map.keySet()) {
          caches.add(new CaffeineCache(name, (Cache<Object, Object>) map.get(name)));
      }
      cacheManager.setCaches(caches);
      return cacheManager;
  }
   
  private Map<String, Object> getCacheType() {
      Map<String, Object> map = new ConcurrentHashMap<>();
      map.put("key-words", Caffeine.newBuilder().recordStats()
              .expireAfterWrite(keyWorksTimeOut, TimeUnit.SECONDS)
              .maximumSize(100)
              .build());
      return map;
  }
}

5、添加redis配置

@Configuration
public class RedisConfig {
  @Bean
  @SuppressWarnings(value = { "unchecked", "rawtypes" })
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
  {
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(connectionFactory);
      template.setKeySerializer(new StringRedisSerializer());
      template.setHashKeySerializer(new StringRedisSerializer());
      template.afterPropertiesSet();
      return template;
  }
}

6、添加监听器,项目启动执行

@Component
public class KeyWordsListener {

  @Autowired
  public KeywordService keywordService;

  @Value("${keywords.words-key}")
  private String WORDS_KEY;

  @EventListener(classes = {ContextRefreshedEvent.class})
  public void onApplicationEvent() {
      keywordService.readKeyWords(“randomKeWords.txt”, WORDS_KEY);
  }
}

7、添加redisService

@Service
public class RedisService {

  @Autowired
  public RedisTemplate redisTemplate;

  /**
    * 缓存基本的对象,Integer、String、实体类等
    *
    * @param key 缓存的键值
    * @param value 缓存的值
    */
  public <T> void setCacheObject(final String key, final T value){
      redisTemplate.opsForValue().set(key, value);
  }
   
  /**
    * 获得缓存的基本对象。
    *
    * @param key 缓存键值
    * @return 缓存键值对应的数据
    */
  public <T> T getCacheObject(final String key){
      ValueOperations<String, T> operation = redisTemplate.opsForValue();
      return operation.get(key);
  }
  /**
    * 是否存在 Key
    * @param key
    * @return
    */
  public boolean hasKey(String key){
      return redisTemplate.hasKey(key);
  }

8、添加关键词处理service

@Service
public class KeywordService {

  @Resource
  private RedisService redisService;

  // 关键词缓存过期时间,单位:秒
  @Value("${keywords.time-out}")
  private Integer keyWorksTimeOut;
  @Value("${keywords.words-key}")
  private String WORDS_KEY;
  @Value("${keywords.chinese-chars}")
  private String CHINESE_CHARS;
  @Value("${keywords.english-chars}")
  private String ENGLISH_CHARS;


  /**
    * 随机生成词,保存到文本
    * @return boolean,成功或失败
    */
  public boolean randomKeyWords() {

      Random random = new Random();
      List<String> keywords = new ArrayList<>();
      int totalKeywords = 100000; // 100k关键词
      for (int i = 0; i < totalKeywords; i++) {
          // 每个词的平均长度为15,但这里我们直接指定长度为15
          String keyword = generateRandomKeyword(15, random);
          keywords.add(keyword);
      }

      try (BufferedWriter writer = new BufferedWriter(
              new OutputStreamWriter(
                      new FileOutputStream(“randomKeWords.txt”),
                      StandardCharsets.UTF_8
              )
      )) {
          for (String s : keywords) {
              writer.write(s);
              writer.newLine();
          }
          return true;
      } catch (IOException e) {
          e.printStackTrace();
          System.out.println("在保存文件时发生错误");
      }
      return false;
  }

  /**
    * 随机生成词(中文和英文混合)
    * @param length
    * @param random
    * @return StringBuilder,词拼成的字符串
    */
  public String generateRandomKeyword(int length, Random random) {
      StringBuilder sb = new StringBuilder(length);
      for (int i = 0; i < length; i++) {
          if (random.nextBoolean()) {
              if (i > 0 && random.nextFloat() < 0.2) {
                  // 20% 的概率插入全角空格(可选)
                  sb.append(""); // 全角空格
              } else {
                  // 插入随机中文字符
                  int index = random.nextInt(CHINESE_CHARS.length());
                  sb.append(CHINESE_CHARS.charAt(index));
              }
          } else {
              // 插入一个英文字符(包括字母和数字)
              int index = random.nextInt(ENGLISH_CHARS.length());
              sb.append(ENGLISH_CHARS.charAt(index));
          }
      }
      return sb.toString();
  }

  /**
    * 读取关键词
    * @param filePath
    * @return ArrayTrie<String>
    */
  @Cacheable(value = "key-words", key = "#key", sync = true)
  public AhoCorasickDoubleArrayTrie<String> readKeyWords(String filePath, String key) {
      AhoCorasickDoubleArrayTrie<String> arrayTrie = null;
      // 二级redis缓存
      if(redisService.hasKey(WORDS_KEY)){
          arrayTrie = redisService.getCacheObject(WORDS_KEY);
          // redis 存在,进行本地一级缓存
          putKeyWords(arrayTrie,false);
          return arrayTrie;
      }
      // 一级、二级缓存都不存在,先读取文本词,再进行缓存
      arrayTrie = getArrayTrie(filePath);
      // 进行一级、二级缓存
      putKeyWords(arrayTrie, true);
      return arrayTrie;
  }

  /**
    *
    * @param arrayTrie 词树
    * @param isPutRedis 是否更新redis
    */
  @CachePut(value = "key-words", key = "#arrayTrie")
  public void putKeyWords(AhoCorasickDoubleArrayTrie<String> arrayTrie,Boolean isPutRedis){
      if(isPutRedis){
          // redis缓存
          //redisService.setCacheObject(WORDS_KEY, arrayTrie, keyWorksTimeOut, TimeUnit.SECONDS);
          redisService.setCacheObject(WORDS_KEY, arrayTrie);
      }
  }

  /**
    * 从保存的文本获取词
    * @param filePath
    * @return ArrayTrie<String>
    */
  private AhoCorasickDoubleArrayTrie<String> getArrayTrie(String filePath){
      Map<String,String> keyWordsMap = new HashMap<>();
      boolean hasWords = false;
      try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
          String line;
          while ((line = reader.readLine()) != null) {
              keyWordsMap.put(line.trim(),line.trim());
              hasWords = true;
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
      if(hasWords) {
          AhoCorasickDoubleArrayTrie<String> arrayTrie = new AhoCorasickDoubleArrayTrie<String>();
          arrayTrie.build(keyWordsMap);
          return arrayTrie;
      }else {
          return null;
      }
  }
}

9、添加controller

@RestController
@RequestMapping("/checkWords")
public class CheckWords {
  @Autowired
  private KeywordService keywordService;

  @Value("${keywords.words-key}")
  private String WORDS_KEY;

  @GetMapping("/checkFormKeyWords")
  public String checkFormKeyWords(@RequestParam("sourceStr") String sourceStr){
      Date start = new Date();
      // 获取关键词
      AhoCorasickDoubleArrayTrie<String> acdat = keywordService.readKeyWords(“randomKeWords.txt”, WORDS_KEY);

      List<AhoCorasickDoubleArrayTrie.Hit<String>> wordList = acdat.parseText(sourceStr);

      String timeStr = "startTime:"+start.getTime()+",nedTime:"+new Date().getTime();
      if(null != wordList && wordList.size() > 0){
          StringBuilder result = new StringBuilder(timeStr+",检验不通过,包含关键词:");
          for (AhoCorasickDoubleArrayTrie.Hit<String> word: wordList
                ) {
              result.append(word).append("、");
          }
          return result.toString();
      }else {
          return timeStr+",校验通过,无关键词";
      }
  }

  /**
    * 随机生成关键词
    */
  @PostMapping("/randomKeyWords")
  public String randomKeyWords(){
      // 保存随机生成的字符到 txt
      if(keywordService.randomKeyWords()){
          return "关键词生成成功";
      }
      return "关键词生成失败";
  }
}

10、添加监听器监听词文本更新状态

自实现

11、启动类添加缓存配置

@SpringBootApplication
@EnableCaching // Caffeine本地缓存
public class ServiceAcApplication {
  public static void main(String[] args) {
      SpringApplication.run(ServiceAcApplication.class, args);
  }
}
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0