Skip to content

Commit

Permalink
Node: add FT.CREATE command (valkey-io#2501)
Browse files Browse the repository at this point in the history
* Add FT.CREATE command for Node

Signed-off-by: Andrew Carbonetto <[email protected]>

---------

Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: BoazBD <[email protected]>
  • Loading branch information
acarbonetto authored and BoazBD committed Oct 27, 2024
1 parent 78a95b9 commit 98f63a2
Show file tree
Hide file tree
Showing 8 changed files with 748 additions and 210 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Java: Added `FT.SEARCH` ([#2439](https://github.com/valkey-io/valkey-glide/pull/2439))
* Java: Added `FT.AGGREGATE` ([#2466](https://github.com/valkey-io/valkey-glide/pull/2466))
* Java: Added `JSON.SET` and `JSON.GET` ([#2462](https://github.com/valkey-io/valkey-glide/pull/2462))
* Node: Added `FT.CREATE` ([#2501](https://github.com/valkey-io/valkey-glide/pull/2501))
* Java: Added `JSON.ARRINSERT` and `JSON.ARRLEN` ([#2476](https://github.com/valkey-io/valkey-glide/pull/2476))
* Java: Added `JSON.DEL` and `JSON.FORGET` ([#2490](https://github.com/valkey-io/valkey-glide/pull/2490))
* Java: Added `FT.ALIASADD`, `FT.ALIASDEL`, `FT.ALIASUPDATE` ([#2442](https://github.com/valkey-io/valkey-glide/pull/2442))
Expand Down
10 changes: 10 additions & 0 deletions node/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ To run the integration tests with existing servers, run the following command:
```bash
npm run test -- --cluster-endpoints=localhost:7000 --standalone-endpoints=localhost:6379
# If those endpoints use TLS, add `--tls=true` (applies to both endpoints)
npm run test -- --cluster-endpoints=localhost:7000 --standalone-endpoints=localhost:6379 --tls=true
```
By default, the server_modules tests do not run using `npm run test`. After pointing to a server with JSON and VSS modules setup,
run the following command:
```bash
npm run test-modules
```
### Submodules
Expand Down
2 changes: 2 additions & 0 deletions node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export * from "./src/GlideClient";
export * from "./src/GlideClusterClient";
export * from "./src/Logger";
export * from "./src/server-modules/GlideJson";
export * from "./src/server-modules/GlideFt";
export * from "./src/server-modules/GlideFtOptions";
export * from "./src/Transaction";
16 changes: 16 additions & 0 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ function initialize() {
GlideClusterClient,
GlideClientConfiguration,
GlideJson,
GlideFt,
TextField,
TagField,
NumericField,
VectorField,
VectorFieldAttributesFlat,
VectorFieldAttributesHnsw,
FtCreateOptions,
GlideRecord,
GlideString,
JsonGetOptions,
Expand Down Expand Up @@ -228,6 +236,14 @@ function initialize() {
Decoder,
DecoderOption,
GeoAddOptions,
GlideFt,
TextField,
TagField,
NumericField,
VectorField,
VectorFieldAttributesFlat,
VectorFieldAttributesHnsw,
FtCreateOptions,
GlideRecord,
GlideJson,
GlideString,
Expand Down
3 changes: 2 additions & 1 deletion node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"compile-protobuf-files": "cd src && pbjs -t static-module -o ProtobufMessage.js ../../glide-core/src/protobuf/*.proto && pbts -o ProtobufMessage.d.ts ProtobufMessage.js",
"fix-protobuf-file": "replace 'this\\.encode\\(message, writer\\)\\.ldelim' 'this.encode(message, writer && writer.len ? writer.fork() : writer).ldelim' src/ProtobufMessage.js",
"test": "npm run build-test-utils && jest --verbose --runInBand --testPathIgnorePatterns='ServerModules'",
"test-modules": "npm run build-test-utils && jest --verbose --runInBand --testPathPattern='ServerModules'",
"test-minimum": "npm run build-test-utils && jest --verbose --runInBand --testNamePattern='^(.(?!(GlideJson|GlideFt|pubsub|kill)))*$'",
"test-modules": "npm run build-test-utils && jest --verbose --runInBand --testNamePattern='(GlideJson|GlideFt)'",
"build-test-utils": "cd ../utils && npm i && npm run build",
"lint:fix": "npm run install-linting && npx eslint -c ../eslint.config.mjs --fix && npm run prettier:format",
"lint": "npm run install-linting && npx eslint -c ../eslint.config.mjs && npm run prettier:check:ci",
Expand Down
176 changes: 176 additions & 0 deletions node/src/server-modules/GlideFt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

import { Decoder, DecoderOption, GlideString } from "../BaseClient";
import { GlideClient } from "../GlideClient";
import { GlideClusterClient } from "../GlideClusterClient";
import { Field, FtCreateOptions } from "./GlideFtOptions";

/** Module for Vector Search commands */
export 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 arguments 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:"]
* });
* ```
*/
static async create(
client: GlideClient | GlideClusterClient,
indexName: GlideString,
schema: Field[],
options?: FtCreateOptions,
): Promise<"OK"> {
const args: GlideString[] = ["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,
);
}
}

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 "TAG": {
if (f.separator) {
args.push("SEPARATOR", f.separator);
}

if (f.caseSensitive) {
args.push("CASESENSITIVE");
}

break;
}

case "VECTOR": {
if (f.attributes) {
args.push(f.attributes.algorithm);

const attributes: GlideString[] = [];

// all VectorFieldAttributes attributes
if (f.attributes.dimension) {
attributes.push(
"DIM",
f.attributes.dimension.toString(),
);
}

if (f.attributes.distanceMetric) {
attributes.push(
"DISTANCE_METRIC",
f.attributes.distanceMetric.toString(),
);
}

if (f.attributes.type) {
attributes.push(
"TYPE",
f.attributes.type.toString(),
);
}

if (f.attributes.initialCap) {
attributes.push(
"INITIAL_CAP",
f.attributes.initialCap.toString(),
);
}

// VectorFieldAttributesHnsw attributes
if ("m" in f.attributes && f.attributes.m) {
attributes.push("M", f.attributes.m.toString());
}

if (
"efContruction" in f.attributes &&
f.attributes.efContruction
) {
attributes.push(
"EF_CONSTRUCTION",
f.attributes.efContruction.toString(),
);
}

if (
"efRuntime" in f.attributes &&
f.attributes.efRuntime
) {
attributes.push(
"EF_RUNTIME",
f.attributes.efRuntime.toString(),
);
}

args.push(attributes.length.toString(), ...attributes);
}

break;
}

default:
// no-op
}
});

return _handleCustomCommand(client, args, {
decoder: Decoder.String,
}) as Promise<"OK">;
}
}

/**
* @internal
*/
function _handleCustomCommand(
client: GlideClient | GlideClusterClient,
args: GlideString[],
decoderOption: DecoderOption,
) {
return client instanceof GlideClient
? (client as GlideClient).customCommand(args, decoderOption)
: (client as GlideClusterClient).customCommand(args, decoderOption);
}
120 changes: 120 additions & 0 deletions node/src/server-modules/GlideFtOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

import { GlideString } from "../BaseClient";

interface BaseField {
/** The name of the field. */
name: GlideString;
/** An alias for field. */
alias?: GlideString;
}

/**
* Field contains any blob of data.
*/
export type TextField = BaseField & {
/** Field identifier */
type: "TEXT";
};

/**
* Tag fields are similar to full-text fields, but they interpret the text as a simple list of
* tags delimited by a separator character.
*
* For HASH fields, separator default is a comma (`,`). For JSON fields, there is no default
* separator; you must declare one explicitly if needed.
*/
export type TagField = BaseField & {
/** Field identifier */
type: "TAG";
/** Specify how text in the attribute is split into individual tags. Must be a single character. */
separator?: GlideString;
/** Preserve the original letter cases of tags. If set to False, characters are converted to lowercase by default. */
caseSensitive?: boolean;
};

/**
* Field contains a number.
*/
export type NumericField = BaseField & {
/** Field identifier */
type: "NUMERIC";
};

/**
* Superclass for vector field implementations, contains common logic.
*/
export type VectorField = BaseField & {
/** Field identifier */
type: "VECTOR";
/** Additional attributes to be passed with the vector field after the algorithm name. */
attributes: VectorFieldAttributesFlat | VectorFieldAttributesHnsw;
};

/**
* Base class for defining vector field attributes to be used after the vector algorithm name.
*/
export interface VectorFieldAttributes {
/** Number of dimensions in the vector. Equivalent to DIM in the option. */
dimension: number;
/**
* The distance metric used in vector type field. Can be one of [L2 | IP | COSINE].
*/
distanceMetric: "L2" | "IP" | "COSINE";
/** Vector type. The only supported type is FLOAT32. */
type: "FLOAT32";
/**
* Initial vector capacity in the index affecting memory allocation size of the index. Defaults to 1024.
*/
initialCap?: number;
}

/**
* Vector field that supports vector search by FLAT (brute force) algorithm.
*
* The algorithm is a brute force linear processing of each vector in the index, yielding exact
* answers within the bounds of the precision of the distance computations.
*/
export type VectorFieldAttributesFlat = VectorFieldAttributes & {
algorithm: "FLAT";
};

/**
* Vector field that supports vector search by HNSM (Hierarchical Navigable Small World) algorithm.
*
* The algorithm provides an approximation of the correct answer in exchange for substantially
* lower execution times.
*/
export type VectorFieldAttributesHnsw = VectorFieldAttributes & {
algorithm: "HNSW";
/**
* Number of maximum allowed outgoing edges for each node in the graph in each layer. Default is 16, maximum is 512.
* Equivalent to the `m` attribute.
*/
numberOfEdges?: number;
/**
* Controls the number of vectors examined during index construction. Default value is 200, Maximum value is 4096.
* Equivalent to the `efContruction` attribute.
*/
vectorsExaminedOnConstruction?: number;
/**
* Controls the number of vectors examined during query operations. Default value is 10, Maximum value is 4096.
* Equivalent to the `efRuntime` attribute.
*/
vectorsExaminedOnRuntime?: number;
};

export type Field = TextField | TagField | NumericField | VectorField;

/**
* Represents the input options to be used in the FT.CREATE command.
* All fields in this class are optional inputs for FT.CREATE.
*/
export interface FtCreateOptions {
/** The type of data to be indexed using FT.CREATE. */
dataType: "JSON" | "HASH";
/** The prefix of the key to be indexed. */
prefixes?: GlideString[];
}
Loading

0 comments on commit 98f63a2

Please sign in to comment.