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)
package
com.****.****.****.user;
import
com.jianlet.service.user.RedisService;
import
org.apache.commons.lang.StringUtils;
import
org.springframework.data.redis.core.*;
import
org.springframework.stereotype.Service;
import
javax.annotation.Resource;
import
java.util.*;
import
java.util.concurrent.TimeUnit;
@Transactional
@Service
("redisService")
public class
RedisServiceImpl
implements
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
getSearchHistoryByUserId(String userid) {
List
stringList = null;
String shistory = RedisKeyUtils.getSearchHistoryKey(userid);
boolean b = redisSearchTemplate.hasKey(shistory);
if (b) {
Cursor
> cursor = redisSearchTemplate.opsForHash().scan(shistory, ScanOptions.NONE);
while (cursor.hasNext()) {
Map.Entry
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
valueOperations = redisSearchTemplate.opsForValue();
List
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
getHotList(String searchkey) {
String key = searchkey;
Long now = System.currentTimeMillis();
List
result = new ArrayList<>();
ZSetOperations zSetOperations = redisSearchTemplate.opsForZSet();
ValueOperations
valueOperations = redisSearchTemplate.opsForValue();
Set
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
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.
package
com.***.***.interceptor;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.core.io.ClassPathResource;
import
java.io.*;
import
java.util.*;
@Configuration
@SuppressWarnings
({"rawtypes", "unchecked"})
public class
SensitiveWordInit {
private String ENCODING = "UTF-8";
public Map initKeyWord() throws IOException {
Set
wordSet = readSensitiveWordFile();
return addSensitiveWordToHashMap(wordSet);
}
private Set
readSensitiveWordFile() throws IOException {
Set
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
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
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.
public class
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
getSensitiveWord(String txt, int matchType) {
Set
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
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.
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.