@retrivora-ai/rag-engine 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -217,6 +217,12 @@ var PostgreSQLProvider_exports = {};
217
217
  __export(PostgreSQLProvider_exports, {
218
218
  PostgreSQLProvider: () => PostgreSQLProvider
219
219
  });
220
+ function stripInternalFilterKeys(filter) {
221
+ if (!filter) return {};
222
+ return Object.fromEntries(
223
+ Object.entries(filter).filter(([key]) => !key.startsWith("__"))
224
+ );
225
+ }
220
226
  var import_pg, PostgreSQLProvider;
221
227
  var init_PostgreSQLProvider = __esm({
222
228
  "src/providers/vectordb/PostgreSQLProvider.ts"() {
@@ -309,8 +315,9 @@ var init_PostgreSQLProvider = __esm({
309
315
  let whereClause = namespace ? `WHERE namespace = $3` : "";
310
316
  const params = [vectorLiteral, topK];
311
317
  if (namespace) params.push(namespace);
312
- if (filter && Object.keys(filter).length > 0) {
313
- const filterConditions = Object.entries(filter).map(([key, val]) => {
318
+ const publicFilter = stripInternalFilterKeys(filter);
319
+ if (Object.keys(publicFilter).length > 0) {
320
+ const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
314
321
  const paramIdx = params.length + 1;
315
322
  params.push(JSON.stringify(val));
316
323
  return `metadata->>'${key}' = $${paramIdx}`;
@@ -360,6 +367,12 @@ var MongoDBProvider_exports = {};
360
367
  __export(MongoDBProvider_exports, {
361
368
  MongoDBProvider: () => MongoDBProvider
362
369
  });
370
+ function stripInternalFilterKeys2(filter) {
371
+ if (!filter) return {};
372
+ return Object.fromEntries(
373
+ Object.entries(filter).filter(([key]) => !key.startsWith("__"))
374
+ );
375
+ }
363
376
  var import_mongodb, MongoDBProvider;
364
377
  var init_MongoDBProvider = __esm({
365
378
  "src/providers/vectordb/MongoDBProvider.ts"() {
@@ -411,6 +424,7 @@ var init_MongoDBProvider = __esm({
411
424
  await this.collection.bulkWrite(operations);
412
425
  }
413
426
  async query(vector, topK, namespace, filter) {
427
+ const publicFilter = stripInternalFilterKeys2(filter);
414
428
  const pipeline = [
415
429
  {
416
430
  $vectorSearch: __spreadValues({
@@ -419,7 +433,7 @@ var init_MongoDBProvider = __esm({
419
433
  queryVector: vector,
420
434
  numCandidates: Math.max(topK * 10, 100),
421
435
  limit: topK
422
- }, filter || namespace ? { filter: __spreadValues(__spreadValues({}, filter || {}), namespace ? { namespace } : {}) } : {})
436
+ }, Object.keys(publicFilter).length > 0 || namespace ? { filter: __spreadValues(__spreadValues({}, publicFilter), namespace ? { namespace } : {}) } : {})
423
437
  },
424
438
  {
425
439
  $project: {
@@ -2752,25 +2766,80 @@ var EmbeddingStrategyResolver = class {
2752
2766
  };
2753
2767
 
2754
2768
  // src/core/Pipeline.ts
2755
- function extractEntityHints(question) {
2756
- var _a;
2757
- const hints = /* @__PURE__ */ new Set();
2769
+ function normalizeHintValue(value) {
2770
+ return value.replace(/\s+/g, " ").trim();
2771
+ }
2772
+ function isLikelyPromptPhrase(value) {
2773
+ return /^(what|which|who|where|when|why|how)\b/i.test(value.trim());
2774
+ }
2775
+ function extractQueryFieldHints(question) {
2776
+ var _a, _b, _c;
2777
+ if (!question.trim()) return [];
2778
+ const hints = /* @__PURE__ */ new Map();
2779
+ const addHint = (value, field) => {
2780
+ const normalizedValue = normalizeHintValue(value);
2781
+ if (!normalizedValue) return;
2782
+ const normalizedField = field ? field.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() : void 0;
2783
+ const key = `${normalizedField != null ? normalizedField : "*"}::${normalizedValue.toLowerCase()}`;
2784
+ if (!hints.has(key)) {
2785
+ hints.set(key, __spreadValues({
2786
+ value: normalizedValue
2787
+ }, normalizedField ? { field: normalizedField } : {}));
2788
+ }
2789
+ };
2758
2790
  for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
2759
- const value = match[1].trim();
2760
- if (value) hints.add(value);
2791
+ addHint(match[1]);
2761
2792
  }
2762
- const namedEntityPatterns = [
2763
- /\b(?:product\s+name|named|called)\s+([a-z0-9][\w\s-]{0,80})$/i,
2764
- /\b(?:product\s+name|named|called)\s+([a-z0-9][\w\s-]{0,80})(?=[?.!,]|$)/i
2793
+ const naturalQuestionPatterns = [
2794
+ /\b(?:what|which)\s+(?:is|are|was|were)\s+(?:the\s+)?([^?.!,]{1,60}?)\s+of\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
2795
+ /\b(?:who|what)\s+(?:is|are|was|were)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
2796
+ /\b(?:about|for|regarding)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi
2765
2797
  ];
2766
- for (const pattern of namedEntityPatterns) {
2767
- const match = question.match(pattern);
2768
- const value = (_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.trim();
2769
- if (value) {
2770
- hints.add(value);
2798
+ for (const pattern of naturalQuestionPatterns) {
2799
+ for (const match of question.matchAll(pattern)) {
2800
+ const value = (_a = match[2]) != null ? _a : match[1];
2801
+ if (value) addHint(value);
2802
+ }
2803
+ }
2804
+ const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
2805
+ const valuePattern = `([^\\n?.!,]{1,120}?)`;
2806
+ const fieldValuePatterns = [
2807
+ new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
2808
+ new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
2809
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
2810
+ ];
2811
+ for (const pattern of fieldValuePatterns) {
2812
+ for (const match of question.matchAll(pattern)) {
2813
+ const field = normalizeHintValue((_b = match[1]) != null ? _b : "");
2814
+ const value = (_c = match[2]) != null ? _c : "";
2815
+ if (field && !isLikelyPromptPhrase(field)) {
2816
+ addHint(value, field);
2817
+ } else {
2818
+ addHint(value);
2819
+ }
2820
+ }
2821
+ }
2822
+ for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3}\b/g)) {
2823
+ addHint(match[0]);
2824
+ }
2825
+ return [...hints.values()];
2826
+ }
2827
+ function buildQueryFilter(question, hints) {
2828
+ const filter = { metadata: {}, keywords: [], queryText: question };
2829
+ for (const hint of hints) {
2830
+ if (hint.field) {
2831
+ filter.metadata[hint.field] = hint.value;
2832
+ } else {
2833
+ filter.keywords.push(hint.value);
2771
2834
  }
2772
2835
  }
2773
- return [...hints].map((hint) => hint.replace(/\s+/g, " ").trim()).filter(Boolean);
2836
+ for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
2837
+ const term = normalizeHintValue(match[0]);
2838
+ if (term && !filter.keywords.includes(term)) filter.keywords.push(term);
2839
+ }
2840
+ if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
2841
+ if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
2842
+ return filter;
2774
2843
  }
2775
2844
  var Pipeline = class {
2776
2845
  constructor(config) {
@@ -2859,11 +2928,10 @@ var Pipeline = class {
2859
2928
  const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
2860
2929
  try {
2861
2930
  const queryVector = await this.embeddingProvider.embed(question, { taskType: "query" });
2862
- const entityHints = extractEntityHints(question);
2863
- const rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
2864
- __queryText: question,
2865
- __entityHints: entityHints
2866
- });
2931
+ const fieldHints = extractQueryFieldHints(question);
2932
+ const filter = buildQueryFilter(question, fieldHints);
2933
+ filter.__fieldHints = fieldHints;
2934
+ const rawMatches = await this.vectorDB.query(queryVector, topK, ns, filter);
2867
2935
  const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
2868
2936
  const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
2869
2937
  ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
@@ -3716,11 +3784,26 @@ init_PostgreSQLProvider();
3716
3784
  // src/providers/vectordb/MultiTablePostgresProvider.ts
3717
3785
  var import_pg2 = require("pg");
3718
3786
  init_BaseVectorProvider();
3719
- var EXACT_MATCH_FIELDS = ["name", "product_name", "productname", "title"];
3787
+ function normalizeFieldName(value) {
3788
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
3789
+ }
3790
+ function sanitizeFilterHints(rawFilter) {
3791
+ if (!Array.isArray(rawFilter == null ? void 0 : rawFilter.__fieldHints)) return [];
3792
+ return rawFilter.__fieldHints.filter((hint) => typeof hint === "object" && hint !== null).map((hint) => __spreadValues({
3793
+ value: typeof hint.value === "string" ? hint.value.trim().toLowerCase() : ""
3794
+ }, typeof hint.field === "string" && hint.field.trim() ? { field: normalizeFieldName(hint.field) } : {})).filter((hint) => Boolean(hint.value));
3795
+ }
3796
+ function stripInternalFilterKeys3(filter) {
3797
+ if (!filter) return {};
3798
+ return Object.fromEntries(
3799
+ Object.entries(filter).filter(([key]) => !key.startsWith("__"))
3800
+ );
3801
+ }
3720
3802
  var MultiTablePostgresProvider = class extends BaseVectorProvider {
3721
3803
  constructor(config) {
3722
3804
  var _a, _b, _c;
3723
3805
  super(config);
3806
+ this.tableSearchConfig = /* @__PURE__ */ new Map();
3724
3807
  const opts = config.options || {};
3725
3808
  if (!opts.connectionString) {
3726
3809
  throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
@@ -3759,6 +3842,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3759
3842
  if (this.tables.length === 0) {
3760
3843
  console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
3761
3844
  } else {
3845
+ this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
3762
3846
  console.log(
3763
3847
  `[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
3764
3848
  );
@@ -3796,28 +3880,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3796
3880
  const allResults = [];
3797
3881
  console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
3798
3882
  const queryText = _filter == null ? void 0 : _filter.__queryText;
3799
- const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter((hint) => typeof hint === "string").map((hint) => hint.trim().toLowerCase()).filter(Boolean) : [];
3883
+ const fieldHints = sanitizeFilterHints(_filter);
3884
+ const explicitFilter = stripInternalFilterKeys3(_filter);
3800
3885
  const queryPromises = this.tables.map(async (table) => {
3886
+ var _a2, _b2, _c;
3801
3887
  try {
3802
3888
  let sqlQuery = "";
3803
3889
  let params = [];
3890
+ const tableConfig = this.tableSearchConfig.get(table);
3891
+ const availableFields = (_a2 = tableConfig == null ? void 0 : tableConfig.availableFields) != null ? _a2 : [];
3892
+ const searchableFields = (_b2 = tableConfig == null ? void 0 : tableConfig.searchableFields) != null ? _b2 : [];
3893
+ const normalizedFieldMap = (_c = tableConfig == null ? void 0 : tableConfig.normalizedFieldMap) != null ? _c : /* @__PURE__ */ new Map();
3894
+ const whereConditions = [];
3804
3895
  if (queryText) {
3805
- const hasEntityHints = entityHints.length > 0;
3806
- const exactNameScoreExpr = hasEntityHints ? `GREATEST(${EXACT_MATCH_FIELDS.map(
3807
- (field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($3::text[]) THEN 1 ELSE 0 END`
3896
+ const genericValueHints = fieldHints.filter((hint) => !hint.field).map((hint) => hint.value);
3897
+ params = genericValueHints.length > 0 ? [vectorLiteral, queryText, genericValueHints] : [vectorLiteral, queryText];
3898
+ const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
3899
+ const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0 ? `GREATEST(${searchableFields.map(
3900
+ (field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
3808
3901
  ).join(", ")})` : "0";
3902
+ const fieldSpecificChecks = fieldHints.filter((hint) => hint.field).map((hint) => {
3903
+ const column = hint.field ? normalizedFieldMap.get(hint.field) : void 0;
3904
+ if (!column) return null;
3905
+ params.push(hint.value);
3906
+ const paramIndex = params.length;
3907
+ return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
3908
+ }).filter((expr) => Boolean(expr));
3909
+ const exactFieldScoreExpr = fieldSpecificChecks.length > 0 ? `GREATEST(${fieldSpecificChecks.join(", ")})` : "0";
3809
3910
  sqlQuery = `
3810
3911
  SELECT *,
3811
3912
  (1 - (embedding <=> $1::vector)) AS vector_score,
3812
3913
  COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
3813
- ${exactNameScoreExpr} AS exact_name_score,
3814
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) + (${exactNameScoreExpr} * 5.0)) AS hybrid_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
3815
3922
  FROM "${table}" t
3816
3923
  ORDER BY hybrid_score DESC
3817
3924
  LIMIT 50
3818
3925
  `;
3819
- params = hasEntityHints ? [vectorLiteral, queryText, entityHints] : [vectorLiteral, queryText];
3820
3926
  } else {
3927
+ params = [vectorLiteral];
3821
3928
  sqlQuery = `
3822
3929
  SELECT *,
3823
3930
  (1 - (embedding <=> $1::vector)) AS hybrid_score
@@ -3825,7 +3932,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3825
3932
  ORDER BY hybrid_score DESC
3826
3933
  LIMIT 50
3827
3934
  `;
3828
- params = [vectorLiteral];
3935
+ }
3936
+ for (const [key, value] of Object.entries(explicitFilter)) {
3937
+ const column = availableFields.includes(key) ? key : normalizedFieldMap.get(normalizeFieldName(key));
3938
+ if (!column) continue;
3939
+ params.push(String(value));
3940
+ whereConditions.push(`COALESCE(to_jsonb(t)->>'${column}', '') = $${params.length}::text`);
3941
+ }
3942
+ if (whereConditions.length > 0) {
3943
+ sqlQuery = sqlQuery.replace(`FROM "${table}" t`, `FROM "${table}" t
3944
+ WHERE ${whereConditions.join(" AND ")}`);
3829
3945
  }
3830
3946
  const result = await this.pool.query(sqlQuery, params);
3831
3947
  if (result.rowCount && result.rowCount > 0) {
@@ -3835,11 +3951,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3835
3951
  }
3836
3952
  const tableResults = [];
3837
3953
  for (const row of result.rows) {
3838
- const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
3954
+ const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
3839
3955
  delete rest.embedding;
3840
3956
  delete rest.vector_score;
3841
3957
  delete rest.keyword_score;
3842
- delete rest.exact_name_score;
3958
+ delete rest.exact_value_score;
3959
+ delete rest.exact_field_score;
3843
3960
  const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
3844
3961
  ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
3845
3962
  tableResults.push({
@@ -3888,6 +4005,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3888
4005
  await this.pool.end();
3889
4006
  }
3890
4007
  }
4008
+ async loadTableSearchConfig(client, tables) {
4009
+ var _a, _b;
4010
+ if (tables.length === 0) return /* @__PURE__ */ new Map();
4011
+ const configuredSearchFields = this.parseConfiguredSearchFields();
4012
+ const result = await client.query(`
4013
+ SELECT table_name, column_name, data_type
4014
+ FROM information_schema.columns
4015
+ WHERE table_schema = 'public'
4016
+ AND table_name = ANY($1::text[])
4017
+ ORDER BY ordinal_position
4018
+ `, [tables]);
4019
+ const rowsByTable = /* @__PURE__ */ new Map();
4020
+ for (const row of result.rows) {
4021
+ const rows = (_a = rowsByTable.get(row.table_name)) != null ? _a : [];
4022
+ rows.push({ column_name: row.column_name, data_type: row.data_type });
4023
+ rowsByTable.set(row.table_name, rows);
4024
+ }
4025
+ const configByTable = /* @__PURE__ */ new Map();
4026
+ for (const table of tables) {
4027
+ const columns = (_b = rowsByTable.get(table)) != null ? _b : [];
4028
+ const availableFields = columns.filter(({ column_name }) => column_name !== "embedding").map(({ column_name }) => column_name);
4029
+ const inferredFields = columns.filter(
4030
+ ({ column_name, data_type }) => column_name !== "embedding" && !["ARRAY", "json", "jsonb", "bytea", "tsvector", "USER-DEFINED"].includes(data_type)
4031
+ ).map(({ column_name }) => column_name);
4032
+ const searchableFields = configuredSearchFields.length > 0 ? configuredSearchFields.filter((field) => columns.some((column) => column.column_name === field)) : inferredFields;
4033
+ const normalizedFieldMap = /* @__PURE__ */ new Map();
4034
+ for (const field of availableFields) {
4035
+ normalizedFieldMap.set(normalizeFieldName(field), field);
4036
+ }
4037
+ configByTable.set(table, {
4038
+ availableFields,
4039
+ searchableFields,
4040
+ normalizedFieldMap
4041
+ });
4042
+ }
4043
+ return configByTable;
4044
+ }
4045
+ parseConfiguredSearchFields() {
4046
+ var _a;
4047
+ const raw = (_a = this.config.options) == null ? void 0 : _a.searchFields;
4048
+ if (Array.isArray(raw)) {
4049
+ return raw.filter((field) => typeof field === "string" && field.trim().length > 0);
4050
+ }
4051
+ return [];
4052
+ }
3891
4053
  };
3892
4054
 
3893
4055
  // src/server.ts
package/dist/server.mjs CHANGED
@@ -34,17 +34,17 @@ import {
34
34
  createIngestHandler,
35
35
  createUploadHandler,
36
36
  getRagConfig
37
- } from "./chunk-6Q7DNWTG.mjs";
37
+ } from "./chunk-5U2DHPIX.mjs";
38
38
  import "./chunk-EDLTMSNY.mjs";
39
39
  import {
40
40
  PineconeProvider
41
41
  } from "./chunk-BMHJTWSU.mjs";
42
42
  import {
43
43
  PostgreSQLProvider
44
- } from "./chunk-IUTAZ7QR.mjs";
44
+ } from "./chunk-6GSARSCP.mjs";
45
45
  import {
46
46
  MongoDBProvider
47
- } from "./chunk-5HXNKSCR.mjs";
47
+ } from "./chunk-IFPISZ2S.mjs";
48
48
  import {
49
49
  MilvusProvider
50
50
  } from "./chunk-VEJNRS4B.mjs";
@@ -335,11 +335,26 @@ function createFromPreset(presetName) {
335
335
 
336
336
  // src/providers/vectordb/MultiTablePostgresProvider.ts
337
337
  import { Pool } from "pg";
338
- var EXACT_MATCH_FIELDS = ["name", "product_name", "productname", "title"];
338
+ function normalizeFieldName(value) {
339
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
340
+ }
341
+ function sanitizeFilterHints(rawFilter) {
342
+ if (!Array.isArray(rawFilter == null ? void 0 : rawFilter.__fieldHints)) return [];
343
+ return rawFilter.__fieldHints.filter((hint) => typeof hint === "object" && hint !== null).map((hint) => __spreadValues({
344
+ value: typeof hint.value === "string" ? hint.value.trim().toLowerCase() : ""
345
+ }, typeof hint.field === "string" && hint.field.trim() ? { field: normalizeFieldName(hint.field) } : {})).filter((hint) => Boolean(hint.value));
346
+ }
347
+ function stripInternalFilterKeys(filter) {
348
+ if (!filter) return {};
349
+ return Object.fromEntries(
350
+ Object.entries(filter).filter(([key]) => !key.startsWith("__"))
351
+ );
352
+ }
339
353
  var MultiTablePostgresProvider = class extends BaseVectorProvider {
340
354
  constructor(config) {
341
355
  var _a, _b, _c;
342
356
  super(config);
357
+ this.tableSearchConfig = /* @__PURE__ */ new Map();
343
358
  const opts = config.options || {};
344
359
  if (!opts.connectionString) {
345
360
  throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
@@ -378,6 +393,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
378
393
  if (this.tables.length === 0) {
379
394
  console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
380
395
  } else {
396
+ this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
381
397
  console.log(
382
398
  `[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
383
399
  );
@@ -415,28 +431,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
415
431
  const allResults = [];
416
432
  console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
417
433
  const queryText = _filter == null ? void 0 : _filter.__queryText;
418
- const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter((hint) => typeof hint === "string").map((hint) => hint.trim().toLowerCase()).filter(Boolean) : [];
434
+ const fieldHints = sanitizeFilterHints(_filter);
435
+ const explicitFilter = stripInternalFilterKeys(_filter);
419
436
  const queryPromises = this.tables.map(async (table) => {
437
+ var _a2, _b2, _c;
420
438
  try {
421
439
  let sqlQuery = "";
422
440
  let params = [];
441
+ const tableConfig = this.tableSearchConfig.get(table);
442
+ const availableFields = (_a2 = tableConfig == null ? void 0 : tableConfig.availableFields) != null ? _a2 : [];
443
+ const searchableFields = (_b2 = tableConfig == null ? void 0 : tableConfig.searchableFields) != null ? _b2 : [];
444
+ const normalizedFieldMap = (_c = tableConfig == null ? void 0 : tableConfig.normalizedFieldMap) != null ? _c : /* @__PURE__ */ new Map();
445
+ const whereConditions = [];
423
446
  if (queryText) {
424
- const hasEntityHints = entityHints.length > 0;
425
- const exactNameScoreExpr = hasEntityHints ? `GREATEST(${EXACT_MATCH_FIELDS.map(
426
- (field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($3::text[]) THEN 1 ELSE 0 END`
447
+ const genericValueHints = fieldHints.filter((hint) => !hint.field).map((hint) => hint.value);
448
+ params = genericValueHints.length > 0 ? [vectorLiteral, queryText, genericValueHints] : [vectorLiteral, queryText];
449
+ const genericValueParamIndex = genericValueHints.length > 0 ? 3 : -1;
450
+ const exactValueScoreExpr = genericValueHints.length > 0 && searchableFields.length > 0 ? `GREATEST(${searchableFields.map(
451
+ (field) => `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
427
452
  ).join(", ")})` : "0";
453
+ const fieldSpecificChecks = fieldHints.filter((hint) => hint.field).map((hint) => {
454
+ const column = hint.field ? normalizedFieldMap.get(hint.field) : void 0;
455
+ if (!column) return null;
456
+ params.push(hint.value);
457
+ const paramIndex = params.length;
458
+ return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
459
+ }).filter((expr) => Boolean(expr));
460
+ const exactFieldScoreExpr = fieldSpecificChecks.length > 0 ? `GREATEST(${fieldSpecificChecks.join(", ")})` : "0";
428
461
  sqlQuery = `
429
462
  SELECT *,
430
463
  (1 - (embedding <=> $1::vector)) AS vector_score,
431
464
  COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
432
- ${exactNameScoreExpr} AS exact_name_score,
433
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) + (${exactNameScoreExpr} * 5.0)) AS hybrid_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
434
473
  FROM "${table}" t
435
474
  ORDER BY hybrid_score DESC
436
475
  LIMIT 50
437
476
  `;
438
- params = hasEntityHints ? [vectorLiteral, queryText, entityHints] : [vectorLiteral, queryText];
439
477
  } else {
478
+ params = [vectorLiteral];
440
479
  sqlQuery = `
441
480
  SELECT *,
442
481
  (1 - (embedding <=> $1::vector)) AS hybrid_score
@@ -444,7 +483,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
444
483
  ORDER BY hybrid_score DESC
445
484
  LIMIT 50
446
485
  `;
447
- params = [vectorLiteral];
486
+ }
487
+ for (const [key, value] of Object.entries(explicitFilter)) {
488
+ const column = availableFields.includes(key) ? key : normalizedFieldMap.get(normalizeFieldName(key));
489
+ if (!column) continue;
490
+ params.push(String(value));
491
+ whereConditions.push(`COALESCE(to_jsonb(t)->>'${column}', '') = $${params.length}::text`);
492
+ }
493
+ if (whereConditions.length > 0) {
494
+ sqlQuery = sqlQuery.replace(`FROM "${table}" t`, `FROM "${table}" t
495
+ WHERE ${whereConditions.join(" AND ")}`);
448
496
  }
449
497
  const result = await this.pool.query(sqlQuery, params);
450
498
  if (result.rowCount && result.rowCount > 0) {
@@ -454,11 +502,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
454
502
  }
455
503
  const tableResults = [];
456
504
  for (const row of result.rows) {
457
- const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
505
+ const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
458
506
  delete rest.embedding;
459
507
  delete rest.vector_score;
460
508
  delete rest.keyword_score;
461
- delete rest.exact_name_score;
509
+ delete rest.exact_value_score;
510
+ delete rest.exact_field_score;
462
511
  const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
463
512
  ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
464
513
  tableResults.push({
@@ -507,6 +556,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
507
556
  await this.pool.end();
508
557
  }
509
558
  }
559
+ async loadTableSearchConfig(client, tables) {
560
+ var _a, _b;
561
+ if (tables.length === 0) return /* @__PURE__ */ new Map();
562
+ const configuredSearchFields = this.parseConfiguredSearchFields();
563
+ const result = await client.query(`
564
+ SELECT table_name, column_name, data_type
565
+ FROM information_schema.columns
566
+ WHERE table_schema = 'public'
567
+ AND table_name = ANY($1::text[])
568
+ ORDER BY ordinal_position
569
+ `, [tables]);
570
+ const rowsByTable = /* @__PURE__ */ new Map();
571
+ for (const row of result.rows) {
572
+ const rows = (_a = rowsByTable.get(row.table_name)) != null ? _a : [];
573
+ rows.push({ column_name: row.column_name, data_type: row.data_type });
574
+ rowsByTable.set(row.table_name, rows);
575
+ }
576
+ const configByTable = /* @__PURE__ */ new Map();
577
+ for (const table of tables) {
578
+ const columns = (_b = rowsByTable.get(table)) != null ? _b : [];
579
+ const availableFields = columns.filter(({ column_name }) => column_name !== "embedding").map(({ column_name }) => column_name);
580
+ const inferredFields = columns.filter(
581
+ ({ column_name, data_type }) => column_name !== "embedding" && !["ARRAY", "json", "jsonb", "bytea", "tsvector", "USER-DEFINED"].includes(data_type)
582
+ ).map(({ column_name }) => column_name);
583
+ const searchableFields = configuredSearchFields.length > 0 ? configuredSearchFields.filter((field) => columns.some((column) => column.column_name === field)) : inferredFields;
584
+ const normalizedFieldMap = /* @__PURE__ */ new Map();
585
+ for (const field of availableFields) {
586
+ normalizedFieldMap.set(normalizeFieldName(field), field);
587
+ }
588
+ configByTable.set(table, {
589
+ availableFields,
590
+ searchableFields,
591
+ normalizedFieldMap
592
+ });
593
+ }
594
+ return configByTable;
595
+ }
596
+ parseConfiguredSearchFields() {
597
+ var _a;
598
+ const raw = (_a = this.config.options) == null ? void 0 : _a.searchFields;
599
+ if (Array.isArray(raw)) {
600
+ return raw.filter((field) => typeof field === "string" && field.trim().length > 0);
601
+ }
602
+ return [];
603
+ }
510
604
  };
511
605
  export {
512
606
  AnthropicProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -28,6 +28,10 @@ export interface VectorDBConfig {
28
28
  * - idPath?: string (e.g. '_id')
29
29
  * - scorePath?: string (e.g. 'similarity')
30
30
  * - contentPath?: string (e.g. 'text')
31
+ *
32
+ * For multi-table PostgreSQL search, the following options are also supported:
33
+ * - tables?: string[] | string
34
+ * - searchFields?: string[] // optional override for which columns receive exact-match boosts
31
35
  */
32
36
  options: Record<string, unknown>;
33
37
  }