上一篇文章关于Young和RESTful的文章中,我们比较了Elasticsearch和Solr,主要对比了两大搜索引擎在搜索相关性方面的能力。我们讨论了控制搜索相关性的因素:
1. 匹配(match)——搜索结果应该包含/排除 哪些条目?
2. 排位(rank)——搜索结果是如何排序的?
上次我们讨论了匹配,Elasticsearch很明显的胜出了。这次我们来看下排名,更具体地说,我们来让这两大搜索引擎在这方面一决雌雄。下一次,我们将讨论每个搜索引擎在自定义搜索相关性方面的可扩展性。
在这篇文章中,我尽量避免逐条比对的方式。现如今搜索引擎之间逐条特性的比对方式已经过时了,这两大搜索巨头在具体特性方面早已是你追我赶的形式了。
只见树木不见森林的理解方式在此处更为合适。把每个搜索引擎看做是一个排位编程语言,这就是我们所要比较的。在实际工作中每个编程语言有其特殊的句法和语法怪癖。当然,这两个搜索引擎都会将你的查询解释成Lucene“机器码”,解释结果之间的不同与Ruby、python或Haskell之间的不同类似。
你会发现Solr是比较简洁的,类似于Perl风格。另一方面,Elasticsearch感觉更像Java的冗长与Python明确强调的综合。想要理解我的意思,我们可以从一些简单的单查询指令案例开始,逐渐深入分析一些复杂的查询流程。
让我们一起开始先从基本查询开始,以此来了解每个查询引擎的查询描述语言DSL。我们将先看下Solr,然后比较下使用 Elasticsearch DSL构建 相同的查询。
上面我们比较Solr的查询DSL和Perl。Solr的查询DSL很像Perl的原因是他们都让你又爱又恨。对于Solr的忠实拥趸,没有其他查询引擎可以更强大。当你编写的代码时那种紧凑感感觉很好。如果你沉迷于它的奥秘,一个查询语句只要瞥一眼就能获得很多信息而无需详细分析。另一方面,就像Perl,其他开发人员不深入理解查询语言的话就容易感到失落。如果你不在日常工作中使用它,Solr查询很快就变成“只写”——难以阅读和维护。Solr的简洁的语法源于它的起源是一个完全基于url的查询语言。Solr出生于Web界API以完全通过URL的查询参数驱动为时尚的时代。所以Solr试图通过地址栏满足所有你想带的参数。对于简单的搜索,这使其很好理解。这样的URL http://solr.quepid.com/solr/statedecoded/select?q =text:(dog catcher law) 看上去相当直观。将参数添加到搜索,比如 &q.op =AND 也说得通(这里我们设置默认操作符为AND,使查询为dog AND catcher AND law)。然而Solr通过 局部参数 的语法来扩展参数查询。举一个例子,另一种重写了最后查询的方法可能是 {!lucene q.op =AND} text:( dog catcher law)。如果你理解局部参数,这段代码是很容易阅读和理解的。如果你不理解,那看起来就会觉得晦涩难懂。
Solr 查询 DSL 的另一方面集中在最后一点本地参数(localparams)上的代码。 Solr 的解释查询使用一个 查询解析器 。上面,我们指定查询解析器为我们的查询字符串作lucene,并使用了绑定符号{!lucene…}。Solr提供了广泛的查询解析器,他们每个都有自己的参数和语法。让我们来看一个例子,使用“edismax”的查询解析器。这个示例搜索提供了在两个字段(catch_line 和 text)上的关键字,并总结得分的相关性,只有匹配50%的查询项 {!edismax mm=50% tie=1 qf='catch_line text'}dog catcher law 才会返回结果。现在,它很简洁了!每一个查询解析器,它都应该被添加,对于Solr往往是由不同的作者贡献的。因此,这将创建起一个广泛的库查询解析器,每一个查询解析器都会有不同的语法和参数。
与solr比较来看,基于es构建的查询语句比较冗长,但可读性好。es是出现在restful和jsonapis盛行的时间,所以以这两种为接口方式。es有意识与我们当前的api进行无缝对接。你可以以json对象的方式构建es查询,如下面的样例:
{"query": { "multi_match": { "query": "dog catcher law", "fields": ["text", "catch_line"], "minimum_should_match": "50%", "type": "most_fields" } }}
这种冗长性是值得的。因为看到这个json,即便对于不懂搜索引擎的人来说,也可以大致猜到其含义。上述查询可以很清楚地被理解:类型为“multi_match”的查询,输入的查询串是“dog catcher law.”,需要匹配的域是“text,catch_line”。即便没有多少搜索引擎的专业知识, 也能猜出minimum_should_match或者most_fields的大概意思。
Elasticsearch总是把参数的作用域限制在当前查询中。这是非常有用的。 Elasticsearch中并没有全局/局部参数,只有当前JSON查询对象和它的参数。要理解这一点,你必须先理解讨厌的 Solr本地参数。Solr的本地参数继承于全局的查询参数。举个例子,你使用下面这个查询参数(用于查询捕狗人相关的法律,加入到查询对象“猫”中):
```
parametersq=dog catcher law&defType=edismax&q.op=AND&bq={!edismax mm=50% tie=1 qf='catch_line text'}cat
```
那么你的局部查询参数默默地接收了外部的` parameterq.op=AND`。更令人不爽的是,使用这样的查询,Solr会给报你一个非常迷惑的“无限递归”的错误。为什么会这样?因为你的bq中的本地参数查询也继承了外部的bq--就是它自己!所以这个查询其实是这样的:
```
isbq={!edismax mm=50% tie=1 q.op=AND bq='{!edismax mm=50% tie=1 q.op=AND bq='...' qf='catch_line text'} qf='catch_line text'}
```
Solr把外部的bq保存到那个'bq'中,因此报告不那么直观:
```
org.apache.solr.search.SyntaxError: Infinite Recursion detected parsing query 'dog catcher law'
```
bq={!edismax mm=50% tie=1 bq='' q.op=OR qf='catch_line text'}对我来说,在这个原子查询语句的构建水平上Elasticsearch是赢家。Elasticsearch使您创建查询使感受到一些惊喜。(看看上面的文本中,大部分是在解释Solr的怪癖)。但是如果你的评价单上有简洁高清这一项, 一旦理解你遇到的怪癖, 你可能更喜欢Solr。
然而,有一个领域Solr做得非常好,那就是把多个查询组合到一起。如果你拥有比较好的查询技能,你一定会使用多条查询而不是单条查询。你会把多条查询以及函数查询(数学方法)串起来,组成一个又大又复杂的排名解决方案。这种情况下,Solr能够给你更强大、更高级别的编程结构。而Elasticsearch相反,它关注的是通用用例。由于基于JSON,Elasticsearch的层级查询意味着,如果你要同时执行多条查询,你会大量地重复自己。
这说明什么呢?说明Solr想要简洁,所以它创造了类似 parameter substitution 和 dereferencing 和特性。这些特性让你能够重用部分查询,同时又保证一定的可读性。更重要的是,Solr的函数查询语法提供一个非常强大的函数(thequery())。这个函数能够比Elasticsearch类似方法更好地把相关度评价和数学公式结合到一起。
接下来一个例子,如何将这两个问题关联在一起?我们用短语(phraseQuery)查询一个头,再用一个文本(textQuery)查询一个正文。用Solr可以这样写:
usersQuery=dog catcher law& phraseQuery={!field f=catch_line v=$usersQuery}& textQuery={!edismax qf=text v=$usersQuery}& q=_val_:"product(query($phraseQuery),query($textQuery))"
现在来分析一下这段代码。我们利用usersQuery查询到相关参数(新建的)。我们又构建了两个额外的变量phraseQuery和textQuery执行在usersQuery里的相关查询。最后,我们将结合起来调用函数算出结果q。(_val_语法是不可靠的)。
这 是你现在在Elasticsearch中做不了的。 Elasticsearch的 函数查询 沙箱可以得到一个单一查询的相关性得分。你不能对访问 Elasticsearch 查询的DSL进行超过一个查询的工作和数学上的得分。你只能通过结合文本分数进行订阅公式( 布尔查询 ,等等)。
这对于有能力的用户来说是一个相当大的缺憾。在上面的多个例子中,一定有一种有效的方式强有力地放大彼此间的相关性分数。 Elasticsearch的函数查询,在他们的辩护中, 视图掩盖最重要的用例,这些策略对于有能力的用户来说其实非常重要。在我们订阅发展的过程中,我们提到多次,我们希望我们有额外的相关性分数来展示有趣的相关策略。
Solr 同样允许你重用查询参数。通过给参数命名,你可以轻易的重新申明任意参数而不必创建又长又臭的JSON对象。例如,如果我们还想从我们的Solr查询中过滤出不匹配textQuery的所有东西,做法很简单。只需要使用一个引用主查询的过滤器查询:
usersQuery=dog catcher law& phraseQuery={!field f=catch_line v=$usersQuery}& textQuery={!edismax qf=text v=$usersQuery}& fq=${textQuery}& q=_val_:"product(query($phraseQuery),query($textQuery))"
与之相反,复杂的 Elasticsearch 查询成了复制粘贴的活。你最终使用的是如下的复杂JSON对象(这里并没有完全复制上面的query查询,因为无法实现多次查询命中)。虽然单独的积木块般的代码更加简短而难以阅读,但你为Solr子查询提供的命名却有助于理解编程者的意图。
{ "query": { "filtered": { "query": { "bool": { "should": [ {"match_phrase": { "catch_line": "dog catcher law" }}, {"match": { "text": "dog catcher law" }} ] } }, "filter": { "query": { "match_phrase": { "catch_line": "dog catcher law" } } } } } }
对我而言,Solr 在这方面胜出。使用它可以很方便地将大量任意的数理命中组合构造成查询。为查询的各个部分命名可以提高可读性。你也不必被局限于使用函数查询所规定的方式来实现命中。Solr能让你避免重复啰嗦,使你更容易阅读实际解决方案中的重点。
我们之前提到了Solr是如何通过查询解析器来解析用户查询及其参数的。查询解析器将查询参数翻译成潜在的Lucene查询。有趣的是,Elasticsearch的工作方式与它大相径庭。Elasticsearch的查询DSL会暴露更多的Lucene主数据类型给用户。比如在上一个查询中,有一段冗长的布尔类查询
"bool": { "should": [ ... ] }
布尔类以及 "SHOULD" 语句与 Lucene 直接关联。它们直接对应了 Lucene 是如何把查询整合到一起的。而Solr 则经常将这些细节从自己的解析查询器中隐藏。例如,你可能没有意识到写q=text:law&bq=catch_line:dog实际上与进行两个布尔 SHOULD 语句作用相同。 Solr 宣扬:“使用boost查询,查询解析将会帮你进行处理”。而 Elasticsearch 则信奉“学会 SHOULD 语句的计算方式,并使用相应的数据类型。”
对我来说还是比较有吸引力的。在solr中,倾向于把更多的“应用”搜索逻辑放到一个查询解析器当中,倾向于为你的用例写有意义的并具备特定领域搜索语义的查询分析器。而在ES中,则鼓励你构建json查询,这更像是Lucene自带的通用查询器,从而让你更接近搜索引擎的本质。但是,使用这种低级别的特征,也使得你的搜索任务无法具备特定领域的语境语义,而这些语义组件,也应该是你搜索应用的一部分。
希望你注意到整个讨论中大量“依赖情况”这样的表述。对我来说,没有谁胜谁负。如果你想要一个与Lucene本质相对应且搜索语法可读的搜索服务,那么你会喜欢ES,它更加的明确,也更容易构建一个“搜索引擎是干什么的”的心理模型。如果你喜欢简洁、抽象的语义,喜欢构建复杂的查询,那么es的冗长和低细节水平,会让你很讨厌。这样你肯定更愿意在solr上花更多时间。
我注意到Solr在更多更具有现实意义的用例中的成功。Elasticsearch则趋于不制造惊喜,但也难以仅仅通过查询DSL来“开拓创新”。社区趋势也同样暴露着这一点。在 上一次 Lucene 革新 中,Solr似乎吸引了更多关于现实中信息检索的讨论。而Elasticsearch则仍主要关注搜索的分析方面。尽管你可以从其中任一社区中获得强有力的解决方案,但是也许这两个社区正处于一个三岔路口——Solr仍然专注于更为单纯的搜索问题,而Elasticsearch则追求分析?
所以,是我错了吗?我很确信我遗漏了什么,而我也十分乐意听到你对这篇文章的反馈!
最后,如果你在为自己的用例选择Elasticsearch或Solr时需要帮助,别害羞, 只管 联系 我 !