@retrivora-ai/rag-engine 0.2.7 → 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,25 +2766,39 @@ 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 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
+ };
2758
2786
  for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
2759
- const value = match[1].trim();
2760
- if (value) hints.add(value);
2761
- }
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
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")
2765
2795
  ];
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);
2796
+ for (const pattern of fieldValuePatterns) {
2797
+ for (const match of question.matchAll(pattern)) {
2798
+ addHint(match[2], match[1]);
2771
2799
  }
2772
2800
  }
2773
- return [...hints].map((hint) => hint.replace(/\s+/g, " ").trim()).filter(Boolean);
2801
+ return [...hints.values()];
2774
2802
  }
2775
2803
  var Pipeline = class {
2776
2804
  constructor(config) {
@@ -2859,10 +2887,10 @@ var Pipeline = class {
2859
2887
  const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
2860
2888
  try {
2861
2889
  const queryVector = await this.embeddingProvider.embed(question, { taskType: "query" });
2862
- const entityHints = extractEntityHints(question);
2890
+ const fieldHints = extractQueryFieldHints(question);
2863
2891
  const rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
2864
2892
  __queryText: question,
2865
- __entityHints: entityHints
2893
+ __fieldHints: fieldHints
2866
2894
  });
2867
2895
  const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
2868
2896
  const context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
@@ -3716,11 +3744,26 @@ init_PostgreSQLProvider();
3716
3744
  // src/providers/vectordb/MultiTablePostgresProvider.ts
3717
3745
  var import_pg2 = require("pg");
3718
3746
  init_BaseVectorProvider();
3719
- var EXACT_MATCH_FIELDS = ["name", "product_name", "productname", "title"];
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
+ }
3720
3762
  var MultiTablePostgresProvider = class extends BaseVectorProvider {
3721
3763
  constructor(config) {
3722
3764
  var _a, _b, _c;
3723
3765
  super(config);
3766
+ this.tableSearchConfig = /* @__PURE__ */ new Map();
3724
3767
  const opts = config.options || {};
3725
3768
  if (!opts.connectionString) {
3726
3769
  throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
@@ -3759,6 +3802,7 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3759
3802
  if (this.tables.length === 0) {
3760
3803
  console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
3761
3804
  } else {
3805
+ this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
3762
3806
  console.log(
3763
3807
  `[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
3764
3808
  );
@@ -3796,28 +3840,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3796
3840
  const allResults = [];
3797
3841
  console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
3798
3842
  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) : [];
3843
+ const fieldHints = sanitizeFilterHints(_filter);
3844
+ const explicitFilter = stripInternalFilterKeys3(_filter);
3800
3845
  const queryPromises = this.tables.map(async (table) => {
3846
+ var _a2, _b2, _c;
3801
3847
  try {
3802
3848
  let sqlQuery = "";
3803
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 = [];
3804
3855
  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`
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`
3808
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";
3809
3870
  sqlQuery = `
3810
3871
  SELECT *,
3811
3872
  (1 - (embedding <=> $1::vector)) AS vector_score,
3812
3873
  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
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
3815
3882
  FROM "${table}" t
3816
3883
  ORDER BY hybrid_score DESC
3817
3884
  LIMIT 50
3818
3885
  `;
3819
- params = hasEntityHints ? [vectorLiteral, queryText, entityHints] : [vectorLiteral, queryText];
3820
3886
  } else {
3887
+ params = [vectorLiteral];
3821
3888
  sqlQuery = `
3822
3889
  SELECT *,
3823
3890
  (1 - (embedding <=> $1::vector)) AS hybrid_score
@@ -3825,7 +3892,16 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3825
3892
  ORDER BY hybrid_score DESC
3826
3893
  LIMIT 50
3827
3894
  `;
3828
- 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 ")}`);
3829
3905
  }
3830
3906
  const result = await this.pool.query(sqlQuery, params);
3831
3907
  if (result.rowCount && result.rowCount > 0) {
@@ -3835,11 +3911,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3835
3911
  }
3836
3912
  const tableResults = [];
3837
3913
  for (const row of result.rows) {
3838
- 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"]);
3839
3915
  delete rest.embedding;
3840
3916
  delete rest.vector_score;
3841
3917
  delete rest.keyword_score;
3842
- delete rest.exact_name_score;
3918
+ delete rest.exact_value_score;
3919
+ delete rest.exact_field_score;
3843
3920
  const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
3844
3921
  ` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
3845
3922
  tableResults.push({
@@ -3888,6 +3965,51 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
3888
3965
  await this.pool.end();
3889
3966
  }
3890
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
+ }
3891
4013
  };
3892
4014
 
3893
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-6Q7DNWTG.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,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.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,30 +7,59 @@ import { BatchProcessor, BatchOptions } from './BatchProcessor';
7
7
  import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
8
8
  import { IngestDocument, ChatResponse } from '../types';
9
9
 
10
- function extractEntityHints(question: string): string[] {
11
- const hints = new Set<string>();
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
+ };
12
43
 
13
44
  for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
14
- const value = match[1].trim();
15
- if (value) hints.add(value);
45
+ addHint(match[1]);
16
46
  }
17
47
 
18
- const namedEntityPatterns = [
19
- /\b(?:product\s+name|named|called)\s+([a-z0-9][\w\s-]{0,80})$/i,
20
- /\b(?:product\s+name|named|called)\s+([a-z0-9][\w\s-]{0,80})(?=[?.!,]|$)/i,
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'),
21
54
  ];
22
55
 
23
- for (const pattern of namedEntityPatterns) {
24
- const match = question.match(pattern);
25
- const value = match?.[1]?.trim();
26
- if (value) {
27
- hints.add(value);
56
+ for (const pattern of fieldValuePatterns) {
57
+ for (const match of question.matchAll(pattern)) {
58
+ addHint(match[2], match[1]);
28
59
  }
29
60
  }
30
61
 
31
- return [...hints]
32
- .map((hint) => hint.replace(/\s+/g, ' ').trim())
33
- .filter(Boolean);
62
+ return [...hints.values()];
34
63
  }
35
64
 
36
65
  /**
@@ -161,11 +190,11 @@ export class Pipeline {
161
190
 
162
191
  try {
163
192
  const queryVector = await this.embeddingProvider.embed(question, { taskType: 'query' });
164
- const entityHints = extractEntityHints(question);
193
+ const fieldHints = extractQueryFieldHints(question);
165
194
  // Pass the original text to the vector DB so it can perform hybrid (keyword) search if supported
166
195
  const rawMatches = await this.vectorDB.query(queryVector, topK, ns, {
167
196
  __queryText: question,
168
- __entityHints: entityHints,
197
+ __fieldHints: fieldHints,
169
198
  });
170
199
 
171
200
  const sources = rawMatches.filter((m) => m.score >= scoreThreshold);
@@ -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
  {