@valkey/valkey-glide
Version:
General Language Independent Driver for the Enterprise (GLIDE) for Valkey
800 lines (799 loc) • 29.7 kB
JavaScript
;
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GlideFt = void 0;
const __1 = require("..");
/** Module for Vector Search commands. */
class GlideFt {
/**
* Creates an index and initiates a backfill of that index.
*
* @param client - The client to execute the command.
* @param indexName - The index name for the index to be created.
* @param schema - The fields of the index schema, specifying the fields and their types.
* @param options - (Optional) Options for the `FT.CREATE` command. See {@link FtCreateOptions}.
* @returns If the index is successfully created, returns "OK".
*
* @example
* ```typescript
* // Example usage of FT.CREATE to create a 6-dimensional JSON index using the HNSW algorithm
* await GlideFt.create(client, "json_idx1", [{
* type: "VECTOR",
* name: "$.vec",
* alias: "VEC",
* attributes: {
* algorithm: "HNSW",
* type: "FLOAT32",
* dimension: 6,
* distanceMetric: "L2",
* numberOfEdges: 32,
* },
* }], {
* dataType: "JSON",
* prefixes: ["json:"]
* });
*
* // Create a text search index with all options:
* // Note: noStopWords/stopWords are mutually exclusive;
* // withOffsets/noOffsets are mutually exclusive.
* await GlideFt.create(client, "text_idx", [
* { type: "TEXT", name: "title", sortable: true, nostem: true, weight: 2.0,
* withsuffixtrie: true },
* { type: "NUMERIC", name: "price", sortable: true },
* { type: "TAG", name: "category", sortable: true },
* ], {
* dataType: "HASH",
* prefixes: ["product:"],
* score: 1.0,
* language: "english",
* skipInitialScan: true,
* minStemSize: 4,
* withOffsets: true,
* stopWords: ["the", "a", "is"],
* punctuation: ".,;!?",
* });
* ```
*/
static async create(client, indexName, schema, options) {
const args = ["FT.CREATE", indexName];
if (options) {
if ("dataType" in options) {
args.push("ON", options.dataType);
}
if ("prefixes" in options && options.prefixes) {
args.push("PREFIX", options.prefixes.length.toString(), ...options.prefixes);
}
if (options.score !== undefined) {
args.push("SCORE", options.score.toString());
}
if (options.language) {
args.push("LANGUAGE", options.language);
}
if (options.skipInitialScan) {
args.push("SKIPINITIALSCAN");
}
if (options.minStemSize !== undefined) {
args.push("MINSTEMSIZE", options.minStemSize.toString());
}
if (options.withOffsets && options.noOffsets) {
throw new Error("withOffsets and noOffsets are mutually exclusive.");
}
if (options.noStopWords && options.stopWords) {
throw new Error("noStopWords and stopWords are mutually exclusive.");
}
if (options.withOffsets) {
args.push("WITHOFFSETS");
}
else if (options.noOffsets) {
args.push("NOOFFSETS");
}
if (options.noStopWords) {
args.push("NOSTOPWORDS");
}
else if (options.stopWords) {
args.push("STOPWORDS", options.stopWords.length.toString(), ...options.stopWords);
}
if (options.punctuation) {
args.push("PUNCTUATION", options.punctuation);
}
}
args.push("SCHEMA");
schema.forEach((f) => {
args.push(f.name);
if (f.alias) {
args.push("AS", f.alias);
}
args.push(f.type);
switch (f.type) {
case "TEXT": {
if (f.nostem) {
args.push("NOSTEM");
}
if (f.weight !== undefined) {
args.push("WEIGHT", f.weight.toString());
}
if (f.withsuffixtrie) {
args.push("WITHSUFFIXTRIE");
}
else if (f.nosuffixtrie) {
args.push("NOSUFFIXTRIE");
}
if (f.sortable) {
args.push("SORTABLE");
}
break;
}
case "TAG": {
if (f.separator) {
args.push("SEPARATOR", f.separator);
}
if (f.caseSensitive) {
args.push("CASESENSITIVE");
}
if (f.sortable) {
args.push("SORTABLE");
}
break;
}
case "NUMERIC": {
if (f.sortable) {
args.push("SORTABLE");
}
break;
}
case "VECTOR": {
if (f.attributes) {
args.push(f.attributes.algorithm);
const attributes = [];
// all VectorFieldAttributes attributes
if (f.attributes.dimensions) {
attributes.push("DIM", f.attributes.dimensions.toString());
}
if (f.attributes.distanceMetric) {
attributes.push("DISTANCE_METRIC", f.attributes.distanceMetric.toString());
}
if (f.attributes.type) {
attributes.push("TYPE", f.attributes.type.toString());
}
else {
attributes.push("TYPE", "FLOAT32");
}
if (f.attributes.initialCap) {
attributes.push("INITIAL_CAP", f.attributes.initialCap.toString());
}
// VectorFieldAttributesHnsw attributes
if ("numberOfEdges" in f.attributes &&
f.attributes.numberOfEdges) {
attributes.push("M", f.attributes.numberOfEdges.toString());
}
if ("vectorsExaminedOnConstruction" in f.attributes &&
f.attributes.vectorsExaminedOnConstruction) {
attributes.push("EF_CONSTRUCTION", f.attributes.vectorsExaminedOnConstruction.toString());
}
if ("vectorsExaminedOnRuntime" in f.attributes &&
f.attributes.vectorsExaminedOnRuntime) {
attributes.push("EF_RUNTIME", f.attributes.vectorsExaminedOnRuntime.toString());
}
args.push(attributes.length.toString(), ...attributes);
}
break;
}
default:
// no-op
}
});
return _handleCustomCommand(client, args, {
decoder: __1.Decoder.String,
});
}
/**
* Deletes an index and associated content. Indexed document keys are unaffected.
*
* @param client - The client to execute the command.
* @param indexName - The index name.
* @returns "OK"
*
* @example
* ```typescript
* // Example usage of FT.DROPINDEX to drop an index
* await GlideFt.dropindex(client, "json_idx1"); // "OK"
* ```
*/
static async dropindex(client, indexName) {
const args = ["FT.DROPINDEX", indexName];
return _handleCustomCommand(client, args, {
decoder: __1.Decoder.String,
});
}
/**
* Lists all indexes.
*
* @param client - The client to execute the command.
* @param options - (Optional) See {@link DecoderOption}.
* @returns An array of index names.
*
* @example
* ```typescript
* console.log(await GlideFt.list(client)); // Output: ["index1", "index2"]
* ```
*/
static async list(client, options) {
return _handleCustomCommand(client, ["FT._LIST"], options);
}
/**
* Runs a search query on an index, and perform aggregate transformations on the results.
*
* @param client - The client to execute the command.
* @param indexName - The index name.
* @param query - The text query to search.
* @param options - Additional parameters for the command - see {@link FtAggregateOptions} and {@link DecoderOption}.
* @returns Results of the last stage of the pipeline.
*
* @example
* ```typescript
* const options: FtAggregateOptions = {
* loadFields: ["__key"],
* clauses: [
* {
* type: "GROUPBY",
* properties: ["@condition"],
* reducers: [
* {
* function: "TOLIST",
* args: ["__key"],
* name: "bicycles",
* },
* ],
* },
* ],
* };
* const result = await GlideFt.aggregate(client, "myIndex", "*", options);
* console.log(result); // Output:
* // [
* // [
* // {
* // key: "condition",
* // value: "refurbished"
* // },
* // {
* // key: "bicycles",
* // value: [ "bicycle:9" ]
* // }
* // ],
* // [
* // {
* // key: "condition",
* // value: "used"
* // },
* // {
* // key: "bicycles",
* // value: [ "bicycle:1", "bicycle:2", "bicycle:3" ]
* // }
* // ],
* // [
* // {
* // key: "condition",
* // value: "new"
* // },
* // {
* // key: "bicycles",
* // value: [ "bicycle:0", "bicycle:5" ]
* // }
* // ]
* // ]
*
* // Aggregate with all query flags:
* const result12 = await GlideFt.aggregate(client, "myIndex", "@score:[20 +inf]",
* { loadAll: true, verbatim: true, inorder: true, slop: 1, dialect: 2 });
* ```
*/
static async aggregate(client, indexName, query, options) {
const args = [
"FT.AGGREGATE",
indexName,
query,
..._addFtAggregateOptions(options),
];
return _handleCustomCommand(client, args, options);
}
/**
* Returns information about a given index.
*
* @param client - The client to execute the command.
* @param indexName - The index name.
* @param options - (Optional) See {@link DecoderOption}.
* @returns Nested maps with info about the index. See example for more details.
*
* @example
* ```typescript
* const info = await GlideFt.info(client, "myIndex");
* console.log(info); // Output:
* // {
* // index_name: 'myIndex',
* // index_status: 'AVAILABLE',
* // key_type: 'JSON',
* // creation_timestamp: 1728348101728771,
* // key_prefixes: [ 'json:' ],
* // num_indexed_vectors: 0,
* // space_usage: 653471,
* // num_docs: 0,
* // vector_space_usage: 653471,
* // index_degradation_percentage: 0,
* // fulltext_space_usage: 0,
* // current_lag: 0,
* // fields: [
* // {
* // identifier: '$.vec',
* // type: 'VECTOR',
* // field_name: 'VEC',
* // option: '',
* // vector_params: {
* // data_type: 'FLOAT32',
* // initial_capacity: 1000,
* // current_capacity: 1000,
* // distance_metric: 'L2',
* // dimension: 6,
* // block_size: 1024,
* // algorithm: 'FLAT'
* // }
* // },
* // {
* // identifier: 'name',
* // type: 'TEXT',
* // field_name: 'name',
* // option: ''
* // },
* // ]
* // }
*
* // Get info with scope options:
* const localInfo = await GlideFt.info(client, "myIndex", { scope: "LOCAL" });
*
* // Get cluster-wide info (requires coordinator):
* const clusterInfo = await GlideFt.info(client, "myIndex", {
* scope: "PRIMARY",
* shardScope: "ALLSHARDS",
* consistency: "CONSISTENT",
* });
* ```
*/
static async info(client, indexName, options) {
const args = ["FT.INFO", indexName];
if (options?.scope) {
args.push(options.scope);
}
if (options?.shardScope) {
args.push(options.shardScope);
}
if (options?.consistency) {
args.push(options.consistency);
}
return _handleCustomCommand(client, args, options).then(__1.convertGlideRecordToRecord);
}
/**
* Parse a query and return information about how that query was parsed.
*
* @param client - The client to execute the command.
* @param indexName - The index name.
* @param query - The text query to search. It is the same as the query passed as
* an argument to {@link search | FT.SEARCH} or {@link aggregate | FT.AGGREGATE}.
* @param options - (Optional) See {@link DecoderOption}.
* @returns A query execution plan.
*
* @example
* ```typescript
* const result = GlideFt.explain(client, "myIndex", "@price:[0 10]");
* console.log(result); // Output: "Field {\n\tprice\n\t0\n\t10\n}"
* ```
*/
static explain(client, indexName, query, options) {
const args = ["FT.EXPLAIN", indexName, query];
return _handleCustomCommand(client, args, options);
}
/**
* Parse a query and return information about how that query was parsed.
* Same as {@link explain | FT.EXPLAIN}, except that the results are
* displayed in a different format.
*
* @param client - The client to execute the command.
* @param indexName - The index name.
* @param query - The text query to search. It is the same as the query passed as
* an argument to {@link search | FT.SEARCH} or {@link aggregate | FT.AGGREGATE}.
* @param options - (Optional) See {@link DecoderOption}.
* @returns A query execution plan.
*
* @example
* ```typescript
* const result = GlideFt.explaincli(client, "myIndex", "@price:[0 10]");
* console.log(result); // Output: ["Field {", "price", "0", "10", "}"]
* ```
*/
static explaincli(client, indexName, query, options) {
const args = ["FT.EXPLAINCLI", indexName, query];
return _handleCustomCommand(client, args, options);
}
/**
* Uses the provided query expression to locate keys within an index. Once located, the count
* and/or content of indexed fields within those keys can be returned.
*
* @param client - The client to execute the command.
* @param indexName - The index name to search into.
* @param query - The text query to search.
* @param options - (Optional) See {@link FtSearchOptions} and {@link DecoderOption}.
* @returns A two-element array, where the first element is the number of documents in the result set, and the
* second element has the format: `GlideRecord<GlideRecord<GlideString>>`:
* a mapping between document names and a map of their attributes.
* When `nocontent` is set, the attribute maps will be empty.
*
* If `count` or `limit` with values `{offset: 0, count: 0}` is
* set, the command returns array with only one element: the number of documents.
*
* @example
* ```typescript
* //
* const vector = Buffer.alloc(24);
* const result = await GlideFt.search(client, "json_idx1", "*=>[KNN 2 @VEC $query_vec]", {params: [{key: "query_vec", value: vector}]});
* console.log(result); // Output:
* // [
* // 2,
* // [
* // {
* // key: "json:2",
* // value: [
* // {
* // key: "$",
* // value: '{"vec":[1.1,1.2,1.3,1.4,1.5,1.6]}',
* // },
* // {
* // key: "__VEC_score",
* // value: "11.1100006104",
* // },
* // ],
* // },
* // {
* // key: "json:0",
* // value: [
* // {
* // key: "$",
* // value: '{"vec":[1,2,3,4,5,6]}',
* // },
* // {
* // key: "__VEC_score",
* // value: "91",
* // },
* // ],
* // },
* // ],
* // ]
*
* // Text search with all options:
* // Note: withSortKeys requires sortby; shardScope/consistency are cluster-mode options.
* const textResult = await GlideFt.search(client, "myIndex", "hello world", {
* verbatim: true,
* inorder: true,
* slop: 1,
* sortby: "price",
* sortbyOrder: SortOrder.ASC,
* withsortkeys: true,
* shardScope: "ALLSHARDS",
* consistency: "CONSISTENT",
* dialect: 2,
* });
* ```
*/
static async search(client, indexName, query, options) {
const args = [
"FT.SEARCH",
indexName,
query,
..._addFtSearchOptions(options),
];
return _handleCustomCommand(client, args, options);
}
/**
* Runs a search query and collects performance profiling information.
*
* @param client - The client to execute the command.
* @param indexName - The index name.
* @param query - The text query to search.
* @param options - (Optional) See {@link FtSearchOptions} and {@link DecoderOption}. Additionally:
* - `limited` (Optional) - Either provide a full verbose output or some brief version.
*
* @returns A two-element array. The first element contains results of the search query being profiled, the
* second element stores profiling information.
*
* @example
* ```typescript
* // Example of running profile on a search query
* const vector = Buffer.alloc(24);
* const result = await GlideFt.profileSearch(client, "json_idx1", "*=>[KNN 2 @VEC $query_vec]", {params: [{key: "query_vec", value: vector}]});
* console.log(result); // Output:
* // result[0] contains `FT.SEARCH` response with the given query
* // result[1] contains profiling data as a `Record<string, number>`
* ```
*/
static async profileSearch(client, indexName, query, options) {
const args = ["FT.PROFILE", indexName, "SEARCH"];
if (options?.limited) {
args.push("LIMITED");
}
args.push("QUERY", query);
if (options) {
args.push(..._addFtSearchOptions(options));
}
return _handleCustomCommand(client, args, options).then((v) => [v[0], (0, __1.convertGlideRecordToRecord)(v[1])]);
}
/**
* Runs an aggregate query and collects performance profiling information.
*
* @param client - The client to execute the command.
* @param indexName - The index name.
* @param query - The text query to search.
* @param options - (Optional) See {@link FtAggregateOptions} and {@link DecoderOption}. Additionally:
* - `limited` (Optional) - Either provide a full verbose output or some brief version.
*
* @returns A two-element array. The first element contains results of the aggregate query being profiled, the
* second element stores profiling information.
*
* @example
* ```typescript
* // Example of running profile on an aggregate query
* const options: FtAggregateOptions = {
* loadFields: ["__key"],
* clauses: [
* {
* type: "GROUPBY",
* properties: ["@condition"],
* reducers: [
* {
* function: "TOLIST",
* args: ["__key"],
* name: "bicycles",
* },
* ],
* },
* ],
* };
* const result = await GlideFt.profileAggregate(client, "myIndex", "*", options);
* console.log(result); // Output:
* // result[0] contains `FT.AGGREGATE` response with the given query
* // result[1] contains profiling data as a `Record<string, number>`
* ```
*/
static async profileAggregate(client, indexName, query, options) {
const args = ["FT.PROFILE", indexName, "AGGREGATE"];
if (options?.limited) {
args.push("LIMITED");
}
args.push("QUERY", query);
if (options) {
args.push(..._addFtAggregateOptions(options));
}
return _handleCustomCommand(client, args, options).then((v) => [v[0], (0, __1.convertGlideRecordToRecord)(v[1])]);
}
/**
* Adds an alias for an index. The new alias name can be used anywhere that an index name is required.
*
* @param client - The client to execute the command.
* @param indexName - The alias to be added to the index.
* @param alias - The index name for which the alias has to be added.
* @returns `"OK"`
*
* @example
* ```typescript
* // Example usage of FT.ALIASADD to add an alias for an index.
* await GlideFt.aliasadd(client, "index", "alias"); // "OK"
* ```
*/
static async aliasadd(client, indexName, alias) {
const args = ["FT.ALIASADD", alias, indexName];
return _handleCustomCommand(client, args, {
decoder: __1.Decoder.String,
});
}
/**
* Deletes an existing alias for an index.
*
* @param client - The client to execute the command.
* @param alias - The existing alias to be deleted for an index.
* @returns `"OK"`
*
* @example
* ```typescript
* // Example usage of FT.ALIASDEL to delete an existing alias.
* await GlideFt.aliasdel(client, "alias"); // "OK"
* ```
*/
static async aliasdel(client, alias) {
const args = ["FT.ALIASDEL", alias];
return _handleCustomCommand(client, args, {
decoder: __1.Decoder.String,
});
}
/**
* Updates an existing alias to point to a different physical index. This command only affects future references to the alias.
*
* @param client - The client to execute the command.
* @param alias - The alias name. This alias will now be pointed to a different index.
* @param indexName - The index name for which an existing alias has to updated.
* @returns `"OK"`
*
* @example
* ```typescript
* // Example usage of FT.ALIASUPDATE to update an alias to point to a different index.
* await GlideFt.aliasupdate(client, "newAlias", "index"); // "OK"
* ```
*/
static async aliasupdate(client, alias, indexName) {
const args = ["FT.ALIASUPDATE", alias, indexName];
return _handleCustomCommand(client, args, {
decoder: __1.Decoder.String,
});
}
/**
* List the index aliases.
*
* @param client - The client to execute the command.
* @param options - (Optional) See {@link DecoderOption}.
* @returns A map of index aliases for indices being aliased.
*
* @example
* ```typescript
* // Example usage of FT._ALIASLIST to query index aliases
* const result = await GlideFt.aliaslist(client);
* console.log(result); // Output:
* //[{"key": "alias1", "value": "index1"}, {"key": "alias2", "value": "index2"}]
* ```
*/
static async aliaslist(client, options) {
const args = ["FT._ALIASLIST"];
return _handleCustomCommand(client, args, options);
}
}
exports.GlideFt = GlideFt;
/**
* @internal
*/
function _addFtAggregateOptions(options) {
if (!options)
return [];
const args = [];
if (options.verbatim)
args.push("VERBATIM");
if (options.inorder)
args.push("INORDER");
if (options.slop !== undefined)
args.push("SLOP", options.slop.toString());
if (options.loadAll)
args.push("LOAD", "*");
else if (options.loadFields)
args.push("LOAD", options.loadFields.length.toString(), ...options.loadFields);
if (options.timeout)
args.push("TIMEOUT", options.timeout.toString());
if (options.params) {
args.push("PARAMS", (options.params.length * 2).toString(), ...options.params.flatMap((param) => [param.key, param.value]));
}
if (options.clauses) {
for (const clause of options.clauses) {
switch (clause.type) {
case "LIMIT":
args.push(clause.type, clause.offset.toString(), clause.count.toString());
break;
case "FILTER":
args.push(clause.type, clause.expression);
break;
case "GROUPBY":
args.push(clause.type, clause.properties.length.toString(), ...clause.properties);
for (const reducer of clause.reducers) {
args.push("REDUCE", reducer.function, reducer.args.length.toString(), ...reducer.args);
if (reducer.name)
args.push("AS", reducer.name);
}
break;
case "SORTBY":
args.push(clause.type, (clause.properties.length * 2).toString());
for (const property of clause.properties)
args.push(property.property, property.order);
if (clause.max)
args.push("MAX", clause.max.toString());
break;
case "APPLY":
args.push(clause.type, clause.expression, "AS", clause.name);
break;
default:
throw new Error("Unknown clause type in FtAggregateOptions");
}
}
}
if (options.dialect !== undefined)
args.push("DIALECT", options.dialect.toString());
return args;
}
/**
* @internal
*/
function _addFtSearchOptions(options) {
if (!options)
return [];
if (!options.sortby && options.sortbyOrder) {
throw new Error("sortbyOrder requires sortby to be set.");
}
if (!options.sortby && options.withsortkeys) {
throw new Error("withsortkeys requires sortby to be set.");
}
const args = [];
// SHARD SCOPE
if (options.shardScope) {
args.push(options.shardScope);
}
// CONSISTENCY
if (options.consistency) {
args.push(options.consistency);
}
// NOCONTENT
if (options.nocontent) {
args.push("NOCONTENT");
}
// VERBATIM
if (options.verbatim) {
args.push("VERBATIM");
}
// INORDER
if (options.inorder) {
args.push("INORDER");
}
// SLOP
if (options.slop !== undefined) {
args.push("SLOP", options.slop.toString());
}
// RETURN
if (options.returnFields) {
const returnFields = [];
options.returnFields.forEach((returnField) => returnField.alias
? returnFields.push(returnField.fieldIdentifier, "AS", returnField.alias)
: returnFields.push(returnField.fieldIdentifier));
args.push("RETURN", returnFields.length.toString(), ...returnFields);
}
// SORTBY
if (options.sortby) {
args.push("SORTBY", options.sortby);
if (options.sortbyOrder) {
args.push(options.sortbyOrder);
}
}
// WITHSORTKEYS
if (options.withsortkeys) {
args.push("WITHSORTKEYS");
}
// TIMEOUT
if (options.timeout) {
args.push("TIMEOUT", options.timeout.toString());
}
// PARAMS
if (options.params) {
args.push("PARAMS", (options.params.length * 2).toString(), ...options.params.flatMap((param) => [param.key, param.value]));
}
// LIMIT
if (options.limit) {
args.push("LIMIT", options.limit.offset.toString(), options.limit.count.toString());
}
// COUNT
if (options.count) {
args.push("COUNT");
}
// DIALECT
if (options.dialect !== undefined) {
args.push("DIALECT", options.dialect.toString());
}
return args;
}
/**
* @internal
*/
async function _handleCustomCommand(client, args, decoderOption = {}) {
return client instanceof __1.GlideClient
? client.customCommand(args, decoderOption)
: client.customCommand(args, decoderOption);
}