@retrivora-ai/rag-engine 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{RagConfig--ibz0b3W.d.mts → RagConfig-D3Inaf9N.d.mts} +1 -1
- package/dist/{RagConfig--ibz0b3W.d.ts → RagConfig-D3Inaf9N.d.ts} +1 -1
- package/dist/{chunk-5U2DHPIX.mjs → chunk-GT72OIOD.mjs} +49 -6
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +49 -6
- package/dist/handlers/index.mjs +1 -1
- package/dist/{index-Dr1HN0se.d.ts → index-BhNJQ2SS.d.ts} +1 -1
- package/dist/{index-w8qIEFvi.d.mts → index-Ymwm-_OR.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/server.d.mts +5 -7
- package/dist/server.d.ts +5 -7
- package/dist/server.js +88 -119
- package/dist/server.mjs +40 -114
- package/package.json +1 -1
- package/src/config/RagConfig.ts +1 -1
- package/src/core/ConfigValidator.ts +16 -0
- package/src/core/Pipeline.ts +42 -5
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +62 -168
package/src/core/Pipeline.ts
CHANGED
|
@@ -37,9 +37,9 @@ function extractQueryFieldHints(question: string): QueryFieldHint[] {
|
|
|
37
37
|
|
|
38
38
|
const normalizedField = field
|
|
39
39
|
? field
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
42
|
+
.trim()
|
|
43
43
|
: undefined;
|
|
44
44
|
|
|
45
45
|
const key = `${normalizedField ?? '*'}::${normalizedValue.toLowerCase()}`;
|
|
@@ -61,6 +61,43 @@ function extractQueryFieldHints(question: string): QueryFieldHint[] {
|
|
|
61
61
|
/\b(?:about|for|regarding)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
62
62
|
];
|
|
63
63
|
|
|
64
|
+
// Targeted patterns: map common person/company question forms to a `name` metadata hint
|
|
65
|
+
const personCompanyPatterns = [
|
|
66
|
+
/\bcompany(?:\s+name)?\s+(?:of|for)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
|
|
67
|
+
/\b(?:which|what)\s+company\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi,
|
|
68
|
+
/\bwhere\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
for (const pattern of personCompanyPatterns) {
|
|
72
|
+
for (const match of question.matchAll(pattern)) {
|
|
73
|
+
const name = match[1];
|
|
74
|
+
if (name) addHint(name, 'name');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Universal patterns: capture common data types across domains so the system
|
|
79
|
+
// can construct metadata filters for arbitrary datasets. These are intentionally
|
|
80
|
+
// generic and provider-agnostic (e.g., `email`, `phone`, `date`, `amount`, `id`).
|
|
81
|
+
const universalPatterns: Array<{ regex: RegExp; field?: string; group?: number }> = [
|
|
82
|
+
{ regex: /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi, field: 'email', group: 1 },
|
|
83
|
+
{ regex: /(\+?\d[\d\-\.\s\(\)]{6,}\d)/g, field: 'phone', group: 1 },
|
|
84
|
+
{ regex: /(\b\d{4}-\d{2}-\d{2}\b|\b\d{1,2}\/\d{1,2}\/\d{2,4}\b)/g, field: 'date', group: 1 },
|
|
85
|
+
{ regex: /(\$\s?\d{1,3}(?:,\d{3})*(?:\.\d+)?)/g, field: 'amount', group: 1 },
|
|
86
|
+
{ regex: /\b(ID|id|identifier)[: ]\s*([A-Za-z0-9\-]{3,})\b/gi, field: 'id', group: 2 },
|
|
87
|
+
// Generic quoted phrase / proper-noun sequences as keywords (already partially handled above)
|
|
88
|
+
{ regex: /"([^"]{2,120})"/g, group: 1 },
|
|
89
|
+
{ regex: /'([^']{2,120})'/g, group: 1 },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const p of universalPatterns) {
|
|
93
|
+
for (const match of question.matchAll(p.regex)) {
|
|
94
|
+
const val = p.group ? match[p.group] ?? match[0] : match[0];
|
|
95
|
+
if (!val) continue;
|
|
96
|
+
if (p.field) addHint(val, p.field);
|
|
97
|
+
else addHint(val);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
64
101
|
for (const pattern of naturalQuestionPatterns) {
|
|
65
102
|
for (const match of question.matchAll(pattern)) {
|
|
66
103
|
const value = match[2] ?? match[1];
|
|
@@ -89,7 +126,7 @@ function extractQueryFieldHints(question: string): QueryFieldHint[] {
|
|
|
89
126
|
}
|
|
90
127
|
}
|
|
91
128
|
|
|
92
|
-
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){
|
|
129
|
+
for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
|
|
93
130
|
addHint(match[0]);
|
|
94
131
|
}
|
|
95
132
|
|
|
@@ -257,7 +294,7 @@ export class Pipeline {
|
|
|
257
294
|
const filter = buildQueryFilter(question, fieldHints) as Record<string, unknown>;
|
|
258
295
|
|
|
259
296
|
// Keep the raw hints available as well for providers that want richer parsing
|
|
260
|
-
filter.
|
|
297
|
+
filter.__entityHints = fieldHints;
|
|
261
298
|
|
|
262
299
|
const rawMatches = await this.vectorDB.query(queryVector, topK, ns, filter);
|
|
263
300
|
|
|
@@ -1,44 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
import { Pool, PoolClient } from 'pg';
|
|
2
5
|
import { BaseVectorProvider } from './BaseVectorProvider';
|
|
3
6
|
import { VectorMatch, UpsertDocument } from '../../types';
|
|
4
7
|
import { VectorDBConfig } from '../../config/RagConfig';
|
|
5
8
|
|
|
6
|
-
interface QueryFieldHint {
|
|
7
|
-
field?: string;
|
|
8
|
-
value: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface TableSearchConfig {
|
|
12
|
-
availableFields: string[];
|
|
13
|
-
searchableFields: string[];
|
|
14
|
-
normalizedFieldMap: Map<string, string>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeFieldName(value: string): string {
|
|
18
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function sanitizeFilterHints(rawFilter?: Record<string, unknown>): QueryFieldHint[] {
|
|
22
|
-
if (!Array.isArray(rawFilter?.__fieldHints)) return [];
|
|
23
|
-
|
|
24
|
-
return (rawFilter.__fieldHints as unknown[])
|
|
25
|
-
.filter((hint): hint is QueryFieldHint => typeof hint === 'object' && hint !== null)
|
|
26
|
-
.map((hint) => ({
|
|
27
|
-
value: typeof hint.value === 'string' ? hint.value.trim().toLowerCase() : '',
|
|
28
|
-
...(typeof hint.field === 'string' && hint.field.trim()
|
|
29
|
-
? { field: normalizeFieldName(hint.field) }
|
|
30
|
-
: {}),
|
|
31
|
-
}))
|
|
32
|
-
.filter((hint) => Boolean(hint.value));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stripInternalFilterKeys(filter?: Record<string, unknown>): Record<string, unknown> {
|
|
36
|
-
if (!filter) return {};
|
|
37
|
-
|
|
38
|
-
return Object.fromEntries(
|
|
39
|
-
Object.entries(filter).filter(([key]) => !key.startsWith('__'))
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
9
|
|
|
43
10
|
/**
|
|
44
11
|
* MultiTablePostgresProvider — PostgreSQL implementation that searches across
|
|
@@ -53,7 +20,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
53
20
|
private readonly dimensions: number;
|
|
54
21
|
private readonly connectionString: string;
|
|
55
22
|
private tables: string[];
|
|
56
|
-
private
|
|
23
|
+
private searchFields: string[];
|
|
57
24
|
|
|
58
25
|
constructor(config: VectorDBConfig) {
|
|
59
26
|
super(config);
|
|
@@ -80,6 +47,16 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
80
47
|
'Set VECTOR_DB_TABLES as a comma-separated list of table names or pass options.tables.'
|
|
81
48
|
);
|
|
82
49
|
}
|
|
50
|
+
|
|
51
|
+
// Exact match fields can come from options.searchFields or be derived from VECTOR_DB_SEARCH_FIELDS env var
|
|
52
|
+
const rawSearchFields: string | string[] =
|
|
53
|
+
(opts.searchFields as string | string[]) ??
|
|
54
|
+
process.env.VECTOR_DB_SEARCH_FIELDS ??
|
|
55
|
+
['name', 'product_name', 'productname', 'title'];
|
|
56
|
+
|
|
57
|
+
this.searchFields = typeof rawSearchFields === 'string'
|
|
58
|
+
? rawSearchFields.split(',').map((f) => f.trim()).filter(Boolean)
|
|
59
|
+
: rawSearchFields;
|
|
83
60
|
}
|
|
84
61
|
|
|
85
62
|
async initialize(): Promise<void> {
|
|
@@ -114,7 +91,6 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
114
91
|
if (this.tables.length === 0) {
|
|
115
92
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
116
93
|
} else {
|
|
117
|
-
this.tableSearchConfig = await this.loadTableSearchConfig(client, this.tables);
|
|
118
94
|
console.log(
|
|
119
95
|
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(', ')}`
|
|
120
96
|
);
|
|
@@ -162,74 +138,50 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
162
138
|
|
|
163
139
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
164
140
|
|
|
165
|
-
const queryText = _filter?.
|
|
166
|
-
const
|
|
167
|
-
|
|
141
|
+
const queryText = _filter?.queryText as string | undefined;
|
|
142
|
+
const entityHints = Array.isArray(_filter?.__entityHints)
|
|
143
|
+
? (_filter.__entityHints as unknown[])
|
|
144
|
+
.filter((hint): hint is { value: string; field?: string } =>
|
|
145
|
+
typeof hint === 'object' && hint !== null && typeof (hint as Record<string, unknown>).value === 'string'
|
|
146
|
+
)
|
|
147
|
+
.map((hint) => hint.value.trim().toLowerCase())
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
: [];
|
|
150
|
+
|
|
151
|
+
console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
|
|
152
|
+
console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(', ')}]`);
|
|
168
153
|
|
|
169
154
|
const queryPromises = this.tables.map(async (table) => {
|
|
170
155
|
try {
|
|
171
156
|
let sqlQuery = '';
|
|
172
157
|
let params: unknown[] = [];
|
|
173
|
-
const tableConfig = this.tableSearchConfig.get(table);
|
|
174
|
-
const availableFields = tableConfig?.availableFields ?? [];
|
|
175
|
-
const searchableFields = tableConfig?.searchableFields ?? [];
|
|
176
|
-
const normalizedFieldMap = tableConfig?.normalizedFieldMap ?? new Map<string, string>();
|
|
177
|
-
const whereConditions: string[] = [];
|
|
178
158
|
|
|
179
159
|
if (queryText) {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
? `GREATEST(${searchableFields.map((field) =>
|
|
192
|
-
`CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${field}', '')) = ANY($${genericValueParamIndex}::text[]) THEN 1 ELSE 0 END`
|
|
193
|
-
).join(', ')})`
|
|
194
|
-
: '0';
|
|
195
|
-
|
|
196
|
-
const fieldSpecificChecks = fieldHints
|
|
197
|
-
.filter((hint) => hint.field)
|
|
198
|
-
.map((hint) => {
|
|
199
|
-
const column = hint.field ? normalizedFieldMap.get(hint.field) : undefined;
|
|
200
|
-
if (!column) return null;
|
|
201
|
-
params.push(hint.value);
|
|
202
|
-
const paramIndex = params.length;
|
|
203
|
-
return `CASE WHEN LOWER(COALESCE(to_jsonb(t)->>'${column}', '')) = $${paramIndex}::text THEN 1 ELSE 0 END`;
|
|
204
|
-
})
|
|
205
|
-
.filter((expr): expr is string => Boolean(expr));
|
|
206
|
-
|
|
207
|
-
const exactFieldScoreExpr = fieldSpecificChecks.length > 0
|
|
208
|
-
? `GREATEST(${fieldSpecificChecks.join(', ')})`
|
|
209
|
-
: '0';
|
|
160
|
+
const hasEntityHints = entityHints.length > 0;
|
|
161
|
+
const exactNameScoreExpr = hasEntityHints
|
|
162
|
+
? `+ (
|
|
163
|
+
SELECT COALESCE(MAX(
|
|
164
|
+
CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(', ')})
|
|
165
|
+
THEN 1.0 ELSE 0.0 END
|
|
166
|
+
), 0)
|
|
167
|
+
FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
|
|
168
|
+
WHERE key IN (${this.searchFields.map(f => `'${f}'`).join(', ')})
|
|
169
|
+
) * 3.0`
|
|
170
|
+
: '';
|
|
210
171
|
|
|
211
172
|
// Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
|
|
212
|
-
// We convert plainto_tsquery's AND (&) operator to OR (|) so that natural language queries
|
|
213
|
-
// (which contain words that are not present verbatim in the row text) still match the intended records.
|
|
214
173
|
sqlQuery = `
|
|
215
174
|
SELECT *,
|
|
216
175
|
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
217
|
-
COALESCE(ts_rank(to_tsvector('english', t
|
|
218
|
-
${
|
|
219
|
-
${exactFieldScoreExpr} AS exact_field_score,
|
|
220
|
-
(
|
|
221
|
-
(1 - (embedding <=> $1::vector)) +
|
|
222
|
-
(COALESCE(ts_rank(to_tsvector('english', t.*::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) +
|
|
223
|
-
(${exactValueScoreExpr} * 3.0) +
|
|
224
|
-
(${exactFieldScoreExpr} * 5.0)
|
|
225
|
-
) AS hybrid_score
|
|
176
|
+
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
|
|
177
|
+
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
226
178
|
FROM "${table}" t
|
|
227
179
|
ORDER BY hybrid_score DESC
|
|
228
180
|
LIMIT 50
|
|
229
181
|
`;
|
|
182
|
+
params = [vectorLiteral, queryText];
|
|
230
183
|
} else {
|
|
231
184
|
// Fallback to Vector-only Search
|
|
232
|
-
params = [vectorLiteral];
|
|
233
185
|
sqlQuery = `
|
|
234
186
|
SELECT *,
|
|
235
187
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
@@ -237,19 +189,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
237
189
|
ORDER BY hybrid_score DESC
|
|
238
190
|
LIMIT 50
|
|
239
191
|
`;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
for (const [key, value] of Object.entries(explicitFilter)) {
|
|
243
|
-
const column = availableFields.includes(key)
|
|
244
|
-
? key
|
|
245
|
-
: normalizedFieldMap.get(normalizeFieldName(key));
|
|
246
|
-
if (!column) continue;
|
|
247
|
-
params.push(String(value));
|
|
248
|
-
whereConditions.push(`COALESCE(to_jsonb(t)->>'${column}', '') = $${params.length}::text`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (whereConditions.length > 0) {
|
|
252
|
-
sqlQuery = sqlQuery.replace(`FROM "${table}" t`, `FROM "${table}" t\n WHERE ${whereConditions.join(' AND ')}`);
|
|
192
|
+
params = [vectorLiteral];
|
|
253
193
|
}
|
|
254
194
|
|
|
255
195
|
const result = await this.pool.query(sqlQuery, params);
|
|
@@ -266,8 +206,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
266
206
|
delete rest.embedding;
|
|
267
207
|
delete rest.vector_score;
|
|
268
208
|
delete rest.keyword_score;
|
|
269
|
-
delete rest.
|
|
270
|
-
delete rest.exact_field_score;
|
|
209
|
+
delete rest.exact_name_score;
|
|
271
210
|
|
|
272
211
|
const content = `[TYPE: ${table.replace(/s$/, '').toUpperCase()}]\n` +
|
|
273
212
|
Object.entries(rest)
|
|
@@ -297,14 +236,32 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
297
236
|
allResults.push(...tableResults);
|
|
298
237
|
}
|
|
299
238
|
|
|
300
|
-
|
|
239
|
+
// 1. Group results by table to ensure we take the top ones from each
|
|
240
|
+
const resultsByTable: Record<string, VectorMatch[]> = {};
|
|
241
|
+
for (const res of allResults) {
|
|
242
|
+
const table = res.metadata?.source_table as string;
|
|
243
|
+
if (!resultsByTable[table]) resultsByTable[table] = [];
|
|
244
|
+
resultsByTable[table].push(res);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 2. Take top 3 from EACH table guaranteed, then fill the rest with overall best
|
|
248
|
+
const balancedResults: VectorMatch[] = [];
|
|
249
|
+
const tables = Object.keys(resultsByTable);
|
|
250
|
+
|
|
251
|
+
// First, ensure every table's best match is included
|
|
252
|
+
for (const table of tables) {
|
|
253
|
+
balancedResults.push(...resultsByTable[table].slice(0, 3));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 3. Sort the balanced list by score
|
|
257
|
+
const finalSorted = balancedResults.sort((a, b) => b.score - a.score);
|
|
301
258
|
|
|
302
259
|
if (finalSorted.length > 0) {
|
|
303
260
|
console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
|
|
304
261
|
console.log(`[MultiTablePostgresProvider] Final top match from "${finalSorted[0].metadata?.source_table ?? 'unknown'}" with score ${finalSorted[0].score.toFixed(4)}`);
|
|
305
262
|
}
|
|
306
263
|
|
|
307
|
-
return finalSorted.slice(0, topK);
|
|
264
|
+
return finalSorted.slice(0, Math.max(topK, 15));
|
|
308
265
|
}
|
|
309
266
|
|
|
310
267
|
async delete(_id: string | number, _namespace?: string): Promise<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
@@ -329,67 +286,4 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
329
286
|
await this.pool.end();
|
|
330
287
|
}
|
|
331
288
|
}
|
|
332
|
-
|
|
333
|
-
private async loadTableSearchConfig(
|
|
334
|
-
client: PoolClient,
|
|
335
|
-
tables: string[]
|
|
336
|
-
): Promise<Map<string, TableSearchConfig>> {
|
|
337
|
-
if (tables.length === 0) return new Map();
|
|
338
|
-
|
|
339
|
-
const configuredSearchFields = this.parseConfiguredSearchFields();
|
|
340
|
-
const result = await client.query(`
|
|
341
|
-
SELECT table_name, column_name, data_type
|
|
342
|
-
FROM information_schema.columns
|
|
343
|
-
WHERE table_schema = 'public'
|
|
344
|
-
AND table_name = ANY($1::text[])
|
|
345
|
-
ORDER BY ordinal_position
|
|
346
|
-
`, [tables]);
|
|
347
|
-
|
|
348
|
-
const rowsByTable = new Map<string, Array<{ column_name: string; data_type: string }>>();
|
|
349
|
-
for (const row of result.rows as Array<{ table_name: string; column_name: string; data_type: string }>) {
|
|
350
|
-
const rows = rowsByTable.get(row.table_name) ?? [];
|
|
351
|
-
rows.push({ column_name: row.column_name, data_type: row.data_type });
|
|
352
|
-
rowsByTable.set(row.table_name, rows);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const configByTable = new Map<string, TableSearchConfig>();
|
|
356
|
-
for (const table of tables) {
|
|
357
|
-
const columns = rowsByTable.get(table) ?? [];
|
|
358
|
-
const availableFields = columns
|
|
359
|
-
.filter(({ column_name }) => column_name !== 'embedding')
|
|
360
|
-
.map(({ column_name }) => column_name);
|
|
361
|
-
|
|
362
|
-
const inferredFields = columns
|
|
363
|
-
.filter(({ column_name, data_type }) =>
|
|
364
|
-
column_name !== 'embedding' &&
|
|
365
|
-
!['ARRAY', 'json', 'jsonb', 'bytea', 'tsvector', 'USER-DEFINED'].includes(data_type)
|
|
366
|
-
)
|
|
367
|
-
.map(({ column_name }) => column_name);
|
|
368
|
-
|
|
369
|
-
const searchableFields = configuredSearchFields.length > 0
|
|
370
|
-
? configuredSearchFields.filter((field) => columns.some((column) => column.column_name === field))
|
|
371
|
-
: inferredFields;
|
|
372
|
-
|
|
373
|
-
const normalizedFieldMap = new Map<string, string>();
|
|
374
|
-
for (const field of availableFields) {
|
|
375
|
-
normalizedFieldMap.set(normalizeFieldName(field), field);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
configByTable.set(table, {
|
|
379
|
-
availableFields,
|
|
380
|
-
searchableFields,
|
|
381
|
-
normalizedFieldMap,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return configByTable;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private parseConfiguredSearchFields(): string[] {
|
|
389
|
-
const raw = this.config.options?.searchFields;
|
|
390
|
-
if (Array.isArray(raw)) {
|
|
391
|
-
return raw.filter((field): field is string => typeof field === 'string' && field.trim().length > 0);
|
|
392
|
-
}
|
|
393
|
-
return [];
|
|
394
|
-
}
|
|
395
289
|
}
|