JSON 分面 API

JSON 分面和分析

JSON 分面展现了与 Solr 传统分面类似的功能,但更注重可用性。它比传统分面有几个优势

  • 更容易以编程方式构建复杂或嵌套的分面

  • JSON 提供的嵌套和结构使分面比传统分面 API 的扁平命名空间更容易阅读和理解。

  • 对指标和分析的一流支持

  • 更标准化的响应格式使客户端更容易解析和使用响应

分面搜索是关于聚合数据并计算有关该数据的指标。

有两种主要类型的分面

  • 将数据(域)划分为多个存储段的分面

  • 为给定存储段计算数据的分面(通常是指标、统计信息或分析函数)

存储段分面示例

以下是一个存储段分面的示例,它根据 cat 字段(类别缩写)将文档划分为存储段,并返回前 3 个存储段

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "categories" : {
      "type": "terms",
      "field": "cat",
      "limit": 3
    }
  }
}'

SolrJ

final TermsFacetMap categoryFacet = new TermsFacetMap("cat").setLimit(3);
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("categories", categoryFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

下面的响应向我们展示了 32 个文档匹配默认根域。12 个文档具有 cat:electronics,4 个文档具有 cat:currency,依此类推。

[...]
  "facets":{
    "count":32,
    "categories":{
      "buckets":[{
          "val":"electronics",
          "count":12},
        {
          "val":"currency",
          "count":4},
        {
          "val":"memory",
          "count":3},
      ]
    }
  }

统计分面示例

统计(也称为聚合分析)分面除了显示查询结果本身之外,还可用于显示从查询结果中派生的信息。例如,统计分面可用于为在电子商务网站上寻找内存的用户提供背景信息。下面的示例计算平均价格(和其他统计信息),并允许用户衡量购物车中的内存条是否价格合理。

curl

curl http://localhost:8983/solr/techproducts/query -d '
q=memory&
fq=inStock:true&
json.facet={
  "avg_price" : "avg(price)",
  "num_suppliers" : "unique(manu_exact)",
  "median_weight" : "percentile(weight,50)"
}'

SolrJ

final JsonQueryRequest request =
    new JsonQueryRequest()
        .setQuery("memory")
        .withFilter("inStock:true")
        .withStatFacet("avg_price", "avg(price)")
        .withStatFacet("min_manufacturedate_dt", "min(manufacturedate_dt)")
        .withStatFacet("num_suppliers", "unique(manu_exact)")
        .withStatFacet("median_weight", "percentile(weight,50)");
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

对上述分面请求的响应将从匹配根域的文档(包含 "memory" 且 inStock:true 的文档)开始,然后在 facets 块中显示请求的统计信息

 "facets" : {
    "count" : 4,
    "avg_price" : 109.9950008392334,
    "num_suppliers" : 3,
    "median_weight" : 352.0
  }

分面类型

有 4 种不同类型的存储段分面,它们以两种不同的方式表现

  • "terms" 和 "range" 分面生成多个存储段,并将域中的每个文档分配到其中一个(或多个)存储段

  • "query" 和 "heatmap" 分面始终生成一个存储段,域中的所有文档都属于该存储段

以下详细介绍了每种分面类型。

术语分面

terms 分面根据字段中的唯一值对域进行存储段划分。

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    categories:{
      "type": "terms",
      "field" : "cat",
      "limit" : 5
    }
  }
}'

SolrJ

final TermsFacetMap categoryFacet = new TermsFacetMap("cat").setLimit(5);
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("categories", categoryFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);
参数 说明

field

要进行分面的字段名称。

offset

用于分页,跳过前 N 个存储桶。默认为 0。

limit

限制返回的存储桶数量。默认为 10。

sort

指定如何对生成的存储桶进行排序。

count 指定文档数量,index 按存储桶值的索引(自然)顺序进行排序。还可以按存储桶中出现的任何 facet 函数/统计信息 进行排序。默认值为 count desc。此参数也可以在 JSON 中指定,例如 sort:{count:desc}。排序顺序可以是“asc”或“desc”

overrequest

在分布式搜索期间从分片内部请求的超出 limit 的存储桶数量。

当各个分片具有非常不同的顶级术语时,较大的值可以提高返回的最终“热门术语”的准确性。

-1 的默认值会导致根据指定的其他选项应用启发式方法。

refine

如果为 true,则启用分布式 facet 精化。这使用第二阶段从分片中检索最终结果所需的任何存储桶,这些分片最初的内部结果中不包含这些存储桶,以便每个分片都为该 facet 中返回的每个存储桶和任何子 facet 做出贡献。这使得返回存储桶的计数和统计信息精确。

overrefine

在分布式搜索期间确定要精化的存储桶时,超出 limit 的要内部考虑的存储桶数量。

当各个分片具有非常不同的顶级术语,并且当前 sort 选项可能导致精化将术语向下推到排序列表中(例如:sort:"count asc")时,较大的值可以提高返回的最终“热门术语”的准确性。

-1 的默认值会导致根据指定的其他选项应用启发式方法。

mincount

仅返回计数至少为该数字的存储桶。默认为 1

missing

一个布尔值,指定是否应返回一个特殊的“缺失”存储桶,该存储桶由字段中没有值的文档定义。默认为 false

numBuckets

一个布尔值。如果为 true,则将“numBuckets”添加到响应中,该响应是一个整数,表示 facet 的存储桶数量(而不是返回的存储桶数量)。默认为 false

allBuckets

布尔值。如果为 true,则向响应添加一个“allBuckets”存储段,表示所有存储段的并集。对于多值字段,这与域中所有文档的存储段不同,因为单个文档可以属于多个存储段。默认为 false

前缀

仅为以指定前缀开头的术语生成存储段。

切面

为每个返回的存储段计算的聚合、指标或嵌套切面

方法

此参数指示要使用的切面算法

  • dv DocValues,收集到序数数组中

  • uif UnInvertedField,收集到序数数组中

  • dvhash DocValues,收集到哈希中 - 提高高基数字段的效率

  • enum TermsEnum,然后与 DocSet 相交(可流式传输)

  • stream 目前等效于 enum。用于已编制索引的非点字段,其中排序为 index asc,并且禁用了 allBucketsnumBucketsmissing

  • smart 为字段类型选择最佳方法(这是默认值)

prelim_sort

一个可选参数,用于指定最终 sort 的近似值,以便在 sort 参数非常昂贵 时在最初收集顶级存储段时使用。

查询切面

查询切面生成一个文档存储段,该存储段与域以及指定查询相匹配。

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "high_popularity": {
      "type": "query",
      "q": "popularity:[8 TO 10]"
    }
  }
}'

SolrJ

QueryFacetMap queryFacet = new QueryFacetMap("popularity:[8 TO 10]");
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("high_popularity", queryFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

用户还可以指定子切面(“存储段”切面或指标)

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "high_popularity": {
      "type": "query",
      "q": "popularity:[8 TO 10]",
      "facet" : {
        "average_price" : "avg(price)"
      }
    }
  }
}'

SolrJ

QueryFacetMap queryFacet =
    new QueryFacetMap("popularity:[8 TO 10]").withStatSubFacet("average_price", "avg(price)");
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("high_popularity", queryFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

示例响应

"high_popularity" : {
  "count" : 36,
  "average_price" : 36.75
}

范围切面

范围切面在日期或数字字段上生成多个存储段。

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "prices": {
      "type": "range",
      "field": "price",
      "start": 0,
      "end": 100,
      "gap": 20
    }
  }
}'

SolrJ

RangeFacetMap rangeFacet = new RangeFacetMap("price", 0.0, 100.0, 20.0);
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("prices", rangeFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

上述范围切面的输出看起来有点像

"prices":{
  "buckets":[
    {
      "val":0.0,  // the bucket value represents the start of each range.  This bucket covers 0-20
      "count":5},
    {
      "val":20.0,
      "count":0},
    {
      "val":40.0,
      "count":0},
    {
      "val":60.0,
      "count":1},
    {
      "val":80.0,
      "count":1}
  ]
}

范围切面参数

范围切面参数名称和语义在很大程度上反映了 facet.range 查询参数样式切面。例如,此处的“start”对应于 facet.range 命令中的“facet.range.start”。

参数 说明

field

要从中生成范围存储段的数字字段或日期字段。

开始

范围的下限。

结束

范围的上限。

间隔

生成每个范围存储段的大小。

hardend

布尔值,如果为真,则表示最后一个存储桶将以“end”结束,即使其宽度小于“gap”。如果为假,则最后一个存储桶的宽度将为“gap”,这可能会超出“end”。

其他

此参数表示,除了计算 startend 之间的每个范围约束的计数之外,还应该计算以下计数…

  • “before”所有字段值低于第一个范围下限的所有记录

  • “after”所有字段值大于最后一个范围上限的所有记录

  • “between”所有字段值介于所有范围的开始和结束边界之间的所有记录

  • “none”不计算此信息

  • “all”before、between 和 after 的快捷方式

include

默认情况下,用于计算 startend 之间范围分面的范围包含其下限,但不包含上限。“before”范围是排他的,“after”范围是包含的。此默认值等同于下面的“lower”,不会导致边界处的重复计数。include 参数可以是以下选项的任意组合

  • “lower”所有基于 gap 的范围都包含其下限

  • “upper”所有基于 gap 的范围都包含其上限

  • “edge”第一个和最后一个 gap 范围包含其边缘边界(即,第一个的 lower,最后一个的 upper),即使未指定相应的上限/下限选项

  • “outer”即使第一个或最后一个范围已经包含这些边界,“before”和“after”范围也将包含其边界。

  • “all”lower、upper、edge、outer 的简写

切面

将为每个返回的存储桶计算的聚合、指标或嵌套分面

ranges

任意范围列表,当指定时,计算给定范围上的分面,而不是 startgapend。使用 startendgap 时,范围或存储桶的宽度始终是固定的。如果需要在不同的范围宽度上计算范围分面,则应指定 ranges

  • 不允许同时指定 startendgapranges,并且请求将失败。

  • 当在范围分面中指定 ranges 时,将忽略 hardendincludeother 参数。

请参阅 任意范围

任意范围

任意范围由范围桶计算的 from 和 to 值组成。此范围可以用两种语法指定。

参数 说明

from

范围的下限。未指定时,默认为 *

to

范围的上限。未指定时,默认为 *

inclusive_from

一个布尔值,如果为 true,则表示包含下限 from。默认为 true

inclusive_to

一个布尔值,如果为 true,则表示包含上限 to。默认为 false

range

范围指定为字符串。这在语义上类似于 facet.interval

  • 当指定 range 时,则会忽略范围中的所有上述参数 fromto

  • range 始终以 ([ 开头,以 )] 结尾

    • ( - 排除下限

    • [ - 包含下限

    • ) - 排除上限

    • ] - 包含上限

例如,对于范围 (5,10],5 被排除,10 被包含

其他有范围的情况

当指定 ranges 时,会忽略 other 参数,但可以通过 ranges 实现相同的效果。

  • before - 这相当于 [*,some_val) 或仅指定 to

  • after - 这相当于 (som_val, *] 或仅指定 from

  • between - 这相当于分别将 startend 指定为 fromto

包含范围

当指定 ranges 时,会忽略 include 参数,但可以通过 ranges 实现相同的效果。lowerupperouteredge 都可以通过组合使用 inclusive_toinclusive_from 来实现。

带有 ranges 的范围分面

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "prices": {
      "type": "range",
      "field": "price",
      "ranges": [
        {
          "from": 0,
          "to": 20,
          "inclusive_from": true,
          "inclusive_to": false
        },
        {
          "range": "[40,100)"
        }
      ]
    }
  }
}'

上述范围切面的输出看起来有点像

{
  "prices": {
    "buckets": [
      {
        "val": "[0,20)",
        "count": 5
      },
      {
        "val": "[40,100)",
        "count": 2
      }
    ]
  }
}
当指定 range 时,其在请求中的值用作响应中的键。在其他情况下,键使用 fromtoinclusive_toinclusive_from 生成。目前,不支持自定义 key

热图分面

heatmap 分面为每个网格单元中具有空间数据的文档生成一个 2D 网格分面计数。

此功能主要记录在参考指南的 空间 部分。主要参数是 type,用于指定 heatmap,以及 field,用于指示空间 RPT 字段。其余参数名称使用与分面.heatmap 查询参数样式分面相同的名称和语义,但没有 "facet.heatmap." 前缀。例如,此处的 geom 对应于 facet.heatmap 命令中的 facet.heatmap.geom

与将域划分为存储区的其他分面不同,heatmap 分面目前不支持 嵌套分面

curl

curl http://localhost:8983/solr/spatialdata/query -d '
{
  "query": "*:*",
  "facet": {
    "locations": {
      "type": "heatmap",
      "field": "location_srpt",
      "geom": "[\"50 20\" TO \"180 90\"]",
      "gridLevel": 4
    }
  }
}'

SolrJ

final JsonQueryRequest request =
    new JsonQueryRequest()
        .setQuery("*:*")
        .setLimit(0)
        .withFacet(
            "locations",
            new HeatmapFacetMap("location_srpt")
                .setHeatmapFormat(HeatmapFacetMap.HeatmapFormat.INTS2D)
                .setRegionQuery("[\"50 20\" TO \"180 90\"]")
                .setGridLevel(4));

分面响应如下所示

{
  "facets": {
    "locations":{
      "gridLevel":1,
      "columns":6,
      "rows":4,
      "minX":-180.0,
      "maxX":90.0,
      "minY":-90.0,
      "maxY":90.0,
      "counts_ints2D":[[68,1270,459,5359,39456,1713],[123,10472,13620,7777,18376,6239],[88,6,3898,989,1314,255],[0,0,30,1,0,1]]
    }
  }
}

统计分面函数

与迄今为止讨论的所有分面不同,聚合函数(也称为分面函数分析函数指标)不会将数据划分为存储区。相反,它们计算域中所有文档上的某个内容。

聚合 示例 说明

sum

sum(sales)

数值的总和

avg

avg(popularity)

数值的平均值

min

min(salary)

最小值

max

max(mul(price,popularity))

最大值

missing

missing(author)

对于给定字段或函数没有值的文档数

countvals

countvals(author)

给定字段或函数的值数

unique

unique(author)

给定字段的唯一值数。超过 100 个值后,它不会产生准确的估计

uniqueBlock

uniqueBlock(_root_)uniqueBlock($fldref),其中 fldref=_root_

与上述相同,但占用空间更小,严格用于 计算块连接块数。给定字段在各个块中必须是唯一的,并且仅支持单值字符串字段,建议使用 docValues。

uniqueBlock({!v=type:parent})uniqueBlock({!v=$qryref}),其中 qryref=type:parent

与上述相同,但使用给定查询的位集来聚合命中。

hll

hll(author)

通过 hyper-log-log 算法分布式基数估计

百分位数

percentile(salary,50,75,99,99.9)

通过 t-digest 算法百分位数估计。按此指标排序时,将使用第一个列出的百分位数作为排序值。

平方和

sumsq(rent)

字段或函数的平方和

方差

variance(rent)

数字字段或函数的方差

标准差

stddev(rent)

字段或函数的标准差

相关性

relatedness('popularity:[100 TO *]','inStock:true')

一个函数,用于计算域中文档与前景集的关联性得分,相对于背景集(两者均定义为查询)。这主要用于构建语义知识图谱时。

诸如 avg 的数字聚合函数可以针对任何数字字段,或针对多个数字字段的嵌套函数,例如 avg(div(popularity,price))

请求聚合函数最常见的方式是作为包含您希望计算的表达式的简单字符串

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "filter": [
    "price:[1.0 TO *]",
    "popularity:[0 TO 10]"
  ],
  "facet": {
    "avg_value": "avg(div(popularity,price))"
  }
}'

SolrJ

final JsonQueryRequest request =
    new JsonQueryRequest()
        .setQuery("*:*")
        .withFilter("price:[1.0 TO *]")
        .withFilter("popularity:[0 TO 10]")
        .withStatFacet("min_manu_id_s", "min(manu_id_s)")
        .withStatFacet("avg_value", "avg(div(popularity,price))");
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

扩展形式允许指定本地参数。这些参数可以被一些专门的聚合(例如 relatedness())显式使用,但也可以用作参数引用,以使聚合表达式更具可读性,而无需使用(全局)请求参数

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "filter": [
    "price:[1.0 TO *]",
    "popularity:[0 TO 10]"
  ],
  "facet": {
    "avg_value" : {
      "type": "func",
      "func": "avg(div($numer,$denom))",
      "numer": "mul(popularity,3.0)",
      "denom": "price"
    }
  }
}'

SolrJ

final Map<String, Object> expandedStatFacet = new HashMap<>();
expandedStatFacet.put("type", "func");
expandedStatFacet.put("func", "avg(div($numer,$denom))");
expandedStatFacet.put("numer", "mul(popularity,3.0)");
expandedStatFacet.put("denom", "price");
final JsonQueryRequest request =
    new JsonQueryRequest()
        .setQuery("*:*")
        .withFilter("price:[1.0 TO *]")
        .withFilter("popularity:[0 TO 10]")
        .withFacet("avg_value", expandedStatFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

嵌套分面

嵌套分面或子分面允许将分面命令嵌套在将域划分为存储段(即 termsrangequery)的任何分面命令下。然后,根据父分面的每个存储段中所有文档集定义的域,对这些子分面进行评估。

语法与顶级分面相同 - 只需将 facet 命令添加到父分面的分面命令块即可。从技术上讲,每个分面命令实际上都是一个子分面,因为我们从一个分面存储段开始,其域由主查询和过滤器定义。

嵌套分面示例

让我们从类别字段 cat 上一个简单的非嵌套 terms 分面开始

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "categories": {
      "type": "terms",
      "field": "cat",
      "limit": 3
    }
  }
}'

SolrJ

final TermsFacetMap categoryFacet = new TermsFacetMap("cat").setLimit(3);
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("categories", categoryFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

上面分面的响应将显示顶级类别以及落入每个类别存储段的文档数。嵌套分面可用于收集有关每个文档存储段的附加信息。例如,使用下面的嵌套分面,我们可以找到顶级类别以及每个类别中的领先制造商

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "categories": {
      "type": "terms",
      "field": "cat",
      "limit": 3,
      "facet": {
        "top_manufacturer": {
          "type": "terms",
          "field": "manu_id_s",
          "limit": 1
        }
      }
    }
  }
}'

SolrJ

final TermsFacetMap topCategoriesFacet = new TermsFacetMap("cat").setLimit(3);
final TermsFacetMap topManufacturerFacet = new TermsFacetMap("manu_id_s").setLimit(1);
topCategoriesFacet.withSubFacet("top_manufacturers", topManufacturerFacet);
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("categories", topCategoriesFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

响应将类似于

"facets":{
    "count":32,
    "categories":{
      "buckets":[{
          "val":"electronics",
          "count":12,
          "top_manufacturer":{
            "buckets":[{
                "val":"corsair",
                "count":3}]}},
        {
          "val":"currency",
          "count":4,
          "top_manufacturer":{
            "buckets":[{
                "val":"boa",
                "count":1}]}}]}}

按嵌套函数对分面排序

字段或术语切面的默认排序按存储桶计数降序排列。我们还可以选择按每个存储桶中出现的任何切面函数升序或降序进行排序

curl

curl http://localhost:8983/solr/techproducts/query -d '
{
  "query": "*:*",
  "facet": {
    "categories":{
      "type" : "terms",     // terms facet creates a bucket for each indexed term in the field
      "field" : "cat",
      "limit": 3,
      "sort" : "avg_price desc",
      "facet" : {
        "avg_price" : "avg(price)",
      }
    }
  }
}'

SolrJ

final TermsFacetMap topCategoriesFacet =
    new TermsFacetMap("cat")
        .setLimit(3)
        .withStatSubFacet("avg_price", "avg(price)")
        .setSort("avg_price desc");
final JsonQueryRequest request =
    new JsonQueryRequest().setQuery("*:*").withFacet("categories", topCategoriesFacet);
QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);

在某些情况下,所需的排序可能是对每个存储桶计算成本很高的聚合函数。可以使用prelim_sort选项来指定排序的近似值,以便最初对存储桶进行排名以确定最优候选(基于limitoverrequest)。只有在对最优候选存储桶进行优化后,才会使用实际的排序

{
  categories:{
    type : terms,
    field : cat,
    refine: true,
    limit: 10,
    overrequest: 100,
    prelim_sort: "sales_rank desc",
    sort : "prod_quality desc",
    facet : {
      prod_quality : "avg(div(prod(rating,sales_rank),prod(num_returns,price)))"
      sales_rank : "sum(sales_rank)"
    }
  }
}

更改域

如上所述,切面根据其文档“域”计算存储桶或统计信息。

  • 默认情况下,顶级切面使用与主查询匹配的所有文档集作为其域。

  • 为其父切面的每个存储桶计算嵌套“子切面”,使用包含该存储桶中所有文档的域。

除了此默认行为外,还可以拓宽、缩小或完全更改域。JSON 切面 API 支持通过其domain属性修改域。在 JSON 切面域更改 中对此进行了更详细的讨论。

特殊统计切面函数

大多数统计切面函数(avgsumsq等)允许用户对文档组执行数学计算。不过,一些函数更复杂,值得单独解释。这些将在以下部分中更详细地描述。

uniqueBlock() 和块连接计数

当集合包含 嵌套文档 时,blockChildrenblockParent 域更改 在搜索父文档时很有用,并且您希望针对所有受影响的子文档(或反之亦然)计算统计信息。但是,如果您只需要知道当前域中存在的所有块的计数,那么更有效的方法是uniqueBlock()聚合函数。

假设我们有带有多个 SKU 的产品,并且我们想为每种颜色计算产品。

{
  "id": "1", "type": "product", "name": "Solr T-Shirt",
  "_childDocuments_": [
    { "id": "11", "type": "SKU", "color": "Red",  "size": "L" },
    { "id": "12", "type": "SKU", "color": "Blue", "size": "L" },
    { "id": "13", "type": "SKU", "color": "Red",  "size": "M" }
  ]
},
{
  "id": "2", "type": "product", "name": "Solr T-Shirt",
  "_childDocuments_": [
    { "id": "21", "type": "SKU", "color": "Blue", "size": "S" }
  ]
}

在针对一组 SKU 文档进行搜索时,我们可以要求对颜色进行分面,并使用嵌套统计来计算所有“块”(即产品)

color: {
  type: terms,
  field: color,
  limit: -1,
  facet: {
    productsCount: "uniqueBlock(_root_)"
      // or "uniqueBlock({!v=type:product})"
  }
}

并获取

color:{
   buckets:[
      { val:Blue, count:2, productsCount:2 },
      { val:Red, count:2, productsCount:1 }
   ]
}

请注意,_root_ 是 Lucene 添加到每个子文档的内部字段,用于引用父文档。聚合 uniqueBlock(_root_) 在功能上等同于 unique(_root_),但针对嵌套文档块结构进行了优化。建议为 uniqueBlock 计算定义 limit: -1,如上例所示,因为 limit 参数的默认值为 10,而 uniqueBlock-1 时应该快得多。

relatedness() 和语义知识图

relatedness(…​) 统计函数允许对文档集进行评分,相对于前景和背景文档集,目的是找到构成“语义知识图”的临时关系

语义知识图的核心利用了倒排索引以及互补的非倒排索引来表示节点(术语)和边(多个术语/节点的相交发布列表中的文档)。这在每对节点及其对应的边之间提供了一层间接关系,使边能够从底层语料库统计中动态生成。因此,任何节点组合都可以使边生成到任何其他节点,并进行评分以揭示节点之间的潜在关系。

— Grainger 等人

语义知识图

relatedness(…​) 函数用于“评分”这些关系,相对于在函数参数中指定为查询的“前景”和“背景”文档集。

与大多数聚合函数不同,relatedness(…​) 函数知道它是否以及如何在 嵌套分面 中使用。它独立于其父/祖先存储桶评估定义当前存储桶的查询,并将这些文档与由前景查询(与祖先存储桶结合)定义的“前景集”相交。然后将结果与针对“背景集”(由背景查询独占定义)执行的类似相交进行比较,以查看当前存储桶与前景集之间是否存在正相关或负相关,相对于背景集。

relatedness(…​)allBuckets 上下文中的语义目前未定义。因此,尽管 relatedness(…​) 统计数据可以针对同时指定 allBuckets:true 的分面请求指定,但 allBuckets 存储桶本身不会包含相关性计算。
虽然将背景集定义为 *:* 或前台查询的某个其他超集非常常见,但这不是严格要求的。relatedness(…​) 函数可用于比较文档集与正交前台/背景查询的统计相关性。

relatedness() 选项

在使用扩展 type:func 语法指定 relatedness() 聚合时,可以使用可选的 min_popularity(浮点数)选项来指定 foreground_popularitybackground_popularity 值的下限,必须满足此下限,relatedness 得分才有效——如果未满足此 min_popularity,则 relatedness 得分将为 -Infinity

用于计算 relatedness() 域相关性的默认实现取决于要计算的面向类型。通用域相关性按术语计算,方法是通过选择性地为每个存储桶关联查询检索 DocSet(咨询 filterCache)并计算 DocSet 与“前台”和“背景”集的交集。对于术语面向(尤其是在高基数字段上),此方法可能导致 filterCache 抖动;因此,relatedness() 在术语面向上的默认值尽可能采用一种方法,该方法直接在所有多个域上收集面向计数,一次扫描(从不接触 filterCache)。可以通过将扩展 type:func 语法 sweep_collection 选项设置为 true(默认值)或 false(禁用扫描收集)来显式控制此“一次扫描”收集。

禁用 relatedness() 统计信息在低基数字段上的扫描收集可能会产生性能优势,前提是 filterCache 足够大,可以容纳关联字段中每个值的条目,而不会对预期使用模式造成抖动。一个合理的启发式方法是,基数小于 1,000 的字段可能会受益于禁用扫描。此启发式不会用于确定默认行为,特别是由于非扫描收集很容易导致 filterCache 抖动,从而对系统范围产生不利影响。
{ "type": "func",
  "func": "relatedness($fore,$back)",
  "min_popularity": 0.001,
}

当对 relatedness() 使用降序排序,并且前台和背景查询不相交时,这可能特别有用,以确保“顶部存储桶”都与这两个集合相关。

relatedness(…​) 请求上进行排序时,可以通过添加 prelim_sort: "count desc" 选项来更快地处理请求。增加 overrequest 可以帮助提高顶部存储桶的准确性。

语义知识图示例

示例文档
curl -sS -X POST 'http://localhost:8983/solr/gettingstarted/update?commit=true' -d '[
{"id":"01",age:15,"state":"AZ","hobbies":["soccer","painting","cycling"]},
{"id":"02",age:22,"state":"AZ","hobbies":["swimming","darts","cycling"]},
{"id":"03",age:27,"state":"AZ","hobbies":["swimming","frisbee","painting"]},
{"id":"04",age:33,"state":"AZ","hobbies":["darts"]},
{"id":"05",age:42,"state":"AZ","hobbies":["swimming","golf","painting"]},
{"id":"06",age:54,"state":"AZ","hobbies":["swimming","golf"]},
{"id":"07",age:67,"state":"AZ","hobbies":["golf","painting"]},
{"id":"08",age:71,"state":"AZ","hobbies":["painting"]},
{"id":"09",age:14,"state":"CO","hobbies":["soccer","frisbee","skiing","swimming","skating"]},
{"id":"10",age:23,"state":"CO","hobbies":["skiing","darts","cycling","swimming"]},
{"id":"11",age:26,"state":"CO","hobbies":["skiing","golf"]},
{"id":"12",age:35,"state":"CO","hobbies":["golf","frisbee","painting","skiing"]},
{"id":"13",age:47,"state":"CO","hobbies":["skiing","darts","painting","skating"]},
{"id":"14",age:51,"state":"CO","hobbies":["skiing","golf"]},
{"id":"15",age:64,"state":"CO","hobbies":["skating","cycling"]},
{"id":"16",age:73,"state":"CO","hobbies":["painting"]},
]'
示例查询
curl -sS -X POST http://localhost:8983/solr/gettingstarted/query -d 'rows=0&q=*:*
&back=*:*                                  (1)
&fore=age:[35 TO *]                        (2)
&json.facet={
  hobby : {
    type : terms,
    field : hobbies,
    limit : 5,
    sort : { r1: desc },                   (3)
    facet : {
      r1 : "relatedness($fore,$back)",     (4)
      location : {
        type : terms,
        field : state,
        limit : 2,
        sort : { r2: desc },               (3)
        facet : {
          r2 : "relatedness($fore,$back)"  (4)
        }
      }
    }
  }
}'
1 使用整个集合作为我们的“背景集”
2 使用查询“age >= 35”来定义我们的(初始)“前台集”
3 对于顶级 hobbies 切面和 state 上的子切面,我们将在 relatedness(…​) 值上进行排序
4 在对 relatedness(…​) 函数的两次调用中,我们使用 参数变量 来引用先前定义的 foreback 查询。
切面响应
"facets":{
  "count":16,
  "hobby":{
    "buckets":[{
        "val":"golf",
        "count":6,                                (1)
        "r1":{
          "relatedness":0.01225,
          "foreground_popularity":0.3125,         (2)
          "background_popularity":0.375},         (3)
        "location":{
          "buckets":[{
              "val":"az",
              "count":3,
              "r2":{
                "relatedness":0.00496,            (4)
                "foreground_popularity":0.1875,   (6)
                "background_popularity":0.5}},    (7)
            {
              "val":"co",
              "count":3,
              "r2":{
                "relatedness":-0.00496,           (5)
                "foreground_popularity":0.125,
                "background_popularity":0.5}}]}},
      {
        "val":"painting",
        "count":8,                                (1)
        "r1":{
          "relatedness":0.01097,
          "foreground_popularity":0.375,
          "background_popularity":0.5},
        "location":{
          "buckets":[{
            ...
1 尽管 hobbies:golf 的总切面 count 低于 hobbies:painting,但它具有更高的 relatedness 分数,表明相对于背景集(整个集合),高尔夫与我们的前景集(35 岁以上的人)的相关性更强,而不是绘画。
2 匹配 age:[35 TO *] hobbies:golf 的文档数量是背景集中文档总数的 31.25%
3 背景集中 37.5% 的文档匹配 hobbies:golf
4 与背景集相比,亚利桑那州 (AZ) 与嵌套前景集(35 岁以上打高尔夫的人)具有相关性相关性——即,“亚利桑那州的人在统计上比全国更可能是‘35 岁以上的高尔夫球手’”。
5 科罗拉多州 (CO) 与嵌套前景集呈相关性——即,“科罗拉多州的人在统计上不太可能是‘35 岁以上的高尔夫球手’,而不是全国”。
6 匹配 age:[35 TO *] hobbies:golf state:AZ 的文档数量是背景集中文档总数的 18.75%
7 背景集中 50% 的文档匹配 state:AZ