标签: LIKE 搜索

  • 在 Elasticsearch 中高速实现类 LIKE 搜索的部分匹配搜索方法

    在 Elasticsearch 中高速实现类 LIKE 搜索的部分匹配搜索方法

    在工作中,经常听到有人反馈关系型数据库(MySQL、PostgreSQL 等)搜索功能存在 “LIKE 搜索速度慢” 的问题。尤其是在处理大量数据的系统中,LIKE 搜索往往会导致性能下降,搜索响应延迟的问题屡见不鲜。因此,越来越多的案例开始考虑从关系型数据库迁移到 Elasticsearch 来解决这一问题。

    Elasticsearch 是一款能够实现高速、灵活全文搜索的强大搜索引擎。但要充分发挥其性能,恰当的数据设计与查询设计至关重要。

    本文将聚焦于 “如何在 Elasticsearch 中高速实现类似 SQL LIKE 搜索的部分匹配搜索” 进行讲解。

    1. 数据类型的差异

    在 Elasticsearch 中进行字符串搜索前,首先需要理解 keyword 型与 text 型的区别。向 Elasticsearch 中注册字符串型字段时,需预先设定该字段采用哪种数据类型。

    keyword 型:适用于精确字符串匹配

    keyword 型是将字符串以原始形式注册到索引中的数据类型。由于其能高速执行完全匹配搜索、排序、聚合等操作,因此像 ID、商品分类这类短字符串通常会注册为 keyword 型。

    text 型:适用于全文搜索

    text 型擅长自然语言处理与全文搜索。设置为该类型的字段在注册到索引时,会通过分析器(Analyzer)将文本按单词或短语拆分后再进行注册。

    文本按何种规则拆分为令牌(Token)由分析器决定,需根据搜索需求设计合适的分析器。

    参考文档:analyzer | Elasticsearch Guide [8.16] | Elastic

    我们可使用分析 API(Analyze API)确认字符串是如何被拆分令牌并注册的。以下是使用默认分析器(standard Analyzer)进行令牌化的示例。

    POST _analyze
    {
      "text":     "Elasticsearch is powerful"
    }
    

    从结果可以看出,文本被拆分为 “elasticsearch”“is”“powerful” 三个令牌。

    {
      "tokens": [
        {
          "token": "elasticsearch",
          "start_offset": 0,
          "end_offset": 13,
          "type": "<ALPHANUM>",
          "position": 0
        },
        {
          "token": "is",
          "start_offset": 14,
          "end_offset": 16,
          "type": "<ALPHANUM>",
          "position": 1
        },
        {
          "token": "powerful",
          "start_offset": 17,
          "end_offset": 25,
          "type": "<ALPHANUM>",
          "position": 2
        }
      ]
    }
    

    通常情况下,部分匹配搜索的目标字段会注册为 text 型。

    2. 实现部分匹配搜索的方法及特点

    keyword 型字段的部分匹配搜索

    对于 keyword 型字段,也可使用 wildcard 查询实现部分匹配搜索。wildcard 查询与 LIKE 搜索类似,是按特定模式匹配字符串的查询,其使用方式与 SQL 的 LIKE 搜索直观上较为接近。

    但需注意,wildcard 查询的计算成本极高,索引规模越大,对搜索系统产生不良影响的可能性就越高。

    wildcard 查询示例(搜索以 “Elasticsearch” 开头的字符串)

    GET test_index/_search
    {
      "query": {
        "wildcard": {
          "message": {
            "value": "Elasticsearch*"
          }
        }
      }
    }
    

    text 型字段的部分匹配搜索

    对 text 型字段进行部分匹配搜索时,通常使用 match 查询或 match_phrase 查询。与 text 型字段拆分令牌后注册到索引的逻辑相同,搜索字符串也会被拆分令牌,只要目标字段与搜索字符串的令牌能够匹配,对应的结果就会命中。其中,match 查询适用于单词搜索,match_phrase 查询适用于短语搜索。

    match 查询示例(搜索以 “Elasticsearch” 开头的字符串)

    GET test_index/_search
    {
      "query": {
        "match": {
          "message": "Elasticsearch"
        }
      }
    }
    

    3. 恰当的分析器设计

    在 Elasticsearch 中进行字符串搜索时,虽然默认使用 match 或 match_phrase 查询,但如果分析器设置不当,可能会出现无法返回预期结果、反而返回大量无关结果等问题。

    例如,在第 1 部分的示例中,若向已注册的 test_index 搜索 “power” 字符串,将无法命中结果。

    搜索 “Elastic” 的查询

    GET test_index/_search
    {
      "query": {
        "match": {
          "message": "power"
        }
      }
    }
    

    搜索结果

    {
      "took": 0,
      "timed_out": false,
      "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": {
          "value": 0,
          "relation": "eq"
        },
        "max_score": null,
        "hits": []
      }
    }
    

    无法命中的原因是,索引中注册的令牌为 “elasticsearch”“is”“powerful”,而 “power” 这一令牌并未被注册。因此,要实现预期的搜索结果,必须分别合理设计:

    • 数据注册时使用的分析器
    • 搜索字符串使用的分析器

    4. 对 text 型字段实现类 LIKE 从句的搜索方法

    要对 text 型字段实现类似 LIKE 从句的 “搜索包含特定模式字符串” 的功能,可通过对应用了包含 N-gram 令牌生成器(tokenizer)的分析器的字段执行 match_phrase 查询来实现。

    使用 N-gram 令牌生成器时,注册的字符串会被机械地按任意长度拆分令牌。例如,将 “Elasticsearch” 这一字符串按 2 个字符(bi-gram)拆分 N-gram 令牌,会得到 “El”“la”“as”……“rc”“ch” 等令牌,这些令牌会被注册到索引中。

    可在创建索引时按如下方式设置分析器,为特定字段配置包含 N-gram 令牌生成器的分析器。

    PUT test_index
    {
      "mappings": {
        "properties": {
          "message": {
            "type": "text",
            "analyzer": "my_analyzer"
          }
        }
      },
      "settings": {
        "analysis": {
          "analyzer": {
            "my_analyzer": {
              "tokenizer": "my_tokenizer"
            }
          },
          "tokenizer": {
            "my_tokenizer": {
              "type": "ngram",
              "min_gram": 2,
              "max_gram": 2
            }
          }
        }
      }
    }
    

    即便使用 match_phrase 搜索 “lastic”,搜索字符串也会被拆分为 “la”“as”“st”“ti”“ic” 等令牌,只有包含所有这些令牌的字符串才会命中。通过这种方式,即可实现相当于 LIKE 搜索的部分匹配搜索。

    GET test_index/_search
    {
      "query": {
        "match_phrase": {
          "message": "lastic"
        }
      }
    }
    
    {
      "took": 1,
      "timed_out": false,
      "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": {
          "value": 1,
          "relation": "eq"
        },
        "max_score": 1.1507283,
        "hits": [
          {
            "_index": "test_index",
            "_id": "ESDup5MBGJks9_KvUZZQ",
            "_score": 1.1507283,
            "_source": {
              "message": "Elasticsearch is powerful"
            }
          }
        ]
      }
    }

    5. 总结

    本次介绍了在 Elasticsearch 中实现类 LIKE 搜索的部分匹配方法。

    若仅需实现部分匹配搜索,使用 wildcard 查询即可达成,但从系统运行时的搜索性能等角度考量,尽管需要提前进行分析器(Analyzer)设置等额外工作,采用 N-gram 结合 match_phrase 查询仍是基础且推荐的方案

    当然,根据具体的搜索需求,还需要进行更细致的分析器及查询设计。尤其在搜索性能与搜索精度之间如何取得平衡,是运用 Elasticsearch 过程中无法回避的关键问题。希望本文能为 Elasticsearch 的搜索设计提供参考。