写这篇博客第一个是为了记录在solr中自定义queryParser(顺便介绍一下solr的queryParser),第二个是在 http://suichangkele.iteye.com/blog/2363599 (自定义得分的PrefixQuery)这篇博客中也说了要在solr中使用自己的query要使用自己的queryParser,第三个是公司业务需求,需要实现更加智能的搜索提示(智能是我自己给加的)。因为以上的原因我自己写了一个queryParser来实现我心中理想的搜索提示(先声明一下,我这里使用的是solr5.5.3的版本)。
1、solr中的queryParser:为什么我们在solr中设置q=*:*就能匹配所有的doc呢?为什么q=name:黄*就能匹配所有的name域是以黄开头的doc呢?原理就是solr使用queryParser将输入的q解析为了一个query,然后使用这个query进行了搜索。solr中有很多的queryParser,比如我们熟知的有lucene、dismax、edismax。我们从solr的源码中来仔细看一下吧:org.apache.solr.search.QParserPlugin在这个类中,可以发现有一个map,在加载org.apache.solr.search.QParserPlugin的时候就会忘这个map中添加很多的内容,在这个map中就能找到我们熟悉的lucene、dismax、edismax,不过他们都不是QueryParser,而是QParserPlugin,不过在QParserPlugin这个类中有createParser方法,用于产生一个QueryParser。我们在solrconfig.xml中可以配置QParserPlugin,在searchHandler中也可以配置defType表示使用的QParserPlugin,使用的名字就是在这个map中存放的内容。在org.apache.solr.search.QParserPlugin类中有一个默认的DEFAULT_QTYPE,也就是在一次查询的时候不指定defType的话默认就是使用这个QTYPE,他便是lucene,也就是使用LuceneQParserPlugin来生成要使用的QueryParser。
2、在solrconfig.xml中定义自己的queryParser 很简单,只要继承org.apache.solr.search.QParserPlugin这个类,实现他的createParser方法即可,然后再solrconfig.xml中配置一下。我这里先做一个最简单的,比如我们把所有的q都转化为query的value,并且需要指定一个默认的域作为query的key(加入说是id域吧),然后封装为一个TermQuery(如此一来,即使你搜q=黄*,我也给你生成一个TermQuery,即:id:黄*,注意这个并不是PrefixQuery,仍然是一个TermQuery,只不过value的部分是黄*).代码如下:
public class TermQueryParserPlugin extends QParserPlugin{
private Logger logger = LoggerFactory.getLogger(TermQueryParserPlugin.class);
@Override
public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
logger.info("解析q:{}",qstr);
return new QParser(qstr,localParams,params,req) {//因为逻辑简单,所以直接使用一个匿名内部类,实现其parser方法即可
SolrParams solrParams = SolrParams.wrapDefaults(localParams, params);//将客户端传来的和本地配置的参数合并
@Override
public Query parse() throws SyntaxError {
String df = solrParams.get("df");//从合并后的参数中去的df参数
try {
return new TermQuery(new Term(df, new BytesRef(qstr.getBytes("UTF-8")))){
public String toString(String s) {//重写的目的是为了在页面好看区别来
return "这是一个termQuery";
};
};
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
};
}
}
然后再在solrconfig.xml中配置 <queryParser name="helloword" class="xxxxx.TermQueryParserPlugin"/>,然后再浏览器中访问你的solr,使用的url为:
http://localhost:8080/solr/product/select?q=黄*&wt=json&indent=true&debugQuery=true&defType=helloword&df=id 后面的参数很重要,倒数第三个是开启debug,倒数第二个是指定使用的queryParserPlugin,使用我们上面配置的helloword,第三个参数是因为我们在自定义的queryParser中要使用(不要和dismax中的df混淆了)。可以发现debug的信息:debug":{
"rawquerystring":"黄*", "querystring":"黄*", "parsedquery":"id:黄*", "parsedquery_toString":"这是一个termQuery",
这样我们就能实现自己的queryParser了。
3、我自己实现的使用ScoredPrefixQuery的queryParser做提示(ScoredPrefixQuery参见http://suichangkele.iteye.com/blog/2363599 博客)
我们的要求是这样的:假设我要提示 特仑苏牛奶,
1、当用于输入t时要输入,telunsuniu时也要输入,即对整个的拼音建立索引并使用前缀查询
2、当用户输入tlsn时提示,即对整个拼音的建立索引,使用前缀搜索
3、当用户输入niun时也要提示,即对分词后的term的拼音建立索引,使用前缀查询
4、当用户输入tl或者nn时提示,即对分词后的term的拼音的前缀建立索引,使用前缀搜索
5、当用户输入牛奶、牛、特伦时提示,即对分词建立索引,查询时使用前缀搜索
写到这我们便明白了要对任何一个输入词做五个域的查询,很显然这个很符合dismaxquery,所以我这里的QueryParser就直接继承了DismaxQueryParser,因为他里面有很多的方法可以直接拿来用。
/** 用于做提示用的QParser*/
public class SuggestQParser extends DisMaxQParser {
private static Logger logger = LoggerFactory.getLogger(SuggestQParser.class);
public SuggestQParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
super(qstr, localParams, params, req);
}
@Override
public Query parse() throws SyntaxError {
SolrParams solrParams = SolrParams.wrapDefaults(localParams, params);//将多个参数合并
queryFields = parseQueryFields(req.getSchema(), solrParams);//获得查询的域以及boost,在这里
/*
* the main query we will execute. we disable the coord because this
* query is an artificial construct
*/
BooleanQuery query = new BooleanQuery(true);
boolean notBlank = addMainQuery(query, solrParams);
if (!notBlank)
return null;
return query;
}
protected boolean addMainQuery(BooleanQuery query, SolrParams solrParams) throws SyntaxError {
//得到tie
float tiebreaker = solrParams.getFloat(DisMaxParams.TIE, 0.0f);
// 得到用户的输入词
String userQuery = getString();
if (userQuery == null) {
throw new RuntimeException("ScoredPrefixQueryParser中不接收空的query,不能使用q.alt参数");
} else {
//1、使用iK进行分词
//2、循环所有的token,每一个token按照df形成一个ScoredPrefixQuery和termQuery,所有df的形成的query封装为一个DisjunctionMaxQuery,并添加到BooleqnQuery中,关系为optional
Analyzer ar = new IKAnalyzer(true);
try {
int termCount = 0;
TokenStream stream = ar.tokenStream("", userQuery);
TermToBytesRefAttribute termAttribute = stream.addAttribute(TermToBytesRefAttribute.class);
BytesRef bf = termAttribute.getBytesRef();//用于存放字符串的东西。
stream.reset();
while(stream.incrementToken()) { //循环token
termAttribute.fillBytesRef();//重新放入字符串。
termCount++;
//每个term形成一个DisjunctionMaxQuery
DisjunctionMaxQuery dis = new DisjunctionMaxQuery(tiebreaker);
String term = bf.utf8ToString();//得到字符串。
logger.info("分词的结果:序号:{},term:{}",new Object[]{termCount,term});
for(String field:queryFields.keySet()){//循环qf,也就是上面中提到的五个域
//形成一个得分的prefixQuery
Query prefixQ = new ScoredPrefixQuery(new Term(field, term));
dis.add(prefixQ);
}
query.add(dis,Occur.SHOULD);//添加该term的dis
}
query.setMinimumNumberShouldMatch(termCount);//这个的目的为了匹配所有的分析的term
logger.info("最后形成的booleanQuery:{}",query);
} catch (IOException e) {
logger.error("处理分词的时候发生错误,字符串为:{}", new Object[]{userQuery},e);
return false;
}finally {
if(ar != null)
ar.close();
}
}
return true;
}
}
至此,自己实现queryParser、使用之前写的ScoredPrefixQuery以及实现提示词的queryParser便完成了。