Solr实现Low Level查询解析(QParser)

2019-03-27 01:17|来源: 网路

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="lucene" class="org.apache.solr.search.LuceneQParserPlugin"/>
  <requestHandler name="/lucene" class="solr.SearchHandler">
    <lst name="defaults">
      <str name="defType">lucene</str>
      <str name="bf">recip(ms(NOW,publishDate),3.16e-13,1,1)</str>
      <str name="qf">title^1.50 content</str>
      
      <bool name="hl">true</bool>
      <str name="hl.fl">title content</str>
      <int name="hl.fragsize">100</int>
      <int name="hl.snippets">3</int>
      
      <str name="fl">*,score</str>
      <str name="qt">standard</str>
      <str name="wt">standard</str>
      <str name="version">2.2</str>
      <str name="echoParams">explicit</str>
      <str name="indent">true</str>
      <str name="debugQuery">on</str>
      <str name="explainOther">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,将其中的”+“解析成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 = "";

	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("AND")!=-1 || q.indexOf("OR")!=-1)) {
	    	  addBasicAndOrQuery(luceneQueryText, query, solrParams);
	    	  luceneQueryText = query.toString();
	    	  useLowLevelTermQuery = true;
	      }
	      
	      LOG.debug("userQuery=" + 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("userQuery=" + userQuery);
		    BooleanQuery parsedUserQuery = new BooleanQuery(true);
		    String[] a = userQuery.split("\\s*AND\\s*");
			String q = "";
			if(a.length==0) {
				createTermQuery(parsedUserQuery, userQuery);
			} if(a.length>=3) {
				if(userQuery.indexOf("OR")==-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("OR")!=-1 || q.length()>0) {
						parsedUserQuery.add(parseOrQuery(q, frontBoost), BooleanClause.Occur.MUST);
					}
				}
				if(a.length==2) {
					q = a[1].trim();
					if(q.indexOf("OR")!=-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("\\s*OR\\s*")) {
			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:"吉林 长白山 内蒙古 九寨沟" | title:"吉林 长白山 内蒙古 九寨沟"^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:"吉林 长白山 内蒙古 九寨沟"
				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 "Tokyo AND food"
 * 
 * @author shirdrn
 * @date   2011-11-03
 */
public class SimpleQParserPlugin extends QParserPlugin {
	@SuppressWarnings("rawtypes")
	@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="simple" class="org.shirdrn.solr.search.SimpleQParserPlugin" />
  <requestHandler name="/simple" class="solr.SearchHandler">
        <lst name="defaults">
                <str name="defType">simple</str>
                <str name="qf">title^1.5 content</str>

                <str name="bf">recip(ms(NOW,publishDate),3.16e-13,1,1)^1.68</str>

                <str name="mainBoost">1.555</str>
                <str name="frontBoost">1.333</str>
                <str name="rearBoost">1.222</str>

                <str name="fl">*,score</str>
                <str name="qt">standard</str>
                <str name="wt">standard</str>
                <str name="version">2.2</str>
                <str name="echoParams">explicit</str>
                <bool name="hl">true</bool>
                <str name="hl.fl">title content</str>
                <int name="hl.snippets">3</int>

                <str name="indent">true</str>
                <str name="debugQuery">on</str>
                <str name="explainOther">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格式响应,如下所示:

<result name="response" numFound="710" start="0" maxScore="2.5198267">
... ...
<lst name="debug">
<str name="rawquerystring">北京OR北平OR首都OR京城OR京都AND首都博物馆OR首博</str>
<str name="querystring">北京OR北平OR首都OR京城OR京都AND首都博物馆OR首博</str>
<str name="parsedquery">
+((+((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="parsedquery_toString">
+((+((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请求参数进行精细地控制,实现更灵活的请求搜索方式。


转自:http://blog.csdn.net/shirdrn/article/details/6920585

相关问答

更多
  • 我不知道为什么这不工作,但这是逻辑上相同的,它的工作: -(myField:superneat AND -myOtherField:somethingElse) 也许这与在查询中定义相同的字段两次有关 尝试在solr-user组中询问,然后在这里发回最后的答案! I don't know why that doesn't work, but this one is logically equivalent and it does work: -(myField:superneat AND -myOther ...
  • 由于默认查询处理程序工作(/ select),我开始认为我的查询处理程序有问题。 为了找出问题所在,我开始禁用部分自己的请求处理程序。 空mm可以给你错误 ! 我曾经有一个mm参数,但我不再需要它了,所以我把它留空了...... Since the default query handler worked(/select) I started to think something was wrong with my query handler. ...
  • 这些DB服务器的用途不同,它在很大程度上取决于您的应用程序(以及您存储的数据类型)是否应该仅使用Solr或MySQL。 MySQL可以很好地存储具有大量关系和表格的数据(彼此相关的表格)。 Solr很适合文本搜索(正如你所说:快速索引),如果你没有很多“相关数据”,你确实可以将这些数据存储在相同的文档中。 有些人确实只使用Solr来存储他们的数据库...但我仍然认为RDBM可以很好地用于某些类型的数据。 例如:如果您想允许快速搜索系统用户并存储他们的完整个人资料,以及一些信息详细信息......最好使用So ...
  • 经过大量的努力,我发现现在没有工具可以解析FAST ESP查询到Solr查询(语法方面)。 我必须发展自己的。 听说Solr开发团队正在尝试开发一种能够做到这一点的工具。 同时,您将不得不等待,或开发自己的工具。 在测试了我开发的那个 - 我将它发布供公众使用。 After a lot of effort, I found out there is no tool for now that can parse FAST ESP queries to Solr queries (syntax wise). I ...
  • 您在每个查询中都给出了参数rows=2147483647 。 该参数的含义是(取自参考文献) 您可以使用rows参数对查询中的结果进行分页。 该参数指定Solr应一次返回客户端的完整结果集中的最大文档数。 默认值为10.也就是说,默认情况下,Solr一次返回10个文档以响应查询。 因此,您告诉Solr生效,在单个响应中发送查询的所有匹配。 这是你糟糕表现的原因。 当查询“java”时 ,谷歌是否会向您发送所有500.000.000次匹配,不会。 为什么不,性能。 我知道的每一个IR应用程序都会为您提供一个带 ...
  • 首先,我认为Solr通配符比“1或多个”更好地归纳为“0或多个”。 我怀疑这是你问题的根源。 (例如,请参阅WildcardQuery的javadoc 。) 其次,你是否在使用词干,因为我的第一个猜测是你正在处理一个词干问题。 Solr通配符在词干方面可能表现得很奇怪。 这是因为通配符扩展是通过搜索存储在倒排索引中的术语列表来进行的; 这些术语将以词干形式出现(可能类似“gatorad”),而不是来自原始源文本(可能是“gatorade”或“gatorades”)的单词。 例如,假设你有一个词干分析器,把“ ...
  • 除了新功能之外,Solr 3.6和Solr 4.0之间是否有任何重大差异? 我发现这个问题很奇怪,至少可以说。 错误修复和新功能是发布的全部内容! 您可以在此处查看 Solr版本的完整更新日志。 不要忘记Solr和Lucene是一致发布的,所以你还需要在两个项目中寻找相关的变化。 我可以安全地使用我在Solr 4.0中的现有查询(在Solr 3.6中工作的查询)吗? 查询应该没问题,但索引 - 可能不是。 引用另一篇SO帖子中的 javanna: 索引格式已更改,但Solr将负责升级索引。 一旦用旧索引启动 ...
  • 我们可以定制嵌入在Solr中的Lucene吗? 是的,你可以 。 但请记住这一点: Lucene和Solr提交者是全文搜索领域的一些最重要的专家。 他们在这个领域有多年的经验。 如果你认为你可以比他们做得更好,那么继续改变Solr以满足你的需求(它是Apache许可的,所以没有任何商业限制),如果你这样做,试着这样做,以便你以后可以贡献它回到项目,这样每个人都可以受益,项目也会向前发展。 对于绝大多数Solr用户而言,库存产品绰绰有余并满足所有需求。 换句话说,在进入更改代码之前,在邮件列表(stackov ...
  • 这里有一篇很好的文章将帮助您完成PHP和SOLR的集成: http://www.ibm.com/developerworks/opensource/library/os-php-apachesolr/ SOLR有许多PHP接口,该文章引用了PHP SOLR客户端: http://code.google.com/p/solr-php-client/ 但也有这个: http://pecl.php.net/package/solr There's a good article here that will hel ...
  • 您面临的问题称为Deep Paging 。 在solr.pl上有一篇关于它的好文章和关于Solr跟踪器的一个不完整的问题 。 本文中提到的解决方案将要求您对结果进行排序,如果这对您不可行,则解决方案将无效。 我们的想法是按照稳定属性进行排序,在文章中price ,然后按价格范围进行过滤,例如fq=price:[9000+TO+10000] 。 如果你将fq与一个合适的start - 比如start=100030 - 你将获得更好的性能,因为solr不会收集与fq不匹配的文档。 但是,您需要提前至少提出一个查 ...