solr中对于关键字置顶(竞价排名)、拉黑的源码实现已经实例讲解(一)

标签: solr 关键字 排名 | 发表时间:2017-03-10 12:43 | 作者:suichangkele
出处:http://www.iteye.com
 

工作中用到了关键词置顶、拉黑的操作,自己毫无办法,考虑了很久打算用payload,但是又来在一个研究lucene源码的群中某个小伙伴给我提示说solr中已经为我们实现了这个功能,顿时大喜,马上百度了一下,然后内心很激动,solr真的太好用了,都为我们考虑到了。不过这远远不够,还有更多的事情需要做,不明白他的实现原理,只能猜,一遍一遍的试错,成本太高,所以还是拿来源码看吧。(对于没有对solr的置顶或者竞价排名从来没有接触过的小伙伴,麻烦先百度下“solr 竞价排名”去看看他的基本的配置文件,好心理有个数,我的这篇博客不是入门级的,不适合从头开始 尴尬

 

      先说一下什么是关键字置顶,在我们使用百度的时候,当输入一个词,可能会有广告出现,而且他们都是排在最前面的,这些就是关键词置顶,或者叫做竞价排名。他的实现并不是使用boost来实现的,如果使用boost的话对于任何的词都会出现在前面,而百度中只是在搜索某些词的时候才会出现关联的广告,所以必须寻求另一种办法来实现。 先说一下,我在写这篇博客时使用的solr是4.7.2.公司就是使用的这个。    在solr中如果想要实现这个功能的话需要使用elevator这个searchComponent,这个searchcomponent在solr的solrConfig是默认存在的,在/elevate这个requestHandler中是开启的,但是在/select这个requestHandler中默认是不开启的,鉴于我们都是使用/select这个requestHandler,所以我们在/select中添加这个component,在/select的最后添加

 <arr name="last-components">
      <str>elevator</str>
 </arr>

 这样就算是打开了,我们先看一下elevator这个searchComponent:

  <searchComponent name="elevator" class="solr.QueryElevationComponent" >
    <str name="queryFieldType">string</str>    <!-- 根据这个域的类型找到分词器,在搜索的时候用来对搜索的词进行分词,可以先不用管他,等会再源码中我会讲解的-->
    <str name="config-file">elevate.xml</str>  <!-- 这个是指定配置文件的来源,也即是conf下的elevate.xml -->
  </searchComponent>

 在使用置顶功能时,必须有一个配置文件,用于说明当搜什么的时候将哪个doc置顶,在solr的配置文件中就存在elevator.xml

<elevate>
 <query text="foo bar">  <!--这个不好用来做例子,要看在elevator中定义的queryFieldType,对于不同的配置,是不同的,一会看了源码就懂了,下面的更好理解一些-->
  <doc id="1" />
  <doc id="2" />
  <doc id="3" />
 </query>
 
 <query text="hello">  <!--这个表示在搜hello的时候,将id是1的doc置顶,而不显示ID是IW-02的-->
   <doc id="1" />  <!-- put the actual document at the top -->
   <doc id="IW-02" exclude="true" /> <!-- exclude this document -->
 </query>
 </elevate>

 看完了这个,就能大概明白了,在搜hello的时候要把id是1的排在前面,但是不显示id是IW-02的,因为他的后面是exclude=true,但是这是不准确的,甚至是错误的,等看完源码就懂了。

 

下面进入源码阶段,在上面的xml的配置中可以发现,在solr4.7中,elevator这个的封装类是QueryElevationComponent,所以我们进入到这个类的源码,先看一下inform方法,他在初始化elevator这个searchComponent的时候调用:

 /** 加载searchComponent,读取elevator.xml到内存中,优先从zk中读取,在从配置文件中读取,如果读取到了就会读取一次 */
  @Override
  public void inform(SolrCore core) {
    IndexSchema schema = core.getLatestSchema();
    String a = initArgs.get(FIELD_TYPE);//读取这个searchComponent配置的queryFieldType,initArgs就是用来封装我们在定义searchComponent的时候配置的参数。
    if (a != null) {
      FieldType ft = schema.getFieldTypes().get(a);//根据a确定fieldType,也就是域的类型
      if (ft == null) {
        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
            "Unknown FieldType: '" + a + "' used in QueryElevationComponent");
      }
      analyzer = ft.getQueryAnalyzer();//根据fieldType获得analyzer。
    }
    
    SchemaField sf = schema.getUniqueKeyField();//获得id的域
    if (sf == null) {
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
          "QueryElevationComponent requires the schema to have a uniqueKeyField.");
    }
    idSchemaFT = sf.getType();//id的fieldType
    idField = sf.getName();//id的域的名字
    
    // register the EditorialMarkerFactory   这个小块我没有细看
    String excludeName = initArgs.get(QueryElevationParams.EXCLUDE_MARKER_FIELD_NAME, "excluded");//获得表示排除的关键字,默认是excluded,使用默认即可
    if (excludeName == null || excludeName.equals("") == true) {
      excludeName = "excluded";
    }
    ExcludedMarkerFactory excludedMarkerFactory = new ExcludedMarkerFactory();
    core.addTransformerFactory(excludeName, excludedMarkerFactory);
    ElevatedMarkerFactory elevatedMarkerFactory = new ElevatedMarkerFactory();
    String markerName = initArgs.get(QueryElevationParams.EDITORIAL_MARKER_FIELD_NAME, "elevated");
    if (markerName == null || markerName.equals("") == true) {
      markerName = "elevated";
    }
    core.addTransformerFactory(markerName, elevatedMarkerFactory);
    
    forceElevation = initArgs.getBool(QueryElevationParams.FORCE_ELEVATION, forceElevation);//他的意思是如果我们在请求的参数中指定了sort,还要不要将我们要置顶的doc置顶,因为他们可能不符合条件,true表示置顶。
    try {
      synchronized (elevationCache) {
        elevationCache.clear();
        String f = initArgs.get(CONFIG_FILE);//获得配置文件,也就是elevator.xml
        if (f == null) {
          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
              "QueryElevationComponent must specify argument: '" + CONFIG_FILE + "' -- path to elevate.xml");
        }
        boolean exists = false;
        
        // check if using ZooKeeper,检查是否使用了zk,如果是的话从zk上读取配置文件,也就是elevator.xml,因为在solrCloud的情况下,配置文件就是在zk上
        ZkController zkController = core.getCoreDescriptor().getCoreContainer().getZkController();
        if (zkController != null) {// 从zk的配置中读取f文件
          exists = zkController.configFileExists(zkController.getZkStateReader()
              .readConfigName(core.getCoreDescriptor().getCloudDescriptor().getCollectionName()), f);
        } else {//如果不是使用solrCloud,从本地的配置中读取,则放入的是null
          File fC = new File(core.getResourceLoader().getConfigDir(), f);//从配置文件,也就是conf目录下查找
          File fD = new File(core.getDataDir(), f);//从data,也就是solrHome下的索引中查找
          if (fC.exists() == fD.exists()) {//如果同时存在或者都不存在,则报错
            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
                "QueryElevationComponent missing config file: '" + f + "\n" + "either: " + fC.getAbsolutePath() + " or "
                    + fD.getAbsolutePath() + " must exist, but not both.");
          }
          if (fC.exists()) {//如果在conf下存在,则将封装的配置文件放入到elevationCache中,是一个map,key是indexRader,因为可能会在indexReader发生变化后重新加载elevator.xml(仅仅是可能,等会就看到了,有时候不会重新加载)
            exists = true;
            log.info("Loading QueryElevation from: " + fC.getAbsolutePath());
            Config cfg = new Config(core.getResourceLoader(), f);
            elevationCache.put(null, loadElevationMap(cfg));//在loadElevationMap中读取配置文件封装为一个java对象,放入到map中,记住key是null,不是一个indexReader,稍后可以发现这种情况下是只加载一次配置文件。
          }
        }
        
        // 如果上面的没有读取到,则从data中读取。
        // in other words, we think this is in the data dir, not the conf dir
        if (!exists) {
          // preload the first data
          RefCounted<SolrIndexSearcher> searchHolder = null;
          try {
            searchHolder = core.getNewestSearcher(false);
            IndexReader reader = searchHolder.get().getIndexReader();
            getElevationMap(reader, core);//这个方法中会读取配置文件,然后将配置文件放入evevationCache中(此处不贴代码了),此时的key不是null,而是真正的indexReader,这种情况在indexReader发生变化时会重新加载的。
          } finally {
            if (searchHolder != null) searchHolder.decref();
          }
        }
      }
    } catch (Exception ex) {
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error initializing QueryElevationComponent.", ex);
    }
  }

 

 

看完上述代码后,我们再看一下这个searchComponent再处理请求的时候的操作,看看prepare方法和process方法

 // 从solrj发情的每一次查询请求,都先处理这个方法
  public void prepare(ResponseBuilder rb) throws IOException {
    
    SolrQueryRequest req = rb.req;//请求
    SolrParams params = req.getParams();//请求中封装的参数
    // A runtime param can skip
    if (!params.getBool(QueryElevationParams.ENABLE, true)) {//如果参数中没有开启enableElevation,则不进行操作,on/true/yes都算开启了,看来要使用置顶功能,必须添加enableElevation=on才会起作用!
      return;
    }
    //exclusive表示是否只返回设置的要置顶的document,而不再从solr中查找其他符合条件的document。默认是false,如果要开启则设置exclusive=on即可
    boolean exclusive = params.getBool(QueryElevationParams.EXCLUSIVE, false);
    // A runtime parameter can alter the config value for forceElevation
    boolean force = params.getBool(QueryElevationParams.FORCE_ELEVATION, forceElevation);//是否强制排序,他的是如果我们在参数中设置了sort,要不要仍然将置顶的那些docuement放在开头,ture表示仍然置顶
    boolean markExcludes = params.getBool(QueryElevationParams.MARK_EXCLUDES, false);//是否在返回的document的结果中不删除置顶为exclude的document,而仅仅是用一个字段标记这些document。
    //下面的这两个的意思是我们可以不使用配置的elevator中的规定,而是在查询时使用从请求中获得的要置顶或者删除的id,太好了,这个会极大的简化我们的操作。
    String boostStr = params.get(QueryElevationParams.IDS); //请求中指定的要置顶的id,使用elevateIds=1,2,3表示,用英文逗号分隔
    String exStr = params.get(QueryElevationParams.EXCLUDE);//请求中指定的排除的id,使用excludeIds表示,同上
    
    Query query = rb.getQuery();//在request中的query
    SolrParams localParams = rb.getQparser().getLocalParams();
    String qstr = localParams == null ? rb.getQueryString() : localParams.get(QueryParsing.V);//在请求中的字符串,如果我输入的是title:aa,那么就是title:aa,并不是aa,所以如果你在elevator.xml中配置的是aa,那么如果从solrj中输入的是title:aa,那么配置文件将不会起作用。
    if (query == null || qstr == null) {
      return;
    }
    
    ElevationObj booster = null;//最终使用的booster,可能来自于请求,也就是上面的boostStr和exStr,也可能来自于配置文件(比如zk或者data中的)
    try {
      
      if (boostStr != null || exStr != null) {//如果在请求中指定了,则使用请求的,不会使用配置的
        List<String> boosts = (boostStr != null) ? StrUtils.splitSmart(boostStr, ",", true) : new ArrayList<String>(0);//可以看出用英文逗号分隔
        List<String> excludes = (exStr != null) ? StrUtils.splitSmart(exStr, ",", true) : new ArrayList<String>(0);
        booster = new ElevationObj(qstr, boosts, excludes);//将从参数中传来的id封装为一个elevationObj。
      } else {//如果没有从请求中传过来,使用配置的,也就是优先使用配置的
        IndexReader reader = req.getSearcher().getIndexReader();
        qstr = getAnalyzedQuery(qstr);//进行分词,这个地方很关键,要对传过来的词进行分词处理,经过分词之后,得到的结果并不一定是之前设置的词,所以导致在elevator.xml中配置的失效,在上面已经说了,我看了看这个getAnalyzerdQuery方法,他是将分的多个term串联起来的,但是没什么鸟用。
        booster = getElevationMap(reader, req.getCore()).get(qstr);//使用当前的indexReader获得ElevatorObj,等会看这个方法的代码。
      }
    } catch (Exception ex) {
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error loading elevation", ex);
    }
    
    if (booster != null) {
      
      rb.req.getContext().put(BOOSTED, booster.ids);//要指定的id
      rb.req.getContext().put(BOOSTED_PRIORITY, booster.priority);//这个priority指定要排序的document的顺序
      
      // Change the query to insert forced documents
      if (exclusive == true) {//如果仅仅是返回那些指定的document
        // we only want these results
        rb.setQuery(booster.include);//include就是那些要置顶的document形成的query,他也是一个booleanquery,等会上代码。
      } else {//重新设置这次请求的query
        BooleanQuery newq = new BooleanQuery(true);
        newq.add(query, BooleanClause.Occur.SHOULD);//添加真正的query
        newq.add(booster.include, BooleanClause.Occur.SHOULD);//要置顶的id形成的query,
        if (booster.exclude != null) {//
          if (markExcludes == false) {//对于排除的id,如果不是打标记,也就是直接删除,
            for (TermQuery tq : booster.exclude) {
              newq.add(new BooleanClause(tq, BooleanClause.Occur.MUST_NOT));
            }
          } else {//设置为打标记,但是不在结果中删除,这个我没有看
            // we are only going to mark items as excluded, not actually exclude them. This works
            // with the EditorialMarkerFactory
            rb.req.getContext().put(EXCLUDED, booster.excludeIds);
          }
        }
        rb.setQuery(newq);//重新设置query
      }
      
      //下面是要排序的部分,很关键
      ElevationComparatorSource comparator = new ElevationComparatorSource(booster);//这个类用于形成一个Comparator,用于排序,他会将置顶的那些docuemnt放在前面。
      // if the sort is 'score desc' use a custom sorting method to
      // insert documents in their proper place
      SortSpec sortSpec = rb.getSortSpec();
      
      if (sortSpec.getSort() == null) {
        sortSpec.setSortAndFields(new Sort(new SortField[] {new SortField("_elevate_", comparator, true),//这个域的名字_elevate_没有关系,关键是comparator,他指定了排序的规则,等会看看他的代码
            new SortField(null, SortField.Type.SCORE, false)}), Arrays.asList(new SchemaField[2]));
      } else {
        // Check if the sort is based on score
        SortSpec modSortSpec = this.modifySortSpec(sortSpec, force, comparator);//如果已经指定了,则根据是否force进行修改。
        if (null != modSortSpec) {
          rb.setSortSpec(modSortSpec);
        }
      }
      .....下面的没有贴
  
  }

 

 

上面还留有几个任务,还有很多的方法和示例没有写,我写在了下一篇博客中。

 

 

 



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [solr 关键字 排名] 推荐:

solr中对于关键字置顶(竞价排名)、拉黑的源码实现已经实例讲解(一)

- - 开源软件 - ITeye博客
工作中用到了关键词置顶、拉黑的操作,自己毫无办法,考虑了很久打算用payload,但是又来在一个研究lucene源码的群中某个小伙伴给我提示说solr中已经为我们实现了这个功能,顿时大喜,马上百度了一下,然后内心很激动,solr真的太好用了,都为我们考虑到了. 不过这远远不够,还有更多的事情需要做,不明白他的实现原理,只能猜,一遍一遍的试错,成本太高,所以还是拿来源码看吧.

Solr SpellCheck 应用

- - 开源软件 - ITeye博客
通过对各类型的SpellCheck组件学习,完成项目拼写检查功能. 本文使用基于拼写词典的实现方式,solr版本为5.3.0. SpellCheck 简述. 拼写检查是对用户错误输入,响应正确的检查建议. 比如输入:周杰轮,响应:你是不是想找 周杰伦. Solr的拼写检查大致可分为两类,基于词典与基于Solr索引.

Solr DocValues详解

- - 企业架构 - ITeye博客
什么是docValues. docValues是一种记录doc字段值的一种形式,在例如在结果排序和统计Facet查询时,需要通过docid取字段值的场景下是非常高效的. 为什么要使用docValues. 这种形式比老版本中利用fieldCache来实现正排查找更加高效,更加节省内存. 倒排索引将字段内存切分成一个term列表,每个term都对应着一个docid列表,这样一种结构使得查询能够非常快速,因为term对应的docid是现成就有的.

solr的使用

- - Web前端 - ITeye博客
solr的原理不和大家一一讲述,主要讲solr在使用过程中的注意事项.  首先是安装solr,安装步骤省略. (不要说我懒,安装步骤导出都是. 成功之后 需要在solr里面建立一个针对你的业务的服务,我想建立一个叫做discuz的服务. 然后你在你的solr目录 :solr-5.5.3/server/solr/  下看见了discuz   ,这是你刚刚创建的,针对某一业务的整个搜索配置都是在这个目录下配置的.

Solr调优参考

- - 淘宝网通用产品团队博客
共整理三部分,第一部分Solr常规处理,第二部分针对性性处理,前者比较通用,后者有局限性. 务必根据具体应用特性,具体调节参数,对比性能. 具体应用需要全面去把控,各个因素一起起作用. 第一部分. E文连接 http://wiki.apache.org/solr/SolrPerformanceFactors.

Solr之缓存篇

- - 淘宝网综合业务平台团队博客
Solr在Lucene之上开发了很多Cache功能,从目前提供的Cache类型有:. 而每种Cache针对具体的查询请求进行对应的Cache. 本文将从几个方面来阐述上述几种Cache在Solr的运用,具体如下:. (1)Cache的生命周期. (2)Cache的使用场景. (3)Cache的配置介绍.

Solr主从备份

- - 研发管理 - ITeye博客
SOLR复制模式,是一种在分布式环境下用于同步主从服务器的一种实现方式,因之前提到的基于rsync的SOLR不同方式部署成本过高,被SOLR1.4版本所替换,取而代之的就是基于HTTP协议的索引文件传输机制,该方式部署简单,只需配置一个文件即可. 以下讲解具体操作步骤: . 步骤分主服务器和从服务器,允许有多个从服务器,即从服务器的配置一样.

solr相似匹配

- - CSDN博客推荐文章
相似匹配   在我们使用网页搜索时,会注意到每一个结果都包含一个 “相似页面” 链接,单击该链接,就会发布另一个搜索请求,查找出与起初结果类似的文档. Solr 使用 MoreLikeThisComponent(MLT)和 MoreLikeThisHandler 实现了一样的功能. 如上所述,MLT 是与标准 SolrRequestHandler 集成在一起的;MoreLikeThisHandler 与 MLT 结合在一起,并添加了一些其他选项,但它要求发布一个单一的请求.

Solr与Mysql集成指南

- sun - 草根网:互联网界的读者文摘
在《企业级搜索引擎Solr使用入门指南》及《企业级搜索引擎Solr交流》中对Solr的使用做了简单介绍. 在数据库驱动的应用中,当时采....

Solr\Lucene优劣势分析

- - 淘宝网综合业务平台团队博客
最早lucene2.4以及以前,追溯到2008年前后,lucene刚刚引起大家的关注,到后来Nutch. 、solr的出现,lucene变得更加热. Nutch、Solr的发展,极大推动了lucene的升级. 对于一些接触过搜索,使用过lucene、solr的人来说,一般都会感觉lucene、solr很牛逼.