`
suichangkele
  • 浏览: 193387 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

    博客分类:
  • solr
阅读更多
 

工作中用到了关键词置顶、拉黑的操作,自己毫无办法,考虑了很久打算用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,他指定了排序的规则,等会看看他的代码,新产生的排序规则是先按照compartor排序,然后再按照score进行排序,置顶功能的关键就是这里。
            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);
        }
      }
      .....下面的没有贴
  
  }

 

 

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

 

 

 

分享到:
评论

相关推荐

    solr-6.2.0源码

    solr-6.2.0 强大的分布式搜索引擎,包含各种详细例子及源码解析

    solr6.6.0源码

    solr6.6.0源码

    solr源码及文档

    solr全文检索,里面包含文档,源代码,jar包,使用的是solr4.2,东西比较全,安装文档就能跑起来,,适合参考借鉴

    solr(solr-9.0.0-src.tgz)源码

    solr(solr-9.0.0-src.tgz)源码

    JAVA+Solr分词项目工程实例Java实用源码整理learns

    JAVA+Solr分词项目工程实例Java实用源码整理learns

    solr5的ik中文分词器源码

    solr5的ik中文分词器源码,解压后需要自己打包成jar包

    java进阶Solr从基础到实战

    在本套课程中,我们将全面的讲解Solr,从Solr基础到Solr高级,再到项目实战,基本上涵盖了Solr中所有的知识点。 主讲内容 章节一:Solr基础(上) 1. 环境搭建 2. 核心讲解 3. 数据导入 4. 各种中文分析器 章节二:...

    Solr-search过程源码分析

    详细阐述Solr-search 源码级别过程

    solr教程+实例

    solr教程+实例

    JAVA+Solr分词项目工程实例Java源码

    JAVA+Solr分词项目工程实例Java源码 解压密码:https://hao.360.cn/?src=lm&ls=n527fd66b97

    lucene-solr源码,编译成的idea项目源码

    本人用ant idea命令花了214分钟,35秒编译的lucene-solr源码,可以用idea打开,把项目放在D:\space\study\java\lucene-solr路径下,再用idea打开就行了

    solr 4.10源码

    solr4.10官方源码,保存留用以后下载

    solr-4.5源码包

    solr-4.5源码包

    solr-5.2.1-src.tgz源码

    Solr源码在MyEclipse下的搭建 1. 下载并按装Ant 下载地址: http://ant.apache.org/bindownload.cgi Ant环境变量配置: ANT_HOME: E:\Program Files\apache-ant-1.9.0 Path: %ANT_HOME%\bin 在cmd中输入ant -v...

    solr5.4.0完整包

    Solr 依存于Lucene,因为Solr底层的核心技术是使用Lucene 来实现的,Solr和Lucene的本质区别有以下三点:搜索服务器,企业级和管理。Lucene本质上是搜索库,不是独立的应用程序,而Solr是。Lucene专注于搜索底层的...

    Solr 源码包Part2

    Solr的安装部署包,只能分卷上传,稍后上传依赖jar包,及部署攻略

    solr实现京东搜索

    使用Solr实现电商网站中商品信息搜索功能。 1、 可以根据关键字搜索商品信息 2、 可以根据商品分类、价格过滤搜索结果 3、 可以根据价格进行排序 4、 实现分页

    solr-5.3.0-src.tgz源码

    当前资源为Apache官方提供的solr-5.3.0-src.tgz源码,包括Lucene和solrj的源码,方便大家学习使用!此外还整理了一套有关solr-5.3.0的window和linux版包以及官方说明文档apache-solr-ref-guide-5.3.pdf,有需要的...

    JAVA上百实例源码以及开源项目源代码

     Java实现HTTP连接与浏览,Java源码下载,输入html文件地址或网址,显示页面和HTML源文件,一步步的实现过程请下载本实例的Java源码,代码中包括丰富的注释,对学习有帮助。 Java实现的FTP连接与数据浏览程序 1个...

    Solr实现电扇站内搜索

    Solr实现电扇站内搜索Solr实现电扇站内搜索Solr实现电扇站内搜索Solr实现电扇站内搜索

Global site tag (gtag.js) - Google Analytics