– 没有了
索引时的Boost
,但你还有我啊!
1. What is this?
Payload在Lucene中表述是一个任意 byte[],它可以(只是可以不是必须)为它每个Term存储额外的信息到倒排索引里面。而且这些Payloads可以被用作Query的一部分来过滤,或者生成对应值用于打分,甚至替代文档的返回值。
Payload能干嘛呢
前面提到了,Payloads可以被用作Query的一部分来过滤,或者生成对应值用于打分,甚至替代文档的返回值。这个非常强大的东西,你理解起来非常容易,你直接当她是一个Hive
中的JSON
类型即可,如果还是有点陌生的话,直接当她是一个Map
吧。即是可以key
找value
。
到这里,你可能要问了。我们本来就是一个动态文档,原本就是键值对的式子,那要你干嘛呢?
其实,此时你应该把她当成一个nested document
。后面会举这个例子,请继续往下阅读。
如果你应该理解上面说的,把她当成一个文档的子文档的话。其实她的功能应该很好理解,显而易见了。
- 更人性化的文档组织方式
- 用内嵌文档(子文档)的某个字段进行操作
- 排序
- Faceting
- 返回指定key的value
- 子文档flatMap
2. Show Cases
这个例子有个名字,它叫Per-Store Pricing,在 lucidworks 多博客文出现过,感觉非常好,先借来改改先用着。原例子用的相对比较稳定的价格作为示例,但我觉得用库存这个动态的属性作为示例会更加容易解释,但不需要过来多说明。
pre-setting :
这是一个在线连锁店的中央仓存系统,它会记录每个商品在每家的库存。由于店铺的位置和大小的原因,商品a不是所有店铺都会销售。
假定这是一个 Snapshot
有了上面这个大前提之后,我们继续来设定一些假定的操作:
- 找出A店所有商品,并按库存量排序
- 找出A店库存在100-200之间的所有商品
- 想某些商品在某些店的库存情况
针对上面设定和问题,我们尝试组织文档和定义操作。
- 再商品信息中加入库存信息,每家店铺都是一个独立的字段
这种作法最大的问题是当然店铺数量很多的时候,一个文档的字段非常多,即是多少店铺就得冗余多少字段。 - 在库存信息中加入商品信息,每家店铺都是一个独立的文档
这种作法即是文档数会很多,但是并不要紧,关键是想查商品的情况时,需要做groupBy
的操作,这是比较耗性能的操作。 - 采用payload的方式,在商品信息加入店铺的库存信息作为payload
这就是我们要讲的内容了。
- 再商品信息中加入库存信息,每家店铺都是一个独立的字段
2. Payloads in Solr
a. 如何在Solr上启用Payload
你把Payload说得这么好,那我怎么Solr里把它上用起来呢,让全世界都知道它的好。在Solr6.6
之后,我们知道它是只是一个字段类型,那么其实很好理解的,即是在schema.xml
多配一个字段类型咯,并把它加对应的字段即可了。
1. Change your schema.xml file
<fieldtype name="payloads" stored="false" indexed="true" class="solr.TextField" >
<analyzer>
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.DelimitedPayloadTokenFilterFactory" encoder="integer"/>
</analyzer>
<similarity class="payloadexample.PayloadSimilarityFactory" />
</fieldtype>
<field name="stock_dpi" type="payloads" indexed="true" stored="true"/>
- 这里
similarity
默认是SchemaSimilarity
,它决定payload_score
与Query得分
的关系。 - encoder 的取值
${float,integer}$
2. Add the payloaded term to the document
我们已经加了这么一个字段类型和字段了,接下来我们需要把文档带着payload信息文档提交给Solr进行索引。当然这也很简单,它就是我们配置的一个字段类型嘛,那么我们要求进行组织字段的值即可了。
按我们上面的配置(为简单起见我们就配置三个字段)
{
"id":"123",
"stock_dpi":"a|100 b|200",
"name":"cafe"
}
b. Payload神器,三把斧
正因为这三把神器,从而真正牵引我来写这篇博客。其实非常简单,也非常实用。好了,我们开始吧。
1. payload() function
payload() 函数并不能作为查询条件,只能用于修改显示的结构作为伪字段出现在文档中。即只用在fl
的条件,不能用于除fl之外
的场景。
payload是对word而言,它是有位置的。即是同一个词在同个字段中的不同位置可以赋不同的payload值的。
payload()函数签名:
payload(field, term [,default[,min|max|average|first]])
函数参数说明
- f 必填,字段名
- v 必填,需要转化的Key
- def 选填, 当对应的key
没有对应的value
时的默认
- func 选填,当有一个key
对应有多个value
时,进什么计算,默认是average
Example
payloads = ‘a|1.0 a|2.0 a|3.0 b|0.1’
payload(payloads, a) = 2.0
payload(payloads, c, -1.0) = -1.0
payload(payloads, a, min) = 1.0
payload(payloads, c, -1.0, min) = -1.0
2. Query Function Parser
这里我们来介绍一下两个迟到Query Parser
,PayloadScoreParser和PayloadCheckParser两个查询解释器。按理说它应该很早应该出现,即使不能跟Lucene4.x
一起出现,但实在不至于到现在Solr6.6
才出生。当然,在此之前有过很多民间的实现。因此这两个解释器的出现,使得Payload
变得有意义、完整。
关于payload的索引存储原理这里我们只做简单的解释,不做深入的解析。
这些查询解析器利用term|payload
在索引期间最终会翻译成SpanQuery,SpanNearQuery或者SpanTermQuery。也就是Term与payload其实是成对存在于索引表上的,跟坡度查询和短语查询差不多意思。
a. PayloadScoceParser : payload_score
简单的说,如果用某个子文档的某个字段的值来参与评分呢?这就是我们接下来要讨论的payload_score
了。
这些与
boost
有几分相似了。多说一句,索引时boost已经Solr6.6
弃用了。为何弃用Boost呢,我想你应该能理解,即是Boost的功能相当单一且不够灵活。当然啦,也是因为Payload的引用,使得Boost更加被弱化了。
我们已经payload字段是按一定的规则组织Term和Payload之间的关系的,如上例中我们是这样的组织:
stock_dpi = a|100 b|200 c|300 a|101 a|102 a|103
- 其中
等号左边
为字段名,即是下面参数f
需要指定的字段名
。 - 等号右边是字段值,它是一系列
键值对
。即这里的每个键便是term
,值为payload
参数说明:
- f 指定使用哪个payloads字段,必填。
v 拿哪个
key
(term
)来查询,必填。相当于说,需要指哪个子文档的哪个字段,它里
v
即是这的哪个字段
。func 它的计算结果作为
payload_score
的分数值,必填。payload function必须填,对payload的进行计算的函数。
includeSpanScore 是否把QueryFunction的查询拼入一般的查询条件中共同打分
是把 payload_score 的查询函数作为SpanQuery的条件,参与原查询的打分规则。
如果是,
$score = score(Q_{org} + Q_{pay}) + pay$
;
否则的话,$score = score(Q_{org}) + pay$
Example
q={!payload_score f=my_field_dpf v=some_term func=max}
q={!payload_score f=stock_dpi v=a func=average}
对func而言,它仅对f
中的v
有多值时,才会有意义。
1. {!payload_score f=payloads v=a func=min} = 1
2. {!payload_score f=payloads v=a func=max} = 3
3. {!payload_score f=payloads v=a func=average} = 2
b. PayloadCheckParser : payload_check
PayloadCheckParser提供的是一个针对Term-Payload检索功能,即是通过指定键值
,搜索含有的所有文档。这么说可能不好理解,简单的说,指定了子文档的字段名和值,希望找到子文档的该字段名下的值能匹配上的所有文档。
因此,它跟普通的查询一样必须需要指定字段名,和对应的值。
参数说明
- f 指定字段名,必填。
payloads 指定payload, 支持多值,之间用空格分开。必填。
v 指定key,必填
Example
方式一:
{!payload_check f=words_dps payloads=”VERB NOUN”}searching stuff
方式二:
{!payload_check f=words_dps payloads=”VERB NOUN” v=searching stuff}
3. Faceting on numeric payloads
Faceting出生很早,是Lucene统计分析的基石,大家绝对不会陌生。Faceting在分析方面那么好、那么优秀,那么她能不能延伸到PayloadQuery呢?
其实facet.range
只支持实际字段,而不支持伪字段。然而她又那么好,不忍舍弃,所以在现在版本得用一些比较偏门的方法,即是facet.query
,对payload()再进行{!frange}
query function查询来完成类facet.range
查询。
后面版本可能会实现对
payload
进行facet.range
,等着吧。
- facet.query={!frange key=up_to_400 l=0 u=400}${computed_price} // includes price=400.00
- facet.query={!frange key=above_400 l=400 incl=false}${computed_price} // excludes price=400.00
3. 回顾例子,Payload On Solr
前面提到了,Payloads可以被用作Query的一部分来过滤,或者生成对应值用于打分,甚至替代文档的返回值。
A. Solr下的原例1 per-store pricing
上面挖了坑,现在我们来把它填上。在Solr6.6
之后的Solr如何优雅解决这个问题的呢。前面我们讨论per-store pricing
问题时,已经给出结论,但我们并没有说明这个方案应当如何操作,接下来我们就来说说它应该如何操作。
在上面我们提出了这如下几问题,如今是时候把他解决掉了。
1. 找出商品A所有商铺,并按库存量排序
2. 找出A店库存在100-200之间的所有商品
3. 想某些商品在某些店的库存情况
现在我们按商品信息进行组织文档,然后把库存信息作为payloads字段类型加入到文档中。即是
{
"id":"product00001"
"name":"product_a",
"price":100.0,
"stock_dpi":"stock_a|100 stock_b|200 stock_c|300",
...
}
现在我们可以这么做
1. q=stock_dpi:stock_a&sort=payloads(stock_dpi, stock_a, 0, min) desc
2. facet.query={!frange key=up_to_400 l=0 u=400}payload(stock_dpi, stock_a, 0)