Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,19 @@ export { AggregationQuery, Reducers } from './query/aggregation.js';
export type { AggregateCommand, SortSpec, LoadField, Reducer } from './query/aggregation.js';
export { Tag, Num, Text, Geo, GeoRadius, Timestamp, FilterExpression } from './query/filter.js';
export type { Inclusive, GeoUnit } from './query/filter.js';
export { BaseQuery, BaseVectorQuery } from './query/base.js';
export type {
BaseQuery,
BaseQueryConfig,
BaseVectorQueryConfig,
ReturnFieldsOptions,
SearchResult,
SearchDocument,
QueryOptions,
FilterInput,
HybridSearchResult,
SortByOptions,
SortDirection,
SortField,
} from './query/base.js';

// Error exports
Expand Down
36 changes: 24 additions & 12 deletions src/indexes/search-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,9 @@ export class SearchIndex {
* Execute a search query against the index
*
* @param query - Query object (VectorQuery, FilterQuery, etc.)
* @param options - Optional query execution options
* @param options - Optional query execution options. If both query-level
* `query.sortBy(...)` and `options.sortBy` are supplied, `options.sortBy`
* takes precedence to preserve the existing `search()` API.
* @returns Search results with documents and scores
*
* @example
Expand Down Expand Up @@ -600,21 +602,31 @@ export class SearchIndex {
}

// Add pagination
if (query.limit !== undefined || query.offset !== undefined) {
const offset = query.offset ?? 0;
const limit = query.limit ?? 10;
const queryLimit = query.getLimit();
const queryOffset = query.getOffset();
if (queryLimit !== undefined || queryOffset !== undefined) {
const offset = queryOffset ?? 0;
const limit = queryLimit ?? 10;
searchOptions.LIMIT = { from: offset, size: limit };
}

// Add sorting if specified
// Add sorting if specified on the query. FT.SEARCH accepts one
// SORTBY clause, so use the first collected sort field.
if (query.sortFields.length > 0) {
const [sortField] = query.sortFields;
searchOptions.SORTBY = {
BY: sortField.field,
DIRECTION: sortField.direction,
};
}

// Add sorting if specified in execution options. These options
// preserve the historical API and override query-level sorting.
if (options?.sortBy) {
searchOptions.SORTBY = options.sortBy;
if (options.sortOrder) {
searchOptions.SORTBY = {
BY: options.sortBy,
DIRECTION: options.sortOrder,
};
}
searchOptions.SORTBY = {
BY: options.sortBy,
...(options.sortOrder ? { DIRECTION: options.sortOrder } : {}),
};
}

// CountQuery (and any other consumer) can opt into NOCONTENT to
Expand Down
33 changes: 23 additions & 10 deletions src/query/aggregation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import type { FtAggregateOptions } from '@redis/search/dist/lib/commands/AGGREGATE.js';
import { QueryValidationError } from '../errors.js';
import { renderFilter, type FilterInput } from './base.js';
import { BaseQuery, renderFilter, type FilterInput, type SortByOptions } from './base.js';

/** Output of {@link AggregationQuery.toCommand}. */
export interface AggregateCommand {
Expand Down Expand Up @@ -224,8 +224,7 @@ function assertPositiveInteger(value: number | undefined, label: string): void {
* const { total, results } = await index.aggregate(q);
* ```
*/
export class AggregationQuery {
private readonly _query: string;
export class AggregationQuery extends BaseQuery {
private readonly steps: Step[] = [];
private _load?: LoadField[];
private _params?: Record<string, string | number>;
Expand All @@ -240,7 +239,7 @@ export class AggregationQuery {
* the rest of the query DSL uses.
*/
constructor(query?: FilterInput) {
this._query = renderFilter(query);
super({ filter: query });
}

/**
Expand Down Expand Up @@ -279,8 +278,16 @@ export class AggregationQuery {
}

/** SORTBY one or more fields. Bare strings sort ASC. */
sortBy(by: SortSpec | SortSpec[], max?: number): this {
const list = Array.isArray(by) ? by : [by];
sortBy(field: string, options?: SortByOptions): this;
sortBy(by: SortSpec | SortSpec[], max?: number): this;
sortBy(by: SortSpec | SortSpec[], maxOrOptions?: number | SortByOptions): this {
const list =
typeof maxOrOptions === 'object' && !Array.isArray(by) && typeof by === 'string'
? [{ field: by, direction: maxOrOptions.direction }]
: Array.isArray(by)
? by
: [by];
const max = typeof maxOrOptions === 'number' ? maxOrOptions : undefined;
if (list.length === 0) {
throw new QueryValidationError('sortBy requires at least one field');
}
Expand Down Expand Up @@ -378,7 +385,12 @@ export class AggregationQuery {

/** The rendered query string this aggregation will use. */
get query(): string {
return this._query;
return this.buildQuery();
}

/** Build the FT.AGGREGATE query string. */
buildQuery(): string {
return renderFilter(this.queryFilter);
}

/** Build the structured options for `client.ft.aggregate(indexName, query, options)`. */
Expand All @@ -395,12 +407,13 @@ export class AggregationQuery {
options.PARAMS = this._params;
}

if (this._load && this._load.length > 0) {
const loadFields = [...(this.returnFields ?? []), ...(this._load ?? [])];
if (loadFields.length > 0) {
// The Redis client types LOAD entries with template-literal types
// (`@${string}` / `$.${string}`). We've already enforced that
// contract via prefixFieldRef, but TS can't see through the
// string return — cast at the boundary.
options.LOAD = this._load.map((f) =>
options.LOAD = loadFields.map((f) =>
typeof f === 'string'
? prefixFieldRef(f)
: f.as !== undefined
Expand All @@ -415,7 +428,7 @@ export class AggregationQuery {
) as FtAggregateOptions['STEPS'];
}

return { query: this._query, options };
return { query: this.buildQuery(), options };
}

private renderStep(step: Step): unknown {
Expand Down
Loading
Loading