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,他指定了排序的规则,等会看看他的代码 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推荐