@retrivora-ai/rag-engine 0.2.8 → 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-CYVPACA7.mjs → chunk-GT72OIOD.mjs} +88 -5
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +88 -5
- 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 +124 -117
- 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 +110 -9
- 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
|
/**
|
|
@@ -2769,7 +2783,11 @@ var EmbeddingStrategyResolver = class {
|
|
|
2769
2783
|
function normalizeHintValue(value) {
|
|
2770
2784
|
return value.replace(/\s+/g, " ").trim();
|
|
2771
2785
|
}
|
|
2786
|
+
function isLikelyPromptPhrase(value) {
|
|
2787
|
+
return /^(what|which|who|where|when|why|how)\b/i.test(value.trim());
|
|
2788
|
+
}
|
|
2772
2789
|
function extractQueryFieldHints(question) {
|
|
2790
|
+
var _a, _b, _c, _d;
|
|
2773
2791
|
if (!question.trim()) return [];
|
|
2774
2792
|
const hints = /* @__PURE__ */ new Map();
|
|
2775
2793
|
const addHint = (value, field) => {
|
|
@@ -2786,6 +2804,46 @@ function extractQueryFieldHints(question) {
|
|
|
2786
2804
|
for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
|
|
2787
2805
|
addHint(match[1]);
|
|
2788
2806
|
}
|
|
2807
|
+
const naturalQuestionPatterns = [
|
|
2808
|
+
/\b(?:what|which)\s+(?:is|are|was|were)\s+(?:the\s+)?([^?.!,]{1,60}?)\s+of\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2809
|
+
/\b(?:who|what)\s+(?:is|are|was|were)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
2810
|
+
/\b(?:about|for|regarding)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi
|
|
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
|
+
}
|
|
2841
|
+
for (const pattern of naturalQuestionPatterns) {
|
|
2842
|
+
for (const match of question.matchAll(pattern)) {
|
|
2843
|
+
const value = (_b = match[2]) != null ? _b : match[1];
|
|
2844
|
+
if (value) addHint(value);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2789
2847
|
const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
|
|
2790
2848
|
const valuePattern = `([^\\n?.!,]{1,120}?)`;
|
|
2791
2849
|
const fieldValuePatterns = [
|
|
@@ -2795,11 +2853,37 @@ function extractQueryFieldHints(question) {
|
|
|
2795
2853
|
];
|
|
2796
2854
|
for (const pattern of fieldValuePatterns) {
|
|
2797
2855
|
for (const match of question.matchAll(pattern)) {
|
|
2798
|
-
|
|
2856
|
+
const field = normalizeHintValue((_c = match[1]) != null ? _c : "");
|
|
2857
|
+
const value = (_d = match[2]) != null ? _d : "";
|
|
2858
|
+
if (field && !isLikelyPromptPhrase(field)) {
|
|
2859
|
+
addHint(value, field);
|
|
2860
|
+
} else {
|
|
2861
|
+
addHint(value);
|
|
2862
|
+
}
|
|
2799
2863
|
}
|
|
2800
2864
|
}
|
|
2865
|
+
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
|
|
2866
|
+
addHint(match[0]);
|
|
2867
|
+
}
|
|
2801
2868
|
return [...hints.values()];
|
|
2802
2869
|
}
|
|
2870
|
+
function buildQueryFilter(question, hints) {
|
|
2871
|
+
const filter = { metadata: {}, keywords: [], queryText: question };
|
|
2872
|
+
for (const hint of hints) {
|
|
2873
|
+
if (hint.field) {
|
|
2874
|
+
filter.metadata[hint.field] = hint.value;
|
|
2875
|
+
} else {
|
|
2876
|
+
filter.keywords.push(hint.value);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
|
|
2880
|
+
const term = normalizeHintValue(match[0]);
|
|
2881
|
+
if (term && !filter.keywords.includes(term)) filter.keywords.push(term);
|
|
2882
|
+
}
|
|
2883
|
+
if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
|
|
2884
|
+
if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
|
|
2885
|
+
return filter;
|
|
2886
|
+
}
|
|
2803
2887
|
var Pipeline = class {
|
|
2804
2888
|
constructor(config) {
|
|
2805
2889
|
this.initialised = false;
|
|
@@ -2888,10 +2972,9 @@ var Pipeline = class {
|
|
|
2888
2972
|
try {
|
|
2889
2973
|
const queryVector = await this.embeddingProvider.embed(question, { taskType: "query" });
|
|
2890
2974
|
const fieldHints = extractQueryFieldHints(question);
|
|
2891
|
-
const
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
});
|
|
2975
|
+
const filter = buildQueryFilter(question, fieldHints);
|
|
2976
|
+
filter.__entityHints = fieldHints;
|
|
2977
|
+
const rawMatches = await this.vectorDB.query(queryVector, topK, ns, filter);
|
|
2895
2978
|
const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
|
|
2896
2979
|
const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
2897
2980
|
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
@@ -3744,26 +3827,10 @@ init_PostgreSQLProvider();
|
|
|
3744
3827
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
3745
3828
|
var import_pg2 = require("pg");
|
|
3746
3829
|
init_BaseVectorProvider();
|
|
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
|
-
}
|
|
3762
3830
|
var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
3763
3831
|
constructor(config) {
|
|
3764
|
-
var _a, _b, _c;
|
|
3832
|
+
var _a, _b, _c, _d, _e;
|
|
3765
3833
|
super(config);
|
|
3766
|
-
this.tableSearchConfig = /* @__PURE__ */ new Map();
|
|
3767
3834
|
const opts = config.options || {};
|
|
3768
3835
|
if (!opts.connectionString) {
|
|
3769
3836
|
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
@@ -3777,6 +3844,8 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3777
3844
|
"[MultiTablePostgresProvider] No tables configured. Set VECTOR_DB_TABLES as a comma-separated list of table names or pass options.tables."
|
|
3778
3845
|
);
|
|
3779
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;
|
|
3780
3849
|
}
|
|
3781
3850
|
async initialize() {
|
|
3782
3851
|
this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
|
|
@@ -3802,7 +3871,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3802
3871
|
if (this.tables.length === 0) {
|
|
3803
3872
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
3804
3873
|
} else {
|
|
3805
|
-
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
3806
3874
|
console.log(
|
|
3807
3875
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
3808
3876
|
);
|
|
@@ -3832,59 +3900,42 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3832
3900
|
* Query all configured tables and merge results, sorted by cosine similarity score.
|
|
3833
3901
|
*/
|
|
3834
3902
|
async query(vector, topK, _namespace, _filter) {
|
|
3835
|
-
var _a, _b;
|
|
3903
|
+
var _a, _b, _c;
|
|
3836
3904
|
if (!this.pool) {
|
|
3837
3905
|
throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
|
|
3838
3906
|
}
|
|
3839
3907
|
const vectorLiteral = `[${vector.join(",")}]`;
|
|
3840
3908
|
const allResults = [];
|
|
3841
3909
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
3842
|
-
const queryText = _filter == null ? void 0 : _filter.
|
|
3843
|
-
const
|
|
3844
|
-
|
|
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) : [];
|
|
3845
3914
|
const queryPromises = this.tables.map(async (table) => {
|
|
3846
|
-
var _a2, _b2, _c;
|
|
3847
3915
|
try {
|
|
3848
3916
|
let sqlQuery = "";
|
|
3849
3917
|
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 = [];
|
|
3855
3918
|
if (queryText) {
|
|
3856
|
-
const
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
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";
|
|
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` : "";
|
|
3870
3928
|
sqlQuery = `
|
|
3871
3929
|
SELECT *,
|
|
3872
3930
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
3873
3931
|
COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
3874
|
-
${
|
|
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
|
|
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
|
|
3882
3933
|
FROM "${table}" t
|
|
3883
3934
|
ORDER BY hybrid_score DESC
|
|
3884
3935
|
LIMIT 50
|
|
3885
3936
|
`;
|
|
3937
|
+
params = [vectorLiteral, queryText];
|
|
3886
3938
|
} else {
|
|
3887
|
-
params = [vectorLiteral];
|
|
3888
3939
|
sqlQuery = `
|
|
3889
3940
|
SELECT *,
|
|
3890
3941
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -3892,16 +3943,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3892
3943
|
ORDER BY hybrid_score DESC
|
|
3893
3944
|
LIMIT 50
|
|
3894
3945
|
`;
|
|
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 ")}`);
|
|
3946
|
+
params = [vectorLiteral];
|
|
3905
3947
|
}
|
|
3906
3948
|
const result = await this.pool.query(sqlQuery, params);
|
|
3907
3949
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -3911,12 +3953,11 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3911
3953
|
}
|
|
3912
3954
|
const tableResults = [];
|
|
3913
3955
|
for (const row of result.rows) {
|
|
3914
|
-
const
|
|
3956
|
+
const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
|
|
3915
3957
|
delete rest.embedding;
|
|
3916
3958
|
delete rest.vector_score;
|
|
3917
3959
|
delete rest.keyword_score;
|
|
3918
|
-
delete rest.
|
|
3919
|
-
delete rest.exact_field_score;
|
|
3960
|
+
delete rest.exact_name_score;
|
|
3920
3961
|
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
3921
3962
|
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
3922
3963
|
tableResults.push({
|
|
@@ -3939,12 +3980,23 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3939
3980
|
for (const tableResults of resultsArray) {
|
|
3940
3981
|
allResults.push(...tableResults);
|
|
3941
3982
|
}
|
|
3942
|
-
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);
|
|
3943
3995
|
if (finalSorted.length > 0) {
|
|
3944
3996
|
console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
|
|
3945
|
-
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)}`);
|
|
3946
3998
|
}
|
|
3947
|
-
return finalSorted.slice(0, topK);
|
|
3999
|
+
return finalSorted.slice(0, Math.max(topK, 15));
|
|
3948
4000
|
}
|
|
3949
4001
|
async delete(_id, _namespace) {
|
|
3950
4002
|
console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
|
|
@@ -3965,51 +4017,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
3965
4017
|
await this.pool.end();
|
|
3966
4018
|
}
|
|
3967
4019
|
}
|
|
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
|
-
}
|
|
4013
4020
|
};
|
|
4014
4021
|
|
|
4015
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
|
|