Solr权威指南(下卷)
上QQ阅读APP看书,第一时间看更新

第11章 Solr高级查询

通过第11章,你将可以学习到以下内容:

❑掌握如何使用Function Query以及如何自定义Function Query;

❑掌握如何使用Geospatial Query;

❑掌握如何使用Pivot Facet和Subfacet;

❑掌握如何使用JSON Facet API来实现复杂的数据统计查询;

❑掌握如何使用Solr中的其他查询组件,比如Elevation(竞价排名组件);

❑掌握如何使用Solr中的Result Clustering组件实现自动结果集聚类分组。

Solr作为一个强大的文本搜索平台,能够根据输入关键字查询并返回索引文档,你可能也已经了解到了Solr的一些核心功能,比如文本分词、关键字高亮、结果集分组等。尽管对于大多数搜索程序来说,将那些与用户查询最佳匹配的索引文档返回给用户是非常重要的,但是Solr还有另外一个比较常见的使用场景:聚集结果集用于数据统计分析。Solr的Pivot Facet支持叠加统计多个Facet(维度),它能够在单个查询中对任意的聚合分类进行计算统计,这使得Solr在提供数据分析报告方面变得很有用并且还十分高效。Solr另一个核心功能就是在查询时能够对数据执行一个Function(函数)进行动态计算,函数计算后的结果可以被用于Filter Query、文档的相关性评分、文档的排序、作为文档的“伪域”被返回。Solr还提供了强大的Geospatial(地理空间)查询功能,Geospatial查询允许你根据一个点或者一个区域进行多边形查询,或以经纬度为圆心在指定半径的圆内进行查询,实现附近的位置查询(比如查询当前用户所处位置附近的酒店或饭店)。有时候,你期望在返回的索引文档的域中引用外部数据源,Solr提供了这个功能。Solr还支持在同一个Solr实例内跨Core在一个外键域上执行Join操作,这类似于SQL里的两个表根据外键进行多表连接查询。上述每个复杂的功能都会在本章中进行讲解。

11.1 Solr函数查询

Solr中的Function Query(函数查询)允许你为每个索引文档执行一个函数进行动态计算值。Function Query是一个比较特殊的查询,函数动态计算后得到的值可以作为一个关键字添加到查询中,也可以作为文档的评分,就像是一个普通的关键字查询同时还能生成相关性评分。通过使用Function Query,函数动态计算值可以被用于修改索引文档的相关性评分,以及查询结果集排序,而且函数动态计算值还可以作为一个“伪域”被动态添加到每个匹配的索引文档中并返回给用户。Function Query还支持嵌套,意思就是一个Function的输出可以作为另一个Function的输入,Function支持任意深度的嵌套。

11.1.1 Function语法

Solr中标准的Function语法是先指定一个Function名称,后面紧跟着一对小括号,小括号内可以传入零个或多个输入参数,语法使用示例如下:

          functionName()
          functionName(input1)
          functionName(input1, input2)
          functionName(input1, input2, ..., inputN)

Function的输入参数可以是以下任意一种形式:

‰❑一个常量值(数字或者字符串),语法:

            100, 1.45, "hello world"

‰❑一个域名称,语法:

            fieldName, field(fieldName)

‰❑另外一个Function,语法:

            functionName(...)

‰❑一个变量,语法:

            q={! func}min($f1, $f2)&f1=sqrt(popularity)&f2=1

尽管Solr Function乍一看让人有点不知所措,其实Solr文档中定义了每个Function的输入参数的类型,大部分的Function都遵循Function的标准语法,但是Constant Function(常量函数)、Field Function(域函数)、Parameter Substitution(替换变量)这些属于特例,它们支持另一种简单语法。Constant Function(常量函数)的语法就是值本身。Field Function(域函数)的语法就是域的名称被一个名称为“field”的函数包裹。Parameter Substitution(替换变量)的语法就是函数的输入变量使用的是一个$开头的变量,该变量引用自请求URL的查询文本中定义的变量。除了这3个特例,其他函数都使用标准的Function语法。

因为Function的所有输入参数可以被看作是一个Function(函数)(常量值可以被看作常量函数),所以Function的标准语法在概念上来讲,就可以理解为functionName(function1, ..., functionN)。假设索引文档中有个fieldContainingNumber域,它其中有个值为-99,那么请思考下面几个Function的使用示例:

          max(2, fieldContainingNumber)                        //输出结果:2
          max(fieldContainingNumber, 2)                        //输出结果:2
          max(2, -99)                                          //输出结果:2
          max(-99, 2)                                          //输出结果:2
          max(2, field(fieldContainingNumber))                 //输出结果:2
          max(field(fieldContainingNumber), add(1,1))          //输出结果:2

从上面示例你会注意到,你可以为Constant Function常量函数(甚至你可以为其他任意标准函数)使用Field Function进行包装,尽管输入的参数顺序以及每个输入参数的含义会有所不同,但是它们最终都是用于计算-99和2之间的最大值。将一个函数的输入参数看作另外一个函数的好处就是它允许你用任意的嵌套函数来实现复杂计算。并不是所有的Function(函数)都支持同样类型的输入参数,有些Function(函数)期望接收字符串类型常量参数,而另外一些Function(函数)可能期望接收Integer或者Float类型的数字。假设fieldContainingString域的域值为"hallo",请思考下面的函数调用示例:

          strdist("hello", fieldContainingString, edit)    //输出结果:0.8
          strdist("hello", "hallo", "edit")                //输出结果:0.8

strdist函数用于计算两个字符串之间的相似度,相似度计算是基于一个指定的算法进行计算,使用哪种算法是通过函数的第3个参数进行指定,我们示例中的"edit"表示采用编辑距算法。假如我们将参数的数据类型指定为错误的,函数将会返回什么呢?

          strdist("hello", 1000, edit)                      //输出结果:0
          strdist(1000, "1000", edit)                       //输出结果:1
          strdist("1001", 1000, edit)                       //输出结果:0.75

你可能会觉得函数会抛出异常,然而实际上函数内部会适当地自动进行数据类型转换,比如在示例中,将数字常量1000转换成字符串"1000"。在大多数情况下,你并不能安全地将一个字符串转换成一个数字,此时Solr可能会抛出一个异常。因此,需要谨记:函数嵌套确实很好用,但是并不是所有的函数都可以随意嵌套,你需要考虑每个函数的输入参数类型是否正确。

Solr的Function可以影响相关性评分,可以被用于Filter Query过滤结果集,可以基于函数计算值进行排序,可以将函数计算值作为索引文档的“伪域”并返回,甚至可以基于函数计算值进行Facet查询统计。下一节我们将深入学习这些用法。

11.1.2 使用函数查询

为了便于后续的示例讲解,请大家从随书源码中获取Core的相关配置文件、测试数据及导入测试数据的测试类,根据我们前面章节所学的知识将本节测试环境需要的"news"Core搭建好。

在Solr中执行一个典型的关键字查询,需要在倒排索引中查找关键字,同时计算每个匹配索引文档的相关性评分,从而决定哪些索引文档与查询关键字比较相关,最后作为结果集返回。然而查询不仅能基于搜索关键字,你可以在查询中插入一个Function并将其看作另外一个搜索关键字。为了演示Function Query,请建立"news" Core并运行随书源码中的IndexNews类导入测试数据。假如已经成功导入了测试数据,可以执行下面的查询示例:

        http://localhost:8080/solr/news/select?
        q="United States" AND France AND President AND _val_:"recip(ms(date),1,100,100
    )"&indent=true

上面的查询表示查询包含"United States"短语且包含France和President关键字,并且函数计算值在[1, 100]区间范围内的索引文档。这里有3个关键点需要引起你的注意:

‰❑_val_语法:用于注入一个Function Query,这里的_val_可以看作主查询中的一个查询Term。

❑Function Query并不会改变最终返回结果集中索引文档的总数。

‰❑查询的最后相关性评分一般是查询中每个Term的相关性评分的总和,"United States"、France和President这些Term的相关性评分是基于tf-idf相似度算法进行计算的,但是Function Query的评分计算是函数自身的计算值。

基于上述3点,你可以了解到示例中的Function Query是为了给新添加的索引文档进行加权。最新的索引文档的相关性评分可能是100,而最旧的索引文档的评分可能是1,剩下的索引文档的评分会落在[1, 100]之间。注意,每个索引文档的最后评分是标准化的,这意味着每个索引文档的最后评分不会都到达100分,最近添加的索引文档相比之前显示会更靠前。

Function在Solr中无处不在,它可以对用户的q参数进行加权,它还可以在不同的Query Parser中使用,比如在eDisMax Query Parser中通过bf参数指定Function;它还可以作为Filter Query的一部分,用于索引文档排序等。但是最重要的是你需要了解Function Query是如何被执行的。前面的示例中你已经见过了"_val_"这样的语法,你可能还记得我们之前介绍过的Function Query Parser,可以通过一个本地参数!func来构造一个Function Query,比如:{! func}functionName(…)。Function Query本质就是将函数计算值作为构造的函数查询的评分,因此,以下几种查询语法是等价的:

        q=solr AND _val_:"add(1, boostField)"
        q=solr AND _query_:"{! func}add(1, boostField)"
        q=solr AND {! func v="add(1, boostField)"}

为一个查询,添加一个Function看起来非常有用,它能修改查询匹配的索引文档的评分。如果你期望过滤掉函数计算值不在指定范围内的索引文档,可以使用Function Range Query Parser来解决函数计算值范围过滤。

如果你需要根据函数计算值的范围来过滤索引文档,那么Function Range Query Parser(简称frange)会比较适用你的使用场景,Frange过滤器通过执行一个指定的Function Query,过滤掉函数计算值不在指定范围内的索引文档。为了演示这种功能,我们搭建测试环境。这里会用到随书源码中的"salestax" Core, Core相关配置文件和测试数据以及导入数据测试类请从相应章节中查找获取。导入完成之后,请看下面这个查询示例:

          http://localhost:8080/solr/salestax/select? q=*:*&
          fq={! frange l=10 u=15}product(basePrice, sum(1, $userSalesTax))&
          userSalesTax=0.07

以上查询先通过sum函数计算$userSalesTax和1的价格之和,然后将basePrice域的域值与sum函数计算返回值通过product函数求乘积,最后通过frange过滤器的l(即lower表示最小值)和u(即upper表示最大值)参数定义了product函数计算返回值的取值范围,符合这个区间范围限制的索引文档将会被返回。你还可以设置incll(即include lower)和inclu(include upper)参数来指定是否包含两个边界值。

你可能会说,能不能自定义一个Function来灵活地过滤任意查询匹配的结果集?关于自定义Function相关内容会在本章的后续章节讲解。现在你已经知道如何为查询添加Function,并且理解了函数评分是如何计算的,接下来让我们继续学习使用函数动态计算值来代替静态的域值。

11.1.3 将函数计算值作为“伪域”返回

在上一节,你已经了解到函数的输入参数可以被看作是一个函数,既然如此,那么似乎我们可以使用Function来替换Field,因为Field和Function最终都是返回一个值。事实也是如此,你不仅可以为每个索引文档动态计算得到一个数值,还可以将这个数值作为一个“伪域”随索引文档一起返回。重新回到我们在上一节中的"salestax"示例,执行下面这个查询:

          http://localhost:8080/solr/salestax/select? q=*:*&
          userSalesTax=0.07&
          fl=id, basePrice, product(basePrice, sum(1, $userSalesTax))

上面这个查询返回的结果会是怎样的呢?正如你看到的那样,你会发现返回的索引文档中多了一个“伪域”, “伪域”的域名称就是我们定义的函数表达式,“伪域”的域值就是函数表达式最终的计算值。之所以称为“伪域”,是因为它并不真正存在于我们的索引数据中,但是它仍然会像其他存储域一样被一起返回。“伪域”的名称使用函数表达式可能会显得冗长难看,不过值得庆幸的是,Solr提供了为“伪域”定义任意你想要的别名的功能,具体如何为“伪域”定义别名,请看下面这个示例:

        http://localhost:8080/solr/salestax/select? q=*:*&
        userSalesTax=0.07&
        fl=id, basePrice, totalPrice:product(basePrice, sum(1, $userSalesTax))

这里我们为"product(basePrice, sum(1, $userSalesTax))"这个伪域定义了一个别名totalPrice,最终返回结果里伪域名称就是我们这里定义的别名了。正因为你可以为“伪域”定义任意的别名,因此也就意味着可以将“伪域”的别名定义为索引文档中真实存在的域的域名称,这样就可以直接使用“伪域”的值来冒充该域的真实域值。当你期望根据用户权限来控制某些用户没有权限访问某个域的真实域值的时候,通过“伪域”别名来冒充真实域会对你很有用。

通过使用Function,你可以在域的域值返回之前对其进行任意操纵,比如经过函数计算变换它的值。你不仅可以通过函数修改文档中任何域的域值,还可以通过函数修改文档的相关性评分,从而影响文档是否应该被返回,或者文档在返回的结果集中的排序。

11.1.4 根据函数进行排序

在上一节,你了解了如何将一个函数的动态计算值添加到索引文档中作为一个“伪域”在查询结果集中返回;你也知道了如何根据函数计算值来对查询结果集进行过滤,以及如何使用函数来修改匹配文档的相关性评分。接下来,让我们继续学习如何基于函数动态计算值来对查询结果集进行排序。根据函数动态计算值来对查询结果集进行排序的语法与普通查询中根据某个域排序的语法没什么太大的不同,具体请看下面这个查询示例:

        http://localhost:8080/solr/salestax/select? q=*:*&
        userSalesTax=0.07&
        sort=product(basePrice, sum(1, $userSalesTax)) asc, score desc

上面这个查询,根据product函数计算值升序进行排序,然后再按文档的评分降序排序,你可以结合其他函数构造更为复杂的Function Query,比如:

        http://localhost:8080/solr/salestax/select?
        q=_query_:"{! func}recip(ms(date),1,100,100)"&
        userSalesTax=0.07&
        totalPriceFunc=product(basePrice, sum(1, $userSalesTax))&
        fq={! frange l=10 u=15 v=$totalPriceFunc}&
        fl=*, totalPrice:$totalPriceFunc&
        sort=$totalPriceFunc asc, score desc

上面这个查询首先根据Function Query Parser对"{! func}recip(ms(date),1,100,100)"查询表达式进行解析,构造成Function Query;然后通过_query_语法将Function Query转换成普通的Query,转换后的查询并没有过滤任何索引文档,它只是用来根据文档的date域的时间远近对索引文档进行加权;然后通过fq对$totalPriceFunc变量表示的函数最终计算值进行区间范围过滤,不在[10, 15]区间内的索引文档将会被过滤掉,通过sort参数先按照$totalPriceFunc变量表示的函数计算值进行升序排序;再按照索引文档的评分降序排序;最后通过fl里将函数计算值当作索引文档的“伪域”一并返回。这个示例综合使用了我们前面所讲解的知识点。

11.1.5 Solr中的内置函数

到目前为止,你已经知道如何在Solr中应用Function。由于Solr内置的函数非常多,而且还在不断增加中,所以本书这部分内容不可能面面俱到,如果本书有遗漏某个函数没有提及,读者可以自行查阅资料学习。但是我会尽量覆盖Solr中内置的大部分常用函数,并详细解释每个函数的用途以及使用语法。Solr中的内置函数大致分为5类:data transformation(数据转换)、Math(数学计算)、Relevancy(计算相关性评分)、Distance(距离计算)、Boolean(布尔操作)。

1.数据转换类函数

Solr中比较常用的函数大都是转换类函数,即将数据通过一个或多个函数计算从一个值转换成另一个值。下面会详细介绍每个转换类函数的用途和用法(如表11-1所示)。

表11-1 数据转换类函数表

2.数学函数

数学计算是比较常用的数据分析操作。Solr全面支持数学计算,支持包括加减乘除以及三角函数等多种数学函数。表11-2列举了Solr中支持的数学函数。

表11-2 Solr中支持的数学函数

3.相关性评分函数

Solr的相关性评分默认是基于DefaultSimilarity类进行计算而来的,DefaultSimilarity利用索引的统计信息来决定哪些索引文档与查询匹配。这些相关性评分是针对每个索引文档进行计算得到一个综合的评分,你还可以使用相关性评分函数对个别查询进行部分评分(比如你只想返回Term的出现频率信息)。相关性评分的所有核心统计都包含于相关性评分函数中,比如tf-idf。表11-3列举了Solr中支持的相关性评分函数。

表11-3 Solr中支持的相关性评分函数

可以使用上面表中的相关性函数来重写评分计算,请思考下面这个Solr查询示例:

        http://localhost:8080/solr/salestax/select?
        fq={! cache=false}text:"microsoft office"&
        q={! func}sum(
            product(
                tf(text, "microsoft"),
                idf(text, "microsoft")
            ),
            product(
                tf(text, "office"),
                idf(text, "office")
            )
        )

上面这个查询首先分别计算"microsoft"和"office"的tf和idf,然后分别计算tf和idf的乘积,最后返回两个乘积的和作为文档的最后评分。然后根据短语"microsoft office"进行过滤,将不符合fq参数要求的索引文档过滤掉。通过这些相关性评分函数,Solr为你打开了评分模型,可以随心所欲地在查询时通过相关性评分函数来干预文档评分。

4.距离计算函数

有时候你希望能够计算两个值之间的距离,比如你要计算地球上两个点(甚至两个向量)之间的空间距离,这在地理空间搜索中会比较有用。你还可以计算两个字符串之间的相似度,表11-4列举了Solr中支持的距离计算函数。

表11-4 Solr中支持的距离计算函数

从以上的表你可以了解到,Solr全方位支持距离计算函数。dish函数允许你指定0-norm、1-norm、2-norm、无穷norm用于N维空间内两个点或向量的距离计算,比如计算二维空间内两点的欧几里得距离:dist(2, x1, y1, x2, y2),计算三维空间内两点的曼哈顿距离:dist(1, x1, y1, z1, x2, y2, z2)。

sqedist函数相比欧几里得函数计算执行开销更小,它返回的是欧几里得距离的平方根,对于二维空间里的坐标点,平方欧式距离计算的是勾股定理(a2+ b2= c2)中的c2,因为欧式距离计算必须要额外的对c2进行开平方根从而得到确切的c值,如果你只需要在文档排序或者文档相关性评分时获取到文档的相对顺序,而不关心两点之间的实际准确距离值,那么使用sqedist函数计算性能会更高效。

hsin函数用于计算球面上两点的距离。radiusInKm参数表示球面的半径,如果你想计算地球上两点的距离,地球的近似半径值是6371.01(赤道半径),由于地球并不是一个完美的球体,因此这个近似半径值的精确度还可以再提升0.5%。如果指定的是经纬度,那么isDegrees需要设置为true,如果坐标点指定的是弧度,那么isDegrees参数设置为false。x1、y1表示第一个坐标点,x2、y2表示第二个坐标点。

ghhsin函数与hsin函数类似,但是ghhsin函数接收的不是角度和弧度,而是geohash值。geohash函数可以接收经度和未读值,并将它们进行geohash编码,得到的编码字符串可以作为ghhsin函数的输入参数,如果在索引中你的某个域存储的是geohash函数编码后的字符串,那么可能会用到这些函数。

strdist函数用于计算两个字符串之间的相似度,一般用于相似Term的模糊匹配。如果将一个字符串看作一个多字符的向量,strdist函数计算的是两个字符串(字符向量)之间的距离,最终计算得到的相似度值范围是[0, 1],0表示一点也不相似,1表示两个字符串完全相等。strdist函数的s1、s2参数表示输入的两个字符串,distType参数用于指定距离计算的算法,如果distType参数值为ngram,默认使用2个字符串相比较,但是可以通过ngram参数覆盖。

geodist函数用于计算地球上两点之间的空间距离。sfield参数必须是LatLonType域,函数返回的距离单位是千米,lon参数表示经度,lat表示纬度,pt参数即point的缩写,即坐标点(x, y)。geodist函数内部使用hsin函数,简化了使用语法。geodist函数是Solr中最常用的距离计算函数,我们会在下一章节更详细的讲解它在Geospatial Search(地理空间查询)中的应用。

5.布尔函数

Boolean操作不仅可以用于关键字查询,它还可以用于构建或连接任意复杂的Function Query(函数查询),通过if、and、or、not、xor、exists等函数,可以检查域值或其他函数计算以及基于这些检查有条件的返回域值,表11-5列举了Solr支持的布尔函数。

表11-5 Solr支持的布尔函数

Solr提供了这么多丰富的函数供我们选择,应该能够满足大部分用户的需求,与Solr中的其他功能一样,你也可以自定义Function来扩展Solr的Function,接下来将学习如何创建我们自己的Function。

11.1.6 自定义函数

有时候,你可能想要执行某些数据操作,但Solr内置函数并不支持。值得庆幸的是,在Solr中,可以很简单地实现自己的自定义函数。可以从技术上进行内存计算做任何事情,比如访问外部文件或数据源获取数据,甚至运行任意你想要的代码。实现自定义的Function唯一的约束就是你能容忍函数计算完成耗费多长时间。因为自定义的Function代码可能会针对每个索引文档进行计算,它需要在合理的响应时间内快速的作出响应。

在本节,我们将演示如何创建一个自定义Function来将多个域的域值拼接成一个字符串。为了能够在Solr中以插件的方式使用我们的自定义Function,你需要完成以下3个步骤:

1)编写一个类表示你的Function,这个类需要继承ValueSource类,它能够为索引中的每个索引文档返回一个计算值。

2)编写一个ValueSourceParser类,它能够解析自定义Function的语法并将其解析为变量传递给第一步自定义的ValueSource类。

3)在solrconfig.xml中注册你在第二步中定义的ValueSourceParser类,指定它的完整类路径以及函数名称,但自定义Function执行时,会使用你定义的函数名称,并且自定义的ValueSourceParser类解析输入参数并传递给第一步自定义的ValueSource类。

我们将要实现的concatenation函数需要继承Solr中的ValueSource类,并重写其getValues方法,最后返回一个FunctionValues对象,FunctionValues对象可以为索引中每个索引文档返回计算值,以下的代码演示了如何创建一个ConcatenateFunction类:

        /**
          * Created by Lanxiaowei
          * 自定义Concatenate函数
          */
        public class ConcatenateFunction extends ValueSource {
            protected final ValueSource valueSource1;
            protected final ValueSource valueSource2;
            protected final String delimiter;
            public ConcatenateFunction(ValueSource valueSource1,
                                          ValueSource valueSource2,
                                          String delimiter) {
                if (valueSource1 == null || valueSource2 == null){
                    throw new SolrException(
                              SolrException.ErrorCode.BAD_REQUEST,
                              "One or more inputs missing for concatenate function"
                    );
                }
                  this.valueSource1 = valueSource1;
                  this.valueSource2 = valueSource2;
                  if (delimiter ! = null){
                      this.delimiter = delimiter;
                  }
                  else{
                      this.delimiter = "";
                  }
              }

              public FunctionValues getValues(Map context, LeafReaderContext readerContext)
    throws IOException {
                  final FunctionValues firstValues = valueSource1.getValues(
                          context, readerContext);
                  final FunctionValues secondValues = valueSource2.getValues(
                          context, readerContext);
                  return new StrDocValues(this) {
                      @Override
                      public String strVal(int doc) {
                          return firstValues.strVal(doc)
                                    .concat(delimiter)
                                    .concat(secondValues.strVal(doc));
                      }
                      @Override
                      public String toString(int doc) {
                          StringBuilder sb = new StringBuilder();
                          sb.append("concatenate(");
                          sb.append("\"" + firstValues.toString(doc) + "\"")
                                    .append(', ')
                                    .append("\"" + secondValues.toString(doc) + "\"")
                                    .append(', ')
                                    .append("\"" + delimiter + "\"");
                          sb.append(')');
                          return sb.toString();
                      }
                  };
              }

              @Override
              public boolean equals(Object o) {
                  if (this.getClass() ! = o.getClass()) return false;
                  ConcatenateFunction other = (ConcatenateFunction) o;
                  return this.valueSource1.equals(other.valueSource1)
          && this.valueSource2.equals(other.valueSource2)
          && this.delimiter == other.delimiter;
              }
              @Override
              public int hashCode() {
                  long combinedHashes;
                  combinedHashes = (this.valueSource1.hashCode()
                          + this.valueSource2.hashCode()
                              + this.delimiter.hashCode());
                      return (int) (combinedHashes ^ (combinedHashes >>> 32));
                  }
                  @Override
                  public String description() {
                      return "Concatenates two values together with an optional delimiter";
                  }
            }

关于ConcatenateFunction类有两个关键点:输入参数和getValues方法的返回值。Concatenate Function类的输入参数由两个ValueSource对象表示,ConcatenateFunction类会将两个ValueSource对象包含的值使用连接符c进行拼接。回顾我们之前讲解的知识,一个Function的输入参数可以是另一个函数的返回值,通过定义两个ValueSource对象而不是定义两个String字符串,你的函数可以接收任意输入。尽管将输入参数统统采用ValueSource类型来定义带来了很大的灵活性,但ConcatenateFunction类的构造函数的第3个参数delimiter是一个String类型,它也可以定义为ValueSource类型,它还可以是从其他域或者其他函数计算后返回的值,在我们这里,我们假设我们的delimiter参数是显式的在请求中传递的。为了能够理解ConcatenateFunction类的输出,你需要查看getValues方法,这个方法返回一个FunctionValues对象,并且getValues方法必须返回FunctionValues类型,因为我们的concatenation函数的返回值是一个字符串,我们在内部使用StrDocValues类表包含这个返回值。StrDocValues类是FunctionValues类的一个实现类,它能够将Integer、Boolean等类型数据返回成一个String。FunctionValues类有很多子类实现,有些实现可能会使用到特定的缓存,因此如果你需要这方面的优化,那么你需要检出Solr的源码进行验证确认。StrDocValues对象内部包含了一个strVal(docid)方法,当Function被执行时,它会针对每个索引文档调用一次,正因为如此,所以对于一些执行开销比较大的复杂查询,你需要确保strVal方法能够尽快执行。

现在你已经知道了Function是如何计算并返回计算值的,下一步就是理解请求参数是如何传入ConcatenateFunction对象的。以下代码演示了如何解析输入参数并传入我们的自定义Function:

        public class ConcatenateFunctionParser extends ValueSourceParser {
            public ValueSource parse(FunctionQParser parser) throws SyntaxError {
                ValueSource value1 = parser.parseValueSource();
                ValueSource value2 = parser.parseValueSource();
                String delimiter = null;
                if (parser.hasMoreArguments()){
                    delimiter = parser.parseArg();
                }
                return new ConcatenateFunction(value1, value2, delimiter);
            }
        }

以上代码演示了如何使用FunctionQParser对象将输入参数进行解析并传入我们自定义的函数中。FunctionQParser按照标准的函数语法进行解析,比如functionName(input1, input2, …),根据请求的函数名称查找合适的ValueSourceParser实现类。可以通过调用FunctionQParser内部的parseValueSource()、parseArg()、parseFloat()等方法来获取传递给我们Function的输入参数。在ConcatenateFunctionParser示例中,我们期望获取两个ValueSource对象(可以是一个域,可以是用户输入的任意字符串或者其他函数的返回值)以及一个delimiter字符串参数,在从请求中读取到这些输入参数之后,我们创建了一个ConcatenateFunction对象并传入输入参数到其构造函数中。

实现自定义concatenation函数需要把创建的类都完成,剩下就是在solrconfig.xml中的<<config>>元素下进行注册,让Solr知道我们自定义的新函数。

        <valueSourceParser name="concat" class="sia.ch15.ConcatenateFunctionParser" />

上面的name属性表示注册的函数名称,函数名称怎么定义完全由我们决定。class表示我们自定义的FunctionParser实现类完整包路径。下面是我们自定义的concat函数的几个使用示例:

        concat("hello", "world", "-")              //返回"hello-world"
        concat("hello", "world", ", ")             //返回"hello, world"
        concat(123,456, ".")                       //返回"123.456"
        concat("no", "delimiter")                  //返回"nodelimiter"
        concat("hello", "world", "field1")         //返回"hellofield1world"

如果想要在查询中使用我们刚刚自定义的concat函数,那么可以这样使用:

        http://localhost:8080/solr/yourcore/select? q=*:*&
        fl=res:concat(concat(field1, field2, ", "), "! ")

上面的查询中我们演示了如何使用concat函数,先将field1和field2这两个域的域值使用逗号连接起来,然后将拼接后的值继续与感叹号!进行拼接。最终concat函数返回值作为“伪域”以别名res的方式返回。