首页 > 代码库 > [Elasticsearch] 部分匹配 (四) - 索引期间优化ngrams及索引期间的即时搜索

[Elasticsearch] 部分匹配 (四) - 索引期间优化ngrams及索引期间的即时搜索

本章翻译自Elasticsearch官方指南的Partial Matching一章。


索引期间的优化(Index-time Optimizations)

眼下我们讨论的全部方案都是在查询期间的。它们不须要不论什么特殊的映射或者索引模式(Indexing Patterns)。它们仅仅是简单地工作在已经存在于索引中的数据之上。

查询期间的灵活性是有代价的:搜索性能。

有时,将这些代价放到查询之外的地方是有价值的。在一个实时的Web应用中。一个额外的100毫秒的延迟会难以承受。

通过在索引期间准备你的数据。能够让你的搜索更加灵活并更具效率。你仍然付出了代价:添加了的索引大小和略微低一些的索引吞吐量,可是这个代价是在索引期间付出的,而不是在每一个查询的运行期间。

你的用户会感激你的。


部分匹配(Partial Matching)的ngrams

我们说过:"你仅仅能找到存在于倒排索引中的词条"。虽然prefix,wildcard以及regexp查询证明了上面的说法并非一定正确。可是运行一个基于单个词条的查询会比遍历词条列表来得到匹配的词条要更快是毫无疑问的。

为了部分匹配而提前准备你的数据可以添加搜索性能。

在索引期间准别数据意味着选择正确的分析链(Analysis Chain),为了部分匹配我们选择的工具叫做n-gram。一个n-gram能够被想象成一个单词上的滑动窗体(Moving Window)。

n表示的是长度。假设我们对单词quick得到n-gram。结果取决于选择的长度:

  • 长度1(unigram): [ q, u, i, c, k ]
  • 长度2(bigram): [ qu, ui, ic, ck ]
  • 长度3(trigram): [ qui, uic, ick ]
  • 长度4(four-gram):[ quic, uick ]
  • 长度5(five-gram):[ quick ]

单纯的n-grams对于匹配单词中的某一部分是实用的,在复合单词的ngrams中我们会用到它。然而,对于即时搜索。我们使用了一种特殊的n-grams。被称为边缘n-grams(Edge n-grams)。

边缘n-grams会将起始点放在单词的开头处。

单词quick的边缘n-gram例如以下所看到的:

  • q
  • qu
  • qui
  • quic
  • quick

你或许注意到它遵循了用户在搜索"quick"时的输入形式。换言之,对于即时搜索而言它们是很完美的词条。


索引期间的即时搜索(Index-time Search-as-you-type)

建立索引期间即时搜索的第一步就是定义你的分析链(Analysis Chain)(在配置解析器中讨论过),在这里我们会具体阐述这些步骤:

准备索引

第一步是配置一个自己定义的edge_ngram词条过滤器,我们将它称为autocomplete_filter:

{
    "filter": {
        "autocomplete_filter": {
            "type":     "edge_ngram",
            "min_gram": 1,
            "max_gram": 20
        }
    }
}

以上配置的作用是,对于此词条过滤器接受的不论什么词条,它都会产生一个最小长度为1。最大长度为20的边缘ngram(Edge ngram)。

然后我们将该词条过滤器配置在自己定义的解析器中,该解析器名为autocomplete。

{
    "analyzer": {
        "autocomplete": {
            "type":      "custom",
            "tokenizer": "standard",
            "filter": [
                "lowercase",
                "autocomplete_filter" 
            ]
        }
    }
}

以上的解析器会使用standard分词器将字符串划分为独立的词条,将它们变成小写形式,然后为它们生成边缘ngrams。这要感谢autocomplete_filter。

创建索引,词条过滤器和解析器的完整请求例如以下所看到的:

PUT /my_index
{
    "settings": {
        "number_of_shards": 1, 
        "analysis": {
            "filter": {
                "autocomplete_filter": { 
                    "type":     "edge_ngram",
                    "min_gram": 1,
                    "max_gram": 20
                }
            },
            "analyzer": {
                "autocomplete": {
                    "type":      "custom",
                    "tokenizer": "standard",
                    "filter": [
                        "lowercase",
                        "autocomplete_filter" 
                    ]
                }
            }
        }
    }
}

你能够通过以下的analyze API来确保行为是正确的:

GET /my_index/_analyze?

analyzer=autocomplete quick brown

返回的词条说明解析器工作正常:

  • q
  • qu
  • qui
  • quic
  • quick
  • b
  • br
  • bro
  • brow
  • brown

为了使用它,我们须要将它适用到字段中,通过update-mapping API:

PUT /my_index/_mapping/my_type
{
    "my_type": {
        "properties": {
            "name": {
                "type":     "string",
                "analyzer": "autocomplete"
            }
        }
    }
}

如今,让我们索引一些測试文档:

POST /my_index/my_type/_bulk
{ "index": { "_id": 1            }}
{ "name": "Brown foxes"    }
{ "index": { "_id": 2            }}
{ "name": "Yellow furballs" }

查询该字段

假设你使用一个针对"brown fo"的简单match查询:

GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "name": "brown fo"
        }
    }
}

你会发现两份文档都匹配了。即使Yellow furballs既不包括brown,也不包括fo:

{

  "hits": [
     {
        "_id": "1",
        "_score": 1.5753809,
        "_source": {
           "name": "Brown foxes"
        }
     },
     {
        "_id": "2",
        "_score": 0.012520773,
        "_source": {
           "name": "Yellow furballs"
        }
     }
  ]
}

通过validate-query API来发现问题:

GET /my_index/my_type/_validate/query?

explain { "query": { "match": { "name": "brown fo" } } }

得到的解释说明了查询会寻找查询字符串中每一个单词的边缘ngrams:

name:b name:br name:bro name:brow name:brown name:f name:fo

name:f这一条件满足了第二份文档。由于furballs被索引为f,fu。fur等。

因此。得到以上的结果也没什么奇怪的。autocomplete解析器被同一时候适用在了索引期间和搜索期间,通常而言这都是正确的行为。

可是当前的场景是为数不多的不应该使用该规则的场景之中的一个。

我们须要确保在倒排索引中含有每一个单词的边缘ngrams,可是只匹配用户输入的完整单词(brown和fo)。我们能够通过在索引期间使用autocomplete解析器,而在搜索期间使用standard解析器来达到这个目的。直接在查询中指定解析器就是一种改变搜索期间分析器的方法:

GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "name": {
                "query":    "brown fo",
                "analyzer": "standard" 
            }
        }
    }
}

另外,还能够在name字段的映射中分别指定index_analyzer和search_analyzer。由于我们仅仅是想改动search_analyzer。所以能够在不正确数据重索引的前提下对映射进行改动:

PUT /my_index/my_type/_mapping
{
    "my_type": {
        "properties": {
            "name": {
                "type":            "string",
                "index_analyzer":  "autocomplete", 
                "search_analyzer": "standard" 
            }
        }
    }
}

此时再通过validate-query API得到的解释例如以下:

name:brown name:fo

反复运行查询后,也只会得到Brown foxes这份文档。

由于大部分的工作都在索引期间完毕了。查询须要做的仅仅是查找两个词条:brown和fo,这比使用match_phrase_prefix来寻找全部以fo开头的词条更加高效。

完毕建议(Completion Suggester)

使用边缘ngrams建立的即时搜索是简单,灵活和迅速的。然而。有些时候它还是不够快。延迟的影响不容忽略,特别当你须要提供实时反馈时。有时最快的搜索方式就是没有搜索。

ES中的完毕建议採用了一种截然不同的解决方式。通过给它提供一个完整的可能完毕列表(Possible Completions)来创建一个有限状态转换器(Finite State Transducer),该转换器是一个用来描写叙述图(Graph)的优化数据结构。为了搜索建议。ES会从图的起始处開始,对用户输入逐个字符地沿着匹配路径(Matching Path)移动。

一旦用户输入被检验完成。它就会依据当前的路径产生全部可能的建议。

该数据结构存在于内存中,因此对前缀查询而言是非常迅速的,比不论什么基于词条的查询都要快。使用它来自己主动完毕名字和品牌(Names and Brands)是一个非常不错的选择,由于它们通常都以某个特定的顺序进行组织。比方"Johnny Rotten"不会被写成"Rotten Johnny"。

当单词顺序不那么easy被预測时。边缘ngrams就是相比完毕建议更好的方案。

边缘ngrams和邮政编码

边缘ngrams这一技术还能够被用在结构化数据上,比方本章前面提到过的邮政编码。当然,postcode字段或许须要被设置为analyzed。而不是not_analyzed,可是你仍然能够通过为邮政编码使用keyword分词器来让它们和not_analyzed字段一样。

TIP

keyword分词器是一个没有不论什么行为(no-operation)的分词器。它接受的不论什么字符串会被原样输出为一个词条。所以对于一些通常被当做not_analyzed字段,然而须要某些处理(如转换为小写)的情况下。是实用处的。

这个样例使用keyword分词器将邮政编码字符串转换为一个字符流,因此我们就行利用边缘ngram词条过滤器了:

{
    "analysis": {
        "filter": {
            "postcode_filter": {
                "type":     "edge_ngram",
                "min_gram": 1,
                "max_gram": 8
            }
        },
        "analyzer": {
            "postcode_index": { 
                "tokenizer": "keyword",
                "filter":    [ "postcode_filter" ]
            },
            "postcode_search": { 
                "tokenizer": "keyword"
            }
        }
    }
}


[Elasticsearch] 部分匹配 (四) - 索引期间优化ngrams及索引期间的即时搜索