@retrivora-ai/rag-engine 0.2.6 → 0.2.8
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/{MongoDBProvider-QHMGD2LZ.mjs → MongoDBProvider-KGO6N23T.mjs} +1 -1
- package/dist/{PostgreSQLProvider-PJ5ER5Z4.mjs → PostgreSQLProvider-ILWADFAP.mjs} +1 -1
- package/dist/{RagConfig-Ttch1N4d.d.mts → RagConfig--ibz0b3W.d.mts} +4 -0
- package/dist/{RagConfig-Ttch1N4d.d.ts → RagConfig--ibz0b3W.d.ts} +4 -0
- package/dist/{chunk-IUTAZ7QR.mjs → chunk-6GSARSCP.mjs} +9 -2
- package/dist/{chunk-7K4KXB6G.mjs → chunk-CYVPACA7.mjs} +41 -3
- package/dist/{chunk-5HXNKSCR.mjs → chunk-IFPISZ2S.mjs} +8 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +56 -4
- package/dist/handlers/index.mjs +1 -1
- package/dist/{index-rK0KAr2S.d.ts → index-Dr1HN0se.d.ts} +1 -1
- package/dist/{index-sbCtrIRT.d.mts → index-w8qIEFvi.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/server.d.mts +7 -4
- package/dist/server.d.ts +7 -4
- package/dist/server.js +166 -23
- package/dist/server.mjs +113 -22
- package/package.json +1 -1
- package/src/config/RagConfig.ts +4 -0
- package/src/core/Pipeline.ts +60 -1
- package/src/providers/vectordb/MongoDBProvider.ts +12 -1
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +167 -27
- package/src/providers/vectordb/PostgreSQLProvider.ts +11 -2
|
@@ -1,11 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
import { Pool, PoolClient } from 'pg';
|
|
5
2
|
import { BaseVectorProvider } from './BaseVectorProvider';
|
|
6
3
|
import { VectorMatch, UpsertDocument } from '../../types';
|
|
7
4
|
import { VectorDBConfig } from '../../config/RagConfig';
|
|
8
5
|
|
|
6
|
+
interface QueryFieldHint {
|
|
7
|
+
field?: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface TableSearchConfig {
|
|
12
|
+
availableFields: string[];
|
|
13
|
+
searchableFields: string[];
|
|
14
|
+
normalizedFieldMap: Map<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeFieldName(value: string): string {
|
|
18
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sanitizeFilterHints(rawFilter?: Record<string, unknown>): QueryFieldHint[] {
|
|
22
|
+
if (!Array.isArray(rawFilter?.__fieldHints)) return [];
|
|
23
|
+
|
|
24
|
+
return (rawFilter.__fieldHints as unknown[])
|
|
25
|
+
.filter((hint): hint is QueryFieldHint => typeof hint === 'object' && hint !== null)
|
|
26
|
+
.map((hint) => ({
|
|
27
|
+
value: typeof hint.value === 'string' ? hint.value.trim().toLowerCase() : '',
|
|
28
|
+
...(typeof hint.field === 'string' && hint.field.trim()
|
|
29
|
+
? { field: normalizeFieldName(hint.field) }
|
|
30
|
+
: {}),
|
|
31
|
+
}))
|
|
32
|
+
.filter((hint) => Boolean(hint.value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stripInternalFilterKeys(filter?: Record<string, unknown>): Record<string, unknown> {
|
|
36
|
+
if (!filter) return {};
|
|
37
|
+
|
|
38
|
+
return Object.fromEntries(
|
|
39
|
+
Object.entries(filter).filter(([key]) => !key.startsWith('__'))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
9
43
|
/**
|
|
10
44
|
* MultiTablePostgresProvider — PostgreSQL implementation that searches across
|
|
11
45
|
* multiple existing tables with pre-existing embeddings.
|
|
@@ -19,6 +53,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
19
53
|
private readonly dimensions: number;
|
|
20
54
|
private readonly connectionString: string;
|
|
21
55
|
private tables: string[];
|
|
56
|
+
private tableSearchConfig = new Map<string, TableSearchConfig>();
|
|
22
57
|
|
|
23
58
|
constructor(config: VectorDBConfig) {
|
|
24
59
|
super(config);
|
|
@@ -79,6 +114,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
79
114
|
if (this.tables.length === 0) {
|
|
80
115
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
81
116
|
} else {
|
|
117
|
+
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
82
118
|
console.log(
|
|
83
119
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(', ')}`
|
|
84
120
|
);
|
|
@@ -127,28 +163,73 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
127
163
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
128
164
|
|
|
129
165
|
const queryText = _filter?.__queryText as string | undefined;
|
|
166
|
+
const fieldHints = sanitizeFilterHints(_filter);
|
|
167
|
+
const explicitFilter = stripInternalFilterKeys(_filter);
|
|
130
168
|
|
|
131
169
|
const queryPromises = this.tables.map(async (table) => {
|
|
132
170
|
try {
|
|
133
171
|
let sqlQuery = '';
|
|
134
172
|
let params: unknown[] = [];
|
|
173
|
+
const tableConfig = this.tableSearchConfig.get(table);
|
|
174
|
+
const availableFields = tableConfig?.availableFields ?? [];
|
|
175
|
+
const searchableFields = tableConfig?.searchableFields ?? [];
|
|
176
|
+
const normalizedFieldMap = tableConfig?.normalizedFieldMap ?? new Map<string, string>();
|
|
177
|
+
const whereConditions: string[] = [];
|
|
135
178
|
|
|
136
179
|
if (queryText) {
|
|
180
|
+
const genericValueHints = fieldHints
|
|
181
|
+
.filter((hint) => !hint.field)
|
|
182
|
+
.map((hint) => hint.value);
|
|
183
|
+
|
|
184
|
+
params = genericValueHints.length > 0
|
|
185
|
+
? [vectorLiteral, queryText, genericValueHints]
|
|
186
|
+
: [vectorLiteral, queryText];
|
|
187
|
+
|
|
188
|
+
const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
|
|
189
|
+
|
|
190
|
+
const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0
|
|
191
|
+
? `GREATEST(${searchableFields.map((field) =>
|
|
192
|
+
`CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
|
|
193
|
+
).join(', ')})`
|
|
194
|
+
: '0';
|
|
195
|
+
|
|
196
|
+
const fieldSpecificChecks = fieldHints
|
|
197
|
+
.filter((hint) => hint.field)
|
|
198
|
+
.map((hint) => {
|
|
199
|
+
const column = hint.field ? normalizedFieldMap.get(hint.field) : undefined;
|
|
200
|
+
if (!column) return null;
|
|
201
|
+
params.push(hint.value);
|
|
202
|
+
const paramIndex = params.length;
|
|
203
|
+
return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
|
|
204
|
+
})
|
|
205
|
+
.filter((expr): expr is string => Boolean(expr));
|
|
206
|
+
|
|
207
|
+
const exactFieldScoreExpr = fieldSpecificChecks.length > 0
|
|
208
|
+
? `GREATEST(${fieldSpecificChecks.join(', ')})`
|
|
209
|
+
: '0';
|
|
210
|
+
|
|
137
211
|
// Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
|
|
138
212
|
// We convert plainto_tsquery's AND (&) operator to OR (|) so that natural language queries
|
|
139
|
-
// (which contain words
|
|
213
|
+
// (which contain words that are not present verbatim in the row text) still match the intended records.
|
|
140
214
|
sqlQuery = `
|
|
141
215
|
SELECT *,
|
|
142
216
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
143
217
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
144
|
-
|
|
218
|
+
${exactValueScoreExpr} AS exact_value_score,
|
|
219
|
+
${exactFieldScoreExpr} AS exact_field_score,
|
|
220
|
+
(
|
|
221
|
+
(1 - (embedding <=> $1::vector)) +
|
|
222
|
+
(COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) +
|
|
223
|
+
(${exactValueScoreExpr} * 3.0) +
|
|
224
|
+
(${exactFieldScoreExpr} * 5.0)
|
|
225
|
+
) AS hybrid_score
|
|
145
226
|
FROM "${table}" t
|
|
146
227
|
ORDER BY hybrid_score DESC
|
|
147
228
|
LIMIT 50
|
|
148
229
|
`;
|
|
149
|
-
params = [vectorLiteral, queryText];
|
|
150
230
|
} else {
|
|
151
231
|
// Fallback to Vector-only Search
|
|
232
|
+
params = [vectorLiteral];
|
|
152
233
|
sqlQuery = `
|
|
153
234
|
SELECT *,
|
|
154
235
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -156,7 +237,19 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
156
237
|
ORDER BY hybrid_score DESC
|
|
157
238
|
LIMIT 50
|
|
158
239
|
`;
|
|
159
|
-
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const [key, value] of Object.entries(explicitFilter)) {
|
|
243
|
+
const column = availableFields.includes(key)
|
|
244
|
+
? key
|
|
245
|
+
: normalizedFieldMap.get(normalizeFieldName(key));
|
|
246
|
+
if (!column) continue;
|
|
247
|
+
params.push(String(value));
|
|
248
|
+
whereConditions.push(`COALESCE(to_jsonb(t)->>'${column}', '') = $${params.length}::text`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (whereConditions.length > 0) {
|
|
252
|
+
sqlQuery = sqlQuery.replace(`FROM "${table}" t`, `FROM "${table}" t\n WHERE ${whereConditions.join(' AND ')}`);
|
|
160
253
|
}
|
|
161
254
|
|
|
162
255
|
const result = await this.pool.query(sqlQuery, params);
|
|
@@ -173,6 +266,8 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
173
266
|
delete rest.embedding;
|
|
174
267
|
delete rest.vector_score;
|
|
175
268
|
delete rest.keyword_score;
|
|
269
|
+
delete rest.exact_value_score;
|
|
270
|
+
delete rest.exact_field_score;
|
|
176
271
|
|
|
177
272
|
const content = `[TYPE: ${table.replace(/s$/, '').toUpperCase()}]\n` +
|
|
178
273
|
Object.entries(rest)
|
|
@@ -202,32 +297,14 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
202
297
|
allResults.push(...tableResults);
|
|
203
298
|
}
|
|
204
299
|
|
|
205
|
-
|
|
206
|
-
const resultsByTable: Record<string, VectorMatch[]> = {};
|
|
207
|
-
for (const res of allResults) {
|
|
208
|
-
const table = res.metadata?.source_table as string;
|
|
209
|
-
if (!resultsByTable[table]) resultsByTable[table] = [];
|
|
210
|
-
resultsByTable[table].push(res);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// 2. Take top 3 from EACH table guaranteed, then fill the rest with overall best
|
|
214
|
-
const balancedResults: VectorMatch[] = [];
|
|
215
|
-
const tables = Object.keys(resultsByTable);
|
|
216
|
-
|
|
217
|
-
// First, ensure every table's best match is included
|
|
218
|
-
for (const table of tables) {
|
|
219
|
-
balancedResults.push(...resultsByTable[table].slice(0, 3));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 3. Sort the balanced list by score
|
|
223
|
-
const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
|
|
300
|
+
const finalSorted = allResults.sort((a, b) => b.score - a.score);
|
|
224
301
|
|
|
225
302
|
if (finalSorted.length > 0) {
|
|
226
303
|
console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
|
|
227
304
|
console.log(`[MultiTablePostgresProvider] Final top match from "${finalSorted[0].metadata?.source_table ?? 'unknown'}" with score ${finalSorted[0].score.toFixed(4)}`);
|
|
228
305
|
}
|
|
229
306
|
|
|
230
|
-
return finalSorted.slice(0,
|
|
307
|
+
return finalSorted.slice(0, topK);
|
|
231
308
|
}
|
|
232
309
|
|
|
233
310
|
async delete(_id: string | number, _namespace?: string): Promise<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
@@ -252,4 +329,67 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
252
329
|
await this.pool.end();
|
|
253
330
|
}
|
|
254
331
|
}
|
|
332
|
+
|
|
333
|
+
private async loadTableSearchConfig(
|
|
334
|
+
client: PoolClient,
|
|
335
|
+
tables: string[]
|
|
336
|
+
): Promise<Map<string, TableSearchConfig>> {
|
|
337
|
+
if (tables.length === 0) return new Map();
|
|
338
|
+
|
|
339
|
+
const configuredSearchFields = this.parseConfiguredSearchFields();
|
|
340
|
+
const result = await client.query(`
|
|
341
|
+
SELECT table_name, column_name, data_type
|
|
342
|
+
FROM information_schema.columns
|
|
343
|
+
WHERE table_schema = 'public'
|
|
344
|
+
AND table_name = ANY($1::text[])
|
|
345
|
+
ORDER BY ordinal_position
|
|
346
|
+
`, [tables]);
|
|
347
|
+
|
|
348
|
+
const rowsByTable = new Map<string, Array<{ column_name: string; data_type: string }>>();
|
|
349
|
+
for (const row of result.rows as Array<{ table_name: string; column_name: string; data_type: string }>) {
|
|
350
|
+
const rows = rowsByTable.get(row.table_name) ?? [];
|
|
351
|
+
rows.push({ column_name: row.column_name, data_type: row.data_type });
|
|
352
|
+
rowsByTable.set(row.table_name, rows);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const configByTable = new Map<string, TableSearchConfig>();
|
|
356
|
+
for (const table of tables) {
|
|
357
|
+
const columns = rowsByTable.get(table) ?? [];
|
|
358
|
+
const availableFields = columns
|
|
359
|
+
.filter(({ column_name }) => column_name !== 'embedding')
|
|
360
|
+
.map(({ column_name }) => column_name);
|
|
361
|
+
|
|
362
|
+
const inferredFields = columns
|
|
363
|
+
.filter(({ column_name, data_type }) =>
|
|
364
|
+
column_name !== 'embedding' &&
|
|
365
|
+
!['ARRAY', 'json', 'jsonb', 'bytea', 'tsvector', 'USER-DEFINED'].includes(data_type)
|
|
366
|
+
)
|
|
367
|
+
.map(({ column_name }) => column_name);
|
|
368
|
+
|
|
369
|
+
const searchableFields = configuredSearchFields.length > 0
|
|
370
|
+
? configuredSearchFields.filter((field) => columns.some((column) => column.column_name === field))
|
|
371
|
+
: inferredFields;
|
|
372
|
+
|
|
373
|
+
const normalizedFieldMap = new Map<string, string>();
|
|
374
|
+
for (const field of availableFields) {
|
|
375
|
+
normalizedFieldMap.set(normalizeFieldName(field), field);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
configByTable.set(table, {
|
|
379
|
+
availableFields,
|
|
380
|
+
searchableFields,
|
|
381
|
+
normalizedFieldMap,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return configByTable;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private parseConfiguredSearchFields(): string[] {
|
|
389
|
+
const raw = this.config.options?.searchFields;
|
|
390
|
+
if (Array.isArray(raw)) {
|
|
391
|
+
return raw.filter((field): field is string => typeof field === 'string' && field.trim().length > 0);
|
|
392
|
+
}
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
255
395
|
}
|
|
@@ -3,6 +3,14 @@ import { VectorDBConfig } from '../../config/RagConfig';
|
|
|
3
3
|
import { BaseVectorProvider } from './BaseVectorProvider';
|
|
4
4
|
import { VectorMatch, UpsertDocument } from '../../types';
|
|
5
5
|
|
|
6
|
+
function stripInternalFilterKeys(filter?: Record<string, unknown>): Record<string, unknown> {
|
|
7
|
+
if (!filter) return {};
|
|
8
|
+
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(filter).filter(([key]) => !key.startsWith('__'))
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* PostgreSQLProvider — PostgreSQL implementation using the pgvector extension.
|
|
8
16
|
*/
|
|
@@ -103,8 +111,9 @@ export class PostgreSQLProvider extends BaseVectorProvider {
|
|
|
103
111
|
const params: unknown[] = [vectorLiteral, topK];
|
|
104
112
|
if (namespace) params.push(namespace);
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
const publicFilter = stripInternalFilterKeys(filter);
|
|
115
|
+
if (Object.keys(publicFilter).length > 0) {
|
|
116
|
+
const filterConditions = Object.entries(publicFilter)
|
|
108
117
|
.map(([key, val]) => {
|
|
109
118
|
const paramIdx = params.length + 1;
|
|
110
119
|
params.push(JSON.stringify(val));
|