@luxdb/sdk 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +99 -8
- package/dist/index.js +248 -6
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,93 @@
|
|
|
1
|
-
import Redis, { RedisOptions } from 'ioredis';
|
|
1
|
+
import Redis, { type RedisOptions } from 'ioredis';
|
|
2
2
|
export interface VSearchResult {
|
|
3
3
|
key: string;
|
|
4
4
|
similarity: number;
|
|
5
5
|
metadata?: Record<string, unknown>;
|
|
6
6
|
}
|
|
7
|
-
export interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export interface TSSample {
|
|
8
|
+
timestamp: number;
|
|
9
|
+
value: number;
|
|
10
|
+
}
|
|
11
|
+
export interface TSAddOptions {
|
|
12
|
+
retention?: number;
|
|
13
|
+
labels?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export interface TSRangeOptions {
|
|
16
|
+
aggregation?: {
|
|
17
|
+
type: 'avg' | 'sum' | 'min' | 'max' | 'count' | 'first' | 'last' | 'range' | 'std.p' | 'std.s' | 'var.p' | 'var.s';
|
|
18
|
+
bucketSize: number;
|
|
12
19
|
};
|
|
13
|
-
|
|
20
|
+
}
|
|
21
|
+
export interface TSMRangeResult {
|
|
22
|
+
key: string;
|
|
23
|
+
labels: Record<string, string>;
|
|
24
|
+
samples: TSSample[];
|
|
25
|
+
}
|
|
26
|
+
export interface KSubEvent {
|
|
27
|
+
pattern: string;
|
|
28
|
+
key: string;
|
|
29
|
+
operation: string;
|
|
30
|
+
}
|
|
31
|
+
export interface TableRow {
|
|
32
|
+
id: number;
|
|
33
|
+
[field: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
declare class TableQueryBuilder {
|
|
36
|
+
private client;
|
|
37
|
+
private name;
|
|
38
|
+
private conditions;
|
|
39
|
+
private orderField?;
|
|
40
|
+
private orderDir?;
|
|
41
|
+
private limitCount?;
|
|
42
|
+
private joinTable?;
|
|
43
|
+
constructor(client: Lux, name: string);
|
|
44
|
+
where(field: string, op: '=' | '!=' | '>' | '<' | '>=' | '<=', value: string | number | boolean): this;
|
|
45
|
+
orderBy(field: string, dir?: 'asc' | 'desc'): this;
|
|
46
|
+
limit(n: number): this;
|
|
47
|
+
join(table: string): this;
|
|
48
|
+
run(): Promise<TableRow[]>;
|
|
49
|
+
insert(data: Record<string, unknown>): Promise<number>;
|
|
50
|
+
update(id: number, data: Record<string, unknown>): Promise<string>;
|
|
51
|
+
delete(...ids: number[]): Promise<number>;
|
|
52
|
+
}
|
|
53
|
+
declare class VectorNamespace {
|
|
54
|
+
private client;
|
|
55
|
+
constructor(client: Lux);
|
|
56
|
+
set(key: string, vector: number[], metadata?: Record<string, unknown>): Promise<string>;
|
|
57
|
+
get(key: string): Promise<{
|
|
58
|
+
dims: number;
|
|
59
|
+
vector: number[];
|
|
60
|
+
metadata?: Record<string, unknown>;
|
|
61
|
+
} | null>;
|
|
62
|
+
search(query: number[], options: {
|
|
63
|
+
topK: number;
|
|
64
|
+
filter?: {
|
|
65
|
+
key: string;
|
|
66
|
+
value: string;
|
|
67
|
+
};
|
|
68
|
+
meta?: boolean;
|
|
69
|
+
}): Promise<VSearchResult[]>;
|
|
70
|
+
count(): Promise<number>;
|
|
71
|
+
}
|
|
72
|
+
declare class TimeSeriesNamespace {
|
|
73
|
+
private client;
|
|
74
|
+
constructor(client: Lux);
|
|
75
|
+
add(key: string, value: number, options?: {
|
|
76
|
+
timestamp?: number | '*';
|
|
77
|
+
retention?: number;
|
|
78
|
+
labels?: Record<string, string>;
|
|
79
|
+
}): Promise<number>;
|
|
80
|
+
get(key: string): Promise<TSSample | null>;
|
|
81
|
+
range(key: string, from: number | '-', to: number | '+', options?: TSRangeOptions): Promise<TSSample[]>;
|
|
82
|
+
mrange(from: number | '-', to: number | '+', filter: string, options?: TSRangeOptions): Promise<TSMRangeResult[]>;
|
|
83
|
+
info(key: string): Promise<Record<string, unknown>>;
|
|
14
84
|
}
|
|
15
85
|
export declare class Lux extends Redis {
|
|
86
|
+
vectors: VectorNamespace;
|
|
87
|
+
timeseries: TimeSeriesNamespace;
|
|
16
88
|
constructor(options?: RedisOptions | string);
|
|
89
|
+
table(name: string): TableQueryBuilder;
|
|
90
|
+
_tquery(args: string[]): Promise<TableRow[]>;
|
|
17
91
|
vset(key: string, vector: number[], options?: {
|
|
18
92
|
metadata?: Record<string, unknown>;
|
|
19
93
|
ex?: number;
|
|
@@ -24,7 +98,24 @@ export declare class Lux extends Redis {
|
|
|
24
98
|
vector: number[];
|
|
25
99
|
metadata?: Record<string, unknown>;
|
|
26
100
|
} | null>;
|
|
27
|
-
vsearch(query: number[], options:
|
|
101
|
+
vsearch(query: number[], options: {
|
|
102
|
+
k: number;
|
|
103
|
+
filter?: {
|
|
104
|
+
key: string;
|
|
105
|
+
value: string;
|
|
106
|
+
};
|
|
107
|
+
meta?: boolean;
|
|
108
|
+
}): Promise<VSearchResult[]>;
|
|
28
109
|
vcard(): Promise<number>;
|
|
110
|
+
tsadd(key: string, timestamp: number | '*', value: number, options?: TSAddOptions): Promise<number>;
|
|
111
|
+
tsmadd(...entries: [string, number | '*', number][]): Promise<string>;
|
|
112
|
+
tsget(key: string): Promise<TSSample | null>;
|
|
113
|
+
tsrange(key: string, from: number | '-', to: number | '+', options?: TSRangeOptions): Promise<TSSample[]>;
|
|
114
|
+
tsmrange(from: number | '-', to: number | '+', filter: string, options?: TSRangeOptions): Promise<TSMRangeResult[]>;
|
|
115
|
+
tsinfo(key: string): Promise<Record<string, unknown>>;
|
|
116
|
+
ksub(patterns: string[], handler: (event: KSubEvent) => void): {
|
|
117
|
+
unsubscribe: () => void;
|
|
118
|
+
connection: Redis;
|
|
119
|
+
};
|
|
29
120
|
}
|
|
30
121
|
export default Lux;
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,139 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Lux = void 0;
|
|
7
7
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
|
+
class TableQueryBuilder {
|
|
9
|
+
constructor(client, name) {
|
|
10
|
+
this.conditions = [];
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.name = name;
|
|
13
|
+
}
|
|
14
|
+
where(field, op, value) {
|
|
15
|
+
const v = typeof value === 'string' ? `'${value}'` : String(value);
|
|
16
|
+
this.conditions.push(`${field} ${op} ${v}`);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
orderBy(field, dir = 'asc') {
|
|
20
|
+
this.orderField = field;
|
|
21
|
+
this.orderDir = dir.toUpperCase();
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
limit(n) {
|
|
25
|
+
this.limitCount = n;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
join(table) {
|
|
29
|
+
this.joinTable = table;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
async run() {
|
|
33
|
+
const args = [this.name];
|
|
34
|
+
if (this.conditions.length) {
|
|
35
|
+
args.push('WHERE', this.conditions.join(' AND '));
|
|
36
|
+
}
|
|
37
|
+
if (this.orderField) {
|
|
38
|
+
args.push('ORDER', 'BY', this.orderField, this.orderDir || 'ASC');
|
|
39
|
+
}
|
|
40
|
+
if (this.limitCount != null) {
|
|
41
|
+
args.push('LIMIT', String(this.limitCount));
|
|
42
|
+
}
|
|
43
|
+
if (this.joinTable) {
|
|
44
|
+
args.push('JOIN', this.joinTable);
|
|
45
|
+
}
|
|
46
|
+
return this.client._tquery(args);
|
|
47
|
+
}
|
|
48
|
+
async insert(data) {
|
|
49
|
+
const args = [this.name];
|
|
50
|
+
for (const [k, v] of Object.entries(data)) {
|
|
51
|
+
args.push(k, String(v));
|
|
52
|
+
}
|
|
53
|
+
const result = await this.client.call('TINSERT', ...args);
|
|
54
|
+
return parseInt(result, 10) || 0;
|
|
55
|
+
}
|
|
56
|
+
async update(id, data) {
|
|
57
|
+
const args = [this.name, id];
|
|
58
|
+
for (const [k, v] of Object.entries(data)) {
|
|
59
|
+
args.push(k, String(v));
|
|
60
|
+
}
|
|
61
|
+
return this.client.call('TUPDATE', ...args);
|
|
62
|
+
}
|
|
63
|
+
async delete(...ids) {
|
|
64
|
+
return this.client.call('TDEL', this.name, ...ids);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class VectorNamespace {
|
|
68
|
+
constructor(client) {
|
|
69
|
+
this.client = client;
|
|
70
|
+
}
|
|
71
|
+
async set(key, vector, metadata) {
|
|
72
|
+
const args = [key, vector.length, ...vector];
|
|
73
|
+
if (metadata) {
|
|
74
|
+
args.push('META', JSON.stringify(metadata));
|
|
75
|
+
}
|
|
76
|
+
return this.client.call('VSET', ...args);
|
|
77
|
+
}
|
|
78
|
+
async get(key) {
|
|
79
|
+
return this.client.vget(key);
|
|
80
|
+
}
|
|
81
|
+
async search(query, options) {
|
|
82
|
+
return this.client.vsearch(query, { k: options.topK, filter: options.filter, meta: options.meta ?? true });
|
|
83
|
+
}
|
|
84
|
+
async count() {
|
|
85
|
+
return this.client.vcard();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class TimeSeriesNamespace {
|
|
89
|
+
constructor(client) {
|
|
90
|
+
this.client = client;
|
|
91
|
+
}
|
|
92
|
+
async add(key, value, options) {
|
|
93
|
+
return this.client.tsadd(key, options?.timestamp ?? '*', value, { retention: options?.retention, labels: options?.labels });
|
|
94
|
+
}
|
|
95
|
+
async get(key) {
|
|
96
|
+
return this.client.tsget(key);
|
|
97
|
+
}
|
|
98
|
+
async range(key, from, to, options) {
|
|
99
|
+
return this.client.tsrange(key, from, to, options);
|
|
100
|
+
}
|
|
101
|
+
async mrange(from, to, filter, options) {
|
|
102
|
+
return this.client.tsmrange(from, to, filter, options);
|
|
103
|
+
}
|
|
104
|
+
async info(key) {
|
|
105
|
+
return this.client.tsinfo(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
8
108
|
class Lux extends ioredis_1.default {
|
|
9
109
|
constructor(options) {
|
|
10
110
|
super(options);
|
|
111
|
+
this.vectors = new VectorNamespace(this);
|
|
112
|
+
this.timeseries = new TimeSeriesNamespace(this);
|
|
113
|
+
}
|
|
114
|
+
table(name) {
|
|
115
|
+
return new TableQueryBuilder(this, name);
|
|
11
116
|
}
|
|
117
|
+
async _tquery(args) {
|
|
118
|
+
const result = await this.call('TQUERY', ...args);
|
|
119
|
+
if (!result || !Array.isArray(result))
|
|
120
|
+
return [];
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (const item of result) {
|
|
123
|
+
if (Array.isArray(item) && item.length >= 2) {
|
|
124
|
+
const row = { id: 0 };
|
|
125
|
+
for (let i = 0; i < item.length - 1; i += 2) {
|
|
126
|
+
const key = String(item[i]);
|
|
127
|
+
const val = item[i + 1];
|
|
128
|
+
if (key === 'id') {
|
|
129
|
+
row.id = parseInt(val, 10);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
row[key] = val;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
rows.push(row);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
// Vector methods (keep for backward compat)
|
|
12
141
|
async vset(key, vector, options) {
|
|
13
142
|
const args = [key, vector.length, ...vector];
|
|
14
143
|
if (options?.metadata) {
|
|
@@ -23,7 +152,7 @@ class Lux extends ioredis_1.default {
|
|
|
23
152
|
return this.call('VSET', ...args);
|
|
24
153
|
}
|
|
25
154
|
async vget(key) {
|
|
26
|
-
const result =
|
|
155
|
+
const result = await this.call('VGET', key);
|
|
27
156
|
if (!result || !Array.isArray(result))
|
|
28
157
|
return null;
|
|
29
158
|
const dims = parseInt(result[0], 10);
|
|
@@ -49,16 +178,13 @@ class Lux extends ioredis_1.default {
|
|
|
49
178
|
if (options.meta) {
|
|
50
179
|
args.push('META');
|
|
51
180
|
}
|
|
52
|
-
const result =
|
|
181
|
+
const result = await this.call('VSEARCH', ...args);
|
|
53
182
|
if (!result || !Array.isArray(result))
|
|
54
183
|
return [];
|
|
55
184
|
const results = [];
|
|
56
185
|
for (const item of result) {
|
|
57
186
|
if (Array.isArray(item)) {
|
|
58
|
-
const entry = {
|
|
59
|
-
key: item[0],
|
|
60
|
-
similarity: parseFloat(item[1]),
|
|
61
|
-
};
|
|
187
|
+
const entry = { key: item[0], similarity: parseFloat(item[1]) };
|
|
62
188
|
if (options.meta && item[2]) {
|
|
63
189
|
try {
|
|
64
190
|
entry.metadata = JSON.parse(item[2]);
|
|
@@ -75,6 +201,122 @@ class Lux extends ioredis_1.default {
|
|
|
75
201
|
async vcard() {
|
|
76
202
|
return this.call('VCARD');
|
|
77
203
|
}
|
|
204
|
+
// Time series methods (keep for backward compat)
|
|
205
|
+
async tsadd(key, timestamp, value, options) {
|
|
206
|
+
const args = [key, timestamp === '*' ? '*' : timestamp, value];
|
|
207
|
+
if (options?.retention != null) {
|
|
208
|
+
args.push('RETENTION', options.retention);
|
|
209
|
+
}
|
|
210
|
+
if (options?.labels) {
|
|
211
|
+
args.push('LABELS');
|
|
212
|
+
for (const [k, v] of Object.entries(options.labels)) {
|
|
213
|
+
args.push(k, v);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return this.call('TSADD', ...args);
|
|
217
|
+
}
|
|
218
|
+
async tsmadd(...entries) {
|
|
219
|
+
const args = [];
|
|
220
|
+
for (const [key, ts, val] of entries) {
|
|
221
|
+
args.push(key, ts === '*' ? '*' : ts, val);
|
|
222
|
+
}
|
|
223
|
+
return this.call('TSMADD', ...args);
|
|
224
|
+
}
|
|
225
|
+
async tsget(key) {
|
|
226
|
+
const result = await this.call('TSGET', key);
|
|
227
|
+
if (!result || !Array.isArray(result) || result.length < 2)
|
|
228
|
+
return null;
|
|
229
|
+
return { timestamp: parseInt(result[0], 10), value: parseFloat(result[1]) };
|
|
230
|
+
}
|
|
231
|
+
async tsrange(key, from, to, options) {
|
|
232
|
+
const args = [key, from === '-' ? '-' : from, to === '+' ? '+' : to];
|
|
233
|
+
if (options?.aggregation) {
|
|
234
|
+
args.push('AGGREGATION', options.aggregation.type, options.aggregation.bucketSize);
|
|
235
|
+
}
|
|
236
|
+
const result = await this.call('TSRANGE', ...args);
|
|
237
|
+
if (!result || !Array.isArray(result))
|
|
238
|
+
return [];
|
|
239
|
+
return result.map((pair) => ({ timestamp: parseInt(pair[0], 10), value: parseFloat(pair[1]) }));
|
|
240
|
+
}
|
|
241
|
+
async tsmrange(from, to, filter, options) {
|
|
242
|
+
const args = [from === '-' ? '-' : from, to === '+' ? '+' : to];
|
|
243
|
+
if (options?.aggregation) {
|
|
244
|
+
args.push('AGGREGATION', options.aggregation.type, options.aggregation.bucketSize);
|
|
245
|
+
}
|
|
246
|
+
args.push('FILTER', filter);
|
|
247
|
+
const result = await this.call('TSMRANGE', ...args);
|
|
248
|
+
if (!result || !Array.isArray(result))
|
|
249
|
+
return [];
|
|
250
|
+
return result.map((series) => {
|
|
251
|
+
const labels = {};
|
|
252
|
+
if (Array.isArray(series[1])) {
|
|
253
|
+
for (const pair of series[1]) {
|
|
254
|
+
if (Array.isArray(pair) && pair.length >= 2)
|
|
255
|
+
labels[pair[0]] = pair[1];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const samples = Array.isArray(series[2])
|
|
259
|
+
? series[2].map((s) => ({ timestamp: parseInt(s[0], 10), value: parseFloat(s[1]) }))
|
|
260
|
+
: [];
|
|
261
|
+
return { key: series[0], labels, samples };
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async tsinfo(key) {
|
|
265
|
+
const result = await this.call('TSINFO', key);
|
|
266
|
+
if (!result || !Array.isArray(result))
|
|
267
|
+
return {};
|
|
268
|
+
const info = {};
|
|
269
|
+
for (let i = 0; i < result.length - 1; i += 2) {
|
|
270
|
+
const k = result[i];
|
|
271
|
+
const v = result[i + 1];
|
|
272
|
+
if (k === 'labels' && Array.isArray(v)) {
|
|
273
|
+
const labels = {};
|
|
274
|
+
for (const pair of v) {
|
|
275
|
+
if (Array.isArray(pair) && pair.length >= 2)
|
|
276
|
+
labels[pair[0]] = pair[1];
|
|
277
|
+
}
|
|
278
|
+
info[k] = labels;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
info[k] = v;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return info;
|
|
285
|
+
}
|
|
286
|
+
// Realtime key subscriptions
|
|
287
|
+
ksub(patterns, handler) {
|
|
288
|
+
const sub = this.duplicate();
|
|
289
|
+
sub.on('error', () => { });
|
|
290
|
+
const dataHandler = sub._dataHandler || sub.dataHandler;
|
|
291
|
+
if (dataHandler && dataHandler.returnReply) {
|
|
292
|
+
const origReturn = dataHandler.returnReply.bind(dataHandler);
|
|
293
|
+
dataHandler.returnReply = (reply) => {
|
|
294
|
+
if (Array.isArray(reply) && reply.length === 4 && reply[0] === 'kmessage') {
|
|
295
|
+
handler({ pattern: reply[1], key: reply[2], operation: reply[3] });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
return origReturn(reply);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const origEmit = sub.emit.bind(sub);
|
|
303
|
+
sub.emit = (event, ...args) => {
|
|
304
|
+
if (event === 'error' && args[0]?.message?.includes('Command queue state error')) {
|
|
305
|
+
const match = args[0].message.match(/Last reply: kmessage,([^,]+),([^,]+),(.+)/);
|
|
306
|
+
if (match) {
|
|
307
|
+
handler({ pattern: match[1], key: match[2], operation: match[3] });
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return origEmit(event, ...args);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
sub.call('KSUB', ...patterns);
|
|
315
|
+
return {
|
|
316
|
+
connection: sub,
|
|
317
|
+
unsubscribe() { sub.disconnect(); },
|
|
318
|
+
};
|
|
319
|
+
}
|
|
78
320
|
}
|
|
79
321
|
exports.Lux = Lux;
|
|
80
322
|
exports.default = Lux;
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luxdb/sdk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Lux SDK - ioredis extended with
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Lux SDK - ioredis extended with vector search, time series, and realtime key subscriptions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": ["dist"],
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc",
|
|
10
|
-
"prepublishOnly": "
|
|
10
|
+
"prepublishOnly": "bun run build"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"ioredis": "^5.0.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
+
"@types/node": "^25.5.0",
|
|
16
17
|
"typescript": "^5.9.0"
|
|
17
18
|
},
|
|
18
19
|
"peerDependencies": {
|
|
@@ -24,5 +25,5 @@
|
|
|
24
25
|
"url": "https://github.com/lux-db/lux",
|
|
25
26
|
"directory": "sdk"
|
|
26
27
|
},
|
|
27
|
-
"keywords": ["lux", "redis", "vector", "search", "database", "ioredis", "similarity"]
|
|
28
|
+
"keywords": ["lux", "redis", "vector", "search", "database", "ioredis", "similarity", "timeseries", "realtime", "subscriptions"]
|
|
28
29
|
}
|