1. 程式人生 > >Solr搜尋統計 JSON Faceting API

Solr搜尋統計 JSON Faceting API

一、背景

我是您不知道的統計和聚合,我很漂亮、也很簡潔,我是結構化,有些迷人的新查詢語法。您可以不知道statsfacet,但您不應該不知道我,我是Solr JSON Facet API,出身於Solr5。

solr 5.3的時候完全重寫了Solr查詢語法,其中最為重要的就是重寫Solr Facet查詢語法。一直以來Solr在統計部分查詢語法,以及Solr的函式的使用上飽受詬病。尤其是當Facet遇上stats時,她變得使我們瘋狂,因為她十分複雜,非常不好用。

JSON Facet API基本上已經實現了SQL的能力,如果不考慮多表問題的話,即不跨多個Collection做JOIN等操作。

為此,Solr完全重寫了一套查詢語法來支援和改善我們在統計方面的需求和體驗。Solr5以後一直在往OLAP方向努力,她變得不再只是一個搜尋引擎,而且是一個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

不管您知不知道,StatsComponent是提供一堆的聚合函式的,我估計您不知道。當然很多同學都不知道,因為它並不好用,或者說非常不好用。不過沒關係,這一囧境已經改善了,Solr如您所願的、如您所期待的一樣美好。
我們直接來看Facet Functions,她提供八個非常常用的函式。其實,現在已經支援標準差了。

  • 函式列表
function example Effect
sum sum(sales) summation of numeric values
avg avg(popularity) average of numeric values
sumsq sumsq(rent) sum of squares
min min(salary) minimum value
max max(mul(price,popularity)) maximum value
unique unique(state) number of unique values (count distinct)
hll hll(state) number of unique values using the HyperLogLog algorithm
percentile percentile(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 
    cat, sum(price) as x 
from 
    table_1 
group by
	cat
sort by 
    x desc

這個功能在Solr之前的版本並不支援,在JSON Facet API支援,同時寫法依然漂亮和簡單。

其實也不能說Solr不支援sort by function也不對,因為Field Value Facet是支援sort by count。對其它的函式貌似還真就不支援了。

統計結果的排序,JSON Facet API的FieldFacet支援三種排序方式,根據indexcountAggregate Functions三種方式。index是表示在索引的位置,count即就是分組的數量,最後是通過聚合函式的計算結果,基本就已經覆蓋了所有使用場景了。

curl http://solr.daming.loc:8983/solr/daming/select?rows=0&json.facet.dm={
        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

這種型別我覺得是最最最簡單的,即是查詢統計。只是她支援多個查詢條件一起統計,並在同一個結果集裡展示。當她與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還要更簡單,主要是JSON Facet API有巢狀的特徵在,同時在每一層Terms Facet可能還有別的聚合函式。可能是基本這些原因,JSON Facet API沒有支援Pivot Facet的吧。

/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會變得非常非常有意思,希望有機會跟大家來討論這個問題。

最後的最後,為了方便我們在SolrJ實現這些功能,社群上已經有大佬在這個事了。估計會在Solr7.7release,具體可以關注一下這幾個ISSUEs,分別是SOLR-12947,SOLR-12965,SOLR-12981。其實,我在此之前也做了這些事情,不過我並沒有及時提交給社群,放在GitHut叫Facet-Helper有興趣的同學也可以瞭解一下。