学习

使用 Redis 搜索

Brian Sam-Bodden
作者
Brian Sam-Bodden, Redis 开发者倡导者
LETTUCE

本教程使用 Lettuce,它是一个不受支持的 Redis 库。对于生产应用程序,我们建议使用 Jedis

目标#

了解 Redis 中内置的搜索和查询引擎如何弥合 SQL 和 NoSQL 系统之间的查询差距。我们将重点关注两个日常用例:全文搜索和自动完成。

议程#

在本课中,您将了解

  • 如何使用 spring-redisearch 和 lettuce-search 在 Redis 中创建搜索索引。
  • 如何在 Spring Boot 应用程序中使用 Redis 来实现分面搜索。
  • 如何使用 Redis 建议功能来实现自动完成。如果您遇到困难:
  • 本课中取得的进展可在 redi2read github 存储库中找到,地址为 https://github.com/redis-developer/redi2read/tree/course/milestone-7

Redis Stack 搜索和查询引擎#

Redis Stack 是 Redis 的一个源可用版本,用于在 Redis 中进行查询、辅助索引和全文搜索。Redis Stack 在 Redis 中实现了一个辅助索引,但与其他 Redis 索引库不同,它不使用内部数据结构,例如排序集。这也支持更高级的功能,例如多字段查询、聚合和全文搜索。此外,Redis Stack 支持对文本查询进行精确短语匹配和数字过滤,而传统 Redis 索引方法既不可能也不高效。在您的 Redis 数据库中拥有一个丰富的查询和聚合引擎,为许多超越缓存的新应用程序打开了大门。即使您需要使用复杂查询访问数据,您也可以使用 Redis 作为您的主要数据库,而无需为更新和索引数据添加代码复杂性。

使用 spring-redisearch#

:::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 使用四个命令提供完成建议

  • FT.SUGADD: 将建议字符串添加到自动完成词典中。
  • FT.SUGGET: 获取字符串的建议列表。
  • FT.SUGDEL: 从自动完成词典中删除建议字符串。
  • FT.SUGLEN: 返回自动完成词典的大小

为作者姓名实现自动完成端点

要为作者姓名创建自动完成建议词典,我们将创建一个 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
  }
]