Redis篇--应用篇4--自动提示,自动补全
1、概述
Redis本身并不直接提供自动补全(Auto-Complete)功能,但可以通过合理的数据结构和算法设计实现自动补全的功能。常见的实现方式是使用有序集合(Sorted Set)或前缀树(Trie)结合键值对(String)来存储和查询前缀匹配的词项。
2、方案概述
(1)、数据结构选择
- 词项存储:使用Redis的Sorted Set来存储所有词项及其权重,以便根据权重排序。
- 前缀索引:使用Redis的Set类型来存储每个前缀对应的词项列表。
(2)、关键操作
- 添加词项:将词项插入到Sorted Set中,并为每个前缀创建索引。
- 查询前缀:根据用户输入的前缀,查找所有匹配的词项,并按权重排序返回结果。
(3)、逻辑梳理
1、添加词项
每次添加一个新词项时,我们需要将其插入到Sorted Set中,并为该词项的所有前缀创建索引。假设我们有一个词项 “apple”,它的权重为 5,我们将执行以下操作:
第一步:插入词项到Sorted Set:
ZADD autocomplete:items 5 "apple"
第二步:拆分词项的前缀,存储前缀到词项的Set集合中
- 前缀"a":
SADD autocomplete:index:a apple
- 前缀"ap":
SADD autocomplete:index:ap apple
- 前缀"app":
SADD autocomplete:index:app apple
- 前缀"appl":
SADD autocomplete:index:appl apple
- 前缀"apple":
SADD autocomplete:index:apple apple
2、查询前缀
当用户输入一个前缀时,我们可以根据该前缀查找所有匹配的词项,并从Sorted Set中获取它们的权重,按权重排序返回结果。假设用户输入了前缀"app",我们将执行以下操作:
第一步:查找匹配的词项
SMEMBERS autocomplete:index:app
如:以"app"开头的词项,此处返回的Set集合为[“apple”, “application”]。
第二步:获取词项的权重并排序
ZREVRANGE autocomplete:items 0 -1 WITHSCORES BYSCORE LIMIT 0 10
如:返回 Sorted Set 中的前 10 个词项及其权重,按权重从高到低排序。
3、更新词项权重
为了实现动态更新词项的权重(例如根据用户的点击次数或搜索频率),我们可以使用ZINCRBY命令来增加某个词项的权重。假设我们要将词项"apple"的权重增加 1:
ZINCRBY autocomplete:items 1 "apple"
4、删除词项
如果需要删除某个词项,我们需要从Sorted Set中移除它,并删除所有与该词项相关的前缀索引。假设我们要删除词项"apple":
第一步:Sorted Set 中移除词项:
ZREM autocomplete:items "apple"
第二步:删除所有前缀索引
- 前缀 "a":
SREM autocomplete:index:a apple
- 前缀 "ap":
SREM autocomplete:index:ap apple
- 前缀 "app":
SREM autocomplete:index:app apple
- 前缀 "appl":
SREM autocomplete:index:appl apple
- 前缀 "apple":
SREM autocomplete:index:apple apple
3、代码示例
(1)、操作实现类
RedisTemplate配置和序列化注入就不赘述了。
第一:存储目标词项到Zset中。拆分词条成多个索引前缀,创建索引和词条的映射关系,通过Set存储。
第二:根据前缀查询符合预期的词项Set列表,回归到Zset中根据权重重新排序。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Set;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class AutocompleteService {
private static final String ITEMS_KEY = "autocomplete:items"; // 词条存储(ZSet存储,可以按照分值排序)
private static final String INDEX_PREFIX = "autocomplete:index:"; // 索引存储(Set存储,存储所有词条索引和词条的关系)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 添加词项
public void addTerm(String term, double score) {
redisTemplate.opsForZSet().add(ITEMS_KEY, term, score);
for (int i = 1; i <= term.length(); i++) {
String prefix = term.substring(0, i);
redisTemplate.opsForSet().add(INDEX_PREFIX + prefix, term);
}
}
// 查询前缀
public List<String> searchByPrefix(String prefix, int limit) {
Set<Object> terms = redisTemplate.opsForSet().members(INDEX_PREFIX + prefix);
if (terms == null || terms.isEmpty()) {
return new ArrayList<>();
}
// 获取词项的权重并排序
List<String> sortedTerms = redisTemplate.opsForZSet()
.reverseRangeByScore(ITEMS_KEY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 0, limit)
.stream()
.filter(terms::contains)
.map(Object::toString)
.collect(Collectors.toList());
return sortedTerms;
}
// 更新词项权重
public void updateTermScore(String term, double increment) {
redisTemplate.opsForZSet().incrementScore(ITEMS_KEY, term, increment);
}
// 删除词项
public void deleteTerm(String term) {
redisTemplate.opsForZSet().remove(ITEMS_KEY, term);
for (int i = 1; i <= term.length(); i++) {
String prefix = term.substring(0, i);
redisTemplate.opsForSet().remove(INDEX_PREFIX + prefix, term);
}
}
}
(2)、测试类
模拟操作:新增词条、根据输入前缀获取符合的所有词条。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/autocomplete")
public class AutoCompleteController {
@Autowired
private AutocompleteService autoCompleteService;
/**
* 添加建议词
* @param term 建议词
* @param score 权重(可选,默认为 1)
* @return 成功消息
*/
@PostMapping("/add")
public String addSuggestion(@RequestParam String term, @RequestParam(required = false, defaultValue = "1.0") double score) {
autoCompleteService.addTerm(term, score);
return "Suggestion added: " + term;
}
/**
* 获取建议词
* @param prefix 用户输入的前缀
* @param limit 返回的建议词数量
* @return 匹配的建议词列表
*/
@GetMapping("/suggest")
public List<String> getSuggestions(@RequestParam String prefix, @RequestParam(defaultValue = "5") int limit) {
return autoCompleteService.searchByPrefix(prefix, limit);
}
}
(3)、测试
第一步:添加词条
本例一共添加apple(权重1),ap11(权重1),ap22(权重2),application(权重2)
可以看到redis中正常存储了Zset的4个词项。
且每一个词项的所有前缀索引也设置了Set集合映射。
第二步:查询前缀,获取自动补全的词条
查询ap,可以看到能查询到所有匹配的词条,并且是按照权重排序的。
在查询app,符合的词条只有2个,也是按照权重排序的。
4、总结
通过合理使用Redis的有序集合和键值对,结合前缀索引和权重排序,可以实现一个自动补全词语的方案。这个方案通常建议用于小型应用,也可以通过分布式架构和Redis模块扩展到大规模应用场景。
5、其他方案实现
- RediSearch:如果你需要更复杂的自动补全功能(如模糊匹配、分词、全文搜索等),可以考虑使用[RediSearch]模块。RediSearch提供了强大的搜索和自动补全功能,适合构建复杂的应用场景。
- Elasticsearch:如果你已经在使用Elasticsearch,也可以考虑将自动补全功能集成到Elasticsearch中,利用其强大的全文搜索和自动补全功能。
- Algolia:如果你不想自己管理基础设施,可以考虑使用Algolia这样的第三方服务,它专门为搜索和自动补全提供了高度优化的解决方案。