在 Elasticsearch 中实现自动完成功能 1:Prefix queries

自动完成与搜索功能不同 - 我们应该在用户键入下一个字符后立即更新自动完成选项,每秒都会访问数据库,过滤数百万条记录,而不会导致任何性能下降!

Elasticsearch 是一种可以轻松实现此类功能的技术,它是一种基于 Apache Lucene 库构建的搜索和分析引擎。 Elasticsearch 具有分布式、多租户架构,具有内置路由和重新平衡功能,使其易于扩展。 它是一种广泛使用的数据存储,用于存储、搜索和分析大量数据。

在这个由三部分组成的博客文章系列中,我将详细介绍如何使用 Elasticsearch 中提供的各种选项来实现自动完成功能。 在第一部分(即这篇文章)中,我们将讨论前缀查询 - prefix queries。 在第二部分中,我们将了解 n-grams,在最后部分中,我们将讨论 complete suggesters。

出于示例目的,我们将使用存储电影数据的索引。 为了简单起见,title 将是该索引中唯一存在的属性。 由于 Elasticsearch 为其操作公开了 REST 接口,因此你可以使用任何基于 REST 的工具与其进行通信。

本系列假设你对 Elasticsearch 有基本的了解。 如果你是 Elasticsearch 的新手,我强烈建议你阅读 "Elastic:开发者上手指南"。

那么让我们开始吧?

前缀查询 - Prefix queries

前缀查询是 Elasticsearch 中自动完成实现的最简单形式。 我们在存储字段时不做任何特殊的事情,大部分工作都是在查询时完成的。 该字段被索引(存储!)为一个简单的文本/关键字字段,并且允许我们根据传递的前缀匹配文档的查询用于查询它。

让我们创建一个索引来运行前缀查询:

bash 复制代码
1.  PUT /movies
2.  {
3.    "mappings": {
4.      "properties": {
5.        "title": {
6.          "type": "keyword",
7.          "fields": {
8.            "analyzed_title": {
9.              "type": "text"
10.            }
11.          }
12.        }
13.      }
14.    }
15.  }

创建索引时,我们需要提供映射,指示我们打算存储的数据类型。 出于以下示例的目的,title 被映射为 keyword 字段,也被映射为支持全文查询的文本字段。 使用 Elasticsearch 的多字段功能可以将一个字段映射为多种类型。

keyword 字段和 text 字段之间的主要区别在于关键字字段不被分析,即我们传递到关键字字段的数据按原样存储。 对文本字段进行分析,即分词化、可能进行转换(例如小写、词干等),并存储在倒排索引中。 倒排索引是一种数据结构,用于存储从术语到它们出现的文档位置的映射,从而实现高效的全文搜索。有关 keyword 和 text 类型的区别,请详细参阅文档 "Elasticsearch:Text vs. Keyword - 它们之间的差异以及它们的行为方式"。

为了测试如何分析我们的数据,我们可以使用 _analyze API。 让我们看看我们的主标题字段将如何分析:

bash 复制代码
1.  GET /movies/_analyze
2.  {
3.    "text": "Chamber of Secrets",
4.    "field": "title"
5.  }

上面命令的响应为:

markdown 复制代码
1.  {
2.    "tokens": [
3.      {
4.        "token": "Chamber of Secrets",
5.        "start_offset": 0,
6.        "end_offset": 18,
7.        "type": "word",
8.        "position": 0
9.      }
10.    ]
11.  }

因此,它只返回一个 token。 为什么? 没错,就是因为它是关键字字段! 让我们测试一下我们 analyzed_title 的表现:

bash 复制代码
1.  GET /movies/_analyze
2.  {
3.    "text": "Chamber of Secrets",
4.    "field": "title.analyzed_title"
5.  }

上面命令的响应为:

json 复制代码
1.  {
2.    "tokens": [
3.      {
4.        "token": "chamber",
5.        "start_offset": 0,
6.        "end_offset": 7,
7.        "type": "<ALPHANUM>",
8.        "position": 0
9.      },
10.      {
11.        "token": "of",
12.        "start_offset": 8,
13.        "end_offset": 10,
14.        "type": "<ALPHANUM>",
15.        "position": 1
16.      },
17.      {
18.        "token": "secrets",
19.        "start_offset": 11,
20.        "end_offset": 18,
21.        "type": "<ALPHANUM>",
22.        "position": 2
23.      }
24.    ]
25.  }

正如所料,它被分解为三个 token。 此外,token 是小写的。 这是为什么? 因为,即使我们不指定任何分析器,默认的标准分析器也会应用于执行基于语法的标记化的文本字段,并且还将这些标记小写。 文本分析是一种高度可配置的过程,由一个或多个字符过滤器、分词器以及一个或多个在管道中运行的分词过滤器组成。 我们可以创建自己的分析器,也可以定制内置分析器。有关分词器的详细介绍,请阅读文章 "Elasticsearch: analyzer"。

让我们将一些哈利波特电影添加到我们的索引中,即让我们索引一些文档:

bash 复制代码
1.  POST /movies/_doc
2.  {
3.    "title": "Harry Potter and the Chamber of Secrets"
4.  }

6.  POST /movies/_doc
7.  {
8.    "title": "Harry Potter and the Prisoner of Azkaban"
9.  }

让我们尝试使用前缀查询来查询我们的主 title 字段(关键字)。 前缀查询是术语级别查询的一种,用于查询非分析字段。 我们将尝试两个不同的请求 - 第一个请求使用 title 中第一个单词的前缀,另一个请求使用标题中第二个单词的前缀:

bash 复制代码
1.  GET /movies/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "prefix": {
5.        "title": "Harr"
6.      }
7.    }
8.  }

上面的响应为:

json 复制代码
1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "movies",
6.          "_id": "er9oHIsByaLf0EuTh81O",
7.          "_score": 1,
8.          "_source": {
9.            "title": "Harry Potter and the Chamber of Secrets"
10.          }
11.        },
12.        {
13.          "_index": "movies",
14.          "_id": "e79oHIsByaLf0EuTjc3H",
15.          "_score": 1,
16.          "_source": {
17.            "title": "Harry Potter and the Prisoner of Azkaban"
18.          }
19.        }
20.      ]
21.    }
22.  }

我们做另外一个查询:

bash 复制代码
1.  GET /movies/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "prefix": {
5.        "title": "Pott"
6.      }
7.    }
8.  }

上述查询返回:

markdown 复制代码
1.  {
2.    "hits": {
3.      "hits": []
4.    }
5.  }

也即没有任何的结果。

tilte 是关键字字段,我们必须提供具有正确大小写的前缀。 如果我们在查询中传递 "harr",它将不匹配。 第一个请求按预期返回上面索引的两个文档。 但第二个请求不会返回任何内容。 这是因为这个查询不支持中缀(在 title 中间匹配)匹配。

如果我们想在 title 内进行匹配,我们应该使用 match_phrase_prefix - 一种用于在分析的文本字段上进行前缀匹配的查询类型:

bash 复制代码
1.  GET /movies/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match_phrase_prefix": {
5.        "title.analyzed_title": {
6.          "query": "pott"
7.        }
8.      }
9.    }
10.  }

上述命令返回的结果为:

json 复制代码
1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "movies",
6.          "_id": "er9oHIsByaLf0EuTh81O",
7.          "_score": 0.18232156,
8.          "_source": {
9.            "title": "Harry Potter and the Chamber of Secrets"
10.          }
11.        },
12.        {
13.          "_index": "movies",
14.          "_id": "e79oHIsByaLf0EuTjc3H",
15.          "_score": 0.18232156,
16.          "_source": {
17.            "title": "Harry Potter and the Prisoner of Azkaban"
18.          }
19.        }
20.      ]
21.    }
22.  }

当我们搜索 analyzed_title 时,"pott" 前缀与属于我们两个文档的标记 "potter" 匹配。 因此,两份文件均被召回。

前缀乱序怎么办? 由于 title 中的单词被分词,我们期望 "potter harry" 与两个文档匹配。 但这是一个短语前缀查询,它尊重输入的顺序。 如果我们想要无序匹配,我们可以使用 match_bool_prefix。

bash 复制代码
1.  GET /movies/_search
2.  {
3.      "query": {
4.          "match_phrase_prefix": {
5.              "title.analyzed_title": {
6.                  "query": "potter harry"
7.              }
8.          }
9.      }
10.  }

上述查询将不会返回任何的结果。而如下的查询:

bash 复制代码
1.  GET /movies/_search?filter_path=**.hits
2.  {
3.    "query": {
4.      "match_bool_prefix": {
5.        "title.analyzed_title": {
6.          "query": "pott harr"
7.        }
8.      }
9.    }
10.  }

将返回如下的结果:

json 复制代码
1.  {
2.    "hits": {
3.      "hits": [
4.        {
5.          "_index": "movies",
6.          "_id": "er9oHIsByaLf0EuTh81O",
7.          "_score": 1,
8.          "_source": {
9.            "title": "Harry Potter and the Chamber of Secrets"
10.          }
11.        },
12.        {
13.          "_index": "movies",
14.          "_id": "e79oHIsByaLf0EuTjc3H",
15.          "_score": 1,
16.          "_source": {
17.            "title": "Harry Potter and the Prisoner of Azkaban"
18.          }
19.        }
20.      ]
21.    }
22.  }

这就是我要讨论的使用前缀查询自动完成的全部内容。 在选择此作为实现自动完成功能的方法时,我们需要考虑一些事项:

  • 这是最不推荐的方法,与其他自动完成(另外的两篇文章)实现相比,这种方法被认为是最慢的方法。 搜索速度很慢,因为我们在索引字段时没有做任何有助于自动完成查询的工作。 它被索引为一个简单的文本字段,将文档与查询文本进行匹配的大部分工作都是在搜索时完成的。 它将转到倒排索引并检查是否有任何标记以查询中提供的文本开头,这是一项昂贵的操作。
  • 在 Elasticsearch 的最新版本中,为术语级别前缀查询添加了 index_prefixes 选项,该选项允许通过将前缀存储在单独的字段中来加速前缀查询。
  • 如果你已经有一个工作索引并且不需要更新映射,那么前缀查询将是适合你的方法,因为自动完成不是系统中频繁使用的功能之一。 但如果是这样,那么你可能会遇到性能问题。 最好使用本系列下一部分中讨论的方法之一并重新索引数据。

如果你想了解这种方法的详细实现,请阅读 "Elasticsearch:创建一个 autocomplete 输入系统 - 前端 + 后端"。

相关推荐
bubble小拾2 小时前
ElasticSearch高级功能详解与读写性能调优
大数据·elasticsearch·搜索引擎
不能放弃治疗3 小时前
重生之我们在ES顶端相遇第 18 章 - Script 使用(进阶)
elasticsearch
hengzhepa4 小时前
ElasticSearch备考 -- Search across cluster
学习·elasticsearch·搜索引擎·全文检索·es
Elastic 中国社区官方博客5 小时前
Elasticsearch:使用 LLM 实现传统搜索自动化
大数据·人工智能·elasticsearch·搜索引擎·ai·自动化·全文检索
慕雪华年6 小时前
【WSL】wsl中ubuntu无法通过useradd添加用户
linux·ubuntu·elasticsearch
Elastic 中国社区官方博客8 小时前
使用 Vertex AI Gemini 模型和 Elasticsearch Playground 快速创建 RAG 应用程序
大数据·人工智能·elasticsearch·搜索引擎·全文检索
alfiy9 小时前
Elasticsearch学习笔记(四) Elasticsearch集群安全配置一
笔记·学习·elasticsearch
alfiy10 小时前
Elasticsearch学习笔记(五)Elastic stack安全配置二
笔记·学习·elasticsearch
丶21361 天前
【大数据】Elasticsearch 实战应用总结
大数据·elasticsearch·搜索引擎
闲人编程1 天前
elasticsearch实战应用
大数据·python·elasticsearch·实战应用