设为首页 收藏本站
查看: 1251|回复: 0

[经验分享] Solr实现Low Level查询解析(QParser)

[复制链接]

尚未签到

发表于 2015-11-12 10:34:19 | 显示全部楼层 |阅读模式
  Solr基于Lucene提供了方便的查询解析和搜索服务器的功能,可以以插件的方式集成,非常容易的扩展我们自己需要的查询解析方式。其中,Solr内置了一些QParser,对一些没有特殊要求的应用来说,可以直接使用这些查询解析组件,而无需做任何修改,只需要了解这些查询解析组件提供的基本参数(Local Params),就可以实现强大的搜索功能。
  对于Solr来说,它的设计目标就是尽可能屏蔽底层Lucene的复杂度和难点,而是通过提供可配置的方式来实现全文检索。我们标题所说的Low Level是指,在Solr里面直接使用Lucene的查询语法,来构造满足需要的查询,例如:+(title:solr) +(+(title:lucene content:hadoop) (title:search)),这样的话,你应该了解Lucene的查询语法。因为在实际应用中,完全使用Solr自带一些QParser可能不能够达到我们的目的,比如,你在对数据进行索引,索引时使用了词典的方式进行分词,词典中出现的一些关键词很可能是与用户交互设计中内容相关的(如搜索某个关键词,向用户推荐一些向关键词),那么,在前端需要将某些关键词进行某种组合,提交到后端进行解析搜索。在后端,就会存在一个专门的查询解析组件(在Solr中成为QParser,可以扩展),最终将解析成Lucene识别的“语言”,从而进行索引搜索,返回搜索结果。
  下面是一个简单的例子:
  用户搜索“北京”,我需要提供相关的一组同义关键词:“北平”、”首都“、”京城“、”京都“;而此时,与”北京“相关的一组关键词:”首都博物馆“、”故宫“、”天坛“、”八达岭长城“,其中”首博“是”首都博物馆“的同义词;我们需要实现的是,当用户搜索”北京“时,对其进行同义词扩展搜索(这个在Solr里面可以直接使用同义词Analyzer),但是当用户点击这组相关关键词时,需要进行扩展,比如点击”首都博物馆“进行搜索,这时扩展搜索Lucene能够解析的形式为:
  

+((title:北京 content:北京) (title:北平 content:北平) (title:首都 content:首都) (title:京城 content:京城) (title:京都 content:京都)) +((title:首都博物馆 content:首都博物馆) (title:首博 content:首博))实际上,如果直接使用Lucene,可能会比较容易的多,只需要根据分词词典中具有的Term(存在于索引中),构造满足实际需要的Query即可实现搜索。但是,在Solr里面,将构造查询解析的逻辑移到了QParser中,基于QParserPlugin可以很好地使用Solr提供的一些基础组件和附加组件,并且,这些自定义组件都是基于solrconfig.xml来进行配置的,比较灵活。
  当然,Solr提供了一个QParserPlugin插件,核心查询解析在LuceneQParser中实现,是一个相对Low Level的组件,只需要在solrconfig.xml中配置好相应的requestHandler即可,实例如下:
  

  <queryParser name=&quot;lucene&quot; class=&quot;org.apache.solr.search.LuceneQParserPlugin&quot;/>
<requestHandler name=&quot;/lucene&quot; class=&quot;solr.SearchHandler&quot;>
<lst name=&quot;defaults&quot;>
<str name=&quot;defType&quot;>lucene</str>
<str name=&quot;bf&quot;>recip(ms(NOW,publishDate),3.16e-13,1,1)</str>
<str name=&quot;qf&quot;>title^1.50 content</str>
<bool name=&quot;hl&quot;>true</bool>
<str name=&quot;hl.fl&quot;>title content</str>
<int name=&quot;hl.fragsize&quot;>100</int>
<int name=&quot;hl.snippets&quot;>3</int>
<str name=&quot;fl&quot;>*,score</str>
<str name=&quot;qt&quot;>standard</str>
<str name=&quot;wt&quot;>standard</str>
<str name=&quot;version&quot;>2.2</str>
<str name=&quot;echoParams&quot;>explicit</str>
<str name=&quot;indent&quot;>true</str>
<str name=&quot;debugQuery&quot;>on</str>
<str name=&quot;explainOther&quot;>on</str>
</lst>
</requestHandler>启动Solr搜索服务器(如,部署在tomcat容器中),如果你直接输入上述Lucene能够识别的Query字符串:http://192.168.0.181:8080/solr/core3/lucene/?q=+((title:北京 content:北京) (title:北平 content:北平) (title:首都 content:首都) (title:京城 content:京城) (title:京都 content:京都)) +((title:首都博物馆 content:首都博物馆) (title:首博 content:首博))&start=0&rows=10查询的各个关键词会解析为OR运算,并非我们的设计意图,如果需要的话,可以修改LuceneQParser,将其中的”&#43;“解析成MUST,才能按实际需要搜索。  下面介绍另外一种方法,直接扩展Solr的QParserPlugin。
  首先,和前端设计定义统一的接口:
  

北京OR北平OR首都OR京城OR京都AND首都博物馆OR首博  <=>  +((title:北京 content:北京) (title:北平 content:北平) (title:首都 content:首都) (title:京城 content:京城) (title:京都 content:京都)) +((title:首都博物馆 content:首都博物馆) (title:首博 content:首博))
  我们通过在扩展的QParser中进行解析,代码如下所示:
  

package org.shirdrn.solr.search;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DisjunctionMaxQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.DefaultSolrParams;
import org.apache.solr.common.params.DisMaxParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.DisMaxQParser;
import org.apache.solr.util.SolrPluginUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Customized solr QParser of the plugin
*
* @author shirdrn 2011/11/03
*/
public class SimpleQParser extends DisMaxQParser {
private final Logger LOG = LoggerFactory.getLogger(SimpleQParser.class);
// using low level Term query? For internal search usage.
private boolean useLowLevelTermQuery = false;
private float tiebreaker = 0f;
private static Float mainBoost = 1.0f;
private static Float frontBoost = 1.0f;
private static Float rearBoost = 1.0f;
private String userQuery = &quot;&quot;;
public SimpleQParser(String qstr, SolrParams localParams,
SolrParams params, SolrQueryRequest req) {
super(qstr, localParams, params, req);
}
@Override
public Query parse() throws ParseException {
SolrParams solrParams = localParams == null ? params : new DefaultSolrParams(localParams, params);
queryFields = SolrPluginUtils.parseFieldBoosts(solrParams.getParams(DisMaxParams.QF));
if (0 == queryFields.size()) {
queryFields.put(req.getSchema().getDefaultSearchFieldName(), 1.0f);
}
/* the main query we will execute.  we disable the coord because
* this query is an artificial construct
*/
BooleanQuery query = new BooleanQuery(true);
addMainQuery(query, solrParams);
// rewrite q parameter for highlighting
if(useLowLevelTermQuery) {
query = new BooleanQuery(true);
rewriteAndOrQuery(userQuery, query, solrParams);
}
addBoostQuery(query, solrParams);
addBoostFunctions(query, solrParams);
return query;
}
protected void addMainQuery(BooleanQuery query, SolrParams solrParams)throws ParseException {
tiebreaker = solrParams.getFloat(DisMaxParams.TIE, 0.0f);
// get the comma separated list of fields used for payload
/*
* a parser for dealing with user input, which will convert things to
* DisjunctionMaxQueries
*/
SolrPluginUtils.DisjunctionMaxQueryParser up = getParser(queryFields,DisMaxParams.QS, solrParams, tiebreaker);
/* * * Main User Query * * */
parsedUserQuery = null;
userQuery = getString();
altUserQuery = null;
if (userQuery == null || userQuery.trim().length() < 1) {
// If no query is specified, we may have an alternate
altUserQuery = getAlternateUserQuery(solrParams);
query.add(altUserQuery, BooleanClause.Occur.MUST);
} else {
// There is a valid query string
userQuery = SolrPluginUtils.partialEscape(SolrPluginUtils.stripUnbalancedQuotes(userQuery)).toString();
userQuery = SolrPluginUtils.stripIllegalOperators(userQuery).toString();
// use low level Term for constructing TermQuery or BooleanQuery.
// warning: for internal AND, OR query, in order to integrate with Solr for obtaining highlight
String luceneQueryText = userQuery;
String q = solrParams.get(CommonParams.Q);
if(q!=null && (q.indexOf(&quot;AND&quot;)!=-1 || q.indexOf(&quot;OR&quot;)!=-1)) {
addBasicAndOrQuery(luceneQueryText, query, solrParams);
luceneQueryText = query.toString();
useLowLevelTermQuery = true;
}
LOG.debug(&quot;userQuery=&quot; + luceneQueryText);
parsedUserQuery = getUserQuery(luceneQueryText, up, solrParams);
BooleanQuery rewritedQuery = rewriteQueries(parsedUserQuery);
query.add(rewritedQuery, BooleanClause.Occur.MUST);
}
}
protected void rewriteAndOrQuery(String userQuery, BooleanQuery query, SolrParams solrParams)throws ParseException {
addBasicAndOrQuery(userQuery, query, solrParams);
}
/**
* Parse mixing MUST and SHOULD query defined by us,
* e.g. 首都OR北京OR北平AND首博OR首都博物馆
* @param userQuery
* @param query
* @param solrParams
* @throws ParseException
*/
protected void addBasicAndOrQuery(String userQuery, BooleanQuery query, SolrParams solrParams)throws ParseException {
userQuery = SolrPluginUtils.partialEscape(SolrPluginUtils.stripUnbalancedQuotes(userQuery)).toString();
userQuery = SolrPluginUtils.stripIllegalOperators(userQuery).toString();
LOG.debug(&quot;userQuery=&quot; + userQuery);
BooleanQuery parsedUserQuery = new BooleanQuery(true);
String[] a = userQuery.split(&quot;\\s*AND\\s*&quot;);
String q = &quot;&quot;;
if(a.length==0) {
createTermQuery(parsedUserQuery, userQuery);
} if(a.length>=3) {
if(userQuery.indexOf(&quot;OR&quot;)==-1) { // e.g. 首都AND北京AND北平
BooleanQuery andBooleanQuery = parseAndQuery(a);
parsedUserQuery.add(andBooleanQuery, BooleanClause.Occur.MUST);
}
} else{
if(a.length>0) {
q = a[0].trim();
if(q.indexOf(&quot;OR&quot;)!=-1 || q.length()>0) {
parsedUserQuery.add(parseOrQuery(q, frontBoost), BooleanClause.Occur.MUST);
}
}
if(a.length==2) {
q = a[1].trim();
if(q.indexOf(&quot;OR&quot;)!=-1 || q.length()>0) {
parsedUserQuery.add(parseOrQuery(q, rearBoost), BooleanClause.Occur.MUST);
}
}
}
parsedUserQuery.setBoost(mainBoost);
BooleanQuery rewritedQuery = rewriteQueries(parsedUserQuery);
query.add(rewritedQuery, BooleanClause.Occur.MUST);
}
/**
* Parse SHOULD query, e.g. 北京OR北平OR首都
* @param ors
* @param boost
* @return
*/
private BooleanQuery parseOrQuery(String ors, Float boost) {
BooleanQuery bq = new BooleanQuery(true);
for(String or : ors.split(&quot;\\s*OR\\s*&quot;)) {
if(!or.isEmpty()) {
createTermQuery(bq, or.trim());
}
}
bq.setBoost(boost);
return bq;
}
/**
* Create TermQuery for some term text, query fields.
* @param bq
* @param qsr
*/
private void createTermQuery(BooleanQuery bq, String qsr) {
for(String field : queryFields.keySet()) {
TermQuery tq = new TermQuery(new Term(field, qsr));
if(queryFields.get(field)!=null) {
tq.setBoost(queryFields.get(field));
}
bq.add(tq, BooleanClause.Occur.SHOULD);
}
}
/**
* Parse MUST query, e.g. 首都AND北京AND北平
* @param ands
* @return
*/
private BooleanQuery parseAndQuery(String[] ands) {
BooleanQuery andBooleanQuery = new BooleanQuery(true);
for(String and : ands) {
if(!and.isEmpty()) {
BooleanQuery bq = new BooleanQuery(true);
createTermQuery(bq, and);
andBooleanQuery.add(bq, BooleanClause.Occur.MUST);
}
}
return andBooleanQuery;
}
/**
* Rewrite a query, especially a {@link BooleanQuery}, whose
* subclauses maybe include {@link BooleanQuery}s, {@link DisjunctionMaxQuery}s,
* {@link TermQuery}s, {@link PhraseQuery}s, {@link PayloadQuery}s, etc.
* @param input
* @return
*/
private BooleanQuery rewriteQueries(Query input) {
BooleanQuery output = new BooleanQuery(true);
if(input instanceof BooleanQuery) {
BooleanQuery bq = (BooleanQuery) input;
for(BooleanClause clause : bq.clauses()) {
if(clause.getQuery() instanceof DisjunctionMaxQuery) {
BooleanClause.Occur occur = clause.getOccur();
output.add(rewriteDisjunctionMaxQueries((DisjunctionMaxQuery) clause.getQuery()), occur); // BooleanClause.Occur.SHOULD
} else {
output.add(clause.getQuery(), clause.getOccur());
}
}
} else if(input instanceof DisjunctionMaxQuery) {
output.add(rewriteDisjunctionMaxQueries((DisjunctionMaxQuery) input), BooleanClause.Occur.SHOULD); // BooleanClause.Occur.SHOULD
}
output.setBoost(input.getBoost()); // boost main clause
return output;
}
/**
* Rewrite the {@link DisjunctionMaxQuery}, because of default parsing
* query string to {@link PhraseQuery}s which are not what we want.
* @param input
* @return
*/
private BooleanQuery rewriteDisjunctionMaxQueries(DisjunctionMaxQuery input) {
// input e.g. (content:&quot;吉林 长白山 内蒙古 九寨沟&quot; | title:&quot;吉林 长白山 内蒙古 九寨沟&quot;^1.5)~1.0
Map<String, BooleanQuery> m = new HashMap<String, BooleanQuery>();
Iterator<Query> iter = input.iterator();
while (iter.hasNext()) {
Query query = iter.next();
if(query instanceof PhraseQuery) {
PhraseQuery pq = (PhraseQuery) query; // e.g. content:&quot;吉林 长白山 内蒙古 九寨沟&quot;
for(Term term : pq.getTerms()) {
BooleanQuery fieldsQuery = m.get(term.text());
if(fieldsQuery==null) {
fieldsQuery = new BooleanQuery(true);
m.put(term.text(), fieldsQuery);
}
fieldsQuery.setBoost(pq.getBoost());
fieldsQuery.add(new TermQuery(term), BooleanClause.Occur.SHOULD);
}
} else if(query instanceof TermQuery) {
TermQuery termQuery = (TermQuery) query;
BooleanQuery fieldsQuery = m.get(termQuery.getTerm().text());
if(fieldsQuery==null) {
fieldsQuery = new BooleanQuery(true);
m.put(termQuery.getTerm().text(), fieldsQuery);
}
fieldsQuery.setBoost(termQuery.getBoost());
fieldsQuery.add(termQuery, BooleanClause.Occur.SHOULD);
}
}
Iterator<Entry<String, BooleanQuery>> it = m.entrySet().iterator();
BooleanQuery mustBooleanQuery = new BooleanQuery(true);
while(it.hasNext()) {
Entry<String, BooleanQuery> entry = it.next();
BooleanQuery shouldBooleanQuery = new BooleanQuery(true);
createTermQuery(shouldBooleanQuery, entry.getKey());
mustBooleanQuery.add(shouldBooleanQuery, BooleanClause.Occur.MUST);
}
return mustBooleanQuery;
}
}
  接下来,QParser的plugin只需要使用上面实现SimpleQParser,非常容易,如下所示:


  

package org.shirdrn.solr.search;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
/**
*
* Simple query parser plugin.
* e.g. search &quot;Tokyo AND food&quot;
*
* @author shirdrn
* @date   2011-11-03
*/
public class SimpleQParserPlugin extends QParserPlugin {
@SuppressWarnings(&quot;rawtypes&quot;)
@Override
public void init(NamedList args) {
}
@Override
public QParser createParser(String qstr, SolrParams localParams,
SolrParams params, SolrQueryRequest req) {
return new  SimpleQParser(qstr, localParams,params, req);
}
}最后,在Solr的solrconfig.xml中配置好对应的requestHandler即可,配置片段示例如下所示:  
  

  <queryParser name=&quot;simple&quot; class=&quot;org.shirdrn.solr.search.SimpleQParserPlugin&quot; />
<requestHandler name=&quot;/simple&quot; class=&quot;solr.SearchHandler&quot;>
<lst name=&quot;defaults&quot;>
<str name=&quot;defType&quot;>simple</str>
<str name=&quot;qf&quot;>title^1.5 content</str>
<str name=&quot;bf&quot;>recip(ms(NOW,publishDate),3.16e-13,1,1)^1.68</str>
<str name=&quot;mainBoost&quot;>1.555</str>
<str name=&quot;frontBoost&quot;>1.333</str>
<str name=&quot;rearBoost&quot;>1.222</str>
<str name=&quot;fl&quot;>*,score</str>
<str name=&quot;qt&quot;>standard</str>
<str name=&quot;wt&quot;>standard</str>
<str name=&quot;version&quot;>2.2</str>
<str name=&quot;echoParams&quot;>explicit</str>
<bool name=&quot;hl&quot;>true</bool>
<str name=&quot;hl.fl&quot;>title content</str>
<int name=&quot;hl.snippets&quot;>3</int>
<str name=&quot;indent&quot;>true</str>
<str name=&quot;debugQuery&quot;>on</str>
<str name=&quot;explainOther&quot;>on</str>
</lst>
</requestHandler>下面,启动Solr搜索服务器,通过搜索:
  

http://192.168.0.181:8080/solr/core/simple/?q=北京OR北平OR首都OR京城OR京都AND首都博物馆OR首博&start=0&rows=10
  就能达到我们的目的,搜索结果的xml&#26684;式响应,如下所示:
  

<result name=&quot;response&quot; numFound=&quot;710&quot; start=&quot;0&quot; maxScore=&quot;2.5198267&quot;>
... ...
<lst name=&quot;debug&quot;>
<str name=&quot;rawquerystring&quot;>北京OR北平OR首都OR京城OR京都AND首都博物馆OR首博</str>
<str name=&quot;querystring&quot;>北京OR北平OR首都OR京城OR京都AND首都博物馆OR首博</str>
<str name=&quot;parsedquery&quot;>
+((+((content:北京 title:北京^1.5 content:北平 title:北平^1.5 content:首都 title:首都^1.5 content:京城 title:京城^1.5 content:京都 title:京都^1.5)^1.333) +((content:首都博物馆 title:首都博物馆^1.5 content:首博 title:首博^1.5)^1.222))^1.555) FunctionQuery(1.0/(3.16E-13*float(ms(const(1320330543420),date(publishDate)))+1.0))
</str>
<str name=&quot;parsedquery_toString&quot;>
+((+((content:北京 title:北京^1.5 content:北平 title:北平^1.5 content:首都 title:首都^1.5 content:京城 title:京城^1.5 content:京都 title:京都^1.5)^1.333) +((content:首都博物馆 title:首都博物馆^1.5 content:首博 title:首博^1.5)^1.222))^1.555) 1.0/(3.16E-13*float(ms(const(1320330543420),date(publishDate)))+1.0)
</str>另外,如必要的时候,还可以扩展SolrDispatcherFilter,对HTTP请求参数进行精细地控制,实现更灵活的请求搜索方式。  

版权声明:本文为博主原创文章,未经博主允许不得转载。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-138238-1-1.html 上篇帖子: Solr 报错:RemoteSolrException: Expected mime type application/octet-stream but got 下篇帖子: SOLR中实现自定义评分函数FunctionQuery
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表