diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts
index d6a770cff67..20a76245572 100644
--- a/handwritten/firestore/dev/src/pipelines/expression.ts
+++ b/handwritten/firestore/dev/src/pipelines/expression.ts
@@ -23,11 +23,14 @@ import {
fieldOrExpression,
isFirestoreValue,
isString,
+ toField,
valueToDefaultExpr,
vectorToExpr,
} from './pipeline-util';
import {HasUserData, Serializer, validateUserInput} from '../serializer';
import {cast} from '../util';
+import {GeoPoint} from '../geo-point';
+import {OptionsUtil} from './options-util';
/**
* @beta
@@ -3100,6 +3103,85 @@ export abstract class Expression
]).asBoolean();
}
+ // /**
+ // * Evaluates if the result of this `expression` is between
+ // * the `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * field('tireWidth').between(constant(2.2), constant(2.4))
+ // *
+ // * // This is functionally equivalent to
+ // * and(field('tireWidth').greaterThanOrEqual(contant(2.2)), field('tireWidth').lessThanOrEqual(constant(2.4)))
+ // * ```
+ // *
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // between(lowerBound: Expression, upperBound: Expression): BooleanExpression;
+ //
+ // /**
+ // * Evaluates if the result of this `expression` is between
+ // * the `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * field('tireWidth').between(2.2, 2.4)
+ // *
+ // * // This is functionally equivalent to
+ // * and(field('tireWidth').greaterThanOrEqual(2.2), field('tireWidth').lessThanOrEqual(2.4))
+ // * ```
+ // *
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // between(lowerBound: unknown, upperBound: unknown): BooleanExpression;
+ //
+ // between(lowerBound: unknown, upperBound: unknown): BooleanExpression {
+ // return new FunctionExpression('between', [
+ // this,
+ // valueToDefaultExpr(lowerBound),
+ // valueToDefaultExpr(upperBound),
+ // ]).asBoolean();
+ // }
+ //
+ // /**
+ // * Evaluates to an HTML-formatted text snippet that renders terms matching
+ // * the search query in `bold`.
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param rquery Define the search query using the search DSL.
+ // */
+ // snippet(rquery: string): Expression;
+ //
+ // /**
+ // * Evaluates to an HTML-formatted text snippet that renders terms matching
+ // * the search query in `bold`.
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param options Define how snippeting behaves.
+ // */
+ // snippet(options: firestore.Pipelines.SnippetOptions): Expression;
+ //
+ // snippet(
+ // queryOrOptions: string | firestore.Pipelines.SnippetOptions,
+ // ): Expression {
+ // const options: firestore.Pipelines.SnippetOptions = isString(queryOrOptions)
+ // ? {rquery: queryOrOptions}
+ // : queryOrOptions;
+ // const rquery = options.rquery;
+ // const internalOptions = {
+ // maxSnippetWidth: options.maxSnippetWidth,
+ // maxSnippets: options.maxSnippets,
+ // separator: options.separator,
+ // };
+ // return new SnippetExpression([this, constant(rquery)], internalOptions);
+ // }
+
// TODO(new-expression): Add new expression method definitions above this line
/**
@@ -3351,6 +3433,35 @@ export class Field
readonly expressionType: firestore.Pipelines.ExpressionType = 'Field';
selectable = true as const;
+ /**
+ * Perform a full-text search on this field.
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ *
+ * @param rquery Define the search query using the rquery DTS.
+ */
+ matches(rquery: string | Expression): BooleanExpression {
+ return new FunctionExpression('matches', [
+ this,
+ valueToDefaultExpr(rquery),
+ ]).asBoolean();
+ }
+
+ /**
+ * Evaluates to the distance in meters between the location specified
+ * by this field and the query location.
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ *
+ * @param location - Compute distance to this GeoPoint.
+ */
+ geoDistance(location: GeoPoint | Expression): Expression {
+ return new FunctionExpression('geo_distance', [
+ this,
+ valueToDefaultExpr(location),
+ ]).asBoolean();
+ }
+
/**
* @beta
* @internal
@@ -3686,6 +3797,51 @@ export class FunctionExpression extends Expression {
}
}
+/**
+ * SnippetExpression extends from FunctionExpression because it
+ * supports options and requires the options util.
+ */
+export class SnippetExpression extends FunctionExpression {
+ /**
+ * @private
+ * @internal
+ */
+ get _optionsUtil(): OptionsUtil {
+ return new OptionsUtil({
+ maxSnippetWidth: {
+ serverName: 'max_snippet_width',
+ },
+ maxSnippets: {
+ serverName: 'max_snippets',
+ },
+ separator: {
+ serverName: 'separator',
+ },
+ });
+ }
+
+ /**
+ * @hideconstructor
+ */
+ constructor(
+ params: Expression[],
+ private _options?: {},
+ ) {
+ super('snippet', params);
+ }
+
+ _toProto(serializer: Serializer): api.IValue {
+ const proto = super._toProto(serializer);
+ proto.functionValue!.options = this._optionsUtil.getOptionsProto(
+ serializer,
+ this._options ?? {},
+ {},
+ );
+
+ return proto;
+ }
+}
+
/**
* @beta
* This class defines the base class for Firestore `Pipeline` functions, which can be evaluated within pipeline
@@ -10258,6 +10414,201 @@ export function isType(
return fieldOrExpression(fieldNameOrExpression).isType(type);
}
+// /**
+// * Perform a full-text search on the specified field.
+// *
+// * @remarks This Expression can only be used within a `Search` stage.
+// *
+// * @param searchField Search the specified field.
+// * @param rquery Define the search query using the search DSL.
+// */
+// export function matches(
+// searchField: string | Field,
+// rquery: string | Expression,
+// ): BooleanExpression {
+// return toField(searchField).matches(rquery);
+// }
+
+/**
+ * Perform a full-text search on the document.
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ *
+ * @param rquery Define the search query using the rquery DTS.
+ */
+export function documentMatches(
+ rquery: string | Expression,
+): BooleanExpression {
+ return new FunctionExpression('document_matches', [
+ valueToDefaultExpr(rquery),
+ ]).asBoolean();
+}
+
+/**
+ * Evaluates to the search score that reflects the topicality of the document
+ * to all the text predicates (for example: `documentMatches`)
+ * in the search query. If `SearchOptions.query` is not set or does not contain
+ * any text predicates, then this topicality score will always be `0`.
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ */
+export function score(): Expression {
+ return new FunctionExpression('score', []).asBoolean();
+}
+
+// /**
+// * Evaluates to an HTML-formatted text snippet that highlights terms matching
+// * the search query in `bold`.
+// *
+// * @remarks This Expression can only be used within a `Search` stage.
+// *
+// * @param searchField Search the specified field for matching terms.
+// * @param rquery Define the search query using the search DSL.
+// */
+// export function snippet(
+// searchField: string | Field,
+// rquery: string,
+// ): Expression;
+//
+// /**
+// * Evaluates to an HTML-formatted text snippet that highlights terms matching
+// * the search query in `bold`.
+// *
+// * @remarks This Expression can only be used within a `Search` stage.
+// *
+// * @param searchField Search the specified field for matching terms.
+// * @param options Define the search query using the search DSL.
+// */
+// export function snippet(
+// searchField: string | Field,
+// options: firestore.Pipelines.SnippetOptions,
+// ): Expression;
+// export function snippet(
+// field: string | Field,
+// queryOrOptions: string | firestore.Pipelines.SnippetOptions,
+// ): Expression {
+// return toField(field).snippet(
+// isString(queryOrOptions) ? {rquery: queryOrOptions} : queryOrOptions,
+// );
+// }
+
+/**
+ * Evaluates to the distance in meters between the location in the specified
+ * field and the query location.
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ *
+ * @param fieldName - Specifies the field in the document which contains
+ * the first GeoPoint for distance computation.
+ * @param location - Compute distance to this GeoPoint.
+ */
+export function geoDistance(
+ fieldName: string | Field,
+ location: GeoPoint | Expression,
+): Expression {
+ return toField(fieldName).geoDistance(location);
+}
+//
+// /**
+// * Evaluates if the value in the field specified by `fieldName` is between
+// * the evaluated values for `lowerBound` (inclusive) and `upperBound` (inclusive).
+// *
+// * @example
+// * ```
+// * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+// * between('tireWidth', constant(2.2), constant(2.4))
+// *
+// * // This is functionally equivalent to
+// * and(greaterThanOrEqual('tireWidth', constant(2.2)), lessThanOrEqual('tireWidth', constant(2.4)))
+// * ```
+// *
+// * @param fieldName - Evaluate if the value stored in this field is between the lower and upper bounds.
+// * @param lowerBound - Lower bound (inclusive) of the range.
+// * @param upperBound - Upper bound (inclusive) of the range.
+// */
+// export function between(
+// fieldName: string,
+// lowerBound: Expression,
+// upperBound: Expression,
+// ): BooleanExpression;
+//
+// /**
+// * Evaluates if the value in the field specified by `fieldName` is between
+// * the values for `lowerBound` (inclusive) and `upperBound` (inclusive).
+// *
+// * @example
+// * ```
+// * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+// * between('tireWidth', 2.2, 2.4)
+// *
+// * // This is functionally equivalent to
+// * and(greaterThanOrEqual('tireWidth', 2.2), lessThanOrEqual('tireWidth', 2.4))
+// * ```
+// *
+// * @param fieldName - Evaluate if the value stored in this field is between the lower and upper bounds.
+// * @param lowerBound - Lower bound (inclusive) of the range.
+// * @param upperBound - Upper bound (inclusive) of the range.
+// */
+// export function between(
+// fieldName: string,
+// lowerBound: unknown,
+// upperBound: unknown,
+// ): BooleanExpression;
+//
+// /**
+// * Evaluates if the result of the specified `expression` is between
+// * the results of `lowerBound` (inclusive) and `upperBound` (inclusive).
+// *
+// * @example
+// * ```
+// * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+// * between(field('tireWidth'), constant(2.2), constant(2.4))
+// *
+// * // This is functionally equivalent to
+// * and(greaterThanOrEqual(field('tireWidth'), constant(2.2)), lessThanOrEqual(field('tireWidth'), constant(2.4)))
+// * ```
+// *
+// * @param expression - Evaluate if the result of this expression is between the lower and upper bounds.
+// * @param lowerBound - Lower bound (inclusive) of the range.
+// * @param upperBound - Upper bound (inclusive) of the range.
+// */
+// export function between(
+// expression: Expression,
+// lowerBound: Expression,
+// upperBound: Expression,
+// ): BooleanExpression;
+//
+// /**
+// * Evaluates if the result of the specified `expression` is between
+// * the `lowerBound` (inclusive) and `upperBound` (inclusive).
+// *
+// * @example
+// * ```
+// * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+// * between(field('tireWidth'), 2.2, 2.4)
+// *
+// * // This is functionally equivalent to
+// * and(greaterThanOrEqual(field('tireWidth'), 2.2), lessThanOrEqual(field('tireWidth'), 2.4))
+// * ```
+// *
+// * @param expression - Evaluate if the result of this expression is between the lower and upper bounds.
+// * @param lowerBound - Lower bound (inclusive) of the range.
+// * @param upperBound - Upper bound (inclusive) of the range.
+// */
+// export function between(
+// expression: Expression,
+// lowerBound: unknown,
+// upperBound: unknown,
+// ): BooleanExpression;
+//
+// export function between(
+// expression: Expression | string,
+// lowerBound: unknown,
+// upperBound: unknown,
+// ): BooleanExpression {
+// return fieldOrExpression(expression).between(lowerBound, upperBound);
+// }
+
// TODO(new-expression): Add new top-level expression function definitions above this line
/**
diff --git a/handwritten/firestore/dev/src/pipelines/pipeline-util.ts b/handwritten/firestore/dev/src/pipelines/pipeline-util.ts
index 5c12257c23f..909da33e7fe 100644
--- a/handwritten/firestore/dev/src/pipelines/pipeline-util.ts
+++ b/handwritten/firestore/dev/src/pipelines/pipeline-util.ts
@@ -577,6 +577,7 @@ export function isSelectable(
export function isOrdering(val: unknown): val is firestore.Pipelines.Ordering {
const candidate = val as firestore.Pipelines.Ordering;
return (
+ val !== undefined &&
isExpr(candidate.expr) &&
(candidate.direction === 'ascending' ||
candidate.direction === 'descending')
@@ -721,10 +722,19 @@ export function fieldOrSelectable(value: string | Selectable): Selectable {
}
}
+/**
+ * @deprecated use selectablesToObject instead
+ */
export function selectablesToMap(
selectables: (firestore.Pipelines.Selectable | string)[],
): Map {
- const result = new Map();
+ return new Map(Object.entries(selectablesToObject(selectables)));
+}
+
+export function selectablesToObject(
+ selectables: (firestore.Pipelines.Selectable | string)[],
+): Record {
+ const result: Record = {};
for (const selectable of selectables) {
let alias: string;
let expression: Expression;
@@ -736,11 +746,11 @@ export function selectablesToMap(
expression = selectable._expr as unknown as Expression;
}
- if (result.get(alias) !== undefined) {
+ if (result[alias] !== undefined) {
throw new Error(`Duplicate alias or field '${alias}'`);
}
- result.set(alias, expression);
+ result[alias] = expression;
}
return result;
}
diff --git a/handwritten/firestore/dev/src/pipelines/pipelines.ts b/handwritten/firestore/dev/src/pipelines/pipelines.ts
index 2844c29ca57..4b183b16935 100644
--- a/handwritten/firestore/dev/src/pipelines/pipelines.ts
+++ b/handwritten/firestore/dev/src/pipelines/pipelines.ts
@@ -39,6 +39,7 @@ import {
selectablesToMap,
toField,
vectorToExpr,
+ selectablesToObject,
} from './pipeline-util';
import {DocumentReference} from '../reference/document-reference';
import {PipelineResponse} from '../reference/types';
@@ -59,6 +60,7 @@ import {
constant,
_mapValue,
field,
+ documentMatches,
} from './expression';
import {
AddFields,
@@ -95,6 +97,8 @@ import {
InternalDocumentsStageOptions,
InternalCollectionGroupStageOptions,
InternalCollectionStageOptions,
+ Search,
+ InternalSearchStageOptions,
} from './stage';
import {StructuredPipeline} from './structured-pipeline';
import Selectable = FirebaseFirestore.Pipelines.Selectable;
@@ -1420,6 +1424,42 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
return this._addStage(new Unnest(internalOptions));
}
+ /**
+ * Add a search stage to the Pipeline.
+ *
+ * @remarks This must be the first stage of the pipeline.
+ * @remarks A limited set of expressions are supported in the search stage.
+ *
+ * @param options - An object that specifies required and optional parameters
+ * for the stage.
+ * @return A new `Pipeline` object with this stage appended to the stage list.
+ */
+ search(options: firestore.Pipelines.SearchStageOptions): Pipeline {
+ // Convert user land convenience types to internal types
+ const normalizedQuery: firestore.Pipelines.BooleanExpression = isExpr(
+ options.query,
+ )
+ ? options.query
+ : documentMatches(options.query);
+ // const normalizedSelect: Record | undefined =
+ // options.select ? selectablesToObject(options.select) : undefined;
+ const normalizedAddFields: Record | undefined =
+ options.addFields ? selectablesToObject(options.addFields) : undefined;
+ const normalizedSort: firestore.Pipelines.Ordering[] | undefined =
+ isOrdering(options.sort) ? [options.sort] : options.sort;
+
+ const internalOptions: InternalSearchStageOptions = {
+ ...options,
+ query: normalizedQuery,
+ // select: normalizedSelect,
+ addFields: normalizedAddFields,
+ sort: normalizedSort,
+ };
+
+ // Add stage to the pipeline
+ return this._addStage(new Search(internalOptions));
+ }
+
/**
* @beta
* Sorts the documents from previous stages based on one or more `Ordering` criteria.
diff --git a/handwritten/firestore/dev/src/pipelines/stage.ts b/handwritten/firestore/dev/src/pipelines/stage.ts
index 9695624fbdd..b06b70e310a 100644
--- a/handwritten/firestore/dev/src/pipelines/stage.ts
+++ b/handwritten/firestore/dev/src/pipelines/stage.ts
@@ -645,6 +645,67 @@ export class Sort implements Stage {
}
}
+export type InternalSearchStageOptions = Omit<
+ firestore.Pipelines.SearchStageOptions,
+ 'query' | 'sort' | 'select' | 'addFields'
+> & {
+ query: firestore.Pipelines.BooleanExpression;
+ sort?: Array;
+ select?: Record;
+ addFields?: Record;
+};
+
+/**
+ * Search stage.
+ */
+export class Search implements Stage {
+ name = 'search';
+
+ constructor(private options: InternalSearchStageOptions) {}
+
+ readonly optionsUtil = new OptionsUtil({
+ query: {
+ serverName: 'query',
+ },
+ limit: {
+ serverName: 'limit',
+ },
+ retrievalDepth: {
+ serverName: 'retrieval_depth',
+ },
+ sort: {
+ serverName: 'sort',
+ },
+ addFields: {
+ serverName: 'add_fields',
+ },
+ select: {
+ serverName: 'select',
+ },
+ offset: {
+ serverName: 'offset',
+ },
+ queryEnhancement: {
+ serverName: 'query_enhancement',
+ },
+ languageCode: {
+ serverName: 'language_code',
+ },
+ });
+
+ _toProto(serializer: Serializer): api.Pipeline.IStage {
+ return {
+ name: this.name,
+ args: [],
+ options: this.optionsUtil.getOptionsProto(
+ serializer,
+ this.options,
+ this.options.rawOptions,
+ ),
+ };
+ }
+}
+
/**
* Raw stage.
*/
diff --git a/handwritten/firestore/dev/system-test/pipeline.ts b/handwritten/firestore/dev/system-test/pipeline.ts
index 8377424c8d2..e5db163e059 100644
--- a/handwritten/firestore/dev/system-test/pipeline.ts
+++ b/handwritten/firestore/dev/system-test/pipeline.ts
@@ -173,53 +173,56 @@ import {getTestDb, getTestRoot} from './firestore';
import {Firestore as InternalFirestore} from '../src';
import {ServiceError} from 'google-gax';
+import {documentMatches, score} from '../src/pipelines/expression';
use(chaiAsPromised);
const timestampDeltaMS = 3000;
+let beginDocCreation = 0;
+let endDocCreation = 0;
-describe.skipClassic('Pipeline class', () => {
- let firestore: Firestore;
- let randomCol: CollectionReference;
- let beginDocCreation = 0;
- let endDocCreation = 0;
-
- async function testCollectionWithDocs(docs: {
+async function testCollectionWithDocs(
+ collection: CollectionReference,
+ docs: {
[id: string]: DocumentData;
- }): Promise> {
- beginDocCreation = new Date().valueOf();
- for (const id in docs) {
- const ref = randomCol.doc(id);
- await ref.set(docs[id]);
- }
- endDocCreation = new Date().valueOf();
- return randomCol;
+ },
+): Promise> {
+ beginDocCreation = new Date().valueOf();
+ for (const id in docs) {
+ const ref = collection.doc(id);
+ await ref.set(docs[id]);
}
-
- function expectResults(result: PipelineSnapshot, ...docs: string[]): void;
- function expectResults(
- result: PipelineSnapshot,
- ...data: DocumentData[]
- ): void;
- function expectResults(
- result: PipelineSnapshot,
- ...data: DocumentData[] | string[]
- ): void {
- if (data.length > 0) {
- if (typeof data[0] === 'string') {
- const actualIds = result.results.map(result => result.id);
- expect(actualIds).to.deep.equal(data);
- } else {
- result.results.forEach(r => {
- expect(r.data()).to.deep.equal(data.shift());
- });
- }
+ endDocCreation = new Date().valueOf();
+ return collection;
+}
+
+function expectResults(result: PipelineSnapshot, ...docs: string[]): void;
+function expectResults(result: PipelineSnapshot, ...data: DocumentData[]): void;
+function expectResults(
+ result: PipelineSnapshot,
+ ...data: DocumentData[] | string[]
+): void {
+ if (data.length > 0) {
+ if (typeof data[0] === 'string') {
+ const actualIds = result.results.map(result => result.id);
+ expect(actualIds).to.deep.equal(data);
} else {
- expect(result.results.length).to.equal(data.length);
+ result.results.forEach(r => {
+ expect(r.data()).to.deep.equal(data.shift());
+ });
}
+ } else {
+ expect(result.results.length).to.equal(data.length);
}
+}
- async function setupBookDocs(): Promise> {
+describe.skipClassic('Pipeline class', () => {
+ let firestore: Firestore;
+ let randomCol: CollectionReference;
+
+ async function setupBookDocs(
+ collection: CollectionReference,
+ ): Promise> {
const bookDocs: {[id: string]: DocumentData} = {
book1: {
title: "The Hitchhiker's Guide to the Galaxy",
@@ -329,12 +332,12 @@ describe.skipClassic('Pipeline class', () => {
embedding: FieldValue.vector([1, 1, 1, 1, 1, 1, 1, 1, 1, 10]),
},
};
- return testCollectionWithDocs(bookDocs);
+ return testCollectionWithDocs(collection, bookDocs);
}
before(async () => {
randomCol = getTestRoot();
- await setupBookDocs();
+ await setupBookDocs(randomCol);
firestore = randomCol.firestore;
});
@@ -5627,6 +5630,625 @@ describe.skipClassic('Pipeline class', () => {
});
});
+// Search tests require a collection with an index, so the test setup and tear
+// down is managed different from the rest of the Pipeline tests. To accomplish
+// this, we break these tests into a separate describe
+describe.skipClassic('Pipeline search', () => {
+ let firestore: Firestore;
+ let restaurantsCollection: CollectionReference;
+
+ async function setupRestaurantDocs(
+ collection: CollectionReference,
+ ): Promise> {
+ const restaurantDocs: {[id: string]: DocumentData} = {
+ sunnySideUp: {
+ 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,
+ },
+ goldenWaffle: {
+ 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,
+ },
+ lotusBlossomThai: {
+ 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,
+ },
+ mileHighCatch: {
+ 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,
+ },
+ peakBurgers: {
+ 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,
+ },
+ solTacos: {
+ 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,
+ },
+ eastsideTacos: {
+ 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,
+ },
+ eastsideChicken: {
+ 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,
+ },
+ };
+
+ // TODO(search) - Migrate this over to IndexTestHelper when search supports the equal filter.
+ // Remove any restaurant docs not in the expected set - perhaps these were
+ // set by another dev or test suite. This has potential to cause flakes in another concurrent
+ // run of these tests, if they have added new test docs.
+ const collectionSnapshot = await collection.get();
+ const expectedDocIds = Object.keys(restaurantDocs);
+ const deletes = collectionSnapshot.docs
+ .filter(ds => expectedDocIds.indexOf(ds.id) < 0)
+ .map(ds => ds.ref.delete());
+ await Promise.all(deletes);
+
+ // Add/overwrite all restaurant docs
+ return testCollectionWithDocs(collection, restaurantDocs);
+ }
+
+ const COLLECTION_NAME = 'TextSearchIntegrationTests';
+
+ // Search tests will use restaurant docs
+ before(async () => {
+ // TODO(search) - Migrate this over to IndexTestHelper when search supports the equal filter.
+ // Note: using a static collection of documents for every search test has an inherent risk
+ // of flakiness. Search requires an index on the collection, which is the reason we use a pre-defined
+ // collection. We cannot use the IndexTestHelper because that relies on an equality match to the testID
+ // field. Search currently does not support the equal expression.
+ firestore = getTestDb();
+ restaurantsCollection = firestore.collection(COLLECTION_NAME);
+ await setupRestaurantDocs(restaurantsCollection);
+ firestore = restaurantsCollection.firestore;
+ });
+
+ describe('search stage', () => {
+ describe('DISABLE query expansion', () => {
+ describe('query', () => {
+ // TODO(search) enable with backend support
+ // it('all search features', async () => {
+ // const queryLocation = new GeoPoint(0, 0);
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: and(
+ // documentMatches('waffles'),
+ // field('description').matches('breakfast'),
+ // field('location').geoDistance(queryLocation).lessThan(1000),
+ // field('avgPrice').between(10, 20),
+ // ),
+ // select: [
+ // field('title'),
+ // field('menu'),
+ // field('description'),
+ // field('location').geoDistance(queryLocation).as('distance'),
+ // ],
+ // addFields: [score().as('searchScore')],
+ // offset: 0,
+ // retrievalDepth: 1000,
+ // limit: 50,
+ // sort: [field('location').geoDistance(queryLocation).ascending()],
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'goldenWaffle');
+ // });
+
+ it('search full document', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search({
+ query: documentMatches('waffles'),
+ // queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'goldenWaffle');
+ });
+
+ // TODO(search) enable with per-field matching
+ // it('search a specific field', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('waffles'),
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'goldenWaffle');
+ // });
+
+ // TODO(search) enable with per-field matching and support for AND expression
+ // it('conjunction of text search predicates', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: and(
+ // field('menu').matches('waffles'),
+ // field('description').matches('diner'),
+ // ),
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ // });
+
+ it('geo near query', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search({
+ query: field('location')
+ .geoDistance(new GeoPoint(39.6985, -105.024))
+ .lessThan(1000 /* m */),
+ // queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'solTacos');
+ });
+
+ // TODO(search) enable with geo+text search indexes
+ // it('conjunction of text search and geo near', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: and(
+ // field('menu').matches('tacos'),
+ // field('location')
+ // .geoDistance(new GeoPoint(39.6985, -105.024))
+ // .lessThan(10_000 /* meters */),
+ // ),
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'solTacos');
+ // });
+
+ it('negate match', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search({
+ query: documentMatches('coffee -waffles'),
+ // queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'sunnySideUp');
+ });
+
+ // TODO(search) this level of rquery is not yet supported
+ it.skip('rquery search the document with conjunction and disjunction', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search({
+ query: documentMatches('(waffles OR pancakes) AND coffee'),
+ // queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ });
+
+ it('rquery as query param', async () => {
+ const ppl = firestore.pipeline().collection(COLLECTION_NAME).search({
+ query: 'chicken wings',
+ // queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'eastsideChicken');
+ });
+
+ // TODO(search) enable with advanced rquery support
+ // it('rquery supports field paths', async () => {
+ // const ppl = firestore.pipeline().collection(COLLECTION_NAME).search({
+ // query:
+ // 'menu:(waffles OR pancakes) AND description:"breakfast all day"',
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'sunnySideUp');
+ // });
+
+ // TODO(search) enable with support of other expressions in the search stage
+ // it('conjunction of rquery and expression', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: and(
+ // documentMatches('tacos'),
+ // field('average_price_per_person').between(8, 15),
+ // ),
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'solTacos');
+ // });
+ });
+
+ describe('addFields', () => {
+ it('score', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search({
+ query: documentMatches('waffles'),
+ addFields: [score().as('searchScore')],
+ // queryEnhancement: 'disabled',
+ })
+ .select('name', 'searchScore', 'snippet');
+
+ const snapshot = await ppl.execute();
+ expect(snapshot.results.length).to.equal(1);
+ expect(snapshot.results[0].get('name')).to.equal('The Golden Waffle');
+ expect(snapshot.results[0].get('searchScore')).to.be.greaterThan(0);
+ });
+
+ // TODO(search) enable with backend support
+ // it('topicality score and snippet', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('waffles'),
+ // addFields: [
+ // score().as('searchScore'),
+ // field('menu').snippet('waffles').as('snippet'),
+ // ],
+ // queryEnhancement: 'disabled',
+ // })
+ // .select('name', 'searchScore', 'snippet');
+ //
+ // const snapshot = await ppl.execute();
+ // expect(snapshot.results.length).to.equal(1);
+ // expect(snapshot.results[0].get('name')).to.equal('The Golden Waffle');
+ // expect(snapshot.results[0].get('searchScore')).to.be.greaterThan(0);
+ // expect(snapshot.results[0].get('snippet')?.length).to.be.greaterThan(
+ // 0,
+ // );
+ // });
+ });
+
+ describe('select', () => {
+ // TODO(search) enable with backend support
+ // it('topicality score and snippet', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('waffles'),
+ // select: [
+ // field('name'),
+ // 'location',
+ // score().as('searchScore'),
+ // field('menu').snippet('waffles').as('snippet'),
+ // ],
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expect(snapshot.results.length).to.equal(1);
+ // expect(snapshot.results[0].get('name')).to.equal('The Golden Waffle');
+ // expect(snapshot.results[0].get('location')).to.equal(
+ // new GeoPoint(39.7183, -104.9621),
+ // );
+ // expect(snapshot.results[0].get('searchScore')).to.be.greaterThan(0);
+ // expect(snapshot.results[0].get('snippet')?.length).to.be.greaterThan(
+ // 0,
+ // );
+ // expect(Object.keys(snapshot.results[0].data()).sort()).to.deep.equal([
+ // 'location',
+ // 'name',
+ // 'searchScore',
+ // 'snippet',
+ // ]);
+ // });
+ });
+
+ describe('sort', () => {
+ it('by score', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search({
+ query: documentMatches('tacos'),
+ sort: score().descending(),
+ // queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'eastsideTacos', 'solTacos');
+ });
+
+ // TODO(search) enable with backend support
+ // it('by distance', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('tacos'),
+ // sort: field('location')
+ // .geoDistance(new GeoPoint(39.6985, -105.024))
+ // .ascending(),
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'solTacos', 'eastsideTacos');
+ // });
+
+ // TODO(search) enable with backend support
+ // it('by multiple orderings', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('tacos OR chicken'),
+ // sort: [
+ // field('location')
+ // .geoDistance(new GeoPoint(39.6985, -105.024))
+ // .ascending(),
+ // score().descending(),
+ // ],
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(
+ // snapshot,
+ // 'solTacos',
+ // 'eastsideTacos',
+ // 'eastsideChicken',
+ // );
+ // });
+ });
+
+ describe('limit', () => {
+ // it('limits the number of documents returned', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: constant(true),
+ // sort: field('location')
+ // .geoDistance(new GeoPoint(39.6985, -105.024))
+ // .ascending(),
+ // limit: 5,
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(
+ // snapshot,
+ // 'solTacos',
+ // 'lotusBlossomThai',
+ // 'goldenWaffle',
+ // );
+ // });
+ // it('limits the number of documents scored', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches(
+ // 'chicken OR tacos OR fish OR waffles',
+ // ),
+ // retrievalDepth: 6,
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(
+ // snapshot,
+ // 'eastsideChicken',
+ // 'eastsideTacos',
+ // 'solTacos',
+ // 'mileHighCatch',
+ // );
+ // });
+ });
+
+ describe('offset', () => {
+ // it('skips N documents', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: constant(true),
+ // limit: 2,
+ // offset: 2,
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'eastsideChicken', 'eastsideTacos');
+ // });
+ });
+ });
+
+ // TODO(search) add these tests when query enhancement is supported
+ describe.skip('REQUIRE query expansion', () => {
+ // it('search full document', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: documentMatches('waffles'),
+ // queryEnhancement: 'required',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ // });
+ //
+ // it('search a specific field', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('waffles'),
+ // queryEnhancement: 'required',
+ // });
+ //
+ // const snapshot = await ppl.execute();
+ // expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ // });
+ });
+ });
+
+ // TODO(search) add these tests when snippet expression is supported
+ describe.skip('snippet', () => {
+ // it('snippet options', async () => {
+ // const ppl1 = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('waffles'),
+ // addFields: [
+ // field('menu')
+ // .snippet({
+ // rquery: 'waffles',
+ // maxSnippetWidth: 10,
+ // })
+ // .as('snippet'),
+ // ],
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot1 = await ppl1.execute();
+ // expect(snapshot1.results.length).to.equal(1);
+ // expect(snapshot1.results[0].get('name')).to.equal('The Golden Waffle');
+ // expect(snapshot1.results[0].get('snippet')?.length).to.be.greaterThan(0);
+ //
+ // const ppl2 = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('waffles'),
+ // addFields: [
+ // field('menu')
+ // .snippet({
+ // rquery: 'waffles',
+ // maxSnippetWidth: 1000,
+ // })
+ // .as('snippet'),
+ // ],
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot2 = await ppl2.execute();
+ // expect(snapshot2.results.length).to.equal(1);
+ // expect(snapshot2.results[0].get('name')).to.equal('The Golden Waffle');
+ // expect(snapshot2.results[0].get('snippet')?.length).to.be.greaterThan(0);
+ //
+ // expect(snapshot2.results[0].get('snippet')?.length).to.be.greaterThan(
+ // snapshot2.results[0].get('snippet')?.length,
+ // );
+ // });
+ //
+ // it('snippet on multiple fields', async () => {
+ // // Get snippet from 1 field
+ // const ppl1 = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: documentMatches('waffle'),
+ // addFields: [
+ // field('menu')
+ // .snippet({
+ // rquery: 'waffles',
+ // maxSnippetWidth: 2000,
+ // })
+ // .as('snippet'),
+ // ],
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot1 = await ppl1.execute();
+ // expect(snapshot1.results.length).to.equal(1);
+ // expect(snapshot1.results[0].get('name')).to.equal('The Golden Waffle');
+ // expect(snapshot1.results[0].get('snippet')?.length).to.be.greaterThan(0);
+ //
+ // // Get snippet from 2 fields
+ // const ppl2 = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: documentMatches('waffle'),
+ // addFields: [
+ // concat(field('menu'), field('description'))
+ // .snippet({
+ // rquery: 'waffles',
+ // maxSnippetWidth: 2000,
+ // })
+ // .as('snippet'),
+ // ],
+ // queryEnhancement: 'disabled',
+ // });
+ //
+ // const snapshot2 = await ppl2.execute();
+ // expect(snapshot2.results.length).to.equal(1);
+ // expect(snapshot2.results[0].get('name')).to.equal('The Golden Waffle');
+ // expect(snapshot2.results[0].get('snippet')?.length).to.be.greaterThan(0);
+ //
+ // // Expect snippet from 2 fields to be longer than snippet from one field
+ // expect(snapshot2.results[0].get('snippet')?.length).to.be.greaterThan(
+ // snapshot2.results[0].get('snippet')?.length,
+ // );
+ // });
+ // });
+ });
+});
+
// This is the Query integration tests from the lite API (no cache support)
// with some additional test cases added for more complete coverage.
// eslint-disable-next-line no-restricted-properties
diff --git a/handwritten/firestore/dev/test/pipelines/pipeline.ts b/handwritten/firestore/dev/test/pipelines/pipeline.ts
index e67514a3342..fa48b1c49ff 100644
--- a/handwritten/firestore/dev/test/pipelines/pipeline.ts
+++ b/handwritten/firestore/dev/test/pipelines/pipeline.ts
@@ -19,14 +19,9 @@ import {expect} from 'chai';
import * as sinon from 'sinon';
import {createInstance, stream} from '../util/helpers';
import {google} from '../../protos/firestore_v1_proto_api';
-import {Timestamp, Pipelines, Firestore} from '../../src';
+import {Timestamp} from '../../src';
import IExecutePipelineRequest = google.firestore.v1.IExecutePipelineRequest;
import IExecutePipelineResponse = google.firestore.v1.IExecutePipelineResponse;
-import Pipeline = Pipelines.Pipeline;
-import field = Pipelines.field;
-import sum = Pipelines.sum;
-import descending = Pipelines.descending;
-import IValue = google.firestore.v1.IValue;
const FIRST_CALL = 0;
const EXECUTE_PIPELINE_REQUEST = 0;
@@ -185,245 +180,3 @@ describe('execute(Pipeline|PipelineExecuteOptions)', () => {
);
});
});
-
-describe('stage option serialization', () => {
- // Default rawOptions
- const rawOptions: Record = {
- foo: 'bar1',
- };
- // Default expected serialized options
- const expectedSerializedOptions: Record = {
- foo: {
- stringValue: 'bar1',
- },
- };
-
- const testDefinitions: Array<{
- name: string;
- pipeline: (firestore: Firestore) => Pipeline;
- stageIndex?: number;
- expectedOptions?: Record;
- }> = [
- {
- name: 'collection stage',
- pipeline: firestore =>
- firestore.pipeline().collection({
- collection: 'foo',
- rawOptions,
- forceIndex: 'foo-index',
- }),
- expectedOptions: {
- ...expectedSerializedOptions,
- force_index: {
- stringValue: 'foo-index',
- },
- },
- },
- {
- name: 'collection group stage',
- pipeline: firestore =>
- firestore.pipeline().collectionGroup({
- collectionId: 'foo',
- rawOptions,
- forceIndex: 'bar-index',
- }),
- expectedOptions: {
- ...expectedSerializedOptions,
- force_index: {
- stringValue: 'bar-index',
- },
- },
- },
- {
- name: 'documents stage',
- pipeline: firestore =>
- firestore.pipeline().documents({
- docs: ['foo/bar'],
- rawOptions,
- }),
- },
- {
- name: 'database stage',
- pipeline: firestore =>
- firestore.pipeline().database({
- rawOptions,
- }),
- },
- {
- name: 'distinct stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .distinct({
- groups: ['foo'],
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'findNearest stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .findNearest({
- field: 'foo',
- vectorValue: [0],
- distanceMeasure: 'euclidean',
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'select stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .select({
- selections: ['foo'],
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'unnest stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .unnest({
- selectable: field('foo'),
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'addFields stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .addFields({
- fields: [field('foo')],
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'aggregate stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .aggregate({
- accumulators: [sum('foo').as('fooSum')],
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'limit stage',
- pipeline: firestore =>
- firestore.pipeline().database().limit({
- limit: 1,
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'offset stage',
- pipeline: firestore =>
- firestore.pipeline().database().offset({
- offset: 1,
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'removeFields stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .removeFields({
- fields: ['foo'],
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'replaceWith stage',
- pipeline: firestore =>
- firestore.pipeline().database().replaceWith({
- map: 'foo',
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'sample stage',
- pipeline: firestore =>
- firestore.pipeline().database().sample({
- documents: 100,
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'sample stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .sort({
- orderings: [descending('foo')],
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'union stage',
- pipeline: firestore =>
- firestore.pipeline().database().union({
- other: firestore.pipeline().database(),
- rawOptions,
- }),
- stageIndex: 1,
- },
- {
- name: 'where stage',
- pipeline: firestore =>
- firestore
- .pipeline()
- .database()
- .where({
- condition: field('foo').equal(1),
- rawOptions,
- }),
- stageIndex: 1,
- },
- ];
-
- testDefinitions.forEach(testDefinition => {
- it(testDefinition.name, async () => {
- const spy = sinon.fake.returns(stream());
- const firestore = await createInstance({
- executePipeline: spy,
- });
-
- await testDefinition.pipeline(firestore).execute();
-
- const expectedOptions = testDefinition.expectedOptions
- ? testDefinition.expectedOptions
- : expectedSerializedOptions;
-
- expect(
- spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]['structuredPipeline'][
- 'pipeline'
- ]['stages'][testDefinition.stageIndex ?? 0]['options'],
- ).to.deep.equal(expectedOptions);
- });
- });
-});
diff --git a/handwritten/firestore/dev/test/pipelines/stage.ts b/handwritten/firestore/dev/test/pipelines/stage.ts
new file mode 100644
index 00000000000..7c2f7ac225a
--- /dev/null
+++ b/handwritten/firestore/dev/test/pipelines/stage.ts
@@ -0,0 +1,359 @@
+/**
+ * @license
+ * 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.
+ * 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.
+ */
+
+import {expect} from 'chai';
+import * as sinon from 'sinon';
+import {createInstance, stream} from '../util/helpers';
+import {google} from '../../protos/firestore_v1_proto_api';
+import {Pipelines, Firestore} from '../../src';
+import Pipeline = Pipelines.Pipeline;
+import field = Pipelines.field;
+import sum = Pipelines.sum;
+import descending = Pipelines.descending;
+import constant = Pipelines.constant;
+import IValue = google.firestore.v1.IValue;
+
+const FIRST_CALL = 0;
+const EXECUTE_PIPELINE_REQUEST = 0;
+
+describe('stage option serialization', () => {
+ // Default rawOptions
+ const rawOptions: Record = {
+ foo: 'bar1',
+ };
+ // Default expected serialized options
+ const expectedSerializedOptions: Record = {
+ foo: {
+ stringValue: 'bar1',
+ },
+ };
+
+ const testDefinitions: Array<{
+ name: string;
+ pipeline: (firestore: Firestore) => Pipeline;
+ stageIndex?: number;
+ expectedOptions?: Record;
+ }> = [
+ {
+ name: 'collection stage',
+ pipeline: firestore =>
+ firestore.pipeline().collection({
+ collection: 'foo',
+ rawOptions,
+ forceIndex: 'foo-index',
+ }),
+ expectedOptions: {
+ ...expectedSerializedOptions,
+ force_index: {
+ stringValue: 'foo-index',
+ },
+ },
+ },
+ {
+ name: 'collection group stage',
+ pipeline: firestore =>
+ firestore.pipeline().collectionGroup({
+ collectionId: 'foo',
+ rawOptions,
+ forceIndex: 'bar-index',
+ }),
+ expectedOptions: {
+ ...expectedSerializedOptions,
+ force_index: {
+ stringValue: 'bar-index',
+ },
+ },
+ },
+ {
+ name: 'documents stage',
+ pipeline: firestore =>
+ firestore.pipeline().documents({
+ docs: ['foo/bar'],
+ rawOptions,
+ }),
+ },
+ {
+ name: 'database stage',
+ pipeline: firestore =>
+ firestore.pipeline().database({
+ rawOptions,
+ }),
+ },
+ {
+ name: 'distinct stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .distinct({
+ groups: ['foo'],
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'findNearest stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .findNearest({
+ field: 'foo',
+ vectorValue: [0],
+ distanceMeasure: 'euclidean',
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'select stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .select({
+ selections: ['foo'],
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'unnest stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .unnest({
+ selectable: field('foo'),
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'addFields stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .addFields({
+ fields: [field('foo')],
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'aggregate stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .aggregate({
+ accumulators: [sum('foo').as('fooSum')],
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'limit stage',
+ pipeline: firestore =>
+ firestore.pipeline().database().limit({
+ limit: 1,
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'offset stage',
+ pipeline: firestore =>
+ firestore.pipeline().database().offset({
+ offset: 1,
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'removeFields stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .removeFields({
+ fields: ['foo'],
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'replaceWith stage',
+ pipeline: firestore =>
+ firestore.pipeline().database().replaceWith({
+ map: 'foo',
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'sample stage',
+ pipeline: firestore =>
+ firestore.pipeline().database().sample({
+ documents: 100,
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'sample stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .sort({
+ orderings: [descending('foo')],
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'union stage',
+ pipeline: firestore =>
+ firestore.pipeline().database().union({
+ other: firestore.pipeline().database(),
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'where stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .where({
+ condition: field('foo').equal(1),
+ rawOptions,
+ }),
+ stageIndex: 1,
+ },
+ {
+ name: 'search stage',
+ pipeline: firestore =>
+ firestore
+ .pipeline()
+ .database()
+ .search({
+ query: 'foo',
+ // limit: 1,
+ // retrievalDepth: 2,
+ // offset: 3,
+ // queryEnhancement: 'required',
+ // languageCode: 'en-US',
+ sort: [field('foo').ascending()],
+ addFields: [constant(true).as('bar')],
+ // select: [field('id')],
+ rawOptions,
+ }),
+ stageIndex: 1,
+ expectedOptions: {
+ add_fields: {
+ mapValue: {
+ fields: {
+ bar: {
+ booleanValue: true,
+ },
+ },
+ },
+ },
+ foo: {
+ stringValue: 'bar1',
+ },
+ // language_code: {
+ // stringValue: 'en-US',
+ // },
+ // limit: {
+ // integerValue: '1',
+ // },
+ // offset: {
+ // integerValue: '3',
+ // },
+ query: {
+ functionValue: {
+ args: [
+ {
+ stringValue: 'foo',
+ },
+ ],
+ name: 'document_matches',
+ },
+ },
+ // query_enhancement: {
+ // stringValue: 'required',
+ // },
+ // retrieval_depth: {
+ // integerValue: '2',
+ // },
+ // select: {
+ // mapValue: {
+ // fields: {
+ // id: {
+ // fieldReferenceValue: 'id',
+ // },
+ // },
+ // },
+ // },
+ sort: {
+ arrayValue: {
+ values: [
+ {
+ mapValue: {
+ fields: {
+ direction: {
+ stringValue: 'ascending',
+ },
+ expression: {
+ fieldReferenceValue: 'foo',
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ];
+
+ testDefinitions.forEach(testDefinition => {
+ it(testDefinition.name, async () => {
+ const spy = sinon.fake.returns(stream());
+ const firestore = await createInstance({
+ executePipeline: spy,
+ });
+
+ await testDefinition.pipeline(firestore).execute();
+
+ const expectedOptions = testDefinition.expectedOptions
+ ? testDefinition.expectedOptions
+ : expectedSerializedOptions;
+
+ expect(
+ spy.args[FIRST_CALL][EXECUTE_PIPELINE_REQUEST]['structuredPipeline'][
+ 'pipeline'
+ ]['stages'][testDefinition.stageIndex ?? 0]['options'],
+ ).to.deep.equal(expectedOptions);
+ });
+ });
+});
diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts
index 69a3554988e..7f4b08ad63a 100644
--- a/handwritten/firestore/types/firestore.d.ts
+++ b/handwritten/firestore/types/firestore.d.ts
@@ -5603,6 +5603,67 @@ declare namespace FirebaseFirestore {
*/
isType(type: Type): BooleanExpression;
+ // TODO(search) enable with backend support
+ // /**
+ // * Evaluates if the result of this `expression` is between
+ // * the `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * field('tireWidth').between(constant(2.2), constant(2.4))
+ // *
+ // * // This is functionally equivalent to
+ // * and(field('tireWidth').greaterThanOrEqual(contant(2.2)), field('tireWidth').lessThanOrEqual(constant(2.4)))
+ // * ```
+ // *
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // between(
+ // lowerBound: Expression,
+ // upperBound: Expression,
+ // ): BooleanExpression;
+ //
+ // /**
+ // * Evaluates if the result of this `expression` is between
+ // * the `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * field('tireWidth').between(2.2, 2.4)
+ // *
+ // * // This is functionally equivalent to
+ // * and(field('tireWidth').greaterThanOrEqual(2.2), field('tireWidth').lessThanOrEqual(2.4))
+ // * ```
+ // *
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // between(lowerBound: unknown, upperBound: unknown): BooleanExpression;
+
+ // TODO(search) enable with backend support
+ // /**
+ // * Evaluates to an HTML-formatted text snippet that renders terms matching
+ // * the search query in `bold`.
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param rquery Define the search query using the search DSL.
+ // */
+ // snippet(rquery: string): Expression;
+ //
+ // /**
+ // * Evaluates to an HTML-formatted text snippet that renders terms matching
+ // * the search query in `bold`.
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param options Define how snippeting behaves.
+ // */
+ // snippet(options: SnippetOptions): Expression;
+
// TODO(new-expression): Add new expression method declarations above this line
/**
* @beta
@@ -5831,6 +5892,27 @@ declare namespace FirebaseFirestore {
* @returns The name of the field.
*/
get fieldName(): string;
+
+ // TODO(search) enable with backend support
+ // /**
+ // * Perform a full-text search on this field.
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param rquery Define the search query using the rquery DTS.
+ // */
+ // matches(rquery: string | Expression): BooleanExpression;
+
+ /**
+ * Evaluates to the distance in meters between the location specified
+ * by this field and the query location.
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ *
+ * @param location - Compute distance to this GeoPoint.
+ */
+ geoDistance(location: GeoPoint | Expression): Expression;
+
/**
* @beta
* @internal
@@ -11432,6 +11514,209 @@ declare namespace FirebaseFirestore {
type: Type,
): BooleanExpression;
+ // TODO(search) enable with backend support
+ // /**
+ // * Perform a full-text search on the specified field.
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param searchField Search the specified field.
+ // * @param rquery Define the search query using the search DSL.
+ // */
+ // export function matches(
+ // searchField: string | Field,
+ // rquery: string | Expression,
+ // ): BooleanExpression;
+
+ /**
+ * Perform a full-text search on all indexed search fields in the document.
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ *
+ * @example
+ * ```typescript
+ * db.pipeline().collection('restaurants').search({
+ * query: documentMatches('waffles OR pancakes')
+ * })
+ * ```
+ *
+ * @param rquery Define the search query using the search DSL.
+ */
+ export function documentMatches(
+ rquery: string | Expression,
+ ): BooleanExpression;
+
+ /**
+ * Evaluates to the search score that reflects the topicality of the document
+ * to all of the text predicates (for example: `documentMatches`)
+ * in the search query. If `SearchOptions.query` is not set or does not contain
+ * any text predicates, then this topicality score will always be `0`.
+ *
+ * @example
+ * ```typescript
+ * db.pipeline().collection('restaurants').search({
+ * query: 'waffles',
+ * sort: score().descending()
+ * })
+ * ```
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ */
+ export function score(): Expression;
+
+ // TODO(search) enable with backend support
+ // /**
+ // * Evaluates to an HTML-formatted text snippet that highlights terms matching
+ // * the search query in `bold`.
+ // *
+ // * @example
+ // * ```typescript
+ // * db.pipeline().collection('restaurants').search({
+ // * query: 'waffles',
+ // * addFields: { snippet: snippet('menu', 'waffles') }
+ // * })
+ // * ```
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param searchField Search the specified field for matching terms.
+ // * @param rquery Define the search query using the search DSL.
+ // */
+ // export function snippet(
+ // searchField: string | Field,
+ // rquery: string,
+ // ): Expression;
+ //
+ // /**
+ // * Evaluates to an HTML-formatted text snippet that highlights terms matching
+ // * the search query in `bold`.
+ // *
+ // * @remarks This Expression can only be used within a `Search` stage.
+ // *
+ // * @param searchField Search the specified field for matching terms.
+ // * @param options Define the search query using the search DSL.
+ // */
+ // export function snippet(
+ // searchField: string | Field,
+ // options: SnippetOptions,
+ // ): Expression;
+
+ /**
+ * Evaluates to the distance in meters between the location in the specified
+ * field and the query location.
+ *
+ * @example
+ * ```typescript
+ * db.pipeline().collection('restaurants').search({
+ * query: 'waffles',
+ * sort: geoDistance('location', new GeoPoint(37.0, -122.0)).ascending()
+ * })
+ * ```
+ *
+ * @remarks This Expression can only be used within a `Search` stage.
+ *
+ * @param fieldName - Specifies the field in the document which contains
+ * the first GeoPoint for distance computation.
+ * @param location - Compute distance to this GeoPoint.
+ */
+ export function geoDistance(
+ fieldName: string | Field,
+ location: GeoPoint | Expression,
+ ): Expression;
+
+ // TODO(search) enable when supported by the backend
+ // /**
+ // * Evaluates if the value in the field specified by `fieldName` is between
+ // * the evaluated values for `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * between('tireWidth', constant(2.2), constant(2.4))
+ // *
+ // * // This is functionally equivalent to
+ // * and(greaterThanOrEqual('tireWidth', constant(2.2)), lessThanOrEqual('tireWidth', constant(2.4)))
+ // * ```
+ // *
+ // * @param fieldName - Evaluate if the value stored in this field is between the lower and upper bounds.
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // export function between(
+ // fieldName: string,
+ // lowerBound: Expression,
+ // upperBound: Expression,
+ // ): BooleanExpression;
+ //
+ // /**
+ // * Evaluates if the value in the field specified by `fieldName` is between
+ // * the values for `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * between('tireWidth', 2.2, 2.4)
+ // *
+ // * // This is functionally equivalent to
+ // * and(greaterThanOrEqual('tireWidth', 2.2), lessThanOrEqual('tireWidth', 2.4))
+ // * ```
+ // *
+ // * @param fieldName - Evaluate if the value stored in this field is between the lower and upper bounds.
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // export function between(
+ // fieldName: string,
+ // lowerBound: unknown,
+ // upperBound: unknown,
+ // ): BooleanExpression;
+ //
+ // /**
+ // * Evaluates if the result of the specified `expression` is between
+ // * the results of `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * between(field('tireWidth'), constant(2.2), constant(2.4))
+ // *
+ // * // This is functionally equivalent to
+ // * and(greaterThanOrEqual(field('tireWidth'), constant(2.2)), lessThanOrEqual(field('tireWidth'), constant(2.4)))
+ // * ```
+ // *
+ // * @param expression - Evaluate if the result of this expression is between the lower and upper bounds.
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // export function between(
+ // expression: Expression,
+ // lowerBound: Expression,
+ // upperBound: Expression,
+ // ): BooleanExpression;
+ //
+ // /**
+ // * Evaluates if the result of the specified `expression` is between
+ // * the `lowerBound` (inclusive) and `upperBound` (inclusive).
+ // *
+ // * @example
+ // * ```
+ // * // Evaluate if the 'tireWidth' is between 2.2 and 2.4
+ // * between(field('tireWidth'), 2.2, 2.4)
+ // *
+ // * // This is functionally equivalent to
+ // * and(greaterThanOrEqual(field('tireWidth'), 2.2), lessThanOrEqual(field('tireWidth'), 2.4))
+ // * ```
+ // *
+ // * @param expression - Evaluate if the result of this expression is between the lower and upper bounds.
+ // * @param lowerBound - Lower bound (inclusive) of the range.
+ // * @param upperBound - Upper bound (inclusive) of the range.
+ // */
+ // export function between(
+ // expression: Expression,
+ // lowerBound: unknown,
+ // upperBound: unknown,
+ // ): BooleanExpression;
+
// TODO(new-expression): Add new top-level expression function declarations above this line
/**
* @beta
@@ -12901,6 +13186,131 @@ declare namespace FirebaseFirestore {
orderings: Ordering[];
};
+ ///**
+ // * Specifies if the `matches` and `snippet` expressions will enhance the user
+ // * provided query to perform matching of synonyms, misspellings, lemmatization,
+ // * stemming.
+ // *
+ // * required - search will fail if the query enhancement times out or if the query
+ // * enhancement is not supported by the project's DRZ compliance
+ // * requirements.
+ // * preferred - search will fall back to the un-enhanced, user provided query, if
+ // * the query enhancement fails.
+ // */
+ // TODO(search) enable with backend support
+ // export type QueryEnhancement = 'disabled' | 'required' | 'preferred';
+
+ /**
+ * Options defining how a SearchStage is evaluated. See {@link @firebase/firestore/pipelines#Pipeline.(search)}.
+ */
+ export type SearchStageOptions = StageOptions & {
+ /**
+ * Specifies the search query that will be used to query and score documents
+ * by the search stage.
+ *
+ * The query can be expressed as an `Expression`, which will be used to score
+ * and filter the results. Not all expressions supported by Pipelines
+ * are supported in the Search query.
+ *
+ * @example
+ * ```typescript
+ * db.pipeline().collection('restaurants').search({
+ * query: documentMatches("breakfast")
+ * })
+ * ```
+ *
+ * The query can also be expressed as a string in the Search DSL:
+ *
+ * @example
+ * ```
+ * db.pipeline().collection('restaurants').search({
+ * query: 'breakfast'
+ * })
+ * ```
+ */
+ query: BooleanExpression | string;
+
+ ///**
+ // * The BCP-47 language code of text in the search query, such as, “en-US” or “sr-Latn”
+ // */
+ // TODO(search) enable with backend support
+ //languageCode?: string;
+
+ // TODO(search) add indexPartition after languageCode
+
+ ///**
+ // * The maximum number of documents to retrieve. Documents will be retrieved in the
+ // * pre-sort order specified by the search index.
+ // */
+ // TODO(search) enable with backend support
+ //retrievalDepth?: number;
+
+ /**
+ * Orderings specify how the input documents are sorted.
+ * One or more ordering are required.
+ */
+ sort?: Ordering | Ordering[];
+
+ // /**
+ // * The number of documents to skip.
+ // */
+ // TODO(search) enable with backend support
+ // offset?: number;
+
+ // /**
+ // * The maximum number of documents to return from the Search stage.
+ // */
+ // TODO(search) enable with backend support
+ // limit?: number;
+
+ // /**
+ // * The fields to keep or add to each document,
+ // * specified as an array of {@link @firebase/firestore/pipelines#Selectable}.
+ // */
+ // TODO(search) enable with backend support
+ // select?: Array;
+
+ /**
+ * The fields to add to each document, specified as a {@link @firebase/firestore/pipelines#Selectable}.
+ */
+ addFields?: Selectable[];
+
+ // /**
+ // * Define the query expansion behavior used by full-text search expressions
+ // * in this search stage.
+ // */
+ // TODO(search) enable with backend support
+ // queryEnhancement?: QueryEnhancement;
+ };
+
+ // TODO(search) enable with backend support
+ // /**
+ // * Options defining how a snippet expression is evaluated.
+ // */
+ // export type SnippetOptions = {
+ // /**
+ // * Define the search query using the search DSL.
+ // */
+ // rquery: string;
+ //
+ // /**
+ // * The maximum width of the string estimated for a variable width font. The
+ // * unit is tenths of ems. The default is `160`.
+ // */
+ // maxSnippetWidth?: number;
+ //
+ // /**
+ // * The maximum number of non-contiguous pieces of text in the returned snippet.
+ // * The default is `1`.
+ // */
+ // maxSnippets?: number;
+ //
+ // /**
+ // * The string to join the pieces. The default value is '\n'
+ // */
+ // separator?: string;
+ // };
+
/**
* @beta
* Represents a field value within the explain statistics, which can be a primitive type (null, string, number, boolean)