@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
package/dist/server.js
CHANGED
|
@@ -217,6 +217,12 @@ var PostgreSQLProvider_exports = {};
|
|
|
217
217
|
__export(PostgreSQLProvider_exports, {
|
|
218
218
|
PostgreSQLProvider: () => PostgreSQLProvider
|
|
219
219
|
});
|
|
220
|
+
function stripInternalFilterKeys(filter) {
|
|
221
|
+
if (!filter) return {};
|
|
222
|
+
return Object.fromEntries(
|
|
223
|
+
Object.entries(filter).filter(([key]) => !key.startsWith("__"))
|
|
224
|
+
);
|
|
225
|
+
}
|
|
220
226
|
var import_pg, PostgreSQLProvider;
|
|
221
227
|
var init_PostgreSQLProvider = __esm({
|
|
222
228
|
"src/providers/vectordb/PostgreSQLProvider.ts"() {
|
|
@@ -309,8 +315,9 @@ var init_PostgreSQLProvider = __esm({
|
|
|
309
315
|
let whereClause = namespace ? `WHERE namespace = $3` : "";
|
|
310
316
|
const params = [vectorLiteral, topK];
|
|
311
317
|
if (namespace) params.push(namespace);
|
|
312
|
-
|
|
313
|
-
|
|
318
|
+
const publicFilter = stripInternalFilterKeys(filter);
|
|
319
|
+
if (Object.keys(publicFilter).length > 0) {
|
|
320
|
+
const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
|
|
314
321
|
const paramIdx = params.length + 1;
|
|
315
322
|
params.push(JSON.stringify(val));
|
|
316
323
|
return `metadata->>'${key}' = $${paramIdx}`;
|
|
@@ -360,6 +367,12 @@ var MongoDBProvider_exports = {};
|
|
|
360
367
|
__export(MongoDBProvider_exports, {
|
|
361
368
|
MongoDBProvider: () => MongoDBProvider
|
|
362
369
|
});
|
|
370
|
+
function stripInternalFilterKeys2(filter) {
|
|
371
|
+
if (!filter) return {};
|
|
372
|
+
return Object.fromEntries(
|
|
373
|
+
Object.entries(filter).filter(([key]) => !key.startsWith("__"))
|
|
374
|
+
);
|
|
375
|
+
}
|
|
363
376
|
var import_mongodb, MongoDBProvider;
|
|
364
377
|
var init_MongoDBProvider = __esm({
|
|
365
378
|
"src/providers/vectordb/MongoDBProvider.ts"() {
|
|
@@ -411,6 +424,7 @@ var init_MongoDBProvider = __esm({
|
|
|
411
424
|
await this.collection.bulkWrite(operations);
|
|
412
425
|
}
|
|
413
426
|
async query(vector, topK, namespace, filter) {
|
|
427
|
+
const publicFilter = stripInternalFilterKeys2(filter);
|
|
414
428
|
const pipeline = [
|
|
415
429
|
{
|
|
416
430
|
$vectorSearch: __spreadValues({
|
|
@@ -419,7 +433,7 @@ var init_MongoDBProvider = __esm({
|
|
|
419
433
|
queryVector: vector,
|
|
420
434
|
numCandidates: Math.max(topK * 10, 100),
|
|
421
435
|
limit: topK
|
|
422
|
-
},
|
|
436
|
+
}, Object.keys(publicFilter).length > 0 || namespace ? { filter: __spreadValues(__spreadValues({}, publicFilter), namespace ? { namespace } : {}) } : {})
|
|
423
437
|
},
|
|
424
438
|
{
|
|
425
439
|
$project: {
|
|
@@ -2752,25 +2766,39 @@ var EmbeddingStrategyResolver = class {
|
|
|
2752
2766
|
};
|
|
2753
2767
|
|
|
2754
2768
|
// src/core/Pipeline.ts
|
|
2755
|
-
function
|
|
2756
|
-
|
|
2757
|
-
|
|
2769
|
+
function normalizeHintValue(value) {
|
|
2770
|
+
return value.replace(/\s+/g, " ").trim();
|
|
2771
|
+
}
|
|
2772
|
+
function extractQueryFieldHints(question) {
|
|
2773
|
+
if (!question.trim()) return [];
|
|
2774
|
+
const hints = /* @__PURE__ */ new Map();
|
|
2775
|
+
const addHint = (value, field) => {
|
|
2776
|
+
const normalizedValue = normalizeHintValue(value);
|
|
2777
|
+
if (!normalizedValue) return;
|
|
2778
|
+
const normalizedField = field ? field.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() : void 0;
|
|
2779
|
+
const key = `${normalizedField != null ? normalizedField : "*"}::${normalizedValue.toLowerCase()}`;
|
|
2780
|
+
if (!hints.has(key)) {
|
|
2781
|
+
hints.set(key, __spreadValues({
|
|
2782
|
+
value: normalizedValue
|
|
2783
|
+
}, normalizedField ? { field: normalizedField } : {}));
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2758
2786
|
for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
}
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
|
|
2787
|
+
addHint(match[1]);
|
|
2788
|
+
}
|
|
2789
|
+
const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
|
|
2790
|
+
const valuePattern = `([^\\n?.!,]{1,120}?)`;
|
|
2791
|
+
const fieldValuePatterns = [
|
|
2792
|
+
new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
2793
|
+
new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
2794
|
+
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
|
|
2765
2795
|
];
|
|
2766
|
-
for (const pattern of
|
|
2767
|
-
const match
|
|
2768
|
-
|
|
2769
|
-
if (value) {
|
|
2770
|
-
hints.add(value);
|
|
2796
|
+
for (const pattern of fieldValuePatterns) {
|
|
2797
|
+
for (const match of question.matchAll(pattern)) {
|
|
2798
|
+
addHint(match[2], match[1]);
|
|
2771
2799
|
}
|
|
2772
2800
|
}
|
|
2773
|
-
return [...hints
|
|
2801
|
+
return [...hints.values()];
|
|
2774
2802
|
}
|
|
2775
2803
|
var Pipeline = class {
|
|
2776
2804
|
constructor(config) {
|
|
@@ -2859,10 +2887,10 @@ var Pipeline = class {
|
|
|
2859
2887
|
const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
|
|
2860
2888
|
try {
|
|
2861
2889
|
const queryVector = await this.embeddingProvider.embed(question, { taskType: "query" });
|
|
2862
|
-
const
|
|
2890
|
+
const fieldHints = extractQueryFieldHints(question);
|
|
2863
2891
|
const rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
|
|
2864
2892
|
__queryText: question,
|
|
2865
|
-
|
|
2893
|
+
__fieldHints: fieldHints
|
|
2866
2894
|
});
|
|
2867
2895
|
const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
|
|
2868
2896
|
const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
@@ -3716,11 +3744,26 @@ init_PostgreSQLProvider();
|
|
|
3716
3744
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
3717
3745
|
var import_pg2 = require("pg");
|
|
3718
3746
|
init_BaseVectorProvider();
|
|
3719
|
-
|
|
3747
|
+
function normalizeFieldName(value) {
|
|
3748
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
3749
|
+
}
|
|
3750
|
+
function sanitizeFilterHints(rawFilter) {
|
|
3751
|
+
if (!Array.isArray(rawFilter == null ? void 0 : rawFilter.__fieldHints)) return [];
|
|
3752
|
+
return rawFilter.__fieldHints.filter((hint) => typeof hint === "object" && hint !== null).map((hint) => __spreadValues({
|
|
3753
|
+
value: typeof hint.value === "string" ? hint.value.trim().toLowerCase() : ""
|
|
3754
|
+
}, typeof hint.field === "string" && hint.field.trim() ? { field: normalizeFieldName(hint.field) } : {})).filter((hint) => Boolean(hint.value));
|
|
3755
|
+
}
|
|
3756
|
+
function stripInternalFilterKeys3(filter) {
|
|
3757
|
+
if (!filter) return {};
|
|
3758
|
+
return Object.fromEntries(
|
|
3759
|
+
Object.entries(filter).filter(([key]) => !key.startsWith("__"))
|
|
3760
|
+
);
|
|
3761
|
+
}
|
|
3720
3762
|
var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
3721
3763
|
constructor(config) {
|
|
3722
3764
|
var _a, _b, _c;
|
|
3723
3765
|
super(config);
|
|
3766
|
+
this.tableSearchConfig = /* @__PURE__ */ new Map();
|
|
3724
3767
|
const opts = config.options || {};
|
|
3725
3768
|
if (!opts.connectionString) {
|
|
3726
3769
|
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
@@ -3759,6 +3802,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3759
3802
|
if (this.tables.length === 0) {
|
|
3760
3803
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
3761
3804
|
} else {
|
|
3805
|
+
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
3762
3806
|
console.log(
|
|
3763
3807
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
3764
3808
|
);
|
|
@@ -3796,28 +3840,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3796
3840
|
const allResults = [];
|
|
3797
3841
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
3798
3842
|
const queryText = _filter == null ? void 0 : _filter.__queryText;
|
|
3799
|
-
const
|
|
3843
|
+
const fieldHints = sanitizeFilterHints(_filter);
|
|
3844
|
+
const explicitFilter = stripInternalFilterKeys3(_filter);
|
|
3800
3845
|
const queryPromises = this.tables.map(async (table) => {
|
|
3846
|
+
var _a2, _b2, _c;
|
|
3801
3847
|
try {
|
|
3802
3848
|
let sqlQuery = "";
|
|
3803
3849
|
let params = [];
|
|
3850
|
+
const tableConfig = this.tableSearchConfig.get(table);
|
|
3851
|
+
const availableFields = (_a2 = tableConfig == null ? void 0 : tableConfig.availableFields) != null ? _a2 : [];
|
|
3852
|
+
const searchableFields = (_b2 = tableConfig == null ? void 0 : tableConfig.searchableFields) != null ? _b2 : [];
|
|
3853
|
+
const normalizedFieldMap = (_c = tableConfig == null ? void 0 : tableConfig.normalizedFieldMap) != null ? _c : /* @__PURE__ */ new Map();
|
|
3854
|
+
const whereConditions = [];
|
|
3804
3855
|
if (queryText) {
|
|
3805
|
-
const
|
|
3806
|
-
|
|
3807
|
-
|
|
3856
|
+
const genericValueHints = fieldHints.filter((hint) => !hint.field).map((hint) => hint.value);
|
|
3857
|
+
params = genericValueHints.length > 0 ? [vectorLiteral, queryText, genericValueHints] : [vectorLiteral, queryText];
|
|
3858
|
+
const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
|
|
3859
|
+
const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0 ? `GREATEST(${searchableFields.map(
|
|
3860
|
+
(field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
|
|
3808
3861
|
).join(", ")})` : "0";
|
|
3862
|
+
const fieldSpecificChecks = fieldHints.filter((hint) => hint.field).map((hint) => {
|
|
3863
|
+
const column = hint.field ? normalizedFieldMap.get(hint.field) : void 0;
|
|
3864
|
+
if (!column) return null;
|
|
3865
|
+
params.push(hint.value);
|
|
3866
|
+
const paramIndex = params.length;
|
|
3867
|
+
return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
|
|
3868
|
+
}).filter((expr) => Boolean(expr));
|
|
3869
|
+
const exactFieldScoreExpr = fieldSpecificChecks.length > 0 ? `GREATEST(${fieldSpecificChecks.join(", ")})` : "0";
|
|
3809
3870
|
sqlQuery = `
|
|
3810
3871
|
SELECT *,
|
|
3811
3872
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
3812
3873
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
3813
|
-
${
|
|
3814
|
-
|
|
3874
|
+
${exactValueScoreExpr} AS exact_value_score,
|
|
3875
|
+
${exactFieldScoreExpr} AS exact_field_score,
|
|
3876
|
+
(
|
|
3877
|
+
(1 - (embedding <=> $1::vector)) +
|
|
3878
|
+
(COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) +
|
|
3879
|
+
(${exactValueScoreExpr} * 3.0) +
|
|
3880
|
+
(${exactFieldScoreExpr} * 5.0)
|
|
3881
|
+
) AS hybrid_score
|
|
3815
3882
|
FROM "${table}" t
|
|
3816
3883
|
ORDER BY hybrid_score DESC
|
|
3817
3884
|
LIMIT 50
|
|
3818
3885
|
`;
|
|
3819
|
-
params = hasEntityHints ? [vectorLiteral, queryText, entityHints] : [vectorLiteral, queryText];
|
|
3820
3886
|
} else {
|
|
3887
|
+
params = [vectorLiteral];
|
|
3821
3888
|
sqlQuery = `
|
|
3822
3889
|
SELECT *,
|
|
3823
3890
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -3825,7 +3892,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3825
3892
|
ORDER BY hybrid_score DESC
|
|
3826
3893
|
LIMIT 50
|
|
3827
3894
|
`;
|
|
3828
|
-
|
|
3895
|
+
}
|
|
3896
|
+
for (const [key, value] of Object.entries(explicitFilter)) {
|
|
3897
|
+
const column = availableFields.includes(key) ? key : normalizedFieldMap.get(normalizeFieldName(key));
|
|
3898
|
+
if (!column) continue;
|
|
3899
|
+
params.push(String(value));
|
|
3900
|
+
whereConditions.push(`COALESCE(to_jsonb(t)->>'${column}', '') = $${params.length}::text`);
|
|
3901
|
+
}
|
|
3902
|
+
if (whereConditions.length > 0) {
|
|
3903
|
+
sqlQuery = sqlQuery.replace(`FROM "${table}" t`, `FROM "${table}" t
|
|
3904
|
+
WHERE ${whereConditions.join(" AND ")}`);
|
|
3829
3905
|
}
|
|
3830
3906
|
const result = await this.pool.query(sqlQuery, params);
|
|
3831
3907
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -3835,11 +3911,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3835
3911
|
}
|
|
3836
3912
|
const tableResults = [];
|
|
3837
3913
|
for (const row of result.rows) {
|
|
3838
|
-
const
|
|
3914
|
+
const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
|
|
3839
3915
|
delete rest.embedding;
|
|
3840
3916
|
delete rest.vector_score;
|
|
3841
3917
|
delete rest.keyword_score;
|
|
3842
|
-
delete rest.
|
|
3918
|
+
delete rest.exact_value_score;
|
|
3919
|
+
delete rest.exact_field_score;
|
|
3843
3920
|
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
3844
3921
|
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
3845
3922
|
tableResults.push({
|
|
@@ -3888,6 +3965,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3888
3965
|
await this.pool.end();
|
|
3889
3966
|
}
|
|
3890
3967
|
}
|
|
3968
|
+
async loadTableSearchConfig(client, tables) {
|
|
3969
|
+
var _a, _b;
|
|
3970
|
+
if (tables.length === 0) return /* @__PURE__ */ new Map();
|
|
3971
|
+
const configuredSearchFields = this.parseConfiguredSearchFields();
|
|
3972
|
+
const result = await client.query(`
|
|
3973
|
+
SELECT table_name, column_name, data_type
|
|
3974
|
+
FROM information_schema.columns
|
|
3975
|
+
WHERE table_schema = 'public'
|
|
3976
|
+
AND table_name = ANY($1::text[])
|
|
3977
|
+
ORDER BY ordinal_position
|
|
3978
|
+
`, [tables]);
|
|
3979
|
+
const rowsByTable = /* @__PURE__ */ new Map();
|
|
3980
|
+
for (const row of result.rows) {
|
|
3981
|
+
const rows = (_a = rowsByTable.get(row.table_name)) != null ? _a : [];
|
|
3982
|
+
rows.push({ column_name: row.column_name, data_type: row.data_type });
|
|
3983
|
+
rowsByTable.set(row.table_name, rows);
|
|
3984
|
+
}
|
|
3985
|
+
const configByTable = /* @__PURE__ */ new Map();
|
|
3986
|
+
for (const table of tables) {
|
|
3987
|
+
const columns = (_b = rowsByTable.get(table)) != null ? _b : [];
|
|
3988
|
+
const availableFields = columns.filter(({ column_name }) => column_name !== "embedding").map(({ column_name }) => column_name);
|
|
3989
|
+
const inferredFields = columns.filter(
|
|
3990
|
+
({ column_name, data_type }) => column_name !== "embedding" && !["ARRAY", "json", "jsonb", "bytea", "tsvector", "USER-DEFINED"].includes(data_type)
|
|
3991
|
+
).map(({ column_name }) => column_name);
|
|
3992
|
+
const searchableFields = configuredSearchFields.length > 0 ? configuredSearchFields.filter((field) => columns.some((column) => column.column_name === field)) : inferredFields;
|
|
3993
|
+
const normalizedFieldMap = /* @__PURE__ */ new Map();
|
|
3994
|
+
for (const field of availableFields) {
|
|
3995
|
+
normalizedFieldMap.set(normalizeFieldName(field), field);
|
|
3996
|
+
}
|
|
3997
|
+
configByTable.set(table, {
|
|
3998
|
+
availableFields,
|
|
3999
|
+
searchableFields,
|
|
4000
|
+
normalizedFieldMap
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
return configByTable;
|
|
4004
|
+
}
|
|
4005
|
+
parseConfiguredSearchFields() {
|
|
4006
|
+
var _a;
|
|
4007
|
+
const raw = (_a = this.config.options) == null ? void 0 : _a.searchFields;
|
|
4008
|
+
if (Array.isArray(raw)) {
|
|
4009
|
+
return raw.filter((field) => typeof field === "string" && field.trim().length > 0);
|
|
4010
|
+
}
|
|
4011
|
+
return [];
|
|
4012
|
+
}
|
|
3891
4013
|
};
|
|
3892
4014
|
|
|
3893
4015
|
// src/server.ts
|
package/dist/server.mjs
CHANGED
|
@@ -34,17 +34,17 @@ import {
|
|
|
34
34
|
createIngestHandler,
|
|
35
35
|
createUploadHandler,
|
|
36
36
|
getRagConfig
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-CYVPACA7.mjs";
|
|
38
38
|
import "./chunk-EDLTMSNY.mjs";
|
|
39
39
|
import {
|
|
40
40
|
PineconeProvider
|
|
41
41
|
} from "./chunk-BMHJTWSU.mjs";
|
|
42
42
|
import {
|
|
43
43
|
PostgreSQLProvider
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-6GSARSCP.mjs";
|
|
45
45
|
import {
|
|
46
46
|
MongoDBProvider
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-IFPISZ2S.mjs";
|
|
48
48
|
import {
|
|
49
49
|
MilvusProvider
|
|
50
50
|
} from "./chunk-VEJNRS4B.mjs";
|
|
@@ -335,11 +335,26 @@ function createFromPreset(presetName) {
|
|
|
335
335
|
|
|
336
336
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
337
337
|
import { Pool } from "pg";
|
|
338
|
-
|
|
338
|
+
function normalizeFieldName(value) {
|
|
339
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
340
|
+
}
|
|
341
|
+
function sanitizeFilterHints(rawFilter) {
|
|
342
|
+
if (!Array.isArray(rawFilter == null ? void 0 : rawFilter.__fieldHints)) return [];
|
|
343
|
+
return rawFilter.__fieldHints.filter((hint) => typeof hint === "object" && hint !== null).map((hint) => __spreadValues({
|
|
344
|
+
value: typeof hint.value === "string" ? hint.value.trim().toLowerCase() : ""
|
|
345
|
+
}, typeof hint.field === "string" && hint.field.trim() ? { field: normalizeFieldName(hint.field) } : {})).filter((hint) => Boolean(hint.value));
|
|
346
|
+
}
|
|
347
|
+
function stripInternalFilterKeys(filter) {
|
|
348
|
+
if (!filter) return {};
|
|
349
|
+
return Object.fromEntries(
|
|
350
|
+
Object.entries(filter).filter(([key]) => !key.startsWith("__"))
|
|
351
|
+
);
|
|
352
|
+
}
|
|
339
353
|
var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
340
354
|
constructor(config) {
|
|
341
355
|
var _a, _b, _c;
|
|
342
356
|
super(config);
|
|
357
|
+
this.tableSearchConfig = /* @__PURE__ */ new Map();
|
|
343
358
|
const opts = config.options || {};
|
|
344
359
|
if (!opts.connectionString) {
|
|
345
360
|
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
@@ -378,6 +393,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
378
393
|
if (this.tables.length === 0) {
|
|
379
394
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
380
395
|
} else {
|
|
396
|
+
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
381
397
|
console.log(
|
|
382
398
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
383
399
|
);
|
|
@@ -415,28 +431,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
415
431
|
const allResults = [];
|
|
416
432
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
417
433
|
const queryText = _filter == null ? void 0 : _filter.__queryText;
|
|
418
|
-
const
|
|
434
|
+
const fieldHints = sanitizeFilterHints(_filter);
|
|
435
|
+
const explicitFilter = stripInternalFilterKeys(_filter);
|
|
419
436
|
const queryPromises = this.tables.map(async (table) => {
|
|
437
|
+
var _a2, _b2, _c;
|
|
420
438
|
try {
|
|
421
439
|
let sqlQuery = "";
|
|
422
440
|
let params = [];
|
|
441
|
+
const tableConfig = this.tableSearchConfig.get(table);
|
|
442
|
+
const availableFields = (_a2 = tableConfig == null ? void 0 : tableConfig.availableFields) != null ? _a2 : [];
|
|
443
|
+
const searchableFields = (_b2 = tableConfig == null ? void 0 : tableConfig.searchableFields) != null ? _b2 : [];
|
|
444
|
+
const normalizedFieldMap = (_c = tableConfig == null ? void 0 : tableConfig.normalizedFieldMap) != null ? _c : /* @__PURE__ */ new Map();
|
|
445
|
+
const whereConditions = [];
|
|
423
446
|
if (queryText) {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
447
|
+
const genericValueHints = fieldHints.filter((hint) => !hint.field).map((hint) => hint.value);
|
|
448
|
+
params = genericValueHints.length > 0 ? [vectorLiteral, queryText, genericValueHints] : [vectorLiteral, queryText];
|
|
449
|
+
const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
|
|
450
|
+
const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0 ? `GREATEST(${searchableFields.map(
|
|
451
|
+
(field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
|
|
427
452
|
).join(", ")})` : "0";
|
|
453
|
+
const fieldSpecificChecks = fieldHints.filter((hint) => hint.field).map((hint) => {
|
|
454
|
+
const column = hint.field ? normalizedFieldMap.get(hint.field) : void 0;
|
|
455
|
+
if (!column) return null;
|
|
456
|
+
params.push(hint.value);
|
|
457
|
+
const paramIndex = params.length;
|
|
458
|
+
return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
|
|
459
|
+
}).filter((expr) => Boolean(expr));
|
|
460
|
+
const exactFieldScoreExpr = fieldSpecificChecks.length > 0 ? `GREATEST(${fieldSpecificChecks.join(", ")})` : "0";
|
|
428
461
|
sqlQuery = `
|
|
429
462
|
SELECT *,
|
|
430
463
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
431
464
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
432
|
-
${
|
|
433
|
-
|
|
465
|
+
${exactValueScoreExpr} AS exact_value_score,
|
|
466
|
+
${exactFieldScoreExpr} AS exact_field_score,
|
|
467
|
+
(
|
|
468
|
+
(1 - (embedding <=> $1::vector)) +
|
|
469
|
+
(COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) +
|
|
470
|
+
(${exactValueScoreExpr} * 3.0) +
|
|
471
|
+
(${exactFieldScoreExpr} * 5.0)
|
|
472
|
+
) AS hybrid_score
|
|
434
473
|
FROM "${table}" t
|
|
435
474
|
ORDER BY hybrid_score DESC
|
|
436
475
|
LIMIT 50
|
|
437
476
|
`;
|
|
438
|
-
params = hasEntityHints ? [vectorLiteral, queryText, entityHints] : [vectorLiteral, queryText];
|
|
439
477
|
} else {
|
|
478
|
+
params = [vectorLiteral];
|
|
440
479
|
sqlQuery = `
|
|
441
480
|
SELECT *,
|
|
442
481
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -444,7 +483,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
444
483
|
ORDER BY hybrid_score DESC
|
|
445
484
|
LIMIT 50
|
|
446
485
|
`;
|
|
447
|
-
|
|
486
|
+
}
|
|
487
|
+
for (const [key, value] of Object.entries(explicitFilter)) {
|
|
488
|
+
const column = availableFields.includes(key) ? key : normalizedFieldMap.get(normalizeFieldName(key));
|
|
489
|
+
if (!column) continue;
|
|
490
|
+
params.push(String(value));
|
|
491
|
+
whereConditions.push(`COALESCE(to_jsonb(t)->>'${column}', '') = $${params.length}::text`);
|
|
492
|
+
}
|
|
493
|
+
if (whereConditions.length > 0) {
|
|
494
|
+
sqlQuery = sqlQuery.replace(`FROM "${table}" t`, `FROM "${table}" t
|
|
495
|
+
WHERE ${whereConditions.join(" AND ")}`);
|
|
448
496
|
}
|
|
449
497
|
const result = await this.pool.query(sqlQuery, params);
|
|
450
498
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -454,11 +502,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
454
502
|
}
|
|
455
503
|
const tableResults = [];
|
|
456
504
|
for (const row of result.rows) {
|
|
457
|
-
const
|
|
505
|
+
const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
|
|
458
506
|
delete rest.embedding;
|
|
459
507
|
delete rest.vector_score;
|
|
460
508
|
delete rest.keyword_score;
|
|
461
|
-
delete rest.
|
|
509
|
+
delete rest.exact_value_score;
|
|
510
|
+
delete rest.exact_field_score;
|
|
462
511
|
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
463
512
|
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
464
513
|
tableResults.push({
|
|
@@ -507,6 +556,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
507
556
|
await this.pool.end();
|
|
508
557
|
}
|
|
509
558
|
}
|
|
559
|
+
async loadTableSearchConfig(client, tables) {
|
|
560
|
+
var _a, _b;
|
|
561
|
+
if (tables.length === 0) return /* @__PURE__ */ new Map();
|
|
562
|
+
const configuredSearchFields = this.parseConfiguredSearchFields();
|
|
563
|
+
const result = await client.query(`
|
|
564
|
+
SELECT table_name, column_name, data_type
|
|
565
|
+
FROM information_schema.columns
|
|
566
|
+
WHERE table_schema = 'public'
|
|
567
|
+
AND table_name = ANY($1::text[])
|
|
568
|
+
ORDER BY ordinal_position
|
|
569
|
+
`, [tables]);
|
|
570
|
+
const rowsByTable = /* @__PURE__ */ new Map();
|
|
571
|
+
for (const row of result.rows) {
|
|
572
|
+
const rows = (_a = rowsByTable.get(row.table_name)) != null ? _a : [];
|
|
573
|
+
rows.push({ column_name: row.column_name, data_type: row.data_type });
|
|
574
|
+
rowsByTable.set(row.table_name, rows);
|
|
575
|
+
}
|
|
576
|
+
const configByTable = /* @__PURE__ */ new Map();
|
|
577
|
+
for (const table of tables) {
|
|
578
|
+
const columns = (_b = rowsByTable.get(table)) != null ? _b : [];
|
|
579
|
+
const availableFields = columns.filter(({ column_name }) => column_name !== "embedding").map(({ column_name }) => column_name);
|
|
580
|
+
const inferredFields = columns.filter(
|
|
581
|
+
({ column_name, data_type }) => column_name !== "embedding" && !["ARRAY", "json", "jsonb", "bytea", "tsvector", "USER-DEFINED"].includes(data_type)
|
|
582
|
+
).map(({ column_name }) => column_name);
|
|
583
|
+
const searchableFields = configuredSearchFields.length > 0 ? configuredSearchFields.filter((field) => columns.some((column) => column.column_name === field)) : inferredFields;
|
|
584
|
+
const normalizedFieldMap = /* @__PURE__ */ new Map();
|
|
585
|
+
for (const field of availableFields) {
|
|
586
|
+
normalizedFieldMap.set(normalizeFieldName(field), field);
|
|
587
|
+
}
|
|
588
|
+
configByTable.set(table, {
|
|
589
|
+
availableFields,
|
|
590
|
+
searchableFields,
|
|
591
|
+
normalizedFieldMap
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return configByTable;
|
|
595
|
+
}
|
|
596
|
+
parseConfiguredSearchFields() {
|
|
597
|
+
var _a;
|
|
598
|
+
const raw = (_a = this.config.options) == null ? void 0 : _a.searchFields;
|
|
599
|
+
if (Array.isArray(raw)) {
|
|
600
|
+
return raw.filter((field) => typeof field === "string" && field.trim().length > 0);
|
|
601
|
+
}
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
510
604
|
};
|
|
511
605
|
export {
|
|
512
606
|
AnthropicProvider,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "MIT",
|
package/src/config/RagConfig.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface VectorDBConfig {
|
|
|
28
28
|
* - idPath?: string (e.g. '_id')
|
|
29
29
|
* - scorePath?: string (e.g. 'similarity')
|
|
30
30
|
* - contentPath?: string (e.g. 'text')
|
|
31
|
+
*
|
|
32
|
+
* For multi-table PostgreSQL search, the following options are also supported:
|
|
33
|
+
* - tables?: string[] | string
|
|
34
|
+
* - searchFields?: string[] // optional override for which columns receive exact-match boosts
|
|
31
35
|
*/
|
|
32
36
|
options: Record<string, unknown>;
|
|
33
37
|
}
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -7,30 +7,59 @@ import { BatchProcessor, BatchOptions } from './BatchProcessor';
|
|
|
7
7
|
import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
|
|
8
8
|
import { IngestDocument, ChatResponse } from '../types';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
interface QueryFieldHint {
|
|
11
|
+
field?: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeHintValue(value: string): string {
|
|
16
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractQueryFieldHints(question: string): QueryFieldHint[] {
|
|
20
|
+
if (!question.trim()) return [];
|
|
21
|
+
|
|
22
|
+
const hints = new Map<string, QueryFieldHint>();
|
|
23
|
+
|
|
24
|
+
const addHint = (value: string, field?: string) => {
|
|
25
|
+
const normalizedValue = normalizeHintValue(value);
|
|
26
|
+
if (!normalizedValue) return;
|
|
27
|
+
|
|
28
|
+
const normalizedField = field
|
|
29
|
+
? field
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
32
|
+
.trim()
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
const key = `${normalizedField ?? '*'}::${normalizedValue.toLowerCase()}`;
|
|
36
|
+
if (!hints.has(key)) {
|
|
37
|
+
hints.set(key, {
|
|
38
|
+
value: normalizedValue,
|
|
39
|
+
...(normalizedField ? { field: normalizedField } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
12
43
|
|
|
13
44
|
for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
|
|
14
|
-
|
|
15
|
-
if (value) hints.add(value);
|
|
45
|
+
addHint(match[1]);
|
|
16
46
|
}
|
|
17
47
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
48
|
+
const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
|
|
49
|
+
const valuePattern = `([^\\n?.!,]{1,120}?)`;
|
|
50
|
+
const fieldValuePatterns = [
|
|
51
|
+
new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
|
|
52
|
+
new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
|
|
53
|
+
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
|
|
21
54
|
];
|
|
22
55
|
|
|
23
|
-
for (const pattern of
|
|
24
|
-
const match
|
|
25
|
-
|
|
26
|
-
if (value) {
|
|
27
|
-
hints.add(value);
|
|
56
|
+
for (const pattern of fieldValuePatterns) {
|
|
57
|
+
for (const match of question.matchAll(pattern)) {
|
|
58
|
+
addHint(match[2], match[1]);
|
|
28
59
|
}
|
|
29
60
|
}
|
|
30
61
|
|
|
31
|
-
return [...hints]
|
|
32
|
-
.map((hint) => hint.replace(/\s+/g, ' ').trim())
|
|
33
|
-
.filter(Boolean);
|
|
62
|
+
return [...hints.values()];
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
/**
|
|
@@ -161,11 +190,11 @@ export class Pipeline {
|
|
|
161
190
|
|
|
162
191
|
try {
|
|
163
192
|
const queryVector = await this.embeddingProvider.embed(question, { taskType: 'query' });
|
|
164
|
-
const
|
|
193
|
+
const fieldHints = extractQueryFieldHints(question);
|
|
165
194
|
// Pass the original text to the vector DB so it can perform hybrid (keyword) search if supported
|
|
166
195
|
const rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
|
|
167
196
|
__queryText: question,
|
|
168
|
-
|
|
197
|
+
__fieldHints: fieldHints,
|
|
169
198
|
});
|
|
170
199
|
|
|
171
200
|
const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
|
|
@@ -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
|
* MongoDBProvider — MongoDB Atlas Vector Search implementation.
|
|
8
16
|
*/
|
|
@@ -67,6 +75,7 @@ export class MongoDBProvider extends BaseVectorProvider {
|
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
async query(vector: number[], topK: number, namespace?: string, filter?: Record<string, unknown>): Promise<VectorMatch[]> {
|
|
78
|
+
const publicFilter = stripInternalFilterKeys(filter);
|
|
70
79
|
const pipeline: Record<string, unknown>[] = [
|
|
71
80
|
{
|
|
72
81
|
$vectorSearch: {
|
|
@@ -75,7 +84,9 @@ export class MongoDBProvider extends BaseVectorProvider {
|
|
|
75
84
|
queryVector: vector,
|
|
76
85
|
numCandidates: Math.max(topK * 10, 100),
|
|
77
86
|
limit: topK,
|
|
78
|
-
...(
|
|
87
|
+
...(Object.keys(publicFilter).length > 0 || namespace
|
|
88
|
+
? { filter: { ...publicFilter, ...(namespace ? { namespace } : {}) } }
|
|
89
|
+
: {}),
|
|
79
90
|
},
|
|
80
91
|
},
|
|
81
92
|
{
|