原文:Apache Solr
八、Solr 排名
在评估搜索响应时,您可能想知道为什么某个特定文档的排名高于另一个文档,以及这些文档是如何获得分数的。在本章中,您将了解 Solr 排名。
本章从默认的排名实现和控制分数的因素开始,逐步深入到更多的细节,以便您可以覆盖现有的排名因素或编写自定义排名器。即使计分是一个高级的话题,并且这一章涵盖了一些数学公式,你也不需要成为一个数学奇才来理解它。
我本可以在第三章中对排名做一个简单的介绍,但是我想深入了解更多细节,超越基本概念,并在实现层面讨论 Solr 排名,这需要一个专门的章节。
Note
在第三章中,你学习了信息检索概念;如果你跳过了那一章,我建议你在继续这一章之前先读一读,尽管这不是强制性的。
本章涵盖以下主题:
- Solr 如何对文档进行排序
- 默认排名模型和因素
- 支持替代型号
- 诊断文档分数
- 自定义排名
Solr 排名简介
相关性排名是任何搜索引擎的核心,Solr 也是如此。为了对用户查询的文档进行排序,Solr 根据模型的算法计算每个匹配文档的分数,并根据它们的相对分数对它们进行排序。它将得分最高的文档排在结果集的最前面。
最初,Lucene 只支持基于 TF-IDF 的向量空间模型来对文档进行排序,但是后来的 4.0 版本引入了额外的模型来支持更灵活的排序。这些模型都扩展了 Lucene 的抽象类Similarity
来实现算法。现有的基于 TF-IDF 的模型,在DefaultSimilarity
类中实现,仍然是排名的默认模型。在本章中,您将了解这些模型。
大多数 Solr 设置使用DefaultSimilarity
,这对于大多数需求来说都很好。我建议您不要担心改变模型,继续使用默认的相似性,除非您真正理解为什么您需要一个替代模型或定制实现。
在 90%的情况下,您将能够通过在索引或查询时应用不同的调优方法来操纵和控制文档的排名,而无需触及Similarity
类。这些方法以这样或那样的方式影响着文档的得分,对其进行适当的调整应该会带来预期的结果。综上所述,以下是控制文档排序的方法:
- 查询语法、运算符和其他组合,如短语、slop 和模糊匹配
- 查询解析器及其配置
- 文本分析
- 索引时提升文档和字段
- 查询时的术语提升
- 函数查询
如果这些定制得到了想要的结果,您应该避免改变排名模型,应该只在真正需要的时候操作它。我建议您清楚地了解您正在对排名进行的更改,并彻底测试它们。
即使您没有改变排名模型的计划,了解它们仍然很重要,这样您就可以了解前面的因素如何影响最终得分,并且可以衡量任何调整对您的文档排名的影响。
您可能有一个业务案例,您觉得调整这些参数没有帮助,并且您的排名要求与 Solr 提供的默认实现不同。在这种情况下,您可以执行以下操作:
- 使用 Lucene 提供的另一个
Similarity
实现,并根据您的需求进行调整。 - 扩展默认相似性以操纵其排名因子。
- 编写您的自定义实现。这将需要你对你的排名需求和你计划使用的算法有一个专家级的理解。你需要小心,因为这可能会让你陷入一个循环迭代,而排名没有任何显著的提高。
在您进一步了解之前,以下是关于 Solr 排名的一些重要事实,将有助于您避免任何假设:
-
无负分:分数始终是大于 0.0 的浮点数,任何文档都不能有负分。
-
分数是相对的:对于一个给定的查询,文档的分数是相对的,可以比较。多个文档可以分别具有与所有排名因素相同的分数,或者它们的分数相加可以生成相同的值。如果需要标准化的分数,可以将每个文档的分数除以查询的最高分数。
-
每场计算:分数是按场计算的。汇总每个字段的单独分数,以计算文档的最终分数。
-
可插入:Solr 允许您扩展可用的
Similarity
实现,以及插入您自己的排名算法。 -
Lucene:Solr 中的排名实现是由 Lucene 提供的。
org.apache.lucene.search.similarities
包包含所有实现各种排名模型的Similarity
类。为了连接您的定制实现,您可以将您的类打包在一个单独的 JAR 中,并将其添加到 Solr 类路径中,而不必担心更改 Solr 或 Lucene 包。 -
按相关性排序:默认情况下,搜索响应按分数降序排序,顶部的文档被标识为最佳匹配。
-
Maximum score: No upper boundary has been specified for the score of documents. The maximum value is based on several scoring factors, including boosts, which combined together can lead to a very high score. The
maxScore
parameter in a search response returns the maximum score for that query. Figure 8-1 shows an example that marks themaxScore
for a query.图 8-1。
Maximum score for a user query
Note
术语相关性排名、Solr 排名和相似性可以互换使用。相反,术语排名模型指的是用于实现相似性的算法。
默认排名
Lucene 的默认排名方法是它如此受欢迎的原因。它速度快,经过优化,易于理解和使用,对于大多数用例都很实用。这是一种(几乎)一刀切的方法。
默认的 Lucene 排名是基于布尔模型(BM)和向量空间模型(VSM)的组合。对于给定的查询,并不是对语料库中的所有文档进行排序,而是只对满足布尔条件(使用布尔运算符指定)的文档子集进行排序。这些缩小范围的文档还通过避免 VSM 对不相关的文档进行不必要的排序来减少处理时间。简单来说,BM 批准的文件由 VSM 排名。搜索响应中的numFound
参数指定 BM 批准的文档数量。图 8-2 标记了numFound
参数,表示匹配文件的数量。
图 8-2。
Number of matching documents
履行
Lucene 中的默认排名是由DefaultSimilarity
类实现的,它扩展了TFIDFSimilarity
类。DefaultSimilarity
类派生出一种基于 TF-IDF 排名的高级形式,用于实际的分数计算。在下一节中,您将了解 TF、IDF 和其他违约排名因素。
在solrconfig.xml
或者schema.xml
中不需要额外的定义,Solr 从用户那里抽象出所有的实现细节。如果你想明确指定这种相似性,尽管这没有意义,你可以在schema.xml
中这样做,如下所示:
<schema>
..
<similarity class="solr.DefaultSimilarity" />
</schema>
得分因素
本节涵盖影响文档分数的因素。一旦您理解了这些因素,您将了解到将这些因素标准化并将它们组合起来得出汇总分数的公式。对因子的理解足以通过使用诸如 boosting 之类的规定来调整分数,而对公式的理解相对来说不那么重要。
Note
术语权重是在每个字段的基础上计算的。为了计算某个字段的术语分数,排名因子使用该特定字段的统计数据。该术语的最终权重是通过汇总其适用的所有字段的分数来计算的。
以下是决定文档得分的主要因素:
- 术语频率(TF):这是一个术语在文档字段中出现的次数。文档的得分与它的术语频率成正比:术语在文档中的计数越高,它的得分就越高。该模型假设,如果一个术语在文档中出现的次数越多,它就越与用户相关。
- 逆文档频率(IDF):此因子根据术语出现的文档数量计算得分。它与计数相反:计数越高,权重越低。这个想法是给予更少的单词更多的重要性。请注意,文档计数是针对该特定字段进行的,不考虑该术语在其他字段中的存在。
在前面的章节中,您使用了停用字词表来过滤掉不重要的术语。stopFilterFactory
直接丢弃列表中的术语,对该术语的任何查询都不会得到匹配。但是有些术语不如其他术语重要,而且无论如何也不能被过滤掉。IDF 排名因子通过降低权重来处理如此频繁出现的术语。例如,在医学领域,诸如患者、结果、治疗、蛋白质和疾病等术语是常见的且重要性较低,而诸如肿瘤、帕金森病和囊肿等术语相对较少且重要性较高。IDF 作为一个因素在全文搜索中对适当地加权这些术语是至关重要的。
- 字段长度(
lengthNorm
):该因子考虑字段中标记的数量,用于确定文档的重要性。字段越短(索引的标记数量越少),文档越重要。length norm 背后的想法是确保一个术语在短字段(如title
)中的出现比在长字段(如abstract
)中的出现更重要。 - 协调因子(
coord
):该因子基于查询和文档之间的术语重叠来排名。这意味着包含大多数查询词的文档更重要,应该得到奖励。 - 提升:文档和字段可以在索引时提升,术语可以在查询时提升。通过给予更高的提升,您指定了一个特定的术语或一组术语更重要,应该得到更高的分数。这是您可以直接轻松地调整特定文档分数的因素。
Note
QueryNorm
是一个额外的因素,但此处未指定,因为它不影响两个文档之间的得分。相反,它使两个查询之间的分数正常化。你会在讨论 Lucene 排名公式的时候了解到QueryNorm
。
排名公式
前面讨论的默认相似性因子不使用计数来计算文档分数。原始值被标准化以获得最有用和最平衡的值,该值为计算文档的实际分数提供了最佳权重。根据需要和信息可用性,Solr 在查询时或索引时对这些因素进行规范化,其值基于一个字段、一个文档或整个集合的统计数据。
在 VSM 中,查询和文档被表示为多维空间中的加权向量,其中每个术语是一个维度,TF-IDF 值构成权重。VSM 使用余弦相似度来计算文档的分数。如果你觉得这个数学概念太难消化,你可以暂时跳过这句话。如果你感兴趣或者你想重温一下基本概念,请参考第三章了解 VSM 的详细信息。
如前所述,Lucene 使用 TF-IDF 的高级形式来计算实际分数。这种高级形式提供了增强、长度标准化和协调因子,以及术语频率和逆文档频率。在 Lucene 中使用 TF-IDF 的高级形式计算得分的公式如下:
)
这个得分公式包含六个函数。每个函数负责计算特定得分因子的标准化值。如等式中所述,这些函数的输出被组合,以生成文档的最终得分。
接下来描述每个函数及其与 Lucene 排名因子的关系。为了便于理解,这些函数是在假设索引只有一个可搜索字段的情况下定义的。
tf(t 在 d 中)
如前所述,TF 是频率一词的缩写。这个函数统计每个查询词在文档中出现的次数。一个文档的得分和它的出现频率成正比。术语出现的频率越高,得分越高。Lucene 通过计算频率的平方根来归一化 TF 权重:
)
以色列国防军(t)
如你所知,IDF 是逆文档频率的缩写。该函数通过计算出现该术语的文档数量的倒数来计算分数。Lucene 通过取反数值的对数来规格化该值:
)
docFreq
和对数值加上数字常数 1,以避免分别出现numDocs/0
和log(0)
这样的未定义值。
坐标(q,d)
coord
代表配位因子。该函数根据文档中重叠的查询词的数量来计算分数。使用此公式计算协调因子:
)
queryNorm(q)
在讨论文档排序因素时,我们跳过了这个函数。queryNorm
的目的是标准化查询之间的分数——与标准化文档之间的分数的其他因素相反。这个函数使得两个查询之间的分数具有可比性。相同的queryNorm
值应用于一个查询的所有文档,因此它不会直接影响文档的得分。查询词的queryNorm
计算如下:
)
其中sumOfSquaredWeights
使用以下公式计算:
)
t.getBoost()
这个函数获取查询时应用的术语 boost,比如查询adidas³ shoes
。值得注意的是,这个函数只考虑查询时的提升,不考虑索引时应用的字段或文档提升。术语 boost 直接影响文档的得分。如果查询词不包含任何提升,则隐式应用默认值 1.0。
范数(t,d)
该函数是两个因素的产物,即lengthNorm
和索引时间提升。lengthNorm
考虑字段的长度,以确定文档的重要性。字段越短(索引的标记数量越少),文档越重要。索引时提升是在索引文档时应用于文档或字段的提升。计算范数的公式如下:
)
Similarity
类在索引文档时计算范数。如果您应用一个替代模型(一个Similarity
实现),文档应该被重新索引。
归一化因子(或norm
)是在索引文档时计算的。关于norm
的一个有趣的事情是,计算出的范数值,也就是float
,被编码成一个字节进行存储。在查询时,从索引中读取norm
字节值,并解码回float
。编码/解码的目的是支持有效的内存利用和减少索引大小,但这是以一些精度损失为代价的。
如果在字段定义中设置omitNorms="true"
时省略了某个字段的规范,则该字段的norm
值将被忽略。
编制索引时,如果任何文档有多个同名字段,则其所有提升将相乘,以计算该字段的总提升。
Note
限制
基于 TF-IDF 的 VSM 是一个简单而强大的模型,适用于大多数情况,尤其是全文搜索,但它有一些局限性。
在高层次上,该模型基于两个主要概念:
- 识别查询中最感兴趣的术语及其权重
- 基于加权术语查找最相关的文档
基于 TF-IDF 的模型认为稀有术语更有趣,但情况并非总是如此。例如,在音乐元数据的搜索引擎中,诸如 love 和 heart 之类的术语很常见,因为许多歌曲标题都包含这些词,但它们的重要性丝毫不减。如果您有类似的情况,您可能希望关闭 IDF 作为一个得分因素,也许在该领域。在本章的后面,你会看到一些覆盖默认因子的示例代码。
基础 VSM 的另一个限制是,它假设术语是独立的(术语之间没有关系),但实际上术语是相关的。例如,对于像信用卡或纸袋这样的词,在 VSM 就失去了这种关系。
解释查询
Solr 搜索参数debugQuery=true
解释了结果集中每个文档的得分计算。搜索响应中的附加元素explain
包含每个文档的一个条目(映射到它的uniqueId
),并提供由函数返回的分数的描述及其聚合,以获得每个查询词的分数。不同的字段可以为相同的查询词返回不同的分数,再次汇总这些分数以计算该词的最终分数。这个词得分再次与其他查询词的得分(如果有的话)相加,以返回文档的最终得分。
为了更好地理解查询解释,您将对一小组文档进行索引,并尝试理解一个示例查询的得分计算:
Create a small set of sample documents. The following is a sample in CSV format: id,title,cat,units,popularity
1,Apple iPhone,phone,100,9
2,Apple iMac,desktop,9,8
3,Apple MacBook Pro laptop,laptop,30,10
4,Lenovo laptop,laptop,40,7
5,Asus Laptop,laptop,60,8
6,HP Laptop,laptop,50,7
Index the documents. $ curl
http://localhost:8983/solr/hellosolr/update/csv?commit=true
--data-binary @explain-sample.csv
-H ’Content-Type:text/plain; charset=utf-8’
Query with the additional parameter debugQuery=true
. $ curl http://localhost:8983/solr/hellosolr/select?
q=apple+laptop&fl=*,score&wt=xml&indent=true&debugQuery=true
&defType=edismax&qf=title+cat&lowercaseOperators=true
图 8-3 包含 Solr 返回的查询解释的快照。以下是一些需要注意的要点:
- 用于每个术语的分数计算的
Similarity
类在方括号中指定。图 8-3 中用下划线标出了一个例子。 - 每个函数返回的分数与产生分数的输入值一起指定。
- 每个函数的单独分数被合计,以获得包含该令牌的所有字段上的术语的最终分数。在图 8-3 中,术语膝上型电脑在两个字段
title
和cat
中匹配,它们的分数被分别计算,标注为 d 和 e - 对一个学期来说,它所有的领域分数相加得到一个最终分数。汇总字段分数取决于查询解析器。eDisMax 取最大值,在图 8-3 中标记为 c。
- 最后将每个标记的汇总分数相加,得出文档的最终分数。在图 8-3 中,注释 a 标注的是最终分数,是学期分数 b 和 c 的总和。
- 相同的
queryNorm
适用于所有条款。
图 8-3。
Explain query
替代排名模型
除了默认的基于 VSM 的排名,Lucene 4.0 引入了新的排名模型。这些模型为 Solr 提供的排名可能性带来了额外的灵活性。
大多数基于 Solr 和 Lucene 的应用程序仍然依赖默认排名。如果您可以通过调整 TF-IDF 权重或给予适当的提升来获得想要的结果,或者如果数学计算的想法吓到了您,您可以继续使用默认算法。这些替代模型需要一定的数学理解。
org.apache.lucene.search.similarities.*
包包含了计算文档相关性等级的所有类。抽象类Similarity
是基类,和DefaultSimilarity
一样,这些模型的实现也扩展了它。
如果您计划评估这些备选模型中的任何一个,首先要理解底层算法。网上有很多研究论文提供了算法的详细描述。选择最符合您需求的模型,然后对其进行评估,看它是否满足这些需求。您可以使用模型创建单独的索引并比较结果,或者运行 A/B 测试。
默认的相似性不需要任何额外的参数,但是这些替代模型允许您通过使用额外的参数来控制算法的行为。
Note
文档应重新编制索引,以充分利用备选排名模型的潜力,因为在编制索引时会计算标准。
这一节涵盖了 Lucene 支持的两种主要的排名方案。
bm25 相似性
BM25Similarity 是 Lucene 提供的备选实现中使用最广泛的排序算法。这种相似性基于 Okapi BM25 算法,其中 BM 代表最佳匹配,是一种概率检索模型。对于包含小文档的索引,它可以产生比基于 TF-IDF 的排序更好的结果。
该模型类似于 VSM,在某种意义上,它采用词袋方法,并考虑术语频率和逆文档频率来计算和总结得分。但是它有一些显著的不同。这些算法之间最重要的两个区别如下:。
-
Field length (
lengthNorm
): BM25 considers the fact that longer documents have more terms, and so the possibility of higher term frequency is also more. As a term can have a high frequency due to the long length and might not be more relevant to the user query, this similarity applies an additional parameter,b
, that normalizes the term frequency due to this possibility. Table 8-1 provides more details of this parameter.表 8-1。
BM25SimilarityFactory Configuration Parameters
| 参数 | 类型 | 描述 | | --- | --- | --- | | `k1` | 浮动 | 该可选参数控制术语频率的饱和度。默认值为 1.2。较高的值会延迟饱和,而较低的值会导致过早饱和。 | | `b` | 浮动 | 此可选参数控制文档长度在规范化术语频率方面的影响程度。默认值为 0.75。较高的值会增加规范化的效果,而较低的值会降低其效果。 | | `discountOverlaps` | 布尔代数学体系的 | 此可选参数确定在基于文档长度计算范数时是否应忽略重叠标记(位置增量为 0 的标记)。默认值为`true`,忽略重叠的令牌。 | -
Saturation: In the VSM, the normalized term frequency of a document grows linearly with the growing term count and has no upper limit. In the BM25-based model, the normalized value grows nonlinearly, and after a point does not grow with the growing term count. This point is the saturation point for term frequency in BM25. Figure 8-4 shows a graph comparing term frequency for DefaultSimilarity (which is VSM based) and BM25Similarity (which is BM25 based).
图 8-4。
Term frequency in DefaultSimilarity vs. BM25Similarity In BM25Similarity, if the term frequency goes beyond a threshold, the increasing count will have no additional effect on the score of the document, and the score of VSM will be more than that of BM25.
以下是使用 bm25 相似度计算分数的步骤:
Register the BM25SimilarityFactory
in schema.xml
by using the similarity
element. Globally, only one similarity class should be defined. If no similarity definition is available in schema.xml
, by default DefaultSimilarity
is used. <schema>
..
<similarity class="solr.BM25SimilarityFactory" />
</schema>
Specify the configuration parameters. Table 8-1 describes the parameters supported by BM25SimilarityFactory
. Here is a sample configuration: <similarity class="solr.BM25SimilarityFactory">
<float name="k1">1.2</float>
<float name="b">0.75</float>
</similarity>
There is no standard rule that specifies the ideal value for the parameters k1
and b
. The optimal combination of k1
and b
is to be computed by experimenting on the dataset. Reindex the documents.
相似性
DFRSimilarity 实现了与随机性模型的差异,随机性模型是一种信息检索的概率模型。该模型通过测量实际术语分布和通过随机过程获得的术语分布之间的差异来计算术语权重。
通过该模型的早期形式观察到,信息项的分布不同于非信息项的分布。信息术语在一些文档中出现得更密集,称为精英文档,而非信息词随机分布在所有文档中。
这个框架由三个模型组成,在schema.xml
中注册工厂时需要配置。这三个模型将在接下来的小节中介绍。
基本模型
这个模型选择了基本的随机性模型。该框架支持七个基本模型,每个模型使用不同的排名算法。表 8-2 提供了支持型号的详细信息。这个步骤之后可以是两个归一化步骤,这增加了算法的灵活性。
表 8-2。
Basic Models Supported by Divergence from Randomness
| 基本模型 | 价值 | 描述 | | --- | --- | --- | | `BasicModelBE` | `Be` | 实现了玻色-爱因斯坦模型的极限形式。在某些情况下,这可能会导致性能问题。BasicModelG 是一个更好的选择。 | | `BasicModelG` | `G` | 实现了玻色-爱因斯坦模型的几何近似。 | | `BasicModelP` | `P` | 实现二项式模型的泊松近似。 | | `BasicModelD` | `D` | 实现二项式模型的散度近似。 | | `BasicModelIn` | `I(n)` | 考虑计算随机性的逆文档频率。 | | `BasicModelIne` | `I(ne)` | 通过使用泊松和逆文档频率的组合来计算随机性。 | | `BasicModelIF` | `I(F)` | 考虑逆文档频率的近似值来计算随机性。 |后效模型
这个模型也被称为第一归一化模型。它平滑从基本模型获得的权重。表 8-3 提供了支持的后效模型的详细信息。
表 8-3。
AfterEffect Supported by Divergence from Randomness
| 后果 | 价值 | 描述 | | --- | --- | --- | | `AfterEffectL` | `L` | 运用拉普拉斯定律。 | | `AfterEffectB` | `B` | 基于两个伯努利过程比值的信息增益模型。 | | `NoAfterEffect` | `none` | 此参数禁用第一次规范化。 |正常化
这个模型也被称为第二归一化模型。它对算法用来标准化术语频率的字段长度进行标准化。表 8-4 提供了支持的标准化模型的详细信息。
表 8-4。
Second Normalization Supported by Divergence from Randomness
| 正常化 | 价值 | 描述 | | --- | --- | --- | | `NormalizationH1` | `H1` | 假设频率项是均匀分布的。 | | `NormalizationH2` | `H2` | 在这个模型中,术语频率与场长度成反比。 | | `NormalizationH3` | `H3` | 实现由 Dirichlet 先验提供的术语频率归一化。 | | `NormalizationZ` | `Z` | 实现由 Pareto-Zipf 规范化提供的术语频率规范化。 | | `NoNormalization` | `none` | 禁用第二次规范化。 |使用
以下是使用 DFRSimilarity 计算文档得分的步骤:
Register the DFRSimilarityFactory
in schema.xml
by using the similarity
element. The basicModel
, afterEffect
, and normalization
parameters are mandatory, and the value of the desired class should be provided for each of these parameters. Tables 8-2, 8-3, and 8-4 provide options for basicModel
, afterEffect
, and normalization
, respectively. The following is a sample schema.xml
configuration: <schema>
..
<similarity class="solr.DFRSimilarityFactory">
<str name="basicModel">P</str>
<str name="afterEffect">L</str>
<str name="normalization">H2</str>
<float name="c">7</float>
<bool name="discountOverlaps">true</bool>
</similarity>
</schema>
DFRSimilarityFactory
supports the optional parameter c
to allow normalization, which controls the behavior of the implementations NormalizationH1
and NormalizationH2
. Reindex the documents. Note
参见 http://terrier.org/docs/v3.5/dfr_description.html
了解随机性模型偏离的全部细节。
其他相似性度量
除了前面讨论的算法,Lucene 还支持其他一些算法。每个都可以通过在一个similarity
元素中注册它的工厂来配置,就像前面的例子一样。以下是 Lucene 支持的其他相似之处:
- IBSimilarity:这为基于信息的概率模型提供了一个框架。这个框架和随机发散有很多相似之处。在注册相似度时,需要三个参数:
distribution
、lambda
和normalization
。详见http://lucene.apache.org/core/5_3_1/core/org/apache/lucene/search/similarities/IBSimilarity.html
的 Javadoc。 - SweetSpotSimilarity:这是对
DefaultSimilarity
的扩展,提供额外的调优选项,用于根据您的数据指定最佳术语频率的最佳点和lengthNorm
值。更多细节请参考http://lucene.apache.org/core/5_3_1/misc/org/apache/lucene/misc/SweetSpotSimilarity.html
的 Javadoc。 - Dirichlet 相似性:这是基于使用 Dirichlet 先验的贝叶斯平滑模型。它支持附加平滑参数
mu
。更多细节请参考https://lucene.apache.org/core/5_3_1/core/org/apache/lucene/search/similarities/LMDirichletSimilarity.html
的 Javadoc。 - LMJelinekMercerSimilarity:这是一个基于 Jelinek-Mercer 平滑方法的语言模型。它支持附加平滑参数
lambda
。更多细节请参考https://lucene.apache.org/core/5_3_1/core/org/apache/lucene/search/similarities/LMJelinekMercerSimilarity.html
的 Javadoc。
每场相似度
您已经了解了 Lucene 中几种可用的相似性选择,以及如何在全局范围内实现它们(无论您选择哪种实现,都适用于所有领域)。Lucene 和 Solr 允许你对不同的字段使用不同的Similarity
实现。
从行为角度来看,全局相似性将应用于所有字段,除非定义了特定于字段的相似性来覆盖该字段的默认相似性。以下是配置每个字段相似性的步骤:
Define solr.SchemaSimilarityFactory
as the global similarity class that delegates similarity if there is a field-specific similarity definition. Here is an example: <schema>
..
<similarity class="solr.SchemaSimilarityFactory">
</schema>
SchemaSimilarityFactory
specifies the DefaultSimilarityFactory
as an implicit global similarity, and any field-specific definition will apply the overriding similarity on that field. The schema.xml
can have only one global similarity factory. If any other Similarity
implementation is defined, that should be commented out. <!--similarity class="solr.BM25SimilarityFactory"-->
To override the global similarity for specific field, specify the applicable similarity factory in the fieldType
definition of that field. The following is a sample definition of DFRSimilarity
on text_general fieldType
. <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true"
words="stopwords.txt"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true"
words="stopwords.txt"/>
<filter class="solr.SynonymFilterFactory"
synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<similarity class="solr.DFRSimilarityFactory">
<str name="basicModel">I(F)</str>
<str name="afterEffect">B</str>
<str name="normalization">H2</str>
</similarity>
</fieldType>
Note
到目前为止,coord
和queryNorm
还没有作为SchemaSimilarityFactory
的一部分实现,所以你会得到 TF-IDF 不同的分数。
自定义相似性
Solr 允许您定制Similarity
实现。如果您想要调整现有相似性的行为,您可以扩展Similarity
实现并覆盖一个方法来插入您想要的计算。
假设您正在开发一个音乐元数据的搜索引擎,并且您发现诸如 love 和 heart 这样的术语很常见,但是仍然和其他不常见的术语一样有趣。如果您使用默认相似性,IDF 因子会认为稀有术语更重要,并赋予它们更高的权重。但在这种情况下并非如此,您想要禁用 IDF 权重。您可以使用以下步骤自定义此默认行为:
Extend the existing Similarity
implementation provided by Lucene. import org.apache.lucene.search.similarities.DefaultSimilarity;
public class NoIDFSimilarity extends DefaultSimilarity {
@Override
public float idf(long docFreq, long numDocs) {
return 1.0f;
}
}
In this class, you have overridden the idf()
method to always return a value of 1.0, which disables the role of IDF in computing the score of a document. If you want to provide your custom computation formula, you can put your code in the method and return the computed value. In DefaultSimilarity
, the method computes the IDF score as follows: public float idf(long docFreq, long numDocs) {
return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0);
}
Add the Java executable JAR of the project containing the class to the classpath. <lib dir="../../../custom-lib" regex="solr-practical-approach-\d.*\.jar" />
Register the custom similarity in schema.xml
either globally or on the desired fieldType
. The following example configures the custom similarity on a specific field. <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100">
<analyzer
<tokenizer class="solr.StandardTokenizerFactory"/>
</analyzer>
<similarity class="com.apress.solr.pa.chapter08.similarity.NoIDFSimilarity"/>
</fieldType>
<similarity class="solr.SchemaSimilarityFactory">
Generally, you want to apply the custom implementation at the field level, as the default similarity works well for most cases and your implementation would address the requirement of a specific field. For the preceding example, it’s not necessary to rebuild the index. But it’s advisable that you reindex the documents after changing the Similarity
implementation. Query for the result. Figure 8-5 contains a snapshot of the ranking explanation. You can see that NoIDFSimilarity
(underlined) is used as the Similarity
implementation for computing the score. You also can see that all IDF computation returns a constant score of 1.0. This constant value disables the default IDF implementation applied while computing the term weight.
图 8-5。
Explanation for custom similarity
前面的例子有一个限制:您可以调整现有的因子来计算相似性,但是不能引入一个新的因子。如果你想插入一个全新的相似性算法,或者是基于一篇研究论文,或者是你自己开发的东西,你需要做更多的工作,而不是扩展现有的Similarity
实现。首先,您需要扩展SimilarityFactory
并实现getSimilarity()
方法来返回您的自定义相似性,这扩展了抽象的Similarity
类。
摘要
在这一章中,你看到了 Lucene 是如何增强 Solr 的相关性排名的,Lucene 支持的各种相关性模型,以及主要模型的细节。您看到了 Lucene 的默认排名简单而强大,这也是 Lucene 更受欢迎的原因。您了解了主要模型中影响文档得分的因素。您还了解了文档分数的解释,这样您就可以理解为什么一个文档会排在另一个文档之上,或者出现在结果集的顶部。您还看到了覆盖默认相似性的示例代码,以自定义排名因素的行为。
九、附加功能
开发搜索引擎的目的是帮助用户以最方便的方式找到最相关的信息。在第六章和 7 中,您了解了检索文档的各种方法以及 Solr 提供的实现。
在本章中,您将了解 Solr 的其他重要特性,这些特性将带来便利并增加用户体验。您还将看到可以用来控制文档排名和向用户推荐其他感兴趣的文档的其他功能。
本章涵盖以下主题:
- 赞助搜索
- 拼写检查
- 自我暗示
- 文档相似度
赞助搜索
对于给定的查询,您可能希望将一组精选的文档提升到搜索结果的顶部。你通常需要这样一个特性,要么允许赞助搜索,要么支持编辑提升。Google AdWords 是赞助搜索的一个典型例子。此外,有时一个相关的文档可能在响应中排名靠后,或者一个不相关的文档可能排名靠后,您将需要一个快速的解决方案来修复这些问题,尤其是当它们在生产中被发现时。
Solr 提供了QueryElevationComponent
作为快速简单的解决方案来满足这些需求。对于指定的查询,它支持以下行为,而不考虑文档的分数:
- 将所需的一组文档置于搜索结果的顶部
- 从搜索结果中排除一组文档
Note
QueryElevationComponent
要求在schema.xml
中定义uniqueKey
字段。它也适用于分布式环境。
使用
以下是使用查询提升组件的步骤:
Define the elevation file. This file contains the rules for elevating and excluding documents. The filename must map to the name provided in the config-file
parameter in QueryElevationComponent
. The following is a sample query elevation file. elevate.xml <elevate>
<query text="dslr camera">
<doc id="101" />
<doc id="103" />
</query>
<query text="laptop">
<doc id="106" />
<doc id="108" exclude="true" />
</query>
</elevate>
The text
attribute of the query
element specifies the query to be editorially boosted. The doc
element represents the documents to be manipulated. Its id
attribute marks the document that should be elevated, and if the additional attribute exclude="true"
is specified, the document is marked for exclusion. The provided id
should map to the uniqueKey
of a document. The elevation file must exist either in the $SOLR_HOME/<core>/conf/
or $SOLR_HOME/<core>/data
directory. If it exists in conf
, modifications to the file will reflect on core reload; and if it exists in data
, modifications will reflect for each IndexReader
. Note The component first looks for elevate.xml
in the conf
directory (or ZooKeeper, if applicable) and then in data
. If you have the file in both directories, conf
will get the priority. Configure the QueryElevationComponent
in solrconfig.xml
. Table 9-1 describes the arguments supported to control the behavior of the component. The following is an example configuration.
表 9-1。
QueryElevationComponent Parameters
| 参数 | 描述 | | --- | --- | | `config-file` | 指定包含查询提升规则的文件的名称。 | | `queryFieldType` | 指定应该用于分析用户查询的`fieldType`。分析的术语与提升文件中定义的查询相匹配。指定的`fieldType`必须存在于`schema.xml`中。如果不想进行分析,将`string`指定为`queryFieldType`。 | | `forceElevation` | 查询提升根据配置从结果集中引入或删除文档,但考虑排序。如果应用除`score desc`之外的任何排序,提升的文档将根据排序条件改变它们的顺序。将该参数设置为`true`会覆盖该行为。 | | `editorialMarkerFieldName` | 该参数有助于区分提升的文档和有机排序的文档。提升的文档获得一个具有此名称的附加字段。默认名称是`editorial`。当分配的名称被添加到`fl`请求参数时,标记被启用。进一步提供了使用这种标记的例子。 | | `markExcludes` | 默认情况下,排除的文档会从搜索结果中删除。将该参数设置为`true`会将此类文档标记为已排除,而不是将其全部删除。 | | `excludeMarkerFieldName` | 此参数为排除的文档分配一个字段名。它仅适用于`markExcludes`参数。分配的默认名称是`excluded`。 |<searchComponent name="elevator" class="solr.QueryElevationComponent" >
<str name="config-file">elevate.xml</str>
<str name="queryFieldType">string</str>
</searchComponent>
Register the component to the last-``components
list of the desired handler. <requestHandler name="/select" class="solr.SearchHandler">
<lst name="defaults">
<str name="echoParams">explicit</str>
</lst>
<arr name="last-components">
<str>elevator</str>
</arr>
</requestHandler>
Provide additional request parameters for the component. Table 9-2 lists the additional parameters supported.
表 9-2。
QueryElevationComponent Request Parameters
| 参数 | 描述 | | --- | --- | | `enableElevation` | 该参数启用`QueryElevationComponent`。 | | `exclusive` | 如果启用此布尔参数,则仅返回提升的结果。有机结果将被忽略。 | | `elevateIds` | 此参数指定要提升的文档 id 的逗号分隔列表。 | | `excludeIds` | 此参数指定要排除的文档 id 的逗号分隔列表。 |Query for results. The following are some sample queries. Search request with QueryElevationComponent enabled $ curl "
http://localhost:8983/solr/hellosolr/select?q=laptop&enableElevation=true
"
Search request that marks the elevated and excluded field $ curl "
http://localhost:8983/solr/hellosolr/select?q=laptop
&markExcludes=true&fl=*,[elevated],[excluded]"
Search request to mark the elevated field, if editorialMarkerFieldName is specified as paid $ curl "
http://localhost:8983/solr/hellosolr/select?q=laptop
&enableElevation=true&fl=*,[paid],score"
Search request that specifies the elevation and exclusion IDs $ curl "
http://localhost:8983/solr/hellosolr/select?q=cable
&df=product&excludeIds=IW-02&elevateIds=3007WFP,9885A004"
拼写检查
用户查询容易出错,所以几乎所有流行的搜索引擎都支持拼写检查功能。对于拼写错误的查询,此功能会建议文本的正确形式。您通常会在搜索框的正下方看到建议。例如,去 Amazon.com 并搜索 laptap(laptop 的拼写错误形式),响应将包含额外的信息,如您的意思是:laptop。
搜索引擎采用两种方法进行拼写检查。他们要么执行原始查询并提供拼写建议,要么执行拼写纠正的查询,并选择运行原始查询。网络搜索引擎,如谷歌、必应和雅虎!采取乐观的拼写纠正方法。它们检索正确形式的查询的结果,并为用户提供执行原始查询的选项。
图 9-1 描述了一个典型的拼写检查示例(来自 Google ),其中拼写错误的查询 wikipedia 被纠正为 Wikipedia,并为其提供了一个结果。
图 9-1。
Example of spell-checking in Google
Solr 为拼写检查提供了现成的支持。它允许您根据 Solr 字段、外部 Lucene 索引或外部文本文件中维护的术语提供建议。Solr 提供了SpellCheckComponent
,它扩展了SearchComponent
来实现这个特性,并且可以配置到任何SearchHandler
来让它工作。
如果你只是想给用户提供一个拼写建议,比如问“你的意思是?”,将SpellCheckComponent
配置到查询处理程序,例如/select
,并在适当的位置显示该组件的响应,以供用户操作。如果您的目的是向用户提供一个自动修正的结果,那么在一个单独的处理程序中配置SpellCheckComponent
,比如/spell
,并在启用排序的情况下进行查询。在本节的后面,您将了解到排序规则。客户端应该使用这个处理程序返回的拼写正确的响应向查询处理程序发出新的请求,比如/select
。这个自动纠正过程需要两个 Solr 请求。
SpellCheckComponent
提供了一组拼写检查的可选选项,可在classname
参数中指定。可以将多个拼写检查器配置为同时执行。
您应该对用于拼写检查的字段执行最少的分析。应该避免将标记转换为完全不同的形式的分析,例如词干分析或同义词扩展。对于基于 Solr 字段的拼写检查器,最好有一个单独的字段用于拼写检查,所有需要拼写建议的字段的内容都可以复制到这个字段中。
SpellCheckComponent
支持分布式环境。
通用参数
表 9-3 指的是通用参数,可以在任何拼写检查器实现中使用。
表 9-3。
SpellCheckComponent Generic Parameters
| 参数 | 描述 | | --- | --- | | `name` | 为拼写检查定义指定一个名称。在为组件配置多个拼写检查器时,此参数非常有用。 | | `classname` | 指定要使用的拼写检查器实现。值可以指定为`solr.`,比如`solr.FileBasedSpellCheck`。默认实现是`IndexBasedSpellCheck`。 |履行
本节解释 Solr 中可用的拼写检查器实现。每个都提供了一组特定参数,可与表 9-3 中的通用参数一起使用。
IndexBasedSpellChecker
使用单独的索引来维护拼写检查词典。该索引是一个附加索引,也称为副索引,它是与主索引分开创建和维护的。在配置拼写检查器时,您可以指定用于创建 sidecar 索引的源字段。
这是在SpellCheckComponent
中注册的默认拼写检查器实现。
表 9-4 规定了配置IndexBasedSpellChecker
的附加参数。
表 9-4。
IndexBasedSpellChecker Parameters
| 参数 | 描述 | | --- | --- | | `field` | 指定 Solr 字段,在`schema.xml`中定义,用于构建字典。 | | `sourceLocation` | 这个拼写检查器不从 Solr 字段加载术语,而是允许您从任意 Lucene 索引中加载术语。该参数指定 Lucene 索引的目录。 | | `spellcheckIndexDir` | 指定将创建边车索引的目录。 | | `buildOnCommit` | 将这个布尔参数设置为`true`会在每次提交时构建字典。默认情况下,该参数设置为`false`。 |DirectSolrSpellChecker
这个实现使用主 Solr 索引,而不是专门为拼写检查构建额外的索引。因为使用了主索引,拼写检查器总是有最新的可用术语,并且还可以省去您定期重建索引的麻烦。
表 9-5 规定了配置DirectSolrSpellChecker
的附加参数。
表 9-5。
DirectSolrSpellChecker Parameters
| 参数 | 描述 | | --- | --- | | `field` | 指定 Solr 字段,在`schema.xml`中定义,用于拼写检查。 | | `accuracy` | 指定建议的准确性级别。默认值为 0.5。 | | `maxEdits` | 指定术语中允许的最大修改次数。 | | `minPrefix` | 指定允许编辑的最小初始字符数。值越高,性能越好。此外,您会注意到前几个字符通常不会拼写错误。默认值为 1。 | | `maxInspections` | 指定在返回建议之前要检查的最大匹配数。默认值为 5。 | | `minQueryLength` | 指定查询中用于生成建议的最小字符数。如果查询短于此长度,将不会生成任何建议。 | | `maxQueryFrequency` | 指定查询词应该出现在文档中的最大数量。如果计数大于指定的阈值,该项将被忽略。该值可以是绝对值(例如 5)或百分比(例如 0.01 或 1%)。默认值为 0.01。 | | `thresholdTokenFrequency` | 指定查询词应该出现在文档中的最小数量。如果计数小于指定的阈值,该项将被忽略。与`maxQueryFrequency`类似,可以指定绝对值或百分比。默认值为 0.0。 |FileBasedSpellChecker
使用一个外部文本文件作为拼写的来源,并用它构建一个 Lucene 索引。当您不希望拼写检查器基于索引文档中的术语,而是从另一个来源(如频繁查询的日志分析或外部主题词表)提取时,这种实现很有帮助。
源文件应该是一个简单的文本文件,每行定义一个单词。这是一个样本文件。
medical-spellings.txt
advanced
aided
assigned
assessed
assisted
..
表 9-6 规定了配置FileBasedSpellChecker
的附加参数。
表 9-6。
FileBasedSpellChecker Parameters
| 参数 | 描述 | | --- | --- | | `sourceLocation` | 指定包含术语的文本文件的路径。 | | `characterEncoding` | 指定文件中术语的编码。 | | `spellcheckIndexDir` | 指定将创建索引的目录。 |WordBreakSolrSpellChecker
该实现侧重于执行拼写检查,以检测由于缺少或不需要的空白而导致的拼写错误。用户很可能会将一个单词拆分成两个单词,或者将两个单词合并成一个单词。一个典型的例子是拼写检查和拼写检查。WordBreakSolrSpellChecker
就是为了解决这个特殊问题而开发的。它组合和分解术语来提供建议。
表 9-7 规定了配置WordBreakSolrSpellChecker
的附加参数。
表 9-7。
WordBreakSolrSpellChecker Parameters
| 参数 | 描述 | | --- | --- | | `field` | 指定 Solr 字段,在`schema.xml`中定义,用于构建字典。 | | `combineWords` | 指定是否应该合并相邻的术语。默认值为`true`。 | | `breakWords` | 指定拼写检查器是否应该尝试将术语拆分为多个术语。默认值为`true`。 | | `maxChanges` | 指定拼写检查器应该进行的最大排序尝试次数。 |它是如何工作的
Solr 中的SpellCheckComponent
类实现了SearchComponent
,它利用了 Lucene 提供的SpellChecker
实现。SpellCheckComponent
处理请求的步骤如下:
The client makes a request to a SearchHandler
that has SpellCheckComponent
registered to it. The SpellCheckComponent
, like any other SearchComponent
, executes in two phases, namely prepare and process. If the received request is to build or reload the dictionary, it is done in the prepare phase. DirectSolrSpellChecker
and WordBreakSolrSpellChecker
don’t require a build or reload. In the process phase, Solr tokenizes the query with the applicable query analyzer and calls the appropriate SpellChecker
with the provided request parameters. The spell-checker implementation generates suggestions from the loaded dictionary or field as applicable. If collation is required, Solr calls the SpellCheckCollator
to collate the spellings. The generated responses are returned to the client. Note
如果您对内部处理的细节感兴趣或者正在计划定制,可以参考这些步骤;否则 Solr 不要求你了解他们。
使用
下面是集成和使用拼写检查器的步骤:
Define the SpellCheckComponent
in solrconfig.xml
, specify the implementation classname, and add the parameters from the table of corresponding classnames. Register a single spell-checker <searchComponent name="spellcheck" class="solr.SpellCheckComponent">
<lst name="spellchecker">
<str name="classname">solr.IndexBasedSpellChecker</str>
<str name="spellcheckIndexDir">./spellchecker</str>
<str name="field">spellings</str>
<str name="buildOnCommit">true</str>
</lst>
</searchComponent>
Register multiple spell-checkers <searchComponent name="spellcheck" class="solr.SpellCheckComponent">
<lst name="spellchecker">
<str name="name">primary</str>
<str name="classname">solr.IndexBasedSpellChecker</str>
..
</lst>
<lst name="spellchecker">
<str name="name">secondary</str>
<str name="classname">solr.FileBasedSpellChecker</str>
..
</lst>
</searchComponent>
Register the component to the desired SearchHandler
. You can register it either to your primary handler that serves the search request or a separate handler dedicated for spell-checking. The following is an example configuration that registers multiple spell-checkers to the /select
handler. <requestHandler name="/select" class="solr.SearchHandler">
<lst name="defaults">
<str name="spellcheck.dictionary">primary</str>
<str name="spellcheck.dictionary">secondary</str>
</lst>
<arr name="last-components">
<str>spellcheck</str>
</arr>
</requestHandler>
Table 9-8 lists the request parameters supported by SpellCheckComponent
.
表 9-8。
SpellCheckerComponent Request Parameters
| 参数 | 描述 | | --- | --- | | `spellcheck` | 默认情况下,拼写检查是禁用的。设置`spellcheck="true"`打开该功能。 | | `spellcheck.q` | 指定拼写检查的查询。如果未指定该参数,组件将从`q`参数中获取值。 | | `spellcheck.count` | 指定要返回的建议的最大数量。默认值为 1。 | | `spellcheck.dictionary` | 指定用于请求的词典。 | | `spellcheck.build` | 此参数清除现有字典,并使用源中的最新内容创建一个新副本。此操作的成本可能很高,应该偶尔触发。 | | `spellcheck.reload` | 将该参数设置为`true`会重新加载拼写检查器和底层词典。 | | `spellcheck.accuracy` | 以浮点值的形式指定所需的精度级别。分数低于此值的建议将不会提供。 | | `spellcheck.alternativeTermCount` | 指定为每个查询词返回的建议数。这有助于构建上下文相关的拼写纠正。 | | `spellcheck.collate` | 如果有多个建议,或者您想向用户提供拼写正确的查询结果,那么将该参数设置为`true`是一个不错的选择。该特性为查询中的每个标记提取最佳建议,并将它们组合成一个新的查询,您可以执行该查询以获得建议的结果。 | | `spellcheck.maxCollations` | 默认情况下,Solr 返回一个排序规则。您可以设置该参数来获取更多的排序规则。 | | `spellcheck.maxCollationTries` | 排序规则的功能确保排序查询在索引中找到匹配项。该特性指定了对索引进行测试的次数。在测试时,原始查询被排序规则替代,并尝试进行匹配。默认值 0 可以跳过测试,从而导致不匹配。较高的值可以确保更好的排序,但代价会很高。 | | `spellcheck.maxCollationEvaluations` | 指定要评估和排序的最大组合数,以形成对索引进行测试的排序规则。默认值已被优化设置为 10,000 个组合。 | | `spellcheck.collateExtendedResult` | 将该参数设置为`true`将扩展响应,以包含排序规则的详细信息。 | | `spellcheck.collateMaxCollectDocs` | 测试排序规则时,如果不需要命中次数,此参数有助于提高性能。对于值 0,通过对所有文档运行测试来提取准确的命中次数。对于大于 0 的值,基于那些文档提供估计。 | | `spellcheck.collateParam.*` | 您可以使用此前缀来覆盖默认的查询参数,或者为归类测试查询提供一个附加的查询参数。通过在`spellcheck.collateParam.fq`中指定覆盖值,可以覆盖所有的`fq`参数。 | | `spellcheck.extendedResults` | 此布尔参数指定是否使用包含更多详细信息的扩展响应格式。扩展响应格式不同于标准建议;对于每个建议,它提供了文档频率,对于每个术语,它提供了一个包含附加信息(如术语频率)的建议块,默认值为`false`。 | | `spellcheck.onlyMorePopular` | 如果该参数设置为`true`,则仅当建议的标记比原始术语具有更高的文档频率时,才返回建议。默认情况下,该参数设置为`false`。 | | `spellcheck.maxResultsForSuggest` | 默认情况下,`correctlySpelled`响应参数被设置为`false`,只有当查询术语在字典和索引中都缺失时,才会返回建议。如果索引的结果达到指定的数量,则将该参数设置为大于 0 的值将返回一个建议。例如,如果响应最多有 9 个结果,值 10 将返回建议,如果有 10 个或更多结果,则不返回建议。 |Query for search results with the spell-checking feature enabled. Builds the spell-checker and then generates a spelling suggestion
$ curl "
http://localhost:8983/solr/hellosolr/select?q=wikipadia
&spellcheck=true&spellcheck.build=true"
任何拼写建议的请求只有在词典建立后才会得到结果。因此,在请求拼写建议之前,spellcheck.build=true
应该至少被触发一次。构建操作可能成本很高,应该不经常触发。
Request suggestions for the user query
$ curl "
http://localhost:8983/solr/hellosolr/select?q=wikipadia
&spellcheck=true&spellcheck.count=5&spellcheck.accuracy=0.6"
自动完成
自动完成是用户搜索体验的一个重要特性。这是一种预输入搜索,只要用户输入几个字符,它就会完成单词或预测其余的单词。对于用户键入的每个额外字符,建议被细化。大多数现代搜索引擎都支持这个特性。
以下是实现自动完成的一些主要原因:
- 实现自动完成最简单的原因是为了避免拼写错误。如果所提供的建议是正确的,并且用户选择了它,则查询将没有拼写错误。
- 如果预测是正确的,用户就不需要键入完整的查询。用户可以选择其中一个建议,然后按回车键。
- 有时用户知道他们想要什么,但不知道如何用语言表达。当一个搜索关键字有许多常见的同义词,或者您正在寻找某个问题的答案时,您可能会遇到这种情况。Google autocomplete 在这方面做得很好,如果弹出一个建议,您可以相当自信地认为您正在制定一个正确的查询。
- 自动完成有助于查询分类和应用过滤器。在电子商务中,建议根据产品的主要方面对产品进行分类。例如,查询 jeans 可能会给出诸如 jeans(男士)或 jeans(女士)的建议,而查询 apple 可能会给出诸如 apple(手机)或 apple(笔记本电脑)的建议。
- 该功能还用于根据查询生成建议。它再次广泛应用于电子商务,根据查询向用户推荐最畅销的产品。例如,只要你输入尼康,自动完成功能就会显示像尼康 3200 这样的畅销产品及其价格,甚至可能还有一张图片。
自我暗示可以有多种类型,也可以基于多种来源。提供给用户的建议可以是完整的短语或特定的单词。短语可以从 Solr 索引或用户日志中提取。如果搜索引擎被广泛使用并拥有大量用户,从日志中提取的短语可以产生更实际的建议。来自日志的查询应该经过一系列的处理,比如过滤掉索引中没有匹配的查询;有拼写错误的查询应该被过滤掉或进行拼写纠正;网络搜索引擎会先过滤掉对敏感话题和不良内容的查询,然后再生成短语。在建议之前应该折叠短语,以便不会多次建议相同或相似的短语。
短语建议可以与令牌建议等其他方法结合使用,令牌建议可以作为一种后备机制。当没有短语建议可用于用户键入的查询时,系统可以完成用户正在键入的单词。
Solr 提供了多种供应和实现来实现自动补全。每一种都有其优点和局限性。您选择哪一种或哪几种组合取决于您的需求。由于为用户按下的每个键都提供了一个建议,所以无论您采用哪种方法都不应该忽略性能,并且应该快速生成建议。Solr 提供的实现可以大致分为两类:
- 传统方法:这种方法利用 Solr 的现有分析器和组件来获得特性。
- SuggestionComponent:专门为支持自动完成功能而开发的组件。
传统方法
传统的自动建议方法指的是 Solr 版之前引入的条款。在这种方法中,自动完成是通过利用 Solr 中已经可用的特性来实现的。您所需要做的就是对它进行适当的配置,让它正常工作。
在这种方法中,应该根据您想要生成的建议类型,在现场配置适当的分析器。对于暗示短语,KeywordTokenizerFactory
是合适的;而对于暗示的话,StandardTokenizerFactory
或者WhitespaceTokenizerFactory
是合适的。您可以两者都用,给短语字段更高的权重。
用于生成建议的字段应该分析为小写,以执行不区分大小写的搜索。一些组件(接下来将详细讨论)不支持查询时分析,在这种情况下,客户端程序可能需要将查询转换成小写形式。任何其他处理都可以在索引时分析中进行,以便适当地匹配标记。
以下是实现自我暗示的传统方法。
术语组件
TermsComponent
提供对字段中索引的术语的直接访问,并返回包含该术语的文档数。该组件直接访问 Lucene 术语词典,因此检索速度很快。
利益
以下是使用TermsComponent
进行自动暗示的好处:
- 直接查找术语词典,因此是所有传统自我暗示方法中最快的。
- 它允许您执行前缀和中缀查询。
限制
以下是这种方法的局限性:
TermsComponent
直接查找术语词典,因此不能应用过滤器将结果限制在文档的子集内。例如,您不能只检索库存产品。- 直接查找的另一个限制是,它还会考虑标记为删除的术语。
TermsComponent
不分析查询。
使用
以下是为自动建议配置TermsComponent
的步骤:
Define the TermsComponent
in solrconfig.xml
. <searchComponent name="terms" class="solr.TermsComponent"/>
Register the component to the desired request handler. <requestHandler name="/terms" class="solr.SearchHandler" startup="lazy">
<lst name="defaults">
<bool name="distrib">true</bool>
</lst>
<arr name="components">
<str>terms</str>
</arr>
</requestHandler>
Table 9-9 defines the important parameters of TermsComponent
for supporting autocomplete.
表 9-9。
TermsComponent Important Request Parameters
| 参数 | 描述 | | --- | --- | | `terms` | 指定`terms="true"`启用`TermsComponent`。默认情况下,该组件处于禁用状态。 | | `terms.fl` | 指定应该从中检索术语的字段名称。 | | `terms.limit` | 指定要检索的最大术语数。 | | `terms.prefix` | 在此参数中指定用户键入的查询,以检索以该查询开头的所有标记。 | | `terms.regex` | 指定用于检索术语的正则表达式模式。这对中缀自动补全很有用。该操作的执行成本可能比`terms.prefix`高,但允许中缀操作。在请求处理程序之前,用户查询需要预处理来形成正则表达式。 | | `terms.regex.flag` | 指定`terms.regex.flag=case_insensitive`执行不区分大小写的正则表达式。 | | `terms.mincount` | 响应中将不会返回文档频率小于指定值的术语。此参数可用于避免拼写错误的内容,假设它们出现的频率很低。 | | `terms.maxcount` | 文档频率超过指定值的术语将不会在响应中返回。此参数有助于消除可能成为停用词的术语。 | | `terms.sort` | 指定`terms.sort=count`首先对出现频率最高的术语进行排序,指定`terms.sort=index`按索引顺序进行排序。 |No specific text analysis is required on the fieldType
. You may just want to convert all tokens to lowercase for case-insensitive matching. A typical fieldType
is defined as follows: <fieldType name="autocomplete" class="solr.TextField" positionIncrementGap="100" >
<analyzer type="index">
<tokenizer class="solr.KeywordTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.KeywordTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
Define the field for autosuggestion. The field need not be stored. <field name="brand" type="autocomplete" indexed="true" stored="false" />
Send a request for each character typed by the user. Suggest all terms beginning with specified characters
$ curl "
http://localhost:8983/solr/hellosolr/terms?terms=true&terms.fl=brand"
Suggest all terms containing the specified characters
$ curl "
http://localhost:8983/solr/hellosolr/terms?terms=true&terms.fl=brand
&terms.regex=.*eebo.*&terms.regex.flag=case_insensitive&terms.limit=5"
面状
分面可以用来实现自我暗示。这有利于在category
等字段上生成建议。方面也将计数与术语一起返回。第七章提供了刻面的细节。
利益
以下是使用FacetComponent
进行自动建议的好处:
- 它允许您过滤要在文档子集上运行的建议。例如,您可以只检索有库存的产品。
限制
以下是这种方法的局限性:
- 分面是一个占用大量内存的过程,尤其是在唯一令牌的数量很大的情况下。与
TermsComponent
相比,它的响应时间也很长。 - 它不支持检索中缀项。
- 它不分析查询。
使用
默认情况下,FacetComponent
对处理程序可用,不需要注册。以下是使用分面配置自动建议的步骤:
Define the fieldType
for text analysis of the faceted field. <fieldType name="autocomplete" class="solr.TextField" positionIncrementGap="100" >
<analyzer type="index">
<tokenizer class="solr.KeywordTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.KeywordTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
Define the field for autosuggestion. The field need not be stored. <field name="autocomplete" type="autocomplete" indexed="true" stored="false" />
Leverage the facet.prefix
request parameter provided by faceting for generating a suggestion. The value supplied to this parameter is not analyzed, so as a convention always lowercase the value before supplying it to the field. Generate a suggestion using the facet.prefix feature of FacetComponent
$ curl "``http://localhost:8983/solr/hellosolr/select?q
&rows=0&facet=true&facet.field=brand&facet.mincount=5
&facet.limit=5&facet.prefix=ree"
表 9-10 描述了FacetComponent
支持自动建议的重要参数。
表 9-10。
FacetComponent Parameters for Autosuggestion
| 参数 | 描述 | | --- | --- | | `facet` | 指定`facet=true`启用`FacetComponent`。 | | `rows` | 设置`rows=0`,因为你不需要从 Solr 获取搜索结果。 | | `facet.prefix` | 指定用户查询以检索以它开头的所有建议。 | | `facet.field` | 指定应在其上执行刻面的字段。 | | `facet.mincount` | 此参数可用于避免拼写错误的内容,假设对计数小于阈值的术语不感兴趣。 | | `facete.limit` | 此参数限制生成的建议数量。 |EdgeNGram
我们在第四章中讨论模式设计时讨论了NGram
。EdgeNGram
从一条边开始,将记号分解成不同大小的子记号。对于 autocomplete,只对从前端创建的标记感兴趣。
利益
以下是使用EdgeNGram
进行自动暗示的好处:
- 当您需要搜索整个文档而不仅仅是拼写建议时,
EdgeNGram
对于生成建议非常有用。电子商务中的一个典型例子是向用户推荐受欢迎的产品,在这种情况下,您还需要返回产品的图片和价格。 - 它允许您将结果限制在文档的子集内。例如,您可以只检索库存产品。
- 可以提升文档,例如,如果您希望热门的匹配文档在建议列表中排名更高。
- 可以执行查询时分析。
限制
以下是这种方法的局限性:
- 这种方法会生成许多令牌,从而增加索引的大小。
- 可能会有性能问题,缓慢的提前键入违背了自动完成的目的。
- 搜索查询在
EdgeNGram
字段上执行,响应可以包含重复的标记。因此,多个文档的建议字段不应具有相同的值。如果您不能删除多余的文档,您需要使用一种混合的方法,即EdgeNGram
和刻面。EdgeNGram
可以在用户查询日志中创建一个单独的索引来生成建议时发挥作用。
使用
以下是使用EdgeNGram
配置自动建议的步骤:
Define a fieldType
to use EdgeNGram
. <fieldType name="edgengram" class="solr.TextField" positionIncrementGap="100"
omitNorms="true" omitTermFreqAndPositions="true">
<analyzer type="index">
<tokenizer class="solr.KeywordTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.EdgeNGramFilterFactory" minGramSize="3" maxGramSize="15" />
</analyzer>
<analyzer type="query">
<tokenizer class="solr.KeywordTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
Set the attribute minGramSize
to specify the minimum characters from which suggestions should start coming up. Specify the attribute maxGramSize
to specify the characters beyond which you don’t want to provide any refinement. The EdgeNGramFilterFactory
should be applied only at index time. In the preceding fieldType
text analysis, you have defined EdgeNGramFilterFactory
to generate grams from a minimum size of 3 to a maximum size of 15. Define the field for generating suggestions. <field name="autocomplete" type="edgengram" indexed="true" stored="true" />
Execute the query to generate suggestions. $ curl "
http://localhost:8983/solr/hellosolr/select?q=autocomplete:lapt
"
建议组件
Solr 3.1 引入了一个专门的组件来解决自动建议的问题。这种方法使用 Lucene 的 suggester 模块,比前面讨论的方法更加强大和灵活。
SuggestComponent
的两个重要方面是字典和查找算法。在配置建议器时,您需要选择两者的实现。接下来将描述这两个方面。
词典
字典指定了维护术语的来源。Solr 提供的实现使用了Dictionary
接口。概括地说,词典可以有以下两种类型:
- 基于索引:使用 Lucene 索引作为生成建议的来源
- 基于文件:使用指定位置的文本文件来生成建议
以下是 Solr 提供的实现。
文档字典工厂
DocumentDictionaryFactory
是一个基于索引的字典,使用术语、权重和可选的有效负载来生成建议。以下是此实现支持的附加参数。
weightField
:如果要给术语分配权重,在此参数中指定包含权重的字段名。分配的默认权重为 0。该字段应该被存储。payloadField
:如果你想给术语分配一个有效载荷,在此参数中指定包含有效载荷的字段名。该字段应该被存储。
DocumentExpressionDictionaryFactory
这个工厂提供了一个DocumentValueSourceDictionary
的实现,它扩展了DocumentDictionary
来支持一个权重表达式,而不是一个数字权重。
以下是此实现支持的附加参数:
weightExpression
:您可以指定一个表达式来计算术语的权重。它使用 Lucene 表达式模块来计算表达式。表达式定义类似于函数查询表达式。例如,"((div(rating,10) + 1) + sqrt(popularity))
"就是一个有效的表达式。表达式中指定的字段应为数字。该参数是必需的。payloadField
:如果你想给术语分配一个有效载荷,在此参数中指定包含有效载荷的字段名。必须存储该字段。
高频词典
这是基于索引的字典的默认实现,它允许您只考虑构建字典的常用术语。频率小于阈值的项被丢弃。
以下是此实现支持的附加参数:
threshold
:该参数指定添加到字典中的术语的阈值。该值可以在 0 和 1 之间,表示该术语应该出现在文档中的比例。该参数是可选的。如果未指定,则考虑所有术语。
FileDictionaryFactory
这是唯一支持的基于文件的字典实现,它允许您从外部文件读取建议。类似于基于索引的字典,权重和有效负载是允许的,可以在由分隔符分隔的术语后指定。
以下是此实现支持的附加参数:
fieldDelimiter
:指定分隔条款、重量和有效载荷的分隔符。默认值为 tab。
这里是一个维护建议的样本文件。
suggestions.txt
# File based suggestions
mobile\t1.0
mobile phone\t3.0
mobile cases\t2.0
算法
在选择了合适的字典实现之后,您需要选择最适合您的用例的lookupImpl
。Lucene 支持一组算法(或数据结构),并为它们提供不同的实现。所有的实现类都扩展了Lookup
抽象类。
以下是支持的数据结构的广泛定义:
- 有限状态转换器(FST):建议者用所有的术语建立一个自动机,用来满足所有的自动建议请求。
- 自动机提供快速搜索,内存占用低,但在所有支持的数据结构中,构建自动机的过程是最慢的。此外,术语不能附加到自动机。对于添加术语,应该从头开始构建。自动机可以作为二进制 blob 保存到磁盘上,以便在 Solr 重启或内核重载时快速重载。
- 三元搜索树(TST):三元搜索树类似于二叉查找树,但是最多可以有三个子树。它为术语查找提供了一种快速灵活的方法。该树可以动态更新。
- JaSpell 算法:该算法由 Bruno Martins 编写,使用三叉树来提供高度复杂的建议。它可以快速构建数据结构。它支持基于 Levenshtein 距离的模糊查找,比 FST 更复杂。参考 Jaspell 网站
http://jaspell.sourceforge.net/
。 - 这些建议可以按字母顺序或诸如评级、受欢迎程度等参数排序。
- 基于索引:这种方法使用 Lucene 索引进行查找。
以下是 Solr 为查找提供的实现。每一个都定义了自己的参数集来支持所提供的功能。
TSTLookupFactory
该实现提供了基于三元搜索树的查找。它的建议者不支持有效负载,也不需要指定任何额外的参数。
FSTLookupFactory
这种实现提供了基于自动机的查找,速度非常快。这是一个很好的建议,除非你需要一个更复杂的。表 9-11 指定了FSTLookupFactory
支持的参数。
表 9-11。
FSTLookupFactory Parameters
| 参数 | 描述 | | --- | --- | | `weightBuckets` | 指定为权重创建的桶数。计数可以在 1 到 255 之间。默认桶数是 10。 | | `exactMatchFirst` | 如果设置为`true`,则首先返回准确的建议,而不考虑自动机中可用的其他字符串的前缀。 |WFSTLookupFactory
这种实现基于加权 FST 算法。它使用最短路径方法来查找最佳建议。它支持exactMatchFirst
参数。表 9-11 提供了关于该参数的信息。
JaspellLookupFactory
该实现基于 JaSpell 算法。它的执行速度很快,对许多问题都很有用。
分析工厂
这种实现使用加权 FST 进行查找。它被称为AnalyzingLookupFactory
,因为它在构建 FST 之前和查找期间分析文本。查找之前的分析提供了强大而灵活的自我暗示。分析链可以配置为使用停用词和同义词扩展等功能。例如,如果将 cell phone 和 mobile phone 定义为同义词,则用户文本单元格可以提供手机壳等建议。
分析器应该仔细配置,因为像停用词这样的特性可能导致没有建议。此外,返回的建议是原始文本,而不是文本的分析形式。表 9-12 指定了AnalyzingLookupFactory
支持的参数。
表 9-12。
AnalyzingLookupFactory Parameters
| 财产 | 描述 | | --- | --- | | `suggestAnalyzerFieldType` | 指定用于分析的`fieldType`。 | | `exactMatchFirst` | 如果设置为`true`,精确匹配将作为首选建议返回,而不考虑其他具有更高权重的建议。默认设置为`true`。 | | `preserveSep` | 如果设置为`true`,则保留标记分隔符(即`cellphone`和`cell phone`不同)。默认设置为`true`。 | | `preservePositionIncrements` | 如果设置为`true`,位置增量保持不变。如果设置为`false`,假设 of 是一个停用词,那么像`best 20`这样的用户查询将生成类似 2015 年最佳的建议。默认设置为`false’`。 |模糊 LookupFactory
FuzzyLookupFactory
扩展了AnalyzingLookup
的功能,允许基于 Levenshtein 距离对分析文本进行模糊匹配。它支持AnalyzingLookupFactory
的所有参数,以及表 9-13 中提到的附加参数。
表 9-13。
FuzzyLookupFactory Parameters
| 财产 | 描述 | | --- | --- | | `maxEdits` | 指定允许的最大编辑次数。默认值为 1。该值的硬限制被指定为 2。 | | `transpositions` | 如果`true`,将使用图元编辑操作计算变调。如果`false`,将使用 Levenshtein 算法。 | | `nonFuzzyPrefix` | 指定查找键的长度,超过该长度的字符将执行模糊匹配。应该只建议包含键的初始前缀字符的匹配。默认值为 1。 | | `minFuzzyLength` | 指定长度,在该长度以下将不对查找关键字执行编辑。默认值为 3。 | | `unicodeAware` | 默认情况下,该参数设置为`false`,前面四个参数以字节为单位。如果该参数设置为`true`,它们将以实际字符进行测量。 |分析 gInfixLookupFactory
使用 Lucene 索引作为字典,并对索引的术语提供灵活的基于前缀的建议。与AnalyzingLookupFactory
类似,它也在构建字典和查找时分析输入文本。表 9-14 指定了AnalyzingInfixLookupFactory
支持的参数。
表 9-14。
AnalyzingInfixLookupFactory Parameters
| 参数 | 定义 | | --- | --- | | `indexPath` | 指定存储和加载索引的目录。默认情况下,索引是在`data`目录中创建的。 | | `allTermsRequired` | 如果`true`,在多项键上应用布尔运算符`AND`;否则,应用操作员`OR`。默认值为`true`。 | | `minPrefixChars` | 指定从开始使用`prefixQuery`的最小字符数。n 元语法是为短于这个长度的前缀生成的,这提供了更快的查找速度,但增加了索引大小。默认值为 4。 | | `highlight` | 如果`true`是默认值,则建议被突出显示。 |BlendedInfixLookupFactory
这个实现是对AnalyzingInfixLookupFactory
的扩展,它允许您对前缀匹配应用权重。它允许您根据第一个匹配单词的位置分配更多的权重。它支持AnalyzingInfixLookupFactory
的所有参数,以及表 9-15 中提到的附加参数。
表 9-15。
BlendedInfixLookupFactory Parameters
| 参数 | 描述 | | --- | --- | | `blenderType` | 指定用于计算权重系数的搅拌机类型。以下是支持的搅拌机类型:`linear`:使用公式`weight×(1 - 0.10×position)`计算重量。它在开始时给予匹配更高的权重。这是默认的`blenderType`。`reciprocal`:使用公式`weight/(1+position)`计算重量。它最终会给匹配项更高的权重。 | | `numFactors` | 指定结果数的倍增因子。默认值为 10。 |FreeTextLookupFactory
当其他建议者找不到匹配时,这个建议者已经实现了回退建议的需求。它在构建词典时构建文本的 N 元语法,并在查找时考虑来自用户查询的最后 N 个单词。它适合处理以前从未见过的查询。表 9-16 指定了FreeTextLookupFactory
支持的参数。
表 9-16。
FreeTextLookupFactory Parameters
| 参数 | 描述 | | --- | --- | | `suggestFreeTextAnalyzerFieldType` | 指定用于分析的`fieldType`。此字段为必填字段。 | | `ngrams` | 指定构建字典时要考虑的最后标记的数量。默认值为 2。 |它是如何工作的
Solr 中的自动建议特性由SuggestComponent
提供,它与SolrSuggester
交互以生成建议。以下是组件遵循的步骤:
The client makes a request to a SearchHandler
, which has SuggestComponent
registered to it. The SuggestComponent
, like any other SearchComponent
, executes in two phases, namely prepare and process. If the received request is to build or reload the dictionary, in the prepare phase the component calls the SolrSuggester
to perform the task. SolrSuggester
is responsible for loading the lookup and dictionary implementations specified in the configuration. In the process phase, the component calls all the registered SolrSuggesters
for getting the suggested results. SolrSuggestor
calls the appropriate implementation, which looks up a key and returns the possible completion for this key. Depending on the implementation, this may be a prefix, misspelling, or even infix. The component converts the suggested results to an appropriate form and adds to the response.
使用
以下是在 Solr 中配置和使用SuggestComponent
的步骤:
Define the SuggestComponent
in solrconfig.xml
. Table 9-17 specifies the parameters supported by the component.
表 9-17。
SuggestComponent Parameters
| 参数 | 描述 | | --- | --- | | `name` | 指定用于生成建议的建议者的名称。 | | `dictionaryImpl` | 指定要使用的字典实现。默认情况下,`HighFrequencyDictionaryFactory`用于基于索引的数据结构,`FileDictionaryFactory`用于基于文件的数据结构。如果`sourceLocation`参数存在,组件假定使用基于文件的字典。 | | `lookupImpl` | 指定要使用的查找实现。如果未提供该参数,默认情况下将使用`JaspellLookupFactory`。 | | `field` | 对于基于索引的字典,此参数指定查找实现要使用的字段。 | | `sourceLocation` | 如果使用`FileDictionaryFactory`,该参数指定字典文件路径。 | | `storeDir` | 字典将被保存到的目录。 | | `buildOnStartup` | 将这个布尔参数设置为`true`会在 Solr 启动和内核重载时构建数据结构。 | | `buildOnCommit` | 将此布尔参数设置为`true`会在提交时构建数据结构。 | | `buildOnOptimize` | 将此布尔参数设置为`true`会在优化时构建数据结构。 |<searchComponent name="suggest" class="solr.SuggestComponent">
<lst name="suggester">
<str name="name">analyzedSuggestion</str>
<str name="lookupImpl">AnalyzingLookupFactory</str>
<str name="dictionaryImpl">DocumentDictionaryFactory</str>
<str name="field">brand</str>
<str name="weightField">popularity</str>
<str name="suggestAnalyzerFieldType">string</str>
<str name="buildOnStartup">false</str>
</lst>
</searchComponent>
Register the component to the desired SearchHandler
. Generally, if you like to have a dedicated endpoint for autosuggestion, add it to components
instead of last-components
. Table 9-18 specifies the parameters that can be configured in the handler or provided in the search request.
表 9-18。
SuggestComponent Parameters for the Request Handler
| 参数 | 描述 | | --- | --- | | `suggest` | 将该布尔参数设置为`true`会启用`SuggestComponent`。 | | `suggest.q` | 应该检索其建议的用户查询。如果未指定该参数,组件将在`q`参数中寻找一个值。 | | `suggest.dictionary` | 该参数是必需的。它指定用于生成建议的 suggester 组件的名称。 | | `suggest.count` | 指定要检索的建议的最大数量。 | | `suggest.build` | 将该参数设置为`true`构建建议者数据结构。构建操作的成本可能很高,因此可以根据需要触发该过程。表 9-17 提供了构建建议器的其他选项。 | | `suggest.buildAll` | 将该参数设置为`true`为组件中注册的所有建议者建立数据结构。 | | `suggest.reload` | 将该参数设置为`true`重新加载建议者数据结构。 | | `suggest.reloadAll` | 将该参数设置为`true`重新加载组件中注册的所有建议者的数据结构。 |<requestHandler name="/suggest" class="solr.SearchHandler" startup="lazy">
<lst name="defaults">
<str name="suggest">true</str>
<str name="suggest.count">10</str>
</lst>
<arr name="components">
<str>suggest</str>
</arr>
</requestHandler>
Define the fieldType
for text analysis. <fieldType class="solr.TextField" name="textSuggest"
positionIncrementGap="100">
<analyzer>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StandardFilterFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
Define the field. The field must be set as stored
. You might want to copy all the fields on which the suggestion should be generated to this field. Query for results. If you are using a dedicated handler for suggestions, you can set suggest=true
and other infrequently changing parameters in it, so that you don’t need to provide those parameters with each request. The following are the sample queries for generating suggestions. Request to build specific suggester data structures
$ curl "``http://localhost:8983/solr/hellosolr/suggest?suggest=true&suggest.buildAll=true
Request to build all the suggester data structures
$ curl "
http://localhost:8983/solr/hellosolr/suggest?suggest=true
&suggest.build=true&suggest.dictionary=analyzedSuggestion"
Request for suggestions in the analyzedSuggestion dictionary
$ curl "
http://localhost:8983/solr/hellosolr/suggest?suggest=true
&suggest.dictionary=analyzedSuggestion&wt=json&suggest.q=lapt"
文档相似度
开发搜索引擎的主要目的是检索与用户查询最相关的文档。一旦用户预览了检索到的文档,他很可能会对其他类似的文档感兴趣。如果应用程序是为搜索新闻、期刊和博客而开发的,那么建议相似的文档就变得更加重要。您可能对查找相似文档感兴趣的另一个领域是重复检测、剽窃和指纹识别。
Solr 提供了MoreLikeThis
特性来解决查找相似文档的问题。这个特性也可以用于构建基于内容的推荐系统,这基本上是一个机器学习问题。基于内容的推荐系统使用项目特征或关键字来描述项目,这在 Solr 字段中已经可用。现在,如果您维护一个用户档案或从用户的购买历史和“喜欢”中提取产品的共同属性,您可以使用这些共同的关键字或属性查询MoreLikeThis
来为用户生成推荐。
MoreLikeThis
采用词袋方法进行相似性检测。它接受一个文档或任意文本作为输入,并返回匹配的文档。它允许您通过配置定义和请求参数来控制匹配功能。
MoreLikeThis
中的相似文档检测是一个两阶段过程。以下是各阶段的处理细节:
- 检测感兴趣的关键字:该算法以统计方式确定文档中的重要和感兴趣的术语,以便检索相似的文档。它选择性地忽略非常常见、非常罕见、非常短或非常长的术语。如果 boost 使能,它们会添加到基于 TF-IDF 系数的项中。
- 匹配文档查询:第一阶段符合条件的术语作为搜索请求提交,检索最相关的文档。
先决条件
以下是使用MoreLikeThis
组件的先决条件:
- 应当存储
uniqueKey
字段。 - 该组件中使用的字段应存储术语向量(
termVectors="true"
)。以下是示例字段定义:<field name="product" type="text_general" indexed="true" stored="true" termVectors="true" />
- 如果术语向量被禁用,则组件从存储的字段生成术语。因此,至少应该存储该字段(
stored="true"
)。
Caution
MoreLikeThis
支持分布式搜索但是有一个公开的 bug https://issues.apache.org/jira/browse/SOLR-4414
。服务于请求的碎片应该包含文档;否则,MoreLikeThis
不会找到任何匹配项。
履行
MoreLikeThis
在 Solr 中有三种实现来满足不同的用户需求。实现如下所示:
MoreLikeThisComponent
:可以注册到任何SearchHandler
的组件列表中。如果您希望检索与主查询检索的每个文档相似的文档,这将非常有用。MoreLikeThisHandler
:这个处理程序允许您提供一个文档或内容流,并检索类似的文档。MLTQParserPlugin
:这个查询解析器通过使用文档 ID 和提供的其他统计信息形成一个MoreLikeThisQuery
。
通用参数
表 9-19 规定了适用于所有实现的通用参数。
表 9-19。
MoreLikeThis Generic Parameters
| 参数 | 描述 | | --- | --- | | `mlt.fl` | 指定应在其中识别感兴趣的术语以确定相似性的字段。该参数不适用于`MLTQParserPlugin`。 | | `mlt.qf` | 指定用于确定相似文档的查询字段。这些字段也必须在`mlt.fl`参数中指定。字段名后面可以跟一个字符运算符(`^`)和相应的 boost。 | | `boost` | 如果该参数设置为`true`,查询中感兴趣的术语将被提升。此参数不适用于`MLTQParserPlugin`。 | | `mlt.mintf` | 指定最小术语频率。计数小于指定值的术语将从感兴趣术语列表中忽略。默认值为 2。 | | `mlt.mindf` | 指定最小文档频率。出现在较少文档中的术语将被忽略。默认值为 5。 | | `mlt.maxdf` | 指定最大文档频率。文档中出现超过指定值的术语将被忽略。 | | `mlt.minwl` | 指定最小单词长度。长度较小的单词将被忽略。默认值为 0,编程为无效。 | | `mlt.maxwl` | 指定最大单词长度。超过这个长度的单词将被忽略。默认值为 0,编程为无效。 | | `mlt.maxqt` | 指定在形成查询的第二阶段要使用的最大术语数。默认值为 25。 | | `mlt.maxntp` | 如果禁用了术语 vector,此参数将指定为每个存储字段解析的最大令牌数。默认值为 5000。 |Note
在 Solr 5.3 之前,MLTQParserPlugin
不支持这些通用参数。在MLTQParserPlugin
中,参数不应以mlt.
为前缀。
MoreLikeThisComponent
如果将MoreLikeThisComponent
配置为任何处理程序,它将为主查询返回的每个文档返回相似的文档。该操作的执行成本很高,而且用户不太可能希望主查询的所有结果都是相似的文档。它适用于指纹识别和重复检测等场景,用于处理一批文档。表 9-20 表示通用参数。
表 9-20。
MoreLikeThisComponent Specific Parameters
| 参数 | 描述 | | --- | --- | | `mlt` | 如果设置为`true`,该布尔参数启用`MoreLikeThis`组件。默认情况下,它被设置为`false`。 | | `mlt.count` | 为主查询的每个结果指定要返回的相似文档的数量。默认值为 5。 |它是如何工作的
下面是MoreLikeThisComponent
处理请求所遵循的步骤:
The client requests a SearchHandler
that has MoreLikeThisComponent
registered to it. The handler invokes the MoreLikeThisComponent
. The MoreLikeThisComponent
does no processing in the prepare phase. In the process phase, all the matching documents retrieved by the main query are provided to MoreLikeThisHelper
for retrieving similar documents. This helper class creates a new Query
object by extracting the interesting terms from the documents and executes it on index to retrieving similar documents. This process is followed for each document retrieved by the main query and so is costly to execute. The retrieved documents are added to response and returned to the client. Note
如果你对内部处理的细节感兴趣或者正在计划定制,可以参考这些步骤;否则 Solr 不要求你了解他们。
用法
MoreLikeThisComponent
是所有SearchHandlers
的默认组件之一,因此不需要注册。以下是一个示例查询:
$ curl ’
http://localhost:8983/solr/hellosolr/select?q=apple
&mlt=true&mlt.fl=product&mlt.mindf=5&mlt.mintf=3&fl=product’
莫雷利克·蒂申德勒
MoreLikeThisHandler
是 Solr 为查找相似文档提供的专用处理程序。与MoreLikeThisComponent
不同,处理程序将返回与请求中指定的文档相似的文档。这是使用MoreLikeThis
的首选方法,可以在用户预览文档时调用。您可以通过提供查询或ContentStream
来找到类似的文档。该处理程序支持常用参数,如fq
、defType
和facets
。除了通用的MoreLikeThis
参数,表 9-21 提到了MoreLikeThisHandler
支持的附加参数。
表 9-21。
MoreLikeThisHandlerSpecific Parameters
| 参数 | 描述 | | --- | --- | | `mlt.match.include` | 此布尔参数指定响应中是否应该包含匹配的文档。默认值为`true`。它仅适用于使用查询查找相似文档的情况。 | | `mlt.match.offset` | 指定为响应主查询而返回的文档的偏移量,应该为该偏移量检索相似的文档。它仅适用于使用查询查找相似文档的情况。 | | `mlt.interestingTerms` | 指定如何在响应中返回感兴趣的术语。Solr 支持三种风格的有趣术语:`none`:禁用该特性。这是默认值。`list`:以列表形式显示术语`details`:显示术语及其提升。 |它是如何工作的
下面是MoreLikeThisHandler
处理请求所遵循的步骤:
MoreLikeThisHandler
accepts either a content stream or a query. If a query is provided, the handler parses it by using a defined query parser and forms a Solr Query
object. If a content stream is provided, it creates a Reader
object. Solr runs the query, if applicable, on the index and retrieves matching document IDs but considers only the first retrieved document for MoreLikeThis
matching. The document ID or the reader is provided to MoreLikeThisHelper
for retrieving similar documents. This helper class creates a new Query
object by extracting the interesting terms from the document or content stream and executes it on index to retrieve the matching documents. The retrieved documents are added to the response, as well as the interesting terms and facets if specified. The generated response is returned to the client. Note
如果你对内部处理的细节感兴趣或者正在计划定制,可以参考这些步骤;否则 Solr 不要求你了解他们。
使用
使用MoreLikeThis
处理程序很容易。以下是步骤:
Define MoreLikeThisHandler
in solrconfig.xml
. <requestHandler name="/mlt" class="solr.MoreLikeThisHandler" />
Query for MoreLikeThis
documents. Find documents similar to document with id APL_1001 $ curl "
http://localhost:8983/solr/mlt?q=id:APL_1001
&mlt.fl=product&mlt.mintf=2&mlt.mindf=5"
MLTQParserPlugin
MLTQParserPlugin
是查询解析器的工厂,它允许您检索与查询中指定的文档相似的文档。该条款允许您启用高亮显示和分页,可以在q
、fq
或bq
中提及。解析器期望文档的uniqueKey
作为一个值,不支持 Lucene 内部文档 ID 或任何任意查询。
从 Solr 5.3 开始,这个解析器支持除了fl
和boost
之外的所有通用参数。参数名不应以mlt
为前缀。
它是如何工作的
下面是 Solr 使用MLTQParserPlugin
解析查询的步骤:
The parser creates a query by using the document ID provided and retrieves the matching document. If document with the specified ID doesn’t exist, Solr will report an exception. For the retrieved document, Solr extracts the vector and term frequency of all the terms in the field list. Using the terms and frequency, it filters out unnecessary terms and creates a PriorityQueue
. Finally, it creates MoreLikeThisQuery
from terms in PriorityQueue
. Note
如果你对内部处理的细节感兴趣或者正在计划定制,可以参考这些步骤;否则 Solr 不要求你了解他们。
使用
MLTQParserPlugin
不需要solrconfig.xml
中的任何定义。UniqueKey
字段的值应该作为文档标识符提供。解析器的用法如下。必须指定至少一个qf
字段。以下是一个查询示例:
$ curl "``http://localhost:8983/solr/hellosolr/select
q={!mlt qf=product mintf=2 mindf=5 maxdf=100}APL_1001"
摘要
本章详细介绍了以下 Solr 特性:赞助搜索、拼写检查、自动完成和文档相似性。您了解了获得这些特性的方法,以及如何调整配置来定制和控制结果的行为。
在下一章中,您将学习各种扩展 Solr 的方法,包括 SolrCloud 模式。您将看到是什么使 Solr 成为高度可伸缩的分布式搜索引擎,以及如何在您的环境中设置它。
十、传统扩展和 SolrCloud
第二章介绍了如何设置 Solr 的独立实例、核心管理、内存管理和其他管理任务。接下来的章节讲述了索引、搜索和 Solr 的其他高级特性。Solr 的这个单一实例非常适合于概念验证、开发、特性测试和简单的产品部署。但是当涉及到任务关键型部署时,您需要考虑性能、可伸缩性、可用性和容错等方面。
在本章中,您将了解 Solr 支持的各种模型,以使您的系统为生产做好准备。你可以选择一个最适合你需要的。您将首先探索传统模型,然后关注 SolrCloud,这是 Solr 提供的共同运行服务器集群的终极模型。
本章涵盖以下主题:
- 独立实例
- 分片
- 主从架构
- 分片和主从的混合体
- 索尔鲁德
- 常见问题
独立模式
独立模式是最简单的 Solr 设置:一个 Solr 服务器(也称为实例)运行在一个特定的端口上,以满足您所有的搜索需求。在这种模式下,文档被索引到同一个实例并在其上进行查询。独立实例是开发活动的首选模式,在这种模式下,您需要频繁地进行更改、重新加载和重新启动。如果高可用性并不重要,并且数据量有限,那么您也可以在生产环境中以这种模式运行。
Solr 引入了多核的概念,这样每个核都可以有自己的索引和配置,而不是有一个单独的 Solr 设置,每个内核运行在不同的端口上,用于不同类型的索引。Solr 中的多核类似于拥有多个表的数据库。
拥有多个内核可以让您轻松管理多个索引。通过 Solr 管理界面,可以在运行时轻松加载、卸载、重命名、交换和优化内核。对于索引和搜索,URL 应该包含核心名称;这就是所有需要的改变。
前面的章节已经探讨了内核、它们的用法和core.properties
文件,所以我假设现在你已经理解了拥有多个内核而不是多个实例的好处。多核作为一项功能,并不局限于独立模式,可以在任何架构中设置。
图 10-1 展示了一个具有 N 个内核的独立 Solr 实例,该实例被请求用于索引和搜索。
图 10-1。
Stand-alone Solr instance with multiple cores
碎片
一旦开始索引越来越多的文档,索引大小就会增加,搜索性能也会受到影响。如果您的应用程序即将达到可接受的响应时间阈值,那么是时候进行分片了。
分片是将一个大的索引分成较小的分区的过程,这些分区在物理上分布在服务器上,但在逻辑上构成一个单元。索引被垂直划分并分布在 Solr 服务器网络上;每个单独的单元称为一个碎片。
假设您正在为一家制造商的文档编制索引,其中包含多个国家的销售点信息。如果索引很小,那么所有文档都可以放在一个 Solr 实例中。但是随着业务的增长,数据量也将增长,您可能希望将索引划分为碎片。分片策略将取决于您的需求和数据量。可以通过使用散列函数简单地对文档进行分片,或者分片可以基于定制的逻辑。图 10-2 展示了两种分片策略:一种是基于国家划分文档,如美国或英国;另一种是以地区为基础,如北美、欧洲、中东、非洲(EMEA)和亚太地区(APAC)。
图 10-2。
Concept of sharding
搜索应用程序应该知道 shard URLs,并可以根据需要查询全部或部分内容。这个请求应该发送给任何一个带有附加的shards
参数的实例,该参数指定了要查询的其他碎片的列表。接收请求的实例将查询分发给在shards
参数中提到的所有碎片,获取它们的结果,合并它们,并响应客户端。这种方法提供了更好的查询响应时间。
以下是使用多个碎片查询 Solr 的示例:
$ curl ’``http://localhost:8983/solr/core1/select?q=*:*&shards=localhost:8984/solr/core1,localhost:8985/solr/core1
图 10-3 描述了一个多共享架构,其中所有的请求都发送给在端口 8983 上运行的 Solr 实例,该实例将查询分配给其他 shards。值得注意的是,文档被分别索引到每个实例,而查询可以被发送到任何一个碎片进行分发。
图 10-3。
Sharding in Solr
以下是关于分片设计需要注意的一些要点:
- 模式应该包含一个
uniqueKey
。 - 这种方法只提供分布式查询,不支持分布式索引。索引应用程序应该将用于索引的文档分别发送到每个碎片。
- 逆文档频率是该碎片中的本地术语计数,而不是所有碎片的聚合值。如果术语分布是偏斜的,分数可能会受到影响。
- Solr 将请求分布在各个分片中,因此应该考虑增加线程池的大小。
主从架构
在主-从架构中,索引和搜索的过程在不同的机器之间是分离的。所有文档都被索引到主服务器,主服务器将索引复制到从实例,客户端将搜索请求发送到任何从实例。这种体系结构有明确的任务分离:索引在主服务器上完成,搜索请求发送给从服务器。每个从服务器都包含主服务器索引的冗余副本,因此客户端可以向任何一个从服务器发送请求。或者,您可以引入一个负载平衡器,它将向客户端提供一个 URL,并在循环的基础上将搜索请求重定向到一个从属服务器。负载平衡器通常能够检测实例的故障,并向实时服务器发送请求。这种方法有助于扩展 Solr 来处理大量请求,还支持高可用性和容错。
在这种体系结构中,索引应用程序很简单,其过程与独立实例上的索引相同。对于搜索请求,通常在客户端应用程序之前有一个负载平衡器,在从属实例之间路由请求。
图 10-4 描述了一个简单的主从架构,其中所有的索引请求都发送给主设备,而搜索请求则发送给从设备。此外,您可以看到一个负载平衡器位于应用程序的前面,用于路由请求。
图 10-4。
Master-slave architecture Note
主实例能够处理搜索请求,但是使用专用的从实例进行搜索是一种规范。
为了建立一个主从架构,Solr 要求您进行一些配置更改,以支持从主服务器到从服务器的索引复制。除了复制更改之外,您可能希望优化主实例以进行索引,优化从实例以进行搜索。
复制过程由ReplicationHandler
实现。为了让主从架构工作,这个处理程序应该在solrconfig.xml
中注册。在处理程序中,您需要指定服务器类型(主服务器或从服务器)并使用适当的设置来获得所需的行为。
需要注意的是,主服务器不知道从服务器。了解主人的是奴隶。从属实例配置有主实例的 URL,它们以指定的时间间隔轮询主实例的更新。
掌握
主服务器的复制处理程序应该包含一个名为master
的元素,从服务器应该包含一个名为slave
的元素。表 10-1 显示了主机支持的复制参数的详细信息。以下是主实例的配置示例。
表 10-1。
Master-Slave Architecture: Master Configuration
| 名字 | 描述 | | --- | --- | | `replicateAfter` | 此属性指定应在何时触发复制的事件。从节点轮询主节点,如果发生了任何指定的事件,将会触发复制。支持的事件有`startup`、`commit`、`optimize`,可以配置多个事件。 | | `confFiles` | 除了索引之外,还可以复制配置文件。此元素指定要复制的文件的逗号分隔列表。只能复制`conf`及其子目录中的文件。仅当主服务器有新的索引要复制时,才复制修改的文件(如果更新了配置文件,但没有修改任何文档,则不会触发复制)。 | | `backupAfter` | 此参数与复制无关,但与备份有关。它指定应在哪个事件之后备份索引。该主题在第二章中有详细介绍。 | | `maxNumberOfBackups` | 此属性指定要维护的备份数量。 | | `commitReserveDuration` | 此参数指定从主机向从机传输 5MB 数据的最大允许时间。 |<requestHandler name="/replication" class="solr.ReplicationHandler" >
<lst name="master">
<str name="replicateAfter">startup</str>
<str name="replicateAfter">commit</str>
<str name="backupAfter">optimize</str>
<str name="confFiles">schema.xml,stopwords.txt,elevate.xml</str>
</lst>
<int name="maxNumberOfBackups">2</int>
</requestHandler>
复制配置文件的规定使您免于在所有实例中应用更改的冗余工作。您应该在主实例中执行配置,复制会负责将其部署到从实例中。但是有一个挑战:使用这种架构,主实例和从实例的solrconfig.xml
文件应该是不同的,并且您不能将主实例的副本复制到从实例。为此,您可以在文件名后添加一个冒号(:
)分隔符,并指定复制到从属服务器时应该使用的名称。下面是一个将主实例中的solrconfig_slave.xml
复制到从实例中作为solrconfig.xml
的例子:
<str name="confFiles">solrconfig_slave.xml:solrconfig.xml,schema.xml</str>
要从conf
中的子目录复制文件,可以如下指定相对路径。conf
之外的文件不会被复制。
<str name="confFiles">schema.xml,velocity/template.html</str>
这样,您就可以设置主服务器了。
奴隶
从属实例应该知道它的主实例以及它应该轮询它的频率。表 10-2 显示了从站支持的复制参数的详细信息。以下是在从属实例中启用复制的配置:
表 10-2。
Master-Slave Architecture: Slave Configuration
| 名字 | 描述 | | --- | --- | | `masterUrl` | 指定主复制处理程序的 URL。 | | `pollInterval` | 指定从设备轮询其主设备以检查更新的时间间隔(以 HH:mm:ss 表示)。更高的间隔将延迟从设备中反映的变化。复制也可以通过复制 API 触发,在这种情况下,您可能希望禁用轮询。可以通过删除此参数来禁用轮询。 | | `httpConnTimeout` | 指定 HTTP 连接超时。 | | `httpReadTimeout` | 指定 HTTP 读取超时。 | | `httpBasicAuthUser` | 如果主服务器需要认证,可以在该参数中指定用户名。 | | `httpBasicAuthPassword` | 指定与用户名对应的密码。 |<requestHandler name="/replication" class="solr.ReplicationHandler">
<lst name="slave">
<str name="masterUrl">``http://localhost:8983/solr/core1/replication</str
<str name="pollInterval">00:00:30</str>
</lst>
</requestHandler>
主从碎片
分片允许 Solr 处理分布式请求,而主从架构有助于实现复制。如果您的应用程序需要这两者,您可以构建一个混合系统,其中每个碎片可以有一个主碎片用于索引,一组从碎片用于搜索。
图 10-5 显示了一个同时使用分片和主从模型的设计。该模型包含两个碎片,每个碎片都有自己的主实例和从实例。
图 10-5。
Hybrid of shards with master-slave architecture
这种方法是所有传统方法中最复杂的,但是它有一些限制。这种 Solr 扩展模型非常复杂,难以管理、监控和维护。
为了解决传统架构的局限性,引入了 SolrCloud。如果您计划实现混合方法,我建议您考虑 SolrCloud,这将在下一节中介绍。
索尔鲁德
在前面的小节中,您看到了扩展 Solr 并将其组合在一起的传统方法,这种方法有以下局限性:
- 索引过程不是分布式的,所有文档都应该被索引到主实例。主服务器可能是单点故障;如果主机停机,步进必须停止。
- 客户端应用程序需要包含一些逻辑,比如文档的路由和负载平衡。
- 通过复制过程很难实现实时更新,因为从实例会频繁地轮询主实例,并且索引会复制到从实例。
- 不支持反向文档频率。
SolrCloud 是在 4.0 版中引入的,比目前所有的模型都要先进一步。它解决了以前的限制,并提供了真正的分布式体系结构。
SolrCloud 提供了高度可用和可伸缩的解决方案,并支持负载平衡、自动路由、乐观并发、故障转移和健壮性。
SolrCloud 建立了一个服务器集群,其中的数据被分割成碎片并分布在多台机器上。它还维护多个碎片副本,以实现容错和高可用性,并且可以在集群中的任何碎片上索引和搜索文档。集群自动将文档分发到适当的碎片上进行索引,并同步副本。类似地,集群中的任何碎片都可以接受搜索请求进行处理。集群负责路由、合并和可用性。如果任何故障节点变为可用,群集会自动将其同步。客户是从所有的实现细节和分配和协调过程中抽象出来的。
您甚至可以在一个有数百台机器的农场上建立这种架构,并从 Solr 管理界面集中管理它。该模型甚至在监控和维护方面提供了很多便利,甚至可以在商用机器上运行。
理解术语
SolrCloud 引入了一些新的术语,以及一些大家可能很熟悉但需要从全新角度理解的术语。第二章简要介绍了一些术语,本节将进一步阐述。事不宜迟,我们开始吧。
结节
节点,通常称为服务器,是在机器上的特定端口上运行 Solr 的 Java 虚拟机实例。图 10-6 描绘了一个 SolrCloud 节点。
图 10-6。
A SolrCloud node
串
一组两个或更多的节点就是一个集群。集群将外部世界从实现细节中抽象出来,并使节点看起来像一个单元。可以在集群中添加或删除节点,以动态扩展或收缩集群,客户端不需要知道任何关于这种变化的信息。客户端只需向集群发出请求,如果得到确认,客户端就不需要担心其他任何事情。确保请求的原子性和进一步处理是集群的工作。
图 10-7 表示一个 SolrCloud 集群,它对索引和搜索应用程序来说是一个单独的单元。
图 10-7。
SolrCloud cluster
集群依靠 ZooKeeper 来协调和管理配置。动物园管理员将在本章后面讨论。
核心
在本书中,我们一直将核心称为单一索引。如果你想创建两种类型的索引,你需要创建两个核心。但是在 SolrCloud 中,情况有所不同。一个索引在物理上可以驻留在多个节点上,但在逻辑上可以分布在几台机器上。节点中的每个物理索引可以被视为一个核心,每个逻辑索引可以由多个核心组成。
Note
核心有多重含义。在独立的 Solr 实例中,一个核心对应一个逻辑/物理索引。在 SolrCloud 中,集合定义逻辑索引,而核心对应于驻留在节点中的物理索引,它是逻辑索引的一部分。
募捐
集合是簇中完整的逻辑索引。一个集群可以有多个集合(多种类型的索引),一个集合可以有多个分片,一个分片可以有多个副本,这些副本可以分布在集群中的多个节点上。
陶瓷或玻璃碎片
分片的概念对 SolrCloud 来说并不陌生。你也可以从传统的分布式体系结构方面了解到这一点。在独立方法中,所有文档都在同一台机器上编制索引。在这种方法中,索引分布在多个碎片中,这些碎片维护一部分逻辑索引的物理副本。用户查询被分发给所有的碎片进行处理,并最终在响应用户之前被其中一个碎片合并。
传统的分片方法要求客户端应用程序包含分片逻辑,但 SolrCloud 会自动处理。
图 10-8 显示了 Solr admin UI 中的云标签,它有一个包含两个碎片的hellocloud
集合。该设置包含一个在端口 8983 和 7574 上运行的双节点集群。这个示例在同一台机器上的不同端口上运行两个实例。在生产环境中,您可能希望在不同的机器上运行节点。
图 10-8。
Sharding in SolrCloud
复制品
副本是作为 Solr 核心在节点中运行的碎片的物理副本。假设一个碎片只有一个物理副本,如果它关闭了,Solr 将只能处理部分结果。为了实现高可用性,需要维护 shard 的冗余副本,每个副本称为一个副本。每个副本对应于 Solr 管理控制台中的一个核心。
在管理界面中,副本由连接碎片的圆圈表示。如果一个圆圈被标为绿色,它是活动的,可以接受请求。图 10-8 显示了一个例子,其中每个碎片有两个副本。
领导者
每个碎片的一个副本被选为领导者,负责协调索引和查询请求。领导者确保所有副本都得到更新并且同步。他们以先到先得的方式当选。如果主服务器关闭,其中一个可用副本将自动升级为主服务器。
图 10-9 显示了一个运行四个节点的典型 SolrCloud 集群。该集群代表两个碎片的集合,一个以名字 S1 开始,另一个以名字 S2 开始。每个以 S 开头的方框代表一个复制品。以 L 结尾的复制品是各自碎片的领导者,所有 S1(或 S2)盒子放在一起构成碎片。
图 10-9。
A typical SolrCloud cluster
与主从架构不同,在主从架构中,主机是在设置实例时配置的,领导者是动态选举的。群集中任何节点上的任何副本都可以被指定为领导者。
在 Solr 管理控制台中,领导者由实心空白圆圈表示。图 10-8 显示在端口 7574 上运行的节点上的副本是shard1
和shard2
的领导者。
启动 SolrCloud
在第二章中,你学会了在单机模式下设置 Solr,这与设置 SolrCloud 是不同的。在这里,您将首先了解如何启动 SolrCloud 实例,这显然是执行任何其他操作的先决条件。一旦启动了 SolrCloud 实例,就需要执行其他设置步骤。启动和设置 SolrCloud 最简单的方式是在交互模式下。如果您以前没有使用 SolrCloud 的经验,我建议您以这种模式启动 Solr,尝试一下它的特性和行为,一旦您对它感到满意,就可以使用标准方法启动 SolrCloud。
以下是设置 SolrCloud cluster 以索引和搜索文档时需要执行的一般步骤。
Start Solr in cloud mode Design the schema and create the required configurations files. Alternatively, you can use a named configset or choose to go schemaless. SolrCloud doesn’t reads the schema.xml and other configurations from conf
directory but relies on ZooKeeper for it. Solr bundles a ZooKeeper and by default starts it for you. Alternatively, you can start a separate ZooKeeper ensemble. Upload the configurations to the preferred ZooKeeper. Create a collection by specifying the ZooKeeper path and other required information.
对话方式
bin/solr
脚本允许您在本地工作站上快速启动一个小型节点集群,而不需要您执行前面列表中提到的步骤。交互过程自动在本地机器的不同端口上实例化多个节点,并设置碎片及其副本和配置集以创建集群。
如果你选择尝试无模式,设置 Solr 就像下载和启动一样简单,你就可以尽情摇滚了!
对于交互模式,按如下方式运行脚本:
$ bin/solr start -e cloud
这个脚本将为您提供一个以交互方式运行的 SolrCloud 实例。设置过程简单明了。即使您对所有选项都按 Enter 键,您也将建立一个名为gettingstarted
的双节点集群。
如果您想绕过交互步骤,从默认设置开始,运行带有-noprompt
选项的脚本:
$ bin/solr start -e cloud -noprompt
成功启动后,终端将打印以下消息:
"SolrCloud example is running, please visit``http://localhost:8983/solr
标准模式
在这种模式下,你需要自己完成所有的艰苦工作。首先,通过向solr
脚本传递一个附加参数来启动 SolrCloud 实例,如下所示:
$ bin/solr start -c -z <zkHost>
-c
:该参数指示 Solr 以云模式启动实例。或者,-cloud
也可以作为选项提供。-z
:默认情况下,SolrCloud 启动一个嵌入式 ZooKeeper 实例,其端口号比当前实例端口号多 1000。例如,如果您在端口 8983 上启动 Solr 节点,那么嵌入式 ZooKeeper 将在端口 9983 上启动。要指定一个外部 ZooKeeper 系综,可以使用-z
参数。
运行此命令后,SolrCloud 应该会成功启动。但是您仍然需要创建集合;按照“创建收藏”的步骤。完成后,您就可以开始添加文档并在集合中搜索结果了。
如果您已经设置了配置和$SOLR_HOME
,那么可以为solr
脚本提供-s
参数,该参数指向您的$SOLR_HOME
目录。下面是一个使用$SOLR_HOME
路径启动实例的例子:
$ bin/solr start -c -s "/home/dev/solr-5.3.1/server/solr"
假设您正在启动另一个节点,或者想要引用现有的 ZooKeeper 集合。这里有一个例子:
$ bin/solr start -c -s "/home/dev/solr-5.3.1/server/solr" -z localhost:9983 -p 8984
通过提供-help
参数,参考完整手册了解bin/solr
脚本中可用的启动选项:
$ bin/solr start -help
重新启动节点
重新启动节点的选项类似于启动节点的选项。此示例重新启动在端口 8983 上运行的节点:
$ bin/solr restart -c -p 8983 -s "/home/dev/solr-5.3.1/server/solr"
创建收藏
要创建集合,有几个先决条件。首先,应该定义模式和其他配置。您甚至可以使用命名的配置集。第二,集合中碎片的数量应该在创建集合时确定。虽然你可以把一个碎片一分为二,但是你不可能动态的扩展或者收缩一个碎片。我知道,很难预测语料库的大小,所以确定准确的碎片数量可能是不可能的。在这种情况下,您可以创建大约数量的碎片,然后在需要时进行分割。
SolrCloud 节点从 ZooKeeper 读取配置,因此集合使用的配置必须上传到 ZooKeeper。Solr 提供了一个将收藏目录上传到 ZooKeeper 的脚本。这将在下一节讨论。
以下是创建集合的示例命令:
$ bin/solr create -c hellocloud -d sample_techproducts_configs
-n hellocloud -shards 2 -replicationFactor 2
-c
指定收藏名称。-d
指定应该上传到 ZooKeeper 并使用的配置目录的路径或现有配置集的名称。-n
指定 ZooKeeper 中收藏的名称。如果未指定该参数,将考虑参数-c
的值。-shards
指定要创建的碎片数量。-replicationFactor
指定为碎片创建的副本数量。如果因子指定为 2,将创建一个引线和一个复本。
您也可以选择使用create_collection
选项。要查看可用于创建集合的所有选项,请按如下方式运行脚本:
$ bin/solr create_collection -help
SolrCloud 提供了用于管理集合的集合 API。如果您想使用 API 创建一个集合,可以按如下方式完成:
$ curl ’``http://localhost:8983/solr/admin/collections
action=CREATE&name=hellocloud&collection=hellocloud&numShards=2&replicationFactor=2’
使用集合 API,您不能指定配置集或配置目录。相反,它应该被预先加载到 ZooKeeper,并且它的名字应该在collection.configName
参数中提供。
Note
参考 Solr 官方参考指南,了解 Solr 在 https://cwiki.apache.org/confluence/display/solr/Collections+API
提供的一整套集合 API。
上传到动物园管理员
在前面的例子中,您看到 Solr 自动将配置上传到 ZooKeeper。如果想要手动将配置上传到 ZooKeeper,Solr 提供了 ZooKeeper 命令行接口(也称为 zkCLI)。zkCLI 脚本可以位于$SOLR_DIST/server/scripts/cloud-scripts
目录中。zkcli.sh
应该用在*nix 机器上,zkcli.bat
应该用在 Windows 机器上。
这个例子使用zkcli.sh
将配置上传到 ZooKeeper:
$ ./zkcli.sh -cmd upconfig -zkhost localhost:9983 -confname hellocloud
-solrhome /home/dev/solr-5.3.1/server/solr
-confdir /home/dev/solr-5.3.1/server/solr/configsets/hello_configs/conf
Note
关于 https://cwiki.apache.org/confluence/display/solr/Command+Line+Utilities
的完整命令集实用程序,请参考 Solr wiki。
删除收藏
可以使用如下的bin/solr
脚本从集群中删除集合:
$ bin/solr delete -c hellocloud -deleteConfig false -p 8983
-c
指定要删除的收藏的名称。- 指定 ZooKeeper 应该保留配置还是删除它。
-p
指定删除集合时要引用的本地实例上的端口。
除了使用bin/solr
脚本删除集合,您还可以使用集合 API 删除集合,如下所示:
$ curl ’``http://localhost:8983/solr/admin/collections?action=DELETE&name=hellocloud
为文件编制索引
SolrCloud 简化了客户端应用程序的索引过程。在传统的分布式体系结构中,客户端程序将所有文档索引到主实例,主实例被复制到从实例进行搜索。此外,客户端应用程序负责将文档路由到适当的碎片。SolrCloud 自动决定将文档发送到哪个碎片进行索引,而无需客户端担心主实例或路由到合适的碎片。
客户端程序将文档发送到集群中的任何碎片进行索引。如果接收请求的碎片是一个副本,它将文档发送给它的领导者进行处理。领导者识别文档应该被索引到的碎片,并将请求发送到该碎片的领导者。该领导者将文档附加到事务日志,对其进行索引,然后将其路由到其所有副本进行索引。如果更新在主服务器上成功,但在副本服务器上失败,客户端仍会得到成功状态的响应。当失败的副本恢复时,它们可以通过与主服务器同步来进行协调。
在传统的主从架构中,所有文档都在主服务器上建立索引,主服务器将索引段复制到从服务器上。从属实例中没有索引进程。相比之下,在 SolrCloud 中,领导者索引文档,然后将其发送给副本进行索引。领导者通常传输文档而不是段,并且索引在领导者和复制品中完成。
在 SolrCloud 中,通常避免从客户端应用程序触发提交,而应该依靠自动提交。如果有多个应用程序索引文档,并且您发现很难确保它们都不会触发提交,那么您可以将IgnoreCommitOptimizeUpdateProcessorFactory
注册到流程链,以禁止来自客户端应用程序的提交。以下是禁用提交的配置示例:
<updateRequestProcessorChain name="preprocessor">
<processor class="solr.IgnoreCommitOptimizeUpdateProcessorFactory">
<int name="statusCode">403</int>
<str name="responseMessage">External commit is disabled on this collection!</str>
</processor>
<processor class="solr.LogUpdateProcessorFactory" />
<processor class="solr.DistributedUpdateProcessorFactory" />
<processor class="solr.RunUpdateProcessorFactory" />
</updateRequestProcessorChain>
负载平衡
SolrCloud 会自动对请求进行负载平衡,您不需要像传统架构那样在前端安装负载平衡器。但是仍然建议使用负载平衡器或智能客户端向集群发出请求。假设您向同一个节点发出所有请求,该节点会将请求转发给其领导者,而该领导者又会确定应该处理请求的分片,然后将其转发给该分片的领导者。如果您的客户端应用程序本身知道请求应该发送到哪个碎片,就可以避免初始路由,从而避免网络不堪重负。像 SolrJ 这样的智能客户端提供了一个负载平衡器,它在循环的基础上分发请求。
如果你把所有的请求发送到同一个碎片,而那个碎片出现故障,所有的请求都会失败。为了解决这种单点故障,SolrJ 使您能够提供 ZooKeeper 的引用,而不是 Solr 节点。因为 ZooKeeper 总是知道集群的状态,所以它永远不会将请求定向到不可用的节点。为了避免这种单点故障,对 SolrJ 的请求应该配置如下:
import org.apache.solr.client.solrj.impl.CloudSolrServer;
import org.apache.solr.common.SolrInputDocument;
// Provide ZooKeeper address as argument
CloudSolrServer server = new CloudSolrServer("localhost:9983");
server.setDefaultCollection("hellocloud");
// Create document
SolrInputDocument document = new SolrInputDocument();
document.addField( "productId", "Mac152016");
document.addField( "name", "Apple MacBook Pro");
server.add(document);
// Let Solr autocommit
// server.commit();
文档路由
默认情况下,SolrCloud 使用哈希函数在碎片之间分发文档。如果您愿意,可以在创建集合时通过指定参数router.name=compositeId
来微调行为。该参数还要求指定参数numShards
,该参数定义了应该在其中分发文档的碎片数量。
如果使用了compositeId
路由器,那么文档的uniqueKey
应该以一个字符串为前缀,Solr 将使用这个字符串来生成一个散列,以标识文档应该被索引到的碎片。前缀后面应该跟一个感叹号(!
)以区别于uniqueKey
。假设原来的uniqueKey
是Mac152016
,并且您想要根据制造商来路由文档。在这种情况下,您可以在文档前面加上制造商名称,例如Apple!Mac152016
。
Solr 最多支持两级路由,中间用感叹号隔开。例如,如果您想要基于制造商和产品类别进行路由,那么文档 ID 应该修改为类似于Apple!Laptop!Mac152016
的内容。
如果使用compositeId
传送文件,那么在搜索时,应该在附加参数_router_
中提供前缀信息。这些信息将帮助 Solr 将查询路由到正确的分片,而不是发送到所有的分片。以下是查询路由的一个示例:
使用_router_
参数,一个示例查询将是q=mac&_router_=Apple
。
$ curl ’``http://localhost:8983/solr/hellocloud/select?q=*:*&_router_=Apple
使用事务日志
Solr 使用事务日志来降低数据丢失的风险。在 Solr 4.0 中引入,它是一个只追加的预写日志,记录每个片上的所有索引操作。在 SolrCloud 中,所有碎片,无论是主碎片还是副本碎片,都有自己的日志文件。
事务日志也称为tlog
,创建在data
目录内的tlog
目录中(与index
目录相邻)。图 10-10 显示了tlog
的目录结构。
图 10-10。
Solr transaction log directory
Solr 将所有文档附加到事务日志的末尾,并在硬提交时翻转。当请求是原子更新时,tlog
记录旧值和新值,这允许您回滚文档。如果在很长时间后执行硬提交,或者以很高的速度对文档进行索引,文件可能会变得非常大,恢复成本也会很高。
执行碎片运行状况检查
索引客户端可以发送表示最小复制因子的附加参数min_rf
,以了解碎片是否处于降级状态以及文档的复制因子是否低于预期阈值。如果您想采取任何预防措施,此信息可能会有所帮助。
查询结果
向 SolrCloud 发送查询几乎和往常一样简单,因为集群对客户端应用程序隐藏了所有实现细节。SolrCloud 处理搜索请求的步骤如下:
The client application sends a request to any node in the cluster. The receiving node determines the eligible shards and sends the query to any available replica of each of them. Each replica returns the matching documents to the receiver shard. The receiver shard merges the results and triggers another request to the replicas, for getting the stored values for the documents to be returned. The receiver responds to the client with the matching documents. Note
查询请求不会被路由到碎片领导者,而是可以由任何碎片复制品来处理。
建议使用智能客户端(如 SolrJ)或负载平衡器来查询集群。Solr 内置了对负载平衡器的支持,但是如果所有的查询都被发送到同一个节点,它将不得不完成所有的路由,并且可能会被请求淹没。反过来,负载平衡器会将查询分布到各个节点上,从而提供更好的性能。像 SolrJ 这样的智能客户端从 ZooKeeper 获取集群状态,并将请求重新发送到一个活动节点。
在完整索引上搜索结果的过程和以前一样,您可能甚至不知道您正在查询一个集群。如果您想要查询索引的一部分(一组指定的碎片),您所需要做的就是传递一个额外的参数shards
,就像传统的碎片架构一样。下面是它们各自的一个例子:
- 查询整个集合/完整索引。
$ curl ’``http://localhost:8983/solr/hellocloud/select?q=*:*
- 通过用逗号分隔的 URL 列表指定
shards
参数来查询一组特定的碎片。$ curl ’
http://localhost:8983/solr/hellocloud/select?q=*:*&shards=localhost:8983/solr
,localhost:7574/solr’
- 通过使用管道(
|
)操作符分隔碎片 URL,查询一组特定的碎片并在其副本之间进行负载平衡。$ curl ’
http://localhost:8983/solr/hellosolr/select?q
=*:*
&shards=localhost:8983/solr|localhost:8483/solr’
Note
像 curl 这样的简单客户端可以用于评估和开发,但是建议在生产中使用像 SolrJ 这样的智能客户端。
如果您想获得关于处理查询的碎片的信息,请将附加参数shards.info=true
传递给请求。
执行恢复
失败是常见的,尤其是当您运行大型集群时,在商用机器上也是如此。当故障节点恢复时,SolrCloud 通过自动与其他碎片同步来从这种故障中恢复。
当停机的非主节点恢复时,它与分片主节点同步,并且只有在它更新后才开始接受请求。如果要重播的文档数量很大,恢复过程可能会很长。
当一个领导者倒下时,Solr 指定另一个复制品作为领导者。当此故障节点变为可用时,它会与当前的主节点同步,以获取它可能会错过的更新。
值得注意的是,当副本从故障中恢复时,它从主服务器的tlog
中获取文档并重放它。但是,如果它发现自己落后太多,就像传统方法一样复制数据段。
碎片分裂
Solr Collections API 允许您将一个分片分成两个分区。现有碎片的文档被分成两部分,每一部分都被复制到一个新的碎片中。现有的碎片可以在以后方便的时候删除。
下面是一个将hellocloud
集合中的shard1
分割成两部分的例子:
$ curl ’
http://localhost:8983/solr/admin/collections?action=SPLITSHARD
&collection=hellocloud&shard=shard1’
添加副本
SolrCloud 提供了扩展和收缩集群的灵活性。假设您想要扩展系统以处理更多的查询,或者您有了新的硬件;在这种情况下,您可能希望向群集中添加一个节点,并为其分配副本。
您可以使用 Solr Collections API 向集合中添加副本。本例向集合hellocloud
中的碎片shard1
添加一个节点:
$ curl ’
http://localhost:8983/solr/admin/collections?action=ADDREPLICA
&collection=hellocloud&shard=shard1&node=192.10.8.120:7573_solr’
动物园管理员
Apache ZooKeeper 是一个成熟快速的开源服务器,广泛应用于分布式系统中,用于协调、同步和维护共享信息。它能够每秒处理数十万个事务,在包括 Hadoop、Spark 和 HBase 在内的分布式系统中非常流行。
ZooKeeper 在内存中维护信息,并构建一组服务器来维护这些配置的冗余副本,以实现快速性能和高可用性。主服务器被称为领导者,并有一组冗余的追随者。图 10-11 描绘了一个 ZooKeeper 系综以及它如何适应 SolrCloud 架构。
图 10-11。
ZooKeeper ensemble Note
更多详情请参考 Apache ZooKeeper 官方网站: https://zookeeper.apache.org/
。
在 SolrCloud 中,集群使用 ZooKeeper 来协调碎片并维护配置。概括地说,SolrCloud 将 ZooKeeper 用于以下用途:
- 配置管理:你在
core/conf
目录下的所有配置文件,包括solrconfig.xml
和schema.xml
,都在 ZooKeeper 中集中维护。所有配置的节点都引用 ZooKeeper。配置的任何变化都应该上传到 ZooKeeper。 - 集群协调:ZooKeeper 维护关于集群、其集合、活动节点和副本状态的信息,这些信息由节点监视以进行协调。
- 首领选举:每个碎片应该有一个首领,并且可以有多个副本。如果领导者倒下,复制品中的一个必须被选为领导者。动物园管理员在这个领袖选举过程中扮演着重要的角色。
Solr 与 ZooKeeper 捆绑在一起,这为评估、开发和初始测试提供了一个很好的起点。但是因为 ZooKeeper ensemble 需要超过一半的服务器,所以建议在生产环境中运行外部 ensemble。法定人数是一个组织中开展业务的最少成员的集合。— www。词汇。com
Note
关于在 https://cwiki.apache.org/confluence/display/solr/Setting+Up+an+External+ZooKeeper+Ensemble
建立外部动物园管理员合奏,请参考 Solr 的官方指南。
常见问题
本节涵盖了一些与 SolrCloud 相关的常见问题。
为什么我的数据/tlog 目录的大小急剧增长?我该怎么处理?
该目录包含原子更新和 SolrCloud 使用的事务日志。所有索引更新都记录在这些文件中,并在硬提交时被截断。如果您的系统高速索引,但硬提交很少执行,文件会变得很大。
您可以通过更频繁地执行自动提交来控制tlog
的大小。下面是您如何在solrconfig.xml
中配置它:
<autoCommit>
<maxTime>${solr.autoCommit.maxTime:30000}</maxTime>
<maxTime>${solr.autoCommit.maxDocs:100000}</maxTime>
<openSearcher>false</openSearcher>
</autoCommit>
我可以完全禁用事务日志吗?会有什么影响?
您可以通过注释掉solrconfig.xml
中的以下部分来禁用tlog
:
<updateLog>
<str name="dir">${solr.ulog.dir:}</str>
<int name="numVersionBuckets">${solr.ulog.numVersionBuckets:65536}</int>
</updateLog>
强烈建议不要禁用事务日志。SolrCloud 的高可用性,以及原子更新和近实时搜索等功能都依赖于tlog
,禁用它将影响对这些功能的支持。
我最近从传统架构迁移到了 SolrCloud。在 SolrCloud 中,有什么我应该小心和不要做的事情吗?
有了 SolrCloud,Solr 中的许多东西都发生了变化,通读本章会让您对这些变化有一个大致的了解。以下是你应该注意的一些关键事项:
- 您应该避免从客户端程序触发提交,而是使用 Solr 的
autocommit
和softCommit
特性。 - 总是优雅地停止节点。如果你一直在做
kill -9
,停止它。 - 避免搜索所有碎片。
我正在迁移到 SolrCloud,但是它无法将配置上传到 ZooKeeper。原因可能是什么?
ZooKeeper 将配置的大小限制在 1MB 以内。如果您的配置集太大,ZooKeeper 将无法上传。由于巨大的同义词文件或集成分类法的定制插件,conf
目录的大小通常很大。您需要一个更好的数据源来解决这个限制。
摘要
在本章中,您了解了 Solr 为可伸缩性提供的各种传统方法。根据您的需求,您可以选择这些方法之一或它们的组合。您还了解了这些方法的局限性以及引入 SolrCloud 的原因。
本章还介绍了 SolrCloud 的突出特性及其基本概念,以及这些特性如何革新 Solr 中的分布式伸缩。您还了解了动物园管理员的概况。
十一、语义搜索
您已经读到了本书的最后一章,在这一过程中,您已经了解了 Solr 的重要特性以及使用它的基本原理。在前面的章节中,您还了解了信息检索概念和相关性排名,这对于理解 Solr 的内部原理以及评分的方式和原因是必不可少的。这些知识对于您大部分时间要做的事情是不可或缺的:调整文档相关性。有了所有这些信息,您应该能够开发一个有效的搜索引擎来检索与查询相关的文档,对它们进行适当的排序,并提供增加用户体验的其他功能。
到目前为止,一切顺利,但用户期望更多。如果你看看市场上的一些搜索应用程序,它们正在实现许多创新功能,并插入不同的框架和组件,以将搜索体验提升到一个新的水平。由于期望值很高,所以需要超越关键词的匹配来理解底层语义和用户意图。例如,谷歌也像一个问答系统。它运用巨大的智能来理解查询的语义,并相应地采取行动。如果您分析您的查询日志,您会发现相当多的查询包含用户的意图,而不仅仅是关键字,尽管这取决于领域。
此时,您应该能够开发一个不错的基于关键字的搜索引擎,但是有一些限制。当你把所有这些放在一起时,系统不会理解语义。例如,如果您的应用程序面向医疗领域,那么用户查询心脏病发作将无法检索到心脏骤停的结果,而这可能是医疗从业者更感兴趣的。
语义搜索通过理解用户意图和术语的上下文含义来解决基于关键字的搜索的局限性。可以以多种方式利用所获得的知识来提高搜索准确度和相关性排名。
语义搜索是一个高级而广泛的话题,介绍它的文本分析技术需要一本专门的书。在本章中,您将了解一些最简单形式的技术,以及可用于进一步探索的参考资料。本章涵盖以下主题:
- 基于关键字的系统的局限性
- 语义搜索简介
- 构建语义功能的常用工具
- 将语义能力集成到 Solr 的技术
- 识别标记的词性
- 从非结构化数据中提取命名实体,如人员、组织和位置
- 语义丰富
关键词系统的局限性
基于关键字的文档排序基本上依赖于关于查询术语的统计信息。尽管它们对于许多用例是有益的,但是如果用户未能制定适当的查询或提供意图查询,它们通常不会为用户提供有价值的结果。如果用户在搜索时多次改写同一个查询,这意味着需要扩展搜索引擎以支持高级文本处理和语义功能。计算机科学家汉斯·彼得·鲁恩描述了关键字系统的局限性:
This fairly simple argument about “importance” avoids linguistic meanings such as grammar and syntax … and pays no attention to the logical and semantic relations established by the author.
基于关键字的系统的主要限制可以分类如下:
- 上下文和意图:基于关键字的系统不知道上下文,也不考虑文档的认知特征。对白色正式衬衫的查询表明用户意图,但是关键字系统可能检索到与用户期望无关的文档。类似地,在音乐搜索引擎中,对今年热门歌曲的查询是纯粹的意图查询,基于关键字的系统可能最终检索到包含这些标记的专辑或标题,这与用户正在寻找的内容无关。
- 重要术语:基于关键字的搜索引擎在统计信息的基础上确定术语的重要性和意义,而忽略底层语义。
- 同义词:基于关键字的引擎检索包含匹配标记的文档,但忽略语言学上指代同一事物的术语。这些被系统视为不相关的被忽略的标记实际上可能更相关,如在前面的心脏病发作的例子中。
- 你可能会争论
synonyms.txt
解决了同义词的问题,但是这有两个限制。首先,该文件应该手动创建,并且仅限于手工制作的同义词。其次,它忽略了语义和一个词的同义词可以根据上下文而不同的事实。 - 多义性:在英语中,单词是多义的(一个单词可以有不同的意思)。
synonyms.txt
中定义的同义词对于这样的单词可能会出错。例如,如果在synonyms.txt
文件中将心脏映射到情绪,那么查询心脏病发作将扩展到情绪发作,而实际上它应该扩展到心脏停搏。 - 非结构化数据:非结构化数据基本上是供人们使用的,其中隐藏着大量的信息。基于关键字的系统未能充分利用这些非结构化数据中的可用知识。
语义搜索
语义搜索是指一套解释意图、上下文、概念、意义和术语之间关系的技术。这个想法是开发一个遵循认知过程的系统,以类似于我们人类的方式理解术语。Technopedia.com 给出了语义搜索的定义:
Semantic search is a data search technology. In this technology, the purpose of search query is not only to find keywords, but also to determine the intention and contextual meaning of words used by a person to search.
在搜索应用程序中利用语义功能的潜力是无限的。您利用它们的方式在很大程度上取决于您的领域、数据和搜索需求。例如,谷歌使用语义功能来传递答案,而不仅仅是链接。图 11-1 展示了 Google 语义能力的一个例子,它精确理解用户意图并回答查询。
图 11-1。
An example of the question-answering capability of Google
从历史上看,搜索引擎是为满足基于关键字的查询而开发的,但由于其局限性,这些引擎也提供了高级搜索功能。这在某些垂直领域是可以接受的(例如,法律搜索),但是很少有用户觉得这有吸引力。用户更喜欢单个盒子,因为它易于使用和简单。由于搜索框是开放式的(你可以键入任何你想要的),用户用自然语言提供查询,使用日常生活中的语言学。早期谷歌的主页上有高级搜索选项,但在 2011 年被隐藏了。谷歌的高级搜索现在可以在它的主页设置中找到,需要额外点击,或者你需要去 www.google.com/advanced_search
。在高级搜索中,您可以提供有关查询术语及其适用字段的详细信息。它通常被图书管理员、律师和医疗从业者所偏爱。
图 11-2 显示了一个智能搜索的例子,由 Amazon.com 为用户查询 white formal shirt for men 执行。搜索引擎通过使用内置的语义功能找到您想要的东西。
图 11-2。
An example of query intent mining in Amazon.com
语义搜索技术通过使用人工智能、自然语言处理和机器学习等技术来对文本进行深度分析。在这一章中,你将学习其中的一些,以及如何将它们集成到 Solr 中。
语义功能可以在索引和搜索时集成到 Solr 中。如果您正在处理新闻、文章、博客、日志或电子邮件,这些数据将是非结构化或半结构化的。在这种情况下,您应该从文本流中提取元数据和可操作的信息。由于非结构化数据是为人类消费而创建的,因此机器可能很难解释,但通过使用自然语言处理等文本处理功能,可以提取有用的信息。
语义处理主要取决于以下几点:
- 知识库:语义知识源包含关于与术语和概念相关的实体或事实的信息。知识可以作为本体、分类法、辞典、其他受控词汇、训练模型甚至一组规则来获得。这些知识可以由第三方供应商提供、由社区众包或内部开发。
- 文本处理:这是指应用于知识的处理、推理规则和推理,以检测实体、建立逻辑连接或确定上下文。
工具
本节介绍了一些工具和技术,您可能希望在处理文本以丰富语义时对它们进行评估。您可以扩展 Solr 组件来插入适合您的文本处理需求的工具。在继续本节内容之前,请参考第三章的“文本处理”部分复习这些概念。
OpenNLP
Apache OpenNLP 项目提供了一套处理自然语言文本的工具,用于执行常见的 NLP 任务,如句子检测、标记化、词性标记和命名实体提取等。OpenNLP 为这些任务中的每一项提供了单独的组件。这些组件可以单独使用,也可以组合起来形成一个文本分析管道。该图书馆使用最大熵和感知器等机器学习技术来训练模型,并建立高级文本处理能力。OpenNLP 发布了一组通用模型,这些模型在一般用例中表现良好。如果您想要为您的特定需求构建一个定制模型,它的组件提供了一个用于训练和评估模型的 API。
该项目获得了 Apache 软件许可证的许可,可以在 https://opennlp.apache.org/
下载。其他 NLP 库也是可用的,比如斯坦福 NLP,但是它们要么不是开源的,要么需要类似 GPL 的许可,这可能不符合许多公司的许可要求。
OpenNLP 的免费模型可以从 http://opennlp.sourceforge.net/models-1.5/
下载。
OpenNLP 集成尚未在 Solr 中提交,也不能作为开箱即用的特性。更多详情请参考 https://wiki.apache.org/solr/OpenNLP
的 Solr wiki。在本章的后面,你会看到将 OpenNLP 与 Solr 集成的例子。
Note
有关整合的更多详情,请参考 JIRA https://issues.apache.org/jira/browse/LUCENE-2899
。
Apache 的游泳
如您所知,UIMA 代表非结构化信息管理架构,这是一个 Apache 项目,允许您开发可互操作的复杂组件,并将它们组合在一起运行。这个框架允许您开发一个分析引擎,可以用来从非结构化的文本中提取元数据和信息。
分析引擎允许您开发一个管道,您可以用它来链接注释器。每个注释器代表一个独立的组件或特性。注释器可以消费和产生一个注释,一个注释的输出可以输入到链中的下一个。该链可以通过使用 XML 配置来形成。
UIMA 的可插拔架构、可重复使用的组件和可配置的管道允许您扔掉整体结构,设计一个多阶段的过程,其中不同的模块需要建立在彼此的基础上,以获得一个强大的分析链。这也允许您向外扩展并异步运行组件。你可能会发现这个框架有点复杂;它有一个学习曲线。
来自不同供应商的注释可供使用,可以添加到管道中。Open Calias 和 AlchemyAPI 等供应商为文本处理提供了各种注释,但需要许可证。
UIMA 的 Solr 集成作为 contrib 模块提供,Solr 的丰富可以通过少量的配置更改来完成。关于 UIMA 集成,请参考位于 https://cwiki.apache.org/confluence/display/solr/UIMA+Integration
的 Solr 官方文档。
阿帕哈奇·斯坦布尔
Apache Stanbol 是一个基于 OSGi 的框架,它提供了一组用于推理和内容增强的可重用组件。它提供的额外好处是内置的 CMS 功能和供应,以持久化语义信息,如实体和事实,并定义知识模型。
没有可用的 Solr 插件,这个框架也不需要任何插件,因为 Stanbol 内部使用 Solr 作为文档存储库。它还使用 Apache OpenNLP 进行自然语言处理,并使用 Apache Clerezza 和 Apache Jena 作为 RDF 和存储框架。Stanbol 提供了一个管理链的 GUI,并提供了额外的功能,如网络服务器和安全功能。
如果您正在从头开始开发一个具有语义功能的系统,您可能想要评估这个框架,因为它为您提供了完整的套件。
应用的技术
语义搜索已经成为一个活跃的研究领域很长一段时间了,仍然是一个没有解决的问题,但在这个领域已经取得了很多进展。典型的例子是 IBM 的沃森;这个智能系统能够用自然语言回答问题,赢得了 2011 年的危险挑战。构建语义能力可能是一项相当复杂的任务,这取决于您想要实现什么。但是您可以使用简单的技术来提高结果质量,有时一点点语义就能帮您走很长的路。
图 11-3 概述了语义技术如何与不同的知识库相结合来处理输入文本和构建智能。从这些知识库中获得的信息可用于扩展术语、指示概念之间的关系、介绍事实以及从输入文本中提取元数据。第三章概述了这些知识库。在本节中,您将看到如何利用这些知识来执行智能搜索。
图 11-3。
Semantic techniques
在本书的前面,您已经了解到 Solr 通过使用向量空间模型等模型来对文档进行排序。该模型将文档视为一个单词包。对于用户查询,它基于诸如术语频率和逆文档频率之类的因素来检索文档,但是它不理解术语之间的关系。诸如此类的语义技术可以在 Solr 中应用,以检索更相关的结果:
- 查询解析:语义技术可以应用于查询以挖掘用户意图。基于领域,可以通过开发文本分类系统或其他技术来挖掘用户意图。这个特性可以通过编写一个定制的查询解析器插入 Solr。基于对意图的理解,解析器可以重构查询,或者用同义词和其他相关概念扩展查询术语。重新制定可能是一项艰巨的任务,应该小心进行。
- 文本分析:在执行文本分析时,可以用语义相关的单词来丰富标记,这与使用同义词过滤器工厂的方式类似。您可以通过编写一个定制的令牌过滤器来插入丰富内容,该过滤器可以在索引文档的字段时应用,也可以在查询结果时应用。本章稍后将介绍自动扩展同义词的示例实现。
- 查询重新排序:意图挖掘甚至可以通过编写定制的重新排序查询来应用,您在第七章的中了解到了这一点,用于改变检索到的文档的顺序。与定制查询解析器的情况不同,查询重排序不会引入全新的文档。
- 索引文档:结构化内容比非结构化内容更有价值。如果您正在索引非结构化数据,如书籍或期刊的文本,您应该对其进行结构化。您可以应用多种技术从这些内容中提取实体和隐藏的事实,并自动生成元数据。包含这些提取的实体的字段可以基于其重要性被提升,可以用于生成方面和控制可以表示结果的方式。在本章的后面,您将学习编写一个定制的更新请求处理器,用于从非结构化内容中自动提取元数据。
- 排名模型:用于对文档评分的排名模型可以进行调整,以考虑术语的语义关系,但这不是一项简单的任务。我建议您考虑其他有助于文档排名的方法,通过使用现有的模型,例如通过应用提升或有效载荷。
图 11-4 描述了语义能力如何应用于 Solr 以提高相关性。
图 11-4。
Application of semantic techniques in Solr
接下来,您将看到各种自然语言处理和语义技术,它们可以集成到 Solr 中以提高结果的精确度。
词性标注
句子中的每个单词都可以被归类到一个词汇类别中,也称为词类。常见的词类包括名词、动词和形容词。这些可以进一步分类,例如,名词可以分为普通名词或专有名词。这种分类和次分类信息可以用来发现术语在上下文中的意义,甚至可以用来提取大量关于单词的有趣信息。我建议你对词类有一个公平的理解,因为这可能有助于你理解单词的重要性和用途。例如,名词用于标识人、地点和事物(例如,衬衫或乔),形容词定义一个名词的属性(例如红色或智能)。类似地,子类如普通名词描述一类实体(如国家或动物),专有名词描述实例(如 America 或 Joe)。图 11-5 提供了示例文本及其词性。在图 11-5 中,标签 NNP、VBD、VBN 和 In 分别指专有名词(单数)、动词(过去式)、动词(过去分词)和连词(介词或从属关系)。
图 11-5。
Part-of-speech tagging
有了对词类的理解,你就能清楚地认识到并非所有的词都同等重要。如果您的系统可以标记词类,这种知识可以用于根据上下文中标记的重要性来控制文档排名。目前,在索引文档时,您要么提升一个文档,要么提升一个字段,但是忽略了这样一个事实,即每个术语可能也需要不同的提升。在一个句子中,通常名词和动词更重要;您可以提取这些术语,并将其索引到不同的字段。这为您提供了一个包含更小且更集中的术语集的字段,在查询时可以为其分配更高的提升。Solr 特性如MoreLikeThis
在这个领域使用更重要的标记会更好。您甚至可以在索引时对它们应用有效负载。
词性标注可能是许多类型的高级分析的必要功能和先决条件。语义丰富的例子,你将在本章后面看到,需要词性标记的单词,因为一个单词的含义和定义可能因其词性而异。词性标注器使用宾夕法尼亚树库项目中的标签来标注句子中单词的词性。
用于 POS 标记的 Solr 插件
在本节中,您将学习从被索引的文档中提取重要的词类,并将提取的术语填充到一个单独的 Solr 字段中。这个过程需要两个主要步骤:
Extract the part of speech from the text. In the provided sample, you’ll use OpenNLP and the trained models freely available on its web site. The model can be downloaded from http://opennlp.sourceforge.net/models-1.5/
. Write a custom update request processor, which will create a new field and add the extracted terms to it.
接下来详细提供将期望的词性添加到单独的 Solr 字段所遵循的步骤。
Write a Java class to tag the part of speech by using OpenNLP. The steps for part of-speech tagging in OpenNLP are provided next. These steps doesn’t relate to Solr but is called by the plugin for getting the POS. Read the trained model using FileInputStream
and instantiate the POSModel
with it. The path of model is passed to the InputStream
during instantiation. OpenNLP prebundles two POS models for English and a few other languages. You can use en-pos-maxent.bin
, a model which is based on maximum entropy framework. You can read more details about maximum entropy at maxent.sourceforge.net/about.html
. InputStream modelIn = new FileInputStream(fileName);
POSModel model = new POSModel(modelIn);
Instantiate the POSTaggerME
class by providing the POSModel
instance to its constructor. POSTaggerME tagger = new POSTaggerME(model);
POSTaggerME
as a prerequisite requires the sentence to be tokenized, which can be done using OpenNLP tokenization. If this code had been part of an analysis chain, the sentence could be tokenized using Solr provided tokenizer. The simplest form of tokenization can even be built using Java String’s split()
method, also used in this example, though it’s prone to creating invalid tokens. String [] tokens = query.split(" ");
Pass the tokenized sentence to the POSTaggerME
instance, which returns a string array containing all the tagged parts of speech. String [] tags = tagger.tag(tokens);
Iterate over the array to map the tags to the corresponding tokens. You have populated the extracted tags to the PartOfSpeech
bean that holds the token and its corresponding part of speech. int i = 0;
List<PartOfSpeech> posList = new ArrayList<>();
for(String token : tokens) {
PartOfSpeech pos = new PartOfSpeech();
pos.setToken(token);
pos.setPos(tags[i]);
posList.add(pos);
i++;
}
Write a custom implementation of UpdateRequestProcessor
and its factory method. Follow the next steps to add the terms with specified parts of speech to a separate field. (Refer to Chapter 5 if you want to refresh your memory about writing a custom update request processor.) Read the parameters from NamedList
in the init()
method of POSUpdateProcessorFactory
, the custom factory, to populate the instance variables for controlling the tagging behavior. Also, set up the POS tagger that can be used for extraction, as mentioned in step 1. private String modelFile;
private String src;
private String dest;
private float boost;
private List<String> allowedPOS;
private PartOfSpeechTagger tagger;
public void init(NamedList args) {
super.init(args);
SolrParams param = SolrParams.toSolrParams(args);
modelFile = param.get("modelFile");
src = param.get("src");
dest = param.get("dest");
boost = param.getFloat("boost", 1.0f);
String posStr = param.get("pos","nnp,nn,nns");
if (null != posStr) {
allowedPOS = Arrays.asList(posStr.split(","));
}
tagger = new PartOfSpeechTagger();
tagger.setup(modelFile);
};
In the processAdd()
method of POSUpdateProcessor
, a custom update request processor, read the field value of the document being indexed and provide it to the tagger object for tagging the parts of speech. Create a new field and add the important tag to it. @Override
public void processAdd(AddUpdateCommand cmd) throws IOException {
SolrInputDocument doc = cmd.getSolrInputDocument();
Object obj = doc.getFieldValue(src);
StringBuilder tokens = new StringBuilder();
if (null != obj && obj instanceof String) {
List<PartOfSpeech> posList = tagger.tag((String) obj);
for(PartOfSpeech pos : posList) {
if (allowedPOS.contains(pos.getPos().toLowerCase())) {
tokens.append(pos.getToken()).append(" ");
}
}
doc.addField(dest, tokens.toString(), boost);
}
// pass it up the chain
super.processAdd(cmd);
}
Add the dependencies to the Solr core library. <lib dir="dir-containing-the-jar" regex=" solr-practical-approach-\d.*\.jar" />
Define the preceding custom processor in solrconfig.xml
. In the parameters, you need to specify the path of the model, the source field, the destination field, the parts of speech to be extracted, and the boost to be provided to the destination field. <updateRequestProcessorChain name="nlp">
<processor class="com.apress.solr.pa.chapter``11``.opennlp.POSUpdateProcessorFactory">
<str name="modelFile">path-to-en-pos-maxent.bin</str>
<str name="src">description</str>
<str name="dest">keywords</str>
<str name="pos">nnp,nn,nns</str>
<float name="boost">1.4</float>
</processor>
<processor class="solr.LogUpdateProcessorFactory" />
<processor class="solr.RunUpdateProcessorFactory" />
</updateRequestProcessorChain>
Register the defined update chain to the /update
handler. <str name="update.chain">nlp</str>
Restart the instance and index documents. The terms with the specified part of speech will be automatically added to the keywords
field, which is defined as a destination field in solrconfig.xml
. The following is an example of the resultant document. {
"id": "1201",
"description": "Bob Marley was born in Jamaica",
"keywords": "Bob Marley Jamaica "
}
命名实体提取
如果您的搜索引擎需要索引非结构化内容,如书籍、期刊或博客,一项至关重要的任务是提取隐藏在文本流中的重要信息。在本节中,您将了解提取这些信息的不同方法,并利用它们来提高精确度和整体搜索体验。
非结构化内容主要是供人们消费的,提取隐藏在其中的重要实体和元数据需要复杂的处理。这些实体可以是通用信息(例如,人员、位置、组织、资金或时间信息)或特定于某个领域的信息(例如,医疗保健中的疾病或解剖)。识别诸如个人、组织和位置等实体的任务被称为命名实体识别(NER)。例如,在文本“鲍勃·马利出生在牙买加”中,NER 应该能够检测出鲍勃·马利是人而牙买加是地点。图 11-6 显示了本例中提取的实体。
图 11-6。
Named entities extracted from content
提取的命名实体可以在 Solr 中以多种方式使用,实际的用法取决于您的需求。Solr 中的一些常见用法如下:
- 使用实体支持分面导航
- 对实体的结果进行排序
- 为自动建议词典查找实体,如人名
- 为实体分配不同的提升,以改变它们在域中的重要性
- 基于检测到的实体类型搜索有限的字段集的查询重构。
NER 的方法可以分为三类,在下面的小节中详细介绍。该方法及其实现取决于您的需求、用例以及您试图提取的实体。前两种方法可以通过使用 Solr 或任何高级 NLP 库的现成特性来实现。对于第三种方法,您将定制 Solr 来集成 OpenNLP 并提取命名实体。定制或提取可以根据您的需要而有所不同。
使用规则和正则表达式
对于 NER 来说,规则和正则表达式是最简单的方法。您可以定义一组规则和一个正则表达式模式,它与实体提取的传入文本相匹配。这种方法适用于提取遵循预定义模式的实体,例如电子邮件 id、URL、电话号码、邮政编码和信用卡号。
在分析链中使用PatternReplaceCharFilterFactory
或PatternReplaceTokenFilterFactory
可以集成一个简单的正则表达式。用于确定电话号码的正则表达式可以像这样简单:
^[0-9+\(\)#\.\s\/ext-]+$
为了提取电子邮件 id,可以使用 Lucene 提供的UAX29URLEmailTokenizerFactory
。参见 http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.UAX29URLEmailTokenizerFactory
了解记号赋予器的详细信息。您可能会觉得有趣的是,有一个针对电子邮件的官方标准正则表达式,称为 RFC 5322 。详见 http://tools.ietf.org/html/rfc5322#section-3.4
。它描述了有效的电子邮件地址必须遵守的语法,但是实现起来太复杂了。
如果您正在寻找复杂的正则表达式规则,您可以评估 Apache UIMA 提供的正则表达式注释器,在这里您可以定义规则集。关于注释器的详细信息,请参考 https://uima.apache.org/downloads/sandbox/RegexAnnotatorUserGuide/RegexAnnotatorUserGuide.html
。如果您正在使用 Drools 之类的规则引擎,您可以通过编写自定义更新处理器或过滤器工厂将其集成到 Solr 中。
如果您想将 OpenNLP 用于基于正则表达式的 NER,您可以使用RegexNameFinder
并指定模式,而不是使用NameFinderME
。参考“使用训练好的模型”一节中的例子,您可以使用RegexNameFinder
进行替换。
这种 NER 方法的局限性在于,遵循指定模式或满足规则的任何不相关的事物都将被检测为有效实体;例如,格式错误的五位数薪水可能会被检测为邮政编码。此外,这种方法仅限于遵循已知模式的实体类型。它不能用于检测名称或组织等实体。
使用字典或地名词典
实体提取的基于词典的方法,也称为基于地名词典的方法,维护适用类别的术语列表。输入文本在地名词典上进行匹配以提取实体。这种方法适用于适用于特定领域且术语有限的实体。典型的例子是你组织中的职位、国籍、宗教、一周中的几天或一年中的几个月。
您可以通过从本地数据源或外部来源(如维基百科)提取信息来构建列表。维护字典的数据结构可以是满足您需求的任何数据结构。它可以像从文本文件填充的 Java 集合一样简单。基于文件的方法在单独的字段中填充实体的一个更简单的实现是使用 Solr 的KeepWordFilterFactory
。以下是对它的一个样本文本分析:
<analyzer>
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.KeepWordFilterFactory" words="keepwords.txt"/>
</analyzer>
该列表可以在数据库表中维护,但是大的列表可能会降低性能。一种更快、更高效的方法是构建一个自动机。可以参考 Solr 建议模块中的 FST 数据结构或者大卫·斯迈利在 https://github.com/OpenSextant/SolrTextTagger
提供的数据结构。
如果您想查找短语和单个术语,可以使用 OpenNLP Chunker 组件或 Solr 提供的ShingleFilterFactory
来处理输入的文本。下面是用于生成不同大小的瓦片区的过滤器定义。已经提供了参数outputUnigrams="true"
来匹配单个令牌。
<filter class="solr.ShingleFilterFactory" maxShingleSize="3" outputUnigrams="true"/>
这种方法的好处是不需要培训,但是不太受欢迎,因为很难维护。它不能用于常见的实体,如名称或组织,因为这些术语可能有歧义,并且不限于一组定义的值。这种方法也忽略了上下文。例如,这种方法不能区分文本汤米·席尔菲格是指一个人还是一个组织。
OpenNLP 提供了一种更好的基于字典的提取方法,它扫描字典中的名称,让您不必担心匹配短语。
以下是 NER 在 OpenNLP 中使用字典的步骤:
Create an XML file containing the dictionary terms. <dictionary case_sensitive="false">
<entry ref="director">
<token>Director</token>
</entry>
<entry ref="producer">
<token>Producer</token>
</entry>
<entry ref="music director">
<token>Music</token><token>Director</token>
</entry>
<entry ref="singer">
<token>Singer</token>
</entry>
</dictionary
Create a FileInputStream
and instantiate the dictionary with it. InputStream modelIn = new FileInputStream(file);
Dictionary dictionary = new Dictionary(modelIn);
Alternatively, the Dictionary
object can be created using a no-arg constructor and tokens added to it as shown here: Dictionary dictionary = new Dictionary();
dictionary.put(new StringList("Director"));
dictionary.put(new StringList("Producer"));
dictionary.put(new StringList("Music", "Director"));
dictionary.put(new StringList("Singer"));
Create the DictionaryNameFinder
instance by using the Dictionary
and assigning a name to it. DictionaryNameFinder dnf = new DictionaryNameFinder(dictionary, "JobTitles");
请参考下面“使用训练模型”一节中的实现步骤,因为其余步骤保持不变。
如果您觉得可以提高精确度,可以使用基于规则和基于地名词典的混合方法。
使用经过训练的模型
对 NER 使用训练模型的方法属于机器学习的监督学习类别:需要人工干预来训练模型,但在模型被训练后,它会返回几乎准确的结果。
这种方法使用统计模型来提取实体。这是提取不限于一组值的实体的首选方法,例如在名称或组织的情况下。这种方法可以找到模型中没有定义或标记的实体。该模型考虑了文本的语义和上下文,很容易解决实体之间的歧义,例如人名和组织名。使用早期的方法无法解决这些问题;训练有素的模特是唯一的出路。它不需要创建难以维护的大型字典。
用于实体提取的 Solr 插件
在本节中,您将学习从被索引的文档中提取命名实体,并将提取的术语填充到一个单独的 Solr 字段中。这个过程需要两个主要步骤:
Extract the named entities from the text. In the provided sample, you’ll use OpenNLP and the trained models freely available on its web site. The model can be downloaded from http://opennlp.sourceforge.net/models-1.5/
. Write a custom update request processor, that will create a new field and add the extracted entities to it.
以下是将提取的命名实体添加到单独的字段时要遵循的详细步骤:
Write a Java class to extract the named entities by using OpenNLP. Here are the steps for extraction: Read the trained model by using FileInputStream
and instantiate the TokenNameFinderModel
with it. OpenNLP requires a separate model for each entity type. To support multiple entities, a separate model should be loaded for each entity. InputStream modelIn = new FileInputStream(fileName);
TokenNameFinderModel model = new TokenNameFinderModel(modelIn);
Instantiate the NameFinderME
class by providing the model to its constructor. A separate instance of NameFinderME
should be created for each entity type. NameFinderME nameFinder = new NameFinderME(model);
NameFinderME
as a prerequisite requires the sentence to be tokenized, which can be done using the OpenNLP tokenization component. If this code is part of the analysis chain, the sentence can be tokenized by using the Solr-provided tokenizer. The simplest form of tokenization can even be using Java String’s split()
method, also used in this example, though it’s prone to creating invalid tokens. String [] sentence = query.split(" ");
Pass the tokenized sentence to the NameFinderMe
instance, which returns the extracted named entity. Span[] spans = nameFinder.find(sentence);
Iterate the Span
array to extract the named entities. We have populated the extracted entities to the NamedEntity
bean that holds the entity and its corresponding entity type. List<NamedEntity> neList = new ArrayList<>();
for (Span span : spans) {
NamedEntity entity = new NamedEntity();
StringBuilder match = new StringBuilder();
for (int i = span.getStart(); i < span.getEnd(); i++) {
match.append(sentence[i]).append(" ");
}
entity.setToken(match.toString().trim.());
entity.setEntity(entityName);
neList.add(entity);
}
After processing the sentences of a document, call clearAdaptiveData()
to clear the cache, which is maintained by OpenNLP to track previous entity extraction of a word. nameFinder.clearAdaptiveData();
Write a custom implementation of UpdateRequestProcessor
and its factory method. The following are the steps to be followed for adding the extracted entities to a separate field. (Refer to Chapter 5, if you want to refresh your memory about writing a custom update request processor.) Read the parameters from NamedList
in the init()
method of NERUpdateProcessorFactory
, the custom factory, to populate the instance variables for controlling the extraction behavior. Also, set up the NamedEntityTagger
that can be used for extraction as mentioned in step 1. private String modelFile;
private String src;
private String dest;
private String entity;
private float boost;
private NamedEntityTagger tagger;
public void init(NamedList args) {
super.init(args);
SolrParams param = SolrParams.toSolrParams(args);
modelFile = param.get("modelFile");
src = param.get("src");
dest = param.get("dest");
entity = param.get("entity","person");
boost = param.getFloat("boost", 1.0f);
tagger = new NamedEntityTagger();
tagger.setup(modelFile, entity);
};
In the processAdd()
method of NERUpdateProcessor
, a custom update request processor, read the field value of the document being indexed and provide it to the NER object for extracting the entities. Create a new field and add the extracted entities to it. @Override
public void processAdd(AddUpdateCommand cmd) throws IOException {
SolrInputDocument doc = cmd.getSolrInputDocument();
Object obj = doc.getFieldValue(src);
if (null != obj && obj instanceof String) {
List<NamedEntity> neList = tagger.tag((String) obj);
for(NamedEntity ne : neList) {
doc.addField(dest, ne.getToken(), boost);
}
}
super.processAdd(cmd);
}
Add the dependencies to the Solr core library. <lib dir="dir-containing-the-jar" regex=" solr-practical-approach-\d.*\.jar" />
Define this custom processor in solrconfig.xml
. In the parameters, you need to specify the path of the model, the source field, the destination field, the entity to be extracted, and the boost to be provided to the destination field. The destination field should be multivalued, as multiple entities can be extracted from the text. <updateRequestProcessorChain name="nlp">
<processor class="com.apress.solr.pa.chapter``11``.opennlp.NERUpdateProcessorFactory">
<str name="modelFile">path-to-en-ner-person.bin</str>
<str name="src">description</str>
<str name="dest">ext_person</str>
<str name="entity">person</str>
<float name="boost">1.8</float>
</processor>
<processor class="solr.LogUpdateProcessorFactory" />
<processor class="solr.RunUpdateProcessorFactory" />
</updateRequestProcessorChain>
Register the defined update chain to the /update
handler. <str name="update.chain">nlp</str>
Restart the instance and index documents. The extracted entities will be automatically added to the destination field. The following is an example of a resultant document. {
"id": "1201",
"description": "Bob Marley and Ricky were born in Jamaica",
"ext_person": [
"Bob Marley ",
"Ricky "
]
}
这段源代码只提取了一种类型的实体。在现实生活中,您可能希望提取多个实体,并将它们填充到不同的字段中。您可以通过加载多个模型并将提取的术语添加到单独的字段来扩展这个更新请求处理器,以适应所需的功能。
OpenNLP 为每个实体类型使用一个单独的模型。相比之下,另一个 NLP 软件包 Stanford NLP 对所有实体使用相同的模型。
这种实体提取的方法应该返回准确的结果,但是输出也取决于标记质量和模型训练的数据(记住,垃圾输入,垃圾输出)。此外,基于模型的方法在内存需求和处理速度方面成本很高。
语义丰富
在第四章中,您学习了使用SynonymFilterFactory
来生成同义词以扩展令牌。这种方法的主要限制是它没有考虑语义。一个词可以是多义的(有多个意思),同义词可以根据它们的词性或上下文而变化。例如,一个典型的泛型synonym.txt
文件可以将单词 large 指定为 big 的同义词,这将把查询 big brother 扩展为 big brother,这在语义上是不正确的。
不使用定义术语及其同义词列表的文本文件,而是使用受控词汇表(如 WordNet 或医学主题词(MeSH ))来应用更复杂的方法,这些词汇表不需要手动定义或手工制作。同义词扩展只是其中的一部分。词汇表和叙词表还包含其他有用的信息,如上义词、下义词和部分义词,您将进一步了解这些信息。该信息可用于理解语义关系和进一步扩展查询。
这些辞典通常由社区或企业维护,它们用最新的单词不断更新语料库。词汇表可以是通用的,也可以是特定于特定领域的。WordNet 是一个通用词库的例子,它可以被整合到任何搜索引擎中,用于任何领域。网格是一个适用于医学领域的词汇。
您还可以通过构建包含适用于您的领域的概念树的分类法和本体论来执行语义丰富。查询术语可以与分类法进行匹配,以提取更宽、更窄或相关的概念(例如altLabel
或prefLabel)
),并执行所需的丰富。你可以从网上获得这些分类法,或者你可以让一个分类法专家为你定义一个。您还可以使用 DBpedia 等资源,这些资源以 RDF 三元组形式提供从 Wikipedia 中提取的结构化信息。Wikidata 是另一个链接数据库,它包含来自 Wikimedia 项目(包括维基百科)的结构化数据。
这些知识库可以通过各种方式集成到 Solr 中。以下是将所需的丰富内容插入 Solr 的方法:
-
文本分析:编写一个定制的令牌过滤器,使用从受控词汇表中提取的同义词或其他关系来扩展令牌。实现可以类似于 Solr 提供的
SynonymFilterFactory
,它使用一个受控的词汇表文件来代替synonyms.txt
文件。SynonymFilterFactory
以最简单的形式支持 WordNet。在本章的后面,您将看到同义词扩展的自定义实现。 -
Query parser: Write a query parser for expanding the user query with its related or narrower term. For example, in an e-commerce web site for clothing, the user query men accessories can be expanded to search for belts or watches. This additional knowledge can be extracted from a taxonomy that contains accessories as a broader term, and belts and watches as its narrower terms. Figure 11-7 specifies the relationship between the broader and narrower concepts. Instead of just expanding the query, you can extend the system to reformulate the query to build a new query.
图 11-7。
Concept tree
同义词扩展
在本节中,您将学习如何自动扩展术语以包含它们的同义词。该过程需要执行以下两项任务:
Knowledge extraction: The first step is to extract the synonyms from the controlled vocabulary. The extraction process depends on the vocabulary and supported format. In the following example, you will extract knowledge from WordNet by using one of the available Java libraries. Solr plug-in: The feature can be plugged into any appropriate extendable Solr component. In this section, you will expand the synonym by using a custom token filter.
在执行这些任务之前,您将学习 WordNet 的基础知识和它提供的信息,然后实际操作同义词扩展。
WordNet
WordNet 是一个大型的英语词汇数据库。它将名词、动词、形容词和副词分成称为同义词集的认知同义词集,每个同义词集表达一个不同的概念。同素集通过概念、语义和词汇关系相互联系。它的结构使它成为计算语言学和自然语言处理的有用工具。它包含 155,287 个单词,组织在 117,659 个同义词集中,总共有 206,941 个词义对。
WordNet 是在 BSD 风格的许可下发布的,可以从它的网站 https://wordnet.princeton.edu/
免费下载。也可以在 http://wordnetweb.princeton.edu/perl/webwn
评测网络版词库。
WordNet 中单词之间的主要关系是同义词,因为它的同义词集将表示相同概念并在许多上下文中可以互换的单词组合在一起。除同义词之外,同义词词典还包含以下主要信息(以及其他信息):
- 上下义关系:这是指词与词之间的关系。它将一个普通的 synset(如 accessory)链接到一个更具体的 synset(如 belt)。比如,accessory 是 belt 的上位词,belt 是 accessory 的下位词。这些关系保持了层次结构,并且是可传递的。
- 部分关系:这是词与词之间的整体-部分关系。例如,纽扣是衬衫的部分名称。
- 转喻:这是指表达越来越具体的方式来描述一个事件的动词。例如,whisper 是 talk 的词源。
- 注释:这是一个词的简短定义。在大多数情况下,它还包含一个或多个说明 synset 成员用法的短句。
WordNet 需要词类和单词作为先决条件。
有一些 Java 库可以用来访问 WordNet,每一个都有自己的优缺点。你可以参考 http://projects.csail.mit.edu/jwi/download.php?f=finlayson.2014.procgwc.7.x.pdf
一篇比较初级库的特性和性能的论文。
用于同义词扩展的 Solr 插件
本节提供了开发术语简单扩展机制的步骤。下面是实现该特性所需的两个步骤
Write a client to extract synonmys from wordnet. Write a Solr plugin to integrate the extracted synonyms for desired expansion. In this section, the enrichment is integrated to Solr using a custom token filter.
使用 WordNet 扩展同义词
要从 WordNet 中提取同义词,有两个先决条件:
Download the database from WordNet’s download page at https://wordnet.princeton.edu/wordnet/download/current-version/
and extract the dictionary from tar/zip
to a folder. You will need a Java library to access the dictionary. The example uses the Java WordNet Library (JWNL). You can use any other library that suits your requirements.
所提供的这些步骤是最简单的形式;您可能需要优化以使代码生产就绪。此外,该程序只提取同义词。您可以扩展它来提取前面讨论过的其他相关信息。从 WordNet 等通用同义词库中提取相关术语时,需要注意的另一件事是,您可能希望执行消歧,以便为要扩展的术语确定合适的同义词集;例如,像 bank 这样的词是多义的,根据上下文可以表示河岸或银行机构。如果您使用特定于领域的词汇表,歧义消除就不那么重要了。(涵盖歧义消除超出了本书的范围。)
以下是使用 WordNet 扩展同义词的步骤:
The JWNL requires an XML-based properties file to be defined. The following is a sample properties file. The path of the extracted WordNet dictionary should be defined in the dictionary_path
parameter in this file. <?xml version="1.0" encoding="UTF-8"?>
<jwnl_properties language="en">
<version publisher="Princeton" number="3.0" language="en"/>
<dictionary class="net.didion.jwnl.dictionary.FileBackedDictionary">
<param name="dictionary_element_factory" value="net.didion.jwnl.princeton.data.PrincetonWN17FileDictionaryElementFactory"/>
<param name="file_manager"
value="net.didion.jwnl.dictionary.file_manager.FileManagerImpl">
<param name="file_type"
value="net.didion.jwnl.princeton.file.PrincetonRandomAccessDictionaryFile"/>
<param name="dictionary_path" value="/path/to/WordNet/dictionary"/>
</param>
</dictionary>
<resource class="PrincetonResource"/>
</jwnl_properties>
Create a new FileInputStream
specifying the path of the JWNL properties file and initialize the JWNL by providing the FileInputStream
. Create an instance of Dictionary
. JWNL.initialize(new FileInputStream(propFile));
Dictionary dictionary = Dictionary.getInstance();
WordNet requires the part of speech to be specified for the tokens, and that value should be converted to a POS enum that is accepted by the dictionary. The part of speech can be tagged by using the OpenNLP part-of-speech tagger, as discussed in the previous example, or any other package you are familiar with. POS pos = null;
switch (posStr) {
case "VB":
case "VBD":
case "VBG":
case "VBN":
case "VBP":
case "VBZ":
pos = POS.VERB;
break;
case "RB":
case "RBR":
case "RBS":
pos = POS.ADVERB;
break;
case "JJS":
case "JJR":
case "JJ":
pos = POS.ADJECTIVE;
break;
// case "NN":
// case "NNS":
// case "NNP":
// case "NNPS":
// pos = POS.NOUN; // break; }
This code snippet has a section commented out to ignore the synonym expansion for nouns as nouns will introduce more ambiguity. You can uncomment it, if you introduce a disambiguation mechanism. Invoke the getIndexWord()
method of the Dictionary
instance by providing the word and its POS. The returned value is IndexWord
. IndexWord word = dictionary.getIndexWord(pos, term);
The getSenses()
method of IndexWord
returns an array of Synset
, which can be traversed to get all the synonyms. The synset returned varies on the basis of the POS provided. The following block of code populates the synonyms
Java set with all the extracted synonyms. Set<String> synonyms = new HashSet<>();
Synset[] synsets = word.getSenses();
for (Synset synset : synsets) {
Word[] words = synset.getWords();
for (Word w : words) {
String synonym = w.getLemma().toString()
.replace("_", " ");
synonyms.add(synonym);
}
}
用于同义词扩展的自定义令牌过滤器
在上一节中,您学习了从 WordNet 中提取同义词。现在,必须将提取的同义词添加到索引的术语中。在本节中,您将学习编写一个定制的标记过滤器,它可以被添加到一个字段的文本分析链中,以用它的同义词来丰富标记。
Lucene 在包org.apache.lucene.analysis.*
中为令牌过滤器定义了类。为了编写您的定制令牌过滤器,您需要扩展以下两个 Lucene 类:
- TokenFilterFactory:创建
TokenFilter
实例的工厂应该扩展这个抽象类。 - TokenFilter:在第四章的中,你了解到令牌过滤器是一个令牌流,它的输入是另一个令牌流。
TokenFilter
是一个抽象类,提供对所有标记的访问,无论是从文档的字段还是从查询文本。自定义实现应该继承这个TokenFilter
抽象类并覆盖incrementToken()
方法。
以下是编写自定义令牌过滤器并将其插入字段的文本分析链时应遵循的步骤:
Write a custom implementation of TokenFilterFactory
that creates the TokenFilter
. Here are the steps: Extend the TokenFilterFactory
abstract class. The ResourceLoaderAware
interface is implemented by classes that optionally need to initialize or load a resource or file. public class CVSynonymFilterFactory extends TokenFilterFactory implements ResourceLoaderAware {
}
Override the create()
abstract method and return an instance of the custom TokenStream
implementation, which you will create in the next step. You can optionally pass additional parameters to the implementing class. Here you pass the instantiated resources: @Override
public TokenStream create(TokenStream input) {
return new CVSynonymFilter(input, dictionary, tagger, maxExpansion);
}
If you want to read parameters from the factory definition in schema.xml
, you can read it from the map that is available as an input to the constructor. You have read the path of the JWNL properties file, the OpenNLP POS model, and the maximum number of desired expansions. public CVSynonymFilterFactory(Map<String, String> args) {
super(args);
maxExpansion = getInt(args, "maxExpansion", 3);
propFile = require(args, "wordnetFile");
modelFile = require(args, "posModel");
}
Initialize the required resources by overriding the inform()
method provided by the ResourceLoaderAware
interface. @Override
public void inform(ResourceLoader loader) throws IOException {
// initialize for wordnet
try {
JWNL.initialize(new FileInputStream(propFile));
dictionary = Dictionary.getInstance();
} catch (JWNLException ex) {
logger.error(ex.getMessage());
ex.printStackTrace();
throw new IOException(ex.getMessage());
}
// initialize for part of speech tagging
tagger = new PartOfSpeechTagger();
tagger.setup(modelFile);
}
Create a custom class by extending the TokenFilter
abstract class that performs the following tasks. Initialize the required attributes of the tokens. You have initialized the CharTermAttribute
, PositionIncrementAttribute
, PositionLengthAttribute
, TypeAttribute
, and OffsetAttribute
that contain the term text, position increment information, information about the number of positions the token spans, the token type, and the start/end token information, respectively. private final CharTermAttribute termAttr = addAttribute(CharTermAttribute.class);
private final PositionIncrementAttribute posIncrAttr
= addAttribute(PositionIncrementAttribute.class);
private final PositionLengthAttribute posLenAttr
= addAttribute(PositionLengthAttribute.class);
private final TypeAttribute typeAttr = addAttribute(TypeAttribute.class);
private final OffsetAttribute offsetAttr = addAttribute(OffsetAttribute.class);
Define the constructor and do the required initialization. public CVSynonymFilter(TokenStream input,
Dictionary dictionary, PartOfSpeechTagger tagger, int maxExpansion) {
super(input);
this.maxExpansion = maxExpansion;
this.tagger = tagger;
this.vocabulary = new WordnetVocabulary(dictionary);
if (null == tagger || null == vocabulary) {
throw new IllegalArgumentException("fst must be non-null");
}
pendingOutput = new ArrayList<String>();
finished = false;
startOffset = 0;
endOffset = 0;
posIncr = 1;
}
Override the incrementToken()
method of TokenFilter
. The method should play back any buffered output before running parsing and then do the required processing for each token in the token stream. The addOutputSynonyms()
method extracts the synonym for each term provided. When the parsing is completed, the method should mandatorily return the Boolean value false
. @Override
public boolean incrementToken() throws IOException {
while (!finished) { // play back any pending tokens synonyms while (pendingTokens.size() > 0) { String nextToken = pendingTokens.remove(0); termAttr.copyBuffer(nextToken.toCharArray(), 0, nextToken.length()); offsetAttr.setOffset(startOffset, endOffset); posIncAttr.setPositionIncrement(posIncr); posIncr = 0; return true; } // extract synonyms for each token if (input.incrementToken()) { String token = termAttr.toString(); startOffset = offsetAttr.startOffset(); endOffset = offsetAttr.endOffset(); addOutputSynonyms(token); } else { finished = true; } } // should always return false return false; } private void addOutputSynonyms(String token) throws IOException { pendingTokens.add(token); List<PartOfSpeech> posList = tagger.tag(token);
if (null == posList || posList.size() < 1) { return; } Set synonyms = vocabulary.getSynonyms(token, posList.get(0) .getPos(), maxExpansion); if (null == synonyms) { return; } for (String syn : synonyms) { pendingTokens.add(syn); } } @Override
public void reset() throws IOException {
super.reset(); finished = false; pendingTokens.clear(); startOffset = 0; endOffset = 0; posIncr = 1; } The processing provided here tags the part of speech for each token instead of the full sentence, for simplicity. Also, the implementation is suitable for tokens and synonyms of a single word. If you are thinking of getting this feature to production, I suggest you refer to SynonymFilter.java
in the org.apache.lucene.analysis.synonym
package and extend it using the approach provided there. Build the program and add the Java binary JAR to the Solr classpath. Alternatively, place the JAR in the $SOLR_HOME/core/lib
directory. <lib dir="./lib" />
Define the custom filter factory to the analysis chain of the desired fieldType
in schema.xml
. <fieldType name="text_semantic" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
<filter class="com.apress.solr.pa.chapter``11``.enrichment.CVSynonymFilterFactory" maxExpansion="3" wordnetFile="path-of-jwnl-properties.xml" posModel="path-to-en-pos-maxent.bin" />
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
Restart the instance, and you are good to query for semantic synonyms. If the token filter has been added to index-time analysis, the content should be reindexed. Verify the expansion in the Analysis tab in the Solr admin UI.
摘要
在这一章中,你学习了搜索引擎的语义方面。您看到了基于关键字的引擎的局限性,并了解了语义搜索增强用户体验和文档可查找性的方法。语义搜索是一个高级而广泛的话题。鉴于本书范围有限,我们将重点放在简单的自然语言处理技术上,用于识别句子中的重要单词,以及从非结构化文本中提取元数据的方法。您还了解了一种基本的语义丰富技术,用于发现之前完全被忽略但用户可能会非常感兴趣的文档。综上所述,本章提供了在 Solr 中集成这些特性的示例源代码。
你已经到了这本书的结尾。我真诚地希望它的内容对您开发实用的搜索引擎有所帮助,并有助于您了解 Apache Solr。