算法高级(32)-搜索引擎中的自动补全功能该如何实现?

前一章讲了搜索中的拼写纠错功能,里面一个很重要的概念就是莱文斯坦距离。这章会讲解搜索中提升用户体验的另一项功能-自动补全。本章直接介绍ES中的实现方式以及真正的搜索引擎对自动补全功能的优化。

大家对上面的这个应该都不陌生,搜索引擎会根据你输入的关键字进行一些提示,这样用户只需要输入部分内容就可以进行选择了。尤其在移动端会比较方便。淘宝、京东的搜索也有类似的功能,只不过行业不同,提示出来的内容也不同罢了。

一、Lucene中的搜索建议

1.使用步骤:

  1. 导入lucene-suggest组件
  2. 指定联想数据来源,lucene suggest提供了几个InputIteratior的默认实现,也可以自定义实现
    1. BufferedInputIterator:对二进制类型的输入进行轮询; 
    2. DocumentInputIterator:从索引中被store的field中轮询; 
    3. FileIterator:从文件中每次读出单行的数据轮询,以\t进行间隔(且\t的个数最多为2个); 
    4. HighFrequencyIterator:从索引中被store的field轮询,忽略长度小于设定值的文本; 
    5. InputIteratorWrapper:遍历BytesRefIterator并且返回的内容不包含payload且weight均为1; 
    6. SortedInputIterator:二进制类型的输入轮询且按照指定的comparator算法进行排序;

InputIteratior中几个方法的作用:

weight():此方法设置某个term的权重,设置的越高suggest的优先级越高;
payload():每个suggestion对应的元数据的二进制表示,我们在传输对象的时候需要转换对象或对象的某个属性为BytesRef类型,相应的suggester调用lookup的时候会返回payloads信息;
hasPayload():判断iterator是否有payloads;
contexts():获取某个term的contexts,用来过滤suggest的内容,如果suggest的列表为空,返回null
hasContexts():获取iterator是否有contexts;
  1. 建立suggest索引:suggester.build(new InputIteratorWrapper{});//根据InputIterator的具体实现决定数据源以及创建索引的规则
  2. 索引建立完毕即可在索引上进行查询,输入模糊的字符,Lucene suggest的内部算法会根据索引的建立规则提出suggest查询的内容。suggester.lookup(name, contexts, 2, true, false);

2.Lucene suggest核心实现一览

Lucene 使用AnalyzingInfixSuggester类中的lookup方法去联想数据来源进行查询,其实就是一个普通的search,所以我们的关键是要维护好这个联想数据来源,各行各业都应该有自己单独的语料库。

二、Elastic Search中的搜索优化策略

应该从这几个方面入手:怎么优化Suggest词库、提升Suggest词准确率、怎么提高响应速度

1.Suggest词库获取

  • 冷启动可以从内容中提取热词数据来解决,或者人工设置
  • 挖掘搜索日志:
    • 挖掘近1个月搜索日志,按照每天独立IP进行统计频次,即每个IP用户天搜索同一关键词多次只记一次,用IP过滤也有其局限性,伪IP,动态IP,局域网共享同一公网IP,都会影响到基于IP来判断用户的准确性,你也可以使用sessionId或者userId来判断
    • 统计后搜索词频次之后,抽取搜索频次>100(自定阈值)的词,同时对日志数据进行清洗,过滤去除大于10个字(去除太长的长尾词),单字和符号内容
    • 定时更新suggest词库中。
  • 搜索日志里面包含大量 误输入词:
    1. 需要在suggest词库里面去掉误输入词,对于搜索频次高的词,可以挖掘其对应的正确词,通过同义词进行查询改写。
    2. 误输入词同义词挖掘可以通过挖掘搜索session序列,使用word2vec训练来获取误输入词的同义词,通过分词器同义词设置,对误输入词进行查询改写。

2.提升Suggest词准确率

  • 使用fuzzy模糊查询:基于编辑距离算法来匹配文档。编辑距离的计算基于我们提供的查询词条和被搜索文档。
  • 排序:从搜索日志挖掘的Suggest词,可以根据搜索词的搜索频次作为热度来设置weight,Suggest会根据weight来排序。

3.提升响应速度

当使用completion suggester的时候, 不是用于完成 类似于 "关键词"这样的模糊匹配场景,而是用于完成关键词前缀匹配的。 对于汉字的处理,无需使用ik/ HanLP一类的分词器,直接使用keyword analyzer,配合去除一些不需要的stop word即可。

代码参考:

        LinkedHashSet<String> returnSet = new LinkedHashSet<>();
        Client client = elasticsearchTemplate.getClient();
        SuggestRequestBuilder suggestRequestBuilder = client.prepareSuggest(elasticsearchTemplate.getPersistentEntityFor(SuggestEntity.class).getIndexName());
        //全拼前缀匹配
        CompletionSuggestionBuilder fullPinyinSuggest = new CompletionSuggestionBuilder("full_pinyin_suggest")
                .field("full_pinyin").text(input).size(10);
        //汉字前缀匹配
        CompletionSuggestionBuilder suggestText = new CompletionSuggestionBuilder("suggestText")
                .field("suggestText").text(input).size(size);
        //拼音搜字母前缀匹配
        CompletionSuggestionBuilder prefixPinyinSuggest = new CompletionSuggestionBuilder("prefix_pinyin_text")
                .field("prefix_pinyin").text(input).size(size);
        suggestRequestBuilder = suggestRequestBuilder.addSuggestion(fullPinyinSuggest).addSuggestion(suggestText).addSuggestion(prefixPinyinSuggest);
        SuggestResponse suggestResponse = suggestRequestBuilder.execute().actionGet();
        Suggest.Suggestion prefixPinyinSuggestion = suggestResponse.getSuggest().getSuggestion("prefix_pinyin_text");
        Suggest.Suggestion fullPinyinSuggestion = suggestResponse.getSuggest().getSuggestion("full_pinyin_suggest");
        Suggest.Suggestion suggestTextsuggestion = suggestResponse.getSuggest().getSuggestion("suggestText");
        List<Suggest.Suggestion.Entry> entries = suggestTextsuggestion.getEntries();
        //汉字前缀匹配
        for (Suggest.Suggestion.Entry entry : entries) {
            List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
            for (Suggest.Suggestion.Entry.Option option : options) {
                returnSet.add(option.getText().toString());
            }
        }
        //全拼suggest补充
        if (returnSet.size() < 10) {
            List<Suggest.Suggestion.Entry> fullPinyinEntries = fullPinyinSuggestion.getEntries();
            for (Suggest.Suggestion.Entry entry : fullPinyinEntries) {
                List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
                for (Suggest.Suggestion.Entry.Option option : options) {
                    if (returnSet.size() < 10) {
                        returnSet.add(option.getText().toString());
                    }
                }
            }
        }
        //首字母拼音suggest补充
        if (returnSet.size() == 0) {
            List<Suggest.Suggestion.Entry> prefixPinyinEntries = prefixPinyinSuggestion.getEntries();
            for (Suggest.Suggestion.Entry entry : prefixPinyinEntries) {
                List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
                for (Suggest.Suggestion.Entry.Option option : options) {
                    returnSet.add(option.getText().toString());
                }
            }
        }
        return new ArrayList<>(returnSet);

三、搜索引擎对搜索提示的优化

搜索引擎的优化,需要更智能,每个人输入相同的关键字,提示出来的内容可能是完全不相同的,这就是所谓的“千人千面”。这就用到了数据分析的知识,可以根据用户一段时间内的搜索历史,分析用户的搜索习惯,结合语料库实现对用户的精准提示。跟输入法的提升功能类似,会根据你过往的输入文本进行自动提示。所以,你付出了隐私,得到的是更大的便捷。这也是没有办法的事情。

四、搜索提示的一点小总结(重点)

  1. 需要一个搜索词库/语料库,各行各业均应该不同
  2. 对用户输入的关键字进行分词
  3. 根据分词及其他搜索条件去语料库中查询若干条(百度是10条)记录返回
  4. 为了提升准确率,通常都是前缀搜索
  5. 会根据莱温斯坦距离进行拼写纠错

五、搜索引擎中的智能提示API调用

如你所见,各大搜索引擎都提供了智能提示的API供广大用户调用,如果你司没有自研的能力,可以直接js中跨域调用,先把系统跑起来再说,给大家提供主流搜索引擎的调用地址,包含电商的哦。

1.搜索引擎JSONP调用接口

提示:URL中的 #content# 为搜索的 关键字

谷歌(Google)

http://suggestqueries.google.com/complete/search?client=youtube&q=#content#&jsonp=window.google.ac.h

callback:window.google.ac.h

window.google.ac.h(["关键字",[["关键字",0],["关键字 歌词",0],["关键字参数",0],["关键字 lyrics",0],["关键字过滤",0],["关键字排名",0],["关键字查询",0],["关键字提取算法",0],["关键字规划师可通过以下哪种方式帮助您制作新的搜索网络广告系列",0],["关键字优化",0]],{"k":1,"q":"uhaB8ZMjzJay-BACee_C0eVdUCA"}])

必应(Bing)

http://api.bing.com/qsonhs.aspx?type=cb&q=#content#&cb=window.bing.sug

 callback:window.bing.sug

if(typeof window.bing.sug == 'function') window.bing.sug({"AS":{"Query":"关键字","FullResults":0}} /* pageview_candidate */);

百度(Baidu)

http://suggestion.baidu.com/su?wd=#content#&cb=window.baidu.sug

callback:window.baidu.sug

window.baidu.sug({q:"关键字",p:false,s:["关键字搜索排名","关键字怎么优化","关键字查询工具","关键字推广","关键词优化","关键词排名","关键字 英文","关键词挖掘","关键词查询","关键词搜索"]});

好搜(So)

https://sug.so.360.cn/suggest?encodein=utf-8&encodeout=utf-8&format=json&word=#content#&callback=window.so.sug

callback:window.so.sug

window.so.sug({"query":"关键字","result":[{"word":"关键字查询"},{"word":"关键字工具"},{"word":"关键字查询工具"},{"word":"关键字挖掘"},{"word":"关键字搜索"},{"word":"关键字英文"},{"word":"关键字是什么"},{"word":"关键字广告"},{"word":"关键字分析"},{"word":"关键字规划师"}],"version":"b","rec":""});

搜狗(Sogou)

 https://www.sogou.com/suggnew/ajajjson?type=web&key=#content#

 callback:window.sogou.sug

window.sogou.sug(["关键字",["关键字查询","关键字搜索","关键字优化","关键字规划师","关键字查询lol","关键字是什么意思","关键字搜索工具","关键字广告图片","关键字排名查询","关键字生成器"],["0;0;0;0","1;0;0;0","2;0;0;0","3;0;0;0","4;0;0;0","5;0;0;0","6;0;0;0","7;0;0;0","8;0;0;0","9;0;0;0"],["","","","","","","","","",""],["0"],"","suglabId_1"],-1);

 淘宝(Taobao)

 https://suggest.taobao.com/sug?code=utf-8&q=#content#&callback=window.taobao.sug

 callback:window.taobao.sug

window.taobao.sug({"result":[["关键字推广","204"],["关键字seo","198"],["关键字 网站","182"],["关键字搜索","119"],["关键字软件","44"],["关键字首页","50"],["关键字收录","35"],["关键字采集","16"],["关键字采集器","10"],["网站关键字","180"]]})

2.搜索建议使用方式

以百度为例,API返回的是JSONP数据,JSONP是跨域访问的一种方式。由于服务器返回的JavaScript代码可以直接引用,通过回调函数的方式就可以间接的获取服务器的数据。

 下面是一个回调搜索建议的例子,window.baidu.sug 返回的是一个json对象:

    <script type="text/javascript">
            window.onload = function() {
                
                //组装查询地址
                var sugurl = "http://suggestion.baidu.com/su?wd=#content#&cb=window.baidu.sug";
                var content = "关键字";
                sugurl = sugurl.replace("#content#", content);

                //定义回调函数
                window.baidu = {
                    sug: function(json) {
                        console.log(json)
                    }
                }

                //动态添加JS脚本
                var script = document.createElement("script");
                script.src = sugurl;
                document.getElementsByTagName("head")[0].appendChild(script);

            }
        </script>

控制台打印的结果:如果要将结果保存在一个字符串数组中,只需要 var arr = json.s 即可。


我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!

参考文章

  1. https://www.cnblogs.com/woider/p/5805248.html
  2. https://blog.csdn.net/m0_37556444/article/details/82734959
  3. https://www.jianshu.com/p/69d56f9c0576
  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十步杀一人_千里不留行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值