@retrivora-ai/rag-engine 0.2.9 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{RagConfig--ibz0b3W.d.mts → RagConfig-D3Inaf9N.d.mts} +1 -1
- package/dist/{RagConfig--ibz0b3W.d.ts → RagConfig-D3Inaf9N.d.ts} +1 -1
- package/dist/{chunk-5U2DHPIX.mjs → chunk-GT72OIOD.mjs} +49 -6
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +49 -6
- package/dist/handlers/index.mjs +1 -1
- package/dist/{index-Dr1HN0se.d.ts → index-BhNJQ2SS.d.ts} +1 -1
- package/dist/{index-w8qIEFvi.d.mts → index-Ymwm-_OR.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/server.d.mts +5 -7
- package/dist/server.d.ts +5 -7
- package/dist/server.js +85 -118
- package/dist/server.mjs +37 -113
- package/package.json +1 -1
- package/src/config/RagConfig.ts +1 -1
- package/src/core/ConfigValidator.ts +16 -0
- package/src/core/Pipeline.ts +42 -5
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +58 -167
package/dist/server.js
CHANGED
|
@@ -1528,6 +1528,20 @@ var ConfigValidator = class {
|
|
|
1528
1528
|
severity: "error"
|
|
1529
1529
|
});
|
|
1530
1530
|
}
|
|
1531
|
+
if (opts.tables && typeof opts.tables !== "string" && !Array.isArray(opts.tables)) {
|
|
1532
|
+
errors.push({
|
|
1533
|
+
field: "vectorDb.options.tables",
|
|
1534
|
+
message: "PostgreSQL tables must be a string or a string array",
|
|
1535
|
+
severity: "error"
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
if (opts.searchFields && typeof opts.searchFields !== "string" && !Array.isArray(opts.searchFields)) {
|
|
1539
|
+
errors.push({
|
|
1540
|
+
field: "vectorDb.options.searchFields",
|
|
1541
|
+
message: "PostgreSQL searchFields must be a string or a string array",
|
|
1542
|
+
severity: "error"
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1531
1545
|
return errors;
|
|
1532
1546
|
}
|
|
1533
1547
|
/**
|
|
@@ -2773,7 +2787,7 @@ function isLikelyPromptPhrase(value) {
|
|
|
2773
2787
|
return /^(what|which|who|where|when|why|how)\b/i.test(value.trim());
|
|
2774
2788
|
}
|
|
2775
2789
|
function extractQueryFieldHints(question) {
|
|
2776
|
-
var _a, _b, _c;
|
|
2790
|
+
var _a, _b, _c, _d;
|
|
2777
2791
|
if (!question.trim()) return [];
|
|
2778
2792
|
const hints = /* @__PURE__ */ new Map();
|
|
2779
2793
|
const addHint = (value, field) => {
|
|
@@ -2795,9 +2809,38 @@ function extractQueryFieldHints(question) {
|
|
|
2795
2809
|
/\b(?:who|what)\s+(?:is|are|was|were)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2796
2810
|
/\b(?:about|for|regarding)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi
|
|
2797
2811
|
];
|
|
2812
|
+
const personCompanyPatterns = [
|
|
2813
|
+
/\bcompany(?:\s+name)?\s+(?:of|for)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2814
|
+
/\b(?:which|what)\s+company\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi,
|
|
2815
|
+
/\bwhere\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi
|
|
2816
|
+
];
|
|
2817
|
+
for (const pattern of personCompanyPatterns) {
|
|
2818
|
+
for (const match of question.matchAll(pattern)) {
|
|
2819
|
+
const name = match[1];
|
|
2820
|
+
if (name) addHint(name, "name");
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
const universalPatterns = [
|
|
2824
|
+
{ regex: /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi, field: "email", group: 1 },
|
|
2825
|
+
{ regex: /(\+?\d[\d\-\.\s\(\)]{6,}\d)/g, field: "phone", group: 1 },
|
|
2826
|
+
{ regex: /(\b\d{4}-\d{2}-\d{2}\b|\b\d{1,2}\/\d{1,2}\/\d{2,4}\b)/g, field: "date", group: 1 },
|
|
2827
|
+
{ regex: /(\$\s?\d{1,3}(?:,\d{3})*(?:\.\d+)?)/g, field: "amount", group: 1 },
|
|
2828
|
+
{ regex: /\b(ID|id|identifier)[: ]\s*([A-Za-z0-9\-]{3,})\b/gi, field: "id", group: 2 },
|
|
2829
|
+
// Generic quoted phrase / proper-noun sequences as keywords (already partially handled above)
|
|
2830
|
+
{ regex: /"([^"]{2,120})"/g, group: 1 },
|
|
2831
|
+
{ regex: /'([^']{2,120})'/g, group: 1 }
|
|
2832
|
+
];
|
|
2833
|
+
for (const p of universalPatterns) {
|
|
2834
|
+
for (const match of question.matchAll(p.regex)) {
|
|
2835
|
+
const val = p.group ? (_a = match[p.group]) != null ? _a : match[0] : match[0];
|
|
2836
|
+
if (!val) continue;
|
|
2837
|
+
if (p.field) addHint(val, p.field);
|
|
2838
|
+
else addHint(val);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2798
2841
|
for (const pattern of naturalQuestionPatterns) {
|
|
2799
2842
|
for (const match of question.matchAll(pattern)) {
|
|
2800
|
-
const value = (
|
|
2843
|
+
const value = (_b = match[2]) != null ? _b : match[1];
|
|
2801
2844
|
if (value) addHint(value);
|
|
2802
2845
|
}
|
|
2803
2846
|
}
|
|
@@ -2810,8 +2853,8 @@ function extractQueryFieldHints(question) {
|
|
|
2810
2853
|
];
|
|
2811
2854
|
for (const pattern of fieldValuePatterns) {
|
|
2812
2855
|
for (const match of question.matchAll(pattern)) {
|
|
2813
|
-
const field = normalizeHintValue((
|
|
2814
|
-
const value = (
|
|
2856
|
+
const field = normalizeHintValue((_c = match[1]) != null ? _c : "");
|
|
2857
|
+
const value = (_d = match[2]) != null ? _d : "";
|
|
2815
2858
|
if (field && !isLikelyPromptPhrase(field)) {
|
|
2816
2859
|
addHint(value, field);
|
|
2817
2860
|
} else {
|
|
@@ -2819,7 +2862,7 @@ function extractQueryFieldHints(question) {
|
|
|
2819
2862
|
}
|
|
2820
2863
|
}
|
|
2821
2864
|
}
|
|
2822
|
-
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){
|
|
2865
|
+
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
|
|
2823
2866
|
addHint(match[0]);
|
|
2824
2867
|
}
|
|
2825
2868
|
return [...hints.values()];
|
|
@@ -2930,7 +2973,7 @@ var Pipeline = class {
|
|
|
2930
2973
|
const queryVector = await this.embeddingProvider.embed(question, { taskType: "query" });
|
|
2931
2974
|
const fieldHints = extractQueryFieldHints(question);
|
|
2932
2975
|
const filter = buildQueryFilter(question, fieldHints);
|
|
2933
|
-
filter.
|
|
2976
|
+
filter.__entityHints = fieldHints;
|
|
2934
2977
|
const rawMatches = await this.vectorDB.query(queryVector, topK, ns, filter);
|
|
2935
2978
|
const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
|
|
2936
2979
|
const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
@@ -3784,26 +3827,10 @@ init_PostgreSQLProvider();
|
|
|
3784
3827
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
3785
3828
|
var import_pg2 = require("pg");
|
|
3786
3829
|
init_BaseVectorProvider();
|
|
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
|
-
}
|
|
3802
3830
|
var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
3803
3831
|
constructor(config) {
|
|
3804
|
-
var _a, _b, _c;
|
|
3832
|
+
var _a, _b, _c, _d, _e;
|
|
3805
3833
|
super(config);
|
|
3806
|
-
this.tableSearchConfig = /* @__PURE__ */ new Map();
|
|
3807
3834
|
const opts = config.options || {};
|
|
3808
3835
|
if (!opts.connectionString) {
|
|
3809
3836
|
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
@@ -3817,6 +3844,8 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3817
3844
|
"[MultiTablePostgresProvider] No tables configured. Set VECTOR_DB_TABLES as a comma-separated list of table names or pass options.tables."
|
|
3818
3845
|
);
|
|
3819
3846
|
}
|
|
3847
|
+
const rawSearchFields = (_e = (_d = opts.searchFields) != null ? _d : process.env.VECTOR_DB_SEARCH_FIELDS) != null ? _e : ["name", "product_name", "productname", "title"];
|
|
3848
|
+
this.searchFields = typeof rawSearchFields === "string" ? rawSearchFields.split(",").map((f) => f.trim()).filter(Boolean) : rawSearchFields;
|
|
3820
3849
|
}
|
|
3821
3850
|
async initialize() {
|
|
3822
3851
|
this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
|
|
@@ -3842,7 +3871,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3842
3871
|
if (this.tables.length === 0) {
|
|
3843
3872
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
3844
3873
|
} else {
|
|
3845
|
-
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
3846
3874
|
console.log(
|
|
3847
3875
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
3848
3876
|
);
|
|
@@ -3872,59 +3900,42 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3872
3900
|
* Query all configured tables and merge results, sorted by cosine similarity score.
|
|
3873
3901
|
*/
|
|
3874
3902
|
async query(vector, topK, _namespace, _filter) {
|
|
3875
|
-
var _a, _b;
|
|
3903
|
+
var _a, _b, _c;
|
|
3876
3904
|
if (!this.pool) {
|
|
3877
3905
|
throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
|
|
3878
3906
|
}
|
|
3879
3907
|
const vectorLiteral = `[${vector.join(",")}]`;
|
|
3880
3908
|
const allResults = [];
|
|
3881
3909
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
3882
|
-
const queryText = _filter == null ? void 0 : _filter.
|
|
3883
|
-
const
|
|
3884
|
-
|
|
3910
|
+
const queryText = _filter == null ? void 0 : _filter.queryText;
|
|
3911
|
+
const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter(
|
|
3912
|
+
(hint) => typeof hint === "object" && hint !== null && typeof hint.value === "string"
|
|
3913
|
+
).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
|
|
3885
3914
|
const queryPromises = this.tables.map(async (table) => {
|
|
3886
|
-
var _a2, _b2, _c;
|
|
3887
3915
|
try {
|
|
3888
3916
|
let sqlQuery = "";
|
|
3889
3917
|
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 = [];
|
|
3895
3918
|
if (queryText) {
|
|
3896
|
-
const
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
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";
|
|
3919
|
+
const hasEntityHints = entityHints.length > 0;
|
|
3920
|
+
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
3921
|
+
SELECT COALESCE(MAX(
|
|
3922
|
+
CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
|
|
3923
|
+
THEN 1.0 ELSE 0.0 END
|
|
3924
|
+
), 0)
|
|
3925
|
+
FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
|
|
3926
|
+
WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
|
|
3927
|
+
) * 3.0` : "";
|
|
3910
3928
|
sqlQuery = `
|
|
3911
3929
|
SELECT *,
|
|
3912
3930
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
3913
3931
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
3914
|
-
${
|
|
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
|
|
3932
|
+
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
3922
3933
|
FROM "${table}" t
|
|
3923
3934
|
ORDER BY hybrid_score DESC
|
|
3924
3935
|
LIMIT 50
|
|
3925
3936
|
`;
|
|
3937
|
+
params = [vectorLiteral, queryText];
|
|
3926
3938
|
} else {
|
|
3927
|
-
params = [vectorLiteral];
|
|
3928
3939
|
sqlQuery = `
|
|
3929
3940
|
SELECT *,
|
|
3930
3941
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -3932,16 +3943,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3932
3943
|
ORDER BY hybrid_score DESC
|
|
3933
3944
|
LIMIT 50
|
|
3934
3945
|
`;
|
|
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 ")}`);
|
|
3946
|
+
params = [vectorLiteral];
|
|
3945
3947
|
}
|
|
3946
3948
|
const result = await this.pool.query(sqlQuery, params);
|
|
3947
3949
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -3951,12 +3953,11 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3951
3953
|
}
|
|
3952
3954
|
const tableResults = [];
|
|
3953
3955
|
for (const row of result.rows) {
|
|
3954
|
-
const
|
|
3956
|
+
const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
|
|
3955
3957
|
delete rest.embedding;
|
|
3956
3958
|
delete rest.vector_score;
|
|
3957
3959
|
delete rest.keyword_score;
|
|
3958
|
-
delete rest.
|
|
3959
|
-
delete rest.exact_field_score;
|
|
3960
|
+
delete rest.exact_name_score;
|
|
3960
3961
|
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
3961
3962
|
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
3962
3963
|
tableResults.push({
|
|
@@ -3979,12 +3980,23 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3979
3980
|
for (const tableResults of resultsArray) {
|
|
3980
3981
|
allResults.push(...tableResults);
|
|
3981
3982
|
}
|
|
3982
|
-
const
|
|
3983
|
+
const resultsByTable = {};
|
|
3984
|
+
for (const res of allResults) {
|
|
3985
|
+
const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
|
|
3986
|
+
if (!resultsByTable[table]) resultsByTable[table] = [];
|
|
3987
|
+
resultsByTable[table].push(res);
|
|
3988
|
+
}
|
|
3989
|
+
const balancedResults = [];
|
|
3990
|
+
const tables = Object.keys(resultsByTable);
|
|
3991
|
+
for (const table of tables) {
|
|
3992
|
+
balancedResults.push(...resultsByTable[table].slice(0, 3));
|
|
3993
|
+
}
|
|
3994
|
+
const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
|
|
3983
3995
|
if (finalSorted.length > 0) {
|
|
3984
3996
|
console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
|
|
3985
|
-
console.log(`[MultiTablePostgresProvider] Final top match from "${(
|
|
3997
|
+
console.log(`[MultiTablePostgresProvider] Final top match from "${(_c = (_b = finalSorted[0].metadata) == null ? void 0 : _b.source_table) != null ? _c : "unknown"}" with score ${finalSorted[0].score.toFixed(4)}`);
|
|
3986
3998
|
}
|
|
3987
|
-
return finalSorted.slice(0, topK);
|
|
3999
|
+
return finalSorted.slice(0, Math.max(topK, 15));
|
|
3988
4000
|
}
|
|
3989
4001
|
async delete(_id, _namespace) {
|
|
3990
4002
|
console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
|
|
@@ -4005,51 +4017,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
4005
4017
|
await this.pool.end();
|
|
4006
4018
|
}
|
|
4007
4019
|
}
|
|
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
|
-
}
|
|
4053
4020
|
};
|
|
4054
4021
|
|
|
4055
4022
|
// src/server.ts
|
package/dist/server.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
createIngestHandler,
|
|
35
35
|
createUploadHandler,
|
|
36
36
|
getRagConfig
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-GT72OIOD.mjs";
|
|
38
38
|
import "./chunk-EDLTMSNY.mjs";
|
|
39
39
|
import {
|
|
40
40
|
PineconeProvider
|
|
@@ -335,26 +335,10 @@ function createFromPreset(presetName) {
|
|
|
335
335
|
|
|
336
336
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
337
337
|
import { Pool } from "pg";
|
|
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
|
-
}
|
|
353
338
|
var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
354
339
|
constructor(config) {
|
|
355
|
-
var _a, _b, _c;
|
|
340
|
+
var _a, _b, _c, _d, _e;
|
|
356
341
|
super(config);
|
|
357
|
-
this.tableSearchConfig = /* @__PURE__ */ new Map();
|
|
358
342
|
const opts = config.options || {};
|
|
359
343
|
if (!opts.connectionString) {
|
|
360
344
|
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
@@ -368,6 +352,8 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
368
352
|
"[MultiTablePostgresProvider] No tables configured. Set VECTOR_DB_TABLES as a comma-separated list of table names or pass options.tables."
|
|
369
353
|
);
|
|
370
354
|
}
|
|
355
|
+
const rawSearchFields = (_e = (_d = opts.searchFields) != null ? _d : process.env.VECTOR_DB_SEARCH_FIELDS) != null ? _e : ["name", "product_name", "productname", "title"];
|
|
356
|
+
this.searchFields = typeof rawSearchFields === "string" ? rawSearchFields.split(",").map((f) => f.trim()).filter(Boolean) : rawSearchFields;
|
|
371
357
|
}
|
|
372
358
|
async initialize() {
|
|
373
359
|
this.pool = new Pool({ connectionString: this.connectionString });
|
|
@@ -393,7 +379,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
393
379
|
if (this.tables.length === 0) {
|
|
394
380
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
395
381
|
} else {
|
|
396
|
-
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
397
382
|
console.log(
|
|
398
383
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
399
384
|
);
|
|
@@ -423,59 +408,42 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
423
408
|
* Query all configured tables and merge results, sorted by cosine similarity score.
|
|
424
409
|
*/
|
|
425
410
|
async query(vector, topK, _namespace, _filter) {
|
|
426
|
-
var _a, _b;
|
|
411
|
+
var _a, _b, _c;
|
|
427
412
|
if (!this.pool) {
|
|
428
413
|
throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
|
|
429
414
|
}
|
|
430
415
|
const vectorLiteral = `[${vector.join(",")}]`;
|
|
431
416
|
const allResults = [];
|
|
432
417
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
433
|
-
const queryText = _filter == null ? void 0 : _filter.
|
|
434
|
-
const
|
|
435
|
-
|
|
418
|
+
const queryText = _filter == null ? void 0 : _filter.queryText;
|
|
419
|
+
const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter(
|
|
420
|
+
(hint) => typeof hint === "object" && hint !== null && typeof hint.value === "string"
|
|
421
|
+
).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
|
|
436
422
|
const queryPromises = this.tables.map(async (table) => {
|
|
437
|
-
var _a2, _b2, _c;
|
|
438
423
|
try {
|
|
439
424
|
let sqlQuery = "";
|
|
440
425
|
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 = [];
|
|
446
426
|
if (queryText) {
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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";
|
|
427
|
+
const hasEntityHints = entityHints.length > 0;
|
|
428
|
+
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
429
|
+
SELECT COALESCE(MAX(
|
|
430
|
+
CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
|
|
431
|
+
THEN 1.0 ELSE 0.0 END
|
|
432
|
+
), 0)
|
|
433
|
+
FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
|
|
434
|
+
WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
|
|
435
|
+
) * 3.0` : "";
|
|
461
436
|
sqlQuery = `
|
|
462
437
|
SELECT *,
|
|
463
438
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
464
439
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
465
|
-
${
|
|
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
|
|
440
|
+
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
473
441
|
FROM "${table}" t
|
|
474
442
|
ORDER BY hybrid_score DESC
|
|
475
443
|
LIMIT 50
|
|
476
444
|
`;
|
|
445
|
+
params = [vectorLiteral, queryText];
|
|
477
446
|
} else {
|
|
478
|
-
params = [vectorLiteral];
|
|
479
447
|
sqlQuery = `
|
|
480
448
|
SELECT *,
|
|
481
449
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -483,16 +451,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
483
451
|
ORDER BY hybrid_score DESC
|
|
484
452
|
LIMIT 50
|
|
485
453
|
`;
|
|
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 ")}`);
|
|
454
|
+
params = [vectorLiteral];
|
|
496
455
|
}
|
|
497
456
|
const result = await this.pool.query(sqlQuery, params);
|
|
498
457
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -502,12 +461,11 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
502
461
|
}
|
|
503
462
|
const tableResults = [];
|
|
504
463
|
for (const row of result.rows) {
|
|
505
|
-
const
|
|
464
|
+
const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
|
|
506
465
|
delete rest.embedding;
|
|
507
466
|
delete rest.vector_score;
|
|
508
467
|
delete rest.keyword_score;
|
|
509
|
-
delete rest.
|
|
510
|
-
delete rest.exact_field_score;
|
|
468
|
+
delete rest.exact_name_score;
|
|
511
469
|
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
512
470
|
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
513
471
|
tableResults.push({
|
|
@@ -530,12 +488,23 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
530
488
|
for (const tableResults of resultsArray) {
|
|
531
489
|
allResults.push(...tableResults);
|
|
532
490
|
}
|
|
533
|
-
const
|
|
491
|
+
const resultsByTable = {};
|
|
492
|
+
for (const res of allResults) {
|
|
493
|
+
const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
|
|
494
|
+
if (!resultsByTable[table]) resultsByTable[table] = [];
|
|
495
|
+
resultsByTable[table].push(res);
|
|
496
|
+
}
|
|
497
|
+
const balancedResults = [];
|
|
498
|
+
const tables = Object.keys(resultsByTable);
|
|
499
|
+
for (const table of tables) {
|
|
500
|
+
balancedResults.push(...resultsByTable[table].slice(0, 3));
|
|
501
|
+
}
|
|
502
|
+
const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
|
|
534
503
|
if (finalSorted.length > 0) {
|
|
535
504
|
console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
|
|
536
|
-
console.log(`[MultiTablePostgresProvider] Final top match from "${(
|
|
505
|
+
console.log(`[MultiTablePostgresProvider] Final top match from "${(_c = (_b = finalSorted[0].metadata) == null ? void 0 : _b.source_table) != null ? _c : "unknown"}" with score ${finalSorted[0].score.toFixed(4)}`);
|
|
537
506
|
}
|
|
538
|
-
return finalSorted.slice(0, topK);
|
|
507
|
+
return finalSorted.slice(0, Math.max(topK, 15));
|
|
539
508
|
}
|
|
540
509
|
async delete(_id, _namespace) {
|
|
541
510
|
console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
|
|
@@ -556,51 +525,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
556
525
|
await this.pool.end();
|
|
557
526
|
}
|
|
558
527
|
}
|
|
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
|
-
}
|
|
604
528
|
};
|
|
605
529
|
export {
|
|
606
530
|
AnthropicProvider,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
@@ -31,7 +31,7 @@ export interface VectorDBConfig {
|
|
|
31
31
|
*
|
|
32
32
|
* For multi-table PostgreSQL search, the following options are also supported:
|
|
33
33
|
* - tables?: string[] | string
|
|
34
|
-
* - searchFields?: string[] // optional override for which columns receive exact-match boosts
|
|
34
|
+
* - searchFields?: string[] | string // optional override for which columns receive exact-match boosts
|
|
35
35
|
*/
|
|
36
36
|
options: Record<string, unknown>;
|
|
37
37
|
}
|
|
@@ -151,6 +151,22 @@ export class ConfigValidator {
|
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
if (opts.tables && typeof opts.tables !== 'string' && !Array.isArray(opts.tables)) {
|
|
155
|
+
errors.push({
|
|
156
|
+
field: 'vectorDb.options.tables',
|
|
157
|
+
message: 'PostgreSQL tables must be a string or a string array',
|
|
158
|
+
severity: 'error',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (opts.searchFields && typeof opts.searchFields !== 'string' && !Array.isArray(opts.searchFields)) {
|
|
163
|
+
errors.push({
|
|
164
|
+
field: 'vectorDb.options.searchFields',
|
|
165
|
+
message: 'PostgreSQL searchFields must be a string or a string array',
|
|
166
|
+
severity: 'error',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
154
170
|
return errors;
|
|
155
171
|
}
|
|
156
172
|
|