如何明智地结合 shingles 和 edgeNgram 来提供灵活的全文搜索?
Posted
技术标签:
【中文标题】如何明智地结合 shingles 和 edgeNgram 来提供灵活的全文搜索?【英文标题】:How to wisely combine shingles and edgeNgram to provide flexible full text search? 【发布时间】:2015-08-20 09:28:20 【问题描述】:我们有一个符合 OData 的 API,可将其部分全文搜索需求委托给 Elasticsearch 集群。
由于 OData 表达式可能会变得相当复杂,因此我们决定将它们简单地转换为等效的 Lucene 查询语法并将其输入到 query_string
查询中。
我们确实支持一些与文本相关的 OData 过滤器表达式,例如:
startswith(field,'bla')
endswith(field,'bla')
substringof('bla',field)
name eq 'bla'
我们匹配的字段可以是 analyzed
、not_analyzed
或两者(即通过多字段)。
搜索到的文本可以是单个标记(例如table
)、只是其中的一部分(例如tab
)或多个标记(例如table 1.
、table 10
等)。
搜索必须不区分大小写。
以下是我们需要支持的一些行为示例:
startswith(name,'table 1')
必须匹配“Table 1”、“table 100”、“Table 1.5”、“table 1 12上层”
endswith(name,'table 1')
必须匹配“Room 1, Table 1”、“Subtable 1”、“table 1”、“Jeff 表 1"
substringof('table 1',name)
必须匹配 "Big Table 1 back", "table 1", "Table 1", "Small Table12"
name eq 'table 1'
必须匹配“Table 1”、“TABLE 1”、“table 1”
所以基本上,我们接受用户输入(即传递给startswith
/endswith
的第二个参数的内容,分别是substringof
的第一个参数,分别是@ 的右侧值987654341@) 并尝试完全匹配它,无论令牌完全匹配还是部分匹配。
目前,我们正在使用下面突出显示的笨拙解决方案,该解决方案运行良好,但远非理想。
在我们的query_string
中,我们使用Regular Expression syntax 匹配not_analyzed
字段。由于该字段是not_analyzed
并且搜索必须不区分大小写,因此我们在准备正则表达式以输入查询的同时进行自己的标记化,以便提出类似的内容,即这相当于 OData 过滤器@ 987654345@(=> 匹配所有name
以“table 8”结尾的文档)
"query":
"query_string":
"query": "name.raw:/.*(T|t)(A|a)(B|b)(L|l)(E|e) 8/",
"lowercase_expanded_terms": false,
"analyze_wildcard": true
因此,尽管此解决方案运行良好且性能也不算太差(结果令人惊讶),但我们仍希望以不同的方式进行处理,并利用分析器的全部功能来改变这一切索引时间而不是搜索时间的负担。但是,由于重新索引我们所有的数据需要数周时间,因此我们想首先调查是否有令牌过滤器和分析器的良好组合可以帮助我们实现上面列举的相同搜索要求。
我的想法是,理想的解决方案将包含一些 shingles(即几个令牌在一起)和 edge-nGram(即在令牌的开头或结尾匹配)的明智组合。但是,我不确定是否可以使它们一起工作以匹配多个令牌,其中一个令牌可能没有完全由用户输入)。例如,如果索引名称字段是“Big Table 123”,我需要substringof('table 1',name)
来匹配它,所以“table”是一个完全匹配的token,而“1”只是下一个token的前缀。
提前感谢您在此分享您的脑细胞。
更新 1:测试 Andrei 的解决方案后
=> 完全匹配 (eq
) 和 startswith
完美匹配。
A. endswith
故障
搜索 substringof('table 112', name)
会产生 107 个文档。搜索更具体的情况,例如 endswith(name, 'table 112')
会产生 1525 个文档,而它应该会产生更少的文档(后缀匹配应该是子字符串匹配的子集)。更深入地检查我发现了一些不匹配,例如“Social Club,Table 12”(不包含“112”)或“Order 312”(既不包含“table”也不包含“112”)。我猜这是因为它们以“12”结尾,而这对于标记“112”来说是一个有效的克,因此是匹配的。
B. substringof
故障
搜索substringof('table',name)
匹配“Party table”、“Alex on big table”,但不匹配“Table 1”、“table 112”等。搜索 substringof('tabl',name)
不匹配任何内容
更新 2
这有点暗示,但我忘了明确提及该解决方案必须与 query_string
查询一起使用,主要是因为 OData 表达式(无论它们可能多么复杂)将不断被翻译成它们的Lucene 等价物。我知道我们正在用 Lucene 的查询语法来权衡 Elasticsearch Query DSL 的强大功能,后者的功能和表现力稍差一些,但这是我们无法真正改变的东西。不过,我们已经非常接近了!
更新 3(2019 年 6 月 25 日):
ES 7.2 引入了一种名为search_as_you_type
的新数据类型,它本机允许这种行为。阅读更多:https://www.elastic.co/guide/en/elasticsearch/reference/7.2/search-as-you-type.html
【问题讨论】:
哇,看起来 Lucene 正则表达式就像它得到的一样原始,构造方式非常稀疏,没有大小写标志等。基本上它很烂。您可以构建自己的表达式。示例\d+
将是 [0-9]+
。你甚至可以对展开循环方法有点花哨。
我有或多或少完全相同的要求,只是我从头开始。如果您尝试走这条路,请详细说明您将如何设置和使用search_as_you_type
。
【参考方案1】:
这是一个有趣的用例。这是我的看法:
"settings":
"analysis":
"analyzer":
"my_ngram_analyzer":
"tokenizer": "my_ngram_tokenizer",
"filter": ["lowercase"]
,
"my_edge_ngram_analyzer":
"tokenizer": "my_edge_ngram_tokenizer",
"filter": ["lowercase"]
,
"my_reverse_edge_ngram_analyzer":
"tokenizer": "keyword",
"filter" : ["lowercase","reverse","substring","reverse"]
,
"lowercase_keyword":
"type": "custom",
"filter": ["lowercase"],
"tokenizer": "keyword"
,
"tokenizer":
"my_ngram_tokenizer":
"type": "nGram",
"min_gram": "2",
"max_gram": "25"
,
"my_edge_ngram_tokenizer":
"type": "edgeNGram",
"min_gram": "2",
"max_gram": "25"
,
"filter":
"substring":
"type": "edgeNGram",
"min_gram": 2,
"max_gram": 25
,
"mappings":
"test_type":
"properties":
"text":
"type": "string",
"analyzer": "my_ngram_analyzer",
"fields":
"starts_with":
"type": "string",
"analyzer": "my_edge_ngram_analyzer"
,
"ends_with":
"type": "string",
"analyzer": "my_reverse_edge_ngram_analyzer"
,
"exact_case_insensitive_match":
"type": "string",
"analyzer": "lowercase_keyword"
my_ngram_analyzer
用于将每个文本分成小块,这些块的大小取决于您的用例。出于测试目的,我选择了 25 个字符。使用lowercase
,因为您说不区分大小写。基本上,这是用于substringof('table 1',name)
的标记器。查询很简单:
"query":
"term":
"text":
"value": "table 1"
my_edge_ngram_analyzer
用于从头开始拆分文本,这专门用于startswith(name,'table 1')
用例。同样,查询很简单:
"query":
"term":
"text.starts_with":
"value": "table 1"
我发现这是最棘手的部分 - endswith(name,'table 1')
的部分。为此,我定义了my_reverse_edge_ngram_analyzer
,它使用keyword
标记器和lowercase
和一个edgeNGram
过滤器,前面和后面跟着一个reverse
filter。这个标记器基本上所做的是将文本拆分为 edgeNGrams,但边缘是文本的结尾,而不是开头(就像常规的 edgeNGram
一样)。
查询:
"query":
"term":
"text.ends_with":
"value": "table 1"
对于name eq 'table 1'
情况,一个简单的keyword
标记器和一个lowercase
过滤器应该可以做到
查询:
"query":
"term":
"text.exact_case_insensitive_match":
"value": "table 1"
关于 query_string
,这稍微改变了解决方案,因为我指望term
不分析输入文本并将其与索引中的一个术语完全匹配。
但这可以用query_string
if the appropriate analyzer
is specified for it“模拟”。
解决方案将是一组如下查询(始终使用该分析器,仅更改字段名称):
"query":
"query_string":
"query": "text.starts_with:(\"table 1\")",
"analyzer": "lowercase_keyword"
【讨论】:
安德烈,非常感谢您的建议,非常感谢!我已经更新了我的问题,因为我们同时添加了对eq
的支持(对于不区分大小写的完全匹配)。您将如何处理这种情况?
对于eq
部分,它应该是一个直截了当的keyword
tokenizer + lowercase
过滤器。更新了我的答案。
非常好,感谢您的及时答复!我将根据真实数据彻底解决问题并尽快回到这里,但乍一看我很有信心它会运作良好。敬请期待……
它 99% 的效果都很好,非常有前途的解决方案!完全匹配和startswith
完美匹配,尽管我发现substringof
和endswith
存在一些小故障。我正在用一些结果更新我上面的问题。
嗯,我对一个不匹配有一个解释,但我感觉你不是用term
测试这个,而是用match
或其他东西。更新后的测试中的不同之处在于您正在搜索带有大写字母的文本:-),例如Table 112
。为了匹配索引中的术语,index
和 search
分析器需要分开。对于search
,我在考虑keyword
和lowercase
,而不是使用term
查询,而是使用match
。以上是关于如何明智地结合 shingles 和 edgeNgram 来提供灵活的全文搜索?的主要内容,如果未能解决你的问题,请参考以下文章