Implementing a Simple Hot Search Feature with User Search History and Sensitive Word Filtering Using Java and Redis
This article demonstrates how to build a simple hot‑search functionality with user search history, hot‑keyword ranking, and profanity filtering in a Spring Boot application using Java, Redis ZSet operations, and a DFA‑based sensitive‑word filter, providing complete controller and service code examples.
In this tutorial a top‑level architect explains how to implement a basic hot‑search system that records each user's search history, ranks hot keywords, and filters inappropriate words. The solution is built with Spring Boot, Java, and Redis, leveraging ZSet for ranking and a DFA‑based sensitive‑word filter.
Features
Display the logged‑in user's search history and allow deletion of personal records.
When a user types a character, store it in a Redis zset together with a timestamp and increment its count (the DFA algorithm is mentioned for optional word analysis).
Accumulate counts for existing keywords to obtain the platform's top‑10 hot queries.
Integrate an important profanity‑filtering step before persisting any keyword.
Controller layer – the required methods are:
Add a hot‑search term (with profanity check).
Increase the hotness score of a term by 1.
Retrieve the top‑10 hot terms for a given key.
Insert a user's personal search record.
Query a user's personal search history.
Service implementation (RedisServiceImpl)
<span style="color: rgb(249, 38, 114); font-weight: bold;">package</span> com.****.****.****.user;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> com.jianlet.service.user.RedisService;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> org.apache.commons.lang.StringUtils;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> org.springframework.data.redis.core.*;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> org.springframework.stereotype.Service;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> javax.annotation.Resource;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> java.util.*;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> java.util.concurrent.TimeUnit;
<span style="color: rgb(117, 113, 94);">@Transactional</span>
<span style="color: rgb(117, 113, 94);">@Service</span>("redisService")
<span style="color: rgb(249, 38, 114); font-weight: bold;">public class</span> RedisServiceImpl <span style="color: rgb(249, 38, 114); font-weight: bold;">implements</span> RedisService {
@Resource(name = "redisSearchTemplate")
private StringRedisTemplate redisSearchTemplate;
// Add a user's search history entry
@Override
public int addSearchHistoryByUserId(String userid, String searchkey) {
String shistory = RedisKeyUtils.getSearchHistoryKey(userid);
boolean b = redisSearchTemplate.hasKey(shistory);
if (b) {
Object hk = redisSearchTemplate.opsForHash().get(shistory, searchkey);
if (hk != null) {
return 1;
} else {
redisSearchTemplate.opsForHash().put(shistory, searchkey, "1");
}
} else {
redisSearchTemplate.opsForHash().put(shistory, searchkey, "1");
}
return 1;
}
// Delete a user's history entry
@Override
public Long delSearchHistoryByUserId(String userid, String searchkey) {
String shistory = RedisKeyUtils.getSearchHistoryKey(userid);
return redisSearchTemplate.opsForHash().delete(shistory, searchkey);
}
// Retrieve a user's search history list
@Override
public List<String> getSearchHistoryByUserId(String userid) {
List<String> stringList = null;
String shistory = RedisKeyUtils.getSearchHistoryKey(userid);
boolean b = redisSearchTemplate.hasKey(shistory);
if (b) {
Cursor<Map.Entry<Object, Object>> cursor = redisSearchTemplate.opsForHash().scan(shistory, ScanOptions.NONE);
while (cursor.hasNext()) {
Map.Entry<Object, Object> map = cursor.next();
String key = map.getKey().toString();
stringList.add(key);
}
return stringList;
}
return null;
}
// Increment hot‑search score for a keyword
@Override
public int incrementScoreByUserId(String searchkey) {
Long now = System.currentTimeMillis();
ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet();
ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue();
List<String> title = new ArrayList<>();
title.add(searchkey);
for (int i = 0, lengh = title.size(); i < lengh; i++) {
String tle = title.get(i);
try {
if (zSetOperations.score("title", tle) <= 0) {
zSetOperations.add("title", tle, 0);
valueOperations.set(tle, String.valueOf(now));
}
} catch (Exception e) {
zSetOperations.add("title", tle, 0);
valueOperations.set(tle, String.valueOf(now));
}
}
return 1;
}
// Get top‑10 hot list (optionally filtered by a search key)
@Override
public List<String> getHotList(String searchkey) {
String key = searchkey;
Long now = System.currentTimeMillis();
List<String> result = new ArrayList<>();
ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet();
ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue();
Set<String> value = zSetOperations.reverseRangeByScore("title", 0, Double.MAX_VALUE);
if (StringUtils.isNotEmpty(searchkey)) {
for (String val : value) {
if (StringUtils.containsIgnoreCase(val, key)) {
if (result.size() > 9) break;
Long time = Long.valueOf(valueOperations.get(val));
if ((now - time) < 2592000000L) {
result.add(val);
} else {
zSetOperations.add("title", val, 0);
}
}
}
} else {
for (String val : value) {
if (result.size() > 9) break;
Long time = Long.valueOf(valueOperations.get(val));
if ((now - time) < 2592000000L) {
result.add(val);
} else {
zSetOperations.add("title", val, 0);
}
}
}
return result;
}
// Increment hotness for a keyword (public API)
@Override
public int incrementScore(String searchkey) {
String key = searchkey;
Long now = System.currentTimeMillis();
ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet();
ValueOperations<String, String> valueOperations = redisSearchTemplate.opsForValue();
zSetOperations.incrementScore("title", key, 1);
valueOperations.getAndSet(key, String.valueOf(now));
return 1;
}
}Sensitive‑word filter implementation
A DFA‑based filter is provided to load a list of prohibited words from static/censorword.txt and either reject or replace them.
<span style="color: rgb(249, 38, 114); font-weight: bold;">package</span> com.***.***.interceptor;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> org.springframework.context.annotation.Configuration;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> org.springframework.core.io.ClassPathResource;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> java.io.*;
<span style="color: rgb(249, 38, 114); font-weight: bold;">import</span> java.util.*;
<span style="color: rgb(117, 113, 94);">@Configuration</span>
<span style="color: rgb(117, 113, 94);">@SuppressWarnings</span>({"rawtypes", "unchecked"})
<span style="color: rgb(249, 38, 114); font-weight: bold;">public class</span> SensitiveWordInit {
private String ENCODING = "UTF-8";
public Map initKeyWord() throws IOException {
Set<String> wordSet = readSensitiveWordFile();
return addSensitiveWordToHashMap(wordSet);
}
private Set<String> readSensitiveWordFile() throws IOException {
Set<String> wordSet = null;
ClassPathResource classPathResource = new ClassPathResource("static/censorword.txt");
InputStream inputStream = classPathResource.getInputStream();
try {
InputStreamReader read = new InputStreamReader(inputStream, ENCODING);
wordSet = new HashSet<>();
BufferedReader br = new BufferedReader(read);
String txt = null;
while ((txt = br.readLine()) != null) {
wordSet.add(txt);
}
br.close();
read.close();
} catch (Exception e) {
e.printStackTrace();
}
return wordSet;
}
private Map addSensitiveWordToHashMap(Set<String> wordSet) {
Map wordMap = new HashMap(wordSet.size());
for (String word : wordSet) {
Map nowMap = wordMap;
for (int i = 0; i < word.length(); i++) {
char keyChar = word.charAt(i);
Object tempMap = nowMap.get(keyChar);
if (tempMap != null) {
nowMap = (Map) tempMap;
} else {
Map<String, String> newMap = new HashMap<>();
newMap.put("isEnd", "0");
nowMap.put(keyChar, newMap);
nowMap = newMap;
}
if (i == word.length() - 1) {
nowMap.put("isEnd", "1");
}
}
}
return wordMap;
}
}The filter class SensitiveFilter provides methods to detect and replace prohibited words, supporting both minimal and maximal match strategies.
<span style="color: rgb(249, 38, 114); font-weight: bold;">public class</span> SensitiveFilter {
private Map sensitiveWordMap = null;
public static int minMatchType = 1;
public static int maxMatchType = 2;
private static SensitiveFilter instance = null;
private SensitiveFilter() throws IOException {
sensitiveWordMap = new SensitiveWordInit().initKeyWord();
}
public static SensitiveFilter getInstance() throws IOException {
if (instance == null) {
instance = new SensitiveFilter();
}
return instance;
}
public Set<String> getSensitiveWord(String txt, int matchType) {
Set<String> sensitiveWordList = new HashSet<>();
for (int i = 0; i < txt.length(); i++) {
int length = CheckSensitiveWord(txt, i, matchType);
if (length > 0) {
sensitiveWordList.add(txt.substring(i, i + length));
i = i + length - 1;
}
}
return sensitiveWordList;
}
public String replaceSensitiveWord(String txt, int matchType, String replaceChar) {
String resultTxt = txt;
Set<String> set = getSensitiveWord(txt, matchType);
for (String word : set) {
String replaceString = getReplaceChars(replaceChar, word.length());
resultTxt = resultTxt.replaceAll(word, replaceString);
}
return resultTxt;
}
private String getReplaceChars(String replaceChar, int length) {
StringBuilder sb = new StringBuilder(replaceChar);
for (int i = 1; i < length; i++) {
sb.append(replaceChar);
}
return sb.toString();
}
public int CheckSensitiveWord(String txt, int beginIndex, int matchType) {
boolean flag = false;
int matchFlag = 0;
Map nowMap = sensitiveWordMap;
for (int i = beginIndex; i < txt.length(); i++) {
char word = txt.charAt(i);
nowMap = (Map) nowMap.get(word);
if (nowMap != null) {
matchFlag++;
if ("1".equals(nowMap.get("isEnd"))) {
flag = true;
if (minMatchType == matchType) {
break;
}
}
} else {
break;
}
}
if (maxMatchType == matchType) {
if (matchFlag < 2 || !flag) {
matchFlag = 0;
}
}
if (minMatchType == matchType) {
if (matchFlag < 2 && !flag) {
matchFlag = 0;
}
}
return matchFlag;
}
}In the controller you simply obtain an instance of SensitiveFilter, call CheckSensitiveWord to reject illegal inputs, or use replaceSensitiveWord to mask them before persisting.
The article also includes promotional notes (e.g., QR codes, group invitations) which are not part of the technical solution.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
