@retrivora-ai/rag-engine 0.2.9 → 0.3.1

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
  /**
@@ -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 = (_a = match[2]) != null ? _a : match[1];
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((_b = match[1]) != null ? _b : "");
2814
- const value = (_c = match[2]) != null ? _c : "";
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]+){1,3}\b/g)) {
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.__fieldHints = fieldHints;
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,44 @@ 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.__queryText;
3883
- const fieldHints = sanitizeFilterHints(_filter);
3884
- 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) : [];
3914
+ console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
3915
+ console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
3885
3916
  const queryPromises = this.tables.map(async (table) => {
3886
- var _a2, _b2, _c;
3887
3917
  try {
3888
3918
  let sqlQuery = "";
3889
3919
  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
3920
  if (queryText) {
3896
- const genericValueHints = fieldHints.filter((hint) => !hint.field).map((hint) => hint.value);
3897
- params = genericValueHints.length > 0 ? [vectorLiteral, queryText, genericValueHints] : [vectorLiteral, queryText];
3898
- const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
3899
- const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0 ? `GREATEST(${searchableFields.map(
3900
- (field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
3901
- ).join(", ")})` : "0";
3902
- const fieldSpecificChecks = fieldHints.filter((hint) => hint.field).map((hint) => {
3903
- const column = hint.field ? normalizedFieldMap.get(hint.field) : void 0;
3904
- if (!column) return null;
3905
- params.push(hint.value);
3906
- const paramIndex = params.length;
3907
- return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
3908
- }).filter((expr) => Boolean(expr));
3909
- const exactFieldScoreExpr = fieldSpecificChecks.length > 0 ? `GREATEST(${fieldSpecificChecks.join(", ")})` : "0";
3921
+ const hasEntityHints = entityHints.length > 0;
3922
+ const exactNameScoreExpr = hasEntityHints ? `+ (
3923
+ SELECT COALESCE(MAX(
3924
+ CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
3925
+ THEN 1.0 ELSE 0.0 END
3926
+ ), 0)
3927
+ FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
3928
+ WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
3929
+ ) * 3.0` : "";
3910
3930
  sqlQuery = `
3911
3931
  SELECT *,
3912
3932
  (1 - (embedding <=> $1::vector)) AS vector_score,
3913
- COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
3914
- ${exactValueScoreExpr} AS exact_value_score,
3915
- ${exactFieldScoreExpr} AS exact_field_score,
3916
- (
3917
- (1 - (embedding <=> $1::vector)) +
3918
- (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) +
3919
- (${exactValueScoreExpr} * 3.0) +
3920
- (${exactFieldScoreExpr} * 5.0)
3921
- ) AS hybrid_score
3933
+ COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
3934
+ ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
3922
3935
  FROM "${table}" t
3923
3936
  ORDER BY hybrid_score DESC
3924
3937
  LIMIT 50
3925
3938
  `;
3939
+ params = [vectorLiteral, queryText];
3926
3940
  } else {
3927
- params = [vectorLiteral];
3928
3941
  sqlQuery = `
3929
3942
  SELECT *,
3930
3943
  (1 - (embedding <=> $1::vector)) AS hybrid_score
@@ -3932,16 +3945,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3932
3945
  ORDER BY hybrid_score DESC
3933
3946
  LIMIT 50
3934
3947
  `;
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 ")}`);
3948
+ params = [vectorLiteral];
3945
3949
  }
3946
3950
  const result = await this.pool.query(sqlQuery, params);
3947
3951
  if (result.rowCount && result.rowCount > 0) {
@@ -3951,12 +3955,11 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3951
3955
  }
3952
3956
  const tableResults = [];
3953
3957
  for (const row of result.rows) {
3954
- const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
3958
+ const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
3955
3959
  delete rest.embedding;
3956
3960
  delete rest.vector_score;
3957
3961
  delete rest.keyword_score;
3958
- delete rest.exact_value_score;
3959
- delete rest.exact_field_score;
3962
+ delete rest.exact_name_score;
3960
3963
  const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
3961
3964
  ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
3962
3965
  tableResults.push({
@@ -3979,12 +3982,23 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3979
3982
  for (const tableResults of resultsArray) {
3980
3983
  allResults.push(...tableResults);
3981
3984
  }
3982
- const finalSorted = allResults.sort((a, b) => b.score - a.score);
3985
+ const resultsByTable = {};
3986
+ for (const res of allResults) {
3987
+ const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
3988
+ if (!resultsByTable[table]) resultsByTable[table] = [];
3989
+ resultsByTable[table].push(res);
3990
+ }
3991
+ const balancedResults = [];
3992
+ const tables = Object.keys(resultsByTable);
3993
+ for (const table of tables) {
3994
+ balancedResults.push(...resultsByTable[table].slice(0, 3));
3995
+ }
3996
+ const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
3983
3997
  if (finalSorted.length > 0) {
3984
3998
  console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
3985
- 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)}`);
3999
+ 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
4000
  }
3987
- return finalSorted.slice(0, topK);
4001
+ return finalSorted.slice(0, Math.max(topK, 15));
3988
4002
  }
3989
4003
  async delete(_id, _namespace) {
3990
4004
  console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
@@ -4005,51 +4019,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
4005
4019
  await this.pool.end();
4006
4020
  }
4007
4021
  }
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
4022
  };
4054
4023
 
4055
4024
  // 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-5U2DHPIX.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,44 @@ 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) : [];
422
+ console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
423
+ console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
436
424
  const queryPromises = this.tables.map(async (table) => {
437
- var _a2, _b2, _c;
438
425
  try {
439
426
  let sqlQuery = "";
440
427
  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
428
  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";
429
+ const hasEntityHints = entityHints.length > 0;
430
+ const exactNameScoreExpr = hasEntityHints ? `+ (
431
+ SELECT COALESCE(MAX(
432
+ CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
433
+ THEN 1.0 ELSE 0.0 END
434
+ ), 0)
435
+ FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
436
+ WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
437
+ ) * 3.0` : "";
461
438
  sqlQuery = `
462
439
  SELECT *,
463
440
  (1 - (embedding <=> $1::vector)) AS vector_score,
464
- 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
441
+ COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
442
+ ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
473
443
  FROM "${table}" t
474
444
  ORDER BY hybrid_score DESC
475
445
  LIMIT 50
476
446
  `;
447
+ params = [vectorLiteral, queryText];
477
448
  } else {
478
- params = [vectorLiteral];
479
449
  sqlQuery = `
480
450
  SELECT *,
481
451
  (1 - (embedding <=> $1::vector)) AS hybrid_score
@@ -483,16 +453,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
483
453
  ORDER BY hybrid_score DESC
484
454
  LIMIT 50
485
455
  `;
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 ")}`);
456
+ params = [vectorLiteral];
496
457
  }
497
458
  const result = await this.pool.query(sqlQuery, params);
498
459
  if (result.rowCount && result.rowCount > 0) {
@@ -502,12 +463,11 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
502
463
  }
503
464
  const tableResults = [];
504
465
  for (const row of result.rows) {
505
- const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
466
+ const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
506
467
  delete rest.embedding;
507
468
  delete rest.vector_score;
508
469
  delete rest.keyword_score;
509
- delete rest.exact_value_score;
510
- delete rest.exact_field_score;
470
+ delete rest.exact_name_score;
511
471
  const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
512
472
  ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
513
473
  tableResults.push({
@@ -530,12 +490,23 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
530
490
  for (const tableResults of resultsArray) {
531
491
  allResults.push(...tableResults);
532
492
  }
533
- const finalSorted = allResults.sort((a, b) => b.score - a.score);
493
+ const resultsByTable = {};
494
+ for (const res of allResults) {
495
+ const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
496
+ if (!resultsByTable[table]) resultsByTable[table] = [];
497
+ resultsByTable[table].push(res);
498
+ }
499
+ const balancedResults = [];
500
+ const tables = Object.keys(resultsByTable);
501
+ for (const table of tables) {
502
+ balancedResults.push(...resultsByTable[table].slice(0, 3));
503
+ }
504
+ const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
534
505
  if (finalSorted.length > 0) {
535
506
  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)}`);
507
+ 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
508
  }
538
- return finalSorted.slice(0, topK);
509
+ return finalSorted.slice(0, Math.max(topK, 15));
539
510
  }
540
511
  async delete(_id, _namespace) {
541
512
  console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
@@ -556,51 +527,6 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
556
527
  await this.pool.end();
557
528
  }
558
529
  }
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
530
  };
605
531
  export {
606
532
  AnthropicProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "0.2.9",
3
+ "version": "0.3.1",
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