regex - 如何明智地结合 shingles 和 edgeNgram 来提供灵活的全文搜索?

标签 regex elasticsearch lucene odata analyzer

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

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

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

  • 我们匹配的字段可以是 analyzednot_analyzed 或两者(即通过多字段)。
    搜索到的文本可以是单个标记(例如 table )、仅其一部分(例如 tab )或多个标记(例如 table 1.table 10 等)。
    搜索必须不区分大小写。

    以下是我们需要支持的一些行为示例:
  • startswith(name,'table 1')必须 “1 12上层表”
  • 匹配 “表1 ”, “表1 00”, “表1 0.5”,
  • endswith(name,'table 1')必须匹配 “房间1,表1 ”, “子表1 ”, “表1 ”, “杰夫表1
  • substringof('table 1',name)必须匹配 “大表1 背”, “表1 ”, “表1 ”, “小表1 2”
  • name eq 'table 1' 必须匹配“ 表 1 ”、“ 表 1 ”、“0x1049 407919 405619 405617

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

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

    在我们的 query_string 中,我们使用 Regular Expression syntax 匹配 not_analyzed 字段。由于该字段是 not_analyzed 并且搜索必须不区分大小写,我们在准备正则表达式以提供给查询的同时进行自己的标记化,以便提出类似的内容,即这等效于 OData 过滤器 endswith(name,'table 8') (= > 匹配所有 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
        }
      }
    

    因此,即使此解决方案运行良好且性能也不算太差(结果出人意料),我们还是希望采用不同的方式并利用分析器的全部功能来转移索引时的所有这些负担时间而不是寻找时间。但是,由于重新索引所有数据需要数周时间,因此我们想首先调查是否存在可以帮助我们实现上述相同搜索要求的 token 过滤器和分析器的良好组合。

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

    提前感谢您分享您的脑细胞。

    更新 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 等价物。我知道我们正在用 Elasticsearch Query DSL 的强大功能与 Lucene 的查询语法进行权衡,Lucene 的查询语法有点不强大,表达能力也不强,但这是我们无法真正改变的。不过,我们非常接近!

    更新 3(2019 年 6 月 25 日):

    ES 7.2 引入了一种名为 search_as_you_type 的新数据类型,它 native 允许这种行为。阅读更多信息:https://www.elastic.co/guide/en/elasticsearch/reference/7.2/search-as-you-type.html

  • 最佳答案

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

    {
      "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"
        }
      }
    }
    

    关于regex - 如何明智地结合 shingles 和 edgeNgram 来提供灵活的全文搜索?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30666371/

    相关文章:

    java - 使用 Java 从文件中提取所有 HTML 标签,包括结束标签,而无需使用 Jsoup 等外部库

    node.js - 正则表达式将 Markdown 中的文本 URL 转换为链接

    ruby-on-rails - 如何在一个查询中按ID和名称搜索产品数据

    groovy - 使用Elasticsearch Groovy动态脚本进行字符串比较

    scala - 如何为 Spark 结构化流编写 ElasticsearchSink

    c# - Lucene:对同一文档多次调用 UpdateDocument 会导致分数不断增加

    java - 基于索引字段的提升

    javascript - html textarea 正则表达式匹配

    regex - 使用 sed 匹配正则表达式

    full-text-search - 无法在 lucene 索引中搜索所有生成的术语