@retrivora-ai/rag-engine 0.2.7 → 0.2.9
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-6Q7DNWTG.mjs → chunk-5U2DHPIX.mjs} +75 -21
- package/dist/{chunk-IUTAZ7QR.mjs → chunk-6GSARSCP.mjs} +9 -2
- 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 +90 -22
- 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 +195 -33
- package/dist/server.mjs +108 -14
- package/package.json +1 -1
- package/src/config/RagConfig.ts +4 -0
- package/src/core/Pipeline.ts +114 -21
- 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,80 @@ 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 isLikelyPromptPhrase(value) {
|
|
2773
|
+
return /^(what|which|who|where|when|why|how)\b/i.test(value.trim());
|
|
2774
|
+
}
|
|
2775
|
+
function extractQueryFieldHints(question) {
|
|
2776
|
+
var _a, _b, _c;
|
|
2777
|
+
if (!question.trim()) return [];
|
|
2778
|
+
const hints = /* @__PURE__ */ new Map();
|
|
2779
|
+
const addHint = (value, field) => {
|
|
2780
|
+
const normalizedValue = normalizeHintValue(value);
|
|
2781
|
+
if (!normalizedValue) return;
|
|
2782
|
+
const normalizedField = field ? field.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() : void 0;
|
|
2783
|
+
const key = `${normalizedField != null ? normalizedField : "*"}::${normalizedValue.toLowerCase()}`;
|
|
2784
|
+
if (!hints.has(key)) {
|
|
2785
|
+
hints.set(key, __spreadValues({
|
|
2786
|
+
value: normalizedValue
|
|
2787
|
+
}, normalizedField ? { field: normalizedField } : {}));
|
|
2788
|
+
}
|
|
2789
|
+
};
|
|
2758
2790
|
for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
|
|
2759
|
-
|
|
2760
|
-
if (value) hints.add(value);
|
|
2791
|
+
addHint(match[1]);
|
|
2761
2792
|
}
|
|
2762
|
-
const
|
|
2763
|
-
/\b(?:
|
|
2764
|
-
/\b(?:
|
|
2793
|
+
const naturalQuestionPatterns = [
|
|
2794
|
+
/\b(?:what|which)\s+(?:is|are|was|were)\s+(?:the\s+)?([^?.!,]{1,60}?)\s+of\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2795
|
+
/\b(?:who|what)\s+(?:is|are|was|were)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2796
|
+
/\b(?:about|for|regarding)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi
|
|
2765
2797
|
];
|
|
2766
|
-
for (const pattern of
|
|
2767
|
-
const match
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2798
|
+
for (const pattern of naturalQuestionPatterns) {
|
|
2799
|
+
for (const match of question.matchAll(pattern)) {
|
|
2800
|
+
const value = (_a = match[2]) != null ? _a : match[1];
|
|
2801
|
+
if (value) addHint(value);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
|
|
2805
|
+
const valuePattern = `([^\\n?.!,]{1,120}?)`;
|
|
2806
|
+
const fieldValuePatterns = [
|
|
2807
|
+
new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
2808
|
+
new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
2809
|
+
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
|
|
2810
|
+
];
|
|
2811
|
+
for (const pattern of fieldValuePatterns) {
|
|
2812
|
+
for (const match of question.matchAll(pattern)) {
|
|
2813
|
+
const field = normalizeHintValue((_b = match[1]) != null ? _b : "");
|
|
2814
|
+
const value = (_c = match[2]) != null ? _c : "";
|
|
2815
|
+
if (field && !isLikelyPromptPhrase(field)) {
|
|
2816
|
+
addHint(value, field);
|
|
2817
|
+
} else {
|
|
2818
|
+
addHint(value);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3}\b/g)) {
|
|
2823
|
+
addHint(match[0]);
|
|
2824
|
+
}
|
|
2825
|
+
return [...hints.values()];
|
|
2826
|
+
}
|
|
2827
|
+
function buildQueryFilter(question, hints) {
|
|
2828
|
+
const filter = { metadata: {}, keywords: [], queryText: question };
|
|
2829
|
+
for (const hint of hints) {
|
|
2830
|
+
if (hint.field) {
|
|
2831
|
+
filter.metadata[hint.field] = hint.value;
|
|
2832
|
+
} else {
|
|
2833
|
+
filter.keywords.push(hint.value);
|
|
2771
2834
|
}
|
|
2772
2835
|
}
|
|
2773
|
-
|
|
2836
|
+
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
|
|
2837
|
+
const term = normalizeHintValue(match[0]);
|
|
2838
|
+
if (term && !filter.keywords.includes(term)) filter.keywords.push(term);
|
|
2839
|
+
}
|
|
2840
|
+
if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
|
|
2841
|
+
if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
|
|
2842
|
+
return filter;
|
|
2774
2843
|
}
|
|
2775
2844
|
var Pipeline = class {
|
|
2776
2845
|
constructor(config) {
|
|
@@ -2859,11 +2928,10 @@ var Pipeline = class {
|
|
|
2859
2928
|
const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
|
|
2860
2929
|
try {
|
|
2861
2930
|
const queryVector = await this.embeddingProvider.embed(question, { taskType: "query" });
|
|
2862
|
-
const
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
});
|
|
2931
|
+
const fieldHints = extractQueryFieldHints(question);
|
|
2932
|
+
const filter = buildQueryFilter(question, fieldHints);
|
|
2933
|
+
filter.__fieldHints = fieldHints;
|
|
2934
|
+
const rawMatches = await this.vectorDB.query(queryVector, topK, ns, filter);
|
|
2867
2935
|
const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
|
|
2868
2936
|
const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
2869
2937
|
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
@@ -3716,11 +3784,26 @@ init_PostgreSQLProvider();
|
|
|
3716
3784
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
3717
3785
|
var import_pg2 = require("pg");
|
|
3718
3786
|
init_BaseVectorProvider();
|
|
3719
|
-
|
|
3787
|
+
function normalizeFieldName(value) {
|
|
3788
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
3789
|
+
}
|
|
3790
|
+
function sanitizeFilterHints(rawFilter) {
|
|
3791
|
+
if (!Array.isArray(rawFilter == null ? void 0 : rawFilter.__fieldHints)) return [];
|
|
3792
|
+
return rawFilter.__fieldHints.filter((hint) => typeof hint === "object" && hint !== null).map((hint) => __spreadValues({
|
|
3793
|
+
value: typeof hint.value === "string" ? hint.value.trim().toLowerCase() : ""
|
|
3794
|
+
}, typeof hint.field === "string" && hint.field.trim() ? { field: normalizeFieldName(hint.field) } : {})).filter((hint) => Boolean(hint.value));
|
|
3795
|
+
}
|
|
3796
|
+
function stripInternalFilterKeys3(filter) {
|
|
3797
|
+
if (!filter) return {};
|
|
3798
|
+
return Object.fromEntries(
|
|
3799
|
+
Object.entries(filter).filter(([key]) => !key.startsWith("__"))
|
|
3800
|
+
);
|
|
3801
|
+
}
|
|
3720
3802
|
var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
3721
3803
|
constructor(config) {
|
|
3722
3804
|
var _a, _b, _c;
|
|
3723
3805
|
super(config);
|
|
3806
|
+
this.tableSearchConfig = /* @__PURE__ */ new Map();
|
|
3724
3807
|
const opts = config.options || {};
|
|
3725
3808
|
if (!opts.connectionString) {
|
|
3726
3809
|
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
@@ -3759,6 +3842,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3759
3842
|
if (this.tables.length === 0) {
|
|
3760
3843
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
3761
3844
|
} else {
|
|
3845
|
+
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
3762
3846
|
console.log(
|
|
3763
3847
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
3764
3848
|
);
|
|
@@ -3796,28 +3880,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3796
3880
|
const allResults = [];
|
|
3797
3881
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
3798
3882
|
const queryText = _filter == null ? void 0 : _filter.__queryText;
|
|
3799
|
-
const
|
|
3883
|
+
const fieldHints = sanitizeFilterHints(_filter);
|
|
3884
|
+
const explicitFilter = stripInternalFilterKeys3(_filter);
|
|
3800
3885
|
const queryPromises = this.tables.map(async (table) => {
|
|
3886
|
+
var _a2, _b2, _c;
|
|
3801
3887
|
try {
|
|
3802
3888
|
let sqlQuery = "";
|
|
3803
3889
|
let params = [];
|
|
3890
|
+
const tableConfig = this.tableSearchConfig.get(table);
|
|
3891
|
+
const availableFields = (_a2 = tableConfig == null ? void 0 : tableConfig.availableFields) != null ? _a2 : [];
|
|
3892
|
+
const searchableFields = (_b2 = tableConfig == null ? void 0 : tableConfig.searchableFields) != null ? _b2 : [];
|
|
3893
|
+
const normalizedFieldMap = (_c = tableConfig == null ? void 0 : tableConfig.normalizedFieldMap) != null ? _c : /* @__PURE__ */ new Map();
|
|
3894
|
+
const whereConditions = [];
|
|
3804
3895
|
if (queryText) {
|
|
3805
|
-
const
|
|
3806
|
-
|
|
3807
|
-
|
|
3896
|
+
const genericValueHints = fieldHints.filter((hint) => !hint.field).map((hint) => hint.value);
|
|
3897
|
+
params = genericValueHints.length > 0 ? [vectorLiteral, queryText, genericValueHints] : [vectorLiteral, queryText];
|
|
3898
|
+
const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
|
|
3899
|
+
const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0 ? `GREATEST(${searchableFields.map(
|
|
3900
|
+
(field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
|
|
3808
3901
|
).join(", ")})` : "0";
|
|
3902
|
+
const fieldSpecificChecks = fieldHints.filter((hint) => hint.field).map((hint) => {
|
|
3903
|
+
const column = hint.field ? normalizedFieldMap.get(hint.field) : void 0;
|
|
3904
|
+
if (!column) return null;
|
|
3905
|
+
params.push(hint.value);
|
|
3906
|
+
const paramIndex = params.length;
|
|
3907
|
+
return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
|
|
3908
|
+
}).filter((expr) => Boolean(expr));
|
|
3909
|
+
const exactFieldScoreExpr = fieldSpecificChecks.length > 0 ? `GREATEST(${fieldSpecificChecks.join(", ")})` : "0";
|
|
3809
3910
|
sqlQuery = `
|
|
3810
3911
|
SELECT *,
|
|
3811
3912
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
3812
3913
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
3813
|
-
${
|
|
3814
|
-
|
|
3914
|
+
${exactValueScoreExpr} AS exact_value_score,
|
|
3915
|
+
${exactFieldScoreExpr} AS exact_field_score,
|
|
3916
|
+
(
|
|
3917
|
+
(1 - (embedding <=> $1::vector)) +
|
|
3918
|
+
(COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) +
|
|
3919
|
+
(${exactValueScoreExpr} * 3.0) +
|
|
3920
|
+
(${exactFieldScoreExpr} * 5.0)
|
|
3921
|
+
) AS hybrid_score
|
|
3815
3922
|
FROM "${table}" t
|
|
3816
3923
|
ORDER BY hybrid_score DESC
|
|
3817
3924
|
LIMIT 50
|
|
3818
3925
|
`;
|
|
3819
|
-
params = hasEntityHints ? [vectorLiteral, queryText, entityHints] : [vectorLiteral, queryText];
|
|
3820
3926
|
} else {
|
|
3927
|
+
params = [vectorLiteral];
|
|
3821
3928
|
sqlQuery = `
|
|
3822
3929
|
SELECT *,
|
|
3823
3930
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -3825,7 +3932,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3825
3932
|
ORDER BY hybrid_score DESC
|
|
3826
3933
|
LIMIT 50
|
|
3827
3934
|
`;
|
|
3828
|
-
|
|
3935
|
+
}
|
|
3936
|
+
for (const [key, value] of Object.entries(explicitFilter)) {
|
|
3937
|
+
const column = availableFields.includes(key) ? key : normalizedFieldMap.get(normalizeFieldName(key));
|
|
3938
|
+
if (!column) continue;
|
|
3939
|
+
params.push(String(value));
|
|
3940
|
+
whereConditions.push(`COALESCE(to_jsonb(t)->>'${column}', '') = $${params.length}::text`);
|
|
3941
|
+
}
|
|
3942
|
+
if (whereConditions.length > 0) {
|
|
3943
|
+
sqlQuery = sqlQuery.replace(`FROM "${table}" t`, `FROM "${table}" t
|
|
3944
|
+
WHERE ${whereConditions.join(" AND ")}`);
|
|
3829
3945
|
}
|
|
3830
3946
|
const result = await this.pool.query(sqlQuery, params);
|
|
3831
3947
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -3835,11 +3951,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3835
3951
|
}
|
|
3836
3952
|
const tableResults = [];
|
|
3837
3953
|
for (const row of result.rows) {
|
|
3838
|
-
const
|
|
3954
|
+
const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
|
|
3839
3955
|
delete rest.embedding;
|
|
3840
3956
|
delete rest.vector_score;
|
|
3841
3957
|
delete rest.keyword_score;
|
|
3842
|
-
delete rest.
|
|
3958
|
+
delete rest.exact_value_score;
|
|
3959
|
+
delete rest.exact_field_score;
|
|
3843
3960
|
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
3844
3961
|
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
3845
3962
|
tableResults.push({
|
|
@@ -3888,6 +4005,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3888
4005
|
await this.pool.end();
|
|
3889
4006
|
}
|
|
3890
4007
|
}
|
|
4008
|
+
async loadTableSearchConfig(client, tables) {
|
|
4009
|
+
var _a, _b;
|
|
4010
|
+
if (tables.length === 0) return /* @__PURE__ */ new Map();
|
|
4011
|
+
const configuredSearchFields = this.parseConfiguredSearchFields();
|
|
4012
|
+
const result = await client.query(`
|
|
4013
|
+
SELECT table_name, column_name, data_type
|
|
4014
|
+
FROM information_schema.columns
|
|
4015
|
+
WHERE table_schema = 'public'
|
|
4016
|
+
AND table_name = ANY($1::text[])
|
|
4017
|
+
ORDER BY ordinal_position
|
|
4018
|
+
`, [tables]);
|
|
4019
|
+
const rowsByTable = /* @__PURE__ */ new Map();
|
|
4020
|
+
for (const row of result.rows) {
|
|
4021
|
+
const rows = (_a = rowsByTable.get(row.table_name)) != null ? _a : [];
|
|
4022
|
+
rows.push({ column_name: row.column_name, data_type: row.data_type });
|
|
4023
|
+
rowsByTable.set(row.table_name, rows);
|
|
4024
|
+
}
|
|
4025
|
+
const configByTable = /* @__PURE__ */ new Map();
|
|
4026
|
+
for (const table of tables) {
|
|
4027
|
+
const columns = (_b = rowsByTable.get(table)) != null ? _b : [];
|
|
4028
|
+
const availableFields = columns.filter(({ column_name }) => column_name !== "embedding").map(({ column_name }) => column_name);
|
|
4029
|
+
const inferredFields = columns.filter(
|
|
4030
|
+
({ column_name, data_type }) => column_name !== "embedding" && !["ARRAY", "json", "jsonb", "bytea", "tsvector", "USER-DEFINED"].includes(data_type)
|
|
4031
|
+
).map(({ column_name }) => column_name);
|
|
4032
|
+
const searchableFields = configuredSearchFields.length > 0 ? configuredSearchFields.filter((field) => columns.some((column) => column.column_name === field)) : inferredFields;
|
|
4033
|
+
const normalizedFieldMap = /* @__PURE__ */ new Map();
|
|
4034
|
+
for (const field of availableFields) {
|
|
4035
|
+
normalizedFieldMap.set(normalizeFieldName(field), field);
|
|
4036
|
+
}
|
|
4037
|
+
configByTable.set(table, {
|
|
4038
|
+
availableFields,
|
|
4039
|
+
searchableFields,
|
|
4040
|
+
normalizedFieldMap
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
return configByTable;
|
|
4044
|
+
}
|
|
4045
|
+
parseConfiguredSearchFields() {
|
|
4046
|
+
var _a;
|
|
4047
|
+
const raw = (_a = this.config.options) == null ? void 0 : _a.searchFields;
|
|
4048
|
+
if (Array.isArray(raw)) {
|
|
4049
|
+
return raw.filter((field) => typeof field === "string" && field.trim().length > 0);
|
|
4050
|
+
}
|
|
4051
|
+
return [];
|
|
4052
|
+
}
|
|
3891
4053
|
};
|
|
3892
4054
|
|
|
3893
4055
|
// 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-5U2DHPIX.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.9",
|
|
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
|
}
|