我们有一个与OData兼容的API,它将某些全文搜索需求委托给Elasticsearch集群。由于OData表达式可能变得非常复杂,因此我们决定将它们简单地转换为等效的Lucene查询语法,并将其输入query_string查询中。
query_string
我们确实支持一些与文本相关的OData过滤器表达式,例如:
startswith(field,'bla')
endswith(field,'bla')
substringof('bla',field)
name eq 'bla'
我们要匹配的字段可以是analyzed,也可以是not_analyzed两者(即通过多字段)。所搜索的文本可以是一个单一的令牌(例如table),仅其一部分(例如tab),或数个标记(例如table 1.,table 10等)。搜索必须不区分大小写。
analyzed
not_analyzed
table
tab
table 1.
table 10
以下是我们需要支持的行为的一些示例:
startswith(name,'table 1')
endswith(name,'table 1')
substringof('table 1',name)
name eq 'table 1'
因此,基本上,我们接受用户输入(即,传递给startswith/ 的第二个参数endswith,分别代表的1st参数和substringof,代表右侧值eq),并尝试完全匹配它们,即令牌是否完全匹配或仅部分。
startswith
endswith
substringof
eq
目前,我们正在使用下面突出显示的笨拙解决方案,该解决方案效果很好,但远非理想。
在我们中query_string,我们not_analyzed使用正则表达式语法匹配字段。由于该字段为not_analyzed,并且搜索必须不区分大小写,因此我们在准备将正则表达式馈入查询时进行了自己的标记化处理,以得出类似这样的信息,即,这等效于OData过滤器endswith(name,'table8')(=> match name以“表8”结尾的所有文档)
endswith(name,'table8')
name
"query": { "query_string": { "query": "name.raw:/.*(T|t)(A|a)(B|b)(L|l)(E|e) 8/", "lowercase_expanded_terms": false, "analyze_wildcard": true } }
因此,尽管此解决方案效果很好,并且性能还不错(这出乎意料),但我们还是想做不同的事情,并利用分析仪的全部功能来转移索引时的所有负担。时间而不是搜索时间。但是,由于重新索引所有数据将需要数周的时间,因此我们想首先调查一下是否有令牌过滤器和分析器的良好组合,可以帮助我们达到上述相同的搜索要求。
我的想法是,理想的解决方案应包含带状疱疹(即多个令牌在一起)和edge-nGram(即在令牌的开头或结尾进行匹配)的某种明智的混合。不过,我不确定的是是否有可能使它们协同工作以匹配多个令牌,而其中一个令牌可能无法由用户完全输入)。例如,如果索引名称字段是“BigTable123”,则需要对其substringof('table 1',name)进行匹配,因此“ table”是完全匹配的标记,而“ 1”只是下一个标记的前缀。
在此先感谢您分享您的脑细胞。
更新1:测试安德烈的解决方案后
=>完全匹配(eq),效果startswith完美。
小endswith故障
搜索substringof('table 112', name)产生107文档。搜索更具体的情况(例如endswith(name, 'table 112')产生1525个文档),而它会产生较少的文档(后缀匹配应该是子字符串匹配的子集)。更深入地检查,我发现了一些不匹配的地方,例如“ SocialClub,Table 12”(不包含“ 112”)或“ Order 312”(既不包含“ table”也不包含“ 112”)。我猜是因为它们以“12”结尾,并且这是令牌“ 112”的有效语法,因此是匹配项。
substringof('table 112', name)
endswith(name, 'table 112')
小substringof故障
正在搜索substringof('table',name)匹配项“ Party table”,“ Alex on big table”,但不匹配“ Table 1”,“ table 112”等。搜索substringof('tabl',name)不匹配任何内容
substringof('table',name)
substringof('tabl',name)
更新2
这有点隐含,但我忘了明确提及该解决方案必须与query_string查询一起使用,主要是因为OData表达式(无论它们可能是多么复杂)将一直转换为它们的Lucene等效项。我知道我们正在使用Lucene的查询语法来权衡ElasticsearchQuery DSL的功能,后者的功能和表现力都稍差一些,但这是我们无法真正改变的。不过,我们已经很接近了!
更新3(2019年6月25日):
ES7.2引入了一种称为的新数据类型search_as_you_type,该数据类型本身就允许这种行为。有关更多信息,请访问:https ://www.elastic.co/guide/en/elasticsearch/reference/7.2/search-as-you- type.html
search_as_you_type
这是一个有趣的用例。这是我的看法:
{ "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
lowercase
{ "query": { "term": { "text": { "value": "table 1" } } } }
my_edge_ngram_analyzer
{ "query": { "term": { "text.starts_with": { "value": "table 1" } } } }
endswith(name,'table1')
my_reverse_edge_ngram_analyzer
keyword
edgeNGram
reverse
{ "query": { "term": { "text.ends_with": { "value": "table 1" } } } }
name eq 'table1'
{ "query": { "term": { "text.exact_case_insensitive_match": { "value": "table 1" } } } }
关于 query_string ,这稍微改变了解决方案,因为我指望term不分析输入文本,并使其与索引中的术语之一完全匹配。
term
但是,query_string 如果analyzer为其指定了适当的位置,则可以“模拟” 。
analyzer
解决方案将是一组类似以下的查询(始终使用该分析器,仅更改字段名称):
{ "query": { "query_string": { "query": "text.starts_with:(\"table 1\")", "analyzer": "lowercase_keyword" } } }