dsmiley commented on code in PR #3418:
URL: https://github.com/apache/solr/pull/3418#discussion_r2320851066


##########
solr/core/src/java/org/apache/solr/search/combine/ReciprocalRankFusion.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.search.combine;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TotalHits;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.handler.component.ShardDoc;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.DocIterator;
+import org.apache.solr.search.DocList;
+import org.apache.solr.search.DocListAndSet;
+import org.apache.solr.search.DocSet;
+import org.apache.solr.search.DocSlice;
+import org.apache.solr.search.QueryResult;
+import org.apache.solr.search.SolrDocumentFetcher;
+import org.apache.solr.search.SolrIndexSearcher;
+
+/**
+ * This class implements a query and response combiner that uses the 
Reciprocal Rank Fusion (RRF)
+ * algorithm to combine multiple ranked lists into a single ranked list.
+ */
+public class ReciprocalRankFusion extends QueryAndResponseCombiner {
+
+  private int k;
+
+  public ReciprocalRankFusion() {
+    this.k = CombinerParams.DEFAULT_COMBINER_RRF_K;
+  }
+
+  @Override
+  public void init(NamedList<?> args) {
+    Object kParam = args.get("k");
+    if (kParam != null) {
+      this.k = Integer.parseInt(kParam.toString());
+    }
+  }
+
+  public int getK() {
+    return k;
+  }
+
+  @Override
+  public QueryResult combine(List<QueryResult> rankedLists, SolrParams 
solrParams) {
+    int kVal = solrParams.getInt(CombinerParams.COMBINER_RRF_K, this.k);
+    List<DocListAndSet> docListAndSet = 
getDocListsAndSetFromQueryResults(rankedLists);
+    QueryResult combinedResult = new QueryResult();
+    combineResults(combinedResult, docListAndSet, false, kVal);
+    return combinedResult;
+  }
+
+  private static List<DocListAndSet> getDocListsAndSetFromQueryResults(

Review Comment:
   minor: IMO this method isn't worthwhile.  Instead, just pass the 
`List<QueryResult>` to combineResults.  That method need not be separated 
either; I think just inline it.



##########
solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java:
##########
@@ -0,0 +1,598 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.CursorMarkParams;
+import org.apache.solr.common.params.GroupParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.response.BasicResultContext;
+import org.apache.solr.response.ResultContext;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.DocListAndSet;
+import org.apache.solr.search.QueryResult;
+import org.apache.solr.search.SolrReturnFields;
+import org.apache.solr.search.SortSpec;
+import org.apache.solr.search.combine.QueryAndResponseCombiner;
+import org.apache.solr.search.combine.ReciprocalRankFusion;
+import org.apache.solr.util.SolrResponseUtil;
+import org.apache.solr.util.plugin.SolrCoreAware;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The CombinedQueryComponent class extends QueryComponent and provides 
support for executing
+ * multiple queries and combining their results.
+ */
+public class CombinedQueryComponent extends QueryComponent implements 
SolrCoreAware {
+
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  public static final String COMPONENT_NAME = "combined_query";
+  protected NamedList<?> initParams;
+  private final Map<String, QueryAndResponseCombiner> combiners = new 
HashMap<>();
+  private int maxCombinerQueries;
+
+  @Override
+  public void init(NamedList<?> args) {
+    super.init(args);
+    this.initParams = args;
+    this.maxCombinerQueries = CombinerParams.DEFAULT_MAX_COMBINER_QUERIES;
+  }
+
+  @Override
+  public void inform(SolrCore core) {
+    for (Map.Entry<String, ?> initEntry : initParams) {
+      if ("combiners".equals(initEntry.getKey())
+          && initEntry.getValue() instanceof NamedList<?> all) {
+        for (int i = 0; i < all.size(); i++) {
+          String name = all.getName(i);
+          NamedList<?> combinerConfig = (NamedList<?>) all.getVal(i);
+          String className = (String) combinerConfig.get("class");
+          QueryAndResponseCombiner combiner =
+              core.getResourceLoader().newInstance(className, 
QueryAndResponseCombiner.class);
+          combiner.init(combinerConfig);
+          combiners.compute(
+              name,
+              (k, existingCombiner) -> {
+                if (existingCombiner == null) {
+                  return combiner;
+                }
+                throw new SolrException(
+                    SolrException.ErrorCode.BAD_REQUEST,
+                    "Found more than one combiner with same name");
+              });
+        }
+      }
+    }
+    Object maxQueries = initParams.get("maxCombinerQueries");
+    if (maxQueries != null) {
+      this.maxCombinerQueries = Integer.parseInt(maxQueries.toString());
+    }
+    combiners.computeIfAbsent(
+        CombinerParams.RECIPROCAL_RANK_FUSION,
+        key -> {
+          ReciprocalRankFusion reciprocalRankFusion = new 
ReciprocalRankFusion();
+          reciprocalRankFusion.init(initParams);
+          return reciprocalRankFusion;
+        });
+  }
+
+  /**
+   * Overrides the prepare method to handle combined queries.
+   *
+   * @param rb the ResponseBuilder to prepare
+   * @throws IOException if an I/O error occurs during preparation
+   */
+  @Override
+  public void prepare(ResponseBuilder rb) throws IOException {
+    if (rb instanceof CombinedQueryResponseBuilder crb) {
+      SolrParams params = crb.req.getParams();
+      if (params.get(CursorMarkParams.CURSOR_MARK_PARAM) != null
+          || params.getBool(GroupParams.GROUP, false)) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Unsupported functionality 
for Combined Queries.");
+      }
+      String[] queriesToCombineKeys = 
params.getParams(CombinerParams.COMBINER_QUERY);
+      if (queriesToCombineKeys.length > maxCombinerQueries) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST,
+            "Too many queries to combine: limit is " + maxCombinerQueries);
+      }
+      for (String queryKey : queriesToCombineKeys) {
+        final var unparsedQuery = params.get(queryKey);
+        ResponseBuilder rbNew = new ResponseBuilder(rb.req, new 
SolrQueryResponse(), rb.components);
+        rbNew.setQueryString(unparsedQuery);
+        super.prepare(rbNew);
+        crb.setFilters(rbNew.getFilters());
+        crb.responseBuilders.add(rbNew);
+      }
+    }
+    super.prepare(rb);
+  }
+
+  /**
+   * Overrides the process method to handle CombinedQueryResponseBuilder 
instances. This method
+   * processes the responses from multiple queries in the SearchIndexer, 
combines them using the

Review Comment:
   What is the "SearchIndexer"?  I think you could drop this part of the 
sentence.
   
   BTW it's best to `{@link classname}` so make class names hyperlinked



##########
solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentTest.java:
##########
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_START;
+
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonParams;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * The CombinedQueryComponentTest class is a unit test suite for the 
CombinedQueryComponent in Solr.

Review Comment:
   this is not a unit test



##########
solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java:
##########
@@ -0,0 +1,598 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.CursorMarkParams;
+import org.apache.solr.common.params.GroupParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.response.BasicResultContext;
+import org.apache.solr.response.ResultContext;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.DocListAndSet;
+import org.apache.solr.search.QueryResult;
+import org.apache.solr.search.SolrReturnFields;
+import org.apache.solr.search.SortSpec;
+import org.apache.solr.search.combine.QueryAndResponseCombiner;
+import org.apache.solr.search.combine.ReciprocalRankFusion;
+import org.apache.solr.util.SolrResponseUtil;
+import org.apache.solr.util.plugin.SolrCoreAware;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The CombinedQueryComponent class extends QueryComponent and provides 
support for executing
+ * multiple queries and combining their results.
+ */
+public class CombinedQueryComponent extends QueryComponent implements 
SolrCoreAware {
+
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  public static final String COMPONENT_NAME = "combined_query";
+  protected NamedList<?> initParams;
+  private final Map<String, QueryAndResponseCombiner> combiners = new 
HashMap<>();
+  private int maxCombinerQueries;
+
+  @Override
+  public void init(NamedList<?> args) {
+    super.init(args);
+    this.initParams = args;
+    this.maxCombinerQueries = CombinerParams.DEFAULT_MAX_COMBINER_QUERIES;
+  }
+
+  @Override
+  public void inform(SolrCore core) {
+    for (Map.Entry<String, ?> initEntry : initParams) {
+      if ("combiners".equals(initEntry.getKey())
+          && initEntry.getValue() instanceof NamedList<?> all) {
+        for (int i = 0; i < all.size(); i++) {
+          String name = all.getName(i);
+          NamedList<?> combinerConfig = (NamedList<?>) all.getVal(i);
+          String className = (String) combinerConfig.get("class");
+          QueryAndResponseCombiner combiner =
+              core.getResourceLoader().newInstance(className, 
QueryAndResponseCombiner.class);
+          combiner.init(combinerConfig);
+          combiners.compute(
+              name,
+              (k, existingCombiner) -> {
+                if (existingCombiner == null) {
+                  return combiner;
+                }
+                throw new SolrException(
+                    SolrException.ErrorCode.BAD_REQUEST,
+                    "Found more than one combiner with same name");
+              });
+        }
+      }
+    }
+    Object maxQueries = initParams.get("maxCombinerQueries");
+    if (maxQueries != null) {
+      this.maxCombinerQueries = Integer.parseInt(maxQueries.toString());
+    }
+    combiners.computeIfAbsent(
+        CombinerParams.RECIPROCAL_RANK_FUSION,
+        key -> {
+          ReciprocalRankFusion reciprocalRankFusion = new 
ReciprocalRankFusion();
+          reciprocalRankFusion.init(initParams);
+          return reciprocalRankFusion;
+        });
+  }
+
+  /**
+   * Overrides the prepare method to handle combined queries.
+   *
+   * @param rb the ResponseBuilder to prepare
+   * @throws IOException if an I/O error occurs during preparation
+   */
+  @Override
+  public void prepare(ResponseBuilder rb) throws IOException {
+    if (rb instanceof CombinedQueryResponseBuilder crb) {
+      SolrParams params = crb.req.getParams();
+      if (params.get(CursorMarkParams.CURSOR_MARK_PARAM) != null
+          || params.getBool(GroupParams.GROUP, false)) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Unsupported functionality 
for Combined Queries.");
+      }
+      String[] queriesToCombineKeys = 
params.getParams(CombinerParams.COMBINER_QUERY);
+      if (queriesToCombineKeys.length > maxCombinerQueries) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST,
+            "Too many queries to combine: limit is " + maxCombinerQueries);
+      }
+      for (String queryKey : queriesToCombineKeys) {
+        final var unparsedQuery = params.get(queryKey);
+        ResponseBuilder rbNew = new ResponseBuilder(rb.req, new 
SolrQueryResponse(), rb.components);
+        rbNew.setQueryString(unparsedQuery);
+        super.prepare(rbNew);
+        crb.setFilters(rbNew.getFilters());
+        crb.responseBuilders.add(rbNew);
+      }
+    }
+    super.prepare(rb);
+  }
+
+  /**
+   * Overrides the process method to handle CombinedQueryResponseBuilder 
instances. This method
+   * processes the responses from multiple queries in the SearchIndexer, 
combines them using the
+   * specified QueryAndResponseCombiner strategy, and sets the appropriate 
results and metadata in
+   * the CombinedQueryResponseBuilder.
+   *
+   * @param rb the ResponseBuilder object to process
+   * @throws IOException if an I/O error occurs during processing
+   */
+  @Override
+  public void process(ResponseBuilder rb) throws IOException {
+    if (rb instanceof CombinedQueryResponseBuilder crb) {
+      boolean partialResults = false;
+      boolean segmentTerminatedEarly = false;
+      boolean setMaxHitsTerminatedEarly = false;
+      List<QueryResult> queryResults = new ArrayList<>();
+      for (ResponseBuilder thisRb : crb.responseBuilders) {

Review Comment:
   TODO (future) parallelize



##########
solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java:
##########
@@ -0,0 +1,598 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.CursorMarkParams;
+import org.apache.solr.common.params.GroupParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.response.BasicResultContext;
+import org.apache.solr.response.ResultContext;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.DocListAndSet;
+import org.apache.solr.search.QueryResult;
+import org.apache.solr.search.SolrReturnFields;
+import org.apache.solr.search.SortSpec;
+import org.apache.solr.search.combine.QueryAndResponseCombiner;
+import org.apache.solr.search.combine.ReciprocalRankFusion;
+import org.apache.solr.util.SolrResponseUtil;
+import org.apache.solr.util.plugin.SolrCoreAware;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The CombinedQueryComponent class extends QueryComponent and provides 
support for executing
+ * multiple queries and combining their results.
+ */
+public class CombinedQueryComponent extends QueryComponent implements 
SolrCoreAware {
+
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  public static final String COMPONENT_NAME = "combined_query";
+  protected NamedList<?> initParams;
+  private final Map<String, QueryAndResponseCombiner> combiners = new 
HashMap<>();
+  private int maxCombinerQueries;
+
+  @Override
+  public void init(NamedList<?> args) {
+    super.init(args);
+    this.initParams = args;
+    this.maxCombinerQueries = CombinerParams.DEFAULT_MAX_COMBINER_QUERIES;
+  }
+
+  @Override
+  public void inform(SolrCore core) {
+    for (Map.Entry<String, ?> initEntry : initParams) {
+      if ("combiners".equals(initEntry.getKey())
+          && initEntry.getValue() instanceof NamedList<?> all) {
+        for (int i = 0; i < all.size(); i++) {
+          String name = all.getName(i);
+          NamedList<?> combinerConfig = (NamedList<?>) all.getVal(i);
+          String className = (String) combinerConfig.get("class");
+          QueryAndResponseCombiner combiner =
+              core.getResourceLoader().newInstance(className, 
QueryAndResponseCombiner.class);
+          combiner.init(combinerConfig);
+          combiners.compute(
+              name,
+              (k, existingCombiner) -> {
+                if (existingCombiner == null) {
+                  return combiner;
+                }
+                throw new SolrException(
+                    SolrException.ErrorCode.BAD_REQUEST,
+                    "Found more than one combiner with same name");
+              });
+        }
+      }
+    }
+    Object maxQueries = initParams.get("maxCombinerQueries");
+    if (maxQueries != null) {
+      this.maxCombinerQueries = Integer.parseInt(maxQueries.toString());
+    }
+    combiners.computeIfAbsent(
+        CombinerParams.RECIPROCAL_RANK_FUSION,
+        key -> {
+          ReciprocalRankFusion reciprocalRankFusion = new 
ReciprocalRankFusion();
+          reciprocalRankFusion.init(initParams);
+          return reciprocalRankFusion;
+        });
+  }
+
+  /**
+   * Overrides the prepare method to handle combined queries.
+   *
+   * @param rb the ResponseBuilder to prepare
+   * @throws IOException if an I/O error occurs during preparation
+   */
+  @Override
+  public void prepare(ResponseBuilder rb) throws IOException {
+    if (rb instanceof CombinedQueryResponseBuilder crb) {
+      SolrParams params = crb.req.getParams();
+      if (params.get(CursorMarkParams.CURSOR_MARK_PARAM) != null
+          || params.getBool(GroupParams.GROUP, false)) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Unsupported functionality 
for Combined Queries.");
+      }
+      String[] queriesToCombineKeys = 
params.getParams(CombinerParams.COMBINER_QUERY);
+      if (queriesToCombineKeys.length > maxCombinerQueries) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST,
+            "Too many queries to combine: limit is " + maxCombinerQueries);
+      }
+      for (String queryKey : queriesToCombineKeys) {
+        final var unparsedQuery = params.get(queryKey);
+        ResponseBuilder rbNew = new ResponseBuilder(rb.req, new 
SolrQueryResponse(), rb.components);
+        rbNew.setQueryString(unparsedQuery);
+        super.prepare(rbNew);

Review Comment:
   wouldn't we want to manipulate the sort spec so that we get all docs up to 
offset (AKA "start" param) + rows since RRF/combiner is going to want to see 
all docs/rankings up to offset+rows?  Otherwise our combiner is blind to the 
"offset" docs.  Assuming you agree, then we need to basically apply paging at 
this layer (our component) instead of letting the subquery do it.



##########
solr/core/src/test/org/apache/solr/handler/component/DistributedCombinedQueryComponentTest.java:
##########
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.solr.BaseDistributedSearchTestCase;
+import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonParams;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * The DistributedCombinedQueryComponentTest class is a JUnit test suite that 
evaluates the
+ * functionality of the CombinedQueryComponent in a Solr distributed search 
environment. It focuses
+ * on testing the integration of lexical and vector queries using the combiner 
component.
+ */
+@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
+public class DistributedCombinedQueryComponentTest extends 
BaseDistributedSearchTestCase {
+
+  private static final int NUM_DOCS = 10;
+  private static final String vectorField = "vector";
+
+  /**
+   * Sets up the test class by initializing the core and setting system 
properties. This method is
+   * executed before all test methods in the class.
+   *
+   * @throws Exception if any exception occurs during initialization
+   */
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    initCore("solrconfig-combined-query.xml", "schema-vector-catchall.xml");
+    System.setProperty("validateAfterInactivity", "200");
+    System.setProperty("solr.httpclient.retries", "0");
+    System.setProperty("distribUpdateSoTimeout", "5000");
+  }
+
+  /**
+   * Prepares Solr input documents for indexing, including adding sample data 
and vector fields.
+   * This method populates the Solr index with test data, including text, 
title, and vector fields.
+   * The vector fields are used to calculate cosine distance for testing 
purposes.
+   *
+   * @throws Exception if any error occurs during the indexing process.
+   */
+  private synchronized void prepareIndexDocs() throws Exception {
+    List<SolrInputDocument> docs = new ArrayList<>();
+    fixShardCount(2);
+    for (int i = 1; i <= NUM_DOCS; i++) {
+      SolrInputDocument doc = new SolrInputDocument();
+      doc.addField("id", Integer.toString(i));
+      doc.addField("text", "test text for doc " + i);
+      doc.addField("title", "title test for doc " + i);
+      doc.addField("mod3_idv", (i % 3));
+      docs.add(doc);
+    }
+    // cosine distance vector1= 1.0
+    docs.get(0).addField(vectorField, Arrays.asList(1f, 2f, 3f, 4f));
+    // cosine distance vector1= 0.998
+    docs.get(1).addField(vectorField, Arrays.asList(1.5f, 2.5f, 3.5f, 4.5f));
+    // cosine distance vector1= 0.992
+    docs.get(2).addField(vectorField, Arrays.asList(7.5f, 15.5f, 17.5f, 
22.5f));
+    // cosine distance vector1= 0.999
+    docs.get(3).addField(vectorField, Arrays.asList(1.4f, 2.4f, 3.4f, 4.4f));
+    // cosine distance vector1= 0.862
+    docs.get(4).addField(vectorField, Arrays.asList(30f, 22f, 35f, 20f));
+    // cosine distance vector1= 0.756
+    docs.get(5).addField(vectorField, Arrays.asList(40f, 1f, 1f, 200f));
+    // cosine distance vector1= 0.970
+    docs.get(6).addField(vectorField, Arrays.asList(5f, 10f, 20f, 40f));
+    // cosine distance vector1= 0.515
+    docs.get(7).addField(vectorField, Arrays.asList(120f, 60f, 30f, 15f));
+    // cosine distance vector1= 0.554
+    docs.get(8).addField(vectorField, Arrays.asList(200f, 50f, 100f, 25f));
+    // cosine distance vector1= 0.997
+    docs.get(9).addField(vectorField, Arrays.asList(1.8f, 2.5f, 3.7f, 4.9f));
+    del("*:*");
+    clients.sort(
+        (client1, client2) -> {
+          try {
+            if (client2 instanceof HttpSolrClient httpClient2
+                && client1 instanceof HttpSolrClient httpClient1)
+              return new URI(httpClient1.getBaseURL()).getPort()
+                  - new URI(httpClient2.getBaseURL()).getPort();
+          } catch (URISyntaxException e) {
+            throw new RuntimeException("Unable to get URI from SolrClient", e);
+          }
+          return 0;
+        });
+    for (SolrInputDocument doc : docs) {
+      indexDoc(doc);
+    }
+    commit();
+  }
+
+  /**
+   * Tests a single lexical query against the Solr server.
+   *
+   * @throws Exception if any exception occurs during the test execution
+   */
+  @Test
+  public void testSingleLexicalQuery() throws Exception {
+    prepareIndexDocs();
+    QueryResponse rsp;
+    rsp =
+        queryServer(
+            createParams(
+                CommonParams.JSON,
+                "{\"queries\":"
+                    + "{\"lexical1\":{\"lucene\":{\"query\":\"id:2^=10\"}}},"
+                    + "\"limit\":5,"
+                    + "\"fields\":[\"id\",\"score\",\"title\"],"
+                    + 
"\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\"]}}",
+                "shards",
+                getShardsString(),
+                CommonParams.QT,
+                "/search"));
+    assertEquals(1, rsp.getResults().size());
+    assertFieldValues(rsp.getResults(), id, "2");
+  }
+
+  @Override
+  protected String getShardsString() {
+    if (deadServers == null) return shards;
+    Arrays.sort(shardsArr);
+    StringBuilder sb = new StringBuilder();
+    for (String shard : shardsArr) {
+      if (!sb.isEmpty()) sb.append(',');
+      sb.append(shard);
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Tests multiple lexical queries using the Solr server.
+   *
+   * @throws Exception if any error occurs during the test execution
+   */
+  @Test
+  public void testMultipleLexicalQuery() throws Exception {
+    prepareIndexDocs();
+    QueryResponse rsp;
+    rsp =
+        queryServer(
+            createParams(
+                CommonParams.JSON,
+                "{\"queries\":"
+                    + "{\"lexical1\":{\"lucene\":{\"query\":\"title:title test 
for doc 1\"}},"
+                    + "\"lexical2\":{\"lucene\":{\"query\":\"text:test text 
for doc 2\"}}},"
+                    + "\"limit\":5,"
+                    + "\"fields\":[\"id\",\"score\",\"title\"],"
+                    + 
"\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
+                "shards",
+                getShardsString(),
+                CommonParams.QT,
+                "/search"));
+    assertEquals(5, rsp.getResults().size());
+    assertFieldValues(rsp.getResults(), id, "2", "1", "4", "5", "8");
+  }
+
+  /**
+   * Test multiple query execution with sort.
+   *
+   * @throws Exception the exception
+   */
+  @Test
+  public void testMultipleQueryWithSort() throws Exception {
+    prepareIndexDocs();
+    QueryResponse rsp;
+    rsp =
+        queryServer(
+            createParams(
+                CommonParams.JSON,
+                "{\"queries\":"
+                    + "{\"lexical1\":{\"lucene\":{\"query\":\"title:title test 
for doc 1\"}},"
+                    + "\"lexical2\":{\"lucene\":{\"query\":\"text:test text 
for doc 2\"}}},"
+                    + "\"limit\":5,\"sort\":\"mod3_idv desc\""
+                    + "\"fields\":[\"id\",\"score\",\"title\"],"
+                    + 
"\"params\":{\"combiner\":true,\"combiner.query\":[\"lexical1\",\"lexical2\"]}}",
+                "shards",
+                getShardsString(),
+                CommonParams.QT,
+                "/search"));
+    assertEquals(5, rsp.getResults().size());
+    assertFieldValues(rsp.getResults(), id, "2", "8", "5", "4", "1");
+  }
+
+  /**
+   * Tests the hybrid query functionality of the system.
+   *
+   * @throws Exception if any unexpected error occurs during the test 
execution.
+   */
+  @Test
+  public void testHybridQueryWithPagination() throws Exception {

Review Comment:
   This test (or another test?) should be more comprehensive so that it's 
easier to validate that pagination is in fact working.  I have doubts based on 
the code, and the test itself doesn't help at all.
   For example, first do the query without pagination (return ~5 docs).  Then 
(same test!), do again (same query) with pagination using an offset & limit 
that slice into the first request.  And maybe one more time; different page.  
It'll be straight-forward to reason about valid pagination results given the 
first set of results.
   As a reviewer I look at the current test as-is and I have no idea if it's 
working.
   I don't care if the test is "hybrid" or not, so long as there are 2 
sub-queries thus exercising typical logic.



##########
solr/core/src/java/org/apache/solr/handler/component/CombinedQueryComponent.java:
##########
@@ -0,0 +1,598 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.CursorMarkParams;
+import org.apache.solr.common.params.GroupParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.response.BasicResultContext;
+import org.apache.solr.response.ResultContext;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.DocListAndSet;
+import org.apache.solr.search.QueryResult;
+import org.apache.solr.search.SolrReturnFields;
+import org.apache.solr.search.SortSpec;
+import org.apache.solr.search.combine.QueryAndResponseCombiner;
+import org.apache.solr.search.combine.ReciprocalRankFusion;
+import org.apache.solr.util.SolrResponseUtil;
+import org.apache.solr.util.plugin.SolrCoreAware;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The CombinedQueryComponent class extends QueryComponent and provides 
support for executing
+ * multiple queries and combining their results.
+ */
+public class CombinedQueryComponent extends QueryComponent implements 
SolrCoreAware {
+
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  public static final String COMPONENT_NAME = "combined_query";
+  protected NamedList<?> initParams;
+  private final Map<String, QueryAndResponseCombiner> combiners = new 
HashMap<>();
+  private int maxCombinerQueries;
+
+  @Override
+  public void init(NamedList<?> args) {
+    super.init(args);
+    this.initParams = args;
+    this.maxCombinerQueries = CombinerParams.DEFAULT_MAX_COMBINER_QUERIES;
+  }
+
+  @Override
+  public void inform(SolrCore core) {
+    for (Map.Entry<String, ?> initEntry : initParams) {
+      if ("combiners".equals(initEntry.getKey())
+          && initEntry.getValue() instanceof NamedList<?> all) {
+        for (int i = 0; i < all.size(); i++) {
+          String name = all.getName(i);
+          NamedList<?> combinerConfig = (NamedList<?>) all.getVal(i);
+          String className = (String) combinerConfig.get("class");
+          QueryAndResponseCombiner combiner =
+              core.getResourceLoader().newInstance(className, 
QueryAndResponseCombiner.class);
+          combiner.init(combinerConfig);
+          combiners.compute(
+              name,
+              (k, existingCombiner) -> {
+                if (existingCombiner == null) {
+                  return combiner;
+                }
+                throw new SolrException(
+                    SolrException.ErrorCode.BAD_REQUEST,
+                    "Found more than one combiner with same name");
+              });
+        }
+      }
+    }
+    Object maxQueries = initParams.get("maxCombinerQueries");
+    if (maxQueries != null) {
+      this.maxCombinerQueries = Integer.parseInt(maxQueries.toString());
+    }
+    combiners.computeIfAbsent(
+        CombinerParams.RECIPROCAL_RANK_FUSION,
+        key -> {
+          ReciprocalRankFusion reciprocalRankFusion = new 
ReciprocalRankFusion();
+          reciprocalRankFusion.init(initParams);
+          return reciprocalRankFusion;
+        });
+  }
+
+  /**
+   * Overrides the prepare method to handle combined queries.
+   *
+   * @param rb the ResponseBuilder to prepare
+   * @throws IOException if an I/O error occurs during preparation
+   */
+  @Override
+  public void prepare(ResponseBuilder rb) throws IOException {
+    if (rb instanceof CombinedQueryResponseBuilder crb) {
+      SolrParams params = crb.req.getParams();
+      if (params.get(CursorMarkParams.CURSOR_MARK_PARAM) != null
+          || params.getBool(GroupParams.GROUP, false)) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Unsupported functionality 
for Combined Queries.");
+      }
+      String[] queriesToCombineKeys = 
params.getParams(CombinerParams.COMBINER_QUERY);
+      if (queriesToCombineKeys.length > maxCombinerQueries) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST,
+            "Too many queries to combine: limit is " + maxCombinerQueries);
+      }
+      for (String queryKey : queriesToCombineKeys) {
+        final var unparsedQuery = params.get(queryKey);
+        ResponseBuilder rbNew = new ResponseBuilder(rb.req, new 
SolrQueryResponse(), rb.components);
+        rbNew.setQueryString(unparsedQuery);
+        super.prepare(rbNew);
+        crb.setFilters(rbNew.getFilters());
+        crb.responseBuilders.add(rbNew);
+      }
+    }
+    super.prepare(rb);
+  }
+
+  /**
+   * Overrides the process method to handle CombinedQueryResponseBuilder 
instances. This method
+   * processes the responses from multiple queries in the SearchIndexer, 
combines them using the
+   * specified QueryAndResponseCombiner strategy, and sets the appropriate 
results and metadata in
+   * the CombinedQueryResponseBuilder.
+   *
+   * @param rb the ResponseBuilder object to process
+   * @throws IOException if an I/O error occurs during processing
+   */
+  @Override
+  public void process(ResponseBuilder rb) throws IOException {
+    if (rb instanceof CombinedQueryResponseBuilder crb) {
+      boolean partialResults = false;
+      boolean segmentTerminatedEarly = false;
+      boolean setMaxHitsTerminatedEarly = false;
+      List<QueryResult> queryResults = new ArrayList<>();
+      for (ResponseBuilder thisRb : crb.responseBuilders) {
+        // Just a placeholder for future implementation for Cursors
+        thisRb.setCursorMark(crb.getCursorMark());
+        super.process(thisRb);
+        DocListAndSet docListAndSet = thisRb.getResults();
+        QueryResult queryResult = new QueryResult();
+        queryResult.setDocListAndSet(docListAndSet);
+        queryResults.add(queryResult);
+        partialResults |= queryResult.isPartialResults();
+        if (queryResult.getSegmentTerminatedEarly() != null) {
+          segmentTerminatedEarly |= queryResult.getSegmentTerminatedEarly();
+        }
+        if (queryResult.getMaxHitsTerminatedEarly() != null) {
+          setMaxHitsTerminatedEarly |= queryResult.getMaxHitsTerminatedEarly();
+        }
+      }
+      prepareCombinedResponseBuilder(
+          rb, crb, queryResults, partialResults, segmentTerminatedEarly, 
setMaxHitsTerminatedEarly);
+      if (crb.mergeFieldHandler != null) {
+        crb.mergeFieldHandler.handleMergeFields(crb, crb.req.getSearcher());
+      } else {
+        doFieldSortValues(rb, crb.req.getSearcher());
+      }
+      doPrefetch(crb);
+    } else {
+      super.process(rb);
+    }
+  }
+
+  private void prepareCombinedResponseBuilder(
+      ResponseBuilder rb,
+      CombinedQueryResponseBuilder crb,
+      List<QueryResult> queryResults,
+      boolean partialResults,
+      boolean segmentTerminatedEarly,
+      boolean setMaxHitsTerminatedEarly)
+      throws IOException {
+    String algorithm =
+        rb.req.getParams().get(CombinerParams.COMBINER_ALGORITHM, 
CombinerParams.DEFAULT_COMBINER);
+    QueryAndResponseCombiner combinerStrategy =
+        QueryAndResponseCombiner.getImplementation(algorithm, combiners);
+    QueryResult combinedQueryResult = combinerStrategy.combine(queryResults, 
rb.req.getParams());
+    combinedQueryResult.setPartialResults(partialResults);
+    combinedQueryResult.setSegmentTerminatedEarly(segmentTerminatedEarly);
+    combinedQueryResult.setMaxHitsTerminatedEarly(setMaxHitsTerminatedEarly);
+    crb.setResult(combinedQueryResult);
+    if (rb.isDebug()) {

Review Comment:
   I think you should specifically look at `isDebugResults()`



##########
solr/core/src/java/org/apache/solr/search/combine/ReciprocalRankFusion.java:
##########
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.search.combine;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TotalHits;
+import org.apache.solr.common.params.CombinerParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.handler.component.ShardDoc;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.DocIterator;
+import org.apache.solr.search.DocList;
+import org.apache.solr.search.DocListAndSet;
+import org.apache.solr.search.DocSet;
+import org.apache.solr.search.DocSlice;
+import org.apache.solr.search.QueryResult;
+import org.apache.solr.search.SolrDocumentFetcher;
+import org.apache.solr.search.SolrIndexSearcher;
+
+/**
+ * This class implements a query and response combiner that uses the 
Reciprocal Rank Fusion (RRF)
+ * algorithm to combine multiple ranked lists into a single ranked list.
+ */
+public class ReciprocalRankFusion extends QueryAndResponseCombiner {
+
+  private int k;
+
+  public ReciprocalRankFusion() {
+    this.k = CombinerParams.DEFAULT_COMBINER_RRF_K;
+  }
+
+  @Override
+  public void init(NamedList<?> args) {
+    Object kParam = args.get("k");
+    if (kParam != null) {
+      this.k = Integer.parseInt(kParam.toString());
+    }
+  }
+
+  public int getK() {
+    return k;
+  }
+
+  @Override
+  public QueryResult combine(List<QueryResult> rankedLists, SolrParams 
solrParams) {
+    int kVal = solrParams.getInt(CombinerParams.COMBINER_RRF_K, this.k);
+    List<DocListAndSet> docListAndSet = 
getDocListsAndSetFromQueryResults(rankedLists);
+    QueryResult combinedResult = new QueryResult();
+    combineResults(combinedResult, docListAndSet, false, kVal);
+    return combinedResult;
+  }
+
+  private static List<DocListAndSet> getDocListsAndSetFromQueryResults(
+      List<QueryResult> rankedLists) {
+    List<DocListAndSet> docLists = new ArrayList<>(rankedLists.size());
+    for (QueryResult rankedList : rankedLists) {
+      docLists.add(rankedList.getDocListAndSet());
+    }
+    return docLists;
+  }
+
+  @Override
+  public List<ShardDoc> combine(Map<String, List<ShardDoc>> shardDocMap, 
SolrParams solrParams) {

Review Comment:
   could use some javadocs to explain what we're doing here.  AFAIKT, this is 
changing the score of the docs to be the RRF score.  The order appears to be 
interleaved since the score gets reset for each list, and because doc IDs are 
unique per shard.  This is very non-obvious reading the code.



##########
solr/core/src/java/org/apache/solr/search/combine/QueryAndResponseCombiner.java:
##########
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.search.combine;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Query;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.handler.component.ShardDoc;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.QueryResult;
+import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.util.plugin.NamedListInitializedPlugin;
+
+/**
+ * The QueryAndResponseCombiner class is an abstract base class for combining 
query results and
+ * shard documents. It provides a framework for different algorithms to be 
implemented for merging
+ * ranked lists and shard documents.
+ */
+public abstract class QueryAndResponseCombiner implements 
NamedListInitializedPlugin {
+  /**
+   * Combines multiple ranked lists into a single QueryResult.
+   *
+   * @param rankedLists a list of ranked lists to be combined
+   * @param solrParams params to be used when provided at query time
+   * @return a new QueryResult containing the combined results
+   * @throws IllegalArgumentException if the input list is empty
+   */
+  public abstract QueryResult combine(List<QueryResult> rankedLists, 
SolrParams solrParams);
+
+  /**
+   * Combines shard documents based on the provided map.
+   *
+   * @param shardDocMap a map where keys represent shard IDs and values are 
lists of ShardDocs for
+   *     each shard
+   * @param solrParams params to be used when provided at query time
+   * @return a combined list of ShardDocs from all shards
+   */
+  public abstract List<ShardDoc> combine(
+      Map<String, List<ShardDoc>> shardDocMap, SolrParams solrParams);
+
+  /**
+   * Retrieves a list of explanations for the given queries and results.
+   *
+   * @param queryKeys the keys associated with the queries
+   * @param queries the list of queries for which explanations are requested
+   * @param queryResult the list of QueryResult corresponding to each query
+   * @param searcher the SolrIndexSearcher used to perform the search
+   * @param schema the IndexSchema used to interpret the search results
+   * @param solrParams params to be used when provided at query time
+   * @return a list of explanations for the given queries and results
+   * @throws IOException if an I/O error occurs during the explanation 
retrieval process
+   */
+  public abstract NamedList<Explanation> getExplanations(

Review Comment:
   Should be a SimpleOrderedMap if the keys are unique.  Presumably "queryKeys" 
are unique, albeit your docs don't say if its so



##########
solr/core/src/test/org/apache/solr/handler/component/CombinedQueryComponentTest.java:
##########
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.component;
+
+import static org.apache.solr.common.params.CursorMarkParams.CURSOR_MARK_START;
+
+import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonParams;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * The CombinedQueryComponentTest class is a unit test suite for the 
CombinedQueryComponent in Solr.
+ * It verifies the functionality of the component by performing various 
queries and validating the
+ * responses.
+ */
+@ThreadLeakScope(ThreadLeakScope.Scope.NONE)

Review Comment:
   why this threadLeakScope?  Javadoc says it's highly discouraged, which I 
believe.
   It's also on the other test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to