queryParser介绍以及自定义queryParser实现搜索提示

写这篇博客第一个是为了记录在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便完成了。

猜你喜欢

转载自suichangkele.iteye.com/blog/2365708