本教程使用 Lettuce,这是一个非官方支持的 Redis 库。对于生产应用,我们建议使用 Jedis
了解 Redis 内置的搜索和查询引擎如何弥合 SQL 和 NoSQL 系统之间的查询鸿沟。我们将重点关注两个日常用例:全文搜索和自动完成。
在本课程中,您将学习
Redis Stack 是一个源可用的 Redis 版本,用于 Redis 中的查询、二级索引和全文搜索。Redis Stack 在 Redis 中实现了二级索引,但与其他 Redis 索引库不同,它不使用内部数据结构,如有序集合。这也支持更高级的功能,如多字段查询、聚合和全文搜索。此外,Redis Stack 支持文本查询的精确短语匹配和数字过滤,这在传统的 Redis 索引方法中既不可能也不高效。在您的 Redis 数据库中拥有丰富的查询和聚合引擎,为许多超越缓存的新应用打开了大门。即使您需要使用复杂查询访问数据,也可以将 Redis 用作您的主要数据库,而无需增加代码更新和索引数据的复杂性。
:::warn
Spring Redis Search 和 LettuSearch 已合并到多模块客户端 LettuceMod 中。请改用 LettuceMod。
:::
Spring Redis Search (https://github.com/RediSearch/spring-redisearch) 是一个基于 LettuSearch (https://github.com/RediSearch/lettusearch) 构建的库,提供从 Spring 应用访问 Redis Stack 的能力。LettuSearch 是一个基于流行的 Redis Java 客户端库 Lettuce 的 Redis Stack Java 客户端。添加 spring-redisearch
依赖项 在您的 Maven pom.xml
中,添加以下依赖项
<dependency>
<groupId>com.redislabs</groupId>
<artifactId>spring-redisearch</artifactId>
<version>3.0.1</version>
</dependency>
要创建索引,您必须定义一个模式来列出要索引的字段及其类型。对于 Book
模型,您将索引四个字段
作者
创建索引使用 FT.CREATE 命令完成。Redis 搜索和查询引擎将使用一个或多个 PREFIX 键模式值扫描数据库,并根据模式定义更新索引。这种主动索引维护使得向现有应用添加索引变得容易。为了创建我们的索引,我们将使用现在熟悉的 CommandLineRunner
模式。我们将把即将创建的索引名称保存在应用的属性字段中,如下所示
app.booksSearchIndexName=books-idx
接下来,创建文件 src/main/java/com/redislabs/edu/redi2read/boot/CreateBooksSearchIndex.java
并添加如下内容
package com.redislabs.edu.redi2read.boot;
import com.redislabs.edu.redi2read.models.Book;
import com.redislabs.lettusearch.CreateOptions;
import com.redislabs.lettusearch.Field;
import com.redislabs.lettusearch.RediSearchCommands;
import com.redislabs.lettusearch.StatefulRediSearchConnection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import io.lettuce.core.RedisCommandExecutionException;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(6)
@Slf4j
public class CreateBooksSearchIndex implements CommandLineRunner {
@Autowired
private StatefulRediSearchConnection<String, String> searchConnection;
@Value("${app.booksSearchIndexName}")
private String searchIndexName;
@Override
@SuppressWarnings({ "unchecked" })
public void run(String... args) throws Exception {
RediSearchCommands<String, String> commands = searchConnection.sync();
try {
commands.ftInfo(searchIndexName);
} catch (RedisCommandExecutionException rcee) {
if (rcee.getMessage().equals("Unknown Index name")) {
CreateOptions<String, String> options = CreateOptions.<String, String>builder()//
.prefix(String.format("%s:", Book.class.getName())).build();
Field<String> title = Field.text("title").sortable(true).build();
Field<String> subtitle = Field.text("subtitle").build();
Field<String> description = Field.text("description").build();
Field<String> author0 = Field.text("authors.[0]").build();
Field<String> author1 = Field.text("authors.[1]").build();
Field<String> author2 = Field.text("authors.[2]").build();
Field<String> author3 = Field.text("authors.[3]").build();
Field<String> author4 = Field.text("authors.[4]").build();
Field<String> author5 = Field.text("authors.[5]").build();
Field<String> author6 = Field.text("authors.[6]").build();
commands.create(
searchIndexName, //
options, //
title, subtitle, description, //
author0, author1, author2, author3, author4, author5, author6 //
);
log.info(">>>> Created Books Search Index...");
}
}
}
}
让我们来分解一下我们的 CreateBooksSearchIndex
CommandLineRunner
在做什么。我们将使用 com.redislabs.lettusearch
包中的类:注入一个 StatefulRediSearchConnection
,它提供对同步模式、异步模式和响应式模式下搜索命令的访问。从 StatefulRediSearchConnection
中,我们使用 sync()
方法获取搜索命令实例(返回同步模式方法)。我们只在索引不存在时才创建索引,这可以通过 FT.INFO 命令抛出异常来指示。为了创建索引,我们构建一个 CreateOptions
对象,传入 Book 类前缀。对于要索引的每个字段,我们创建一个 Field 对象
作者存储在一个 Set 中,因此它们被序列化为带前缀的索引字段(authors.[0], authors.[1]
, ...)。我们最多索引了 6 位作者。要创建索引,我们调用 `create` 方法,传入索引名称、CreateOptions 和字段。要查看更多选项和所有字段类型,请参阅 https://redis.ac.cn/commands/ft.create/ 在服务器重启时,您应该运行 Redis CLI MONITOR 来查看以下命令
1617601021.779396 [0 172.21.0.1:59396] "FT.INFO" "books-idx"
1617601021.786192 [0 172.21.0.1:59396] "FT.CREATE" "books-idx" "PREFIX" "1" "com.redislabs.edu.redi2read.models.Book:" "SCHEMA" "title" "TEXT" "SORTABLE" "subtitle" "TEXT" "description" "TEXT" "authors.[0]" "TEXT" "authors.[1]" "TEXT" "authors.[2]" "TEXT" "authors.[3]" "TEXT" "authors.[4]" "TEXT" "authors.[5]" "TEXT" "authors.[6]" "TEXT"
您可以使用 Redis CLI 中的以下命令查看索引信息
127.0.0.1:6379> FT.INFO "books-idx"
1) index_name
2) books-idx
...
9) num_docs
10) "2403"
11) max_doc_id
12) "2403"
13) num_terms
14) "32863"
15) num_records
16) "413522"
FT.INFO 命令输出中针对 “books-idx”
索引的此片段显示,已索引了 2,403 个文档(系统中的书籍数量)。从我们索引的文档中,共有 32,863 个术语和接近五十万条记录。
Redis Stack 是一个全文搜索引擎,允许应用运行强大的查询。例如,要搜索包含“networking”相关信息的所有书籍,您将运行以下命令
127.0.0.1:6379> FT.SEARCH books-idx "networking" RETURN 1 title
返回如下结果
1) (integer) 299
2) "com.redislabs.edu.redi2read.models.Book:3030028496"
3) 1) "title"
2) "Ubiquitous Networking"
4) "com.redislabs.edu.redi2read.models.Book:9811078718"
5) 1) "title"
2) "Progress in Computing, Analytics and Networking"
6) "com.redislabs.edu.redi2read.models.Book:9811033765"
7) 1) "title"
2) "Progress in Intelligent Computing Techniques: Theory, Practice, and Applications"
8) "com.redislabs.edu.redi2read.models.Book:981100448X"
9) 1) "title"
2) "Proceedings of Fifth International Conference on Soft Computing for Problem Solving"
10) "com.redislabs.edu.redi2read.models.Book:1787129411"
11) 1) "title"
2) "OpenStack: Building a Cloud Environment"
12) "com.redislabs.edu.redi2read.models.Book:3319982044"
13) 1) "title"
2) "Engineering Applications of Neural Networks"
14) "com.redislabs.edu.redi2read.models.Book:3319390287"
15) 1) "title"
2) "Open Problems in Network Security"
16) "com.redislabs.edu.redi2read.models.Book:0133887642"
17) 1) "title"
2) "Web and Network Data Science"
18) "com.redislabs.edu.redi2read.models.Book:3319163132"
19) 1) "title"
2) "Databases in Networked Information Systems"
20) "com.redislabs.edu.redi2read.models.Book:1260108422"
21) 1) "title"
2) "Gray Hat Hacking: The Ethical Hacker's Handbook, Fifth Edition"
如您所见,即使我们使用了单词“networking”,标题中包含“network”一词的书籍也会被返回。这是因为标题已被索引为文本,因此该字段被分词和词干化。此外,该命令未指定字段,因此术语“networking”(及相关术语)在索引的所有文本字段中搜索。这就是为什么有些标题不显示搜索词;在这些情况下,该术语已在另一个索引字段中找到。如果您想在特定字段上搜索,请使用 @field
表示法,如下所示:
127.0.0.1:6379> FT.SEARCH books-idx "@title:networking" RETURN 1 title
尝试针对索引执行其他全文搜索查询。
前缀匹配
127.0.0.1:6379> FT.SEARCH books-idx "clo*" RETURN 4 title subtitle authors.[0] authors.[1]
模糊搜索
127.0.0.1:6379> FT.SEARCH books-idx "%scal%" RETURN 2 title subtitle
并集
127.0.0.1:6379> FT.SEARCH books-idx "rust | %scal%" RETURN 3 title subtitle authors.[0]
您可以在 Redis Search 文档中找到有关查询语法的更多信息。将搜索添加到 Books Controller 为了向 BooksController
添加全文搜索功能,我们将首先注入一个 StatefulRediSearchConnection
,然后简单地将文本查询参数传递给 RediSearchCommands
接口中提供的搜索方法
@Value("${app.booksSearchIndexName}")
private String searchIndexName;
@Autowired
private StatefulRediSearchConnection<String, String> searchConnection;
@GetMapping("/search")
public SearchResults<String,String> search(@RequestParam(name="q")String query) {
RediSearchCommands<String, String> commands = searchConnection.sync();
SearchResults<String, String> results = commands.search(searchIndexName, query);
return results;
}
附带导入
import com.redislabs.lettusearch.RediSearchCommands;
import com.redislabs.lettusearch.SearchResults;
import com.redislabs.lettusearch.StatefulRediSearchConnection;
import org.springframework.beans.factory.annotation.Value;
我们可以使用 curl 执行之前尝试过的一些示例查询
curl --location --request GET 'http://localhost:8080/api/books/search/?q=%25scal%25'
返回如下结果
[
{
"infoLink": "https://play.google.com/store/books/details?id=xVU2AAAAQBAJ&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=xVU2AAAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api",
"_class": "com.redislabs.edu.redi2read.models.Book",
"id": "1449340326",
"language": "en",
"title": "Scala Cookbook",
"price": "43.11",
"currency": "USD",
"categories.[0]": "com.redislabs.edu.redi2read.models.Category:23a4992c-973d-4f36-b4b1-6678c5c87b28",
"subtitle": "Recipes for Object-Oriented and Functional Programming",
"authors.[0]": "Alvin Alexander",
"pageCount": "722",
"description": "..."
},
{
"infoLink": "https://play.google.com/store/books/details?id=d5EIBgAAQBAJ&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=d5EIBgAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api",
"_class": "com.redislabs.edu.redi2read.models.Book",
"id": "178355875X",
"language": "en",
"title": "Scala for Machine Learning",
"price": "22.39",
"currency": "USD",
"categories.[0]": "com.redislabs.edu.redi2read.models.Category:15129267-bee9-486d-88e7-54de709276ef",
"authors.[0]": "Patrick R. Nicolas",
"pageCount": "520",
"description": "..."
},
...
]
Redis Stack 提供了一个 completion suggester,通常用于自动完成/边输入边搜索功能。这是一项导航功能,可在用户输入时引导他们找到相关结果,提高搜索精度。Redis 提供四种命令来获取 completion suggestions
实现作者姓名的自动完成端点
为了创建作者姓名的自动完成建议字典,我们将创建一个 CommandLineRunner,它将遍历书籍,并对于作者 Set<String> 中的每位作者,将其添加到字典中。与 RediSearch 自动维护的搜索索引不同,建议字典需要您使用 FT.SUGADD 和 FT.SUGDEL 手动维护。将自动完成字典名称的属性添加到 src/main/resources/application.properties
app.autoCompleteKey=author-autocomplete
添加文件 src/main/java/com/redislabs/edu/redi2read/boot/CreateAuthorNameSuggestions.java
并添加如下内容:
package com.redislabs.edu.redi2read.boot;
import com.redislabs.edu.redi2read.repositories.BookRepository;
import com.redislabs.lettusearch.RediSearchCommands;
import com.redislabs.lettusearch.StatefulRediSearchConnection;
import com.redislabs.lettusearch.Suggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(7)
@Slf4j
public class CreateAuthorNameSuggestions implements CommandLineRunner {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private BookRepository bookRepository;
@Autowired
private StatefulRediSearchConnection<String, String> searchConnection;
@Value("${app.autoCompleteKey}")
private String autoCompleteKey;
@Override
public void run(String... args) throws Exception {
if (!redisTemplate.hasKey(autoCompleteKey)) {
RediSearchCommands<String, String> commands = searchConnection.sync();
bookRepository.findAll().forEach(book -> {
if (book.getAuthors() != null) {
book.getAuthors().forEach(author -> {
Suggestion<String> suggestion = Suggestion.builder(author).score(1d).build();
commands.sugadd(autoCompleteKey, suggestion);
});
}
});
log.info(">>>> Created Author Name Suggestions...");
}
}
}
让我们来分解一下 CreateAuthorNameSuggestions
CommandLineRunner
的逻辑
BookRepository
遍历所有书籍要在控制器中使用自动建议功能,我们可以添加一个新方法
@Value("${app.autoCompleteKey}")
private String autoCompleteKey;
@GetMapping("/authors")
public List<Suggestion<String>> authorAutoComplete(@RequestParam(name="q")String query) {
RediSearchCommands<String, String> commands = searchConnection.sync();
SuggetOptions options = SuggetOptions.builder().max(20L).build();
return commands.sugget(autoCompleteKey, query, options);
}
附带导入
import com.redislabs.lettusearch.Suggestion;
import com.redislabs.lettusearch.SuggetOptions;
在 authorAutoComplete
方法中,我们使用 FT.SUGGET 命令(通过 RediSearchCommands
对象的 sugget 方法)并使用 SuggetOptions
配置构建查询。在上面的示例中,我们将最大结果数设置为 20。我们可以使用 curl 来构建对新端点的请求。在此示例中,我将“brian s”作为查询传递:
curl --location --request GET 'http://localhost:8080/api/books/authors/?q=brian%20s'
这将返回包含 2 个 JSON 对象的响应
[
{
"string": "Brian Steele",
"score": null,
"payload": null
},
{
"string": "Brian Sam-Bodden",
"score": null,
"payload": null
}
]
如果我们在查询中再添加一个字母,使其变为“brian sa”
curl --location --request GET 'http://localhost:8080/api/books/authors/?q=brian%20sa'
我们将获得预期的建议集范围缩小结果
[
{
"string": "Brian Sam-Bodden",
"score": null,
"payload": null
}
]