options) {
+ this(new FunctionExpression(name, params, options));
+ }
+
@Override
Value toProto() {
return expr.toProto();
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
index 695051c208..89cc366b0d 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
@@ -120,7 +120,7 @@ public static Expression constant(Timestamp value) {
*/
@BetaApi
public static BooleanExpression constant(Boolean value) {
- return equal(new Constant(value), true);
+ return new BooleanConstant(new Constant(value));
}
/**
@@ -3390,6 +3390,155 @@ public static BooleanExpression isError(Expression expr) {
return new BooleanFunctionExpression("is_error", expr);
}
+ /**
+ * Evaluates to the distance in meters between the location in the specified field and the query
+ * location.
+ *
+ * This Expression can only be used within a {@code Search} stage.
+ *
+ * @param fieldName Specifies the field in the document which contains the first {@link GeoPoint}
+ * for distance computation.
+ * @param location Compute distance to this {@link GeoPoint}.
+ * @return A new {@link Expression} representing the geoDistance operation.
+ */
+ @BetaApi
+ public static Expression geoDistance(String fieldName, GeoPoint location) {
+ return geoDistance(field(fieldName), location);
+ }
+
+ /**
+ * Evaluates to the distance in meters between the location in the specified field and the query
+ * location.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param field Specifies the field in the document which contains the first {@link GeoPoint} for
+ * distance computation.
+ * @param location Compute distance to this {@link GeoPoint}.
+ * @return A new {@link Expression} representing the geoDistance operation.
+ */
+ @BetaApi
+ public static Expression geoDistance(Field field, GeoPoint location) {
+ return new FunctionExpression("geo_distance", java.util.Arrays.asList(field, constant(location)));
+ }
+
+ /**
+ * Perform a full-text search on all indexed search fields in the document.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param rquery Define the search query using the search DTS.
+ * @return A new {@link BooleanExpression} representing the documentMatches operation.
+ */
+ @BetaApi
+ public static BooleanExpression documentMatches(String rquery) {
+ return new BooleanFunctionExpression("document_matches", constant(rquery));
+ }
+
+ /**
+ * Perform a full-text search on the specified field.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param fieldName Perform search on this field.
+ * @param rquery Define the search query using the rquery DTS.
+ */
+ @InternalApi
+ static BooleanExpression matches(String fieldName, String rquery) {
+ return matches(field(fieldName), rquery);
+ }
+
+ /**
+ * Perform a full-text search on the specified field.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param field Perform search on this field.
+ * @param rquery Define the search query using the rquery DTS.
+ */
+ @InternalApi
+ static BooleanExpression matches(Field field, String rquery) {
+ return new BooleanFunctionExpression("matches", field, constant(rquery));
+ }
+
+ /**
+ * Evaluates to the search score that reflects the topicality of the document to all of the text
+ * predicates ({@code matches} and {@code documentMatches}) in the search query.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @return A new {@link Expression} representing the score operation.
+ */
+ @BetaApi
+ public static Expression score() {
+ return new FunctionExpression("score", com.google.common.collect.ImmutableList.of());
+ }
+
+ /**
+ * Evaluates to an HTML-formatted text snippet that highlights terms matching the search query in
+ * {@code bold}.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param fieldName Search the specified field for matching terms.
+ * @param rquery Define the search query using the search DTS.
+ * @return A new {@link Expression} representing the snippet operation.
+ */
+ @BetaApi
+ public static Expression snippet(String fieldName, String rquery) {
+ return new FunctionExpression(
+ "snippet", java.util.Arrays.asList(field(fieldName), constant(rquery)));
+ }
+
+ /**
+ * Evaluates to an HTML-formatted text snippet that highlights terms matching the search query in
+ * {@code bold}.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param rquery Define the search query using the search DTS.
+ * @return A new {@link Expression} representing the snippet operation.
+ */
+ @BetaApi
+ public final Expression snippet(String rquery) {
+ return new FunctionExpression(
+ "snippet",
+ java.util.Arrays.asList(this, constant(rquery)),
+ java.util.Collections.singletonMap(
+ "query", com.google.cloud.firestore.PipelineUtils.encodeValue(rquery)));
+ }
+
+ @InternalApi
+ static BooleanExpression between(String fieldName, Expression lowerBound, Expression upperBound) {
+ return between(field(fieldName), lowerBound, upperBound);
+ }
+
+ @InternalApi
+ static BooleanExpression between(String fieldName, Object lowerBound, Object upperBound) {
+ return between(fieldName, toExprOrConstant(lowerBound), toExprOrConstant(upperBound));
+ }
+
+ @InternalApi
+ static BooleanExpression between(
+ Expression expression, Expression lowerBound, Expression upperBound) {
+ return new BooleanFunctionExpression("between", expression, lowerBound, upperBound);
+ }
+
+ @InternalApi
+ static BooleanExpression between(Expression expression, Object lowerBound, Object upperBound) {
+ return between(expression, toExprOrConstant(lowerBound), toExprOrConstant(upperBound));
+ }
+
+ @InternalApi
+ public final BooleanExpression between(Expression lowerBound, Expression upperBound) {
+ return Expression.between(this, lowerBound, upperBound);
+ }
+
+ @InternalApi
+ public final BooleanExpression between(Object lowerBound, Object upperBound) {
+ return Expression.between(this, lowerBound, upperBound);
+ }
+
// Other Utility Functions
/**
* Creates an expression that returns the document ID from a path.
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Field.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Field.java
index 0b5729040f..77079b1388 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Field.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Field.java
@@ -90,6 +90,32 @@ public Value toProto() {
return Value.newBuilder().setFieldReferenceValue(path.toString()).build();
}
+ /**
+ * Evaluates to the distance in meters between the location specified by this field and the query
+ * location.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param location Compute distance to this {@link com.google.cloud.firestore.GeoPoint}.
+ * @return A new {@link Expression} representing the geoDistance operation.
+ */
+ @BetaApi
+ public Expression geoDistance(com.google.cloud.firestore.GeoPoint location) {
+ return Expression.geoDistance(this, location);
+ }
+
+ /**
+ * Perform a full-text search on this field.
+ *
+ *
This Expression can only be used within a {@code Search} stage.
+ *
+ * @param rquery Define the search query using the rquery DTS.
+ */
+ @InternalApi
+ BooleanExpression matches(String rquery) {
+ return Expression.matches(this, rquery);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
index eacbf953e3..b2ad7afbf1 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
@@ -28,10 +28,17 @@
public class FunctionExpression extends Expression {
private final String name;
private final List params;
+ private final java.util.Map options;
FunctionExpression(String name, List extends Expression> params) {
+ this(name, params, java.util.Collections.emptyMap());
+ }
+
+ FunctionExpression(
+ String name, List extends Expression> params, java.util.Map options) {
this.name = name;
- this.params = Collections.unmodifiableList(params);
+ this.params = java.util.Collections.unmodifiableList(params);
+ this.options = java.util.Collections.unmodifiableMap(options);
}
@InternalApi
@@ -44,7 +51,8 @@ Value toProto() {
.addAllArgs(
this.params.stream()
.map(FunctionUtils::exprToValue)
- .collect(Collectors.toList())))
+ .collect(Collectors.toList()))
+ .putAllOptions(this.options))
.build();
}
@@ -53,11 +61,13 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FunctionExpression that = (FunctionExpression) o;
- return Objects.equal(name, that.name) && Objects.equal(params, that.params);
+ return java.util.Objects.equals(name, that.name)
+ && java.util.Objects.equals(params, that.params)
+ && java.util.Objects.equals(options, that.options);
}
@Override
public int hashCode() {
- return Objects.hashCode(name, params);
+ return java.util.Objects.hash(name, params, options);
}
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Selectable.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Selectable.java
index cf7bc71c05..053a97f174 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Selectable.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Selectable.java
@@ -19,4 +19,24 @@
import com.google.api.core.BetaApi;
@BetaApi
-public interface Selectable {}
+public interface Selectable {
+ /**
+ * Converts an object to a {@link Selectable}.
+ *
+ * @param o The object to convert. Supported types: {@link Selectable}, {@link String}, {@link
+ * com.google.cloud.firestore.FieldPath}.
+ * @return The converted {@link Selectable}.
+ */
+ @com.google.api.core.InternalApi
+ static Selectable toSelectable(Object o) {
+ if (o instanceof Selectable) {
+ return (Selectable) o;
+ } else if (o instanceof String) {
+ return Expression.field((String) o);
+ } else if (o instanceof com.google.cloud.firestore.FieldPath) {
+ return Expression.field((com.google.cloud.firestore.FieldPath) o);
+ } else {
+ throw new IllegalArgumentException("Unknown Selectable type: " + o.getClass().getName());
+ }
+ }
+}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
new file mode 100644
index 0000000000..1e9f96e876
--- /dev/null
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed 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 com.google.cloud.firestore.pipeline.stages;
+
+import static com.google.cloud.firestore.PipelineUtils.encodeValue;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.documentMatches;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.field;
+
+import com.google.api.core.BetaApi;
+import com.google.api.core.InternalApi;
+import com.google.cloud.firestore.PipelineUtils;
+import com.google.cloud.firestore.pipeline.expressions.BooleanExpression;
+import com.google.cloud.firestore.pipeline.expressions.Ordering;
+import com.google.cloud.firestore.pipeline.expressions.Selectable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.firestore.v1.Value;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * The Search stage executes full-text search or geo search operations.
+ *
+ * The Search stage must be the first stage in a Pipeline.
+ */
+@BetaApi
+public final class Search extends Stage {
+
+ /**
+ * Specifies if the `matches` and `snippet` expressions will enhance the user provided query to
+ * perform matching of synonyms, misspellings, lemmatization, stemming.
+ */
+ public static final class QueryEnhancement {
+ final String protoString;
+
+ private QueryEnhancement(String protoString) {
+ this.protoString = protoString;
+ }
+
+ /**
+ * Search will fall back to the un-enhanced, user provided query, if the query enhancement fails.
+ */
+ public static final QueryEnhancement PREFERRED = new QueryEnhancement("preferred");
+
+ /**
+ * Search will fail if the query enhancement times out or if the query enhancement is not
+ * supported by the project's DRZ compliance requirements.
+ */
+ public static final QueryEnhancement REQUIRED = new QueryEnhancement("required");
+
+ /** Search will use the un-enhanced, user provided query. */
+ public static final QueryEnhancement DISABLED = new QueryEnhancement("disabled");
+
+ Value toProto() {
+ return encodeValue(protoString);
+ }
+ }
+
+ @InternalApi
+ public Search(InternalOptions options) {
+ super("search", options);
+ }
+
+ /**
+ * Create {@link Search} with an expression search query.
+ *
+ *
{@code query} specifies the search query that will be used to query and score documents by
+ * the search stage.
+ */
+ public static Search withQuery(BooleanExpression query) {
+ return new Search(InternalOptions.of("query", encodeValue(query)));
+ }
+
+ /**
+ * Create {@link Search} with an expression search query.
+ *
+ *
{@code query} specifies the search query that will be used to query and score documents by
+ * the search stage.
+ */
+ public static Search withQuery(String rquery) {
+ return withQuery(documentMatches(rquery));
+ }
+
+ /** Specify the fields to add to each document. */
+ public Search withAddFields(Selectable field, Selectable... additionalFields) {
+ Selectable[] allFields = new Selectable[additionalFields.length + 1];
+ allFields[0] = field;
+ System.arraycopy(additionalFields, 0, allFields, 1, additionalFields.length);
+ Map map =
+ PipelineUtils.selectablesToMap(allFields).entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> encodeValue(e.getValue())));
+ return new Search(options.with("add_fields", encodeValue(map)));
+ }
+
+ /** Specify the fields to keep or add to each document. */
+ public Search withSelect(Selectable selection, Object... additionalSelections) {
+ Selectable[] allSelections = new Selectable[additionalSelections.length + 1];
+ allSelections[0] = selection;
+ for (int i = 0; i < additionalSelections.length; i++) {
+ allSelections[i + 1] = Selectable.toSelectable(additionalSelections[i]);
+ }
+ Map map =
+ PipelineUtils.selectablesToMap(allSelections).entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> encodeValue(e.getValue())));
+ return new Search(options.with("select", encodeValue(map)));
+ }
+
+ /** Specify the fields to keep or add to each document. */
+ public Search withSelect(String fieldName, Object... additionalSelections) {
+ return withSelect(field(fieldName), additionalSelections);
+ }
+
+ /** Specify how the returned documents are sorted. One or more ordering are required. */
+ public Search withSort(Ordering order, Ordering... additionalOrderings) {
+ Ordering[] allOrderings = new Ordering[additionalOrderings.length + 1];
+ allOrderings[0] = order;
+ System.arraycopy(additionalOrderings, 0, allOrderings, 1, additionalOrderings.length);
+ return new Search(
+ options.with(
+ "sort", Lists.transform(Arrays.asList(allOrderings), Ordering::toProto)));
+ }
+
+ /** Specify the maximum number of documents to return from the Search stage. */
+ public Search withLimit(long limit) {
+ return new Search(options.with("limit", encodeValue(limit)));
+ }
+
+ /**
+ * Specify the maximum number of documents for the search stage to score. Documents will be
+ * processed in the pre-sort order specified by the search index.
+ */
+ public Search withRetrievalDepth(long retrievalDepth) {
+ return new Search(options.with("retrieval_depth", encodeValue(retrievalDepth)));
+ }
+
+ /** Specify the number of documents to skip. */
+ public Search withOffset(long offset) {
+ return new Search(options.with("offset", encodeValue(offset)));
+ }
+
+ /** Specify the BCP-47 language code of text in the search query, such as, “en-US” or “sr-Latn” */
+ public Search withLanguageCode(String value) {
+ return new Search(options.with("language_code", encodeValue(value)));
+ }
+
+ /**
+ * Specify the query expansion behavior used by full-text search expressions in this search stage.
+ * Default: {@code .PREFERRED}
+ */
+ public Search withQueryEnhancement(QueryEnhancement queryEnhancement) {
+ return new Search(options.with("query_enhancement", queryEnhancement.toProto()));
+ }
+
+ @Override
+ Iterable toStageArgs() {
+ return ImmutableList.of();
+ }
+}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Stage.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Stage.java
index 8f82195f72..820a9f9012 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Stage.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Stage.java
@@ -31,7 +31,8 @@ public abstract class Stage {
this.options = options;
}
- final Pipeline.Stage toStageProto() {
+ @com.google.api.core.InternalApi
+ public final Pipeline.Stage toStageProto() {
return Pipeline.Stage.newBuilder()
.setName(name)
.addAllArgs(toStageArgs())
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
new file mode 100644
index 0000000000..805aa034d9
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed 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 com.google.cloud.firestore;
+
+import static com.google.cloud.firestore.pipeline.expressions.Expression.constant;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.field;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.firestore.pipeline.stages.Search;
+import com.google.firestore.v1.Pipeline.Stage;
+import com.google.firestore.v1.Value;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PipelineProtoTest {
+
+ @Test
+ public void testSearchStageProtoEncoding() {
+ FirestoreOptions options =
+ FirestoreOptions.newBuilder()
+ .setProjectId("new-project")
+ .setDatabaseId("(default)")
+ .build();
+ Firestore firestore = options.getService();
+
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection("foo")
+ .search(
+ Search.withQuery("foo")
+ .withLimit(1)
+ .withRetrievalDepth(2)
+ .withOffset(3)
+ .withQueryEnhancement(Search.QueryEnhancement.REQUIRED)
+ .withLanguageCode("en-US")
+ .withSort(field("foo").ascending())
+ .withAddFields(constant(true).as("bar"))
+ .withSelect(field("id")));
+
+ com.google.firestore.v1.Pipeline protoPipeline = pipeline.toProto();
+ assertThat(protoPipeline.getStagesCount()).isEqualTo(2);
+
+ Stage collectionStage = protoPipeline.getStages(0);
+ assertThat(collectionStage.getName()).isEqualTo("collection");
+ assertThat(collectionStage.getArgs(0).getReferenceValue()).isEqualTo("/foo");
+
+ Stage searchStage = protoPipeline.getStages(1);
+ assertThat(searchStage.getName()).isEqualTo("search");
+
+ java.util.Map optionsMap = searchStage.getOptionsMap();
+
+ // query
+ Value query = optionsMap.get("query");
+ assertThat(query).isNotNull();
+ assertThat(query.getFunctionValue().getName()).isEqualTo("document_matches");
+ assertThat(query.getFunctionValue().getArgs(0).getStringValue()).isEqualTo("foo");
+
+ // limit
+ assertThat(optionsMap.get("limit").getIntegerValue()).isEqualTo(1L);
+
+ // retrieval_depth
+ assertThat(optionsMap.get("retrieval_depth").getIntegerValue()).isEqualTo(2L);
+
+ // offset
+ assertThat(optionsMap.get("offset").getIntegerValue()).isEqualTo(3L);
+
+ // query_enhancement
+ assertThat(optionsMap.get("query_enhancement").getStringValue()).isEqualTo("required");
+
+ // language_code
+ assertThat(optionsMap.get("language_code").getStringValue()).isEqualTo("en-US");
+
+ // select
+ Value select = optionsMap.get("select");
+ assertThat(select.getMapValue().getFieldsMap().get("id").getFieldReferenceValue())
+ .isEqualTo("id");
+
+ // sort
+ Value sort = optionsMap.get("sort");
+ java.util.Map sortEntry =
+ sort.getArrayValue().getValues(0).getMapValue().getFieldsMap();
+ assertThat(sortEntry.get("direction").getStringValue()).isEqualTo("ascending");
+ assertThat(sortEntry.get("expression").getFieldReferenceValue()).isEqualTo("foo");
+
+ // add_fields
+ Value addFields = optionsMap.get("add_fields");
+ assertThat(addFields.getMapValue().getFieldsMap().get("bar").getBooleanValue()).isTrue();
+ }
+}
From b002cebca0c45ab9eb648bd6a2109fb1d62c2f43 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Mon, 23 Mar 2026 17:07:21 -0600
Subject: [PATCH 02/15] Spotless and tests
---
.../pipeline/expressions/Expression.java | 3 +-
.../expressions/FunctionExpression.java | 2 -
.../firestore/pipeline/stages/Search.java | 6 +-
.../firestore/it/ITPipelineSearchTest.java | 567 ++++++++++++++++++
4 files changed, 572 insertions(+), 6 deletions(-)
create mode 100644 google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
index 89cc366b0d..caa1fe55ef 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
@@ -3419,7 +3419,8 @@ public static Expression geoDistance(String fieldName, GeoPoint location) {
*/
@BetaApi
public static Expression geoDistance(Field field, GeoPoint location) {
- return new FunctionExpression("geo_distance", java.util.Arrays.asList(field, constant(location)));
+ return new FunctionExpression(
+ "geo_distance", java.util.Arrays.asList(field, constant(location)));
}
/**
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
index b2ad7afbf1..fc01d51398 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
@@ -18,9 +18,7 @@
import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
-import com.google.common.base.Objects;
import com.google.firestore.v1.Value;
-import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
index 1e9f96e876..d69d15bd16 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
@@ -53,7 +53,8 @@ private QueryEnhancement(String protoString) {
}
/**
- * Search will fall back to the un-enhanced, user provided query, if the query enhancement fails.
+ * Search will fall back to the un-enhanced, user provided query, if the query enhancement
+ * fails.
*/
public static final QueryEnhancement PREFERRED = new QueryEnhancement("preferred");
@@ -131,8 +132,7 @@ public Search withSort(Ordering order, Ordering... additionalOrderings) {
allOrderings[0] = order;
System.arraycopy(additionalOrderings, 0, allOrderings, 1, additionalOrderings.length);
return new Search(
- options.with(
- "sort", Lists.transform(Arrays.asList(allOrderings), Ordering::toProto)));
+ options.with("sort", Lists.transform(Arrays.asList(allOrderings), Ordering::toProto)));
}
/** Specify the maximum number of documents to return from the Search stage. */
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
new file mode 100644
index 0000000000..c32e74d618
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -0,0 +1,567 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed 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 com.google.cloud.firestore.it;
+
+import static com.google.cloud.firestore.it.ITQueryTest.map;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.and;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.concat;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.constant;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.documentMatches;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.field;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.greaterThanOrEqual;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.lessThanOrEqual;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.score;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.snippet;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.firestore.CollectionReference;
+import com.google.cloud.firestore.GeoPoint;
+import com.google.cloud.firestore.LocalFirestoreHelper;
+import com.google.cloud.firestore.Pipeline;
+import com.google.cloud.firestore.PipelineResult;
+import com.google.cloud.firestore.WriteBatch;
+import com.google.cloud.firestore.pipeline.stages.Search;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ITPipelineSearchTest extends ITBaseTest {
+
+ private CollectionReference restaurantsCollection;
+
+ private static final Map> restaurantDocs = new HashMap<>();
+
+ static {
+ restaurantDocs.put(
+ "sunnySideUp",
+ map(
+ "name", "The Sunny Side Up",
+ "description",
+ "A cozy neighborhood diner serving classic breakfast favorites all day long, from fluffy pancakes to savory omelets.",
+ "location", new GeoPoint(39.7541, -105.0002),
+ "menu",
+ "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes - $10
- Steak and Eggs - $16
Sides
- Hash Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee - $2
",
+ "average_price_per_person", 15));
+ restaurantDocs.put(
+ "goldenWaffle",
+ map(
+ "name", "The Golden Waffle",
+ "description",
+ "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to 11:00 AM.",
+ "location", new GeoPoint(39.7183, -104.9621),
+ "menu",
+ "Signature Waffles
- Strawberry Delight - $11
- Chicken and Waffles - $14
- Chocolate Chip Crunch - $10
Drinks
- Fresh OJ - $4
- Artisan Coffee - $3
",
+ "average_price_per_person", 13));
+ restaurantDocs.put(
+ "lotusBlossomThai",
+ map(
+ "name", "Lotus Blossom Thai",
+ "description",
+ "Authentic Thai cuisine featuring hand-crushed spices and traditional family recipes from the Chiang Mai region.",
+ "location", new GeoPoint(39.7315, -104.9847),
+ "menu",
+ "Appetizers
- Spring Rolls - $7
- Chicken Satay - $9
Main Course
- Pad Thai - $15
- Green Curry - $16
- Drunken Noodles - $15
",
+ "average_price_per_person", 22));
+ restaurantDocs.put(
+ "mileHighCatch",
+ map(
+ "name", "Mile High Catch",
+ "description",
+ "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic shellfish in an upscale atmosphere.",
+ "location", new GeoPoint(39.7401, -104.9903),
+ "menu",
+ "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster Cocktail - $22
Entrees
- Pan-Seared Salmon - $28
- King Crab Legs - $45
- Fish and Chips - $19
",
+ "average_price_per_person", 45));
+ restaurantDocs.put(
+ "peakBurgers",
+ map(
+ "name", "Peak Burgers",
+ "description",
+ "Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
+ "location", new GeoPoint(39.7622, -105.0125),
+ "menu",
+ "Burgers
- The Peak Double - $12
- Bison Burger - $15
- Veggie Stack - $11
Sides
- Truffle Fries - $6
- Onion Rings - $5
",
+ "average_price_per_person", 18));
+ restaurantDocs.put(
+ "solTacos",
+ map(
+ "name", "El Sol Tacos",
+ "description",
+ "A vibrant street-side taco stand serving up quick, delicious, and traditional Mexican street food.",
+ "location", new GeoPoint(39.6952, -105.0274),
+ "menu",
+ "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo Asado
- Nopales (Cactus)
Beverages
- Horchata - $4
- Mexican Coke - $3
",
+ "average_price_per_person", 12));
+ restaurantDocs.put(
+ "eastsideTacos",
+ map(
+ "name", "Eastside Cantina",
+ "description",
+ "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the city.",
+ "location", new GeoPoint(39.735, -104.885),
+ "menu",
+ "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos - $4.50
- Shrimp Tacos - $5
Drinks
- House Margarita - $9
- Jarritos - $3
",
+ "average_price_per_person", 18));
+ restaurantDocs.put(
+ "eastsideChicken",
+ map(
+ "name", "Eastside Chicken",
+ "description", "Fried chicken to go - next to Eastside Cantina.",
+ "location", new GeoPoint(39.735, -104.885),
+ "menu",
+ "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich - $9
Drinks
- House Margarita - $9
- Jarritos - $3
",
+ "average_price_per_person", 12));
+ }
+
+ @Override
+ public void primeBackend() throws Exception {
+ // Disable priming as it uses Watch/Listen, which is not supported by the 'enterprise' database.
+ }
+
+ @Before
+ public void setupRestaurantDocs() throws Exception {
+ restaurantsCollection =
+ firestore.collection("SearchIntegrationTests-" + LocalFirestoreHelper.autoId());
+
+ WriteBatch batch = firestore.batch();
+ for (Map.Entry> entry : restaurantDocs.entrySet()) {
+ batch.set(restaurantsCollection.document(entry.getKey()), entry.getValue());
+ }
+ batch.commit().get(10, TimeUnit.SECONDS);
+ }
+
+ private void assertResultIds(Pipeline.Snapshot snapshot, String... ids) {
+ List resultIds =
+ snapshot.getResults().stream()
+ .map(PipelineResult::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ assertThat(resultIds).containsExactlyElementsIn(Arrays.asList(ids)).inOrder();
+ }
+
+ // =========================================================================
+ // Search stage
+ // =========================================================================
+
+ // --- DISABLE query expansion ---
+
+ // query
+ @Test
+ public void searchWithLanguageCode() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery("waffles")
+ .withLanguageCode("en")
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle");
+ }
+
+ @Test
+ public void searchFullDocument() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery("waffles").withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle");
+ }
+
+ @Test
+ public void searchSpecificField() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:waffles"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle");
+ }
+
+ @Test
+ public void geoNearQuery() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(
+ field("location")
+ .geoDistance(new GeoPoint(39.6985, -105.024))
+ .lessThan(1000))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos");
+ }
+
+ @Test
+ public void conjunctionOfTextSearchPredicates() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(
+ and(documentMatches("menu:waffles"), documentMatches("description:diner")))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ }
+
+ @Test
+ public void conjunctionOfTextSearchAndGeoNear() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(
+ and(
+ documentMatches("menu:tacos"),
+ field("location")
+ .geoDistance(new GeoPoint(39.6985, -105.024))
+ .lessThan(10000)))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos");
+ }
+
+ @Test
+ public void negateMatch() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:-waffles"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(
+ snapshot,
+ "eastsideTacos",
+ "solTacos",
+ "peakBurgers",
+ "mileHighCatch",
+ "lotusBlossomThai",
+ "sunnySideUp");
+ }
+
+ @Test
+ public void rquerySearchTheDocumentWithConjunctionAndDisjunction() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("(waffles OR pancakes) AND coffee"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ }
+
+ @Test
+ public void rqueryAsQueryParam() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery("(waffles OR pancakes) AND coffee")
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ }
+
+ @Test
+ public void rquerySupportsFieldPaths() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all day\"")
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "sunnySideUp");
+ }
+
+ @Test
+ public void conjunctionOfRqueryAndExpression() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(
+ and(
+ documentMatches("tacos"),
+ greaterThanOrEqual("average_price_per_person", 8),
+ lessThanOrEqual("average_price_per_person", 15)))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos");
+ }
+
+ // --- REQUIRE query expansion ---
+
+ @Test
+ public void requireQueryExpansion_searchFullDocument() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("waffles"))
+ .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ }
+
+ @Test
+ public void requireQueryExpansion_searchSpecificField() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:waffles"))
+ .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ }
+
+ // add fields
+ @Test
+ public void addFields_topicalityScoreAndSnippet() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:waffles"))
+ .withAddFields(
+ score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+ .select("name", "searchScore", "snippet");
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertThat(snapshot.getResults()).hasSize(1);
+ PipelineResult result = snapshot.getResults().get(0);
+ assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+ }
+
+ // select
+ @Test
+ public void select_topicalityScoreAndSnippet() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:waffles"))
+ .withSelect(
+ field("name"),
+ field("location"),
+ score().as("searchScore"),
+ snippet("menu", "waffles").as("snippet"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertThat(snapshot.getResults()).hasSize(1);
+ PipelineResult result = snapshot.getResults().get(0);
+ assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ assertThat(result.getData().get("location")).isEqualTo(new GeoPoint(39.7183, -104.9621));
+ assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+
+ List sortedKeys =
+ result.getData().keySet().stream().sorted().collect(Collectors.toList());
+ assertThat(sortedKeys).containsExactly("location", "name", "searchScore", "snippet").inOrder();
+ }
+
+ // sort
+ @Test
+ public void sort_byTopicality() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:tacos"))
+ .withSort(score().descending())
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "eastsideTacos", "solTacos");
+ }
+
+ @Test
+ public void sort_byDistance() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:tacos"))
+ .withSort(
+ field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos", "eastsideTacos");
+ }
+
+ @Test
+ public void sort_byMultipleOrderings() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:tacos OR chicken"))
+ .withSort(
+ field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending(),
+ score().descending())
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos", "eastsideTacos", "eastsideChicken");
+ }
+
+ // limit
+ @Test
+ public void limit_limitsTheNumberOfDocumentsReturned() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(constant(true))
+ .withSort(
+ field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
+ .withLimit(5)
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "goldenWaffle");
+ }
+
+ @Test
+ public void limit_limitsTheNumberOfDocumentsScored() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("menu:chicken OR tacos OR fish OR waffles"))
+ .withRetrievalDepth(6)
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "eastsideChicken", "eastsideTacos", "solTacos", "mileHighCatch");
+ }
+
+ // offset
+ @Test
+ public void offset_skipsNDocuments() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(constant(true))
+ .withLimit(2)
+ .withOffset(2)
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "eastsideChicken", "eastsideTacos");
+ }
+
+ // =========================================================================
+ // Snippet
+ // =========================================================================
+
+ @Test
+ public void snippetOnMultipleFields() throws Exception {
+ // Get snippet from 1 field
+ Pipeline pipeline1 =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("waffle"))
+ .withAddFields(snippet("menu", "waffles").as("snippet"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot1 = pipeline1.execute().get();
+ assertThat(snapshot1.getResults()).hasSize(1);
+ assertThat(snapshot1.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
+ String snip1 = (String) snapshot1.getResults().get(0).getData().get("snippet");
+ assertThat(snip1.length()).isGreaterThan(0);
+
+ // Get snippet from 2 fields
+ Pipeline pipeline2 =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("waffle"))
+ .withAddFields(
+ concat(field("menu"), field("description"))
+ .snippet("waffles") // Without SnippetOptions in Java
+ .as("snippet"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot2 = pipeline2.execute().get();
+ assertThat(snapshot2.getResults()).hasSize(1);
+ assertThat(snapshot2.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
+ String snip2 = (String) snapshot2.getResults().get(0).getData().get("snippet");
+ assertThat(snip2.length()).isGreaterThan(snip1.length());
+ }
+}
From 2e4cdf0ab3b934c07a8cadeb4aa89613b4df69e1 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Wed, 25 Mar 2026 13:49:17 -0600
Subject: [PATCH 03/15] Skip search tests on Standard edition
---
.../google/cloud/firestore/it/ITPipelineSearchTest.java | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index c32e74d618..40ac777238 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -27,6 +27,7 @@
import static com.google.cloud.firestore.pipeline.expressions.Expression.score;
import static com.google.cloud.firestore.pipeline.expressions.Expression.snippet;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeFalse;
import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.GeoPoint;
@@ -138,11 +139,15 @@ public class ITPipelineSearchTest extends ITBaseTest {
@Override
public void primeBackend() throws Exception {
- // Disable priming as it uses Watch/Listen, which is not supported by the 'enterprise' database.
+ // Disable priming as it uses Watch/Listen
}
@Before
public void setupRestaurantDocs() throws Exception {
+ assumeFalse(
+ "This test suite only runs against the Enterprise edition.",
+ !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
+
restaurantsCollection =
firestore.collection("SearchIntegrationTests-" + LocalFirestoreHelper.autoId());
From 56e09f57d1e8352b2fdee7d2c62495f4fea6ff4d Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Wed, 25 Mar 2026 15:14:50 -0600
Subject: [PATCH 04/15] Update copyright. Also fix tests to remove tests of
unsupported features
---
.../firestore/pipeline/stages/Search.java | 2 +-
.../firestore/it/ITPipelineSearchTest.java | 234 +++++++++---------
2 files changed, 121 insertions(+), 115 deletions(-)
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
index d69d15bd16..db56233728 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index 40ac777238..671c80911f 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -148,8 +148,7 @@ public void setupRestaurantDocs() throws Exception {
"This test suite only runs against the Enterprise edition.",
!getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
- restaurantsCollection =
- firestore.collection("SearchIntegrationTests-" + LocalFirestoreHelper.autoId());
+ restaurantsCollection = firestore.collection("SearchIntegrationTests");
WriteBatch batch = firestore.batch();
for (Map.Entry> entry : restaurantDocs.entrySet()) {
@@ -202,19 +201,19 @@ public void searchFullDocument() throws Exception {
assertResultIds(snapshot, "goldenWaffle");
}
- @Test
- public void searchSpecificField() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("menu:waffles"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle");
- }
+ // @Test
+ // public void searchSpecificField() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(restaurantsCollection.getId())
+ // .search(
+ // Search.withQuery(field("menu").matches("waffles"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle");
+ // }
@Test
public void geoNearQuery() throws Exception {
@@ -233,6 +232,7 @@ public void geoNearQuery() throws Exception {
assertResultIds(snapshot, "solTacos");
}
+
@Test
public void conjunctionOfTextSearchPredicates() throws Exception {
Pipeline pipeline =
@@ -241,51 +241,54 @@ public void conjunctionOfTextSearchPredicates() throws Exception {
.collection(restaurantsCollection.getId())
.search(
Search.withQuery(
- and(documentMatches("menu:waffles"), documentMatches("description:diner")))
+ and(documentMatches("waffles"), documentMatches("diner")))
+ // TODO(search) switch back to field matches when supported by the backend
+ // and(field("menu").matches("waffles"), field("description").matches("diner")))
.withQueryEnhancement(Search.QueryEnhancement.DISABLED));
Pipeline.Snapshot snapshot = pipeline.execute().get();
assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
}
- @Test
- public void conjunctionOfTextSearchAndGeoNear() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(
- and(
- documentMatches("menu:tacos"),
- field("location")
- .geoDistance(new GeoPoint(39.6985, -105.024))
- .lessThan(10000)))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos");
- }
+ // TODO(search) enable test when geo+text search indexes are supported
+ // @Test
+ // public void conjunctionOfTextSearchAndGeoNear() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(restaurantsCollection.getId())
+ // .search(
+ // Search.withQuery(
+ // and(
+ // field("menu").matches("tacos"),
+ // field("location")
+ // .geoDistance(new GeoPoint(39.6985, -105.024))
+ // .lessThan(10000)))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos");
+ // }
@Test
public void negateMatch() throws Exception {
Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("menu:-waffles"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("-waffles"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
Pipeline.Snapshot snapshot = pipeline.execute().get();
assertResultIds(
- snapshot,
- "eastsideTacos",
- "solTacos",
- "peakBurgers",
- "mileHighCatch",
- "lotusBlossomThai",
- "sunnySideUp");
+ snapshot,
+ "eastsideTacos",
+ "solTacos",
+ "peakBurgers",
+ "mileHighCatch",
+ "lotusBlossomThai",
+ "sunnySideUp");
}
@Test
@@ -316,19 +319,20 @@ public void rqueryAsQueryParam() throws Exception {
assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
}
- @Test
- public void rquerySupportsFieldPaths() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all day\"")
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "sunnySideUp");
- }
+ // TODO(search) enable when rquery supports field paths
+ //@Test
+ //public void rquerySupportsFieldPaths() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(restaurantsCollection.getId())
+ // .search(
+ // Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all day\"")
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "sunnySideUp");
+ //}
@Test
public void conjunctionOfRqueryAndExpression() throws Exception {
@@ -364,41 +368,42 @@ public void requireQueryExpansion_searchFullDocument() throws Exception {
assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
}
- @Test
- public void requireQueryExpansion_searchSpecificField() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("menu:waffles"))
- .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
- }
+ // TODO(search) re-enable when backend supports field matches
+ // @Test
+ // public void requireQueryExpansion_searchSpecificField() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(restaurantsCollection.getId())
+ // .search(
+ // Search.withQuery(field("menu").matches("waffles"))
+ // .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ // }
// add fields
- @Test
- public void addFields_topicalityScoreAndSnippet() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("menu:waffles"))
- .withAddFields(
- score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
- .select("name", "searchScore", "snippet");
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertThat(snapshot.getResults()).hasSize(1);
- PipelineResult result = snapshot.getResults().get(0);
- assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
- assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
- assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
- }
+ @Test
+ public void addFields_topicalityScoreAndSnippet() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("waffles"))
+ .withAddFields(
+ score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+ .select("name", "searchScore", "snippet");
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertThat(snapshot.getResults()).hasSize(1);
+ PipelineResult result = snapshot.getResults().get(0);
+ assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+ }
// select
@Test
@@ -408,7 +413,7 @@ public void select_topicalityScoreAndSnippet() throws Exception {
.pipeline()
.collection(restaurantsCollection.getId())
.search(
- Search.withQuery(documentMatches("menu:waffles"))
+ Search.withQuery(documentMatches("waffles"))
.withSelect(
field("name"),
field("location"),
@@ -437,7 +442,7 @@ public void sort_byTopicality() throws Exception {
.pipeline()
.collection(restaurantsCollection.getId())
.search(
- Search.withQuery(documentMatches("menu:tacos"))
+ Search.withQuery(documentMatches("tacos"))
.withSort(score().descending())
.withQueryEnhancement(Search.QueryEnhancement.DISABLED));
@@ -452,7 +457,7 @@ public void sort_byDistance() throws Exception {
.pipeline()
.collection(restaurantsCollection.getId())
.search(
- Search.withQuery(documentMatches("menu:tacos"))
+ Search.withQuery(constant(true))
.withSort(
field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
.withQueryEnhancement(Search.QueryEnhancement.DISABLED));
@@ -461,22 +466,23 @@ public void sort_byDistance() throws Exception {
assertResultIds(snapshot, "solTacos", "eastsideTacos");
}
- @Test
- public void sort_byMultipleOrderings() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("menu:tacos OR chicken"))
- .withSort(
- field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending(),
- score().descending())
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos", "eastsideTacos", "eastsideChicken");
- }
+ // TODO(search) re-enable when geo+text search indexes are supported
+ // @Test
+ // public void sort_byMultipleOrderings() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(restaurantsCollection.getId())
+ // .search(
+ // Search.withQuery(field("menu").matches("tacos OR chicken"))
+ // .withSort(
+ // field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending(),
+ // score().descending())
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos", "eastsideTacos", "eastsideChicken");
+ // }
// limit
@Test
@@ -503,7 +509,7 @@ public void limit_limitsTheNumberOfDocumentsScored() throws Exception {
.pipeline()
.collection(restaurantsCollection.getId())
.search(
- Search.withQuery(documentMatches("menu:chicken OR tacos OR fish OR waffles"))
+ Search.withQuery(documentMatches("chicken OR tacos OR fish OR waffles"))
.withRetrievalDepth(6)
.withQueryEnhancement(Search.QueryEnhancement.DISABLED));
From 3858f8a03fa1211474e77e451556cb1b7e559e48 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Thu, 26 Mar 2026 08:10:25 -0400
Subject: [PATCH 05/15] Make FunctionExpression constructor accepting options
internal because this is untested for general cosumption
---
.../cloud/firestore/pipeline/expressions/FunctionExpression.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
index fc01d51398..6150dabb80 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java
@@ -32,6 +32,7 @@ public class FunctionExpression extends Expression {
this(name, params, java.util.Collections.emptyMap());
}
+ @InternalApi
FunctionExpression(
String name, List extends Expression> params, java.util.Map options) {
this.name = name;
From 1c97c79d406aeba3bed6af5b7523140d9fff3de9 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Thu, 26 Mar 2026 08:13:51 -0400
Subject: [PATCH 06/15] spotless
---
.../firestore/it/ITPipelineSearchTest.java | 94 +++++++++----------
1 file changed, 47 insertions(+), 47 deletions(-)
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index 671c80911f..2db23a5a5e 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -31,7 +31,6 @@
import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.GeoPoint;
-import com.google.cloud.firestore.LocalFirestoreHelper;
import com.google.cloud.firestore.Pipeline;
import com.google.cloud.firestore.PipelineResult;
import com.google.cloud.firestore.WriteBatch;
@@ -145,8 +144,8 @@ public void primeBackend() throws Exception {
@Before
public void setupRestaurantDocs() throws Exception {
assumeFalse(
- "This test suite only runs against the Enterprise edition.",
- !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
+ "This test suite only runs against the Enterprise edition.",
+ !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
restaurantsCollection = firestore.collection("SearchIntegrationTests");
@@ -232,7 +231,6 @@ public void geoNearQuery() throws Exception {
assertResultIds(snapshot, "solTacos");
}
-
@Test
public void conjunctionOfTextSearchPredicates() throws Exception {
Pipeline pipeline =
@@ -240,10 +238,10 @@ public void conjunctionOfTextSearchPredicates() throws Exception {
.pipeline()
.collection(restaurantsCollection.getId())
.search(
- Search.withQuery(
- and(documentMatches("waffles"), documentMatches("diner")))
- // TODO(search) switch back to field matches when supported by the backend
- // and(field("menu").matches("waffles"), field("description").matches("diner")))
+ Search.withQuery(and(documentMatches("waffles"), documentMatches("diner")))
+ // TODO(search) switch back to field matches when supported by the backend
+ // and(field("menu").matches("waffles"),
+ // field("description").matches("diner")))
.withQueryEnhancement(Search.QueryEnhancement.DISABLED));
Pipeline.Snapshot snapshot = pipeline.execute().get();
@@ -273,22 +271,22 @@ public void conjunctionOfTextSearchPredicates() throws Exception {
@Test
public void negateMatch() throws Exception {
Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("-waffles"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("-waffles"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
Pipeline.Snapshot snapshot = pipeline.execute().get();
assertResultIds(
- snapshot,
- "eastsideTacos",
- "solTacos",
- "peakBurgers",
- "mileHighCatch",
- "lotusBlossomThai",
- "sunnySideUp");
+ snapshot,
+ "eastsideTacos",
+ "solTacos",
+ "peakBurgers",
+ "mileHighCatch",
+ "lotusBlossomThai",
+ "sunnySideUp");
}
@Test
@@ -320,19 +318,20 @@ public void rqueryAsQueryParam() throws Exception {
}
// TODO(search) enable when rquery supports field paths
- //@Test
- //public void rquerySupportsFieldPaths() throws Exception {
+ // @Test
+ // public void rquerySupportsFieldPaths() throws Exception {
// Pipeline pipeline =
// firestore
// .pipeline()
// .collection(restaurantsCollection.getId())
// .search(
- // Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all day\"")
+ // Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all
+ // day\"")
// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
//
// Pipeline.Snapshot snapshot = pipeline.execute().get();
// assertResultIds(snapshot, "sunnySideUp");
- //}
+ // }
@Test
public void conjunctionOfRqueryAndExpression() throws Exception {
@@ -384,26 +383,26 @@ public void requireQueryExpansion_searchFullDocument() throws Exception {
// }
// add fields
- @Test
- public void addFields_topicalityScoreAndSnippet() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("waffles"))
- .withAddFields(
- score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
- .select("name", "searchScore", "snippet");
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertThat(snapshot.getResults()).hasSize(1);
- PipelineResult result = snapshot.getResults().get(0);
- assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
- assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
- assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
- }
+ @Test
+ public void addFields_topicalityScoreAndSnippet() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(restaurantsCollection.getId())
+ .search(
+ Search.withQuery(documentMatches("waffles"))
+ .withAddFields(
+ score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
+ .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+ .select("name", "searchScore", "snippet");
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertThat(snapshot.getResults()).hasSize(1);
+ PipelineResult result = snapshot.getResults().get(0);
+ assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+ }
// select
@Test
@@ -457,7 +456,7 @@ public void sort_byDistance() throws Exception {
.pipeline()
.collection(restaurantsCollection.getId())
.search(
- Search.withQuery(constant(true))
+ Search.withQuery(constant(true))
.withSort(
field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
.withQueryEnhancement(Search.QueryEnhancement.DISABLED));
@@ -476,7 +475,8 @@ public void sort_byDistance() throws Exception {
// .search(
// Search.withQuery(field("menu").matches("tacos OR chicken"))
// .withSort(
- // field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending(),
+ // field("location").geoDistance(new GeoPoint(39.6985,
+ // -105.024)).ascending(),
// score().descending())
// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
//
From 41c12caeb8fdde39654c6c0b2e13ca56ff6948b8 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Wed, 1 Apr 2026 09:24:52 -0600
Subject: [PATCH 07/15] ref doc cleanup
---
.../pipeline/expressions/Expression.java | 56 +++++++++++++++++--
.../firestore/pipeline/stages/Search.java | 14 ++++-
2 files changed, 62 insertions(+), 8 deletions(-)
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
index caa1fe55ef..f0edd14340 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
@@ -3412,6 +3412,14 @@ public static Expression geoDistance(String fieldName, GeoPoint location) {
*
* This Expression can only be used within a {@code Search} stage.
*
+ *
Example:
+ *
+ *
{@code
+ * db.pipeline().collection("restaurants").search(
+ * Search.withQuery("waffles").withSort(geoDistance(field("location"), new GeoPoint(37.0, -122.0)).ascending())
+ * )
+ * }
+ *
* @param field Specifies the field in the document which contains the first {@link GeoPoint} for
* distance computation.
* @param location Compute distance to this {@link GeoPoint}.
@@ -3428,7 +3436,13 @@ public static Expression geoDistance(Field field, GeoPoint location) {
*
* This Expression can only be used within a {@code Search} stage.
*
- * @param rquery Define the search query using the search DTS.
+ *
Example:
+ *
+ *
{@code
+ * db.pipeline().collection("restaurants").search(Search.withQuery(documentMatches("waffles OR pancakes")))
+ * }
+ *
+ * @param rquery Define the search query using the search DSL.
* @return A new {@link BooleanExpression} representing the documentMatches operation.
*/
@BetaApi
@@ -3441,8 +3455,14 @@ public static BooleanExpression documentMatches(String rquery) {
*
* This Expression can only be used within a {@code Search} stage.
*
+ *
Example:
+ *
+ *
{@code
+ * db.pipeline().collection("restaurants").search(Search.withQuery(matches("menu", "waffles")))
+ * }
+ *
* @param fieldName Perform search on this field.
- * @param rquery Define the search query using the rquery DTS.
+ * @param rquery Define the search query using the search DSL.
*/
@InternalApi
static BooleanExpression matches(String fieldName, String rquery) {
@@ -3455,7 +3475,7 @@ static BooleanExpression matches(String fieldName, String rquery) {
* This Expression can only be used within a {@code Search} stage.
*
* @param field Perform search on this field.
- * @param rquery Define the search query using the rquery DTS.
+ * @param rquery Define the search query using the search DSL.
*/
@InternalApi
static BooleanExpression matches(Field field, String rquery) {
@@ -3464,10 +3484,18 @@ static BooleanExpression matches(Field field, String rquery) {
/**
* Evaluates to the search score that reflects the topicality of the document to all of the text
- * predicates ({@code matches} and {@code documentMatches}) in the search query.
+ * predicates (for example: {@code documentMatches}) in the search query.
*
*
This Expression can only be used within a {@code Search} stage.
*
+ *
Example:
+ *
+ *
{@code
+ * db.pipeline().collection("restaurants").search(
+ * Search.withQuery("waffles").withSort(score().descending())
+ * )
+ * }
+ *
* @return A new {@link Expression} representing the score operation.
*/
@BetaApi
@@ -3481,8 +3509,16 @@ public static Expression score() {
*
* This Expression can only be used within a {@code Search} stage.
*
+ *
Example:
+ *
+ *
{@code
+ * db.pipeline().collection("restaurants").search(
+ * Search.withQuery("waffles").withAddFields(snippet("menu", "waffles").as("snippet"))
+ * )
+ * }
+ *
* @param fieldName Search the specified field for matching terms.
- * @param rquery Define the search query using the search DTS.
+ * @param rquery Define the search query using the search DSL.
* @return A new {@link Expression} representing the snippet operation.
*/
@BetaApi
@@ -3497,7 +3533,15 @@ public static Expression snippet(String fieldName, String rquery) {
*
* This Expression can only be used within a {@code Search} stage.
*
- * @param rquery Define the search query using the search DTS.
+ *
Example:
+ *
+ *
{@code
+ * db.pipeline().collection("restaurants").search(
+ * Search.withQuery("waffles").withAddFields(field("menu").snippet("waffles").as("snippet"))
+ * )
+ * }
+ *
+ * @param rquery Define the search query using the search DSL.
* @return A new {@link Expression} representing the snippet operation.
*/
@BetaApi
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
index db56233728..63b3f15492 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
@@ -37,6 +37,16 @@
* The Search stage executes full-text search or geo search operations.
*
* The Search stage must be the first stage in a Pipeline.
+ *
+ *
Example:
+ *
+ *
{@code
+ * db.pipeline().collection("restaurants").search(
+ * Search.withQuery(documentMatches("waffles OR pancakes"))
+ * .withSort(score().descending())
+ * .withLimit(10)
+ * );
+ * }
*/
@BetaApi
public final class Search extends Stage {
@@ -141,8 +151,8 @@ public Search withLimit(long limit) {
}
/**
- * Specify the maximum number of documents for the search stage to score. Documents will be
- * processed in the pre-sort order specified by the search index.
+ * Specify the maximum number of documents to retrieve. Documents will be retrieved in the
+ * pre-sort order specified by the search index.
*/
public Search withRetrievalDepth(long retrievalDepth) {
return new Search(options.with("retrieval_depth", encodeValue(retrievalDepth)));
From 7fff988e294136971ef2625f37455ea1d34cd5cc Mon Sep 17 00:00:00 2001
From: cloud-java-bot
Date: Wed, 1 Apr 2026 15:53:38 +0000
Subject: [PATCH 08/15] chore: generate libraries at Wed Apr 1 15:51:29 UTC
2026
---
.../firestore/it/ITPipelineSearchTest.java | 52 ++++++++++++++-----
1 file changed, 38 insertions(+), 14 deletions(-)
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index 2db23a5a5e..ed18028bd2 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -60,40 +60,54 @@ public class ITPipelineSearchTest extends ITBaseTest {
map(
"name", "The Sunny Side Up",
"description",
- "A cozy neighborhood diner serving classic breakfast favorites all day long, from fluffy pancakes to savory omelets.",
+ "A cozy neighborhood diner serving classic breakfast favorites all day long, from"
+ + " fluffy pancakes to savory omelets.",
"location", new GeoPoint(39.7541, -105.0002),
"menu",
- "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes - $10
- Steak and Eggs - $16
Sides
- Hash Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee - $2
",
+ "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes"
+ + " - $10
- Steak and Eggs - $16
Sides
- Hash"
+ + " Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee -"
+ + " $2
",
"average_price_per_person", 15));
restaurantDocs.put(
"goldenWaffle",
map(
"name", "The Golden Waffle",
"description",
- "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to 11:00 AM.",
+ "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to"
+ + " 11:00 AM.",
"location", new GeoPoint(39.7183, -104.9621),
"menu",
- "Signature Waffles
- Strawberry Delight - $11
- Chicken and Waffles - $14
- Chocolate Chip Crunch - $10
Drinks
- Fresh OJ - $4
- Artisan Coffee - $3
",
+ "Signature Waffles
- Strawberry Delight - $11
- Chicken and"
+ + " Waffles - $14
- Chocolate Chip Crunch -"
+ + " $10
Drinks
- Fresh OJ - $4
- Artisan Coffee -"
+ + " $3
",
"average_price_per_person", 13));
restaurantDocs.put(
"lotusBlossomThai",
map(
"name", "Lotus Blossom Thai",
"description",
- "Authentic Thai cuisine featuring hand-crushed spices and traditional family recipes from the Chiang Mai region.",
+ "Authentic Thai cuisine featuring hand-crushed spices and traditional family"
+ + " recipes from the Chiang Mai region.",
"location", new GeoPoint(39.7315, -104.9847),
"menu",
- "Appetizers
- Spring Rolls - $7
- Chicken Satay - $9
Main Course
- Pad Thai - $15
- Green Curry - $16
- Drunken Noodles - $15
",
+ "Appetizers
- Spring Rolls - $7
- Chicken Satay -"
+ + " $9
Main Course
- Pad Thai - $15
- Green Curry"
+ + " - $16
- Drunken Noodles - $15
",
"average_price_per_person", 22));
restaurantDocs.put(
"mileHighCatch",
map(
"name", "Mile High Catch",
"description",
- "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic shellfish in an upscale atmosphere.",
+ "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic"
+ + " shellfish in an upscale atmosphere.",
"location", new GeoPoint(39.7401, -104.9903),
"menu",
- "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster Cocktail - $22
Entrees
- Pan-Seared Salmon - $28
- King Crab Legs - $45
- Fish and Chips - $19
",
+ "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster"
+ + " Cocktail - $22
Entrees
- Pan-Seared Salmon -"
+ + " $28
- King Crab Legs - $45
- Fish and Chips - $19
",
"average_price_per_person", 45));
restaurantDocs.put(
"peakBurgers",
@@ -103,27 +117,35 @@ public class ITPipelineSearchTest extends ITBaseTest {
"Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
"location", new GeoPoint(39.7622, -105.0125),
"menu",
- "Burgers
- The Peak Double - $12
- Bison Burger - $15
- Veggie Stack - $11
Sides
- Truffle Fries - $6
- Onion Rings - $5
",
+ "Burgers
- The Peak Double - $12
- Bison Burger -"
+ + " $15
- Veggie Stack - $11
Sides
- Truffle Fries"
+ + " - $6
- Onion Rings - $5
",
"average_price_per_person", 18));
restaurantDocs.put(
"solTacos",
map(
"name", "El Sol Tacos",
"description",
- "A vibrant street-side taco stand serving up quick, delicious, and traditional Mexican street food.",
+ "A vibrant street-side taco stand serving up quick, delicious, and traditional"
+ + " Mexican street food.",
"location", new GeoPoint(39.6952, -105.0274),
"menu",
- "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo Asado
- Nopales (Cactus)
Beverages
- Horchata - $4
- Mexican Coke - $3
",
+ "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo"
+ + " Asado
- Nopales (Cactus)
Beverages
- Horchata"
+ + " - $4
- Mexican Coke - $3
",
"average_price_per_person", 12));
restaurantDocs.put(
"eastsideTacos",
map(
"name", "Eastside Cantina",
"description",
- "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the city.",
+ "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the"
+ + " city.",
"location", new GeoPoint(39.735, -104.885),
"menu",
- "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos - $4.50
- Shrimp Tacos - $5
Drinks
- House Margarita - $9
- Jarritos - $3
",
+ "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos -"
+ + " $4.50
- Shrimp Tacos - $5
Drinks
- House"
+ + " Margarita - $9
- Jarritos - $3
",
"average_price_per_person", 18));
restaurantDocs.put(
"eastsideChicken",
@@ -132,7 +154,9 @@ public class ITPipelineSearchTest extends ITBaseTest {
"description", "Fried chicken to go - next to Eastside Cantina.",
"location", new GeoPoint(39.735, -104.885),
"menu",
- "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich - $9
Drinks
- House Margarita - $9
- Jarritos - $3
",
+ "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich -"
+ + " $9
Drinks
- House Margarita - $9
- Jarritos -"
+ + " $3
",
"average_price_per_person", 12));
}
From 4f0c0c87b0e2e06d1bd0a57be96c2dd8eed4bef1 Mon Sep 17 00:00:00 2001
From: cloud-java-bot
Date: Wed, 1 Apr 2026 15:56:21 +0000
Subject: [PATCH 09/15] chore: generate libraries at Wed Apr 1 15:54:06 UTC
2026
---
.../firestore/it/ITPipelineSearchTest.java | 153 ++++++++++--------
1 file changed, 89 insertions(+), 64 deletions(-)
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index ed18028bd2..f1a8ba5b70 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -58,106 +58,131 @@ public class ITPipelineSearchTest extends ITBaseTest {
restaurantDocs.put(
"sunnySideUp",
map(
- "name", "The Sunny Side Up",
+ "name",
+ "The Sunny Side Up",
"description",
- "A cozy neighborhood diner serving classic breakfast favorites all day long, from"
- + " fluffy pancakes to savory omelets.",
- "location", new GeoPoint(39.7541, -105.0002),
+ "A cozy neighborhood diner serving classic breakfast favorites all day long, from"
+ + " fluffy pancakes to savory omelets.",
+ "location",
+ new GeoPoint(39.7541, -105.0002),
"menu",
- "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes"
- + " - $10
- Steak and Eggs - $16
Sides
- Hash"
- + " Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee -"
- + " $2
",
- "average_price_per_person", 15));
+ "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes"
+ + " - $10
- Steak and Eggs - $16
Sides
- Hash"
+ + " Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee -"
+ + " $2
",
+ "average_price_per_person",
+ 15));
restaurantDocs.put(
"goldenWaffle",
map(
- "name", "The Golden Waffle",
+ "name",
+ "The Golden Waffle",
"description",
- "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to"
- + " 11:00 AM.",
- "location", new GeoPoint(39.7183, -104.9621),
+ "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to"
+ + " 11:00 AM.",
+ "location",
+ new GeoPoint(39.7183, -104.9621),
"menu",
- "Signature Waffles
- Strawberry Delight - $11
- Chicken and"
- + " Waffles - $14
- Chocolate Chip Crunch -"
- + " $10
Drinks
- Fresh OJ - $4
- Artisan Coffee -"
- + " $3
",
- "average_price_per_person", 13));
+ "Signature Waffles
- Strawberry Delight - $11
- Chicken and"
+ + " Waffles - $14
- Chocolate Chip Crunch -"
+ + " $10
Drinks
- Fresh OJ - $4
- Artisan Coffee -"
+ + " $3
",
+ "average_price_per_person",
+ 13));
restaurantDocs.put(
"lotusBlossomThai",
map(
- "name", "Lotus Blossom Thai",
+ "name",
+ "Lotus Blossom Thai",
"description",
- "Authentic Thai cuisine featuring hand-crushed spices and traditional family"
- + " recipes from the Chiang Mai region.",
- "location", new GeoPoint(39.7315, -104.9847),
+ "Authentic Thai cuisine featuring hand-crushed spices and traditional family"
+ + " recipes from the Chiang Mai region.",
+ "location",
+ new GeoPoint(39.7315, -104.9847),
"menu",
- "Appetizers
- Spring Rolls - $7
- Chicken Satay -"
- + " $9
Main Course
- Pad Thai - $15
- Green Curry"
- + " - $16
- Drunken Noodles - $15
",
- "average_price_per_person", 22));
+ "Appetizers
- Spring Rolls - $7
- Chicken Satay -"
+ + " $9
Main Course
- Pad Thai - $15
- Green Curry"
+ + " - $16
- Drunken Noodles - $15
",
+ "average_price_per_person",
+ 22));
restaurantDocs.put(
"mileHighCatch",
map(
- "name", "Mile High Catch",
+ "name",
+ "Mile High Catch",
"description",
- "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic"
- + " shellfish in an upscale atmosphere.",
- "location", new GeoPoint(39.7401, -104.9903),
+ "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic"
+ + " shellfish in an upscale atmosphere.",
+ "location",
+ new GeoPoint(39.7401, -104.9903),
"menu",
- "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster"
- + " Cocktail - $22
Entrees
- Pan-Seared Salmon -"
- + " $28
- King Crab Legs - $45
- Fish and Chips - $19
",
- "average_price_per_person", 45));
+ "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster"
+ + " Cocktail - $22
Entrees
- Pan-Seared Salmon -"
+ + " $28
- King Crab Legs - $45
- Fish and Chips - $19
",
+ "average_price_per_person",
+ 45));
restaurantDocs.put(
"peakBurgers",
map(
- "name", "Peak Burgers",
+ "name",
+ "Peak Burgers",
"description",
- "Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
- "location", new GeoPoint(39.7622, -105.0125),
+ "Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
+ "location",
+ new GeoPoint(39.7622, -105.0125),
"menu",
- "Burgers
- The Peak Double - $12
- Bison Burger -"
- + " $15
- Veggie Stack - $11
Sides
- Truffle Fries"
- + " - $6
- Onion Rings - $5
",
- "average_price_per_person", 18));
+ "Burgers
- The Peak Double - $12
- Bison Burger -"
+ + " $15
- Veggie Stack - $11
Sides
- Truffle Fries"
+ + " - $6
- Onion Rings - $5
",
+ "average_price_per_person",
+ 18));
restaurantDocs.put(
"solTacos",
map(
- "name", "El Sol Tacos",
+ "name",
+ "El Sol Tacos",
"description",
- "A vibrant street-side taco stand serving up quick, delicious, and traditional"
- + " Mexican street food.",
- "location", new GeoPoint(39.6952, -105.0274),
+ "A vibrant street-side taco stand serving up quick, delicious, and traditional"
+ + " Mexican street food.",
+ "location",
+ new GeoPoint(39.6952, -105.0274),
"menu",
- "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo"
- + " Asado
- Nopales (Cactus)
Beverages
- Horchata"
- + " - $4
- Mexican Coke - $3
",
- "average_price_per_person", 12));
+ "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo"
+ + " Asado
- Nopales (Cactus)
Beverages
- Horchata"
+ + " - $4
- Mexican Coke - $3
",
+ "average_price_per_person",
+ 12));
restaurantDocs.put(
"eastsideTacos",
map(
- "name", "Eastside Cantina",
+ "name",
+ "Eastside Cantina",
"description",
- "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the"
- + " city.",
- "location", new GeoPoint(39.735, -104.885),
+ "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the"
+ + " city.",
+ "location",
+ new GeoPoint(39.735, -104.885),
"menu",
- "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos -"
- + " $4.50
- Shrimp Tacos - $5
Drinks
- House"
- + " Margarita - $9
- Jarritos - $3
",
- "average_price_per_person", 18));
+ "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos -"
+ + " $4.50
- Shrimp Tacos - $5
Drinks
- House"
+ + " Margarita - $9
- Jarritos - $3
",
+ "average_price_per_person",
+ 18));
restaurantDocs.put(
"eastsideChicken",
map(
- "name", "Eastside Chicken",
- "description", "Fried chicken to go - next to Eastside Cantina.",
- "location", new GeoPoint(39.735, -104.885),
+ "name",
+ "Eastside Chicken",
+ "description",
+ "Fried chicken to go - next to Eastside Cantina.",
+ "location",
+ new GeoPoint(39.735, -104.885),
"menu",
- "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich -"
- + " $9
Drinks
- House Margarita - $9
- Jarritos -"
- + " $3
",
- "average_price_per_person", 12));
+ "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich -"
+ + " $9
Drinks
- House Margarita - $9
- Jarritos -"
+ + " $3
",
+ "average_price_per_person",
+ 12));
}
@Override
From 62e45aee6b7fdba29d1362d8a0cc2f02eca727b0 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Wed, 1 Apr 2026 09:56:42 -0600
Subject: [PATCH 10/15] replaced DSL acronym will full word
---
.../firestore/pipeline/expressions/Expression.java | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
index 459eab3de8..142244fedf 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
@@ -4455,7 +4455,7 @@ public static Expression geoDistance(Field field, GeoPoint location) {
* db.pipeline().collection("restaurants").search(Search.withQuery(documentMatches("waffles OR pancakes")))
* }
*
- * @param rquery Define the search query using the search DSL.
+ * @param rquery Define the search query using the search domain-specific language (DSL).
* @return A new {@link BooleanExpression} representing the documentMatches operation.
*/
@BetaApi
@@ -4475,7 +4475,7 @@ public static BooleanExpression documentMatches(String rquery) {
* }
*
* @param fieldName Perform search on this field.
- * @param rquery Define the search query using the search DSL.
+ * @param rquery Define the search query using the search domain-specific language (DSL).
*/
@InternalApi
static BooleanExpression matches(String fieldName, String rquery) {
@@ -4488,7 +4488,7 @@ static BooleanExpression matches(String fieldName, String rquery) {
* This Expression can only be used within a {@code Search} stage.
*
* @param field Perform search on this field.
- * @param rquery Define the search query using the search DSL.
+ * @param rquery Define the search query using the search domain-specific language (DSL).
*/
@InternalApi
static BooleanExpression matches(Field field, String rquery) {
@@ -4531,7 +4531,7 @@ public static Expression score() {
* }
*
* @param fieldName Search the specified field for matching terms.
- * @param rquery Define the search query using the search DSL.
+ * @param rquery Define the search query using the search domain-specific language (DSL).
* @return A new {@link Expression} representing the snippet operation.
*/
@BetaApi
@@ -4554,7 +4554,7 @@ public static Expression snippet(String fieldName, String rquery) {
* )
* }
*
- * @param rquery Define the search query using the search DSL.
+ * @param rquery Define the search query using the search domain-specific language (DSL).
* @return A new {@link Expression} representing the snippet operation.
*/
@BetaApi
From fc16d5a979de5ab13075f112c50f6f2e635a47ca Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Thu, 2 Apr 2026 09:06:20 -0600
Subject: [PATCH 11/15] test cleanup
---
.../firestore/pipeline/stages/Search.java | 82 +-
.../cloud/firestore/PipelineProtoTest.java | 26 +-
.../firestore/it/ITPipelineSearchTest.java | 1205 +++++++++--------
3 files changed, 689 insertions(+), 624 deletions(-)
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
index 63b3f15492..76284eccd5 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
@@ -55,6 +55,7 @@ public final class Search extends Stage {
* Specifies if the `matches` and `snippet` expressions will enhance the user provided query to
* perform matching of synonyms, misspellings, lemmatization, stemming.
*/
+ @BetaApi
public static final class QueryEnhancement {
final String protoString;
@@ -118,23 +119,24 @@ public Search withAddFields(Selectable field, Selectable... additionalFields) {
return new Search(options.with("add_fields", encodeValue(map)));
}
- /** Specify the fields to keep or add to each document. */
- public Search withSelect(Selectable selection, Object... additionalSelections) {
- Selectable[] allSelections = new Selectable[additionalSelections.length + 1];
- allSelections[0] = selection;
- for (int i = 0; i < additionalSelections.length; i++) {
- allSelections[i + 1] = Selectable.toSelectable(additionalSelections[i]);
- }
- Map map =
- PipelineUtils.selectablesToMap(allSelections).entrySet().stream()
- .collect(Collectors.toMap(Map.Entry::getKey, e -> encodeValue(e.getValue())));
- return new Search(options.with("select", encodeValue(map)));
- }
-
- /** Specify the fields to keep or add to each document. */
- public Search withSelect(String fieldName, Object... additionalSelections) {
- return withSelect(field(fieldName), additionalSelections);
- }
+// TODO(search) enable with backend support
+// /** Specify the fields to keep or add to each document. */
+// public Search withSelect(Selectable selection, Object... additionalSelections) {
+// Selectable[] allSelections = new Selectable[additionalSelections.length + 1];
+// allSelections[0] = selection;
+// for (int i = 0; i < additionalSelections.length; i++) {
+// allSelections[i + 1] = Selectable.toSelectable(additionalSelections[i]);
+// }
+// Map map =
+// PipelineUtils.selectablesToMap(allSelections).entrySet().stream()
+// .collect(Collectors.toMap(Map.Entry::getKey, e -> encodeValue(e.getValue())));
+// return new Search(options.with("select", encodeValue(map)));
+// }
+
+// /** Specify the fields to keep or add to each document. */
+// public Search withSelect(String fieldName, Object... additionalSelections) {
+// return withSelect(field(fieldName), additionalSelections);
+// }
/** Specify how the returned documents are sorted. One or more ordering are required. */
public Search withSort(Ordering order, Ordering... additionalOrderings) {
@@ -145,10 +147,11 @@ public Search withSort(Ordering order, Ordering... additionalOrderings) {
options.with("sort", Lists.transform(Arrays.asList(allOrderings), Ordering::toProto)));
}
- /** Specify the maximum number of documents to return from the Search stage. */
- public Search withLimit(long limit) {
- return new Search(options.with("limit", encodeValue(limit)));
- }
+// TODO(search) enable with backend support
+// /** Specify the maximum number of documents to return from the Search stage. */
+// public Search withLimit(long limit) {
+// return new Search(options.with("limit", encodeValue(limit)));
+// }
/**
* Specify the maximum number of documents to retrieve. Documents will be retrieved in the
@@ -158,23 +161,26 @@ public Search withRetrievalDepth(long retrievalDepth) {
return new Search(options.with("retrieval_depth", encodeValue(retrievalDepth)));
}
- /** Specify the number of documents to skip. */
- public Search withOffset(long offset) {
- return new Search(options.with("offset", encodeValue(offset)));
- }
-
- /** Specify the BCP-47 language code of text in the search query, such as, “en-US” or “sr-Latn” */
- public Search withLanguageCode(String value) {
- return new Search(options.with("language_code", encodeValue(value)));
- }
-
- /**
- * Specify the query expansion behavior used by full-text search expressions in this search stage.
- * Default: {@code .PREFERRED}
- */
- public Search withQueryEnhancement(QueryEnhancement queryEnhancement) {
- return new Search(options.with("query_enhancement", queryEnhancement.toProto()));
- }
+ // TODO(search) enable with backend support
+// /** Specify the number of documents to skip. */
+// public Search withOffset(long offset) {
+// return new Search(options.with("offset", encodeValue(offset)));
+// }
+
+// TODO(search) enable with backend support
+// /** Specify the BCP-47 language code of text in the search query, such as, “en-US” or “sr-Latn” */
+// public Search withLanguageCode(String value) {
+// return new Search(options.with("language_code", encodeValue(value)));
+// }
+
+ // TODO(search) enable with backend support
+// /**
+// * Specify the query expansion behavior used by full-text search expressions in this search stage.
+// * Default: {@code .PREFERRED}
+// */
+// public Search withQueryEnhancement(QueryEnhancement queryEnhancement) {
+// return new Search(options.with("query_enhancement", queryEnhancement.toProto()));
+// }
@Override
Iterable toStageArgs() {
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
index 805aa034d9..f1b2411203 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
@@ -45,14 +45,14 @@ public void testSearchStageProtoEncoding() {
.collection("foo")
.search(
Search.withQuery("foo")
- .withLimit(1)
+// .withLimit(1)
.withRetrievalDepth(2)
- .withOffset(3)
- .withQueryEnhancement(Search.QueryEnhancement.REQUIRED)
- .withLanguageCode("en-US")
+// .withOffset(3)
+// .withQueryEnhancement(Search.QueryEnhancement.REQUIRED)
+// .withLanguageCode("en-US")
.withSort(field("foo").ascending())
- .withAddFields(constant(true).as("bar"))
- .withSelect(field("id")));
+ .withAddFields(constant(true).as("bar")));
+// .withSelect(field("id")));
com.google.firestore.v1.Pipeline protoPipeline = pipeline.toProto();
assertThat(protoPipeline.getStagesCount()).isEqualTo(2);
@@ -73,24 +73,24 @@ public void testSearchStageProtoEncoding() {
assertThat(query.getFunctionValue().getArgs(0).getStringValue()).isEqualTo("foo");
// limit
- assertThat(optionsMap.get("limit").getIntegerValue()).isEqualTo(1L);
+// assertThat(optionsMap.get("limit").getIntegerValue()).isEqualTo(1L);
// retrieval_depth
assertThat(optionsMap.get("retrieval_depth").getIntegerValue()).isEqualTo(2L);
// offset
- assertThat(optionsMap.get("offset").getIntegerValue()).isEqualTo(3L);
+// assertThat(optionsMap.get("offset").getIntegerValue()).isEqualTo(3L);
// query_enhancement
- assertThat(optionsMap.get("query_enhancement").getStringValue()).isEqualTo("required");
+// assertThat(optionsMap.get("query_enhancement").getStringValue()).isEqualTo("required");
// language_code
- assertThat(optionsMap.get("language_code").getStringValue()).isEqualTo("en-US");
+// assertThat(optionsMap.get("language_code").getStringValue()).isEqualTo("en-US");
// select
- Value select = optionsMap.get("select");
- assertThat(select.getMapValue().getFieldsMap().get("id").getFieldReferenceValue())
- .isEqualTo("id");
+// Value select = optionsMap.get("select");
+// assertThat(select.getMapValue().getFieldsMap().get("id").getFieldReferenceValue())
+// .isEqualTo("id");
// sort
Value sort = optionsMap.get("sort");
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index f1a8ba5b70..5ca04b6dff 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -35,6 +35,7 @@
import com.google.cloud.firestore.PipelineResult;
import com.google.cloud.firestore.WriteBatch;
import com.google.cloud.firestore.pipeline.stages.Search;
+
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -42,6 +43,7 @@
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -50,578 +52,635 @@
@RunWith(JUnit4.class)
public class ITPipelineSearchTest extends ITBaseTest {
- private CollectionReference restaurantsCollection;
-
- private static final Map> restaurantDocs = new HashMap<>();
-
- static {
- restaurantDocs.put(
- "sunnySideUp",
- map(
- "name",
- "The Sunny Side Up",
- "description",
- "A cozy neighborhood diner serving classic breakfast favorites all day long, from"
- + " fluffy pancakes to savory omelets.",
- "location",
- new GeoPoint(39.7541, -105.0002),
- "menu",
- "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes"
- + " - $10
- Steak and Eggs - $16
Sides
- Hash"
- + " Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee -"
- + " $2
",
- "average_price_per_person",
- 15));
- restaurantDocs.put(
- "goldenWaffle",
- map(
- "name",
- "The Golden Waffle",
- "description",
- "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to"
- + " 11:00 AM.",
- "location",
- new GeoPoint(39.7183, -104.9621),
- "menu",
- "Signature Waffles
- Strawberry Delight - $11
- Chicken and"
- + " Waffles - $14
- Chocolate Chip Crunch -"
- + " $10
Drinks
- Fresh OJ - $4
- Artisan Coffee -"
- + " $3
",
- "average_price_per_person",
- 13));
- restaurantDocs.put(
- "lotusBlossomThai",
- map(
- "name",
- "Lotus Blossom Thai",
- "description",
- "Authentic Thai cuisine featuring hand-crushed spices and traditional family"
- + " recipes from the Chiang Mai region.",
- "location",
- new GeoPoint(39.7315, -104.9847),
- "menu",
- "Appetizers
- Spring Rolls - $7
- Chicken Satay -"
- + " $9
Main Course
- Pad Thai - $15
- Green Curry"
- + " - $16
- Drunken Noodles - $15
",
- "average_price_per_person",
- 22));
- restaurantDocs.put(
- "mileHighCatch",
- map(
- "name",
- "Mile High Catch",
- "description",
- "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic"
- + " shellfish in an upscale atmosphere.",
- "location",
- new GeoPoint(39.7401, -104.9903),
- "menu",
- "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster"
- + " Cocktail - $22
Entrees
- Pan-Seared Salmon -"
- + " $28
- King Crab Legs - $45
- Fish and Chips - $19
",
- "average_price_per_person",
- 45));
- restaurantDocs.put(
- "peakBurgers",
- map(
- "name",
- "Peak Burgers",
- "description",
- "Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
- "location",
- new GeoPoint(39.7622, -105.0125),
- "menu",
- "Burgers
- The Peak Double - $12
- Bison Burger -"
- + " $15
- Veggie Stack - $11
Sides
- Truffle Fries"
- + " - $6
- Onion Rings - $5
",
- "average_price_per_person",
- 18));
- restaurantDocs.put(
- "solTacos",
- map(
- "name",
- "El Sol Tacos",
- "description",
- "A vibrant street-side taco stand serving up quick, delicious, and traditional"
- + " Mexican street food.",
- "location",
- new GeoPoint(39.6952, -105.0274),
- "menu",
- "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo"
- + " Asado
- Nopales (Cactus)
Beverages
- Horchata"
- + " - $4
- Mexican Coke - $3
",
- "average_price_per_person",
- 12));
- restaurantDocs.put(
- "eastsideTacos",
- map(
- "name",
- "Eastside Cantina",
- "description",
- "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the"
- + " city.",
- "location",
- new GeoPoint(39.735, -104.885),
- "menu",
- "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos -"
- + " $4.50
- Shrimp Tacos - $5
Drinks
- House"
- + " Margarita - $9
- Jarritos - $3
",
- "average_price_per_person",
- 18));
- restaurantDocs.put(
- "eastsideChicken",
- map(
- "name",
- "Eastside Chicken",
- "description",
- "Fried chicken to go - next to Eastside Cantina.",
- "location",
- new GeoPoint(39.735, -104.885),
- "menu",
- "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich -"
- + " $9
Drinks
- House Margarita - $9
- Jarritos -"
- + " $3
",
- "average_price_per_person",
- 12));
- }
-
- @Override
- public void primeBackend() throws Exception {
- // Disable priming as it uses Watch/Listen
- }
-
- @Before
- public void setupRestaurantDocs() throws Exception {
- assumeFalse(
- "This test suite only runs against the Enterprise edition.",
- !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
-
- restaurantsCollection = firestore.collection("SearchIntegrationTests");
-
- WriteBatch batch = firestore.batch();
- for (Map.Entry> entry : restaurantDocs.entrySet()) {
- batch.set(restaurantsCollection.document(entry.getKey()), entry.getValue());
+ private final String COLLECTION_NAME = "TextSearchIntegrationTests";
+
+ private CollectionReference restaurantsCollection;
+
+ private static final Map> restaurantDocs = new HashMap<>();
+
+ static {
+ restaurantDocs.put(
+ "sunnySideUp",
+ map(
+ "name",
+ "The Sunny Side Up",
+ "description",
+ "A cozy neighborhood diner serving classic breakfast favorites all day long, from"
+ + " fluffy pancakes to savory omelets.",
+ "location",
+ new GeoPoint(39.7541, -105.0002),
+ "menu",
+ "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes"
+ + " - $10
- Steak and Eggs - $16
Sides
- Hash"
+ + " Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee -"
+ + " $2
",
+ "average_price_per_person",
+ 15));
+ restaurantDocs.put(
+ "goldenWaffle",
+ map(
+ "name",
+ "The Golden Waffle",
+ "description",
+ "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to"
+ + " 11:00 AM.",
+ "location",
+ new GeoPoint(39.7183, -104.9621),
+ "menu",
+ "Signature Waffles
- Strawberry Delight - $11
- Chicken and"
+ + " Waffles - $14
- Chocolate Chip Crunch -"
+ + " $10
Drinks
- Fresh OJ - $4
- Artisan Coffee -"
+ + " $3
",
+ "average_price_per_person",
+ 13));
+ restaurantDocs.put(
+ "lotusBlossomThai",
+ map(
+ "name",
+ "Lotus Blossom Thai",
+ "description",
+ "Authentic Thai cuisine featuring hand-crushed spices and traditional family"
+ + " recipes from the Chiang Mai region.",
+ "location",
+ new GeoPoint(39.7315, -104.9847),
+ "menu",
+ "Appetizers
- Spring Rolls - $7
- Chicken Satay -"
+ + " $9
Main Course
- Pad Thai - $15
- Green Curry"
+ + " - $16
- Drunken Noodles - $15
",
+ "average_price_per_person",
+ 22));
+ restaurantDocs.put(
+ "mileHighCatch",
+ map(
+ "name",
+ "Mile High Catch",
+ "description",
+ "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic"
+ + " shellfish in an upscale atmosphere.",
+ "location",
+ new GeoPoint(39.7401, -104.9903),
+ "menu",
+ "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster"
+ + " Cocktail - $22
Entrees
- Pan-Seared Salmon -"
+ + " $28
- King Crab Legs - $45
- Fish and Chips - $19
",
+ "average_price_per_person",
+ 45));
+ restaurantDocs.put(
+ "peakBurgers",
+ map(
+ "name",
+ "Peak Burgers",
+ "description",
+ "Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
+ "location",
+ new GeoPoint(39.7622, -105.0125),
+ "menu",
+ "Burgers
- The Peak Double - $12
- Bison Burger -"
+ + " $15
- Veggie Stack - $11
Sides
- Truffle Fries"
+ + " - $6
- Onion Rings - $5
",
+ "average_price_per_person",
+ 18));
+ restaurantDocs.put(
+ "solTacos",
+ map(
+ "name",
+ "El Sol Tacos",
+ "description",
+ "A vibrant street-side taco stand serving up quick, delicious, and traditional"
+ + " Mexican street food.",
+ "location",
+ new GeoPoint(39.6952, -105.0274),
+ "menu",
+ "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo"
+ + " Asado
- Nopales (Cactus)
Beverages
- Horchata"
+ + " - $4
- Mexican Coke - $3
",
+ "average_price_per_person",
+ 12));
+ restaurantDocs.put(
+ "eastsideTacos",
+ map(
+ "name",
+ "Eastside Cantina",
+ "description",
+ "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the"
+ + " city.",
+ "location",
+ new GeoPoint(39.735, -104.885),
+ "menu",
+ "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos -"
+ + " $4.50
- Shrimp Tacos - $5
Drinks
- House"
+ + " Margarita - $9
- Jarritos - $3
",
+ "average_price_per_person",
+ 18));
+ restaurantDocs.put(
+ "eastsideChicken",
+ map(
+ "name",
+ "Eastside Chicken",
+ "description",
+ "Fried chicken to go - next to Eastside Cantina.",
+ "location",
+ new GeoPoint(39.735, -104.885),
+ "menu",
+ "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich -"
+ + " $9
Drinks
- House Margarita - $9
- Jarritos -"
+ + " $3
",
+ "average_price_per_person",
+ 12));
+ }
+
+ @Override
+ public void primeBackend() throws Exception {
+ // Disable priming as it uses Watch/Listen
}
- batch.commit().get(10, TimeUnit.SECONDS);
- }
-
- private void assertResultIds(Pipeline.Snapshot snapshot, String... ids) {
- List resultIds =
- snapshot.getResults().stream()
- .map(PipelineResult::getId)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
- assertThat(resultIds).containsExactlyElementsIn(Arrays.asList(ids)).inOrder();
- }
-
- // =========================================================================
- // Search stage
- // =========================================================================
-
- // --- DISABLE query expansion ---
-
- // query
- @Test
- public void searchWithLanguageCode() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery("waffles")
- .withLanguageCode("en")
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle");
- }
-
- @Test
- public void searchFullDocument() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery("waffles").withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle");
- }
-
- // @Test
- // public void searchSpecificField() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(restaurantsCollection.getId())
- // .search(
- // Search.withQuery(field("menu").matches("waffles"))
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "goldenWaffle");
- // }
-
- @Test
- public void geoNearQuery() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(
- field("location")
- .geoDistance(new GeoPoint(39.6985, -105.024))
- .lessThan(1000))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos");
- }
-
- @Test
- public void conjunctionOfTextSearchPredicates() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(and(documentMatches("waffles"), documentMatches("diner")))
- // TODO(search) switch back to field matches when supported by the backend
- // and(field("menu").matches("waffles"),
- // field("description").matches("diner")))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
- }
-
- // TODO(search) enable test when geo+text search indexes are supported
- // @Test
- // public void conjunctionOfTextSearchAndGeoNear() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(restaurantsCollection.getId())
- // .search(
- // Search.withQuery(
- // and(
- // field("menu").matches("tacos"),
- // field("location")
- // .geoDistance(new GeoPoint(39.6985, -105.024))
- // .lessThan(10000)))
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "solTacos");
- // }
-
- @Test
- public void negateMatch() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("-waffles"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(
- snapshot,
- "eastsideTacos",
- "solTacos",
- "peakBurgers",
- "mileHighCatch",
- "lotusBlossomThai",
- "sunnySideUp");
- }
-
- @Test
- public void rquerySearchTheDocumentWithConjunctionAndDisjunction() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("(waffles OR pancakes) AND coffee"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
- }
-
- @Test
- public void rqueryAsQueryParam() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery("(waffles OR pancakes) AND coffee")
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
- }
-
- // TODO(search) enable when rquery supports field paths
- // @Test
- // public void rquerySupportsFieldPaths() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(restaurantsCollection.getId())
- // .search(
- // Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all
- // day\"")
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "sunnySideUp");
- // }
-
- @Test
- public void conjunctionOfRqueryAndExpression() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(
- and(
- documentMatches("tacos"),
- greaterThanOrEqual("average_price_per_person", 8),
- lessThanOrEqual("average_price_per_person", 15)))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos");
- }
-
- // --- REQUIRE query expansion ---
-
- @Test
- public void requireQueryExpansion_searchFullDocument() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("waffles"))
- .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
- }
-
- // TODO(search) re-enable when backend supports field matches
- // @Test
- // public void requireQueryExpansion_searchSpecificField() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(restaurantsCollection.getId())
- // .search(
- // Search.withQuery(field("menu").matches("waffles"))
- // .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
- // }
-
- // add fields
- @Test
- public void addFields_topicalityScoreAndSnippet() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("waffles"))
- .withAddFields(
- score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
- .select("name", "searchScore", "snippet");
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertThat(snapshot.getResults()).hasSize(1);
- PipelineResult result = snapshot.getResults().get(0);
- assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
- assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
- assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
- }
-
- // select
- @Test
- public void select_topicalityScoreAndSnippet() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("waffles"))
- .withSelect(
- field("name"),
- field("location"),
- score().as("searchScore"),
- snippet("menu", "waffles").as("snippet"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertThat(snapshot.getResults()).hasSize(1);
- PipelineResult result = snapshot.getResults().get(0);
- assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
- assertThat(result.getData().get("location")).isEqualTo(new GeoPoint(39.7183, -104.9621));
- assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
- assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
-
- List sortedKeys =
- result.getData().keySet().stream().sorted().collect(Collectors.toList());
- assertThat(sortedKeys).containsExactly("location", "name", "searchScore", "snippet").inOrder();
- }
-
- // sort
- @Test
- public void sort_byTopicality() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("tacos"))
- .withSort(score().descending())
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "eastsideTacos", "solTacos");
- }
-
- @Test
- public void sort_byDistance() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(constant(true))
- .withSort(
- field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos", "eastsideTacos");
- }
-
- // TODO(search) re-enable when geo+text search indexes are supported
- // @Test
- // public void sort_byMultipleOrderings() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(restaurantsCollection.getId())
- // .search(
- // Search.withQuery(field("menu").matches("tacos OR chicken"))
- // .withSort(
- // field("location").geoDistance(new GeoPoint(39.6985,
- // -105.024)).ascending(),
- // score().descending())
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "solTacos", "eastsideTacos", "eastsideChicken");
- // }
-
- // limit
- @Test
- public void limit_limitsTheNumberOfDocumentsReturned() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(constant(true))
- .withSort(
- field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
- .withLimit(5)
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "goldenWaffle");
- }
-
- @Test
- public void limit_limitsTheNumberOfDocumentsScored() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("chicken OR tacos OR fish OR waffles"))
- .withRetrievalDepth(6)
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "eastsideChicken", "eastsideTacos", "solTacos", "mileHighCatch");
- }
-
- // offset
- @Test
- public void offset_skipsNDocuments() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(constant(true))
- .withLimit(2)
- .withOffset(2)
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "eastsideChicken", "eastsideTacos");
- }
-
- // =========================================================================
- // Snippet
- // =========================================================================
-
- @Test
- public void snippetOnMultipleFields() throws Exception {
- // Get snippet from 1 field
- Pipeline pipeline1 =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("waffle"))
- .withAddFields(snippet("menu", "waffles").as("snippet"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot1 = pipeline1.execute().get();
- assertThat(snapshot1.getResults()).hasSize(1);
- assertThat(snapshot1.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
- String snip1 = (String) snapshot1.getResults().get(0).getData().get("snippet");
- assertThat(snip1.length()).isGreaterThan(0);
-
- // Get snippet from 2 fields
- Pipeline pipeline2 =
- firestore
- .pipeline()
- .collection(restaurantsCollection.getId())
- .search(
- Search.withQuery(documentMatches("waffle"))
- .withAddFields(
- concat(field("menu"), field("description"))
- .snippet("waffles") // Without SnippetOptions in Java
- .as("snippet"))
- .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot2 = pipeline2.execute().get();
- assertThat(snapshot2.getResults()).hasSize(1);
- assertThat(snapshot2.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
- String snip2 = (String) snapshot2.getResults().get(0).getData().get("snippet");
- assertThat(snip2.length()).isGreaterThan(snip1.length());
- }
+
+ @Before
+ public void setupRestaurantDocs() throws Exception {
+ assumeFalse(
+ "This test suite only runs against the Enterprise edition.",
+ !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
+
+ restaurantsCollection = firestore.collection(COLLECTION_NAME);
+
+ WriteBatch batch = firestore.batch();
+ for (Map.Entry> entry : restaurantDocs.entrySet()) {
+ batch.set(restaurantsCollection.document(entry.getKey()), entry.getValue());
+ }
+ batch.commit().get(10, TimeUnit.SECONDS);
+ }
+
+ private void assertResultIds(Pipeline.Snapshot snapshot, String... ids) {
+ List resultIds =
+ snapshot.getResults().stream()
+ .map(PipelineResult::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ assertThat(resultIds).containsExactlyElementsIn(Arrays.asList(ids)).inOrder();
+ }
+
+ // =========================================================================
+ // Search stage
+ // =========================================================================
+
+ // --- DISABLE query expansion ---
+
+ // query
+// TODO(search) enable with backend support
+// @Test
+// public void searchWithLanguageCode() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery("waffles")
+// .withLanguageCode("en")
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "goldenWaffle");
+// }
+
+ @Test
+ public void searchFullDocument() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery("waffles"));
+// Search.withQuery("waffles").withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle");
+ }
+
+ // @Test
+ // public void searchSpecificField() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(field("menu").matches("waffles"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle");
+ // }
+
+ @Test
+ public void geoNearQuery() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(
+ field("location")
+ .geoDistance(new GeoPoint(39.6985, -105.024))
+ .lessThanOrEqual(1000)));
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos");
+ }
+
+// TODO(search) enable with backend support
+// @Test
+// public void conjunctionOfTextSearchPredicates() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(
+// and(field("menu").matches("waffles"),
+// field("description").matches("diner")))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+// }
+
+ // TODO(search) enable test when geo+text search indexes are supported
+ // @Test
+ // public void conjunctionOfTextSearchAndGeoNear() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(
+ // and(
+ // field("menu").matches("tacos"),
+ // field("location")
+ // .geoDistance(new GeoPoint(39.6985, -105.024))
+ // .lessThan(10000)))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos");
+ // }
+
+ @Test
+ public void negateMatch() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(documentMatches("coffee -waffles")));
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(
+ snapshot,
+ "sunnySideUp");
+ }
+
+// TODO(search) enable with backend support
+// @Test
+// public void rquerySearchTheDocumentWithConjunctionAndDisjunction() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("(waffles OR pancakes) AND coffee"))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+// }
+
+ @Test
+ public void rqueryAsQueryParam() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery("chicken wings"));
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "eastsideChicken");
+ }
+
+ // TODO(search) enable when rquery supports field paths
+ // @Test
+ // public void rquerySupportsFieldPaths() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all
+ // day\"")
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "sunnySideUp");
+ // }
+
+// TODO(search) enable with backend support
+// @Test
+// public void conjunctionOfRqueryAndExpression() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(
+// and(
+// documentMatches("tacos"),
+// greaterThanOrEqual("average_price_per_person", 8),
+// lessThanOrEqual("average_price_per_person", 15)))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "solTacos");
+// }
+
+ // --- REQUIRE query expansion ---
+
+// TODO(search) enable with backend support
+// @Test
+// public void requireQueryExpansion_searchFullDocument() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("waffles"))
+// .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+// }
+
+ // TODO(search) re-enable when backend supports field matches and QueryEnhancement
+ // @Test
+ // public void requireQueryExpansion_searchSpecificField() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(field("menu").matches("waffles"))
+ // .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ // }
+
+ // add fields
+ @Test
+ public void addFields_score() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(documentMatches("waffles"))
+ .withAddFields(score().as("searchScore")))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+ .select("name", "searchScore");
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertThat(snapshot.getResults()).hasSize(1);
+ PipelineResult result = snapshot.getResults().get(0);
+ assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ }
+
+// @Test
+// public void addFields_geoDistance() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("waffles"))
+// .withAddFields(
+// field("location").geoDistance(new GeoPoint(39.6985, -105.024)).as("geoDistance")))
+//// .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+// .select("name", "geoDistance");
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertThat(snapshot.getResults()).hasSize(1);
+// PipelineResult result = snapshot.getResults().get(0);
+// assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+// assertThat((Double) result.getData().get("geoDistance")).isGreaterThan(0.0);
+// }
+
+ // TODO(search) enable with backend support
+// @Test
+// public void addFields_multipleFields() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("waffles"))
+// .withAddFields(
+// score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+// .select("name", "searchScore", "snippet");
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertThat(snapshot.getResults()).hasSize(1);
+// PipelineResult result = snapshot.getResults().get(0);
+// assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+// assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+// assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+// }
+
+ // select
+ // TODO(search) enable with backend support
+// @Test
+// public void select_topicalityScoreAndSnippet() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("waffles"))
+// .withSelect(
+// field("name"),
+// field("location"),
+// score().as("searchScore"),
+// snippet("menu", "waffles").as("snippet"))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertThat(snapshot.getResults()).hasSize(1);
+// PipelineResult result = snapshot.getResults().get(0);
+// assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+// assertThat(result.getData().get("location")).isEqualTo(new GeoPoint(39.7183, -104.9621));
+// assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+// assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+//
+// List sortedKeys =
+// result.getData().keySet().stream().sorted().collect(Collectors.toList());
+// assertThat(sortedKeys).containsExactly("location", "name", "searchScore", "snippet").inOrder();
+// }
+
+ // sort
+ @Test
+ public void sort_byScore() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(documentMatches("tacos"))
+ .withSort(score().descending()));
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "eastsideTacos", "solTacos");
+ }
+
+ @Test
+ public void sort_byDistance() throws Exception {
+ GeoPoint queryLocation = new GeoPoint(39.6985, -105.024);
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(field("location").geoDistance(queryLocation).lessThanOrEqual(5600))
+ .withSort(field("location").geoDistance(queryLocation).ascending()));
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "mileHighCatch");
+ }
+
+ // TODO(search) re-enable when geo+text search indexes are supported
+ // @Test
+ // public void sort_byMultipleOrderings() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(field("menu").matches("tacos OR chicken"))
+ // .withSort(
+ // field("location").geoDistance(new GeoPoint(39.6985,
+ // -105.024)).ascending(),
+ // score().descending())
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos", "eastsideTacos", "eastsideChicken");
+ // }
+
+ // limit
+ // TODO(search) enable with backend support
+// @Test
+// public void limit_limitsTheNumberOfDocumentsReturned() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(constant(true))
+// .withSort(
+// field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
+// .withLimit(5)
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "goldenWaffle");
+// }
+
+// @Test
+// public void limit_limitsTheNumberOfDocumentsRetrieved() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("chicken"))
+// .withRetrievalDepth(3));
+//// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "eastsideChicken", "lotusBlossomThai", "goldenWaffle");
+//
+// pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("chicken")));
+//// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "eastsideChicken", "lotusBlossomThai", "goldenWaffle", "eastsideCantina");
+// }
+
+ // offset
+ // TODO(search) enable with backend support
+// @Test
+// public void offset_skipsNDocuments() throws Exception {
+// Pipeline pipeline =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(constant(true))
+// .withLimit(2)
+// .withOffset(2)
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot = pipeline.execute().get();
+// assertResultIds(snapshot, "eastsideChicken", "eastsideTacos");
+// }
+
+ // =========================================================================
+ // Snippet
+ // =========================================================================
+
+ // TODO(search) enable with backend support
+// @Test
+// public void snippetOnMultipleFields() throws Exception {
+// // Get snippet from 1 field
+// Pipeline pipeline1 =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("waffle"))
+// .withAddFields(snippet("menu", "waffles").as("snippet"))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot1 = pipeline1.execute().get();
+// assertThat(snapshot1.getResults()).hasSize(1);
+// assertThat(snapshot1.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
+// String snip1 = (String) snapshot1.getResults().get(0).getData().get("snippet");
+// assertThat(snip1.length()).isGreaterThan(0);
+//
+// // Get snippet from 2 fields
+// Pipeline pipeline2 =
+// firestore
+// .pipeline()
+// .collection(COLLECTION_NAME)
+// .search(
+// Search.withQuery(documentMatches("waffle"))
+// .withAddFields(
+// concat(field("menu"), field("description"))
+// .snippet("waffles") // Without SnippetOptions in Java
+// .as("snippet"))
+// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+//
+// Pipeline.Snapshot snapshot2 = pipeline2.execute().get();
+// assertThat(snapshot2.getResults()).hasSize(1);
+// assertThat(snapshot2.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
+// String snip2 = (String) snapshot2.getResults().get(0).getData().get("snippet");
+// assertThat(snip2.length()).isGreaterThan(snip1.length());
+// }
}
From e4e308e6939f32c33666cd1f2d498a5c56b31485 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Thu, 2 Apr 2026 09:26:37 -0600
Subject: [PATCH 12/15] Formatting
---
.../firestore/pipeline/stages/Search.java | 82 +-
.../cloud/firestore/PipelineProtoTest.java | 24 +-
.../firestore/it/ITPipelineSearchTest.java | 1258 ++++++++---------
3 files changed, 677 insertions(+), 687 deletions(-)
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
index 76284eccd5..9779be6fc3 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
@@ -119,24 +119,24 @@ public Search withAddFields(Selectable field, Selectable... additionalFields) {
return new Search(options.with("add_fields", encodeValue(map)));
}
-// TODO(search) enable with backend support
-// /** Specify the fields to keep or add to each document. */
-// public Search withSelect(Selectable selection, Object... additionalSelections) {
-// Selectable[] allSelections = new Selectable[additionalSelections.length + 1];
-// allSelections[0] = selection;
-// for (int i = 0; i < additionalSelections.length; i++) {
-// allSelections[i + 1] = Selectable.toSelectable(additionalSelections[i]);
-// }
-// Map map =
-// PipelineUtils.selectablesToMap(allSelections).entrySet().stream()
-// .collect(Collectors.toMap(Map.Entry::getKey, e -> encodeValue(e.getValue())));
-// return new Search(options.with("select", encodeValue(map)));
-// }
-
-// /** Specify the fields to keep or add to each document. */
-// public Search withSelect(String fieldName, Object... additionalSelections) {
-// return withSelect(field(fieldName), additionalSelections);
-// }
+ // TODO(search) enable with backend support
+ // /** Specify the fields to keep or add to each document. */
+ // public Search withSelect(Selectable selection, Object... additionalSelections) {
+ // Selectable[] allSelections = new Selectable[additionalSelections.length + 1];
+ // allSelections[0] = selection;
+ // for (int i = 0; i < additionalSelections.length; i++) {
+ // allSelections[i + 1] = Selectable.toSelectable(additionalSelections[i]);
+ // }
+ // Map map =
+ // PipelineUtils.selectablesToMap(allSelections).entrySet().stream()
+ // .collect(Collectors.toMap(Map.Entry::getKey, e -> encodeValue(e.getValue())));
+ // return new Search(options.with("select", encodeValue(map)));
+ // }
+
+ // /** Specify the fields to keep or add to each document. */
+ // public Search withSelect(String fieldName, Object... additionalSelections) {
+ // return withSelect(field(fieldName), additionalSelections);
+ // }
/** Specify how the returned documents are sorted. One or more ordering are required. */
public Search withSort(Ordering order, Ordering... additionalOrderings) {
@@ -147,11 +147,11 @@ public Search withSort(Ordering order, Ordering... additionalOrderings) {
options.with("sort", Lists.transform(Arrays.asList(allOrderings), Ordering::toProto)));
}
-// TODO(search) enable with backend support
-// /** Specify the maximum number of documents to return from the Search stage. */
-// public Search withLimit(long limit) {
-// return new Search(options.with("limit", encodeValue(limit)));
-// }
+ // TODO(search) enable with backend support
+ // /** Specify the maximum number of documents to return from the Search stage. */
+ // public Search withLimit(long limit) {
+ // return new Search(options.with("limit", encodeValue(limit)));
+ // }
/**
* Specify the maximum number of documents to retrieve. Documents will be retrieved in the
@@ -162,25 +162,27 @@ public Search withRetrievalDepth(long retrievalDepth) {
}
// TODO(search) enable with backend support
-// /** Specify the number of documents to skip. */
-// public Search withOffset(long offset) {
-// return new Search(options.with("offset", encodeValue(offset)));
-// }
-
-// TODO(search) enable with backend support
-// /** Specify the BCP-47 language code of text in the search query, such as, “en-US” or “sr-Latn” */
-// public Search withLanguageCode(String value) {
-// return new Search(options.with("language_code", encodeValue(value)));
-// }
+ // /** Specify the number of documents to skip. */
+ // public Search withOffset(long offset) {
+ // return new Search(options.with("offset", encodeValue(offset)));
+ // }
+
+ // TODO(search) enable with backend support
+ // /** Specify the BCP-47 language code of text in the search query, such as, “en-US” or
+ // “sr-Latn” */
+ // public Search withLanguageCode(String value) {
+ // return new Search(options.with("language_code", encodeValue(value)));
+ // }
// TODO(search) enable with backend support
-// /**
-// * Specify the query expansion behavior used by full-text search expressions in this search stage.
-// * Default: {@code .PREFERRED}
-// */
-// public Search withQueryEnhancement(QueryEnhancement queryEnhancement) {
-// return new Search(options.with("query_enhancement", queryEnhancement.toProto()));
-// }
+ // /**
+ // * Specify the query expansion behavior used by full-text search expressions in this search
+ // stage.
+ // * Default: {@code .PREFERRED}
+ // */
+ // public Search withQueryEnhancement(QueryEnhancement queryEnhancement) {
+ // return new Search(options.with("query_enhancement", queryEnhancement.toProto()));
+ // }
@Override
Iterable toStageArgs() {
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
index f1b2411203..42b647e843 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/PipelineProtoTest.java
@@ -45,14 +45,14 @@ public void testSearchStageProtoEncoding() {
.collection("foo")
.search(
Search.withQuery("foo")
-// .withLimit(1)
+ // .withLimit(1)
.withRetrievalDepth(2)
-// .withOffset(3)
-// .withQueryEnhancement(Search.QueryEnhancement.REQUIRED)
-// .withLanguageCode("en-US")
+ // .withOffset(3)
+ // .withQueryEnhancement(Search.QueryEnhancement.REQUIRED)
+ // .withLanguageCode("en-US")
.withSort(field("foo").ascending())
.withAddFields(constant(true).as("bar")));
-// .withSelect(field("id")));
+ // .withSelect(field("id")));
com.google.firestore.v1.Pipeline protoPipeline = pipeline.toProto();
assertThat(protoPipeline.getStagesCount()).isEqualTo(2);
@@ -73,24 +73,24 @@ public void testSearchStageProtoEncoding() {
assertThat(query.getFunctionValue().getArgs(0).getStringValue()).isEqualTo("foo");
// limit
-// assertThat(optionsMap.get("limit").getIntegerValue()).isEqualTo(1L);
+ // assertThat(optionsMap.get("limit").getIntegerValue()).isEqualTo(1L);
// retrieval_depth
assertThat(optionsMap.get("retrieval_depth").getIntegerValue()).isEqualTo(2L);
// offset
-// assertThat(optionsMap.get("offset").getIntegerValue()).isEqualTo(3L);
+ // assertThat(optionsMap.get("offset").getIntegerValue()).isEqualTo(3L);
// query_enhancement
-// assertThat(optionsMap.get("query_enhancement").getStringValue()).isEqualTo("required");
+ // assertThat(optionsMap.get("query_enhancement").getStringValue()).isEqualTo("required");
// language_code
-// assertThat(optionsMap.get("language_code").getStringValue()).isEqualTo("en-US");
+ // assertThat(optionsMap.get("language_code").getStringValue()).isEqualTo("en-US");
// select
-// Value select = optionsMap.get("select");
-// assertThat(select.getMapValue().getFieldsMap().get("id").getFieldReferenceValue())
-// .isEqualTo("id");
+ // Value select = optionsMap.get("select");
+ // assertThat(select.getMapValue().getFieldsMap().get("id").getFieldReferenceValue())
+ // .isEqualTo("id");
// sort
Value sort = optionsMap.get("sort");
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index 5ca04b6dff..0a26b3cdb0 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -17,15 +17,9 @@
package com.google.cloud.firestore.it;
import static com.google.cloud.firestore.it.ITQueryTest.map;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.and;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.concat;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.constant;
import static com.google.cloud.firestore.pipeline.expressions.Expression.documentMatches;
import static com.google.cloud.firestore.pipeline.expressions.Expression.field;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.greaterThanOrEqual;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.lessThanOrEqual;
import static com.google.cloud.firestore.pipeline.expressions.Expression.score;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.snippet;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
@@ -35,7 +29,6 @@
import com.google.cloud.firestore.PipelineResult;
import com.google.cloud.firestore.WriteBatch;
import com.google.cloud.firestore.pipeline.stages.Search;
-
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -43,7 +36,6 @@
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -54,633 +46,629 @@ public class ITPipelineSearchTest extends ITBaseTest {
private final String COLLECTION_NAME = "TextSearchIntegrationTests";
- private CollectionReference restaurantsCollection;
-
- private static final Map> restaurantDocs = new HashMap<>();
-
- static {
- restaurantDocs.put(
- "sunnySideUp",
- map(
- "name",
- "The Sunny Side Up",
- "description",
- "A cozy neighborhood diner serving classic breakfast favorites all day long, from"
- + " fluffy pancakes to savory omelets.",
- "location",
- new GeoPoint(39.7541, -105.0002),
- "menu",
- "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes"
- + " - $10
- Steak and Eggs - $16
Sides
- Hash"
- + " Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee -"
- + " $2
",
- "average_price_per_person",
- 15));
- restaurantDocs.put(
- "goldenWaffle",
- map(
- "name",
- "The Golden Waffle",
- "description",
- "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to"
- + " 11:00 AM.",
- "location",
- new GeoPoint(39.7183, -104.9621),
- "menu",
- "Signature Waffles
- Strawberry Delight - $11
- Chicken and"
- + " Waffles - $14
- Chocolate Chip Crunch -"
- + " $10
Drinks
- Fresh OJ - $4
- Artisan Coffee -"
- + " $3
",
- "average_price_per_person",
- 13));
- restaurantDocs.put(
- "lotusBlossomThai",
- map(
- "name",
- "Lotus Blossom Thai",
- "description",
- "Authentic Thai cuisine featuring hand-crushed spices and traditional family"
- + " recipes from the Chiang Mai region.",
- "location",
- new GeoPoint(39.7315, -104.9847),
- "menu",
- "Appetizers
- Spring Rolls - $7
- Chicken Satay -"
- + " $9
Main Course
- Pad Thai - $15
- Green Curry"
- + " - $16
- Drunken Noodles - $15
",
- "average_price_per_person",
- 22));
- restaurantDocs.put(
- "mileHighCatch",
- map(
- "name",
- "Mile High Catch",
- "description",
- "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic"
- + " shellfish in an upscale atmosphere.",
- "location",
- new GeoPoint(39.7401, -104.9903),
- "menu",
- "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster"
- + " Cocktail - $22
Entrees
- Pan-Seared Salmon -"
- + " $28
- King Crab Legs - $45
- Fish and Chips - $19
",
- "average_price_per_person",
- 45));
- restaurantDocs.put(
- "peakBurgers",
- map(
- "name",
- "Peak Burgers",
- "description",
- "Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
- "location",
- new GeoPoint(39.7622, -105.0125),
- "menu",
- "Burgers
- The Peak Double - $12
- Bison Burger -"
- + " $15
- Veggie Stack - $11
Sides
- Truffle Fries"
- + " - $6
- Onion Rings - $5
",
- "average_price_per_person",
- 18));
- restaurantDocs.put(
- "solTacos",
- map(
- "name",
- "El Sol Tacos",
- "description",
- "A vibrant street-side taco stand serving up quick, delicious, and traditional"
- + " Mexican street food.",
- "location",
- new GeoPoint(39.6952, -105.0274),
- "menu",
- "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo"
- + " Asado
- Nopales (Cactus)
Beverages
- Horchata"
- + " - $4
- Mexican Coke - $3
",
- "average_price_per_person",
- 12));
- restaurantDocs.put(
- "eastsideTacos",
- map(
- "name",
- "Eastside Cantina",
- "description",
- "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the"
- + " city.",
- "location",
- new GeoPoint(39.735, -104.885),
- "menu",
- "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos -"
- + " $4.50
- Shrimp Tacos - $5
Drinks
- House"
- + " Margarita - $9
- Jarritos - $3
",
- "average_price_per_person",
- 18));
- restaurantDocs.put(
- "eastsideChicken",
- map(
- "name",
- "Eastside Chicken",
- "description",
- "Fried chicken to go - next to Eastside Cantina.",
- "location",
- new GeoPoint(39.735, -104.885),
- "menu",
- "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich -"
- + " $9
Drinks
- House Margarita - $9
- Jarritos -"
- + " $3
",
- "average_price_per_person",
- 12));
- }
-
- @Override
- public void primeBackend() throws Exception {
- // Disable priming as it uses Watch/Listen
- }
-
- @Before
- public void setupRestaurantDocs() throws Exception {
- assumeFalse(
- "This test suite only runs against the Enterprise edition.",
- !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
-
- restaurantsCollection = firestore.collection(COLLECTION_NAME);
-
- WriteBatch batch = firestore.batch();
- for (Map.Entry> entry : restaurantDocs.entrySet()) {
- batch.set(restaurantsCollection.document(entry.getKey()), entry.getValue());
- }
- batch.commit().get(10, TimeUnit.SECONDS);
- }
-
- private void assertResultIds(Pipeline.Snapshot snapshot, String... ids) {
- List resultIds =
- snapshot.getResults().stream()
- .map(PipelineResult::getId)
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
- assertThat(resultIds).containsExactlyElementsIn(Arrays.asList(ids)).inOrder();
- }
-
- // =========================================================================
- // Search stage
- // =========================================================================
-
- // --- DISABLE query expansion ---
-
- // query
-// TODO(search) enable with backend support
-// @Test
-// public void searchWithLanguageCode() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery("waffles")
-// .withLanguageCode("en")
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "goldenWaffle");
-// }
-
- @Test
- public void searchFullDocument() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(COLLECTION_NAME)
- .search(
- Search.withQuery("waffles"));
-// Search.withQuery("waffles").withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "goldenWaffle");
- }
-
- // @Test
- // public void searchSpecificField() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(COLLECTION_NAME)
- // .search(
- // Search.withQuery(field("menu").matches("waffles"))
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "goldenWaffle");
- // }
-
- @Test
- public void geoNearQuery() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(COLLECTION_NAME)
- .search(
- Search.withQuery(
- field("location")
- .geoDistance(new GeoPoint(39.6985, -105.024))
- .lessThanOrEqual(1000)));
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos");
+ private CollectionReference restaurantsCollection;
+
+ private static final Map> restaurantDocs = new HashMap<>();
+
+ static {
+ restaurantDocs.put(
+ "sunnySideUp",
+ map(
+ "name",
+ "The Sunny Side Up",
+ "description",
+ "A cozy neighborhood diner serving classic breakfast favorites all day long, from"
+ + " fluffy pancakes to savory omelets.",
+ "location",
+ new GeoPoint(39.7541, -105.0002),
+ "menu",
+ "Breakfast Classics
- Denver Omelet - $12
- Buttermilk Pancakes"
+ + " - $10
- Steak and Eggs - $16
Sides
- Hash"
+ + " Browns - $4
- Thick-cut Bacon - $5
- Drip Coffee -"
+ + " $2
",
+ "average_price_per_person",
+ 15));
+ restaurantDocs.put(
+ "goldenWaffle",
+ map(
+ "name",
+ "The Golden Waffle",
+ "description",
+ "Specializing exclusively in Belgian-style waffles. Open daily from 6:00 AM to"
+ + " 11:00 AM.",
+ "location",
+ new GeoPoint(39.7183, -104.9621),
+ "menu",
+ "Signature Waffles
- Strawberry Delight - $11
- Chicken and"
+ + " Waffles - $14
- Chocolate Chip Crunch -"
+ + " $10
Drinks
- Fresh OJ - $4
- Artisan Coffee -"
+ + " $3
",
+ "average_price_per_person",
+ 13));
+ restaurantDocs.put(
+ "lotusBlossomThai",
+ map(
+ "name",
+ "Lotus Blossom Thai",
+ "description",
+ "Authentic Thai cuisine featuring hand-crushed spices and traditional family"
+ + " recipes from the Chiang Mai region.",
+ "location",
+ new GeoPoint(39.7315, -104.9847),
+ "menu",
+ "Appetizers
- Spring Rolls - $7
- Chicken Satay -"
+ + " $9
Main Course
- Pad Thai - $15
- Green Curry"
+ + " - $16
- Drunken Noodles - $15
",
+ "average_price_per_person",
+ 22));
+ restaurantDocs.put(
+ "mileHighCatch",
+ map(
+ "name",
+ "Mile High Catch",
+ "description",
+ "Freshly sourced seafood offering a wide variety of Pacific fish and Atlantic"
+ + " shellfish in an upscale atmosphere.",
+ "location",
+ new GeoPoint(39.7401, -104.9903),
+ "menu",
+ "From the Raw Bar
- Oysters (Half Dozen) - $18
- Lobster"
+ + " Cocktail - $22
Entrees
- Pan-Seared Salmon -"
+ + " $28
- King Crab Legs - $45
- Fish and Chips - $19
",
+ "average_price_per_person",
+ 45));
+ restaurantDocs.put(
+ "peakBurgers",
+ map(
+ "name",
+ "Peak Burgers",
+ "description",
+ "Casual burger joint focused on locally sourced Colorado beef and hand-cut fries.",
+ "location",
+ new GeoPoint(39.7622, -105.0125),
+ "menu",
+ "Burgers
- The Peak Double - $12
- Bison Burger -"
+ + " $15
- Veggie Stack - $11
Sides
- Truffle Fries"
+ + " - $6
- Onion Rings - $5
",
+ "average_price_per_person",
+ 18));
+ restaurantDocs.put(
+ "solTacos",
+ map(
+ "name",
+ "El Sol Tacos",
+ "description",
+ "A vibrant street-side taco stand serving up quick, delicious, and traditional"
+ + " Mexican street food.",
+ "location",
+ new GeoPoint(39.6952, -105.0274),
+ "menu",
+ "Tacos ($3.50 each)
- Al Pastor
- Carne Asada
- Pollo"
+ + " Asado
- Nopales (Cactus)
Beverages
- Horchata"
+ + " - $4
- Mexican Coke - $3
",
+ "average_price_per_person",
+ 12));
+ restaurantDocs.put(
+ "eastsideTacos",
+ map(
+ "name",
+ "Eastside Cantina",
+ "description",
+ "Authentic street tacos and hand-shaken margaritas on the vibrant east side of the"
+ + " city.",
+ "location",
+ new GeoPoint(39.735, -104.885),
+ "menu",
+ "Tacos
- Carnitas Tacos - $4
- Barbacoa Tacos -"
+ + " $4.50
- Shrimp Tacos - $5
Drinks
- House"
+ + " Margarita - $9
- Jarritos - $3
",
+ "average_price_per_person",
+ 18));
+ restaurantDocs.put(
+ "eastsideChicken",
+ map(
+ "name",
+ "Eastside Chicken",
+ "description",
+ "Fried chicken to go - next to Eastside Cantina.",
+ "location",
+ new GeoPoint(39.735, -104.885),
+ "menu",
+ "Fried Chicken
- Drumstick - $4
- Wings - $1
- Sandwich -"
+ + " $9
Drinks
- House Margarita - $9
- Jarritos -"
+ + " $3
",
+ "average_price_per_person",
+ 12));
+ }
+
+ @Override
+ public void primeBackend() throws Exception {
+ // Disable priming as it uses Watch/Listen
+ }
+
+ @Before
+ public void setupRestaurantDocs() throws Exception {
+ assumeFalse(
+ "This test suite only runs against the Enterprise edition.",
+ !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
+
+ restaurantsCollection = firestore.collection(COLLECTION_NAME);
+
+ WriteBatch batch = firestore.batch();
+ for (Map.Entry> entry : restaurantDocs.entrySet()) {
+ batch.set(restaurantsCollection.document(entry.getKey()), entry.getValue());
}
-
-// TODO(search) enable with backend support
-// @Test
-// public void conjunctionOfTextSearchPredicates() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(
-// and(field("menu").matches("waffles"),
-// field("description").matches("diner")))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
-// }
-
- // TODO(search) enable test when geo+text search indexes are supported
- // @Test
- // public void conjunctionOfTextSearchAndGeoNear() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(COLLECTION_NAME)
- // .search(
- // Search.withQuery(
- // and(
- // field("menu").matches("tacos"),
- // field("location")
- // .geoDistance(new GeoPoint(39.6985, -105.024))
- // .lessThan(10000)))
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "solTacos");
- // }
-
- @Test
- public void negateMatch() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(COLLECTION_NAME)
- .search(
- Search.withQuery(documentMatches("coffee -waffles")));
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(
- snapshot,
- "sunnySideUp");
- }
-
-// TODO(search) enable with backend support
-// @Test
-// public void rquerySearchTheDocumentWithConjunctionAndDisjunction() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("(waffles OR pancakes) AND coffee"))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
-// }
-
- @Test
- public void rqueryAsQueryParam() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(COLLECTION_NAME)
- .search(
- Search.withQuery("chicken wings"));
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "eastsideChicken");
- }
-
- // TODO(search) enable when rquery supports field paths
- // @Test
- // public void rquerySupportsFieldPaths() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(COLLECTION_NAME)
- // .search(
- // Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all
- // day\"")
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "sunnySideUp");
- // }
-
-// TODO(search) enable with backend support
-// @Test
-// public void conjunctionOfRqueryAndExpression() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(
-// and(
-// documentMatches("tacos"),
-// greaterThanOrEqual("average_price_per_person", 8),
-// lessThanOrEqual("average_price_per_person", 15)))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "solTacos");
-// }
-
- // --- REQUIRE query expansion ---
-
-// TODO(search) enable with backend support
-// @Test
-// public void requireQueryExpansion_searchFullDocument() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("waffles"))
-// .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
-// }
-
- // TODO(search) re-enable when backend supports field matches and QueryEnhancement
- // @Test
- // public void requireQueryExpansion_searchSpecificField() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(COLLECTION_NAME)
- // .search(
- // Search.withQuery(field("menu").matches("waffles"))
- // .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
- //
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
- // }
-
- // add fields
- @Test
- public void addFields_score() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(COLLECTION_NAME)
- .search(
- Search.withQuery(documentMatches("waffles"))
- .withAddFields(score().as("searchScore")))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
- .select("name", "searchScore");
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertThat(snapshot.getResults()).hasSize(1);
- PipelineResult result = snapshot.getResults().get(0);
- assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
- assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
- }
-
-// @Test
-// public void addFields_geoDistance() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("waffles"))
-// .withAddFields(
-// field("location").geoDistance(new GeoPoint(39.6985, -105.024)).as("geoDistance")))
-//// .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
-// .select("name", "geoDistance");
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertThat(snapshot.getResults()).hasSize(1);
-// PipelineResult result = snapshot.getResults().get(0);
-// assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
-// assertThat((Double) result.getData().get("geoDistance")).isGreaterThan(0.0);
-// }
-
- // TODO(search) enable with backend support
-// @Test
-// public void addFields_multipleFields() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("waffles"))
-// .withAddFields(
-// score().as("searchScore"), snippet("menu", "waffles").as("snippet"))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
-// .select("name", "searchScore", "snippet");
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertThat(snapshot.getResults()).hasSize(1);
-// PipelineResult result = snapshot.getResults().get(0);
-// assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
-// assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
-// assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
-// }
-
- // select
- // TODO(search) enable with backend support
-// @Test
-// public void select_topicalityScoreAndSnippet() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("waffles"))
-// .withSelect(
-// field("name"),
-// field("location"),
-// score().as("searchScore"),
-// snippet("menu", "waffles").as("snippet"))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertThat(snapshot.getResults()).hasSize(1);
-// PipelineResult result = snapshot.getResults().get(0);
-// assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
-// assertThat(result.getData().get("location")).isEqualTo(new GeoPoint(39.7183, -104.9621));
-// assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
-// assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
-//
-// List sortedKeys =
-// result.getData().keySet().stream().sorted().collect(Collectors.toList());
-// assertThat(sortedKeys).containsExactly("location", "name", "searchScore", "snippet").inOrder();
-// }
-
- // sort
- @Test
- public void sort_byScore() throws Exception {
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(COLLECTION_NAME)
- .search(
- Search.withQuery(documentMatches("tacos"))
- .withSort(score().descending()));
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "eastsideTacos", "solTacos");
- }
-
- @Test
- public void sort_byDistance() throws Exception {
- GeoPoint queryLocation = new GeoPoint(39.6985, -105.024);
- Pipeline pipeline =
- firestore
- .pipeline()
- .collection(COLLECTION_NAME)
- .search(
- Search.withQuery(field("location").geoDistance(queryLocation).lessThanOrEqual(5600))
- .withSort(field("location").geoDistance(queryLocation).ascending()));
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-
- Pipeline.Snapshot snapshot = pipeline.execute().get();
- assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "mileHighCatch");
- }
-
- // TODO(search) re-enable when geo+text search indexes are supported
- // @Test
- // public void sort_byMultipleOrderings() throws Exception {
- // Pipeline pipeline =
- // firestore
- // .pipeline()
- // .collection(COLLECTION_NAME)
- // .search(
- // Search.withQuery(field("menu").matches("tacos OR chicken"))
- // .withSort(
- // field("location").geoDistance(new GeoPoint(39.6985,
- // -105.024)).ascending(),
- // score().descending())
- // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ batch.commit().get(10, TimeUnit.SECONDS);
+ }
+
+ private void assertResultIds(Pipeline.Snapshot snapshot, String... ids) {
+ List resultIds =
+ snapshot.getResults().stream()
+ .map(PipelineResult::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ assertThat(resultIds).containsExactlyElementsIn(Arrays.asList(ids)).inOrder();
+ }
+
+ // =========================================================================
+ // Search stage
+ // =========================================================================
+
+ // --- DISABLE query expansion ---
+
+ // query
+ // TODO(search) enable with backend support
+ // @Test
+ // public void searchWithLanguageCode() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery("waffles")
+ // .withLanguageCode("en")
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle");
+ // }
+
+ @Test
+ public void searchFullDocument() throws Exception {
+ Pipeline pipeline =
+ firestore.pipeline().collection(COLLECTION_NAME).search(Search.withQuery("waffles"));
//
- // Pipeline.Snapshot snapshot = pipeline.execute().get();
- // assertResultIds(snapshot, "solTacos", "eastsideTacos", "eastsideChicken");
- // }
-
- // limit
- // TODO(search) enable with backend support
-// @Test
-// public void limit_limitsTheNumberOfDocumentsReturned() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(constant(true))
-// .withSort(
-// field("location").geoDistance(new GeoPoint(39.6985, -105.024)).ascending())
-// .withLimit(5)
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "goldenWaffle");
-// }
-
-// @Test
-// public void limit_limitsTheNumberOfDocumentsRetrieved() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("chicken"))
-// .withRetrievalDepth(3));
-//// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "eastsideChicken", "lotusBlossomThai", "goldenWaffle");
-//
-// pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("chicken")));
-//// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "eastsideChicken", "lotusBlossomThai", "goldenWaffle", "eastsideCantina");
-// }
-
- // offset
- // TODO(search) enable with backend support
-// @Test
-// public void offset_skipsNDocuments() throws Exception {
-// Pipeline pipeline =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(constant(true))
-// .withLimit(2)
-// .withOffset(2)
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot = pipeline.execute().get();
-// assertResultIds(snapshot, "eastsideChicken", "eastsideTacos");
-// }
-
- // =========================================================================
- // Snippet
- // =========================================================================
-
- // TODO(search) enable with backend support
-// @Test
-// public void snippetOnMultipleFields() throws Exception {
-// // Get snippet from 1 field
-// Pipeline pipeline1 =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("waffle"))
-// .withAddFields(snippet("menu", "waffles").as("snippet"))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot1 = pipeline1.execute().get();
-// assertThat(snapshot1.getResults()).hasSize(1);
-// assertThat(snapshot1.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
-// String snip1 = (String) snapshot1.getResults().get(0).getData().get("snippet");
-// assertThat(snip1.length()).isGreaterThan(0);
-//
-// // Get snippet from 2 fields
-// Pipeline pipeline2 =
-// firestore
-// .pipeline()
-// .collection(COLLECTION_NAME)
-// .search(
-// Search.withQuery(documentMatches("waffle"))
-// .withAddFields(
-// concat(field("menu"), field("description"))
-// .snippet("waffles") // Without SnippetOptions in Java
-// .as("snippet"))
-// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
-//
-// Pipeline.Snapshot snapshot2 = pipeline2.execute().get();
-// assertThat(snapshot2.getResults()).hasSize(1);
-// assertThat(snapshot2.getResults().get(0).getData().get("name")).isEqualTo("The Golden Waffle");
-// String snip2 = (String) snapshot2.getResults().get(0).getData().get("snippet");
-// assertThat(snip2.length()).isGreaterThan(snip1.length());
-// }
+ // Search.withQuery("waffles").withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "goldenWaffle");
+ }
+
+ // @Test
+ // public void searchSpecificField() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(field("menu").matches("waffles"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle");
+ // }
+
+ @Test
+ public void geoNearQuery() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(
+ field("location")
+ .geoDistance(new GeoPoint(39.6985, -105.024))
+ .lessThanOrEqual(1000)));
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos");
+ }
+
+ // TODO(search) enable with backend support
+ // @Test
+ // public void conjunctionOfTextSearchPredicates() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(
+ // and(field("menu").matches("waffles"),
+ // field("description").matches("diner")))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ // }
+
+ // TODO(search) enable test when geo+text search indexes are supported
+ // @Test
+ // public void conjunctionOfTextSearchAndGeoNear() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(
+ // and(
+ // field("menu").matches("tacos"),
+ // field("location")
+ // .geoDistance(new GeoPoint(39.6985, -105.024))
+ // .lessThan(10000)))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos");
+ // }
+
+ @Test
+ public void negateMatch() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(Search.withQuery(documentMatches("coffee -waffles")));
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "sunnySideUp");
+ }
+
+ // TODO(search) enable with backend support
+ // @Test
+ // public void rquerySearchTheDocumentWithConjunctionAndDisjunction() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("(waffles OR pancakes) AND coffee"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ // }
+
+ @Test
+ public void rqueryAsQueryParam() throws Exception {
+ Pipeline pipeline =
+ firestore.pipeline().collection(COLLECTION_NAME).search(Search.withQuery("chicken wings"));
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "eastsideChicken");
+ }
+
+ // TODO(search) enable when rquery supports field paths
+ // @Test
+ // public void rquerySupportsFieldPaths() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery("menu:(waffles OR pancakes) AND description:\"breakfast all
+ // day\"")
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "sunnySideUp");
+ // }
+
+ // TODO(search) enable with backend support
+ // @Test
+ // public void conjunctionOfRqueryAndExpression() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(
+ // and(
+ // documentMatches("tacos"),
+ // greaterThanOrEqual("average_price_per_person", 8),
+ // lessThanOrEqual("average_price_per_person", 15)))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos");
+ // }
+
+ // --- REQUIRE query expansion ---
+
+ // TODO(search) enable with backend support
+ // @Test
+ // public void requireQueryExpansion_searchFullDocument() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("waffles"))
+ // .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ // }
+
+ // TODO(search) re-enable when backend supports field matches and QueryEnhancement
+ // @Test
+ // public void requireQueryExpansion_searchSpecificField() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(field("menu").matches("waffles"))
+ // .withQueryEnhancement(Search.QueryEnhancement.REQUIRED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "goldenWaffle", "sunnySideUp");
+ // }
+
+ // add fields
+ @Test
+ public void addFields_score() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(documentMatches("waffles"))
+ .withAddFields(score().as("searchScore")))
+ //
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+ .select("name", "searchScore");
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertThat(snapshot.getResults()).hasSize(1);
+ PipelineResult result = snapshot.getResults().get(0);
+ assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ }
+
+ // @Test
+ // public void addFields_geoDistance() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("waffles"))
+ // .withAddFields(
+ // field("location").geoDistance(new
+ // GeoPoint(39.6985, -105.024)).as("geoDistance")))
+ //// .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+ // .select("name", "geoDistance");
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertThat(snapshot.getResults()).hasSize(1);
+ // PipelineResult result = snapshot.getResults().get(0);
+ // assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ // assertThat((Double) result.getData().get("geoDistance")).isGreaterThan(0.0);
+ // }
+
+ // TODO(search) enable with backend support
+ // @Test
+ // public void addFields_multipleFields() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("waffles"))
+ // .withAddFields(
+ // score().as("searchScore"), snippet("menu",
+ // "waffles").as("snippet"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED))
+ // .select("name", "searchScore", "snippet");
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertThat(snapshot.getResults()).hasSize(1);
+ // PipelineResult result = snapshot.getResults().get(0);
+ // assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ // assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ // assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+ // }
+
+ // select
+ // TODO(search) enable with backend support
+ // @Test
+ // public void select_topicalityScoreAndSnippet() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("waffles"))
+ // .withSelect(
+ // field("name"),
+ // field("location"),
+ // score().as("searchScore"),
+ // snippet("menu", "waffles").as("snippet"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertThat(snapshot.getResults()).hasSize(1);
+ // PipelineResult result = snapshot.getResults().get(0);
+ // assertThat(result.getData().get("name")).isEqualTo("The Golden Waffle");
+ // assertThat(result.getData().get("location")).isEqualTo(new GeoPoint(39.7183, -104.9621));
+ // assertThat((Double) result.getData().get("searchScore")).isGreaterThan(0.0);
+ // assertThat(((String) result.getData().get("snippet")).length()).isGreaterThan(0);
+ //
+ // List sortedKeys =
+ // result.getData().keySet().stream().sorted().collect(Collectors.toList());
+ // assertThat(sortedKeys).containsExactly("location", "name", "searchScore",
+ // "snippet").inOrder();
+ // }
+
+ // sort
+ @Test
+ public void sort_byScore() throws Exception {
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(Search.withQuery(documentMatches("tacos")).withSort(score().descending()));
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "eastsideTacos", "solTacos");
+ }
+
+ @Test
+ public void sort_byDistance() throws Exception {
+ GeoPoint queryLocation = new GeoPoint(39.6985, -105.024);
+ Pipeline pipeline =
+ firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search(
+ Search.withQuery(field("location").geoDistance(queryLocation).lessThanOrEqual(5600))
+ .withSort(field("location").geoDistance(queryLocation).ascending()));
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+
+ Pipeline.Snapshot snapshot = pipeline.execute().get();
+ assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "mileHighCatch");
+ }
+
+ // TODO(search) re-enable when geo+text search indexes are supported
+ // @Test
+ // public void sort_byMultipleOrderings() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(field("menu").matches("tacos OR chicken"))
+ // .withSort(
+ // field("location").geoDistance(new GeoPoint(39.6985,
+ // -105.024)).ascending(),
+ // score().descending())
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos", "eastsideTacos", "eastsideChicken");
+ // }
+
+ // limit
+ // TODO(search) enable with backend support
+ // @Test
+ // public void limit_limitsTheNumberOfDocumentsReturned() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(constant(true))
+ // .withSort(
+ // field("location").geoDistance(new GeoPoint(39.6985,
+ // -105.024)).ascending())
+ // .withLimit(5)
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "solTacos", "lotusBlossomThai", "goldenWaffle");
+ // }
+
+ // @Test
+ // public void limit_limitsTheNumberOfDocumentsRetrieved() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("chicken"))
+ // .withRetrievalDepth(3));
+ //// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "eastsideChicken", "lotusBlossomThai", "goldenWaffle");
+ //
+ // pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("chicken")));
+ //// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "eastsideChicken", "lotusBlossomThai", "goldenWaffle",
+ // "eastsideCantina");
+ // }
+
+ // offset
+ // TODO(search) enable with backend support
+ // @Test
+ // public void offset_skipsNDocuments() throws Exception {
+ // Pipeline pipeline =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(constant(true))
+ // .withLimit(2)
+ // .withOffset(2)
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot = pipeline.execute().get();
+ // assertResultIds(snapshot, "eastsideChicken", "eastsideTacos");
+ // }
+
+ // =========================================================================
+ // Snippet
+ // =========================================================================
+
+ // TODO(search) enable with backend support
+ // @Test
+ // public void snippetOnMultipleFields() throws Exception {
+ // // Get snippet from 1 field
+ // Pipeline pipeline1 =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("waffle"))
+ // .withAddFields(snippet("menu", "waffles").as("snippet"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot1 = pipeline1.execute().get();
+ // assertThat(snapshot1.getResults()).hasSize(1);
+ // assertThat(snapshot1.getResults().get(0).getData().get("name")).isEqualTo("The Golden
+ // Waffle");
+ // String snip1 = (String) snapshot1.getResults().get(0).getData().get("snippet");
+ // assertThat(snip1.length()).isGreaterThan(0);
+ //
+ // // Get snippet from 2 fields
+ // Pipeline pipeline2 =
+ // firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search(
+ // Search.withQuery(documentMatches("waffle"))
+ // .withAddFields(
+ // concat(field("menu"), field("description"))
+ // .snippet("waffles") // Without SnippetOptions in Java
+ // .as("snippet"))
+ // .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
+ //
+ // Pipeline.Snapshot snapshot2 = pipeline2.execute().get();
+ // assertThat(snapshot2.getResults()).hasSize(1);
+ // assertThat(snapshot2.getResults().get(0).getData().get("name")).isEqualTo("The Golden
+ // Waffle");
+ // String snip2 = (String) snapshot2.getResults().get(0).getData().get("snippet");
+ // assertThat(snip2.length()).isGreaterThan(snip1.length());
+ // }
}
From 8f800569e4fd35cf6a6b78cdd4202234c5679c9f Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Thu, 2 Apr 2026 09:43:15 -0600
Subject: [PATCH 13/15] make QueryEnhancement internal. Test with geoDistance
standalone function
---
.../com/google/cloud/firestore/pipeline/stages/Search.java | 3 ++-
.../com/google/cloud/firestore/it/ITPipelineSearchTest.java | 6 ++----
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
index 9779be6fc3..a06d759de5 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Search.java
@@ -56,7 +56,8 @@ public final class Search extends Stage {
* perform matching of synonyms, misspellings, lemmatization, stemming.
*/
@BetaApi
- public static final class QueryEnhancement {
+ @InternalApi
+ static final class QueryEnhancement {
final String protoString;
private QueryEnhancement(String protoString) {
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index 0a26b3cdb0..138d067f03 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -17,9 +17,7 @@
package com.google.cloud.firestore.it;
import static com.google.cloud.firestore.it.ITQueryTest.map;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.documentMatches;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.field;
-import static com.google.cloud.firestore.pipeline.expressions.Expression.score;
+import static com.google.cloud.firestore.pipeline.expressions.Expression.*;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
@@ -536,7 +534,7 @@ public void sort_byDistance() throws Exception {
.collection(COLLECTION_NAME)
.search(
Search.withQuery(field("location").geoDistance(queryLocation).lessThanOrEqual(5600))
- .withSort(field("location").geoDistance(queryLocation).ascending()));
+ .withSort(geoDistance("location", queryLocation).ascending()));
// .withQueryEnhancement(Search.QueryEnhancement.DISABLED));
Pipeline.Snapshot snapshot = pipeline.execute().get();
From 5ae808d35b04462185a6957933cc2da6b48fdffe Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Thu, 2 Apr 2026 12:56:35 -0600
Subject: [PATCH 14/15] skip search tests unless running against nightly and
enterprise
---
.../google/cloud/firestore/it/ITPipelineSearchTest.java | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index 138d067f03..d91802817f 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -19,7 +19,7 @@
import static com.google.cloud.firestore.it.ITQueryTest.map;
import static com.google.cloud.firestore.pipeline.expressions.Expression.*;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.GeoPoint;
@@ -186,9 +186,10 @@ public void primeBackend() throws Exception {
@Before
public void setupRestaurantDocs() throws Exception {
- assumeFalse(
- "This test suite only runs against the Enterprise edition.",
- !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE));
+ assumeTrue(
+ "This test suite only runs against the Enterprise edition in Nightly.",
+ getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE)
+ && "NIGHTLY".equalsIgnoreCase(getTargetBackend()));
restaurantsCollection = firestore.collection(COLLECTION_NAME);
From eea979949977ce50f0e6d6369b1b8a7fc32dd82f Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Fri, 3 Apr 2026 08:41:17 -0600
Subject: [PATCH 15/15] Update pipeline search test setup to only run once for
the class. This required a refactor of BaseTest. Also added logic to test
setup to ensure unexpected docs are cleaned up
---
.../google/cloud/firestore/it/ITBaseTest.java | 9 ++--
.../firestore/it/ITPipelineSearchTest.java | 46 ++++++++++++++-----
2 files changed, 40 insertions(+), 15 deletions(-)
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java
index 32c8ba852a..725bb2cf08 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java
@@ -79,8 +79,7 @@ static FirestoreEdition getFirestoreEdition() {
return FirestoreEdition.valueOf(firestoreEdition.toUpperCase());
}
- @Before
- public void before() throws Exception {
+ public static FirestoreOptions.Builder getOptionsBuilder() {
FirestoreOptions.Builder optionsBuilder = FirestoreOptions.newBuilder();
String dbPropertyName = "FIRESTORE_NAMED_DATABASE";
@@ -110,8 +109,12 @@ public void before() throws Exception {
optionsBuilder.setEmulatorHost("localhost:8080");
}
}
+ return optionsBuilder;
+ }
- firestoreOptions = optionsBuilder.build();
+ @Before
+ public void before() throws Exception {
+ firestoreOptions = getOptionsBuilder().build();
logger.log(
Level.INFO,
"Integration test against " + firestoreOptions.getTransportChannelProvider().getEndpoint());
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
index d91802817f..d3f8cb8b3b 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineSearchTest.java
@@ -22,9 +22,12 @@
import static org.junit.Assume.assumeTrue;
import com.google.cloud.firestore.CollectionReference;
+import com.google.cloud.firestore.DocumentSnapshot;
+import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.GeoPoint;
import com.google.cloud.firestore.Pipeline;
import com.google.cloud.firestore.PipelineResult;
+import com.google.cloud.firestore.QuerySnapshot;
import com.google.cloud.firestore.WriteBatch;
import com.google.cloud.firestore.pipeline.stages.Search;
import java.util.Arrays;
@@ -34,7 +37,7 @@
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
-import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -42,9 +45,7 @@
@RunWith(JUnit4.class)
public class ITPipelineSearchTest extends ITBaseTest {
- private final String COLLECTION_NAME = "TextSearchIntegrationTests";
-
- private CollectionReference restaurantsCollection;
+ private static final String COLLECTION_NAME = "TextSearchIntegrationTests";
private static final Map> restaurantDocs = new HashMap<>();
@@ -184,20 +185,41 @@ public void primeBackend() throws Exception {
// Disable priming as it uses Watch/Listen
}
- @Before
- public void setupRestaurantDocs() throws Exception {
+ @BeforeClass
+ public static void setupRestaurantDocs() throws Exception {
assumeTrue(
"This test suite only runs against the Enterprise edition in Nightly.",
getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE)
&& "NIGHTLY".equalsIgnoreCase(getTargetBackend()));
- restaurantsCollection = firestore.collection(COLLECTION_NAME);
-
- WriteBatch batch = firestore.batch();
- for (Map.Entry> entry : restaurantDocs.entrySet()) {
- batch.set(restaurantsCollection.document(entry.getKey()), entry.getValue());
+ // Initialize a temporary Firestore instance for class-level setup.
+ Firestore db = getOptionsBuilder().build().getService();
+
+ // Setup restaurant docs
+ try {
+ // Get the existing contents of the test collection
+ CollectionReference collection = db.collection(COLLECTION_NAME);
+ QuerySnapshot snapshot = collection.get().get();
+
+ // A batch will be used to update the test collection to the desired state
+ WriteBatch batch = db.batch();
+
+ // Delete unexpected documents
+ for (DocumentSnapshot doc : snapshot.getDocuments()) {
+ if (!restaurantDocs.containsKey(doc.getId())) {
+ batch.delete(doc.getReference());
+ }
+ }
+
+ // Add/overwrite expected documents
+ for (Map.Entry> entry : restaurantDocs.entrySet()) {
+ batch.set(collection.document(entry.getKey()), entry.getValue());
+ }
+
+ batch.commit().get(10, TimeUnit.SECONDS);
+ } finally {
+ db.close();
}
- batch.commit().get(10, TimeUnit.SECONDS);
}
private void assertResultIds(Pipeline.Snapshot snapshot, String... ids) {