仿京东淘宝搜索框实战

搜索是很多内容类app必不可少的功能,而搜索框提示则为用户关键词输入提供了一种引导,一个友好的搜索框提示不仅能提升用户体验,还能帮助用户节省触达商品的时间提升搜索效率。

image
image
image

搜索框功能主要有3部分组成:

  • 智能补全
  • 关联数量
  • 拼写纠错

实现流程

ES官方文档建议通过phrase Suggester实行搜索框的自动补全,但这种查询对中文支持不太友好,经常会不做提示;下面我们通过n-gram来实现符合中国人民使用习惯的提示框。

  • 什么是edge n-gram

假设有一个词hello,普通建索引时,就是把这个词hello放入倒排索引
用户输入h、he时会找不到索引(倒排索引中只有hello),因此匹配失败
而对于输入即搜索这种应用场景,可以使用一种特殊的n-gram,称为边界n-grams (edge n-grams)
所谓的edge n-gram,就是指它会固定从一边开始,进行窗口滑动,每次滑动长度为1,最终的结果取决于 n 的选择长度
以单词hello为例,它的edge n-gram的结果如下:

h
he
hel
hell
hello

因此可以发现到,在使用edge n-gram建索引时,一个单词会生成好几个索引,而这些索引一定是从头开始
这符合了输入即搜索的特性,即是用户打h、he能找到倒排中的索引h、he,而这些索引对应著的数据就是hello

依赖插件:

构建索引库:

PUT st
{

    "mappings" : {
      "dynamic" : "false",
      "properties" : {
        "suggest" : {
          "type" : "text",
          "analyzer" : "autocomplete" ,
          "search_analyzer": "autocomplete_search"
        },
        "weight" : {
          "type" : "integer"
        },
        "count" : {
          "type" : "integer"
        }
      }
    },
    "settings" : {
      "index" : {
        "number_of_shards" : "1",
        "analysis" : {
          "filter" : {
            "length_filter" : {
              "type" : "length",
              "min" : "2"
            },
            "my_pinyin" : {
              "keep_joined_full_pinyin" : "true",
              "lowercase" : "true",
              "keep_original" : "true",
              "remove_duplicated_term" : "true",
              "keep_separate_first_letter" : "false",
              "type" : "pinyin",
              "limit_first_letter_length" : "16",
              "keep_full_pinyin" : "true"
            }
          },
          "analyzer" : {
            "autocomplete_search" : {
              "filter" : [
                "lowercase"
              ],
              "tokenizer" : "search_py"
            },
            "autocomplete" : {
              "filter" : [
                "lowercase",
                "length_filter",
                "my_pinyin"
              ],
              "tokenizer" : "autocomplete"
            } 
          },
          "tokenizer" : {
            "autocomplete" : {

              "min_gram" : "2",
              "type" : "edge_ngram",
              "max_gram" : "16"
            } ,
            "search_py" : {
              "keep_joined_full_pinyin" : "true",
              "keep_none_chinese_in_first_letter " : "true",
              "lowercase" : "true",
              "none_chinese_pinyin_tokenize" : "false",
              "keep_none_chinese_in_joined_full_pinyin" : "true",
              "keep_original" : "true",
              "keep_first_letter" : "true",
              "keep_separate_first_letter" : "false",
              "type" : "pinyin",
              "limit_first_letter_length" : "16",
              "keep_full_pinyin" : "false"
            }
          }
        },
        "number_of_replicas" : "1" 
      }
    }
}

Mapping
  • _id
    文档id, 这里取关键词md5后的字符串作为id,方便更新和删除
  • suggest
    离线任务定时提取用户高频输入的搜索关键词与商品分类、标签信息索引到该字段;其中analyzer与search_analyzer分别使用自定义分词器。
  • weight
    关键词权重;默认取词频,也可以手动指定
  • count
    搜索词关联的商品数量
Setting
  • length_filter
    用来控制分词后的term长度,这里限制为2,当term字符长度<2时会被忽略
  • pinyin
keep_first_letter:这个参数会将词的第一个字母全部拼起来.例如:刘德华->ldh.默认为:true
keep_separate_first_letter:这个会将第一个字母一个个分开.例如:刘德华->l,d,h.默认为:flase.如果开启,可能导致查询结果太过于模糊,准确率太低.
limit_first_letter_length:设置最大keep_first_letter结果的长度,默认为:16
keep_full_pinyin:如果打开,它将保存词的全拼,并按字分开保存.例如:刘德华> [liu,de,hua],默认为:true
keep_joined_full_pinyin:如果打开将保存词的全拼.例如:刘德华> [liudehua],默认为:false
keep_none_chinese:将非中文字母或数字保留在结果中.默认为:true
keep_none_chinese_together:保证非中文在一起.默认为: true, 例如: DJ音乐家 -> DJ,yin,yue,jia, 如果设置为:false, 例如: DJ音乐家 -> D,J,yin,yue,jia, 注意: keep_none_chinese应该先开启.
keep_none_chinese_in_first_letter:将非中文字母保留在首字母中.例如: 刘德华AT2016->ldhat2016, 默认为:true
keep_none_chinese_in_joined_full_pinyin:将非中文字母保留为完整拼音. 例如: 刘德华2016->liudehua2016, 默认为: false
none_chinese_pinyin_tokenize:如果他们是拼音,切分非中文成单独的拼音项. 默认为:true,例如: liudehuaalibaba13zhuanghan -> liu,de,hua,a,li,ba,ba,13,zhuang,han, 注意: keep_none_chinese和keep_none_chinese_together需要先开启.
keep_original:是否保持原词.默认为:false
lowercase:小写非中文字母.默认为:true
trim_whitespace:去掉空格.默认为:true
remove_duplicated_term:保存索引时删除重复的词语.例如: de的>de, 默认为: false, 注意:开启可能会影响位置相关的查询.
ignore_pinyin_offset:在6.0之后,严格限制偏移量,不允许使用重叠的标记.使用此参数时,忽略偏移量将允许使用重叠的标记.请注意,所有与位置相关的查询或突出显示都将变为错误,您应使用多个字段并为不同的字段指定不同的设置查询目的.如果需要偏移量,请将其设置为false。默认值:true

  • autocomplete_search
    查询时使用的分词器
  • autocomplete
    索引时使用的分词器
  • edge_ngram
    edge_ngram是ES自带的token解析器,从min_gram处开始依次分词,当达到max_gram或至文本结尾时停止分词。

插入测试数据

curl -XPUT "http://1.0.0.1:9200/st/_doc/1" -H 'Content-Type: application/json' -d'{   "suggest": "小米手机",  "count":110,"weight":10}'

curl -XPUT "http://1.0.0.1:9200/st/_doc/2" -H 'Content-Type: application/json' -d'{   "suggest": "小米手机新款",  "count":110,"weight":8}'

curl -XPUT "http://1.0.0.1:9200/st/_doc/3" -H 'Content-Type: application/json' -d'{   "suggest": "小米手机 5g",  "count":110,"weight":10}'

curl -XPUT "http://1.0.0.1:9200/st/_doc/4" -H 'Content-Type: application/json' -d'{   "suggest": "小米128g",  "count":110,"weight":6}'

curl -XPUT "http://1.0.0.1:9200/st/_doc/5" -H 'Content-Type: application/json' -d'{   "suggest": "小米袋装",  "count":110,"weight":6}'

curl -XPUT "http://1.0.0.1:9200/st/_doc/6" -H 'Content-Type: application/json' -d'{   "suggest": "华为5g新款",  "count":110,"weight":10}'

curl -XPUT "http://1.0.0.1:9200/st/_doc/7" -H 'Content-Type: application/json' -d'{   "suggest": "华为手机",  "count":110,  "weight":10}'

curl -XPUT "http://1.0.0.1:9200/st/_doc/8" -H 'Content-Type: application/json' -d'{   "suggest": "小蜜蜂", "count":110, "weight":8}'

权重计算

  • 全量同步商城分类及标签数据,这类权重值给到Int最大值
  • 通过spark离线任务增量迭代客户端搜索埋点日志,剔除过长搜索词后进行分组统计,并将sessionid去重后>3的入库

关联商品数量

  • 在增量更新suggest中关键词时会去商品库进行一次聚合查询
  • 为了避免对用户搜索业务的影响,任务会放在凌晨进行
  • 为提升效率,应该使用Elasticsearch的Multi Search接口批量进行count,同时批量更新数据库里建议词的count值
  • 由于商品是实时入库的而关联统计是定期离线执行的,所以在进行相关提示时会显示“约”

定期修正

  • 离线任务会定时对近一月用户输入关键词进行分析统计,将weight值<10的删除,weight>10的入库,以修正增量数据的误差
  • 删除时不要使用delete by query进行删除,这样会非常损耗es的性能,应该使用关键词md5后的id批量删除
  • 由于数据量较大,更新时间比较长,为了不影响搜索性能,这里会先写入到一个临时索引库,通过bulk批量添加到临时索引中,然后通过别名切换来更新;

自动补全效果展示

  • 输入“小米sj”获取【小米手机】相关提示词;支持特殊符号输入(如 H&M服饰),也可全拼音或全汉字输入
GET st/_search
{
  "sort": [
    {
      "weight": {
        "order": "desc"
      }
    }
  ], 
  "query": {
        "multi_match": {
          "query": "小米sj",
          "fuzziness": 1,
          "prefix_length" : 1,
          "fields": ["suggest"]
        }
      }

}

需要注意的是,这里使用fuzziness来模糊匹配提升用户体验,fuzziness=1 允许用户输入一个错别字,并通过prefix_length设置为1来跳过开头首个字符的判断,因为一般用户输入出错大多发生在后面

  • 返回结果
{
        "_index" : "st",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : null,
        "_source" : {
          "suggest" : "小米手机",
          "count" : 110,
          "weight" : 10
        },
        "sort" : [
          10
        ]
      },
      {
        "_index" : "st",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : null,
        "_source" : {
          "suggest" : "小米手机新款",
          "count" : 110,
          "weight" : 8
        },
        "sort" : [
          8
        ]
      }

智能纠错效果展示

  • 如果Elasticsearch返回的是空结果,此时应该需要增加拼写纠错的处理(拼写纠错也可以在调用Elasticsearch搜索的时候带上,但是通常情况下用户并没有拼写错误,所以建议还是在后面单独调用suggester);如果返回的suggest不为空,则根据新的词调用建议词服务;比如用户输入了【小密】,调用Elasticsearch的suggester获取到的结果是【小米】,则再根据小米进行搜索建议词处理
GET st/_search
{
  "_source": false, 
  "size": 0, 
  "suggest": { 
    "term-suggestion": {
      "text": "小密",
     "term": {
       "field": "suggest",
       "min_word_length":2, 
       "prefix_length":1,
       “min_doc_freq”:2,
       "size":2
     }
    }
  }
}

需要注意:
其中min_word_length是用来控制候选词长度的,这里设置为2,意思是当term长度>=2才会被显示;
prefix_length=1表示忽略首字符是错别字,大多数输入错别字发生在后面;
min_doc_freq 当建议词出现文档频率低于该值时将被忽略,线上可适当调大该值以提升搜索效果

  • 返回结果
{
        "text" : "小密",
        "offset" : 0,
        "length" : 0,
        "options" : [
          {
            "text" : "小米",
            "score" : 0.5,
            "freq" : 5
          },
          {
            "text" : "小蜜",
            "score" : 0.5,
            "freq" : 1
          }
        ]
      }

线上效果

易企秀商城

作者:易企秀工程师 Yarn ->个人主页

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容