From ddd840c30504fde3ecdba6287e921af375fe5a94 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Thu, 12 Mar 2026 10:17:52 -0600
Subject: [PATCH 1/8] search implementation
---
.../firestore/dev/src/pipelines/expression.ts | 353 +++++++++
.../firestore/dev/src/pipelines/pipelines.ts | 27 +
.../firestore/dev/src/pipelines/stage.ts | 55 ++
.../firestore/dev/system-test/pipeline.ts | 674 +++++++++++++++++-
handwritten/firestore/types/firestore.d.ts | 353 +++++++++
5 files changed, 1424 insertions(+), 38 deletions(-)
diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts
index 20054a59b81..dd6cc73452d 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 DTS (TODO(search) link).
+ */
+ 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,53 @@ 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 {
+ return {
+ functionValue: {
+ ...super._toProto(serializer),
+ options: this._optionsUtil.getOptionsProto(
+ serializer,
+ this._options ?? {},
+ {},
+ ),
+ },
+ };
+ }
+}
+
/**
* @beta
* This class defines the base class for Firestore `Pipeline` functions, which can be evaluated within pipeline
@@ -10194,6 +10352,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 DTS.
+ */
+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 of the text predicates (`queryMatch`)
+ * 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('search_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 DTS (TODO(search) link).
+ */
+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 DTS (TODO(search) link).
+ */
+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/pipelines.ts b/handwritten/firestore/dev/src/pipelines/pipelines.ts
index 2844c29ca57..0a5c01f1a2f 100644
--- a/handwritten/firestore/dev/src/pipelines/pipelines.ts
+++ b/handwritten/firestore/dev/src/pipelines/pipelines.ts
@@ -59,6 +59,7 @@ import {
constant,
_mapValue,
field,
+ documentMatches,
} from './expression';
import {
AddFields,
@@ -95,6 +96,8 @@ import {
InternalDocumentsStageOptions,
InternalCollectionGroupStageOptions,
InternalCollectionStageOptions,
+ Search,
+ InternalSearchStageOptions,
} from './stage';
import {StructuredPipeline} from './structured-pipeline';
import Selectable = FirebaseFirestore.Pipelines.Selectable;
@@ -1420,6 +1423,30 @@ 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 {
+ const normalizedQuery = isString(options.query)
+ ? documentMatches(options.query)
+ : (options.query as BooleanExpression | undefined);
+
+ const internalOptions: InternalSearchStageOptions = {
+ ...options,
+ query: normalizedQuery,
+ };
+
+ // 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..e73c4610fbb 100644
--- a/handwritten/firestore/dev/src/pipelines/stage.ts
+++ b/handwritten/firestore/dev/src/pipelines/stage.ts
@@ -645,6 +645,61 @@ export class Sort implements Stage {
}
}
+export type InternalSearchStageOptions = Omit<
+ firestore.Pipelines.SearchStageOptions,
+ 'query'
+> & {
+ query?: BooleanExpression;
+};
+
+/**
+ * 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',
+ },
+ queryExpansion: {
+ serverName: 'query_expansion',
+ },
+ });
+
+ _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 7bdb4405b17..b78da0bc133 100644
--- a/handwritten/firestore/dev/system-test/pipeline.ts
+++ b/handwritten/firestore/dev/system-test/pipeline.ts
@@ -171,53 +171,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);
}
+}
+
+describe.skipClassic('Pipeline class', () => {
+ let firestore: Firestore;
+ let randomCol: CollectionReference;
- async function setupBookDocs(): Promise> {
+ async function setupBookDocs(
+ collection: CollectionReference,
+ ): Promise> {
const bookDocs: {[id: string]: DocumentData} = {
book1: {
title: "The Hitchhiker's Guide to the Galaxy",
@@ -327,12 +330,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;
});
@@ -5503,6 +5506,601 @@ 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);
+ }
+
+ // 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('SearchIntegrationTests');
+ await setupRestaurantDocs(restaurantsCollection);
+ firestore = restaurantsCollection.firestore;
+ });
+
+ describe('search stage', () => {
+ describe('DISABLE query expansion', () => {
+ describe('query', () => {
+ it('all search features', async () => {
+ const queryLocation = new GeoPoint(0, 0);
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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('restaurants')
+ .search({
+ query: documentMatches('waffles'),
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'goldenWaffle');
+ });
+
+ it('search a specific field', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .search({
+ query: field('menu').matches('waffles'),
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'goldenWaffle');
+ });
+
+ it('geo near query', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .search({
+ query: field('location')
+ .geoDistance(new GeoPoint(39.6985, -105.024))
+ .lessThan(1000 /* m */),
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'solTacos');
+ });
+
+ it('conjunction of text search predicates', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .search({
+ query: and(
+ field('menu').matches('waffles'),
+ field('description').matches('diner'),
+ ),
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ });
+
+ it('conjunction of text search and geo near', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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('restaurants')
+ .search({
+ query: field('menu').matches('-waffles'),
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(
+ snapshot,
+ 'eastSideTacos',
+ 'solTacos',
+ 'peakBurgers',
+ 'mileHighCatch',
+ 'lotusBlossomThai',
+ 'sunnySideUp',
+ );
+ });
+
+ it('rquery search the document with conjunction and disjunction', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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('restaurants').search({
+ query: '(waffles OR pancakes) AND coffee',
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ });
+
+ it('rquery supports field paths', async () => {
+ const ppl = firestore.pipeline().collection('restaurants').search({
+ query:
+ 'menu:(waffles OR pancakes) AND description:"breakfast all day"',
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'sunnySideUp');
+ });
+
+ it('conjunction of rquery and expression', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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('topicality score and snippet', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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', () => {
+ it('topicality score and snippet', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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 topicality', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .search({
+ query: field('menu').matches('tacos'),
+ sort: score().descending(),
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'eastsideTacos', 'solTacos');
+ });
+
+ it('by distance', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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');
+ });
+
+ it('by multiple orderings', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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('restaurants')
+ .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('restaurants')
+ .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('restaurants')
+ .search({
+ query: constant(true),
+ limit: 2,
+ offset: 2,
+ queryEnhancement: 'disabled',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'eastsideChicken', 'eastsideTacos');
+ });
+ });
+ });
+
+ describe('REQUIRE query expansion', () => {
+ it('search full document', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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('restaurants')
+ .search({
+ query: field('menu').matches('waffles'),
+ queryEnhancement: 'required',
+ });
+
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ });
+ });
+ });
+
+ describe('snippet', () => {
+ it('snippet options', async () => {
+ const ppl1 = firestore
+ .pipeline()
+ .collection('restaurants')
+ .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('restaurants')
+ .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('restaurants')
+ .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('restaurants')
+ .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/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts
index e58444a2053..a539d4feb73 100644
--- a/handwritten/firestore/types/firestore.d.ts
+++ b/handwritten/firestore/types/firestore.d.ts
@@ -5603,6 +5603,65 @@ declare namespace FirebaseFirestore {
*/
isType(type: Type): 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(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;
+
+ /**
+ * 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 DTS (TODO(search) link).
+ */
+ 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 +5890,26 @@ declare namespace FirebaseFirestore {
* @returns The name of the field.
*/
get fieldName(): string;
+
+ /**
+ * 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
@@ -11375,6 +11454,174 @@ declare namespace FirebaseFirestore {
expression: Expression,
type: Type,
): BooleanExpression;
+ /**
+ * 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 DTS.
+ */
+ export function matches(
+ searchField: string | Field,
+ rquery: string | Expression,
+ ): BooleanExpression;
+
+ /**
+ * 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;
+
+ /**
+ * Evaluates to the search score that reflects the topicality of the document
+ * to all of the text predicates (`queryMatch`)
+ * 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;
+
+ /**
+ * 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 DTS (TODO(search) link).
+ */
+ 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 DTS (TODO(search) link).
+ */
+ 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.
+ *
+ * @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;
+
+ /**
+ * 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
/**
@@ -12845,6 +13092,112 @@ 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.
+ */
+ 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
+ * ```
+ * db.pipeline().collection('restaurants').search({
+ * query: or(
+ * documentContainsText("breakfast"),
+ * field('menu').containsText('waffle AND coffee')
+ * )
+ * })
+ * ```
+ *
+ * The query can also be expressed as a string in the Search DSL:
+ *
+ * @example
+ * ```
+ * db.pipeline().collection('restaurants').search({
+ * query: 'menu:(waffle and coffee) OR breakfast'
+ * })
+ * ```
+ */
+ query: BooleanExpression | string;
+ /**
+ * The maximum number of documents to return from the Search stage.
+ */
+ limit?: number;
+ /**
+ * 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.
+ */
+ retrievalDepth?: number;
+ /**
+ * Orderings specify how the input documents are sorted.
+ * One or more ordering are required.
+ */
+ sort?: Ordering | Ordering[];
+ /**
+ * The fields to add to each document, specified as a {@link @firebase/firestore/pipelines#Selectable}.
+ */
+ addFields?: Selectable[];
+ /**
+ * The fields to keep or add to each document,
+ * specified as an array of {@link @firebase/firestore/pipelines#Selectable}.
+ */
+ select?: Array;
+ /**
+ * The number of documents to skip.
+ */
+ offset?: number;
+ /**
+ * Define the query expansion behavior used by full-text search expressions
+ * in this search stage.
+ */
+ queryEnhancement?: QueryEnhancement;
+ };
+
+ /**
+ * Options defining how a snippet expression is evaluated.
+ */
+ export type SnippetOptions = {
+ /**
+ * Define the search query using the search DTS.
+ */
+ 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)
From 61e61197026781ac252de61e62c7583b22d7fa4b Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Mon, 16 Mar 2026 13:25:45 -0600
Subject: [PATCH 2/8] Search fixes and cleanup
---
.../dev/src/pipelines/pipeline-util.ts | 16 +-
.../firestore/dev/src/pipelines/pipelines.ts | 19 +-
.../firestore/dev/src/pipelines/stage.ts | 14 +-
.../firestore/dev/test/pipelines/pipeline.ts | 249 +-----------
.../firestore/dev/test/pipelines/stage.ts | 359 ++++++++++++++++++
handwritten/firestore/types/firestore.d.ts | 26 +-
6 files changed, 419 insertions(+), 264 deletions(-)
create mode 100644 handwritten/firestore/dev/test/pipelines/stage.ts
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 0a5c01f1a2f..61af8375a1a 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';
@@ -1434,13 +1435,25 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
* @return A new `Pipeline` object with this stage appended to the stage list.
*/
search(options: firestore.Pipelines.SearchStageOptions): Pipeline {
- const normalizedQuery = isString(options.query)
- ? documentMatches(options.query)
- : (options.query as BooleanExpression | undefined);
+ // 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
diff --git a/handwritten/firestore/dev/src/pipelines/stage.ts b/handwritten/firestore/dev/src/pipelines/stage.ts
index e73c4610fbb..b06b70e310a 100644
--- a/handwritten/firestore/dev/src/pipelines/stage.ts
+++ b/handwritten/firestore/dev/src/pipelines/stage.ts
@@ -647,9 +647,12 @@ export class Sort implements Stage {
export type InternalSearchStageOptions = Omit<
firestore.Pipelines.SearchStageOptions,
- 'query'
+ 'query' | 'sort' | 'select' | 'addFields'
> & {
- query?: BooleanExpression;
+ query: firestore.Pipelines.BooleanExpression;
+ sort?: Array;
+ select?: Record;
+ addFields?: Record;
};
/**
@@ -682,8 +685,11 @@ export class Search implements Stage {
offset: {
serverName: 'offset',
},
- queryExpansion: {
- serverName: 'query_expansion',
+ queryEnhancement: {
+ serverName: 'query_enhancement',
+ },
+ languageCode: {
+ serverName: 'language_code',
},
});
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..d3b4535574c
--- /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 a539d4feb73..a9c382de08a 100644
--- a/handwritten/firestore/types/firestore.d.ts
+++ b/handwritten/firestore/types/firestore.d.ts
@@ -13137,33 +13137,47 @@ declare namespace FirebaseFirestore {
* ```
*/
query: BooleanExpression | string;
+
/**
- * The maximum number of documents to return from the Search stage.
+ * The BCP-47 language code of text in the search query, such as, “en-US” or “sr-Latn”
*/
- limit?: number;
+ languageCode?: string;
+
+ // TODO(search) add indexPartition after languageCode
+
/**
* 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.
*/
retrievalDepth?: number;
+
/**
* Orderings specify how the input documents are sorted.
* One or more ordering are required.
*/
sort?: Ordering | Ordering[];
+
/**
- * The fields to add to each document, specified as a {@link @firebase/firestore/pipelines#Selectable}.
+ * The number of documents to skip.
*/
- addFields?: Selectable[];
+ offset?: number;
+
+ /**
+ * The maximum number of documents to return from the Search stage.
+ */
+ limit?: number;
+
/**
* The fields to keep or add to each document,
* specified as an array of {@link @firebase/firestore/pipelines#Selectable}.
*/
select?: Array;
+
/**
- * The number of documents to skip.
+ * The fields to add to each document, specified as a {@link @firebase/firestore/pipelines#Selectable}.
*/
- offset?: number;
+ addFields?: Selectable[];
+
/**
* Define the query expansion behavior used by full-text search expressions
* in this search stage.
From 265178ad4ab7b0577ffc7bc70cc3f51d49f252d2 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Mon, 16 Mar 2026 14:43:35 -0600
Subject: [PATCH 3/8] Fixes for score and snippet expression serialization
---
.../firestore/dev/src/pipelines/expression.ts | 20 +++++++++----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts
index dd6cc73452d..0f0be4eb447 100644
--- a/handwritten/firestore/dev/src/pipelines/expression.ts
+++ b/handwritten/firestore/dev/src/pipelines/expression.ts
@@ -3831,16 +3831,14 @@ export class SnippetExpression extends FunctionExpression {
}
_toProto(serializer: Serializer): api.IValue {
- return {
- functionValue: {
- ...super._toProto(serializer),
- options: this._optionsUtil.getOptionsProto(
- serializer,
- this._options ?? {},
- {},
- ),
- },
- };
+ const proto = super._toProto(serializer);
+ proto.functionValue!.options = this._optionsUtil.getOptionsProto(
+ serializer,
+ this._options ?? {},
+ {},
+ );
+
+ return proto;
}
}
@@ -10391,7 +10389,7 @@ export function documentMatches(
* @remarks This Expression can only be used within a `Search` stage.
*/
export function score(): Expression {
- return new FunctionExpression('search_score', []).asBoolean();
+ return new FunctionExpression('score', []).asBoolean();
}
/**
From e22c5fe46cab938598a3b7dbf69dc43de14611b9 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Mon, 23 Mar 2026 09:29:07 -0600
Subject: [PATCH 4/8] Update
handwritten/firestore/dev/src/pipelines/expression.ts
Co-authored-by: Daniel La Rocque
---
handwritten/firestore/dev/src/pipelines/expression.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts
index cf1f47f6044..5ecee7cdb2a 100644
--- a/handwritten/firestore/dev/src/pipelines/expression.ts
+++ b/handwritten/firestore/dev/src/pipelines/expression.ts
@@ -3108,7 +3108,7 @@ export abstract class Expression
* the `lowerBound` (inclusive) and `upperBound` (inclusive).
*
* @example
- * ```
+ * ```typescript
* // Evaluate if the 'tireWidth' is between 2.2 and 2.4
* field('tireWidth').between(constant(2.2), constant(2.4))
*
From f53e05004f58ce929e2d47d47d5b902ac87bb6cb Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Mon, 23 Mar 2026 09:29:20 -0600
Subject: [PATCH 5/8] Update
handwritten/firestore/dev/src/pipelines/expression.ts
Co-authored-by: Daniel La Rocque
---
handwritten/firestore/dev/src/pipelines/expression.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts
index 5ecee7cdb2a..e7d931f43ca 100644
--- a/handwritten/firestore/dev/src/pipelines/expression.ts
+++ b/handwritten/firestore/dev/src/pipelines/expression.ts
@@ -3116,8 +3116,8 @@ export abstract class Expression
* 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.
+ * @param lowerBound - An `Expression` that evaluates to the lower bound (inclusive) of the range.
+ * @param upperBound - An `Expression` that evaluates to the upper bound (inclusive) of the range.
*/
between(lowerBound: Expression, upperBound: Expression): BooleanExpression;
From 0e925babf77309c9547915fbd8942419de1caec9 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Fri, 27 Mar 2026 15:12:28 -0400
Subject: [PATCH 6/8] documentation cleanup
---
.../firestore/dev/src/pipelines/expression.ts | 10 ++--
handwritten/firestore/types/firestore.d.ts | 57 ++++++++++++++-----
2 files changed, 49 insertions(+), 18 deletions(-)
diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts
index 0f0be4eb447..f5a5076677b 100644
--- a/handwritten/firestore/dev/src/pipelines/expression.ts
+++ b/handwritten/firestore/dev/src/pipelines/expression.ts
@@ -3153,7 +3153,7 @@ export abstract class Expression
*
* @remarks This Expression can only be used within a `Search` stage.
*
- * @param rquery Define the search query using the search DTS (TODO(search) link).
+ * @param rquery Define the search query using the search DSL.
*/
snippet(rquery: string): Expression;
@@ -10356,7 +10356,7 @@ export function isType(
* @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 DTS.
+ * @param rquery Define the search query using the search DSL.
*/
export function matches(
searchField: string | Field,
@@ -10382,7 +10382,7 @@ export function documentMatches(
/**
* Evaluates to the search score that reflects the topicality of the document
- * to all of the text predicates (`queryMatch`)
+ * 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`.
*
@@ -10399,7 +10399,7 @@ export function score(): Expression {
* @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 DTS (TODO(search) link).
+ * @param rquery Define the search query using the search DSL.
*/
export function snippet(
searchField: string | Field,
@@ -10413,7 +10413,7 @@ export function snippet(
* @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 DTS (TODO(search) link).
+ * @param options Define the search query using the search DSL.
*/
export function snippet(
searchField: string | Field,
diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts
index a9c382de08a..08795f7f5af 100644
--- a/handwritten/firestore/types/firestore.d.ts
+++ b/handwritten/firestore/types/firestore.d.ts
@@ -5648,7 +5648,7 @@ declare namespace FirebaseFirestore {
*
* @remarks This Expression can only be used within a `Search` stage.
*
- * @param rquery Define the search query using the search DTS (TODO(search) link).
+ * @param rquery Define the search query using the search DSL.
*/
snippet(rquery: string): Expression;
@@ -11460,7 +11460,7 @@ declare namespace FirebaseFirestore {
* @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 DTS.
+ * @param rquery Define the search query using the search DSL.
*/
export function matches(
searchField: string | Field,
@@ -11468,11 +11468,18 @@ declare namespace FirebaseFirestore {
): BooleanExpression;
/**
- * Perform a full-text search on the document.
+ * Perform a full-text search on all indexed search fields in the document.
*
* @remarks This Expression can only be used within a `Search` stage.
*
- * @param rquery Define the search query using the rquery DTS.
+ * @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,
@@ -11480,10 +11487,18 @@ declare namespace FirebaseFirestore {
/**
* Evaluates to the search score that reflects the topicality of the document
- * to all of the text predicates (`queryMatch`)
+ * 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;
@@ -11492,10 +11507,18 @@ declare namespace FirebaseFirestore {
* 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 DTS (TODO(search) link).
+ * @param rquery Define the search query using the search DSL.
*/
export function snippet(
searchField: string | Field,
@@ -11509,7 +11532,7 @@ declare namespace FirebaseFirestore {
* @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 DTS (TODO(search) link).
+ * @param options Define the search query using the search DSL.
*/
export function snippet(
searchField: string | Field,
@@ -11520,6 +11543,14 @@ declare namespace FirebaseFirestore {
* 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
@@ -13118,11 +13149,11 @@ declare namespace FirebaseFirestore {
* are supported in the Search query.
*
* @example
- * ```
+ * ```typescript
* db.pipeline().collection('restaurants').search({
* query: or(
- * documentContainsText("breakfast"),
- * field('menu').containsText('waffle AND coffee')
+ * documentMatches("breakfast"),
+ * matches('menu', 'waffle AND coffee')
* )
* })
* ```
@@ -13146,8 +13177,8 @@ declare namespace FirebaseFirestore {
// TODO(search) add indexPartition after languageCode
/**
- * 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.
+ * The maximum number of documents to retrieve. Documents will be retrieved in the
+ * pre-sort order specified by the search index.
*/
retrievalDepth?: number;
@@ -13190,7 +13221,7 @@ declare namespace FirebaseFirestore {
*/
export type SnippetOptions = {
/**
- * Define the search query using the search DTS.
+ * Define the search query using the search DSL.
*/
rquery: string;
From 781b5c71b447e72b7e1d21a7c840e3896ed7f437 Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Fri, 27 Mar 2026 16:05:56 -0400
Subject: [PATCH 7/8] Trim search API to final API
---
.../firestore/dev/src/pipelines/expression.ts | 454 ++++----
.../firestore/dev/src/pipelines/pipelines.ts | 6 +-
.../firestore/dev/system-test/pipeline.ts | 981 +++++++++---------
.../firestore/dev/test/pipelines/stage.ts | 60 +-
handwritten/firestore/types/firestore.d.ts | 560 +++++-----
5 files changed, 1052 insertions(+), 1009 deletions(-)
diff --git a/handwritten/firestore/dev/src/pipelines/expression.ts b/handwritten/firestore/dev/src/pipelines/expression.ts
index f5a5076677b..c00e6d12082 100644
--- a/handwritten/firestore/dev/src/pipelines/expression.ts
+++ b/handwritten/firestore/dev/src/pipelines/expression.ts
@@ -3103,84 +3103,84 @@ 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);
- }
+ // /**
+ // * 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
@@ -10350,20 +10350,20 @@ 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 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.
@@ -10392,41 +10392,41 @@ 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 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
@@ -10444,106 +10444,106 @@ export function geoDistance(
): 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);
-}
+//
+// /**
+// * 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/pipelines.ts b/handwritten/firestore/dev/src/pipelines/pipelines.ts
index 61af8375a1a..4b183b16935 100644
--- a/handwritten/firestore/dev/src/pipelines/pipelines.ts
+++ b/handwritten/firestore/dev/src/pipelines/pipelines.ts
@@ -1441,8 +1441,8 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
)
? options.query
: documentMatches(options.query);
- const normalizedSelect: Record | undefined =
- options.select ? selectablesToObject(options.select) : undefined;
+ // 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 =
@@ -1451,7 +1451,7 @@ export class Pipeline implements firestore.Pipelines.Pipeline {
const internalOptions: InternalSearchStageOptions = {
...options,
query: normalizedQuery,
- select: normalizedSelect,
+ // select: normalizedSelect,
addFields: normalizedAddFields,
sort: normalizedSort,
};
diff --git a/handwritten/firestore/dev/system-test/pipeline.ts b/handwritten/firestore/dev/system-test/pipeline.ts
index b78da0bc133..708f3926fd3 100644
--- a/handwritten/firestore/dev/system-test/pipeline.ts
+++ b/handwritten/firestore/dev/system-test/pipeline.ts
@@ -5597,506 +5597,537 @@ describe.skipClassic('Pipeline search', () => {
return testCollectionWithDocs(collection, restaurantDocs);
}
- // 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('SearchIntegrationTests');
- await setupRestaurantDocs(restaurantsCollection);
- firestore = restaurantsCollection.firestore;
- });
-
- describe('search stage', () => {
- describe('DISABLE query expansion', () => {
- describe('query', () => {
- it('all search features', async () => {
- const queryLocation = new GeoPoint(0, 0);
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .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('restaurants')
- .search({
- query: documentMatches('waffles'),
- queryEnhancement: 'disabled',
- });
-
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'goldenWaffle');
- });
-
- it('search a specific field', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: field('menu').matches('waffles'),
- queryEnhancement: 'disabled',
- });
-
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'goldenWaffle');
- });
-
- it('geo near query', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: field('location')
- .geoDistance(new GeoPoint(39.6985, -105.024))
- .lessThan(1000 /* m */),
- queryEnhancement: 'disabled',
- });
-
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'solTacos');
- });
-
- it('conjunction of text search predicates', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: and(
- field('menu').matches('waffles'),
- field('description').matches('diner'),
- ),
- queryEnhancement: 'disabled',
- });
-
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
- });
+ describe('text search', () => {
+ 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');
+ });
- it('conjunction of text search and geo near', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: and(
- field('menu').matches('tacos'),
- field('location')
+ // 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(10_000 /* meters */),
- ),
- queryEnhancement: 'disabled',
- });
+ .lessThan(1000 /* m */),
+ // queryEnhancement: 'disabled',
+ });
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'solTacos');
- });
-
- it('negate match', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: field('menu').matches('-waffles'),
- queryEnhancement: 'disabled',
- });
-
- const snapshot = await ppl.execute();
- expectResults(
- snapshot,
- 'eastSideTacos',
- 'solTacos',
- 'peakBurgers',
- 'mileHighCatch',
- 'lotusBlossomThai',
- 'sunnySideUp',
- );
- });
-
- it('rquery search the document with conjunction and disjunction', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .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('restaurants').search({
- query: '(waffles OR pancakes) AND coffee',
- queryEnhancement: 'disabled',
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, 'solTacos');
});
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
- });
-
- it('rquery supports field paths', async () => {
- const ppl = firestore.pipeline().collection('restaurants').search({
- query:
- 'menu:(waffles OR pancakes) AND description:"breakfast all day"',
- queryEnhancement: 'disabled',
+ // 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');
});
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'sunnySideUp');
- });
-
- it('conjunction of rquery and expression', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .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('topicality score and snippet', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .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', () => {
- it('topicality score and snippet', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .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 topicality', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: field('menu').matches('tacos'),
- sort: score().descending(),
- queryEnhancement: 'disabled',
- });
-
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'eastsideTacos', 'solTacos');
- });
+ // 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('by distance', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: field('menu').matches('tacos'),
- sort: field('location')
- .geoDistance(new GeoPoint(39.6985, -105.024))
- .ascending(),
- queryEnhancement: 'disabled',
- });
+ 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');
+ });
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'solTacos', 'eastsideTacos');
+ // 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');
+ // });
});
- it('by multiple orderings', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .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('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');
- describe('limit', () => {
- it('limits the number of documents returned', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: constant(true),
- sort: field('location')
- .geoDistance(new GeoPoint(39.6985, -105.024))
- .ascending(),
- limit: 5,
- 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('searchScore')).to.be.greaterThan(0);
+ });
- const snapshot = await ppl.execute();
- expectResults(
- snapshot,
- 'solTacos',
- 'lotusBlossomThai',
- 'goldenWaffle',
- );
+ // 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,
+ // );
+ // });
});
- it('limits the number of documents scored', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .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('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('offset', () => {
- it('skips N documents', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: constant(true),
- limit: 2,
- offset: 2,
- queryEnhancement: 'disabled',
- });
-
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'eastsideChicken', 'eastsideTacos');
- });
- });
- });
-
- describe('REQUIRE query expansion', () => {
- it('search full document', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: documentMatches('waffles'),
- queryEnhancement: 'required',
+ 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');
});
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
- });
-
- it('search a specific field', async () => {
- const ppl = firestore
- .pipeline()
- .collection('restaurants')
- .search({
- query: field('menu').matches('waffles'),
- queryEnhancement: 'required',
- });
-
- const snapshot = await ppl.execute();
- expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
- });
- });
- });
-
- describe('snippet', () => {
- it('snippet options', async () => {
- const ppl1 = firestore
- .pipeline()
- .collection('restaurants')
- .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('restaurants')
- .search({
- query: field('menu').matches('waffles'),
- addFields: [
- field('menu')
- .snippet({
- rquery: 'waffles',
- maxSnippetWidth: 1000,
- })
- .as('snippet'),
- ],
- queryEnhancement: 'disabled',
+ // 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',
+ // );
+ // });
});
- 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('restaurants')
- .search({
- query: documentMatches('waffle'),
- addFields: [
- field('menu')
- .snippet({
- rquery: 'waffles',
- maxSnippetWidth: 2000,
- })
- .as('snippet'),
- ],
- queryEnhancement: 'disabled',
+ 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',
+ // );
+ // });
});
- 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('restaurants')
- .search({
- query: documentMatches('waffle'),
- addFields: [
- concat(field('menu'), field('description'))
- .snippet({
- rquery: 'waffles',
- maxSnippetWidth: 2000,
- })
- .as('snippet'),
- ],
- queryEnhancement: 'disabled',
+ 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');
+ // });
});
+ });
- 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,
- );
+ // 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,
+ // );
+ // });
+ // });
});
});
});
diff --git a/handwritten/firestore/dev/test/pipelines/stage.ts b/handwritten/firestore/dev/test/pipelines/stage.ts
index d3b4535574c..7c2f7ac225a 100644
--- a/handwritten/firestore/dev/test/pipelines/stage.ts
+++ b/handwritten/firestore/dev/test/pipelines/stage.ts
@@ -256,14 +256,14 @@ describe('stage option serialization', () => {
.database()
.search({
query: 'foo',
- limit: 1,
- retrievalDepth: 2,
- offset: 3,
- queryEnhancement: 'required',
- languageCode: 'en-US',
+ // limit: 1,
+ // retrievalDepth: 2,
+ // offset: 3,
+ // queryEnhancement: 'required',
+ // languageCode: 'en-US',
sort: [field('foo').ascending()],
addFields: [constant(true).as('bar')],
- select: [field('id')],
+ // select: [field('id')],
rawOptions,
}),
stageIndex: 1,
@@ -280,15 +280,15 @@ describe('stage option serialization', () => {
foo: {
stringValue: 'bar1',
},
- language_code: {
- stringValue: 'en-US',
- },
- limit: {
- integerValue: '1',
- },
- offset: {
- integerValue: '3',
- },
+ // language_code: {
+ // stringValue: 'en-US',
+ // },
+ // limit: {
+ // integerValue: '1',
+ // },
+ // offset: {
+ // integerValue: '3',
+ // },
query: {
functionValue: {
args: [
@@ -299,21 +299,21 @@ describe('stage option serialization', () => {
name: 'document_matches',
},
},
- query_enhancement: {
- stringValue: 'required',
- },
- retrieval_depth: {
- integerValue: '2',
- },
- select: {
- mapValue: {
- fields: {
- id: {
- fieldReferenceValue: 'id',
- },
- },
- },
- },
+ // query_enhancement: {
+ // stringValue: 'required',
+ // },
+ // retrieval_depth: {
+ // integerValue: '2',
+ // },
+ // select: {
+ // mapValue: {
+ // fields: {
+ // id: {
+ // fieldReferenceValue: 'id',
+ // },
+ // },
+ // },
+ // },
sort: {
arrayValue: {
values: [
diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts
index 08795f7f5af..9cc4fd6f20a 100644
--- a/handwritten/firestore/types/firestore.d.ts
+++ b/handwritten/firestore/types/firestore.d.ts
@@ -5603,64 +5603,66 @@ declare namespace FirebaseFirestore {
*/
isType(type: Type): 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(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;
-
- /**
- * 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(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
/**
@@ -5891,14 +5893,15 @@ declare namespace FirebaseFirestore {
*/
get fieldName(): string;
- /**
- * 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;
+ // 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
@@ -11454,18 +11457,20 @@ declare namespace FirebaseFirestore {
expression: Expression,
type: Type,
): BooleanExpression;
- /**
- * 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;
+
+ // 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.
@@ -11503,41 +11508,42 @@ declare namespace FirebaseFirestore {
*/
export function score(): Expression;
- /**
- * 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;
+ // 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
@@ -11562,97 +11568,98 @@ declare namespace FirebaseFirestore {
location: GeoPoint | Expression,
): Expression;
- /**
- * 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(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
/**
@@ -13123,18 +13130,19 @@ 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.
- */
- export type QueryEnhancement = 'disabled' | 'required' | 'preferred';
+ ///**
+ // * 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)}.
@@ -13151,10 +13159,7 @@ declare namespace FirebaseFirestore {
* @example
* ```typescript
* db.pipeline().collection('restaurants').search({
- * query: or(
- * documentMatches("breakfast"),
- * matches('menu', 'waffle AND coffee')
- * )
+ * query: documentMatches("breakfast")
* })
* ```
*
@@ -13163,24 +13168,26 @@ declare namespace FirebaseFirestore {
* @example
* ```
* db.pipeline().collection('restaurants').search({
- * query: 'menu:(waffle and coffee) OR breakfast'
+ * query: 'breakfast'
* })
* ```
*/
query: BooleanExpression | string;
- /**
- * The BCP-47 language code of text in the search query, such as, “en-US” or “sr-Latn”
- */
- languageCode?: 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.
- */
- retrievalDepth?: number;
+ ///**
+ // * 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.
@@ -13188,60 +13195,65 @@ declare namespace FirebaseFirestore {
*/
sort?: Ordering | Ordering[];
- /**
- * The number of documents to skip.
- */
- offset?: number;
+ // /**
+ // * 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.
- */
- limit?: 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}.
- */
- select?: Array;
+ // /**
+ // * 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.
- */
- queryEnhancement?: QueryEnhancement;
+ // /**
+ // * Define the query expansion behavior used by full-text search expressions
+ // * in this search stage.
+ // */
+ // TODO(search) enable with backend support
+ // queryEnhancement?: QueryEnhancement;
};
- /**
- * 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;
- };
+ // 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
From e6f646b90901daf2ae445cb0d671b5852a6d7b3b Mon Sep 17 00:00:00 2001
From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com>
Date: Fri, 27 Mar 2026 16:10:22 -0400
Subject: [PATCH 8/8] format
---
.../firestore/dev/system-test/pipeline.ts | 965 +++++++++---------
1 file changed, 479 insertions(+), 486 deletions(-)
diff --git a/handwritten/firestore/dev/system-test/pipeline.ts b/handwritten/firestore/dev/system-test/pipeline.ts
index 708f3926fd3..91fd7b46ed5 100644
--- a/handwritten/firestore/dev/system-test/pipeline.ts
+++ b/handwritten/firestore/dev/system-test/pipeline.ts
@@ -5597,539 +5597,532 @@ describe.skipClassic('Pipeline search', () => {
return testCollectionWithDocs(collection, restaurantDocs);
}
- describe('text search', () => {
- 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');
- });
+ const COLLECTION_NAME = 'TextSearchIntegrationTests';
- // 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');
- });
+ // 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;
+ });
- // 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');
- });
+ 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');
+ // });
- // 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('search full document', async () => {
+ const ppl = firestore
+ .pipeline()
+ .collection(COLLECTION_NAME)
+ .search({
+ query: documentMatches('waffles'),
+ // queryEnhancement: 'disabled',
+ });
- 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');
- });
+ 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');
+ // });
- // 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');
- // });
+ 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');
});
- 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 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');
+ // });
- // 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,
- // );
- // });
+ 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');
});
- 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',
- // ]);
- // });
+ // 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');
});
- 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');
+ it('rquery as query param', async () => {
+ const ppl = firestore.pipeline().collection(COLLECTION_NAME).search({
+ query: 'chicken wings',
+ // queryEnhancement: 'disabled',
});
- // 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',
- // );
- // });
+ const snapshot = await ppl.execute();
+ expectResults(snapshot, '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',
- // );
- // });
+ // 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);
});
- 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) 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',
+ // );
+ // });
});
- // TODO(search) add these tests when query enhancement is supported
- describe.skip('REQUIRE query expansion', () => {
- // it('search full document', async () => {
+ describe('limit', () => {
+ // it('limits the number of documents returned', async () => {
// const ppl = firestore
// .pipeline()
// .collection(COLLECTION_NAME)
// .search({
- // query: documentMatches('waffles'),
- // queryEnhancement: 'required',
+ // 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, 'goldenWaffle', 'sunnySideUp');
+ // 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',
+ // });
//
- // it('search a specific field', async () => {
+ // 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: field('menu').matches('waffles'),
- // queryEnhancement: 'required',
+ // query: constant(true),
+ // limit: 2,
+ // offset: 2,
+ // queryEnhancement: 'disabled',
// });
//
// const snapshot = await ppl.execute();
- // expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ // expectResults(snapshot, 'eastsideChicken', 'eastsideTacos');
// });
});
});
- // 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);
+ // 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 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 snapshot = await ppl.execute();
+ // expectResults(snapshot, 'goldenWaffle', 'sunnySideUp');
+ // });
//
- // 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);
+ // it('search a specific field', async () => {
+ // const ppl = firestore
+ // .pipeline()
+ // .collection(COLLECTION_NAME)
+ // .search({
+ // query: field('menu').matches('waffles'),
+ // queryEnhancement: 'required',
+ // });
//
- // // 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,
- // );
- // });
+ // 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)