UNPKG

@valkey/valkey-glide

Version:

General Language Independent Driver for the Enterprise (GLIDE) for Valkey

800 lines (799 loc) 29.7 kB
"use strict"; /** * 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); }