本教程使用 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 Stack 的 Java 客户端,基于流行的 Redis Java 客户端库 Lettuce。在您的 Maven pom.xml
中添加 spring-redisearch
依赖项,添加以下依赖项
<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()
方法(返回同步模式方法)获取 Search 命令的实例。我们只在索引不存在时创建索引,这将由 FT.INFO 命令命令抛出异常来表示。要创建索引,我们构建一个 CreateOptions
对象,传递 Book 类的前缀。对于要索引的每个字段,我们创建一个 Field 对象
作者存储在一个集合中,因此它们被序列化为带前缀的索引字段 (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"
这段来自 “books-idx”
索引的 FT.INFO 命令输出的片段显示,已索引 2,403 个文档(系统中的图书数量)。从我们已索引的文档中,有 32,863 个术语和近 50 万条记录。
Redis Stack 是一个全文搜索引擎,允许应用程序运行强大的查询。例如,要搜索所有包含“网络”相关信息的书籍,您将运行以下命令
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"
如您所见,标题中包含“network”一词的书籍被返回,即使我们使用了“networking”一词。这是因为标题已索引为文本,因此字段被标记化并进行了词干提取。此外,该命令没有指定字段,因此“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 'https://localhost:8080/api/books/search/?q=%25scal%25'
这将返回
[
{
"infoLink": "https://play.google.com/store/books/details?id=xVU2AAAAQBAJ&source=gbs_api",
"thumbnail": "https://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": "https://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 提供了一个完成建议器,它通常用于自动完成/边输入边搜索功能。这是一个导航功能,旨在引导用户在输入时找到相关结果,提高搜索精度。Redis 使用四个命令提供完成建议
要为作者姓名创建自动完成建议词典,我们将创建一个 CommandLineRunner,它将循环遍历书籍,并对作者集合中的每个作者,将其添加到词典中。与 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 'https://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 'https://localhost:8080/api/books/authors/?q=brian%20sa'
我们将得到预期的建议集缩小
[
{
"string": "Brian Sam-Bodden",
"score": null,
"payload": null
}
]