solr facet - yaokun123/php-wiki GitHub Wiki

Facet

一、简介

Faceting是Solr中比较强大的一个功能,尤其是将其与传统的关系型数据库或者NOSQL数据库相比,你会越发觉得它的强大。Faceted Search又称Faceted Navigation(Faceted导航)或者Faceted Browsing(Faceted浏览)。它允许你运行一个查询,而后基于索引文档的某个维度或方面对查询匹配的索引文档进行高标准的分类。它允许你选择一个Filter对结果集进行过滤。

当你在一个新闻网站搜索时,可能希望能够通过时间帧帮你过滤出部分结果,比如过去1小时、过去24小时、上一周。或者通过分类帮你过滤,比如政治、科技和经济。当你在一个招聘网站上搜索,可能希望能够根据城市、工作类型、行业、公司名称等来过滤出部分你感兴趣的工作职位,并且可以显示每个分类下匹配的结果总数量。因为我们只能在网站上展示有限个数的分类项,所以大部分搜索程序经常会将那些比较热门流行的分类放在前面显示,从而使得用户能够快速鸟瞰整个搜索结果集,而不用单击每个分类进去查看。这里面的每个分类就相当于是一个Facet(维度)。

尽管Facet最基本的形式只是一个域的值列表,但Solr中的Facet允许你将一些动态元数据随着查询结果集一并返回给用户。同时Solr还提供了很多Facet的高级功能,比如基于Function计算结果值的Facet、基于区间值的Facet、基于任意查询的Facet。Solr还支持分层多维度的Facet以及多选的Facet,即便该Document已经被Filter Query过滤掉,依然能够被统计。

如果上面的概念比较官方不够直白,不好理解的话,那么我举例说明吧。比如学生可以按班级来分类、可以按性别来分类、可以按身高来分类、可以按年龄来分类、可以按考试分数来分类、可以按兴趣爱好来分类、可以按出生地址分类等,上面所说的班级、性别、身高、年龄、考试分数、兴趣爱好、出生地址等,这些都是把学生进行分类归组的一个个维度。可能有些人就要问了,这不就是分组吗?他跟分组有什么区别?乍一看貌似Facet跟Group是一个概念,其实Facet跟Group还是有点区别的,比如,按考试分数统计,我们一般不会说统计60分有几个人,61分有几个人,62分有几个人,63分有几个人......一直到100分,实际我们一般会这样统计:60分以下有几人,60~70分有几人,70~80分有几人,80~90分有几人,90~100分有几人。这里所说的60分以下、60~70分、70~80、80~90、90~100表示的是分数段,即数字范围,不是简简单单按照某个域进行分组,甚至会有更复杂的维度统计。比如,我要你统计各个分数段男生多少人、女生多少人,这其实就是多个查询条件组合成一个维度。说到这里我想大家应该都豁然开朗了,Facet即维度不仅仅是建立在某个域上,他可以建立在对某个域进行函数计算后得到的计算值上,它还可以建立在某个查询条件上,该查询条件你可以任意组合,这是Group分组所办不到的。如果你仅仅是对某个域进行Facet统计,那就跟Group类似了。你可以把Facet理解为Group的火力升级版,功能更强大!

Facet查询由两部分组成,即计算并显示Facet给用户和根据用户选定的值过滤结果集。此时,你可能想知道:到底Facet查询是什么?假如在手机APP上搜索外卖,你可能期望用户界面上能够提供多个维度作为导航元素方便你筛选外卖,正如图所示:

在图中,外卖类型、价格、区域就相当于3个Facet,每个Facet下是相关的值列表以及各项匹配的索引文档的统计数字,当你单击其中任意一项,会返回该项下匹配的索引文档,这就是Facet查询。但是后面的数字并不是必须统计的,像淘宝、京东这些电商网站的Facet查询部分仅仅只是罗列了每个Facet维度的子项。

二、Facet的简单示例

域说明如下:

id:表示主键域
name:即饭店的名称
city:表示所在城市
type:表示饭店的类型
state:表示饭店所在州
tags:表示体现饭店特色或优势的标签
price:表示饭店主营食物的价格

注:这里的所有域我们都定义成indexed=true且stored=true,只是为了演示目的,
实际项目中stored是不是设置为true需要根据需求决定。但Facet查询要求Facet Field必须是indexed=true,
如果一个域indexed不等于true那就无法在该域上进行查询,这点需要注意。

先从Solr最常见的方式开始:在指定域的每个唯一值上执行Facet查询即Field Facet。

当你执行一个Field Facet查询时,它会返回该域上每个唯一值列表以及每个唯一值匹配到的索引文档的总数。下面我们将学习如何创建一个Field Facet查询,以及学习Facet查询相关的请求参数。

http://localhost:8080/solr/restaurants/select?q=*:*&rows=0&facet=true&facet.field=name

上面我们演示了Solr中Facet查询的最基本形式:在指定域上执行Facet查询,在这种Facet查询中,会对域的域值进行分组统计,当然如果该域配置了分词器,那么就是对域的域值分词后得到的每一个唯一Term进行分组统计。

但并不是每个域的域值都是单个值,比如我们schema.xml中的tags域就是一个多值域,让我们看看在一个多值域上执行Field Facet查询会是什么情况?

http://localhost:8080/solr/restaurants/select?q=*:*&rows=0&facet=true&facet.field=tags

通过上面的查询结果,我们会发现:backfast(早餐)和coffee(咖啡)在restaurants测试文档中时出现频率比较高的两个。这是因为它们在测试文档中出现的频率比较高,而每个Trem后面的统计数字就是它们在索引文档中出现的总次数。对于那些索引文档中出现频率比较高的Trem你可以通过TagClound(标签云)的形式展现给用户,比如出现频率高的Trem使用大号字体,然后用户会有比较大的几率去单击比较醒目的Trem,从而实现导航用户的浏览行为。TagClound(标签云)对于用户来说是从分类的角度来鸟瞰查询结果的常用方法。

除了可以使用示例中的tags多值域进行Facet查询外,你还可以对纯文本域进行Facet。一般纯文本域需要分词处理,但可能会包含一些噪音词,比如停用词之类的,也许会干扰你的Facet查询,因此你需要对文本域配置停用词过滤器。

另外一点需要记住的是Facet查询是针对域的唯一值进行分组统计,如果该域是StringField,即不会进行分词处理,那么就直接根据该域的每个域值进行分组统计。但是对于TextField,那么就是对分词后得到的每个Token进行分类统计,统计的数字就是每个Token在所有文档中出现的总次数。对于Solr开发者,我们一般建议单独建一个新域用于Facet查询,将值域复制到新的域,这里你可以使用solr中的复制域。

此时你应该对Facet有个初步感觉了,对在单值域和多值域上执行Facet查询也基本熟悉了。目前还只是使用Facet的默认设置来执行查询。只对20个索引文档执行Facet查询看起来非常的容易,假如我们有上百万的Trem呢?Solr提供了很多Facet参数允许你对Facet查询进行调整或干预Facet查询行为。

参数 描述
facet true/false 表示对哪个域执行Facet Search,此参数可以指定多次
facet.field 任意indexed=true的域 表示对哪个域执行Facet Search,此参数可以指定多次
facet.sort index/count Count表示按照统计数字大小排序,或者按照trem的字母表顺序进行排序,默认值为count
facet.limit >=-1的Integer数字 表示每个facet组只返回前N项,此参数可以对每个facet域进行设置
facet.mincount >=0的Integer数字 表示统计的每一项数字的最小值,此参数可以对每个facet域进行设置
facet.method enum/fc/fcs method=enum时会遍历索引中的所有Term,计算这些Term的交集。method=fc(即File Cache的缩写)时会遍历匹配facet查询的所有文档,然后找出包含该term的文档。当域包含的唯一值只是几个,那么使用enum方式会更快一点。对于所有域(Boolean域除外),facet method默认值是fc。fcs会对单值StringField域提供基于每个段文件的Field Cache(域缓存),当你的索引数据需要频繁更新时,对于StringField使用fcs,执行性能会更好。它还可以接受一个本地参数,即通过指定处理不同段文件的线程数参数来加快facet的执行速度。此参数可以对每个facet域进行设置
facet.enum.cache.minDf >=0的Integer数字 高级参数,表示Term至少匹配多少个Document才启用域缓存,若此参数设置为0,则表示始终都启用域缓存,默认值就是零。若此参数设置为大于0,则可以减少内存占用,但却是以减缓Facet查询速度为代价的。一般这个值推荐设置为20~50,他并不影响最终的Facet返回结果,只是影响Facet查询性能。此参数可以对每个Facet域进行设置
facet.prefix 任意字符串 表示只对指定前缀的Term进行Facet查询,对于想要查找相似Term或用于构建autocomplete功能时会有用
facet.missing true/false 表示是否对空值Term也进行Facet统计,默认值false
facet.offset >=0的Integer数字 用于指定Facet查询返回结果集的偏移量,偏移量从0开始计算。当你需要对Facet查询返回的结果集进行分页时会使用
facet.threads Integer数字 用于指定当对多个域执行Facet查询时使用的处理线程个数。默认值为零,表示默认情况下,会使用单线程串行的方式执行每个域上的Facet查询。此参数设置为正数,则表示会创建指定数量的线程以多线程并行的方式去执行Facet查询。设置为负数即表示线程数不受限制。当你有多个域需要执行Facet查询时,采用多进程方式会明显加快Facet查询速度。注意:此参数只适用于Field Facet Query,并且当你的Solr查询并发量很大时请不要开启此参数,比如互联网电商项目中请不要使用。

facet.field参数可以指定多次,比如像下面这样

&facet.field=tags&facet.field=type

有些Facet参数支持对每个域单独设置,基本语法如下:

f.<fieldName>.<FacetParameter>=<value>

其中<fieldName>表示域名称,<FacetParameter>表示Facet参数名称,<value>表示该参数对应的参数值

三、Query Facet

除了能够对任意的索引域执行Facet查询之外,你还可以对任意的子查询统计匹配的索引文档总个数,而后你能够根据统计的数据进行分析。Solr提供这种功能,它被称为Query Facet。

演示这种功能的最好的方式就是通过示例。假如你想通过一个查询,统计处价格落在[5,25]区间内的饭店,但同时你又想了解美国东岸、西岸和中部各有多少饭店。你当然可以通过4个独立的查询来实现这个需求,如下所示

//查询价格落在[5,25]区间的饭店
http://localhost:8080/solr/restaurants/select?q=*:*&fq=price:[5 TO 25]

//过滤出价格落在[5,25]区间内美国东部饭店个数
http://localhost:8080/solr/restaurants/select?q=*:*&fq=price:[5 TO 25]&fq=state:("New York" OR "Georgia" OR "South Carolina")

//过滤出价格落在[5,25]区间内美国西部饭店个数
http://localhost:8080/solr/restaurants/select?q=*:*&fq=price:[5 TO 25]&fq=state:("Hlinois" OR "Texas")

//过滤出价格落在[5,25]区间内美国中部饭店个数
http://localhost:8080/solr/restaurants/select?q=*:*&fq=price:[5 TO 25]&fq=state:("California")

上面采用的方式通过4此独立查询虽然也能达到同样的目的,但是这种方式并不可取,下面我们将演示如何使用Query Facet将多个查询合并为一个查询

http://localhost:8080/solr/restaurants/select?q=*:*&
fq=price:[5 TO 25]&
facet=true&
facet.query=state:("New York" OR "Georgia" OR "South Carolina")&
facet.query=state:("Hlinois" OR "Texas")&
facet.query=state:("California")

查询结构如下所示:
"facet_counts""{
    "facet_queries"{
        "state:("New York" OR "Georgia" OR "South Carolina")":5,
        "state:("Hlinois" OR "Texas")":3,
        "state:("California")":3
    }
}

正如你看到的那样,多个子查询可以通过facet.query参数结合成一个查询请求。同理,也可以将多个价格区间组合成一个Query,这样可以重构我们的查询请求,如下所示

http://localhost:8080/solr/restaurants/select?q=*:*&rows=0&facet=true&
facet.query=price:[* TO 5]&
facet.query=price:[5 TO 10]&
facet.query=price:[10 TO 20]&
facet.query=price:[20 TO 50]&
facet.query=price:[50 TO *]&

返回的查询结构如下所示:
"facet_counts":{
    "facet_queries":{
        "price:[* TO 5]":6,
        "price:[5 TO 10]":5,
        "price:[10 TO 50]":3,
        "price:[20 TO 50]":6,
        "price:[50 TO *]":0
    }
}

上面这个示例演示了如何在任意一个Solr Query中构建一个Facet查询并返回统计信息。其实,在上面示例中,你也可以创建一个price_range域即表示当前price落在哪个区间,比如price_range域的域值可以是这样的:2-50,表示价格在[2,50]之间,这样你在添加索引文档时就需要确定每个price落在区间内,然后赋值为price_range域并写入到索引中。 这样你就只需要根据price_range一个域进行Facet查询了,但是这样做有个弊端,即未来如果price域数据有变化,那么price_range域也要随着更新,你的索引需要重建。这是一个非常痛苦的过程,他别是当你的索引文档数量在不断递增膨胀时,针对这个问题Query Facet提供了一个不错的选择。它允许在查询时非常灵活的指定重新定义哪些Facet Value应该被计算统计和返回。

四、Range Facet

Range Facet顾名思义,它表示Facet区间范围查询,一般用于对数字或日期的区间范围查询。这类似与我们普通的区间范围查询,但Range Facet会统计每个区间匹配的索引文档总数。以前当你有多个区间范围时可能需要指定3个facet.query参数来实现,通过Range Facet可以简化你的查询,使用示例如下所示:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.range=price&
facet.range.start=0&
facet.range.end=50&
facet.range.gap=5

上面示例中facet.range参数表示对哪个域执行Facet区间查询,facet.range.start参数表示区间的上限值,facet.range.end表示区间的下限值,facet.range.gap参数按照每个区间分布多少个值进行自动区间划分,返回的查询结果如下所示:

"facet_ranges":{
    "price":{
        "counts"[
            "0.0",6,
            "5.0",5,
            "10.0",0,
            "15.0",3,
            ........,
            "45.0",1
         ],
        "gap":5.0,
        "start":0.0,
        "end":50.0
    }
}

首先Range Query会统计落入每个区间内的索引文档的总个数,尽管有些区间内并没有任何索引文档,但任然返回了该区间统计信息。然后每个区间不再是人工一个个去指定,而是通过指定一个facet.range.gap参数在限定的facet.range.start和facet.range.end参数之间自动进行区间分割,facet.range.gap越大,分割出的每个区间范围越大,gap的值如何设置取决于你的项目需求。通过这种自动分割区间的方式确实能够为我们节省很多时间,特别是区间范围很多的时候,你不用再动手指定N个facet.query参数。

这里还有其他的可选参数,比如facet.range.hardend,facet.range.other,facet.range.include下面详细描述了每个参数的含义:

参数 描述
facet.range 任何indexed=true的数字域或date域的名称 指定你需要在哪个域上面执行Facet Range Query,此参数可以指定多次
facet.range.start 数字域或date域的区间范围上限值即最小值 区间范围的上限值,此参数可以对每个Facet域进行设置
facet.range.end 数字域或date域的区间范围下限值即最大值 区间范围的下限值,此参数可以对每个Facet域进行设置
facet.range.gap 对于date域而言,gap值可以为DateMath表达式,比如+1DAY、+2MONTHS、+1HOUR,对于数字域,你只需要指定一个数字即可 gap即表示区间递增的公差,用于按照指定的gap(间隔)对 [facet.range.start,facet.range.end]区间,进行自动划分,生成多个子区间,此参数可以对每个Facet域进行设置
facet.range.hardend true/false 比如你facet.range.start=2015-01-01,而facet.range.end=2015-09-20,假如你facet.range.gap=+1MONTH即表示按一个月把start与end之间的时间根据gap值分成9份,如果hardend为true,那最后一份的时间范围是2015-09-01至2015-09-20,如果hardend为false,那最后一份的时间范围是2015-09-01至2015-10-01,即直接无视end参数的限制,严格按照gap的间隔来算,此参数可以对每个Facet域进行设置
facet.range.other before/after/between/all/none before:表示需要对start之前的日期做个统计,after:表示需要对end之后的日期做个统计,between:表示需要对start与end之间的日期做个统计,none:表示不做任何汇总统计,all:表示before、after、between都需要做统计,此参数可以对每个Facet域进行设置
facet.range.include lower/upper/edge/outer/all lower:表示划分的所有子区间都包含上限值即最小值,upper:表示划分的所有子区间都包含下限值即最大值,edge: 表示划分的第一个子区间包含上限值即最小值,最后一个区间都包含下限值即最大值,outer:表示当你的facet.range.other设置为before或after时,是否包含before或after这两个边界值。all:表示分别指定上面四个参数。此参数可以指定多次,且可以对每个域进行单独设置。

对于Field Facet,你可以通过f..=语法为每个域指定Range Facet相关参数。

当你在对数字域或日期域进行区间范围的Facet查询时使用Range Tacet语法显然要比使用普通的Query Facet更方便更简洁。当Range Query变得超级复杂时,你可以选择使用多个独立的Query Facet来分解Range Facet。在上面3中查询中,Field Facet是使用最广泛的一种,而且它使用起来足够简单。

五、FacetFilter

基本上,在Facet查询上应用一个Filter Query相比在Query上应用一个Filter Query并不难。假设在3个维度进行查询统计,分别是state域、city域、以及根据price域的一个查询。那么你可以这样查询。

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.field=state&
facet.field=city&
facet.query=price:[* TO 10]&
facet.query=price:[10 TO 25]&
facet.query=price:[25 TO 50]&
facet.query=price:[50 TO *]

返回的查询结果如下所示:

"facet_queries":{
    "price:[* TO 10]:11",
    "price:[10 TO 25]:5",
    "price:[25 TO 50]:4",
    "price:[50 TO *]:0",
    "facet_field":{
        "state":[
            "Georgia",6,
            "California",4,
            "New York",4,
            "Texas",3,
            "Illinois",2,
            "South Carolina",1
         ],
         "city":[
            "Atlanta,GA",6,
            "New York,NY",4,
            "Austin,TX",3,
            "San Francisco,CA",3,
            "Chicago,IL",2,
            "GreenVille,SC",1,
            "Los Angeles,CA",1
         ]
    }
}

我们以及知道如何为一个普通Query添加Filter Query,那么如果为Facet查询也添加一个Filter会发生什么呢?请看下面示例:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field=state&
facet.field=city&
facet.query=price:[* TO 10]&
facet.query=price:[10 TO 25]&
facet.query=price:[25 TO 50]&
facet.query=price:[50 TO *]&
fq=state:California

上面的查询中,我们为Facet Query添加一个Filter Query,最终返回结果如下所示:

"facet_queries":{
    "price:[* TO 10]:2",
    "price:[10 TO 25]:0",
    "price:[25 TO 50]:2",
    "price:[50 TO *]:0",
    "facet_field":{
        "state":[
            "California",4,
         ],
         "city":[
            "San Francisco,CA",3,
            "Los Angeles,CA",1
         ]
    }
}

上面示例中的fq参数表示的Filter Query作用于state域,同时对Field Query和Query Facet返回的结果集进行过滤,这点类似于普通Query上的Filter Query。由于Filter Query会过滤掉不符合条件的索引文档,因此会影响每个Facet查询最终统计的数字。此外,你可以为Facet Query添加多个Filter Query,请看下面查询示例:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field=state&
facet.field=city&
facet.query=price:[* TO 10]&
facet.query=price:[10 TO 25]&
facet.query=price:[25 TO 50]&
facet.query=price:[50 TO *]&
fq=state:California&
fq=price:[* TO 10]

上面示例中我们为Facet Query应用了两个Filter Query,对Facet查询返回的索引文档进行过滤,不符合条件的索引文档不进行Facet统计,最终返回的结果集如下啊所示:

"facet_queries":{
    "price:[* TO 10]:2",
    "price:[10 TO 25]:0",
    "price:[25 TO 50]:0",
    "price:[50 TO *]:0",
    "facet_field":{
        "state":[
            "California",4,
         ],
         "city":[
            "San Francisco,CA",2,
         ]
    },
    "facet_dates":{},
    "facet_range":{}
}

正如你看到的那样,价格区间的Facet查询只有第一个区间有统计数字,其他区间由于都不符合fq=price:[* TO 10]这个条件,所以最终返回的索引文档总个数都为0.

我们的示例中的域都是单值域StringField,因此如果单值域域值不符合Filter Query的查询条件,那么就会导致对该域进行Facet Query时该域的某些值统计出来的数值为0,这对于单值域来说是有意义的。但是如果域是分词域(即域类型为TextField)或者多值域(即multivalued=true的索引域,比如我们示例中的tag域)时,在这些域上执行Facet查询,会返回什么结果呢?那么,请看下面的示例:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field=name&
facet.field=tags&

上面的示例中,我们对name和tags域分别进行Facet查询统计,其中tags域是多值域,最终返回结果如下所示:

"facet_field":{
        "name":[
            "Starbucks",4,
            "McDonalds",5,
            "Pizza Hut",3,
            "Red Lobster",3,
            "Freddy's Pizza Shop",1,
            "Sprig",1,
            "The Iberian Pig",1,
         ],
         "tags":[
            "breakfast",11,
            "coffee",11,
            "sit-down",8,
            ......(太多)
            "upscale",1,
         ]
    }

你会发现多值域的值列表中的每一项都会单独统计,分词域同理。tags域上Facet查询返回的项太多,同样可以为其添加Filter Query进行过滤,比如我们希望tags域上的Facet查询只返回包含coffeee的索引文档的总个数,那么此时可以像下面这样执行查询:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field=name&
facet.field=tags&
fq=tags:coffee

返回的查询结果如下所示:

"facet_field":{
        "name":[
            "Starbucks",6,
            "McDonalds",5
         ],
         "tags":[
            "breakfast",11,
            "coffee",11,
            "fast food",5,
            "hamburgers",5,
            "wi-fi",5,
         ]
    }

同理,你也可以为多值域的Facet Query添加多个Filter Query。请看下面的查询示例:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field=name&
facet.field=tags&
fq=tags:coffee&
fq=tags:hamburgers

上面的示例中,我们为Facet Query添加了两个Filter Query,用于对Facet Query进行过滤,他只是最终每项返回的统计数字,并不会过滤掉某些统计项,最终返回的查询结果如下所示:

"facet_field":{
        "name":[
            "McDonalds",5
         ],
         "tags":[
            "breakfast",5,
            "coffee",5,
            "fast food",5,
            "hamburgers",5,
            "wi-fi",5,
         ]
    }

你会发现对于多值域,如果多值域列表中有部分不符合Filter Query的查询条件,那么符合查询条件的项仍然会被统计并返回的。 我们在上面示例中指定的每个Filter Query其实是毫无意义的,实际上,你可以将fq=tags:coffee&fq=tags:hamburgers改写成fq=tags:(coffee AND hamburgers)。这种写法会减少在Filter缓存中查找的次数,而且也更容易控制多个Filter Value之间的交互。完全没有必要在Facet Query中使用AND操作符去连接多个fq。

前面的示例中我们大都是在对单值域进行Facet Query,但实际上,通常更具挑战性的是我们需要对多值域或分词域进行Facet Query。举个例子,假如在一个分词域上进行Facet Query,其中有一个值域为Los Angeles,进过分词后生成两个token,即Los、Angeles(假设不考虑大小写转换和词干还原),若我们的Filter Query表达式是fq=city:Los Angeles,此时实际含义是查询包含Los、Angeles的索引文档,然后统计符合要求的索引文档总个数。为了在fq中支持使用空格分割的多个Term的Phrase Query,你需要将多个Term使用双引号包裹起来。因此此时的查询语法就是:fq=city:"Los Angeles"。当你盲目的对多个Trem使用双引号进行包裹,为Facet Query添加Filter Query时,可能会遇到问题。如果使用双引号包裹多个Trem,那么查询语法会被破坏。除非你对查询表达式中特殊字符进行转义。因此,如果你的查询表达式中已经包含了双引号,那么此时需要将查询表达式的双引号转义成",比如fq=name:"the"in"crowd"。双引号和转义双引号已经够让你心烦了,但是还需要小心谨慎得失Solr对于分词域的处理后生成了哪些Token,因为后续对该分词域进行Facet Query时,需要对分出来的每个Token进行统计查询,你需要在脑海中清楚内部这种分词行为是如何工作的以及最终会生成什么,而且分词还分索引阶段和查询阶段,这又为你增加了点复杂度,庆幸的是,Solr以及为我们提供TermQParserPlugin来解决此类问题,比如你使用fq={!term}the "in"crowd来构建一个TermQuery,此时不需要考虑双引号转义问题。

使用Term Query Parser来构建Facet Query上的Filter Query的一个缺点就是它不支持Boolean语法,所以如果你想在一个Filter Query里连接多个Facet值,可能需要使用Nested Query Parser来实现,下面分别演示两个方式连接你的Filter Query:一种就是通过Term Query Parser定义多个Filter Query;另一种就是通过Nested Query Parser连接两个Term Query,而后在Filter Query中通过AND操作符连接两个_query_(Nested Query 即嵌套查询,它能够将任意查询转换成_query_这个伪域)。

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field=name&
facet.field=tags&
fq={! term f=tags}coffee&
fq={! term f=tags}hamburgers

//通过Nested Query Parser定义多个伪域_query_,然后在一个Filter Query中通过AND操作符连接两个伪域

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field=name&
facet.field=tags&
fq=_query_:"{! term f=tags}coffee" AND _query_:"{! term f=tags}hamburgers"

Notice:有些内容尚未覆盖到,比如处于友好显示目的为Facet重命名,以及已经被Filter Query过滤掉的索引文档如何也纳入Facet统计。

六、Multiselect Faceting

当我们发起一个Facet Query,Facet返回Facet名称可能并不是我们想要的,为此,Solr允许你再Facet Query返回结果之前修改Facet的显示名称,以更友好的名称返回给用户。对于那些已经被Filter Query过滤掉的Document,Solr也允许将其纳入Facet统计之内,Solr中将这种功能称为Multiselect Faceting。

即Filter Query能过滤查询结果集中将要返沪的索引文档,但并不影响最终统计的索引文档总个数,看起来就像Filter Query并没有起到作用一样。

1、key

所有的Facet(维度)都有一个方便开发者区分它们彼此的名称,如果是Field Facet或Range Facet,那么Facet的名称就是Field的名称,如果是Query Facet,那么Facet的名称就是query的查询表达式或者Facet Value或Function动态计算的值。通过使用key本地参数,你可以很容易的对任何Facet名称进行重命名,具体请看下面示例:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.field={! key="Location"}city&
facet.query={! key="<$10"}price:[* TO 10]&
facet.query={! key="$10-$25"}price:[10 TO 25]&
facet.query={! key="$25-$50"}price:[25 TO 50]&
facet.query={! key=">$50"}price:[50 TO *]&

最终返回的查询结果如下所示:

"facet_queries":{
    "<$10":11,
    "$10-$25":5,
    "$25-$50":4,
    ">$50":0,
},
"facet_fields":{
    "Location"[
        "Atlanta,GA",6,
        ......
        "Los Angeles,GA",1,
    ]
}

Facet的重命名功能在很多场景下非常有用,它允许你的搜索程序请求Query Facet。举个例子,你不需要在接收到Facet Query返回的结果集之后的处理阶段去理解底层的Query,Facet返回的结果集会以比较友好的名称显示每项统计数据,而你也不用关心当前Facet Query查询在底层是基于哪个域或者关联哪个Query。此外可以为每个Facet定义一个唯一的名称,通过指定key参数的方式可以为同一个域指定多个唯一的名称,这对于Field Facet和Range Facet来说会比较有用。同时多个域也可以映射到同一个名称上,不过这依赖于你查询时定义的查询条件。

2、tag

Solr中提供tag参数,它能够为某些Facet的Filter Query打上标签,以便于你能够控制它与Solr其他功能之间的交互。 当一个Filter Query应用到一个Solr Query请求上时,最终返回的结果集是求Query查询返回的结果集与Filter Query查询返回的结果集之间的交集。默认情况下,Facet Query也是这种查询机制。不符合Filter Query的Facet Value以及被排除在外,不会纳入Facet查询统计,这在大多数情况下是有用的。

但是这种查询机制仍然存在问题,比如你想查询统计"California"州下的饭店数量,你可能会应用一个Filter Query:fq=state:California。此时其他州的饭店数量就不会统计了,如果你仍然希望能够统计其他州的数据,以便于继续单击其他州继续你的浏览查询行为,而不是通过浏览器返回到上一级页面,那么你就需要使用Facet Exclusion功能。

Facet Exclusion功能允许你将那些已经被应用在域Facet Query之上的任意Filter Query移除掉的索引文档重新添加到Facet查询统计中。这样在统计Facet数量的时候完全可以忽略任意Filter Query的影响,而应用于Facet Query之上的Filter Query将只会影响最终返回的索引文档,但并不影响每个Facet统计的文档总个数。实现这种功能你需要tag和ex这两个本地参数。下面的示例演示如何使用这两个参数:

http://localhost:8080/restaurants/select?q=*:*&facet=true&
facet.mincount=1&
facet.filed={! ex=tagForState}state&
facet.filed={! ex=tagForCity}city&
facet.query={! ex=extagForPrice}price:[* TO 5]&
facet.query={! ex=extagForPrice}price:[5 TO 10]&
........
facet.query={! ex=extagForPrice}price:[50 TO *]&
fq={! tag="tagForState"}state:California

上面的示例中,重点就是我们为Facet Query定义了Filter Query,即过滤掉不在California州的饭店,同时通过tag参数为Filter Query打上了一个标签,名为tagForState,标签名称是可以随意取的。然后在每个Query Facet上通过ex参数来应用我们刚刚打的那个标签,应用一个标签的隐含的含义就是当前Facet Query自动忽略该标签指代的Filter Query对索引文档总个数统计阶段的影响,但是Facet Query最终返回的索引文档任然会进行过滤。其中ex即exclude的缩写即排除的意思。

你会发现,最终返回的索引文档却又全部是California州下的饭店信息,跟Filter Query的过滤条件相吻合。

那么设计这种功能到底又什么用呢?或者说到底什么场景下可以使用到它呢?我想这是大家此刻最想知道的事情。下面依然搜“饭店”为例进行说明,假如你进入一个饭店的搜索界面,界面初始在网页左侧或者其他位置显示了每个州下的饭店数量,当然可能还有其他Facet(维度)的统计,比如价格区间,这里暂且以州这个维度进行讲解说明。看到下面这个界面展示,你可以很清楚的了解到每个州下有多少饭店:

Georgia(6)
California(4)
New York(4)
Texas(3)
Illinois(2)

假设随机单击了Georgia州这个连接,那么此刻后台的搜索程序会将你单击的这个Georgia作为一个过滤条件,过滤出仅仅在Georgia州下的饭店,底层构建的Filter Query可能是类似这样的:fq=state:Georgia,正常情况下会在网站界面的右侧为你展示所有Georgia州下的饭店,但默认情况下Filter Query还会影响最终Facet Query的对每个Facet(维度)下匹配的索引文档总个数的统计。即在你单击Georgia州这个连接只会,网页左侧的Facet统计数据展示部分可能会变成这样:

Georgia(6)

其他州的统计数据不显示了,因为它们不符合fq=state:Georgia这个过滤条件已经被Filter Query排除掉。此时,这并不是我们想要的结果,因为可能你还想要再浏览California州有哪些饭店,怎么办?没办法,你只能通过浏览器后退返回上一级重新选择单击,这是一个很不好的用户体验。但是如果我们单击某个州的连接,能够在右侧展示符合该过滤条件下的所有饭店,但左侧的Facet统计不受影响,那么就可以实现连续单击浏览行为,即此刻我们希望在单击了某个州连接之后,左侧的Facet统计数据展示部分保存不变,即依然如下显示:

Georgia(6)
California(4)
New York(4)
Texas(3)
Illinois(2)

那么此刻你就需要结合tag+ex这两个本地参数来实现这个功能。

在Solr中,Facet查询大量使用了Solr的缓存,所以如果想要最大化的提升Facet查询性能的话,那么你需要优化solr的内置缓存。有关solr缓存将在后续章节详细讲解。除了solr性能调优,你可能还希望了解Facet更高级方面的知识,其中一个就是Pivot Faceting。solr提供了两个和Facet叠加分组统计的功能类似SQL里的Group By两个字段的意思。即在某个Facet Query执行后返回的结果集基础之上在执行其他Facet Query。如果没有Pivot Faceting,要想是想这种功能,你可能需要执行多次独立Facet Query,不过这种做法它没有很好的系统伸缩性,而且索引文档数据量不断增长的时候,他很容易导致你必须运行十几甚至几百个Facet查询,这显然不可取,sole中的Pivot Faceting就是设计用来解决此类问题的。Pivot Faceting允许你跨多个维度在一个查询中进行Facet查询统计。有关Facet留到后续章节再做详解。

⚠️ **GitHub.com Fallback** ⚠️