一尘不染

如何明智地将shingles和edgeNgram结合使用以提供灵活的全文本搜索?

elasticsearch

我们有一个与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')必须匹配“ 表1 ”,“ 表1 00”,“ 表1 .5”,“ 表1 12上层”
  • endswith(name,'table 1')必须与“ Room 1, Table 1 ”,“ Sub table 1 ”,“ table 1 ”,“ Jeff table 1 ” 匹配
  • substringof('table 1',name)必须匹配“大 表1 返回”,“ 表1 ”,“ 表1 ”,“小 1 2”
  • name eq 'table 1'必须匹配“ 表1 ”,“ 表1 ”,“ 表1

因此,基本上,我们接受用户输入(即,传递给startswith/
的第二个参数endswith,分别代表的1st参数和substringof,代表右侧值eq),并尝试完全匹配它们,即令牌是否完全匹配或仅部分。

目前,我们正在使用下面突出显示的笨拙解决方案,该解决方案效果很好,但远非理想。

在我们中query_string,我们not_analyzed使用正则表达式语法匹配字段。由于该字段为not_analyzed,并且搜索必须不区分大小写,因此我们在准备将正则表达式馈入查询时进行了自己的标记化处理,以得出类似这样的信息,即,这等效于OData过滤器endswith(name,'table8')(=> match name以“表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
    }
  }

因此,尽管此解决方案效果很好,并且性能还不错(这出乎意料),但我们还是想做不同的事情,并利用分析仪的全部功能来转移索引时的所有负担。时间而不是搜索时间。但是,由于重新索引所有数据将需要数周的时间,因此我们想首先调查一下是否有令牌过滤器和分析器的良好组合,可以帮助我们达到上述相同的搜索要求。

我的想法是,理想的解决方案应包含带状疱疹(即多个令牌在一起)和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故障

正在搜索substringof('table',name)匹配项“ Party table”,“ Alex on big table”,但不匹配“
Table 1”,“ table 112”等。搜索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


阅读 365

收藏
2020-06-22

共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,'table1')。为此,我定义my_reverse_edge_ngram_analyzer了将分keyword词器与lowercaseedgeNGram过滤器一起使用的过滤reverse。该标记器的基本作用是将文本拆分为edgeNGrams,但edge是文本的结尾,而不是开始(就像常规一样edgeNGram)。查询:
    {
      "query": {
        "term": {
          "text.ends_with": {
            "value": "table 1"
          }
        }
      }
    }
  • 对于这种name eq 'table1'情况,应使用简单的keyword标记lowercase器和过滤器来执行查询:
    {
      "query": {
        "term": {
          "text.exact_case_insensitive_match": {
            "value": "table 1"
          }
        }
      }
    }

关于 query_string ,这稍微改变了解决方案,因为我指望term不分析输入文本,并使其与索引中的术语之一完全匹配。

但是,query_string
如果analyzer为其指定了适当的位置可以“模拟” 。

解决方案将是一组类似以下的查询(始终使用该分析器,仅更改字段名称):

    {
      "query": {
        "query_string": {
          "query": "text.starts_with:(\"table 1\")",
          "analyzer": "lowercase_keyword"
        }
      }
    }
2020-06-22