如何在您的 Elastic 企业搜索引擎中添加针对更多语言的支持

enterprise-search-site-search-light-1680x980.png

Elastic App Search 中的引擎可让您对文档进行索引,并提供开箱即用且可微调的搜索功能。默认情况下,这些引擎支持预定义的一系列语言。如果您的语言不在该预定义的列表内,也不用担心,本篇博文讲解了您可以如何添加针对更多语言的支持。我们的方法是创建一个 App Search 引擎,并为该引擎配备针对该语言设置的分析器。

在详细讲解之前,我们先定义一下 Elasticsearch 分析器是什么:

Elasticsearch 分析器是包含下列三个低层级构建基块的软件包:字符筛选器、分词器和分词筛选器。分析器既可是内置的,也可自定义。内置分析器会将构建基块预先打包成适用于不同语言和文本类型的分析器。

每个字段的分析器用于:

  • 索引。对于每个文档字段,系统都会使用其对应的分析器进行处理,并将其拆分成分词以便于搜索。
  • 搜索。对于搜索查询,系统会使用分析器进行分析,确保正确匹配已完成分析的索引字段。

通过 Elasticsearch 基于索引的引擎,您能够使用既有的 Elasticsearch 索引创建 App Search 引擎。我们将会使用自己的分析器和映射创建一个 Elasticsearch 索引,并在 App Search 中使用该索引。

此过程分为四步:

  1. 创建一个 Elasticsearch 索引并对文档进行索引
  2. 向该索引添加语言分析器
  3. 更新索引映射以使用分析器
  4. 重新对文档进行索引

1. 创建一个 Elasticsearch 索引并对文档进行索引

开始时,我们使用一个尚未针对任何语言进行优化的索引。我们假设这是一个没有预定义映射的全新索引,是在首次对文档进行索引时创建的。

在 Elasticsearch 中,映射过程用于定义如何存储和索引文档及文档内的字段。每份文档都包含一系列字段,而每个字段又都有其自己的数据类型。对数据进行映射时,您会创建一个映射定义,其中包含与文档相关的一个字段列表。

回到我们的示例。索引名为 books(书),其中 title(书名)采用罗马尼亚语。之所以选择罗马尼亚语,是因为这是我的母语,而且它不在 App Search 默认支持的那一系列语言之内。  

POST books/_doc/1
{
  "title": "Un veac de singurătate",
  "author": "Gabriel García Márquez"
}

POST books/_doc/2
{
  "title": "Dragoste în vremea holerei",
  "author": "Gabriel García Márquez"
}

POST books/_doc/3
{
  "title": "Obosit de viaţă, obosit de moarte",
  "author": "Mo Yan"
}

POST books/_doc/4
{
  "title": "Maestrul și Margareta",
  "author": "Mihail Bulgakov"
}

2. 向 books 索引添加语言分析器

检查 books 索引的映射后,我们发现此索引尚未针对罗马尼亚语进行优化。您之所以能确定这一点,是因为在 settings(设置)基块中没有 analysis(分析)字段,而且文本字段不使用自定义分析器。

GET books
{
  "books": {
    "aliases": {},
    "mappings": {
      "properties": {
        "author": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "title": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    },
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        },
        "number_of_shards": "1",
        "provided_name": "books",
        "creation_date": "1679310576178",
        "number_of_replicas": "1",
        "uuid": "0KuiDk8iSZ-YHVQGg3B0iw",
        "version": {
          "created": "8080099"
        }
      }
    }
  }
}

如果尝试使用 books 索引创建 App Search 引擎,我们将会面临两个问题。首先,搜索结果将不会针对罗马尼亚语进行优化;其次,诸如精度调整等功能会被禁用。

关于不同类型 Elastic App Search 引擎的简单备注:

  • 默认选项是 App Search 托管引擎,此选项会自动创建和托管隐藏的 Elasticsearch 索引。如果使用此选项,您必须使用 App Search documents API 在引擎内采集数据。
  • 如果使用另一选项,App Search 会使用既有 Elasticsearch 索引创建引擎;如为这种情况,App Search 将会按原样使用此索引。这时,您可以使用 Elasticsearch index documents API 直接在底层索引中采集数据。

[相关文章:Elasticsearch Search API:定位 App Search 文档的新方法]

当您使用既有 Elasticsearch 索引创建引擎时,如果映射不遵守 App Search 的规范,则不会为该引擎启用所有功能。我们通过查看一个由 App Search 完全托管的引擎来详细了解一下 App Search 的命名规范。这个引擎有两个字段,title(书名)和 author(作者),使用的语言是英语。

GET .ent-search-engine-documents-app-search-books/_mapping/field/title
{
  ".ent-search-engine-documents-app-search-books": {
    "mappings": {
      "title": {
        "full_name": "title",
        "mapping": {
          "title": {
            "type": "text",
            "fields": {
              "date": {
                "type": "date",
                "format": "strict_date_time||strict_date",
                "ignore_malformed": true
              },
              "delimiter": {
                "type": "text",
                "index_options": "freqs",
                "analyzer": "iq_text_delimiter"
              },
              "enum": {
                "type": "keyword",
                "ignore_above": 2048
              },
              "float": {
                "type": "double",
                "ignore_malformed": true
              },
              "joined": {
                "type": "text",
                "index_options": "freqs",
                "analyzer": "i_text_bigram",
                "search_analyzer": "q_text_bigram"
              },
              "location": {
                "type": "geo_point",
                "ignore_malformed": true,
                "ignore_z_value": false
              },
              "prefix": {
                "type": "text",
                "index_options": "docs",
                "analyzer": "i_prefix",
                "search_analyzer": "q_prefix"
              },
              "stem": {
                "type": "text",
                "analyzer": "iq_text_stem"
              }
            },
            "index_options": "freqs",
            "analyzer": "iq_text_base"
          }
        }
      }
    }
  }
}

您会看到 title(书名)字段有数个子字段。date(日期)、float(浮点)和 location(位置)子字段不是文本字段。

现在,我们感兴趣的是如何设置 App Search 所需的文本字段。有好多个字段呢!此文档页面介绍了 App Search 中所用到的文本字段。我们看一下 App Search 为属于 App Search 托管引擎的隐藏式索引所设置的分析器:

GET .ent-search-engine-documents-app-search-books/_settings/index.analysis*
{
  ".ent-search-engine-documents-app-search-books": {
    "settings": {
      "index": {
        "analysis": {
          "filter": {
            "front_ngram": {
              "type": "edge_ngram",
              "min_gram": "1",
              "max_gram": "12"
            },
            "bigram_joiner": {
              "max_shingle_size": "2",
              "token_separator": "",
              "output_unigrams": "false",
              "type": "shingle"
            },
            "bigram_max_size": {
              "type": "length",
              "max": "16",
              "min": "0"
            },
            "en-stem-filter": {
              "name": "light_english",
              "type": "stemmer"
            },
            "bigram_joiner_unigrams": {
              "max_shingle_size": "2",
              "token_separator": "",
              "output_unigrams": "true",
              "type": "shingle"
            },
            "delimiter": {
              "split_on_numerics": "true",
              "generate_word_parts": "true",
              "preserve_original": "false",
              "catenate_words": "true",
              "generate_number_parts": "true",
              "catenate_all": "true",
              "split_on_case_change": "true",
              "type": "word_delimiter_graph",
              "catenate_numbers": "true",
              "stem_english_possessive": "true"
            },
            "en-stop-words-filter": {
              "type": "stop",
              "stopwords": "_english_"
            }
          },
          "analyzer": {
            "i_prefix": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "front_ngram"
              ],
              "tokenizer": "standard"
            },
            "iq_text_delimiter": {
              "filter": [
                "delimiter",
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stop-words-filter",
                "en-stem-filter"
              ],
              "tokenizer": "whitespace"
            },
            "q_prefix": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding"
              ],
              "tokenizer": "standard"
            },
            "iq_text_base": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stop-words-filter"
              ],
              "tokenizer": "standard"
            },
            "iq_text_stem": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stop-words-filter",
                "en-stem-filter"
              ],
              "tokenizer": "standard"
            },
            "i_text_bigram": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stem-filter",
                "bigram_joiner",
                "bigram_max_size"
              ],
              "tokenizer": "standard"
            },
            "q_text_bigram": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stem-filter",
                "bigram_joiner_unigrams",
                "bigram_max_size"
              ],
              "tokenizer": "standard"
            }
          }
        }
      }
    }
  }
}

如果我们想为其他语言(例如挪威语、芬兰语或阿拉伯语)创建一个可在 App Search 中使用的索引,我们也需要类似的分析器。在我们的示例中,我们需要确保词干和停用词筛选器使用罗马尼亚语版本。

回到我们最初的 books 索引,我们要添加正确的分析器。

简单提醒。对于既有索引,分析器是只有在索引关闭时才能进行更改的一类 Elasticsearch 设置。采用这种方法时,由于我们一开始使用的是既有索引,所以我们需要关闭索引,添加分析器,然后再重新打开索引。

注意:作为一种备选方法,您还可以使用正确的映射从头开始重新创建索引,然后对所有文档进行索引。如果这种方法更适合您的用例,您完全可以跳过此指南中有关打开和关闭索引并重新进行索引的部分。

您可以通过运行 POST books/_close 来关闭索引。然后,我们将添加分析器:

PUT books/_settings
{
  "analysis": {
    "filter": {
      "front_ngram": {
        "type": "edge_ngram",
        "min_gram": "1",
        "max_gram": "12"
      },
      "bigram_joiner": {
        "max_shingle_size": "2",
        "token_separator": "",
        "output_unigrams": "false",
        "type": "shingle"
      },
      "bigram_max_size": {
        "type": "length",
        "max": "16",
        "min": "0"
      },
      "ro-stem-filter": {
        "name": "romanian",
        "type": "stemmer"
      },
      "bigram_joiner_unigrams": {
        "max_shingle_size": "2",
        "token_separator": "",
        "output_unigrams": "true",
        "type": "shingle"
      },
      "delimiter": {
        "split_on_numerics": "true",
        "generate_word_parts": "true",
        "preserve_original": "false",
        "catenate_words": "true",
        "generate_number_parts": "true",
        "catenate_all": "true",
        "split_on_case_change": "true",
        "type": "word_delimiter_graph",
        "catenate_numbers": "true"
      },
      "ro-stop-words-filter": {
        "type": "stop",
        "stopwords": "_romanian_"
      }
    },
    "analyzer": {
      "i_prefix": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "front_ngram"
        ],
        "tokenizer": "standard"
      },
      "iq_text_delimiter": {
        "filter": [
          "delimiter",
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stop-words-filter",
          "ro-stem-filter"
        ],
        "tokenizer": "whitespace"
      },
      "q_prefix": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding"
        ],
        "tokenizer": "standard"
      },
      "iq_text_base": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stop-words-filter"
        ],
        "tokenizer": "standard"
      },
      "iq_text_stem": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stop-words-filter",
          "ro-stem-filter"
        ],
        "tokenizer": "standard"
      },
      "i_text_bigram": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stem-filter",
          "bigram_joiner",
          "bigram_max_size"
        ],
        "tokenizer": "standard"
      },
      "q_text_bigram": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stem-filter",
          "bigram_joiner_unigrams",
          "bigram_max_size"
        ],
        "tokenizer": "standard"
      }
    }
  }
}

您可以看到,我们添加了 ro-stem-filter 以使用罗马尼亚语进行词干提取,此筛选器针对特定于罗马尼亚语的词形变化将能够提高搜索相关性。我们包括了罗马尼亚语停用词筛选器 (ro-stop-words-filter),从而确保不会将罗马尼亚语停用词用于搜索目的。

现在我们将通过执行 POST books/_open 来重新打开索引。

3. 更新索引映射以使用分析器

完成分析设置之后,我们就可以修改索引映射了。App Search 使用动态模版 来确保新字段拥有正确的子字段和分析器。对于我们的示例,我们仅会向既有的 titleauthor 字段添加子字段:

PUT books/_mapping
{
  "properties": {
    "author": {
      "type": "text",
      "fields": {
        "delimiter": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "iq_text_delimiter"
        },
        "enum": {
          "type": "keyword",
          "ignore_above": 2048
        },
        "joined": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "i_text_bigram",
          "search_analyzer": "q_text_bigram"
        },
        "prefix": {
          "type": "text",
          "index_options": "docs",
          "analyzer": "i_prefix",
          "search_analyzer": "q_prefix"
        },
        "stem": {
          "type": "text",
          "analyzer": "iq_text_stem"
        }
      }
    },
    "title": {
      "type": "text",
      "fields": {
        "delimiter": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "iq_text_delimiter"
        },
        "enum": {
          "type": "keyword",
          "ignore_above": 2048
        },
        "joined": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "i_text_bigram",
          "search_analyzer": "q_text_bigram"
        },
        "prefix": {
          "type": "text",
          "index_options": "docs",
          "analyzer": "i_prefix",
          "search_analyzer": "q_prefix"
        },
        "stem": {
          "type": "text",
          "analyzer": "iq_text_stem"
        }
      }
    }
  }
}

4. 重新对文档进行索引

books 索引现在已基本就绪,可在 App Search 使用了。

我们只需要确保在我们修改映射之前所索引的文档拥有全部正确字段。因此,我们可以使用 update_by_query 就地运行一次重新索引:

POST books/_update_by_query?refresh
{
  "query": {
    "match_all": {
    }
  }
}

鉴于我们使用的是 match_all 查询,所有既有文档都会予以更新。

通过使用 update by query 请求,我们还可以包含一个脚本参数来定义文档更新方式。

请注意:我们并未更改文档,但我们的确想按原样对既有文档进行重新索引,从而确保文本字段 authortitle 拥有正确的子字段。因此,我们无需在 update by query 请求中包含脚本

我们现在就拥有了一个针对语言进行优化的索引,而且此索引可在 App Search 中搭配 Elasticsearch 引擎使用。在下面的截图中您将会亲眼看到它有什么优点。

我们将会使用书名百年孤独作为参考。罗马尼亚语版的翻译书名是 Un veac de singurătate。请注意这个单词 veac,它是“世纪/百年”这个词对应的罗马尼亚语单词。 我们使用 veac 的复数形式 veacuri 来运行一次搜索。我们将这条数据记录采集到了我们将要查看的两个示例中:

{
  "title": "Un veac de singurătate",
  "author": "Gabriel García Márquez"
}

如果索引未针对语言进行优化,罗马尼亚语书名 Un veac de singurătate 会使用标准分析器进行索引;尽管标准分析器对大部分语言的效果都很好,但有时并不能匹配到相关的文档。搜索 veacuri 后显示没有任何结果,因为这条搜索输入并不匹配数据记录中的任何纯文本。

相关性调整管理字段

然而,使用针对语言进行优化的索引后,当我们搜索 veacuri 的时候,Elastic App Search 就匹配到了罗马尼亚语单词 veac 并返回了我们要找的数据。在“相关性调整”视图中还有精度调整字段。请查看下图中突出显示的部分:

相关性调整 精度调整

所以,完成上述步骤后,我们在 Elastic Enterprise Search 中为我的母语罗马尼亚语添加了支持。您可以复制本指南中所用到的步骤,来创建针对 Elasticsearch 支持的任何其他语言进行优化的索引。如需了解 Elasticsearch 中所支持语言分析器的完整列表,请查看此文档页面。

Elasticsearch 中的分析器是一个令人着迷的话题。如果您有兴趣了解更多内容,下面列出了一些其他资源: