zoukankan      html  css  js  c++  java
  • 搜索推荐系统根据用户搜索频率(热搜)排序

    之前写的三叉树,有点儿简单,并不能满足实际项目的需要。先简单分析一下solr中搜索推荐系统的核心算法。

      wiki中有关于solr的搜索推荐的详细描述,但是核心算法需要自己查看源代码。关于wiki上的解读,之前做了一次简单的翻译,根据此文档,详细研读了源代码,先把核心思想呈现出来。

       基本流程如下:当用户输入搜索词语前缀时,通过前端调用solr的suggest,找到Suggeser对象,Suggester根据匹配的field从主索引库中读取field下面的terms,来构建dictionry,由于主索引库中的terms是经过合并和排序的,索引在构建三叉树的时候,省去了用pinyin4j组件进行排序的过程。接下来,就是通过对字典的折中处理,来实现自平衡的三叉树,以提高检索效率。三叉树构建完之后,进行前缀匹配查询,搜索出所有符合要求的词元,然后加入到优先级队列中,构建有限容量的堆,调整堆顶的值为最小。之所以Lucene自己写了PriorityQueue<T>,而不用jdk自身的,是因为jdk的PriorityQueue<T>,容量可以扩展的,他会把所有匹配出来的词元都加进去,然后输出top N词元,这明显是内存浪费。之前的一篇关于从海量数据中,查找出top N数据的博客中,已经阐述了堆排序的思想,不赘述。最后通过优先级队列输出结果。

      Suggester---Lookup----LookupImpl(TSTLookup、JaSpellLookup、FSTLookup),之前研读的是TSTLookup。排序的核心思想是:构建完字典之后,得到Dictionry对象,由Dictionary对象得到InputIterator,对字典进行扫描读取,能读取到两个变量:一个为term,另一个为term的权重,排序用的。对字典扫描结束后,把terms和weight分别加载到两个list中,以便插入三叉树中。那么,三叉树节点对象的设计,就很重要了。封装以下属性:storedChar、val(weight)、token(最后节点存储的term成词)。插入的具体逻辑,自己对上次的写的三叉树,进行了改进,代码如下:

    package chinese.utility.ternaryTree;
    /**
    * 三叉树节点
    * @author TongXueQiang
    * @date 2016/03/12
    * @since JDK 1.7
    */
    public class TernaryNode {
    public char storedChar; //节点存储的单个字符
    public String token;//最后一个节点存储的term(成词)
    public TernaryNode leftNode,centerNode,rightNode;//子节点

    public TernaryNode (char storedChar) {
    this.storedChar = storedChar;
    }
    }

    package chinese.utility.ternaryTree;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Stack;

    /**
    * 自定义三叉树

    * @author TongXueQiang
    * @date 2016/03/12
    * @since JDK 1.7
    */
    public class TernaryTree {
    // 根节点,不存储字符
    private static TernaryNode root = new TernaryNode('');

    /**
    * 向树中插入字符

    * @param TernaryNode
    * @param word
    * @return
    */
    public void insert(String word) {
    root = insert(root, word, 0);
    }

    public TernaryNode insert(TernaryNode currentTernaryNode, String word, int index) {
    if (word == null || word.length() < index) {
    return currentTernaryNode;
    }
    char[] charArray = word.toCharArray();

    if (currentTernaryNode == null) {
    currentTernaryNode = new TernaryNode(charArray[index]);
    }

    if (currentTernaryNode.storedChar > charArray[index]) {
    currentTernaryNode.leftNode = insert(currentTernaryNode.leftNode, word, index);
    } else if (currentTernaryNode.storedChar < charArray[index]) {
    currentTernaryNode.rightNode = insert(currentTernaryNode.rightNode, word, index);
    } else {
    if (index != word.length() - 1) {
    currentTernaryNode.centerNode = insert(currentTernaryNode.centerNode, word, index + 1);
    } else {
    currentTernaryNode.token = word;
    }
    }

    return currentTernaryNode;
    }

    /**
    * 查找以指定前缀开头的所有字符串

    * @param prefix
    * @return
    */
    public List<TernaryNode> prefixCompletion(String prefix) {
    // 首先查找前缀的最后一个节点
    TernaryNode TernaryNode = findNode(prefix);
    return prefixCompletion(TernaryNode);
    }

    public List<TernaryNode> prefixCompletion(TernaryNode p) {
    List<TernaryNode> suggest = new ArrayList<TernaryNode>();

    if (p == null) return suggest;
    if ((p.centerNode == null) && (p.token == null)) return suggest;
    if ((p.centerNode == null) && (p.token != null)) {
    suggest.add(p);
    return suggest;
    }

    if (p.token != null) {
    suggest.add(p);
    }

    p = p.centerNode;

    Stack<TernaryNode> s = new Stack<TernaryNode>();
    s.push(p);
    while (!s.isEmpty()) {
    TernaryNode top = s.pop();

    if (top.token != null) {
    suggest.add(top);
    }
    if (top.centerNode != null) {
    s.push(top.centerNode);
    }
    if (top.leftNode != null) {
    s.push(top.leftNode);
    }
    if (top.rightNode != null) {
    s.push(top.rightNode);
    }
    }
    return suggest;
    }

    /**
    * 查找前缀的下一个centerTernaryNode,作为searchPrefix的开始节点

    * @param prefix
    * @return
    */
    public TernaryNode findNode(String prefix) {
    return findNode(root, prefix, 0);
    }

    private TernaryNode findNode(TernaryNode TernaryNode, String prefix, int index) {
    if (prefix == null || prefix.equals("")) {
    return null;
    }
    if (TernaryNode == null) {
    return null;
    }
    char[] charArray = prefix.toCharArray();
    // 如果当前字符小于当前节点存储的字符,查找左节点
    if (charArray[index] < TernaryNode.storedChar) {
    return findNode(TernaryNode.leftNode, prefix, index);
    }
    // 如果当前字符大于当前节点存储的字符,查找右节点
    else if (charArray[index] > TernaryNode.storedChar) {
    return findNode(TernaryNode.rightNode, prefix, index);
    } else {// 相等
    // 递归终止条件
    if (index !=charArray.length - 1) {
    return findNode(TernaryNode.centerNode, prefix, ++index);

    }
    return TernaryNode;
    }
    }

    改进后的三叉树,前缀匹配查找后得到结果不再是简单的字符串,而是节点对象,里面封装了匹配的term和weight值。接下来,把得到的suggest加入得到优先级队列中,得到升序的结果。测试代码如下:

    package chinese.utility.test;

    import java.util.Stack;

    import chinese.utility.utils.PriorityQueue;

    /**
    * 输出topN,用Lucene的PriorityQueue,思路和之前写过的堆排序类似,队列中容量为N,
    * 当向队列中插入的时候,调整堆,让堆顶元素最小,当超过容量的时候,在插入的时候,如果插入
    * 的元素比堆顶元素大,则替换之,如果小,废弃之。最后按升序排列输出topN.要实现lessThan
    * 方法,确定比较的项目,比如输入token的权重weight.
    * @author hadoop
    * @date 2016/03/11
    */
    public class MyPriorityQueue extends PriorityQueue<Object> {

    public MyPriorityQueue(int num) {
    super(num);
    }

    @Override
    protected boolean lessThan(Object a, Object b) {
    return (Integer)a < (Integer)b;
    }

    public static void main(String[] f) {
    MyPriorityQueue priQueue = new MyPriorityQueue(3); 
    priQueue.insertWithOverflow(100);
    priQueue.insertWithOverflow(98);
    priQueue.insertWithOverflow(84);
    priQueue.insertWithOverflow(78);
    // 打印结果,输出前三个最大值,按降序排列
    Stack<Integer> stack = new Stack<Integer>(); 
    Integer v = (Integer) priQueue.pop();

    while (v != null) {
    stack.push(v);
    v = (Integer) priQueue.pop();
    }

    Integer num = stack.pop();
    while (num != null) {
    System.out.println(num);
    if (stack.isEmpty()) break;
    num = stack.pop();
    }
    }
    }

    结果是:

    100
    98
    84

    然而,上述排序是根据term的词库频率,并不是根据用户的搜索频率,没有实现热搜。要想实现热搜,需要用另一个方案。之前的一篇博客中,有这么一道题目:用户在进行搜索时,搜索引擎的日志,会记录下用户的搜索轨迹。比如有一篇文档,里面有千万级的数量,去除重复后可能就有300万行,如何从中找出top 10的搜索字符串?之前只是写了思路,没有给出具体实现。实际项目中,需要借鉴这个思路,重新整理一下:

    第一步:对文本进行排序和折中处理,更新文本,要要用到pinyin4j项目包;

    第二步:把更新后的字典,加载到三叉树中,实现平衡的三叉树,自定义的三叉树要增加节点字符出现次数的变量,以便实现词频统计;

    第三步:遍历字典,每次读到的词语,用三叉树查询,得到频率,然后把读到的词语和频率写到另一个文件中,用空格分开,类似于Key-value键值对形式;

    第四步:和从海量数据中查找出前k个最小或最大值的算法(java)问题雷同,从海量数据中查找出前10个最小值;

    第五步:得到最小频率值的堆后,从新的文本中找到对应的词语,加入到set中,统一频率的词语会有很多,而不是一个。

    在实际项目中,如何记录用户的搜索频率?上述思路只能作为借鉴,看一下美团是如何做的:

    考虑专门为关键字建立一个索引collection,利用solr前缀查询实现。solr中的copyField能很好解决我们同时索引多个字段(汉字、pinyin, abbre)的需求,且field的multiValued属性设置为true时能解决同一个关键字的多音字组合问题。配置如下:

    schema.xml:
    
    <field name="kw" type="string" indexed="true" stored="true" />  
    <field name="pinyin" type="string" indexed="true" stored="false" multiValued="true"/>
    <field name="abbre" type="string" indexed="true" stored="false" multiValued="true"/>
    <field name="kwfreq" type="int" indexed="true" stored="true" />
    <field name="_version_" type="long" indexed="true" stored="true"/>
    <field name="suggest" type="suggest_text" indexed="true" stored="false" multiValued="true" />
    ------------------multiValued表示字段是多值的-------------------------------------
    <uniqueKey>kw</uniqueKey>
    <defaultSearchField>suggest</defaultSearchField>
    
    说明:
    kw为原始关键字
    pinyin和abbre的multiValued=true,在使用solrj建此索引时,定义成集合类型即可:如关键字“重庆”的pinyin字段为{chongqing,zhongqing}, abbre字段为{cq, zq}
    kwfreq为用户搜索关键的频率,用于查询的时候排序
    
    -------------------------------------------------------
    
    <copyField source="kw" dest="suggest" />
    <copyField source="pinyin" dest="suggest" />
    <copyField source="abbre" dest="suggest" />
    
    ------------------suggest_text----------------------------------
    
    <fieldType name="suggest_text" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
        <analyzer type="index">
                <tokenizer class="solr.KeywordTokenizerFactory" />
                &lt;filter class="solr.SynonymFilterFactory" 
                        synonyms="synonyms.txt" 
                        ignoreCase="true" 
                        expand="true" />
                <filter class="solr.StopFilterFactory" 
                        ignoreCase="true" 
                        words="stopwords.txt" 
                        enablePositionIncrements="true" />
                <filter class="solr.LowerCaseFilterFactory" />
                <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt" />
        </analyzer>
        <analyzer type="query">
                <tokenizer class="solr.KeywordTokenizerFactory" />
                <filter class="solr.StopFilterFactory" 
                        ignoreCase="true" 
                        words="stopwords.txt" 
                        enablePositionIncrements="true" />
                <filter class="solr.LowerCaseFilterFactory" />
                <filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt" />
        </analyzer>
    </fieldType>
    

    KeywordTokenizerFactory:这个分词器不进行任何分词!整个字符流变为单个词元。String域类型也有类似的效果,但是它不能配置文本分析的其它处理组件,比如大小写转换。任何用于排序和大部分Faceting功能的索引域,这个索引域只有能一个原始域值中的一个词元。

    前缀查询构造:

    private SolrQuery getSuggestQuery(String prefix, Integer limit) {
        SolrQuery solrQuery = new SolrQuery();
        StringBuilder sb = new StringBuilder();
        sb.append(“suggest:").append(prefix).append("*");
        solrQuery.setQuery(sb.toString());
        solrQuery.addField("kw");
        solrQuery.addField("kwfreq");
        solrQuery.addSort("kwfreq", SolrQuery.ORDER.desc);
        solrQuery.setStart(0);
        solrQuery.setRows(limit);
        return solrQuery;
    }
  • 相关阅读:
    Minecraft我的世界如何联机
    关于我的博客地址jvav的由来
    ThreadLocal使用
    使用免费tk域名
    MVC的request,response流程
    TiDB原理与集群架构
    net5 自定义 中间件
    net5 Autofac支持
    net5 Autofac 支持AOP (1)
    mysql 索引优化
  • 原文地址:https://www.cnblogs.com/txq157/p/5269896.html
Copyright © 2011-2022 走看看