This is an automated email from the ASF dual-hosted git repository.
andywebb pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 2d1ea60432c SOLR-17923: Add fullOuterJoin stream function (#3676)
2d1ea60432c is described below
commit 2d1ea60432c4ba4f6adb05bc507ce057b3d2f281
Author: Andy Webb <[email protected]>
AuthorDate: Mon Oct 13 17:58:18 2025 +0100
SOLR-17923: Add fullOuterJoin stream function (#3676)
---
solr/CHANGES.txt | 2 +
.../pages/stream-decorator-reference.adoc | 196 ++++++++++++++++-----
.../java/org/apache/solr/client/solrj/io/Lang.java | 2 +
.../solrj/io/stream/FullOuterJoinStream.java | 126 +++++++++++++
.../org/apache/solr/client/solrj/io/TestLang.java | 1 +
.../solrj/io/stream/StreamDecoratorTest.java | 158 +++++++++++++++++
6 files changed, 443 insertions(+), 42 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 88071ca526b..130b99e6f40 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -9,6 +9,8 @@ New Features
---------------------
* SOLR-17915: shards.preference=replica.location now supports the "host"
option for routing to replicas on the same host. (Houston Putman)
+* SOLR-17923: Add fullOuterJoin stream function (Andy Webb)
+
Improvements
---------------------
* SOLR-17860: DocBasedVersionConstraintsProcessorFactory now supports PULL
replicas. (Houston Putman)
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
index 608ee65b06c..03740f8f1d9 100644
---
a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
+++
b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
@@ -786,73 +786,42 @@ fetch(addresses,
The example above fetches addresses for users by matching the username in the
tuple with the userId field in the addresses collection.
-== having
-
-The `having` expression wraps a stream and applies a boolean operation to each
tuple.
-It emits only tuples for which the boolean operation returns *true*.
-
-=== having Parameters
-
-* `StreamExpression`: (Mandatory) The stream source for the having function.
-* `booleanEvaluator`: (Mandatory) The following boolean operations are
supported: `eq` (equals), `gt` (greater than), `lt` (less than), `gteq`
(greater than or equal to), `lteq` (less than or equal to), `and`, `or`, `eor`
(exclusive or), and `not`.
-Boolean evaluators can be nested with other evaluators to form complex boolean
logic.
-
-The comparison evaluators compare the value in a specific field with a value,
whether a string, number, or boolean.
-For example: `eq(field1, 10)`, returns `true` if `field1` is equal to 10.
-
-=== having Syntax
-
-[source,text]
-----
-having(rollup(over=a_s,
- sum(a_i),
- search(collection1,
- q="*:*",
- qt="/export",
- fl="id,a_s,a_i,a_f",
- sort="a_s asc")),
- and(gt(sum(a_i), 100), lt(sum(a_i), 110)))
-
-----
-
-In this example, the `having` expression iterates the aggregated tuples from
the `rollup` expression and emits all tuples where the field `sum(a_i)` is
greater than 100 and less than 110.
-
-== leftOuterJoin
+== fullOuterJoin
-The `leftOuterJoin` function wraps two streams, Left and Right, and emits
tuples from Left.
-If there is a tuple in Right equal (as defined by `on`) then the values in
that tuple will be included in the emitted tuple.
-An equal tuple in Right *need not* exist for the Left tuple to be emitted.
-This supports one-to-one, one-to-many, many-to-one, and many-to-many left
outer join scenarios.
-The tuples are emitted in the order in which they appear in the Left stream.
+The `fullOuterJoin` function wraps two streams, Left and Right, and emits
tuples from both.
+If there is a tuple in Right equal to that in the Left (as defined by `on`)
then the values in that tuple will be included in the emitted tuple.
+An equal tuple in one stream *need not* exist for a tuple in the other to be
emitted.
+This supports one-to-one, one-to-many, many-to-one, and many-to-many full
outer join scenarios.
+The tuples are emitted in the order in which they appear in the streams.
Both streams must be sorted by the fields being used to determine equality
(using the `on` parameter).
If both tuples contain a field of the same name then the value from the Right
stream will be used in the emitted tuple.
You can wrap the incoming streams with a `select` function to be specific
about which field values are included in the emitted tuple.
-=== leftOuterJoin Parameters
+=== fullOuterJoin Parameters
* `StreamExpression for StreamLeft`
* `StreamExpression for StreamRight`
* `on`: Fields to be used for checking equality of tuples between Left and
Right.
Can be of the format `on="fieldName"`,
`on="fieldNameInLeft=fieldNameInRight"`, or `on="fieldName,
otherFieldName=rightOtherFieldName"`.
-=== leftOuterJoin Syntax
+=== fullOuterJoin Syntax
[source,text]
----
-leftOuterJoin(
+fullOuterJoin(
search(people, q="*:*", qt="/export", fl="personId,name", sort="personId
asc"),
search(pets, q="type:cat", qt="/export", fl="personId,petName",
sort="personId asc"),
on="personId"
)
-leftOuterJoin(
+fullOuterJoin(
search(people, q="*:*", qt="/export", fl="personId,name", sort="personId
asc"),
search(pets, q="type:cat", qt="/export", fl="ownerId,petName", sort="ownerId
asc"),
on="personId=ownerId"
)
-leftOuterJoin(
+fullOuterJoin(
search(people, q="*:*", qt="/export", fl="personId,name", sort="personId
asc"),
select(
search(pets, q="type:cat", qt="/export", fl="ownerId,name", sort="ownerId
asc"),
@@ -863,6 +832,103 @@ leftOuterJoin(
)
----
+=== Reciprocal Rank Fusion (RRF) using fullOuterJoin
+
+The `fullOuterJoin` function can be used to construct an RRF algorithm for
merging together result sets, as illustrated below.
+
+(In a real-world example the two `sort/list/tuple` blocks would likely use
`search` instead.)
+
+[source,text]
+----
+top(
+ n=10,
+ sort(
+ select(
+ fullOuterJoin(
+ sort(
+ select(
+ sort(
+ list(
+ tuple(id=1, title="L 1", left="a", score=4.5),
+ tuple(id=2, title="L 2", left="b", score=3.5),
+ tuple(id=3, title="L 3", left="c", score=2.5),
+ tuple(id=4, title="L 4", left="d", score=2.5),
+ tuple(id=5, title="L 5", left="e", score=2.5),
+ tuple(id=6, title="L 6", left="f", score=2.5),
+ ),
+ by="score desc"
+ ),
+ *,
+ score as scoreL,
+ add(recNum(),1) as rankL,
+ div(1,add(rankL,60)) as rrL
+ ),
+ by="id asc"
+ ),
+ sort(
+ select(
+ sort(
+ list(
+ tuple(id=3, title="R 3", right="g", score=0.9),
+ tuple(id=2, title="R 2", right="h", score=0.8),
+ tuple(id=4, title="R 4", right="i", score=0.7),
+ tuple(id=7, title="R 7", right="j", score=0.6),
+ tuple(id=8, title="R 8", right="k", score=0.5),
+ tuple(id=9, title="R 9", right="l", score=0.5),
+ ),
+ by="score desc"
+ ),
+ *,
+ score as scoreR,
+ add(recNum(),1) as rankR,
+ div(1,add(rankR,60)) as rrR
+ ),
+ by="id asc"
+ ),
+ on="id"
+ ),
+ *,
+ replace(rrL,null,withValue=0),
+ replace(rrR,null,withValue=0),
+ add(rrL,rrR) as rrf,
+ ),
+ by="rrf desc"
+ ),
+ sort="rrf desc"
+)
+----
+
+== having
+
+The `having` expression wraps a stream and applies a boolean operation to each
tuple.
+It emits only tuples for which the boolean operation returns *true*.
+
+=== having Parameters
+
+* `StreamExpression`: (Mandatory) The stream source for the having function.
+* `booleanEvaluator`: (Mandatory) The following boolean operations are
supported: `eq` (equals), `gt` (greater than), `lt` (less than), `gteq`
(greater than or equal to), `lteq` (less than or equal to), `and`, `or`, `eor`
(exclusive or), and `not`.
+Boolean evaluators can be nested with other evaluators to form complex boolean
logic.
+
+The comparison evaluators compare the value in a specific field with a value,
whether a string, number, or boolean.
+For example: `eq(field1, 10)`, returns `true` if `field1` is equal to 10.
+
+=== having Syntax
+
+[source,text]
+----
+having(rollup(over=a_s,
+ sum(a_i),
+ search(collection1,
+ q="*:*",
+ qt="/export",
+ fl="id,a_s,a_i,a_f",
+ sort="a_s asc")),
+ and(gt(sum(a_i), 100), lt(sum(a_i), 110)))
+
+----
+
+In this example, the `having` expression iterates the aggregated tuples from
the `rollup` expression and emits all tuples where the field `sum(a_i)` is
greater than 100 and less than 110.
+
== hashJoin
The `hashJoin` function wraps two streams, Left and Right, and for every tuple
in Left which exists in Right will emit a tuple containing the fields of both
tuples.
@@ -986,6 +1052,52 @@ intersect(
)
----
+== leftOuterJoin
+
+The `leftOuterJoin` function wraps two streams, Left and Right, and emits
tuples from Left.
+If there is a tuple in Right equal (as defined by `on`) then the values in
that tuple will be included in the emitted tuple.
+An equal tuple in Right *need not* exist for the Left tuple to be emitted.
+This supports one-to-one, one-to-many, many-to-one, and many-to-many left
outer join scenarios.
+The tuples are emitted in the order in which they appear in the Left stream.
+Both streams must be sorted by the fields being used to determine equality
(using the `on` parameter).
+If both tuples contain a field of the same name then the value from the Right
stream will be used in the emitted tuple.
+
+You can wrap the incoming streams with a `select` function to be specific
about which field values are included in the emitted tuple.
+
+=== leftOuterJoin Parameters
+
+* `StreamExpression for StreamLeft`
+* `StreamExpression for StreamRight`
+* `on`: Fields to be used for checking equality of tuples between Left and
Right.
+Can be of the format `on="fieldName"`,
`on="fieldNameInLeft=fieldNameInRight"`, or `on="fieldName,
otherFieldName=rightOtherFieldName"`.
+
+=== leftOuterJoin Syntax
+
+[source,text]
+----
+leftOuterJoin(
+ search(people, q="*:*", qt="/export", fl="personId,name", sort="personId
asc"),
+ search(pets, q="type:cat", qt="/export", fl="personId,petName",
sort="personId asc"),
+ on="personId"
+)
+
+leftOuterJoin(
+ search(people, q="*:*", qt="/export", fl="personId,name", sort="personId
asc"),
+ search(pets, q="type:cat", qt="/export", fl="ownerId,petName", sort="ownerId
asc"),
+ on="personId=ownerId"
+)
+
+leftOuterJoin(
+ search(people, q="*:*", qt="/export", fl="personId,name", sort="personId
asc"),
+ select(
+ search(pets, q="type:cat", qt="/export", fl="ownerId,name", sort="ownerId
asc"),
+ ownerId,
+ name as petName
+ ),
+ on="personId=ownerId"
+)
+----
+
[#list_expression]
== list
diff --git
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java
index 4da6766cb44..927fb1eef5d 100644
--- a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java
+++ b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/Lang.java
@@ -272,6 +272,7 @@ import org.apache.solr.client.solrj.io.stream.Facet2DStream;
import org.apache.solr.client.solrj.io.stream.FacetStream;
import org.apache.solr.client.solrj.io.stream.FeaturesSelectionStream;
import org.apache.solr.client.solrj.io.stream.FetchStream;
+import org.apache.solr.client.solrj.io.stream.FullOuterJoinStream;
import org.apache.solr.client.solrj.io.stream.GetStream;
import org.apache.solr.client.solrj.io.stream.HashJoinStream;
import org.apache.solr.client.solrj.io.stream.HashRollupStream;
@@ -355,6 +356,7 @@ public class Lang {
.withFunctionName("stats", StatsStream.class)
.withFunctionName("innerJoin", InnerJoinStream.class)
.withFunctionName("leftOuterJoin", LeftOuterJoinStream.class)
+ .withFunctionName("fullOuterJoin", FullOuterJoinStream.class)
.withFunctionName("hashJoin", HashJoinStream.class)
.withFunctionName("outerHashJoin", OuterHashJoinStream.class)
.withFunctionName("intersect", IntersectStream.class)
diff --git
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/FullOuterJoinStream.java
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/FullOuterJoinStream.java
new file mode 100644
index 00000000000..4dc9d5ed432
--- /dev/null
+++
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/FullOuterJoinStream.java
@@ -0,0 +1,126 @@
+/*
+ * 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.client.solrj.io.stream;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import org.apache.solr.client.solrj.io.Tuple;
+import org.apache.solr.client.solrj.io.comp.StreamComparator;
+import org.apache.solr.client.solrj.io.eq.StreamEqualitor;
+import org.apache.solr.client.solrj.io.stream.expr.StreamExpression;
+import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
+
+/**
+ * Joins leftStream with rightStream based on an Equalitor. Both streams must
be sorted by the
+ * fields being joined on. Resulting stream is sorted by the equalitor.
+ *
+ * @since 9.10.0
+ */
+public class FullOuterJoinStream extends BiJoinStream {
+
+ @SuppressWarnings("JdkObsolete")
+ private final LinkedList<Tuple> joinedTuples = new LinkedList<>();
+
+ @SuppressWarnings("JdkObsolete")
+ private final LinkedList<Tuple> leftTupleGroup = new LinkedList<>();
+
+ @SuppressWarnings("JdkObsolete")
+ private final LinkedList<Tuple> rightTupleGroup = new LinkedList<>();
+
+ public FullOuterJoinStream(TupleStream leftStream, TupleStream rightStream,
StreamEqualitor eq)
+ throws IOException {
+ super(leftStream, rightStream, eq);
+ }
+
+ public FullOuterJoinStream(StreamExpression expression, StreamFactory
factory)
+ throws IOException {
+ super(expression, factory);
+ }
+
+ @Override
+ public Tuple read() throws IOException {
+ // if we've already figured out the next joined tuple then just return it
+ if (joinedTuples.size() > 0) {
+ return joinedTuples.removeFirst();
+ }
+
+ // keep going until we find something to return or both streams are empty
+ while (true) {
+
+ // load next set of equal tuples from leftStream into leftTupleGroup
+ if (0 == leftTupleGroup.size()) {
+ loadEqualTupleGroup(leftStream, leftTupleGroup, leftStreamComparator);
+ }
+
+ // same for right
+ if (0 == rightTupleGroup.size()) {
+ loadEqualTupleGroup(rightStream, rightTupleGroup,
rightStreamComparator);
+ }
+
+ Boolean leftFinished = (0 == leftTupleGroup.size() ||
leftTupleGroup.get(0).EOF);
+ Boolean rightFinished = (0 == rightTupleGroup.size() ||
rightTupleGroup.get(0).EOF);
+
+ // If both streams are at EOF, we're done
+ if (leftFinished && rightFinished) {
+ return Tuple.EOF();
+ }
+
+ // If the left stream is at the EOF, we just return the next element
from the right stream
+ if (leftFinished) {
+ return rightTupleGroup.removeFirst();
+ }
+
+ // If the right stream is at the EOF, we just return the next element
from the left stream
+ if (rightFinished) {
+ return leftTupleGroup.removeFirst();
+ }
+
+ // At this point we know both left and right groups have at least 1
member
+ if (eq.test(leftTupleGroup.get(0), rightTupleGroup.get(0))) {
+ // The groups are equal. Join em together and build the joinedTuples
+ for (Tuple left : leftTupleGroup) {
+ for (Tuple right : rightTupleGroup) {
+ Tuple clone = left.clone();
+ clone.merge(right);
+ joinedTuples.add(clone);
+ }
+ }
+
+ // Cause each to advance next time we need to look
+ leftTupleGroup.clear();
+ rightTupleGroup.clear();
+
+ return joinedTuples.removeFirst();
+ } else {
+ int c = iterationComparator.compare(leftTupleGroup.get(0),
rightTupleGroup.get(0));
+ if (c < 0) {
+ // If there's no match, we still advance the left stream while
returning every element.
+ // Because it's an outer join we still return the left tuple if no
match on right.
+ return leftTupleGroup.removeFirst();
+ } else {
+ // return right item as it didn't match left and this is a full
outer join
+ return rightTupleGroup.removeFirst();
+ }
+ }
+ }
+ }
+
+ @Override
+ public StreamComparator getStreamSort() {
+ return iterationComparator;
+ }
+}
diff --git
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java
index b405c8733e8..3a6f58580ef 100644
---
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java
+++
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/TestLang.java
@@ -61,6 +61,7 @@ public class TestLang extends SolrTestCase {
"stats",
"innerJoin",
"leftOuterJoin",
+ "fullOuterJoin",
"hashJoin",
"outerHashJoin",
"intersect",
diff --git
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamDecoratorTest.java
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamDecoratorTest.java
index f90274409cb..f31c11a43b4 100644
---
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamDecoratorTest.java
+++
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamDecoratorTest.java
@@ -2519,6 +2519,164 @@ public class StreamDecoratorTest extends
SolrCloudTestCase {
tuples = getTuples(stream);
assertEquals(10, tuples.size());
assertOrder(tuples, 1, 1, 15, 15, 2, 3, 4, 5, 6, 7);
+
+ // Basic mixed order, with id in right (compare fullOuterJoin ordering)
+ expression =
+ StreamExpressionParser.parse(
+ "leftOuterJoin("
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:left\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i desc, join2_s asc, id desc\"),"
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:right\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i desc, join2_s asc, id desc\"),"
+ + "on=\"join1_i, join2_s\")");
+ stream = new LeftOuterJoinStream(expression, factory);
+ stream.setStreamContext(streamContext);
+ tuples = getTuples(stream);
+ assertEquals(10, tuples.size());
+ assertOrder(tuples, 14, 6, 10, 11, 12, 9, 8, 9, 8, 2);
+
+ } finally {
+ solrClientCache.close();
+ }
+ }
+
+ @Test
+ public void testFullOuterJoinStream() throws Exception {
+
+ new UpdateRequest()
+ .add(id, "1", "side_s", "left", "join1_i", "0", "join2_s", "a",
"ident_s", "left_1") // 8, 9
+ .add(
+ id, "15", "side_s", "left", "join1_i", "0", "join2_s", "a",
"ident_s", "left_1") // 8, 9
+ .add(id, "2", "side_s", "left", "join1_i", "0", "join2_s", "b",
"ident_s", "left_2")
+ .add(id, "3", "side_s", "left", "join1_i", "1", "join2_s", "a",
"ident_s", "left_3") // 10
+ .add(id, "4", "side_s", "left", "join1_i", "1", "join2_s", "b",
"ident_s", "left_4") // 11
+ .add(id, "5", "side_s", "left", "join1_i", "1", "join2_s", "c",
"ident_s", "left_5") // 12
+ .add(id, "6", "side_s", "left", "join1_i", "2", "join2_s", "d",
"ident_s", "left_6")
+ .add(id, "7", "side_s", "left", "join1_i", "3", "join2_s", "e",
"ident_s", "left_7") // 14
+ .add(
+ id, "8", "side_s", "right", "join1_i", "0", "join2_s", "a",
"ident_s", "right_1",
+ "join3_i", "0") // 1,15
+ .add(
+ id, "9", "side_s", "right", "join1_i", "0", "join2_s", "a",
"ident_s", "right_2",
+ "join3_i", "0") // 1,15
+ .add(
+ id, "10", "side_s", "right", "join1_i", "1", "join2_s", "a",
"ident_s", "right_3",
+ "join3_i", "1") // 3
+ .add(
+ id, "11", "side_s", "right", "join1_i", "1", "join2_s", "b",
"ident_s", "right_4",
+ "join3_i", "1") // 4
+ .add(
+ id, "12", "side_s", "right", "join1_i", "1", "join2_s", "c",
"ident_s", "right_5",
+ "join3_i", "1") // 5
+ .add(
+ id, "13", "side_s", "right", "join1_i", "2", "join2_s", "dad",
"ident_s", "right_6",
+ "join3_i", "2")
+ .add(
+ id, "14", "side_s", "right", "join1_i", "3", "join2_s", "e",
"ident_s", "right_7",
+ "join3_i", "3") // 7
+ .commit(cluster.getSolrClient(), COLLECTIONORALIAS);
+
+ StreamExpression expression;
+ TupleStream stream;
+ List<Tuple> tuples;
+ StreamContext streamContext = new StreamContext();
+ SolrClientCache solrClientCache = new SolrClientCache();
+ streamContext.setSolrClientCache(solrClientCache);
+
+ StreamFactory factory =
+ new StreamFactory()
+ .withCollectionZkHost(COLLECTIONORALIAS,
cluster.getZkServer().getZkAddress())
+ .withFunctionName("search", CloudSolrStream.class)
+ .withFunctionName("fullOuterJoin", FullOuterJoinStream.class);
+
+ // Basic test
+ try {
+ expression =
+ StreamExpressionParser.parse(
+ "fullOuterJoin("
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:left\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i asc, join2_s asc, id asc\"),"
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:right\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i asc, join2_s asc, id asc\"),"
+ + "on=\"join1_i=join1_i, join2_s=join2_s\")");
+ stream = new FullOuterJoinStream(expression, factory);
+ stream.setStreamContext(streamContext);
+ tuples = getTuples(stream);
+ assertEquals(11, tuples.size());
+ assertOrder(tuples, 8, 9, 8, 9, 2, 10, 11, 12, 6, 13, 14);
+
+ // Basic desc
+ expression =
+ StreamExpressionParser.parse(
+ "fullOuterJoin("
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:left\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i desc, join2_s asc\"),"
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:right\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i desc, join2_s asc\"),"
+ + "on=\"join1_i, join2_s\")");
+ stream = new FullOuterJoinStream(expression, factory);
+ stream.setStreamContext(streamContext);
+ tuples = getTuples(stream);
+ assertEquals(11, tuples.size());
+ assertOrder(tuples, 14, 6, 13, 10, 11, 12, 9, 8, 9, 8, 2);
+
+ // Results in both searches, no join matches
+ expression =
+ StreamExpressionParser.parse(
+ "fullOuterJoin("
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:left\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"ident_s asc\"),"
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:right\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"ident_s asc\", aliases=\"ident_s=right_ident_s\"),"
+ + "on=\"ident_s=right_ident_s\")");
+ stream = new FullOuterJoinStream(expression, factory);
+ stream.setStreamContext(streamContext);
+ tuples = getTuples(stream);
+ assertEquals(15, tuples.size());
+ assertOrder(tuples, 1, 15, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
+
+ // Differing field names
+ expression =
+ StreamExpressionParser.parse(
+ "fullOuterJoin("
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:left\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i asc, join2_s asc, id asc\"),"
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:right\", fl=\"id,join3_i,join2_s,ident_s\",
sort=\"join3_i asc, join2_s asc, id asc\", aliases=\"join3_i=aliasesField\"),"
+ + "on=\"join1_i=aliasesField, join2_s=join2_s\")");
+ stream = new FullOuterJoinStream(expression, factory);
+ stream.setStreamContext(streamContext);
+ tuples = getTuples(stream);
+ assertEquals(11, tuples.size());
+ assertOrder(tuples, 8, 9, 8, 9, 2, 10, 11, 12, 6, 13, 14);
+
+ // Basic mixed order, with id in right (compare leftOuterJoin order
above)
+ expression =
+ StreamExpressionParser.parse(
+ "fullOuterJoin("
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:left\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i desc, join2_s asc, id desc\"),"
+ + "search("
+ + COLLECTIONORALIAS
+ + ", q=\"side_s:right\", fl=\"id,join1_i,join2_s,ident_s\",
sort=\"join1_i desc, join2_s asc, id desc\"),"
+ + "on=\"join1_i, join2_s\")");
+ stream = new FullOuterJoinStream(expression, factory);
+ stream.setStreamContext(streamContext);
+ tuples = getTuples(stream);
+ assertEquals(11, tuples.size());
+ assertOrder(tuples, 14, 6, 13, 10, 11, 12, 9, 8, 9, 8, 2);
+
} finally {
solrClientCache.close();
}