Solr搜索统计 JSON Faceting API
Solr搜索统计 JSON Faceting API
大明搜索 发表于11个月前
Solr搜索统计 JSON Faceting API
  • 发表于 11个月前
  • 阅读 314
  • 收藏 9
  • 点赞 2
  • 评论 0

移动开发云端新模式探索实践 >>>   

摘要: 如果你一直用在Solr,并打算使用Solr来做一些统计分析的话,接下来的内容你会非常感兴&...

一、背景

我是您不知道的统计和聚合,我很漂亮、也很简洁,我是结构化,有些迷人的新查询语法。您可以不知道statsfacet,但你不应该不知道我,我是Solr JSON Facet API,出身于Solr5。

solr 5.3的时候完全重写了Solr查询语法,其中最为重要的就是重写Solr Facet查询语法。一直以来Solr在统计部分查询语法,以及Solr的函数的使用上饱受诟病。尤其是当Facet遇上stats时,她变得使我们疯狂。 比如一个简单的 sum函数或者unique函数都是没有直接提供的,想要做一个求和的操作是非常痛苦的事情。

为此,Solr完全重写了一套查询语法来支持和改善我们在统计方面的需求和体验。Solr5以后一直在往OLTP方向努力,她变得不只是一个搜索引擎,而且是一个NoSQL数据库。当然,她很早之前就定位在NoSQL数据库上,其实并没有往这方面发力。

二、JSON Facet查询语法

1. what is JSON Facet API

完全支持原有Facet统计,以及Faceet+stats组合功能,即是把原本Stats中非常好用的统计函数带到JSON Facet API中。除此之外,JSON Facet API还带新一个全新概念Facet Domain

简单的说,JSON Facet API = JSON Request PAI + Stats + Faceting。把几个特征合到一起的怪物即是我们的JSON Facet API了,这是非常非常棒的。

2. Facet Syntax

1. <facet_name> : { <type_type> : <facet_parameters> }
2. <facet_name> : { type : facet_type, <facet_facet_parameters>}

这语法非常简单,为了你可直观感受我们JSON Facet的魅力,我们先来看个比较有意思的例子。

Q1

curl http://solr.daming.loc:89893/solr/daming/select?rows=0&json.facet={
        total_price:sum(price), 
        price:avg(price)
    }

respones :   
{
    "response":{"numFound":250126,"start":0,"docs":[]},
    "facets":{
        "count":250126,
        "total_price":3.372768789E9,
        "price":13484.548634460922
    }
}

直接通过一个聚合函数直接来计算你想要的结果,这个在之前做法是要通过stats component来完成。

curl http://solr.daming.loc:8983/solr/daming/select?rows=0&stats=true&stats.field=price

response :
{
    "response": {
        "numFound": 250121,
        "start": 0,
        "docs": []
    },
    "stats": {
        "stats_fields": {
            "price": {
                "min": 1.0,
                "max": 2545000.0,
                "count": 250121,
                "missing": 0,
                "sum": 3.372768789E9,
                "sumOfSquares": 1.01177312029675E14,
                "mean": 13484.548634460922,
                "stddev": 14922.50991024902
            }
        }
    }
}

此时,你可能会觉得原来语法会更简单。确实,从查询语句来说,着实是原来方法简洁一些,但是我们再来看一下Response的内容。新版API查询的响应结果就比较统一,对于旧版本API来说,每种组件的结果集都是千奇百怪,各不一样的。这给接口用调用方带来N多问题,提供方也很烦,在写接口文档的时候,需要做大量解释。

Q2

上面的例子想说JSON Facet API好,其实是比较牵强的,那么我们继续来看,直至说服来用我们新版API。

前提:这是一张销售表,表其中有订单ID、用户ID、交易平台、单价、数量、成交金额等

需求,统计每个平台交易情况,含用户数、交易总额、客单价等

即是分平台统计,独立用户数、对成交金额字段进行一次Sum计算、对成交金额进行Avg计算。用Sql表达即是

select 
    platform, count(distinct user_id), sum(price), avg(price), count(*) 
from 
    order 
group by 
    platform
  • JSON Facet API
curl http://solr.daming.loc:8983/solr/daming/select?rows=0&json.facet={
        'platform' : {
            type : terms, // facet_type
            field : platform, // facet_facet_parameters
            facet : {
                user_amount : 'unique(user_id)', // 这里必须有`'`或者`"`
                total_price : 'sum(price)', 
                mean_price : 'avg(price)'
            }
        }
    }

Response: 
{
    "response": {"numFound": 250553, "start": 0, "docs": [] },
    "facets": {
        "count": 250553,
        "platform": {
            "buckets": [
                {
                    "val": "android",
                    "count": 69260,
                    "user_amount": 41071,
                    "total_price": 1.158913603E9,
                    "mean_price": 16732.79819520647
                },
                {
                    "val": "ios",
                    "count": 47599,
                    "user_amount": 26138,
                    "total_price": 9.8423645E8,
                    "mean_price": 20677.670749385492
                },
                {
                    "val": "wap",
                    "count": 39708,
                    "user_amount": 29992,
                    "total_price": 5.2660921E8,
                    "mean_price": 13262.043165105268
                },
                {
                    "val": "pc",
                    "count": 93986,
                    "user_amount": 61574,
                    "total_price": 7.08147217E8,
                    "mean_price": 7534.603206860596
                }
            ]
        }
    }
}
  • Stats Component & Facet 上面看了JSON Facet API感觉还好,比较简洁。下面来看看stats & facet component这个Query也能做我们上面想做的事情。这些我就不谈Stats效率与Facet效率的差异,您可以亲自测试一下,Facet效率要略高。
curl http://solr.daming.loc:8983/solr/daming/select?rows=0&&stats=true&stats.field={!mean=true+sum=true}price&stats.field={!countDistinct=true+count=true}user_id&wt=xml&omitHeader=true&stats.facet=platform

{
    "response": {"numFound": 250553, "start": 0, "docs": [] },
    "stats": {
        "stats_fields": {
            "price": {
                "sum": 3.37790648E9,
                "mean": 13481.804169177778,
                "facets": {
                    "plat": {
                        "pc": {
                            "sum": 7.08147217E8,
                            "mean": 7534.603206860596
                        },
                        "android": {
                            "sum": 1.158913603E9,
                            "mean": 16732.79819520647
                        },
                        "wap": {
                            "sum": 5.2660921E8,
                            "mean": 13262.043165105268
                        },
                        "ios": {
                            "sum": 9.8423645E8,
                            "mean": 20677.670749385492
                        }
                    }
                }
            },
            "user_id": {
                "count": 250553,
                "countDistinct": 147045,
                "facets": {
                    "plat": {
                        "pc": {
                            "count": 93986,
                            "countDistinct": 61574
                        },
                        "android": {
                            "count": 69260,
                            "countDistinct": 41071
                        },
                        "wap": {
                            "count": 39708,
                            "countDistinct": 29992
                        },
                        "ios": {
                            "count": 47599,
                            "countDistinct": 26138
                        }
                    }
                }
            }
        }
    }
}

这些用Local ParameterFunction Query的内容才能做到count(distinct field),才能把结果集做成这么简洁漂亮的哈。

如果,您不觉得JSON Facet API还不够有魅力吸引你,那没关系我们不再举例了,我们接着往下看吧。

三、 Type of Faceting

1. Facet Functions

不管您知不知道,Stats是提供一堆的聚合函数的,我估计您不知道。当然很多同学都不知道,因为它并不好用,或者说非常不好用。不过没关系,这一囧境已经改善了,Solr如您所愿的、如您所期待的一样美好。 我们直接来看Facet Functions,她提供八个非常常用的函数。其实,现在已经支持标准差了。

  • 函数列表
functionexampleEffect
sumsum(sales)summation of numeric values
avgavg(popularity)average of numeric values
sumsqsumsq(rent)sum of squares
minmin(salary)minimum value
maxmax(mul(price,popularity))maximum value
uniqueunique(state)number of unique values (count distinct)
hllhll(state)number of unique values using the HyperLogLog algorithm
percentilepercentile(salary,50,75,99,99.9)calculates percentiles

stats是支持select distinct(field) from table。 但JSON Facet API暂时还不支持,也还没收到风什么时候会支持,这有点小可惜。

hllunique提供功能基本一样,就是在算法实现有些差异。希望,以后有机会来谈谈Facet Functions的效率问题。

2. 不一样的排序

貌似,如果我没有记错的话,Solr之前并不支持sort by aggregation_function(field)。由于这个Solr之前没有支持,所以用SQL来描述一下吧。


select 
    sum(price) as x 
from 
    table_1 
sort by 
    x desc

这个功能在Solr之前的版本并不支持,在JSON Facet API支持,同时写法依然漂亮和简单。

其实也不能说Solr不支持sort by function也不对,因为Field Value Facet是支持sort by count。对其它的函数貌似还真就不支持了。

curl http://solr.daming.loc:8983/solr/daming/select?rows=0&json.facet={
        type : terms,
        field : cat, 
        sort : {x : desc}, // new sort by sum function
        facet : {
            x : "sum(price)",
            y : "avg(price)"
        }
    }

四、Facet Type

按Yonik的说法,JSON Facet分两类,一种是把数据按条件分桶;第二种对每个桶的文档进行聚合、统计。前面介绍了一些聚合、统计的功能,这些都属于第二类。接下来,我们来看看另一种类型,这种类型可以直接对应之前Facet Component。我们知道之前Facet Component提供多种Facet查询,详细可以看看Wiki

原来Solr提供以下几种Facet类型,我们也会按着原来Facet API的类型来讲。

  1. Arbitrary Query Faceting
  2. Field Value Faceting
  3. Facct by Range
  4. Pivot Faceting
  5. Interval Faceting
  6. Query Facet

除此之外,还有一个叫Date Faceting,这个并没有什么意义,直接用Facet by Range即可。 另一个叫Multi_select Faceting,这个JSON Facet API同时有支持,将来有机会再来看,今天不介绍这一块内容。

1. Terms Facet

Terms Facet即是Field Value Faceting。具体的一些参数名完全一样,所以也不再介绍了。我们今天只看她怎么用,当然怎么用这个事情,我们之前介绍JSON Request API时已经介绍过她的规则了。

json.facet={ // 我们用的一个高级JSON,格式上她相对比较随意,可以有引号包着,也可能没有引号。
    my_terms : {
        type : terms,
        field : platform,
        mincount : 1
    }
}

这样之后就变得非常简单和清晰了,不像之前那样冗长。 其实也没啥,把原本的查询结构化,之前Solr查询大家应该是很熟悉的了,它是这样的/select?rows=0&facet=true&facet.field=price&facet.mincount=1

如你所有原本Field Value Facet并不支持多个Field同时进行Facet,但是JSON Facet API已经支持的。因为她有Buckets的概念,所以她就可以有一些好玩的东西。

另外有两个参数是之前Faceting并没有支持的

  • numBuckets

    A boolean. If true, adds “numBuckets” to the response, an integer representing the number of buckets for the facet (as opposed to the number of buckets returned). Defaults to false.

  • allBuckets

    A boolean. If true, adds an “allBuckets” bucket to the response, representing the union of all of the buckets. For multi-valued fields, this is different than a bucket for all of the documents in the domain since a single document can belong to multiple buckets. Defaults to false.

2. Facet by Range

原本的Facet就已经支持Facet by Range的了,跟Terms Facet一样,我并没有太多想说的东西。直接来看示例吧。

json.facet={
    my_range : {
        type : range,
        field : age,
        start : 18, 
        end : 34,
        gap : 2
    }
}

还原成旧版本API即是,/select?facet=true&facet.range=age&f.age.facet.range.start=18&f.age.facet.range.end=34&f.age.facet.range.gap=2。 还好,我们一般都只用这三参数,如果再多两个参数,我想我一定会崩溃的。接下来我们来看看另三个不学用参数,但我想你一定会有机会用到的。

  • 1.hardend

    A boolean, which if true means that the last bucket will end at “end” even if it is less than “gap” wide. If false, the last bucket will be “gap” wide, which may extend past “end”.

  • 2.other

    This param indicates that in addition to the counts for each range constraint between facet.range.start and facet.range.end, counts should also be computed for…

    1. "before" all records with field values lower then lower bound of the first range
    2. "after" all records with field values greater then the upper bound of the last range
    3. "between" all records with field values between the start and end bounds of all ranges
    4. "none" compute none of this information
    5. "all" shortcut for before, between, and after
  • 3.include

    By default, the ranges used to compute range faceting between facet.range.start and facet.range.end are inclusive of their lower bounds and exclusive of the upper bounds. The “before” range is exclusive and the “after” range is inclusive. This default, equivalent to lower below, will not result in double counting at the boundaries. This behavior can be modified by the facet.range.include param, which can be any combination of the following options…

    1. "lower" all gap based ranges include their lower bound
    2. "upper" all gap based ranges include their upper bound
    3. "edge" the first and last gap ranges include their edge bounds (ie: lower for the first one, upper for the last one) even if the corresponding upper/lower option is not specified
    4. "outer" the “before” and “after” ranges will be inclusive of their bounds, even if the first or last ranges already include those boundaries.
    5. "all" shorthand for lower, upper, edge, outer

3. Query

这种类型我觉得是最最最简单的,即是查询统计。只是她支持多个查询统计一起查询,并在同一个结果集里展示。若非如此,我并不觉得原本的Facet API要Facet Query有什么意义。当然,她还是跟stats结合搞出一些事情的。

但是,但是我们是在谈新版本API:JSON Facet API。她赋予Query Faceting有更多更多的意义。简单的说,我们所有Faceting都同时支持Faceting Function/Faceting Domain配合使用。这使得她变得更加有意思。

来看一个示例。

json.facet={
    my_query : {
        term:query,
        q : 'version:5.3.0',
        facet : {
            x : 'unique(min_version)',
            y : 'sum(lines)'
        }
    }
}

结果是:

{
    "response":{"numFound":1000493,"start":0,"docs":[]},
    "facets":{
        "count":1000493,
        "my_query":{
            "count":2019, // Query Facet结果
            "x":14330, // unique(min_version) : count(distinct min_version)
            "y":1883333 // sum(lines)
        }
    }
}

4. Pivot Faceting : Decision Tree

这类似于SQL中的

select 
    country, province, count(city)
from 
    table_2
group by
    country, province

当然,她还可以继续分层,即是还能有townvillage等等。

在原本Facet API的表现也是非常简单的,可能比较JSON Facet API更简单。 /select?facet=tue&facet.pivot=country,province,city如此即可。

新版本的是

json.facet={
    country : {
        field : country,
        type : terms,
        facet : {
            province : {
                field : province,
                type : terms,
                facet : {
                    province : {
                        field : province,
                        type : terms
                    }
                }
            }
        }
    }
}

她是变得如此如此冗长,这有悖我们所期望的简洁和漂亮的初心。

其实JSON Facet API并没有提供Pivot Faceting,只是我们可以通过这个实现类似的功能。

OK!这个解释,我自己都觉得挺难服众的。

接下来,我们换个说法,或许您能接受。此时您应该能看出来,这是一个nested facet,即是嵌套Faceting。我们也知道新的Facet API支持一些Aggregation Functions,同时还支持Facet Domain(后续会介绍)。采用这种多层嵌套的方式,一方面使我们的结构统一;另一方面可以每一层多做一些事情,诸如Aggregation Functions或者做Facet Domain。

好吧好吧,我们大多都是在最后一层的时候来才会有做一些额外的统计的需求。所以,往后的版本还是有需要把Pivot Faceting加上的。

语法我都帮Yonik想好了,哈哈哈(纯粹是个人YY)

json.facet={
    type : pviot,
    field : [country,province,city]
}

5. Interval Faceting

这一个JSON Facet API依然还没有提供,但实际上并没有太大的影响(其实我想说没有任何影响来着)。因为,可以用Range of Faceting或者用Multi Query Faceting来代替。

五、结束语

前面我们谈一次JSON Request API,今天聊的这部分内容只是JSON Request API的一部分,也是非常非常重要的一部分。也是我极力想把她推广开来的一部分内容。

后续,我们将继续跟踪JSON Facet API新动态,同时也会继续更介绍她。如果没有意外的话,下一篇会介绍Facet Domain。之后,可能会来看看JSON Facet API的性能问题,尤其是count distinct方面的性能,特别当Count distinct遇上分布式架构时。count distinct会变得非常非常有意思,希望有机会跟大家来讨论这个问题。

  • 打赏
  • 点赞
  • 收藏
  • 分享
共有 人打赏支持
粉丝 11
博文 6
码字总数 15125
作品 1
×
大明搜索
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: