@retrivora-ai/rag-engine 0.2.6 → 0.2.8

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,6 +2766,40 @@ var EmbeddingStrategyResolver = class {
2752
2766
  };
2753
2767
 
2754
2768
  // src/core/Pipeline.ts
2769
+ function normalizeHintValue(value) {
2770
+ return value.replace(/\s+/g, " ").trim();
2771
+ }
2772
+ function extractQueryFieldHints(question) {
2773
+ if (!question.trim()) return [];
2774
+ const hints = /* @__PURE__ */ new Map();
2775
+ const addHint = (value, field) => {
2776
+ const normalizedValue = normalizeHintValue(value);
2777
+ if (!normalizedValue) return;
2778
+ const normalizedField = field ? field.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() : void 0;
2779
+ const key = `${normalizedField != null ? normalizedField : "*"}::${normalizedValue.toLowerCase()}`;
2780
+ if (!hints.has(key)) {
2781
+ hints.set(key, __spreadValues({
2782
+ value: normalizedValue
2783
+ }, normalizedField ? { field: normalizedField } : {}));
2784
+ }
2785
+ };
2786
+ for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
2787
+ addHint(match[1]);
2788
+ }
2789
+ const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
2790
+ const valuePattern = `([^\\n?.!,]{1,120}?)`;
2791
+ const fieldValuePatterns = [
2792
+ new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
2793
+ new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
2794
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
2795
+ ];
2796
+ for (const pattern of fieldValuePatterns) {
2797
+ for (const match of question.matchAll(pattern)) {
2798
+ addHint(match[2], match[1]);
2799
+ }
2800
+ }
2801
+ return [...hints.values()];
2802
+ }
2755
2803
  var Pipeline = class {
2756
2804
  constructor(config) {
2757
2805
  this.initialised = false;
@@ -2839,7 +2887,11 @@ var Pipeline = class {
2839
2887
  const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
2840
2888
  try {
2841
2889
  const queryVector = await this.embeddingProvider.embed(question, { taskType: "query" });
2842
- const rawMatches = await this.vectorDB.query(queryVector, topK, ns, { __queryText: question });
2890
+ const fieldHints = extractQueryFieldHints(question);
2891
+ const rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
2892
+ __queryText: question,
2893
+ __fieldHints: fieldHints
2894
+ });
2843
2895
  const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
2844
2896
  const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
2845
2897
  ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
@@ -3692,10 +3744,26 @@ init_PostgreSQLProvider();
3692
3744
  // src/providers/vectordb/MultiTablePostgresProvider.ts
3693
3745
  var import_pg2 = require("pg");
3694
3746
  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
+ }
3695
3762
  var MultiTablePostgresProvider = class extends BaseVectorProvider {
3696
3763
  constructor(config) {
3697
3764
  var _a, _b, _c;
3698
3765
  super(config);
3766
+ this.tableSearchConfig = /* @__PURE__ */ new Map();
3699
3767
  const opts = config.options || {};
3700
3768
  if (!opts.connectionString) {
3701
3769
  throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
@@ -3734,6 +3802,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3734
3802
  if (this.tables.length === 0) {
3735
3803
  console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
3736
3804
  } else {
3805
+ this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
3737
3806
  console.log(
3738
3807
  `[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
3739
3808
  );
@@ -3763,7 +3832,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3763
3832
  * Query all configured tables and merge results, sorted by cosine similarity score.
3764
3833
  */
3765
3834
  async query(vector, topK, _namespace, _filter) {
3766
- var _a, _b, _c;
3835
+ var _a, _b;
3767
3836
  if (!this.pool) {
3768
3837
  throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
3769
3838
  }
@@ -3771,22 +3840,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3771
3840
  const allResults = [];
3772
3841
  console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
3773
3842
  const queryText = _filter == null ? void 0 : _filter.__queryText;
3843
+ const fieldHints = sanitizeFilterHints(_filter);
3844
+ const explicitFilter = stripInternalFilterKeys3(_filter);
3774
3845
  const queryPromises = this.tables.map(async (table) => {
3846
+ var _a2, _b2, _c;
3775
3847
  try {
3776
3848
  let sqlQuery = "";
3777
3849
  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 = [];
3778
3855
  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";
3779
3870
  sqlQuery = `
3780
3871
  SELECT *,
3781
3872
  (1 - (embedding <=> $1::vector)) AS vector_score,
3782
3873
  COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
3783
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0)) AS hybrid_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
3784
3882
  FROM "${table}" t
3785
3883
  ORDER BY hybrid_score DESC
3786
3884
  LIMIT 50
3787
3885
  `;
3788
- params = [vectorLiteral, queryText];
3789
3886
  } else {
3887
+ params = [vectorLiteral];
3790
3888
  sqlQuery = `
3791
3889
  SELECT *,
3792
3890
  (1 - (embedding <=> $1::vector)) AS hybrid_score
@@ -3794,7 +3892,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3794
3892
  ORDER BY hybrid_score DESC
3795
3893
  LIMIT 50
3796
3894
  `;
3797
- params = [vectorLiteral];
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 ")}`);
3798
3905
  }
3799
3906
  const result = await this.pool.query(sqlQuery, params);
3800
3907
  if (result.rowCount && result.rowCount > 0) {
@@ -3804,10 +3911,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3804
3911
  }
3805
3912
  const tableResults = [];
3806
3913
  for (const row of result.rows) {
3807
- const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
3914
+ const _d = row, { hybrid_score, id } = _d, rest = __objRest(_d, ["hybrid_score", "id"]);
3808
3915
  delete rest.embedding;
3809
3916
  delete rest.vector_score;
3810
3917
  delete rest.keyword_score;
3918
+ delete rest.exact_value_score;
3919
+ delete rest.exact_field_score;
3811
3920
  const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
3812
3921
  ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
3813
3922
  tableResults.push({
@@ -3830,23 +3939,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3830
3939
  for (const tableResults of resultsArray) {
3831
3940
  allResults.push(...tableResults);
3832
3941
  }
3833
- const resultsByTable = {};
3834
- for (const res of allResults) {
3835
- const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
3836
- if (!resultsByTable[table]) resultsByTable[table] = [];
3837
- resultsByTable[table].push(res);
3838
- }
3839
- const balancedResults = [];
3840
- const tables = Object.keys(resultsByTable);
3841
- for (const table of tables) {
3842
- balancedResults.push(...resultsByTable[table].slice(0, 3));
3843
- }
3844
- const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
3942
+ const finalSorted = allResults.sort((a, b) => b.score - a.score);
3845
3943
  if (finalSorted.length > 0) {
3846
3944
  console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
3847
- 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)}`);
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)}`);
3848
3946
  }
3849
- return finalSorted.slice(0, Math.max(topK, 15));
3947
+ return finalSorted.slice(0, topK);
3850
3948
  }
3851
3949
  async delete(_id, _namespace) {
3852
3950
  console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
@@ -3867,6 +3965,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3867
3965
  await this.pool.end();
3868
3966
  }
3869
3967
  }
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
+ }
3870
4013
  };
3871
4014
 
3872
4015
  // 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-7K4KXB6G.mjs";
37
+ } from "./chunk-CYVPACA7.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,10 +335,26 @@ 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
+ }
338
353
  var MultiTablePostgresProvider = class extends BaseVectorProvider {
339
354
  constructor(config) {
340
355
  var _a, _b, _c;
341
356
  super(config);
357
+ this.tableSearchConfig = /* @__PURE__ */ new Map();
342
358
  const opts = config.options || {};
343
359
  if (!opts.connectionString) {
344
360
  throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
@@ -377,6 +393,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
377
393
  if (this.tables.length === 0) {
378
394
  console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
379
395
  } else {
396
+ this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
380
397
  console.log(
381
398
  `[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
382
399
  );
@@ -406,7 +423,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
406
423
  * Query all configured tables and merge results, sorted by cosine similarity score.
407
424
  */
408
425
  async query(vector, topK, _namespace, _filter) {
409
- var _a, _b, _c;
426
+ var _a, _b;
410
427
  if (!this.pool) {
411
428
  throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
412
429
  }
@@ -414,22 +431,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
414
431
  const allResults = [];
415
432
  console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
416
433
  const queryText = _filter == null ? void 0 : _filter.__queryText;
434
+ const fieldHints = sanitizeFilterHints(_filter);
435
+ const explicitFilter = stripInternalFilterKeys(_filter);
417
436
  const queryPromises = this.tables.map(async (table) => {
437
+ var _a2, _b2, _c;
418
438
  try {
419
439
  let sqlQuery = "";
420
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 = [];
421
446
  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";
422
461
  sqlQuery = `
423
462
  SELECT *,
424
463
  (1 - (embedding <=> $1::vector)) AS vector_score,
425
464
  COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
426
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.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
427
473
  FROM "${table}" t
428
474
  ORDER BY hybrid_score DESC
429
475
  LIMIT 50
430
476
  `;
431
- params = [vectorLiteral, queryText];
432
477
  } else {
478
+ params = [vectorLiteral];
433
479
  sqlQuery = `
434
480
  SELECT *,
435
481
  (1 - (embedding <=> $1::vector)) AS hybrid_score
@@ -437,7 +483,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
437
483
  ORDER BY hybrid_score DESC
438
484
  LIMIT 50
439
485
  `;
440
- 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 ")}`);
441
496
  }
442
497
  const result = await this.pool.query(sqlQuery, params);
443
498
  if (result.rowCount && result.rowCount > 0) {
@@ -447,10 +502,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
447
502
  }
448
503
  const tableResults = [];
449
504
  for (const row of result.rows) {
450
- 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"]);
451
506
  delete rest.embedding;
452
507
  delete rest.vector_score;
453
508
  delete rest.keyword_score;
509
+ delete rest.exact_value_score;
510
+ delete rest.exact_field_score;
454
511
  const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
455
512
  ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
456
513
  tableResults.push({
@@ -473,23 +530,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
473
530
  for (const tableResults of resultsArray) {
474
531
  allResults.push(...tableResults);
475
532
  }
476
- const resultsByTable = {};
477
- for (const res of allResults) {
478
- const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
479
- if (!resultsByTable[table]) resultsByTable[table] = [];
480
- resultsByTable[table].push(res);
481
- }
482
- const balancedResults = [];
483
- const tables = Object.keys(resultsByTable);
484
- for (const table of tables) {
485
- balancedResults.push(...resultsByTable[table].slice(0, 3));
486
- }
487
- const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
533
+ const finalSorted = allResults.sort((a, b) => b.score - a.score);
488
534
  if (finalSorted.length > 0) {
489
535
  console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
490
- 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)}`);
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)}`);
491
537
  }
492
- return finalSorted.slice(0, Math.max(topK, 15));
538
+ return finalSorted.slice(0, topK);
493
539
  }
494
540
  async delete(_id, _namespace) {
495
541
  console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
@@ -510,6 +556,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
510
556
  await this.pool.end();
511
557
  }
512
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
+ }
513
604
  };
514
605
  export {
515
606
  AnthropicProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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
  }
@@ -7,6 +7,61 @@ import { BatchProcessor, BatchOptions } from './BatchProcessor';
7
7
  import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
8
8
  import { IngestDocument, ChatResponse } from '../types';
9
9
 
10
+ interface QueryFieldHint {
11
+ field?: string;
12
+ value: string;
13
+ }
14
+
15
+ function normalizeHintValue(value: string): string {
16
+ return value.replace(/\s+/g, ' ').trim();
17
+ }
18
+
19
+ function extractQueryFieldHints(question: string): QueryFieldHint[] {
20
+ if (!question.trim()) return [];
21
+
22
+ const hints = new Map<string, QueryFieldHint>();
23
+
24
+ const addHint = (value: string, field?: string) => {
25
+ const normalizedValue = normalizeHintValue(value);
26
+ if (!normalizedValue) return;
27
+
28
+ const normalizedField = field
29
+ ? field
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9]+/g, ' ')
32
+ .trim()
33
+ : undefined;
34
+
35
+ const key = `${normalizedField ?? '*'}::${normalizedValue.toLowerCase()}`;
36
+ if (!hints.has(key)) {
37
+ hints.set(key, {
38
+ value: normalizedValue,
39
+ ...(normalizedField ? { field: normalizedField } : {}),
40
+ });
41
+ }
42
+ };
43
+
44
+ for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
45
+ addHint(match[1]);
46
+ }
47
+
48
+ const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
49
+ const valuePattern = `([^\\n?.!,]{1,120}?)`;
50
+ const fieldValuePatterns = [
51
+ new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
52
+ new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
53
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
54
+ ];
55
+
56
+ for (const pattern of fieldValuePatterns) {
57
+ for (const match of question.matchAll(pattern)) {
58
+ addHint(match[2], match[1]);
59
+ }
60
+ }
61
+
62
+ return [...hints.values()];
63
+ }
64
+
10
65
  /**
11
66
  * Pipeline — orchestrates the RAG flow: Embed → Search → Augment → Generate.
12
67
  *
@@ -135,8 +190,12 @@ export class Pipeline {
135
190
 
136
191
  try {
137
192
  const queryVector = await this.embeddingProvider.embed(question, { taskType: 'query' });
193
+ const fieldHints = extractQueryFieldHints(question);
138
194
  // Pass the original text to the vector DB so it can perform hybrid (keyword) search if supported
139
- const rawMatches = await this.vectorDB.query(queryVector, topK, ns, { __queryText: question });
195
+ const rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
196
+ __queryText: question,
197
+ __fieldHints: fieldHints,
198
+ });
140
199
 
141
200
  const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
142
201
  const context = sources.length
@@ -3,6 +3,14 @@ import { VectorDBConfig } from '../../config/RagConfig';
3
3
  import { BaseVectorProvider } from './BaseVectorProvider';
4
4
  import { VectorMatch, UpsertDocument } from '../../types';
5
5
 
6
+ function stripInternalFilterKeys(filter?: Record<string, unknown>): Record<string, unknown> {
7
+ if (!filter) return {};
8
+
9
+ return Object.fromEntries(
10
+ Object.entries(filter).filter(([key]) => !key.startsWith('__'))
11
+ );
12
+ }
13
+
6
14
  /**
7
15
  * MongoDBProvider — MongoDB Atlas Vector Search implementation.
8
16
  */
@@ -67,6 +75,7 @@ export class MongoDBProvider extends BaseVectorProvider {
67
75
  }
68
76
 
69
77
  async query(vector: number[], topK: number, namespace?: string, filter?: Record<string, unknown>): Promise<VectorMatch[]> {
78
+ const publicFilter = stripInternalFilterKeys(filter);
70
79
  const pipeline: Record<string, unknown>[] = [
71
80
  {
72
81
  $vectorSearch: {
@@ -75,7 +84,9 @@ export class MongoDBProvider extends BaseVectorProvider {
75
84
  queryVector: vector,
76
85
  numCandidates: Math.max(topK * 10, 100),
77
86
  limit: topK,
78
- ...(filter || namespace ? { filter: { ...(filter || {}), ...(namespace ? { namespace } : {}) } } : {}),
87
+ ...(Object.keys(publicFilter).length > 0 || namespace
88
+ ? { filter: { ...publicFilter, ...(namespace ? { namespace } : {}) } }
89
+ : {}),
79
90
  },
80
91
  },
81
92
  {