@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/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
- addHint(match[2], match[1]);
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 rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
2892
- __queryText: question,
2893
- __fieldHints: fieldHints
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.__queryText;
3843
- const fieldHints = sanitizeFilterHints(_filter);
3844
- const explicitFilter = stripInternalFilterKeys3(_filter);
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 genericValueHints = fieldHints.filter((hint) => !hint.field).map((hint) => hint.value);
3857
- params = genericValueHints.length > 0 ? [vectorLiteral, queryText, genericValueHints] : [vectorLiteral, queryText];
3858
- const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
3859
- const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0 ? `GREATEST(${searchableFields.map(
3860
- (field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
3861
- ).join(", ")})` : "0";
3862
- const fieldSpecificChecks = fieldHints.filter((hint) => hint.field).map((hint) => {
3863
- const column = hint.field ? normalizedFieldMap.get(hint.field) : void 0;
3864
- if (!column) return null;
3865
- params.push(hint.value);
3866
- const paramIndex = params.length;
3867
- return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
3868
- }).filter((expr) => Boolean(expr));
3869
- const exactFieldScoreExpr = fieldSpecificChecks.length > 0 ? `GREATEST(${fieldSpecificChecks.join(", ")})` : "0";
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
- ${exactValueScoreExpr} AS exact_value_score,
3875
- ${exactFieldScoreExpr} AS exact_field_score,
3876
- (
3877
- (1 - (embedding <=> $1::vector)) +
3878
- (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) +
3879
- (${exactValueScoreExpr} * 3.0) +
3880
- (${exactFieldScoreExpr} * 5.0)
3881
- ) AS hybrid_score
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 _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
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.exact_value_score;
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 finalSorted = allResults.sort((a, b) => b.score - a.score);
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 "${(_b = (_a = finalSorted[0].metadata) == null ? void 0 : _a.source_table) != null ? _b : "unknown"}" with score ${finalSorted[0].score.toFixed(4)}`);
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-CYVPACA7.mjs";
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.__queryText;
434
- const fieldHints = sanitizeFilterHints(_filter);
435
- const explicitFilter = stripInternalFilterKeys(_filter);
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 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`
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";
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
- ${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
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 _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
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.exact_value_score;
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 finalSorted = allResults.sort((a, b) => b.score - a.score);
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 "${(_b = (_a = finalSorted[0].metadata) == null ? void 0 : _a.source_table) != null ? _b : "unknown"}" with score ${finalSorted[0].score.toFixed(4)}`);
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.2.8",
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",
@@ -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