@retrivora-ai/rag-engine 0.2.7 → 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-6Q7DNWTG.mjs → chunk-CYVPACA7.mjs} +33 -19
- 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 +48 -20
- 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 +153 -31
- package/dist/server.mjs +108 -14
- package/package.json +1 -1
- package/src/config/RagConfig.ts +4 -0
- package/src/core/Pipeline.ts +46 -17
- package/src/providers/vectordb/MongoDBProvider.ts +12 -1
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +161 -19
- package/src/providers/vectordb/PostgreSQLProvider.ts +11 -2
|
@@ -3,7 +3,42 @@ import { BaseVectorProvider } from './BaseVectorProvider';
|
|
|
3
3
|
import { VectorMatch, UpsertDocument } from '../../types';
|
|
4
4
|
import { VectorDBConfig } from '../../config/RagConfig';
|
|
5
5
|
|
|
6
|
-
|
|
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
|
+
}
|
|
7
42
|
|
|
8
43
|
/**
|
|
9
44
|
* MultiTablePostgresProvider — PostgreSQL implementation that searches across
|
|
@@ -18,6 +53,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
18
53
|
private readonly dimensions: number;
|
|
19
54
|
private readonly connectionString: string;
|
|
20
55
|
private tables: string[];
|
|
56
|
+
private tableSearchConfig = new Map<string, TableSearchConfig>();
|
|
21
57
|
|
|
22
58
|
constructor(config: VectorDBConfig) {
|
|
23
59
|
super(config);
|
|
@@ -78,6 +114,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
78
114
|
if (this.tables.length === 0) {
|
|
79
115
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
80
116
|
} else {
|
|
117
|
+
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
81
118
|
console.log(
|
|
82
119
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(', ')}`
|
|
83
120
|
);
|
|
@@ -126,44 +163,73 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
126
163
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
127
164
|
|
|
128
165
|
const queryText = _filter?.__queryText as string | undefined;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
.filter((hint): hint is string => typeof hint === 'string')
|
|
132
|
-
.map((hint) => hint.trim().toLowerCase())
|
|
133
|
-
.filter(Boolean)
|
|
134
|
-
: [];
|
|
166
|
+
const fieldHints = sanitizeFilterHints(_filter);
|
|
167
|
+
const explicitFilter = stripInternalFilterKeys(_filter);
|
|
135
168
|
|
|
136
169
|
const queryPromises = this.tables.map(async (table) => {
|
|
137
170
|
try {
|
|
138
171
|
let sqlQuery = '';
|
|
139
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[] = [];
|
|
140
178
|
|
|
141
179
|
if (queryText) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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`
|
|
146
193
|
).join(', ')})`
|
|
147
194
|
: '0';
|
|
148
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
|
+
|
|
149
211
|
// Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
|
|
150
212
|
// We convert plainto_tsquery's AND (&) operator to OR (|) so that natural language queries
|
|
151
|
-
// (which contain words
|
|
213
|
+
// (which contain words that are not present verbatim in the row text) still match the intended records.
|
|
152
214
|
sqlQuery = `
|
|
153
215
|
SELECT *,
|
|
154
216
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
155
217
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
156
|
-
${
|
|
157
|
-
|
|
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
|
|
158
226
|
FROM "${table}" t
|
|
159
227
|
ORDER BY hybrid_score DESC
|
|
160
228
|
LIMIT 50
|
|
161
229
|
`;
|
|
162
|
-
params = hasEntityHints
|
|
163
|
-
? [vectorLiteral, queryText, entityHints]
|
|
164
|
-
: [vectorLiteral, queryText];
|
|
165
230
|
} else {
|
|
166
231
|
// Fallback to Vector-only Search
|
|
232
|
+
params = [vectorLiteral];
|
|
167
233
|
sqlQuery = `
|
|
168
234
|
SELECT *,
|
|
169
235
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -171,7 +237,19 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
171
237
|
ORDER BY hybrid_score DESC
|
|
172
238
|
LIMIT 50
|
|
173
239
|
`;
|
|
174
|
-
|
|
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 ')}`);
|
|
175
253
|
}
|
|
176
254
|
|
|
177
255
|
const result = await this.pool.query(sqlQuery, params);
|
|
@@ -188,7 +266,8 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
188
266
|
delete rest.embedding;
|
|
189
267
|
delete rest.vector_score;
|
|
190
268
|
delete rest.keyword_score;
|
|
191
|
-
delete rest.
|
|
269
|
+
delete rest.exact_value_score;
|
|
270
|
+
delete rest.exact_field_score;
|
|
192
271
|
|
|
193
272
|
const content = `[TYPE: ${table.replace(/s$/, '').toUpperCase()}]\n` +
|
|
194
273
|
Object.entries(rest)
|
|
@@ -250,4 +329,67 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
250
329
|
await this.pool.end();
|
|
251
330
|
}
|
|
252
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
|
+
}
|
|
253
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));
|