feat(firestore): Added search stage support for languageCode, offset, limit, and retrievalDepth (#9884)

* feat(firestore): Added search stage support for languageCode, offset, limit, and retrievalDepth

* Create four-bottles-marry.md

* docgen

* PR feedback

* Update packages/firestore/src/lite-api/stage_options.ts

Co-authored-by: Daniel La Rocque <[email protected]>

* Improve ref doc

* More search refDoc improvements

* Consistent capitalization of search within ref docs

* remove deleted rut file

* docs devsite

---------

Co-authored-by: Daniel La Rocque <[email protected]>
This commit is contained in:
Mark Duckworth
2026-04-29 13:55:37 -06:00
committed by GitHub
co-authored by Daniel La Rocque
parent 0fff8d4cfd
commit e327557a2a
10 changed files with 165 additions and 113 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@firebase/firestore": patch
---
Added search stage support for languageCode, offset, limit, and retrievalDepth
@@ -1447,7 +1447,11 @@ export function score(): Expression;
// @beta
export type SearchStageOptions = StageOptions & {
query: BooleanExpression | string;
languageCode?: string;
retrievalDepth?: number;
sort?: Ordering | Ordering[];
offset?: number;
limit?: number;
addFields?: Selectable[];
};
@@ -1475,7 +1475,11 @@ export function score(): Expression;
// @beta
export type SearchStageOptions = StageOptions & {
query: BooleanExpression | string;
languageCode?: string;
retrievalDepth?: number;
sort?: Ordering | Ordering[];
offset?: number;
limit?: number;
addFields?: Selectable[];
};
@@ -88,7 +88,7 @@ selectable: true;
Evaluates to the distance in meters between the location specified by this field and the query location.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
+9 -5
View File
@@ -478,7 +478,7 @@ https://github.com/firebase/firebase-js-sdk
| [RemoveFieldsStageOptions](./firestore_lite_pipelines.md#removefieldsstageoptions) | Options defining how a RemoveFieldsStage is evaluated. See [Pipeline.removeFields()](./firestore_pipelines.pipeline.md#pipelineremovefields)<!-- -->. |
| [ReplaceWithStageOptions](./firestore_lite_pipelines.md#replacewithstageoptions) | Options defining how a ReplaceWithStage is evaluated. See [Pipeline.replaceWith()](./firestore_pipelines.pipeline.md#pipelinereplacewith)<!-- -->. |
| [SampleStageOptions](./firestore_lite_pipelines.md#samplestageoptions) | Defines the options for evaluating a sample stage within a pipeline. This type combines common [StageOptions](./firestore_pipelines.md#stageoptions) with a specific configuration where only one of the defined sampling methods can be applied.<!-- -->See [Pipeline.sample()](./firestore_pipelines.pipeline.md#pipelinesample) to create a sample stage.. |
| [SearchStageOptions](./firestore_lite_pipelines.md#searchstageoptions) | <b><i>(Public Preview)</i></b> Options defining how a SearchStage is evaluated. See . |
| [SearchStageOptions](./firestore_lite_pipelines.md#searchstageoptions) | <b><i>(Public Preview)</i></b> Options defining how a search stage is evaluated. See [Pipeline.search()](./firestore_pipelines.pipeline.md#pipelinesearch)<!-- -->. |
| [SelectStageOptions](./firestore_lite_pipelines.md#selectstageoptions) | Options defining how a SelectStage is evaluated. See [Pipeline.select()](./firestore_pipelines.pipeline.md#pipelineselect)<!-- -->. |
| [SetOptions](./firestore_lite_pipelines.md#setoptions) | An options object that configures the behavior of [setDoc()](./firestore_lite.md#setdoc_ee215ad)<!-- -->, and calls. These calls can be configured to perform granular merges instead of overwriting the target documents in their entirety by providing a <code>SetOptions</code> with <code>merge: true</code>. |
| [SortStageOptions](./firestore_lite_pipelines.md#sortstageoptions) | Options defining how a SortStage is evaluated. See [Pipeline.sort()](./firestore_pipelines.pipeline.md#pipelinesort)<!-- -->. |
@@ -600,7 +600,7 @@ rand();
Evaluates to the search score that reflects the topicality of the document to all of the text predicates (for example: `documentMatches`<!-- -->) in the search query. If `SearchOptions.query` is not set or does not contain any text predicates, then this topicality score will always be `0`<!-- -->.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
@@ -6196,7 +6196,7 @@ A new [Expression](./firestore_pipelines.expression.md#expression_class) represe
Evaluates to the distance in meters between the location in the specified field and the query location.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
@@ -10351,7 +10351,7 @@ const results: PipelineResults = snapshot.results;
Perform a full-text search on all indexed search fields in the document.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
@@ -12243,14 +12243,18 @@ export declare type SampleStageOptions = StageOptions & OneOf<{
> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.
>
Options defining how a SearchStage is evaluated. See .
Options defining how a search stage is evaluated. See [Pipeline.search()](./firestore_pipelines.pipeline.md#pipelinesearch)<!-- -->.
<b>Signature:</b>
```typescript
export declare type SearchStageOptions = StageOptions & {
query: BooleanExpression | string;
languageCode?: string;
retrievalDepth?: number;
sort?: Ordering | Ordering[];
offset?: number;
limit?: number;
addFields?: Selectable[];
};
```
+1 -1
View File
@@ -88,7 +88,7 @@ selectable: true;
Evaluates to the distance in meters between the location specified by this field and the query location.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
+9 -5
View File
@@ -483,7 +483,7 @@ https://github.com/firebase/firebase-js-sdk
| [RemoveFieldsStageOptions](./firestore_pipelines.md#removefieldsstageoptions) | Options defining how a RemoveFieldsStage is evaluated. See [Pipeline.removeFields()](./firestore_pipelines.pipeline.md#pipelineremovefields)<!-- -->. |
| [ReplaceWithStageOptions](./firestore_pipelines.md#replacewithstageoptions) | Options defining how a ReplaceWithStage is evaluated. See [Pipeline.replaceWith()](./firestore_pipelines.pipeline.md#pipelinereplacewith)<!-- -->. |
| [SampleStageOptions](./firestore_pipelines.md#samplestageoptions) | Defines the options for evaluating a sample stage within a pipeline. This type combines common [StageOptions](./firestore_pipelines.md#stageoptions) with a specific configuration where only one of the defined sampling methods can be applied.<!-- -->See [Pipeline.sample()](./firestore_pipelines.pipeline.md#pipelinesample) to create a sample stage.. |
| [SearchStageOptions](./firestore_pipelines.md#searchstageoptions) | <b><i>(Public Preview)</i></b> Options defining how a SearchStage is evaluated. See . |
| [SearchStageOptions](./firestore_pipelines.md#searchstageoptions) | <b><i>(Public Preview)</i></b> Options defining how a search stage is evaluated. See [Pipeline.search()](./firestore_pipelines.pipeline.md#pipelinesearch)<!-- -->. |
| [SelectStageOptions](./firestore_pipelines.md#selectstageoptions) | Options defining how a SelectStage is evaluated. See [Pipeline.select()](./firestore_pipelines.pipeline.md#pipelineselect)<!-- -->. |
| [SetOptions](./firestore_pipelines.md#setoptions) | An options object that configures the behavior of [setDoc()](./firestore_lite.md#setdoc_ee215ad)<!-- -->, and calls. These calls can be configured to perform granular merges instead of overwriting the target documents in their entirety by providing a <code>SetOptions</code> with <code>merge: true</code>. |
| [SortStageOptions](./firestore_pipelines.md#sortstageoptions) | Options defining how a SortStage is evaluated. See [Pipeline.sort()](./firestore_pipelines.pipeline.md#pipelinesort)<!-- -->. |
@@ -605,7 +605,7 @@ rand();
Evaluates to the search score that reflects the topicality of the document to all of the text predicates (for example: `documentMatches`<!-- -->) in the search query. If `SearchOptions.query` is not set or does not contain any text predicates, then this topicality score will always be `0`<!-- -->.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
@@ -6201,7 +6201,7 @@ A new [Expression](./firestore_pipelines.expression.md#expression_class) represe
Evaluates to the distance in meters between the location in the specified field and the query location.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
@@ -10417,7 +10417,7 @@ const results: PipelineResult[] = snapshot.results;
Perform a full-text search on all indexed search fields in the document.
This Expression can only be used within a `Search` stage.
This Expression can only be used within a `search` stage.
<b>Signature:</b>
@@ -12309,14 +12309,18 @@ export declare type SampleStageOptions = StageOptions & OneOf<{
> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.
>
Options defining how a SearchStage is evaluated. See .
Options defining how a search stage is evaluated. See [Pipeline.search()](./firestore_pipelines.pipeline.md#pipelinesearch)<!-- -->.
<b>Signature:</b>
```typescript
export declare type SearchStageOptions = StageOptions & {
query: BooleanExpression | string;
languageCode?: string;
retrievalDepth?: number;
sort?: Ordering | Ordering[];
offset?: number;
limit?: number;
addFields?: Selectable[];
};
```
+10 -10
View File
@@ -3456,7 +3456,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData {
// * Evaluates to an HTML-formatted text snippet that renders terms matching
// * the search query in `<b>bold</b>`.
// *
// * @remarks This Expression can only be used within a `Search` stage.
// * @remarks This Expression can only be used within a `search` stage.
// *
// * @param rquery Define the search query using the search domain-specific language (DSL).
// */
@@ -3466,7 +3466,7 @@ export abstract class Expression implements ProtoValueSerializable, UserData {
// * Evaluates to an HTML-formatted text snippet that renders terms matching
// * the search query in `<b>bold</b>`.
// *
// * @remarks This Expression can only be used within a `Search` stage.
// * @remarks This Expression can only be used within a `search` stage.
// *
// * @param options Define how snippeting behaves.
// */
@@ -3795,7 +3795,7 @@ export class Field extends Expression implements Selectable {
// /**
// * Perform a full-text search on this field.
// *
// * @remarks This Expression can only be used within a `Search` stage.
// * @remarks This Expression can only be used within a `search` stage.
// *
// * @param rquery Define the search query using the search domain-specific language (DSL).
// */
@@ -3812,7 +3812,7 @@ export class Field extends Expression implements Selectable {
* 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.
* @remarks This Expression can only be used within a `search` stage.
*
* @param location - Compute distance to this GeoPoint.
*/
@@ -11649,7 +11649,7 @@ export function timestampExtract(
// /**
// * Perform a full-text search on the specified field.
// *
// * @remarks This Expression can only be used within a `Search` stage.
// * @remarks This Expression can only be used within a `search` stage.
// *
// * @example
// * ```typescript
@@ -11672,7 +11672,7 @@ export function timestampExtract(
* @beta
* Perform a full-text search on all indexed search fields in the document.
*
* @remarks This Expression can only be used within a `Search` stage.
* @remarks This Expression can only be used within a `search` stage.
*
* @example
* ```typescript
@@ -11709,7 +11709,7 @@ export function documentMatches(
* })
* ```
*
* @remarks This Expression can only be used within a `Search` stage.
* @remarks This Expression can only be used within a `search` stage.
*/
export function score(): Expression {
return new FunctionExpression('score', [], 'score');
@@ -11747,7 +11747,7 @@ export function score(): Expression {
// * Evaluates to an HTML-formatted text snippet that highlights terms matching
// * the search query in `<b>bold</b>`.
// *
// * @remarks This Expression can only be used within a `Search` stage.
// * @remarks This Expression can only be used within a `search` stage.
// *
// * @example
// * ```typescript
@@ -11769,7 +11769,7 @@ export function score(): Expression {
// * Evaluates to an HTML-formatted text snippet that highlights terms matching
// * the search query in `<b>bold</b>`.
// *
// * @remarks This Expression can only be used within a `Search` stage.
// * @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 domain-specific language (DSL).
@@ -11792,7 +11792,7 @@ export function score(): 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.
* @remarks This Expression can only be used within a `search` stage.
*
* @example
* ```typescript
@@ -270,7 +270,7 @@ export type QueryEnhancement = 'disabled' | 'required' | 'preferred';
/**
* @beta
* Options defining how a SearchStage is evaluated. See {@link @firebase/firestore/pipelines#Pipeline.(search)}.
* Options defining how a search stage is evaluated. See {@link @firebase/firestore/pipelines#Pipeline.(search:1)}.
*/
export type SearchStageOptions = StageOptions & {
/**
@@ -279,7 +279,7 @@ export type SearchStageOptions = StageOptions & {
*
* 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.
* are supported in the search query.
*
* @example
* ```typescript
@@ -291,13 +291,14 @@ export type SearchStageOptions = StageOptions & {
* })
* ```
*
* The query can also be expressed as a string in the Search domain-specific language (DSL):
* The query can also be expressed as a string in the search domain-specific language (DSL), which will be used for searching the document.
*
* @example
* ```typescript
* db.pipeline().collection('restaurants').search({
* query: 'menu:(waffle and coffee) OR breakfast'
* })
* db.pipeline().collection('restaurants').search({ query: 'breakfast -diner' });
*
* // The above query is equivalent to:
* db.pipeline().collection('restaurants').search({ query: documentMatches('breakfast -diner') });
* ```
*
* The query can also represent a geoDistance query:
@@ -311,20 +312,19 @@ export type SearchStageOptions = StageOptions & {
*/
query: BooleanExpression | string;
// TODO(search) enable with backend support
// /**
// * 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” or “sr”.
*/
languageCode?: string;
// TODO(search) add indexPartition after languageCode
// TODO(search) enable with backend support
// /**
// * 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 from the search index. Documents will be retrieved in the
* pre-sort order specified by the search index. The `retrievalDepth` is a limit applied before documents
* are scored and sorted, which can reduce costs of expensive scoring and sorting operations.
*/
retrievalDepth?: number;
/**
* Orderings specify how the returned documents are sorted.
@@ -332,17 +332,16 @@ export type SearchStageOptions = StageOptions & {
*/
sort?: Ordering | Ordering[];
// TODO(search) enable with backend support
// /**
// * The number of documents to skip from the beginning of the search result set.
// */
// offset?: number;
/**
* The number of documents to skip from the beginning of the search result set.
*/
offset?: number;
// TODO(search) enable with backend support
// /**
// * The maximum number of documents to return from the Search stage.
// */
// limit?: number;
/**
* The maximum number of documents to return from the `search` stage. The `limit` is applied after documents
* are scored and sorted.
*/
limit?: number;
// TODO(search) enable with backend support
// /**
@@ -6962,7 +6962,7 @@ apiDescribe.skipClassic('Pipelines', persistence => {
1,
async firestoreInstance => {
firestore = firestoreInstance[0];
restaurantsCollection = collection(firestore, 'SearchIntegrationTests');
restaurantsCollection = collection(firestore, COLLECTION_NAME);
await setupRestaurantDocs(restaurantsCollection);
setupDeferred.resolve();
@@ -7299,70 +7299,102 @@ apiDescribe.skipClassic('Pipelines', persistence => {
// });
});
// TODO(search) enable with backend support
// 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 execute(ppl);
// 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 execute(ppl);
// expectResults(
// snapshot,
// 'eastsideChicken',
// 'eastsideTacos',
// 'solTacos',
// 'mileHighCatch'
// );
// });
// });
describe('languageCode', () => {
const rquery = 'al pastor';
it('en', async () => {
const ppl = firestore
.pipeline()
.collection(COLLECTION_NAME)
.search({
query: documentMatches(rquery),
sort: score().descending(),
languageCode: 'en'
// queryEnhancement: 'disabled'
});
// TODO(search) enable with backend support
// 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 execute(ppl);
// expectResults(snapshot, 'eastsideChicken', 'eastsideTacos');
// });
// });
const snapshot = await execute(ppl);
expectResults(snapshot, 'solTacos');
});
it('unknown', async () => {
const ppl = firestore
.pipeline()
.collection(COLLECTION_NAME)
.search({
query: documentMatches(rquery),
sort: score().descending(),
languageCode: 'unknown'
// queryEnhancement: 'disabled'
});
await expect(execute(ppl)).to.be.rejectedWith(/3 INVALID_ARGUMENT.*/);
});
});
describe('limit', () => {
it('limits the number of documents returned', async () => {
const ppl = firestore
.pipeline()
.collection(COLLECTION_NAME)
.search({
query: field('location')
.geoDistance(new GeoPoint(39.6985, -105.024))
.lessThanOrEqual(100000000),
sort: field('location')
.geoDistance(new GeoPoint(39.6985, -105.024))
.ascending(),
limit: 3
// queryEnhancement: 'disabled'
});
const snapshot = await execute(ppl);
expectResults(
snapshot,
'solTacos',
'lotusBlossomThai',
'mileHighCatch'
);
});
it('limits the number of documents scored via retrievalDepth', async () => {
const commonSearchParams = {
query: documentMatches('taco'),
addFields: [score().as('score')],
sort: score().descending()
// queryEnhancement: 'disabled' as QueryEnhancement
};
let ppl = firestore
.pipeline()
.collection(COLLECTION_NAME)
.search({ ...commonSearchParams, retrievalDepth: 2 });
let snapshot = await execute(ppl);
expectResults(snapshot, 'solTacos', 'eastsideTacos');
ppl = firestore
.pipeline()
.collection(COLLECTION_NAME)
.search({ ...commonSearchParams, retrievalDepth: 1 });
snapshot = await execute(ppl);
expectResults(snapshot, 'eastsideTacos');
});
});
describe('offset', () => {
it('skips N documents', async () => {
const ppl = firestore.pipeline().collection(COLLECTION_NAME).search({
query: 'chicken',
limit: 2,
offset: 2
// queryEnhancement: 'disabled'
});
const snapshot = await execute(ppl);
expectResults(snapshot, 'goldenWaffle');
});
});
});
// TODO(search) enable with backend support