diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 95984ae6a09..95225c15c75 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -69,6 +69,8 @@ Improvements
* SOLR-14342: Load cores in an order that makes collections available sooner and reduces leaderVoteWait timeouts in
large SolrCloud clusters. (David Smiley)
+* SOLR-14329: Add support to choose collapse group to expand in ExpandComponent based on cost (Munendra S N)
+
Optimizations
---------------------
* SOLR-8306: Do not collect expand documents when expand.rows=0 (Marshall Sanders, Amelia Henderson)
diff --git a/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java b/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java
index 47fa0c05f57..bca7e4005c1 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/ExpandComponent.java
@@ -31,6 +31,7 @@ import com.carrotsearch.hppc.cursors.IntObjectCursor;
import com.carrotsearch.hppc.cursors.LongCursor;
import com.carrotsearch.hppc.cursors.LongObjectCursor;
import com.carrotsearch.hppc.cursors.ObjectCursor;
+import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
@@ -92,6 +93,8 @@ import org.apache.solr.util.plugin.SolrCoreAware;
* The CollapsingPostFilter collapses a result set on a field.
*
* The ExpandComponent expands the collapsed groups for a single page.
+ * When multiple collapse groups are specified then, the field is chosen from collapse group with min cost.
+ * If the cost are equal then, the field is chosen from first collapse group.
*
* http parameters:
*
@@ -100,7 +103,7 @@ import org.apache.solr.util.plugin.SolrCoreAware;
* expand.sort=field asc|desc
* expand.q=*:* (optional, overrides the main query)
* expand.fq=type:child (optional, overrides the main filter queries)
- * expand.field=field (mandatory if the not used with the CollapsingQParserPlugin)
+ * expand.field=field (mandatory, if the not used with the CollapsingQParserPlugin. This is given higher priority when both are present)
*/
public class ExpandComponent extends SearchComponent implements PluginInfoInitialized, SolrCoreAware {
public static final String COMPONENT_NAME = "expand";
@@ -143,11 +146,17 @@ public class ExpandComponent extends SearchComponent implements PluginInfoInitia
if (field == null) {
List filters = rb.getFilters();
if (filters != null) {
+ int cost = Integer.MAX_VALUE;
for (Query q : filters) {
if (q instanceof CollapsingQParserPlugin.CollapsingPostFilter) {
CollapsingQParserPlugin.CollapsingPostFilter cp = (CollapsingQParserPlugin.CollapsingPostFilter) q;
- field = cp.getField();
- hint = cp.hint;
+ // if there are multiple collapse pick the low cost one
+ // if cost are equal then first one is picked
+ if (cp.getCost() < cost) {
+ cost = cp.getCost();
+ field = cp.getField();
+ hint = cp.hint;
+ }
}
}
}
@@ -189,7 +198,7 @@ public class ExpandComponent extends SearchComponent implements PluginInfoInitia
}
} else {
for (String fq : fqs) {
- if (fq != null && fq.trim().length() != 0 && !fq.equals("*:*")) {
+ if (StringUtils.isNotBlank(fq) && !fq.equals("*:*")) {
QParser fqp = QParser.getParser(fq, req);
newFilters.add(fqp.getQuery());
}
diff --git a/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java b/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java
index 01e28e2c657..a7b63b6fe77 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/DistributedExpandComponentTest.java
@@ -90,6 +90,19 @@ public class DistributedExpandComponentTest extends BaseDistributedSearchTestCas
//Test page 2
query("q", "*:*", "start","1", "rows", "1", "fq", "{!collapse field="+group+"}", "defType", "edismax", "bf", "field(test_i)", "expand", "true", "fl","*,score");
+ // multiple collapse and equal cost
+ ModifiableSolrParams baseParams = params("q", "*:*", "defType", "edismax", "expand", "true", "fl", "*,score",
+ "bf", "field(test_i)", "expand.sort", "id asc");
+ baseParams.set("fq", "{!collapse field="+group+"}", "{!collapse field=test_i}");
+ query(baseParams);
+
+ // multiple collapse and unequal cost case1
+ baseParams.set("fq", "{!collapse cost=1000 field="+group+"}", "{!collapse cost=2000 field=test_i}");
+ query(baseParams);
+
+ // multiple collapse and unequal cost case2
+ baseParams.set("fq", "{!collapse cost=1000 field="+group+"}", "{!collapse cost=200 field=test_i}");
+ query(baseParams);
ignoreException("missing expand field");
SolrException e = expectThrows(SolrException.class, () -> query("q", "*:*", "expand", "true"));
diff --git a/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java b/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java
index edc2de205ad..7f23d68dcb0 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/TestExpandComponent.java
@@ -419,6 +419,48 @@ public class TestExpandComponent extends SolrTestCaseJ4 {
"/response/lst[@name='expanded']/result[@name='1"+floatAppend+"']/doc[1]/str[@name='id'][.='1']",
"/response/lst[@name='expanded']/result[@name='2"+floatAppend+"']/doc[1]/str[@name='id'][.='5']"
);
+
+ // With multiple collapse
+
+ // with different cost
+ params = params("q", "*:*", "defType", "edismax", "expand", "true", "bf", "field(test_i)", "expand.sort", "id asc");
+ params.set("fq", "{!collapse cost=1000 field="+group+"}", "{!collapse cost=2000 field=test_f}");
+ assertQ(req(params),
+ "*[count(/response/result/doc)=1]",
+ "/response/result/doc[1]/str[@name='id'][.='2']",
+ "/response/lst[@name='expanded']/result[@name='1"+floatAppend+"']/doc[1]/str[@name='id'][.='1']"
+ );
+
+ // with same cost (default cost)
+ params.set("fq", "{!collapse field="+group+"}", "{!collapse field=test_f}");
+ assertQ(req(params),
+ "*[count(/response/result/doc)=1]",
+ "/response/result/doc[1]/str[@name='id'][.='2']",
+ "/response/lst[@name='expanded']/result[@name='1"+floatAppend+"']/doc[1]/str[@name='id'][.='1']"
+ );
+
+ // with different cost but choose the test_f
+ params.set("fq", "{!collapse cost=3000 field="+group+"}", "{!collapse cost=2000 field=test_f}");
+ assertQ(req(params),
+ "*[count(/response/result/doc)=1]",
+ "/response/result/doc[1]/str[@name='id'][.='2']",
+ "/response/lst[@name='expanded']/result[@name='200.0']/doc[1]/str[@name='id'][.='3']",
+ "/response/lst[@name='expanded']/result[@name='200.0']/doc[2]/str[@name='id'][.='6']",
+ "/response/lst[@name='expanded']/result[@name='200.0']/doc[3]/str[@name='id'][.='8']"
+ );
+
+ // with different cost and nullPolicy
+ params.set("bf", "ord(id)");
+ params.set("fq", "{!collapse cost=1000 field="+group+" nullPolicy=collapse}", "{!collapse cost=2000 field=test_f}");
+ assertQ(req(params),
+ "*[count(/response/result/doc)=2]",
+ "/response/result/doc[1]/str[@name='id'][.='8']",
+ "/response/result/doc[2]/str[@name='id'][.='7']",
+ "/response/lst[@name='expanded']/result[@name='2"+floatAppend+"']/doc[1]/str[@name='id'][.='5']",
+ "/response/lst[@name='expanded']/result[@name='2"+floatAppend+"']/doc[2]/str[@name='id'][.='6']",
+ "/response/lst[@name='expanded']/result[@name='1"+floatAppend+"']/doc[1]/str[@name='id'][.='1']",
+ "/response/lst[@name='expanded']/result[@name='1"+floatAppend+"']/doc[2]/str[@name='id'][.='2']"
+ );
}
@Test
diff --git a/solr/solr-ref-guide/src/collapse-and-expand-results.adoc b/solr/solr-ref-guide/src/collapse-and-expand-results.adoc
index fec769aa22f..1ef7aa9672f 100644
--- a/solr/solr-ref-guide/src/collapse-and-expand-results.adoc
+++ b/solr/solr-ref-guide/src/collapse-and-expand-results.adoc
@@ -116,6 +116,12 @@ Collapse on `group_field` with a hint to use the top level field cache:
fq={!collapse field=group_field hint=top_fc}
----
+Collapse with custom `cost` which defaults to `100`
+[source,text]
+----
+fq={!collapse cost=1000 field=group_field}
+----
+
The CollapsingQParserPlugin fully supports the QueryElevationComponent.
== Expand Component
@@ -138,12 +144,22 @@ The ExpandComponent can now be used to expand the results so you can see the doc
q=foo&fq={!collapse field=ISBN}&expand=true
----
+[IMPORTANT]
+====
+When used with CollapsingQParserPlugin and there are multiple collapse groups then, the field is chosen from group with least cost.
+If there are multiple collapse groups with same cost then, the first specified one is chosen
+====
+
The “expand=true” parameter turns on the ExpandComponent. The ExpandComponent adds a new section to the search output labeled “expanded”.
Inside the expanded section there is a _map_ with each group head pointing to the expanded documents that are within the group. As applications iterate the main collapsed result set, they can access the _expanded_ map to retrieve the expanded groups.
The ExpandComponent has the following parameters:
+`expand.field`::
+Field on which expand documents need to be populated. When `expand=true`, either this parameter needs to be specified or should be used with CollapsingQParserPlugin.
+When both are specified, this parameter is given higher priority.
+
`expand.sort`::
Orders the documents within the expanded groups. The default is `score desc`.