掘金 后端 ( ) • 2024-04-18 14:42

需要采用ES实现 "全文搜索" 👉 没有错 需求就这4个字

想要 中文"分词"搜索 , 同时也需要像"like匹配"那样,不要随意召回大量数据,希望数据精确的匹配上

暂时不要求拼音检索

1.ik分词

通常我们做中文搜索的时候,会选用ik分词器进行ik分词检索,最基础的检索方案如下

  • 1.第一步 创建索引
    PUT test_ik
    
  • 2.第二步 配置映射信息
    • 一般采用ik_max_word来处理写入文档时的分词,搜索时使用ik_smart
    PUT test_a/_mappings
    {
      "dynamic": "runtime",
      "properties": {
        "ik": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        }
    }
    
  • 3.第三步 检查索引并填充数据
    • 验证下是否正确
     GET test_a
    
    • 造数据
    POST test_a/_doc
    {
      "ik": "用户名信息张三李四王五击破完全没考虑"
    }
    POST test_a/_doc
    {
      "ik": "之前在 DSL 中一次问卷调查中,收集到如下几个和搜索类型相关的问题。"
    }
    POST test_a/_doc
    {
      "ik": "本文继续缩小范围,把重心缩小为最常用的:精准匹配检索、全文检索、组合检索三种类型。精准匹配检索和全文检索的本质区别:精准匹配把检索的整个文本不做分词处理,当前一个串整体理。而全文检索需要分词处理,对分词后的每个词单独检索然后大bool组合检索。"
    }
    
  • 4.第四步 检索验证
    • 1.使用多字段全文检索方案
    {
      "query": {
        "bool": {
          "should": [
            {
              "multi_match": {
                "query": "缩小范围"
              }
            }
          ]
        }
      },
      "highlight": {
        "fields": {
          "*": {
            "pre_tags": [
              "<em>"
            ],
            "post_tags": [
              "</em>"
            ]
          }
        }
      }
    }
    
    结果:
    {
        "took": 1039,
        "timed_out": false,
        "_shards": {
            "total": 1,
            "successful": 1,
            "skipped": 0,
            "failed": 0
        },
        "hits": {
            "total": {
                "value": 1,
                "relation": "eq"
            },
            "max_score": 3.5733945,
            "hits": [
                {
                    "_index": "test_a",
                    "_type": "_doc",
                    "_id": "Sx9Z744BYpIYyAE61FNi",
                    "_score": 3.5733945,
                    "_source": {
                        "ik": "本文继续缩小范围,把重心缩小为最常用的:精准匹配检索、全文检索、组合检索三种类型。精准匹配检索和全文检索的本质区别:精准匹配把检索的整个文本不做分词处理,当前一个串整体理。而全文检索需要分词处理,对分词后的每个词单独检索然后大bool组合检索。"
                    },
                    "highlight": {
                        "ik": [
                            "本文继续<em>缩小</em><em>范围</em>,把重心<em>缩小</em>为最常用的:精准匹配检索、全文检索、组合检索三种类型。精准匹配检索和全文检索的本质区别:精准匹配把检索的整个文本不做分词处理,当前一个串整体理。"
                        ]
                    }
                }
            ]
        }
    }
    
    结果可以看到 检索到了ik字段的信息

这样看 ik分词检索似乎真的达到了我们需要的效果。

但是有时候会出现一个情况 ik词库中并没有这个词,你搜索的句子可能是文章中的某一段,但ik召回了匹配更多词的记录。我们希望记录更匹配的排到最前面,然而ik的召回变得并没有那么准确。

这个时候我想到 通过合并simple string query 和 ik 并设置最低分限制避免ssq召回评分为0的数据。 我通过bool来合并

{
  "query": {
    "bool": {
      "should": [
        {
          "multi_match": {
            "query": "缩小范围"
          }
        },
        {
          "simple_query_string": {
            "query": "缩小范围"
          }
        }
      ]
    }
  },
  "highlight": {
    "fields": {
      "*": {
        "pre_tags": [
          "<em>"
        ],
        "post_tags": [
          "</em>"
        ]
      }
    }
  }
}

似乎是成功了...

但是这个时候 出现了情况,对方过来和你说:“我想要空格分词 然后精确的检索这个内容”,我不想召回这么多无关数据。

那么我这个时候第一个想到的就是 match_phrase, 类似于sql的like,那么使用这个方案似乎必须舍弃ik分词,这是最简单的方式将需求处理掉。

Ngram方案

在实现过程中发现了网上有其他的方案,似乎是使用 ngram 来作替代, 那么开始尝试。

1.创建索引

 DELETE /testngram
 PUT /testngram
 {
   "settings": {
     "index": {
       "max_ngram_diff": 10
     },
     "analysis": {
       "analyzer": {
         "ngram_analyzer": {
           "tokenizer": "ngram_tokenizer",
           "filter": [
             "lowercase"
           ]
         }
       },
       "tokenizer": {
         "ngram_tokenizer": {
           "type": "ngram",
           "min_gram": 1,
           "max_gram": 10,
           "token_chars": [
             "letter",
             "digit"
           ]
         }
       },
       "normalizer": {
         "custom_normalizer": {
           "type": "custom",
           "filter": [
             "lowercase",
             "asciifolding"
           ]
         }
       }
     }
   },
   "mappings": {
     "properties": {
       "name": {
         "type": "text",
         "analyzer": "ngram_analyzer"
       },
       "key": {
         "type": "keyword",
         "normalizer": "custom_normalizer"
       }
     }
   }
 }

2.插入数据

 POST /testngram/doc/1
 {
   "name": "Apple iPhone 13",
   "key": "hello"
 }
 
 POST /testngram/doc/2
 {
   "name": "Apple iPhone 14",
   "key": "HEllo"
 }
 
 POST /testngram/doc/3
 {
   "name": "据记者了,郑州文化馆、河南省大河文化艺术中心自2016年以来,通过组织第十一届、第十二届中国郑州国际少林武术节书画展,通过书画展文化艺术搭台,是认真贯彻习中央文艺工作座谈会重要讲话精神,响应文化部开展深入生活、扎根人民主题实践活动。以作品的真善美陶冶人类崇高之襟怀品格,树立中华民族的文化自信,用写意精神推动社会文学艺术的繁荣发展。",
   "key": "worlD"
 }
 
 POST /testngram/doc/4
 {
   "name": "恭喜发财 红包拿来"
 }
 
 POST testngram/_analyze
 {
   "analyzer": "ngram_analyzer",
   "text": "据记者了,郑州文化馆、河南省大河文化艺术中心自2016年以来,通过组织第十一届、第十二届中国郑州国际少林武术节书画展,通过书画展文化艺术搭台,是认真贯彻习中央文艺工作座谈会重要讲话精神,响应文化部开展深入生活、扎根人民主题实践活动。以作品的真善美陶冶人类崇高之襟怀品格,树立中华民族的文化自信,用写意精神推动社会文学艺术的繁荣发展。"
 }
 

3.执行查询

 GET /testngram/_search
 {
   "query": {
     "multi_match": {
       "query": "hello 郑州文 来,通",
       "analyzer": "whitespace"
     }
   }
 }

也可以作为一个方案。

但以上的几种方案于我个人都并不满意,并不能够达到自己想要的效果。

在我目前的学习水平看来中文分词与like匹配似乎是有冲突的,并不能做到二者兼得。

以上就是我在实践过程中对ES的简单使用,如果有更好的处理方案,希望能够分享建议!