@anvia/redis 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Indra Zulfi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,112 @@
1
+ // src/helpers.ts
2
+ import { createHash } from "crypto";
3
+
4
+ // src/types.ts
5
+ import { SchemaFieldTypes, VectorAlgorithms } from "redis";
6
+ var documentIdField = "__anvia_document_id";
7
+ var documentField = "__anvia_document";
8
+ var vectorField = "__anvia_vector";
9
+ var reservedFieldPrefix = "__anvia_";
10
+
11
+ // src/helpers.ts
12
+ function assertNoReservedMetadata(metadata) {
13
+ for (const key of Object.keys(metadata ?? {})) {
14
+ if (key.startsWith(reservedFieldPrefix)) {
15
+ throw new Error(`Metadata key ${key} is reserved for Anvia Redis fields`);
16
+ }
17
+ }
18
+ }
19
+ function redisKeyId(keyPrefix, id) {
20
+ const hash = createHash("sha256").update(id).digest("hex").slice(0, 32);
21
+ return `${keyPrefix}${hash}`;
22
+ }
23
+ function serializeDocument(document) {
24
+ return typeof document === "string" ? document : JSON.stringify(document);
25
+ }
26
+ function parseDocument(document) {
27
+ if (document === null || document === void 0) {
28
+ return "";
29
+ }
30
+ if (typeof document !== "string") {
31
+ return document;
32
+ }
33
+ try {
34
+ return JSON.parse(document);
35
+ } catch {
36
+ return document;
37
+ }
38
+ }
39
+ function redisHashEntries(keyPrefix, document) {
40
+ if (document.embeddings.length === 0) {
41
+ throw new Error(`Document ${document.id} has no embeddings`);
42
+ }
43
+ assertNoReservedMetadata(document.metadata);
44
+ return document.embeddings.map((embedding, index) => {
45
+ const logicalId = document.embeddings.length === 1 ? document.id : `${document.id}#embedding:${index}`;
46
+ return {
47
+ key: redisKeyId(keyPrefix, logicalId),
48
+ fields: {
49
+ [documentIdField]: document.id,
50
+ [documentField]: serializeDocument(document.document),
51
+ [vectorField]: Buffer.from(new Float32Array(embedding.vector).buffer),
52
+ ...document.metadata ?? {}
53
+ }
54
+ };
55
+ });
56
+ }
57
+ function parseQueryResults(response, threshold) {
58
+ const raw = response;
59
+ const documents = raw.documents ?? [];
60
+ const byId = /* @__PURE__ */ new Map();
61
+ for (const doc of documents) {
62
+ const value = doc.value ?? {};
63
+ const scoreRaw = value.score ?? value.__vector_score;
64
+ const score = typeof scoreRaw === "string" ? parseFloat(scoreRaw) : scoreRaw ?? 0;
65
+ if (threshold !== void 0 && score < threshold) {
66
+ continue;
67
+ }
68
+ const id = String(value[documentIdField] ?? "");
69
+ const result = {
70
+ id,
71
+ score,
72
+ document: parseDocument(value[documentField]),
73
+ ...metadataFromFields(value)
74
+ };
75
+ const current = byId.get(id);
76
+ if (current === void 0 || result.score > current.score) {
77
+ byId.set(id, result);
78
+ }
79
+ }
80
+ return [...byId.values()];
81
+ }
82
+ async function defaultRedisClient() {
83
+ const redis = await import("redis");
84
+ const url = process.env.REDIS_URL ?? "redis://localhost:6379";
85
+ const client = redis.createClient({ url });
86
+ await client.connect();
87
+ return client;
88
+ }
89
+ function metadataFromFields(fields) {
90
+ const metadata = Object.fromEntries(
91
+ Object.entries(fields).filter(
92
+ ([key]) => !key.startsWith(reservedFieldPrefix) && key !== vectorField && key !== "score" && key !== "__vector_score"
93
+ )
94
+ );
95
+ return Object.keys(metadata).length === 0 ? {} : { metadata };
96
+ }
97
+
98
+ export {
99
+ SchemaFieldTypes,
100
+ VectorAlgorithms,
101
+ documentIdField,
102
+ documentField,
103
+ vectorField,
104
+ assertNoReservedMetadata,
105
+ redisKeyId,
106
+ serializeDocument,
107
+ parseDocument,
108
+ redisHashEntries,
109
+ parseQueryResults,
110
+ defaultRedisClient
111
+ };
112
+ //# sourceMappingURL=chunk-KFFPZPCE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers.ts","../src/types.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { EmbeddedDocument, VectorMetadata } from \"@anvia/core/embeddings\";\nimport type { VectorSearchResult } from \"@anvia/core/vector-store\";\nimport {\n documentField,\n documentIdField,\n type RedisClientLike,\n reservedFieldPrefix,\n vectorField,\n} from \"./types.js\";\n\nexport function assertNoReservedMetadata(metadata: VectorMetadata | undefined): void {\n for (const key of Object.keys(metadata ?? {})) {\n if (key.startsWith(reservedFieldPrefix)) {\n throw new Error(`Metadata key ${key} is reserved for Anvia Redis fields`);\n }\n }\n}\n\nexport function redisKeyId(keyPrefix: string, id: string): string {\n const hash = createHash(\"sha256\").update(id).digest(\"hex\").slice(0, 32);\n return `${keyPrefix}${hash}`;\n}\n\nexport function serializeDocument(document: unknown): string {\n return typeof document === \"string\" ? document : JSON.stringify(document);\n}\n\nexport function parseDocument<T>(document: unknown): T {\n if (document === null || document === undefined) {\n return \"\" as T;\n }\n if (typeof document !== \"string\") {\n return document as T;\n }\n try {\n return JSON.parse(document) as T;\n } catch {\n return document as T;\n }\n}\n\nexport function redisHashEntries<T, Metadata extends VectorMetadata>(\n keyPrefix: string,\n document: EmbeddedDocument<T, Metadata>,\n): Array<{ key: string; fields: Record<string, unknown> }> {\n if (document.embeddings.length === 0) {\n throw new Error(`Document ${document.id} has no embeddings`);\n }\n assertNoReservedMetadata(document.metadata);\n\n return document.embeddings.map((embedding, index) => {\n const logicalId =\n document.embeddings.length === 1 ? document.id : `${document.id}#embedding:${index}`;\n return {\n key: redisKeyId(keyPrefix, logicalId),\n fields: {\n [documentIdField]: document.id,\n [documentField]: serializeDocument(document.document),\n [vectorField]: Buffer.from(new Float32Array(embedding.vector).buffer),\n ...(document.metadata ?? {}),\n },\n };\n });\n}\n\nexport function parseQueryResults<T, Metadata extends VectorMetadata>(\n response: unknown,\n threshold: number | undefined,\n): Array<VectorSearchResult<T, Metadata>> {\n const raw = response as {\n total?: number;\n documents?: Array<{\n id?: string;\n value?: Record<string, unknown>;\n }>;\n };\n const documents = raw.documents ?? [];\n const byId = new Map<string, VectorSearchResult<T, Metadata>>();\n\n for (const doc of documents) {\n const value = doc.value ?? {};\n const scoreRaw = value.score ?? value.__vector_score;\n const score = typeof scoreRaw === \"string\" ? parseFloat(scoreRaw) : ((scoreRaw as number) ?? 0);\n\n if (threshold !== undefined && score < threshold) {\n continue;\n }\n\n const id = String(value[documentIdField] ?? \"\");\n const result = {\n id,\n score,\n document: parseDocument(value[documentField]),\n ...metadataFromFields<Metadata>(value),\n } as VectorSearchResult<T, Metadata>;\n const current = byId.get(id);\n if (current === undefined || result.score > current.score) {\n byId.set(id, result);\n }\n }\n\n return [...byId.values()];\n}\n\nexport async function defaultRedisClient(): Promise<RedisClientLike> {\n const redis = await import(\"redis\");\n const url = process.env.REDIS_URL ?? \"redis://localhost:6379\";\n const client = redis.createClient({ url });\n await client.connect();\n return client as unknown as RedisClientLike;\n}\n\nfunction metadataFromFields<Metadata extends VectorMetadata>(\n fields: Record<string, unknown>,\n): { metadata?: Metadata | undefined } {\n const metadata = Object.fromEntries(\n Object.entries(fields).filter(\n ([key]) =>\n !key.startsWith(reservedFieldPrefix) &&\n key !== vectorField &&\n key !== \"score\" &&\n key !== \"__vector_score\",\n ),\n ) as Metadata;\n return Object.keys(metadata).length === 0 ? {} : { metadata };\n}\n","import { SchemaFieldTypes, VectorAlgorithms } from \"redis\";\n\nexport const documentIdField = \"__anvia_document_id\";\nexport const documentField = \"__anvia_document\";\nexport const vectorField = \"__anvia_vector\";\nexport const reservedFieldPrefix = \"__anvia_\";\n\nexport type RedisDistance = \"COSINE\" | \"L2\" | \"IP\";\n\nexport type RedisClientLike = {\n ft: {\n create(\n indexName: string,\n schema: Record<string, unknown>,\n options?: Record<string, unknown>,\n ): Promise<unknown>;\n search(indexName: string, query: string, options?: Record<string, unknown>): Promise<unknown>;\n dropindex(indexName: string): Promise<unknown>;\n info(indexName: string): Promise<unknown>;\n };\n hSet(key: string, fieldValues: Record<string, unknown>): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n};\n\nexport type RedisVectorStoreConnectOptions = {\n client?: RedisClientLike | undefined;\n indexName: string;\n keyPrefix?: string | undefined;\n vectorSize: number;\n createIfMissing?: boolean | undefined;\n distance?: RedisDistance | undefined;\n};\n\nexport { SchemaFieldTypes, VectorAlgorithms };\n"],"mappings":"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","names":[]}
@@ -0,0 +1,19 @@
1
+ import {
2
+ assertNoReservedMetadata,
3
+ defaultRedisClient,
4
+ parseDocument,
5
+ parseQueryResults,
6
+ redisHashEntries,
7
+ redisKeyId,
8
+ serializeDocument
9
+ } from "./chunk-KFFPZPCE.js";
10
+ export {
11
+ assertNoReservedMetadata,
12
+ defaultRedisClient,
13
+ parseDocument,
14
+ parseQueryResults,
15
+ redisHashEntries,
16
+ redisKeyId,
17
+ serializeDocument
18
+ };
19
+ //# sourceMappingURL=helpers-ICLEB6TC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,54 @@
1
+ import { VectorFilter, VectorSearchIndex, VectorSearchRequest, VectorSearchResult, VectorSearchToolOptions } from '@anvia/core/vector-store';
2
+ import { VectorMetadata, EmbeddingModel, EmbeddedDocument } from '@anvia/core/embeddings';
3
+ import { Tool } from '@anvia/core/tool';
4
+
5
+ declare function filterToRedisQuery(filter: VectorFilter | undefined): string;
6
+
7
+ type RedisDistance = "COSINE" | "L2" | "IP";
8
+ type RedisClientLike = {
9
+ ft: {
10
+ create(indexName: string, schema: Record<string, unknown>, options?: Record<string, unknown>): Promise<unknown>;
11
+ search(indexName: string, query: string, options?: Record<string, unknown>): Promise<unknown>;
12
+ dropindex(indexName: string): Promise<unknown>;
13
+ info(indexName: string): Promise<unknown>;
14
+ };
15
+ hSet(key: string, fieldValues: Record<string, unknown>): Promise<unknown>;
16
+ expire(key: string, seconds: number): Promise<unknown>;
17
+ };
18
+ type RedisVectorStoreConnectOptions = {
19
+ client?: RedisClientLike | undefined;
20
+ indexName: string;
21
+ keyPrefix?: string | undefined;
22
+ vectorSize: number;
23
+ createIfMissing?: boolean | undefined;
24
+ distance?: RedisDistance | undefined;
25
+ };
26
+
27
+ declare class RedisVectorIndex<T, Metadata extends VectorMetadata = VectorMetadata> implements VectorSearchIndex<T, Metadata> {
28
+ private readonly model;
29
+ private readonly client;
30
+ private readonly indexName;
31
+ constructor(model: EmbeddingModel, client: RedisClientLike, indexName: string);
32
+ search(request: VectorSearchRequest): Promise<Array<VectorSearchResult<T, Metadata>>>;
33
+ searchIds(request: VectorSearchRequest): Promise<Array<{
34
+ score: number;
35
+ id: string;
36
+ }>>;
37
+ asTool(options: VectorSearchToolOptions): Tool<{
38
+ query: string;
39
+ topK?: number;
40
+ }, unknown>;
41
+ }
42
+
43
+ declare class RedisVectorStore<T, Metadata extends VectorMetadata = VectorMetadata> {
44
+ private readonly client;
45
+ private readonly indexName;
46
+ private readonly keyPrefix;
47
+ private readonly vectorSize;
48
+ private constructor();
49
+ static connect<T, Metadata extends VectorMetadata = VectorMetadata>(options: RedisVectorStoreConnectOptions): Promise<RedisVectorStore<T, Metadata>>;
50
+ upsertDocuments(documents: Array<EmbeddedDocument<T, Metadata>>): Promise<void>;
51
+ index(model: EmbeddingModel): RedisVectorIndex<T, Metadata>;
52
+ }
53
+
54
+ export { type RedisClientLike, type RedisDistance, RedisVectorIndex, RedisVectorStore, type RedisVectorStoreConnectOptions, filterToRedisQuery };
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
1
+ import {
2
+ SchemaFieldTypes,
3
+ VectorAlgorithms,
4
+ documentField,
5
+ documentIdField,
6
+ parseQueryResults,
7
+ redisHashEntries,
8
+ vectorField
9
+ } from "./chunk-KFFPZPCE.js";
10
+
11
+ // src/filters.ts
12
+ function filterToRedisQuery(filter) {
13
+ if (filter === void 0) {
14
+ return "*";
15
+ }
16
+ return `(${translateFilter(filter)})`;
17
+ }
18
+ function translateFilter(filter) {
19
+ switch (filter.type) {
20
+ case "eq":
21
+ if (typeof filter.value === "string") {
22
+ return `@${filter.key}:"${escapeRedisValue(filter.value)}"`;
23
+ }
24
+ if (typeof filter.value === "boolean") {
25
+ return `@${filter.key}:{${filter.value ? "1" : "0"}}`;
26
+ }
27
+ return `@${filter.key}:[${filter.value} ${filter.value}]`;
28
+ case "gt":
29
+ return `@${filter.key}:[(${filter.value} +inf]`;
30
+ case "lt":
31
+ return `@${filter.key}:[-inf (${filter.value}]`;
32
+ case "and":
33
+ return filter.filters.map((f) => `(${translateFilter(f)})`).join(" ");
34
+ case "or":
35
+ return filter.filters.map((f) => `(${translateFilter(f)})`).join(" | ");
36
+ }
37
+ }
38
+ function escapeRedisValue(value) {
39
+ return value.replace(/(["\\])/g, "\\$1");
40
+ }
41
+
42
+ // src/search-index.ts
43
+ import { embedText } from "@anvia/core/embeddings";
44
+ import {
45
+ createVectorSearchTool
46
+ } from "@anvia/core/vector-store";
47
+ var RedisVectorIndex = class {
48
+ constructor(model, client, indexName) {
49
+ this.model = model;
50
+ this.client = client;
51
+ this.indexName = indexName;
52
+ }
53
+ model;
54
+ client;
55
+ indexName;
56
+ async search(request) {
57
+ const queryEmbedding = await embedText(this.model, request.query);
58
+ const filterQuery = filterToRedisQuery(request.filter);
59
+ const knnQuery = `${filterQuery}=>[KNN ${request.topK} @${vectorField} $vec AS score]`;
60
+ const response = await this.client.ft.search(this.indexName, knnQuery, {
61
+ PARAMS: {
62
+ vec: Buffer.from(new Float32Array(queryEmbedding.vector).buffer)
63
+ },
64
+ RETURN: ["__anvia_document_id", "__anvia_document", "score"],
65
+ DIALECT: 2
66
+ });
67
+ return parseQueryResults(response, request.threshold);
68
+ }
69
+ async searchIds(request) {
70
+ return (await this.search(request)).map(({ score, id }) => ({ score, id }));
71
+ }
72
+ asTool(options) {
73
+ return createVectorSearchTool(this, options);
74
+ }
75
+ };
76
+
77
+ // src/store.ts
78
+ var RedisVectorStore = class _RedisVectorStore {
79
+ constructor(client, indexName, keyPrefix, vectorSize) {
80
+ this.client = client;
81
+ this.indexName = indexName;
82
+ this.keyPrefix = keyPrefix;
83
+ this.vectorSize = vectorSize;
84
+ }
85
+ client;
86
+ indexName;
87
+ keyPrefix;
88
+ vectorSize;
89
+ static async connect(options) {
90
+ const client = options.client ?? await (await import("./helpers-ICLEB6TC.js")).defaultRedisClient();
91
+ const keyPrefix = options.keyPrefix ?? `anvia:${options.indexName}:`;
92
+ if (options.createIfMissing === false) {
93
+ await client.ft.info(options.indexName);
94
+ return new _RedisVectorStore(
95
+ client,
96
+ options.indexName,
97
+ keyPrefix,
98
+ options.vectorSize
99
+ );
100
+ }
101
+ try {
102
+ await client.ft.info(options.indexName);
103
+ } catch {
104
+ const distanceMap = {
105
+ COSINE: "COSINE",
106
+ L2: "L2",
107
+ IP: "IP"
108
+ };
109
+ await client.ft.create(
110
+ options.indexName,
111
+ {
112
+ [documentIdField]: { type: SchemaFieldTypes.TEXT },
113
+ [documentField]: { type: SchemaFieldTypes.TEXT },
114
+ [vectorField]: {
115
+ type: SchemaFieldTypes.VECTOR,
116
+ ALGORITHM: VectorAlgorithms.HNSW,
117
+ TYPE: "FLOAT32",
118
+ DIM: options.vectorSize,
119
+ DISTANCE_METRIC: distanceMap[options.distance ?? "COSINE"]
120
+ }
121
+ },
122
+ { ON: "HASH", PREFIX: keyPrefix }
123
+ );
124
+ }
125
+ return new _RedisVectorStore(
126
+ client,
127
+ options.indexName,
128
+ keyPrefix,
129
+ options.vectorSize
130
+ );
131
+ }
132
+ async upsertDocuments(documents) {
133
+ const entries = documents.flatMap((document) => redisHashEntries(this.keyPrefix, document));
134
+ for (const entry of entries) {
135
+ await this.client.hSet(entry.key, entry.fields);
136
+ }
137
+ }
138
+ index(model) {
139
+ return new RedisVectorIndex(model, this.client, this.indexName);
140
+ }
141
+ };
142
+ export {
143
+ RedisVectorIndex,
144
+ RedisVectorStore,
145
+ filterToRedisQuery
146
+ };
147
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/filters.ts","../src/search-index.ts","../src/store.ts"],"sourcesContent":["import type { VectorFilter } from \"@anvia/core/vector-store\";\n\nexport function filterToRedisQuery(filter: VectorFilter | undefined): string {\n if (filter === undefined) {\n return \"*\";\n }\n\n return `(${translateFilter(filter)})`;\n}\n\nfunction translateFilter(filter: VectorFilter): string {\n switch (filter.type) {\n case \"eq\":\n if (typeof filter.value === \"string\") {\n return `@${filter.key}:\"${escapeRedisValue(filter.value)}\"`;\n }\n if (typeof filter.value === \"boolean\") {\n return `@${filter.key}:{${filter.value ? \"1\" : \"0\"}}`;\n }\n return `@${filter.key}:[${filter.value} ${filter.value}]`;\n case \"gt\":\n return `@${filter.key}:[(${filter.value} +inf]`;\n case \"lt\":\n return `@${filter.key}:[-inf (${filter.value}]`;\n case \"and\":\n return filter.filters.map((f) => `(${translateFilter(f)})`).join(\" \");\n case \"or\":\n return filter.filters.map((f) => `(${translateFilter(f)})`).join(\" | \");\n }\n}\n\nfunction escapeRedisValue(value: string): string {\n return value.replace(/([\"\\\\])/g, \"\\\\$1\");\n}\n","import { type EmbeddingModel, embedText, type VectorMetadata } from \"@anvia/core/embeddings\";\nimport type { Tool } from \"@anvia/core/tool\";\nimport {\n createVectorSearchTool,\n type VectorSearchIndex,\n type VectorSearchRequest,\n type VectorSearchResult,\n type VectorSearchToolOptions,\n} from \"@anvia/core/vector-store\";\nimport { filterToRedisQuery } from \"./filters.js\";\nimport { parseQueryResults } from \"./helpers.js\";\nimport { type RedisClientLike, vectorField } from \"./types.js\";\n\nexport class RedisVectorIndex<T, Metadata extends VectorMetadata = VectorMetadata>\n implements VectorSearchIndex<T, Metadata>\n{\n constructor(\n private readonly model: EmbeddingModel,\n private readonly client: RedisClientLike,\n private readonly indexName: string,\n ) {}\n\n async search(request: VectorSearchRequest): Promise<Array<VectorSearchResult<T, Metadata>>> {\n const queryEmbedding = await embedText(this.model, request.query);\n const filterQuery = filterToRedisQuery(request.filter);\n const knnQuery = `${filterQuery}=>[KNN ${request.topK} @${vectorField} $vec AS score]`;\n\n const response = await this.client.ft.search(this.indexName, knnQuery, {\n PARAMS: {\n vec: Buffer.from(new Float32Array(queryEmbedding.vector).buffer),\n },\n RETURN: [\"__anvia_document_id\", \"__anvia_document\", \"score\"],\n DIALECT: 2,\n });\n\n return parseQueryResults<T, Metadata>(response, request.threshold);\n }\n\n async searchIds(request: VectorSearchRequest): Promise<Array<{ score: number; id: string }>> {\n return (await this.search(request)).map(({ score, id }) => ({ score, id }));\n }\n\n asTool(options: VectorSearchToolOptions): Tool<{ query: string; topK?: number }, unknown> {\n return createVectorSearchTool(this, options);\n }\n}\n","import type { EmbeddedDocument, EmbeddingModel, VectorMetadata } from \"@anvia/core/embeddings\";\nimport { redisHashEntries } from \"./helpers.js\";\nimport { RedisVectorIndex } from \"./search-index.js\";\nimport {\n documentField,\n documentIdField,\n type RedisClientLike,\n type RedisDistance,\n type RedisVectorStoreConnectOptions,\n SchemaFieldTypes,\n VectorAlgorithms,\n vectorField,\n} from \"./types.js\";\n\nexport class RedisVectorStore<T, Metadata extends VectorMetadata = VectorMetadata> {\n private constructor(\n private readonly client: RedisClientLike,\n private readonly indexName: string,\n private readonly keyPrefix: string,\n private readonly vectorSize: number,\n ) {}\n\n static async connect<T, Metadata extends VectorMetadata = VectorMetadata>(\n options: RedisVectorStoreConnectOptions,\n ): Promise<RedisVectorStore<T, Metadata>> {\n const client = options.client ?? (await (await import(\"./helpers.js\")).defaultRedisClient());\n const keyPrefix = options.keyPrefix ?? `anvia:${options.indexName}:`;\n\n if (options.createIfMissing === false) {\n await client.ft.info(options.indexName);\n return new RedisVectorStore<T, Metadata>(\n client,\n options.indexName,\n keyPrefix,\n options.vectorSize,\n );\n }\n\n try {\n await client.ft.info(options.indexName);\n } catch {\n const distanceMap: Record<RedisDistance, string> = {\n COSINE: \"COSINE\",\n L2: \"L2\",\n IP: \"IP\",\n };\n await client.ft.create(\n options.indexName,\n {\n [documentIdField]: { type: SchemaFieldTypes.TEXT },\n [documentField]: { type: SchemaFieldTypes.TEXT },\n [vectorField]: {\n type: SchemaFieldTypes.VECTOR,\n ALGORITHM: VectorAlgorithms.HNSW,\n TYPE: \"FLOAT32\",\n DIM: options.vectorSize,\n DISTANCE_METRIC: distanceMap[options.distance ?? \"COSINE\"],\n },\n },\n { ON: \"HASH\", PREFIX: keyPrefix },\n );\n }\n\n return new RedisVectorStore<T, Metadata>(\n client,\n options.indexName,\n keyPrefix,\n options.vectorSize,\n );\n }\n\n async upsertDocuments(documents: Array<EmbeddedDocument<T, Metadata>>): Promise<void> {\n const entries = documents.flatMap((document) => redisHashEntries(this.keyPrefix, document));\n for (const entry of entries) {\n await this.client.hSet(entry.key, entry.fields);\n }\n }\n\n index(model: EmbeddingModel): RedisVectorIndex<T, Metadata> {\n return new RedisVectorIndex(model, this.client, this.indexName);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@anvia/redis",
3
+ "version": "0.2.0",
4
+ "description": "Redis vector store adapter for Anvia.",
5
+ "author": "anvia",
6
+ "maintainer": "Indra Zulfi",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/anvia-hq/anvia",
11
+ "directory": "packages/vector-redis"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "redis": "^4.7.0",
30
+ "@anvia/core": "0.6.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^24.9.1",
34
+ "tsup": "^8.5.0",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.8"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup src/index.ts --format esm --dts --sourcemap --clean",
40
+ "test": "vitest run",
41
+ "typecheck": "tsc --noEmit"
42
+ }
43
+ }