如何明智地结合带状疱疹和edgeNgram来提供灵活的全文搜索?

Val*_*Val 4 regex lucene analyzer odata elasticsearch

我们有一个符合OData的API,它将一些全文搜索需求委托给Elasticsearch集群.由于OData表达式可能变得非常复杂,我们决定将它们简单地转换为等效的Lucene查询语法并将其提供给query_string查询.

我们支持一些与文本相关的OData过滤器表达式,例如:

  • startswith(field,'bla')
  • endswith(field,'bla')
  • substringof('bla',field)
  • name eq 'bla'

我们对匹配字段可以是analyzed,not_analyzed或两者(通过多场IE).所搜索的文本可以是一个单一的令牌(例如table),只有其(例如一部分tab),或数个标记(例如table 1.,table 10等).搜索必须不区分大小写.

以下是我们需要支持的行为的一些示例:

  • startswith(name,'table 1')必须匹配" 表1 "," 表1 00"," 表1 .5"," 表1 12上层"
  • endswith(name,'table 1')必须匹配"房间1,表1 ","子表1 "," 表1 ","杰夫表1 "
  • substringof('table 1',name)必须匹配"Big Table 1 back"," table 1 "," Table 1 ","Small Table1 2"
  • name eq 'table 1'必须匹配" 表1 "," 表1 "," 表1 "

所以基本上,我们接受用户输入(即传递给startswith/ 的第二个参数的内容endswith,或者相应的第一个参数substringof,相应的右侧值eq)并尝试完全匹配它,令牌是否完全匹配或仅部分.

现在,我们正在摆脱下面突出显示的笨拙的解决方案,该解决方案效果很好,但远非理想.

在我们中query_string,我们not_analyzed使用正则表达式语法匹配字段.由于字段是not_analyzed并且搜索必须不区分大小写,我们在准备正则表达式时会自己进行标记,以便提供类似的内容,即这相当于OData过滤器endswith(name,'table 8')(=>匹配)所有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
    }
  }
Run Code Online (Sandbox Code Playgroud)

所以,即使这个解决方案工作得很好而且性能也不差(出乎意料的是),我们希望以不同的方式做到这一点并充分利用分析仪的全部功能,以便在索引时转移所有这些负担时间而不是搜索时间.但是,由于重新索引我们的所有数据需要数周时间,因此我们首先要研究是否有一个令牌过滤器和分析器的良好组合,这将有助于我们实现上面列举的相同搜索要求.

我的想法是理想的解决方案将包含一些明智的混合带状疱疹(即几个令牌在一起)和edge-nGram(即在令牌的开头或结尾匹配).但是,我不确定是否可以使它们一起工作以匹配多个令牌,其中一个令牌可能没有被用户完全输入).例如,如果索引名称字段是"Big Table 123",我需要substringof('table 1',name)匹配它,因此"table"是完全匹配的标记,而"1"只是下一个标记的前缀.

在此预先感谢您分享您的braincells.

更新1:在测试了Andrei的解决方案之后

=>完全匹配(eq)并startswith完美地工作.

A. endswith毛刺

搜索substringof('table 112', name)产量107个文档.搜索更具体的情况,例如endswith(name, 'table 112')yield 1525 docs,同时它应该产生更少的docs(后缀匹配应该是子字符串匹配的子集).检查更深入我发现了一些不匹配,例如"社交俱乐部,表12"(不包含"112")或"订单312"(既不包含"表"也不包含"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的强大功能,这种语法的功能不那么强大且表现力较弱,但这是我们无法真正改变的.不过,我们相当接近!

And*_*fan 9

这是一个有趣的用例.这是我的看法:

{
  "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"
            }
          }
        }
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
  • my_ngram_analyzer用于将每个文本拆分成小块,块的大小取决于您的用例.出于测试目的,我选择了25个字符.lowercase因为你说不区分大小写所以使用.基本上,这是用于的标记器substringof('table 1',name).查询很简单:
{
  "query": {
    "term": {
      "text": {
        "value": "table 1"
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
  • my_edge_ngram_analyzer用于从开头开始拆分文本,这是专门用于startswith(name,'table 1')用例.同样,查询很简单:
{
  "query": {
    "term": {
      "text.starts_with": {
        "value": "table 1"
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
  • 我发现这是最棘手的部分 - 一个用于endswith(name,'table 1').为此,我限定my_reverse_edge_ngram_analyzer其使用keyword一起标记生成器以lowercase及一个edgeNGram过滤器由一个前面和后面reverse滤波.这个标记器基本上做的是在edgeNGrams中分割文本,但边缘是文本的结尾,而不是开头(就像常规一样edgeNGram).查询:
{
  "query": {
    "term": {
      "text.ends_with": {
        "value": "table 1"
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
  • 对于这种name eq 'table 1'情况,一个简单的keyword标记化lowercase器和一个过滤器应该这样做查询:
{
  "query": {
    "term": {
      "text.exact_case_insensitive_match": {
        "value": "table 1"
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

但是,query_string 如果analyzer为其指定了适当的值,可以"模拟" .

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

{
  "query": {
    "query_string": {
      "query": "text.starts_with:(\"table 1\")",
      "analyzer": "lowercase_keyword"
    }
  }
}
Run Code Online (Sandbox Code Playgroud)