@jungjaehoon/mama-core 1.4.2 → 1.5.0
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/db/migrations/026-create-canonical-entity-tables.sql +99 -0
- package/db/migrations/027-create-entity-review-support.sql +44 -0
- package/db/migrations/028-create-entity-audit-tables.sql +45 -0
- package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
- package/db/migrations/030-case-first-memory-substrate.sql +901 -0
- package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
- package/dist/canonicalize.d.ts +38 -0
- package/dist/canonicalize.js +106 -0
- package/dist/cases/case-links.d.ts +116 -0
- package/dist/cases/case-links.js +794 -0
- package/dist/cases/composition-overrides.d.ts +90 -0
- package/dist/cases/composition-overrides.js +724 -0
- package/dist/cases/corrections.d.ts +156 -0
- package/dist/cases/corrections.js +1045 -0
- package/dist/cases/freshness.d.ts +57 -0
- package/dist/cases/freshness.js +283 -0
- package/dist/cases/live-state.d.ts +48 -0
- package/dist/cases/live-state.js +256 -0
- package/dist/cases/membership-explain.d.ts +56 -0
- package/dist/cases/membership-explain.js +140 -0
- package/dist/cases/membership-matcher.d.ts +23 -0
- package/dist/cases/membership-matcher.js +193 -0
- package/dist/cases/merge-split.d.ts +61 -0
- package/dist/cases/merge-split.js +368 -0
- package/dist/cases/role-inference.d.ts +10 -0
- package/dist/cases/role-inference.js +85 -0
- package/dist/cases/search-rollup.d.ts +23 -0
- package/dist/cases/search-rollup.js +225 -0
- package/dist/cases/sqlite-transaction.d.ts +10 -0
- package/dist/cases/sqlite-transaction.js +62 -0
- package/dist/cases/store.d.ts +74 -0
- package/dist/cases/store.js +816 -0
- package/dist/cases/target-ref.d.ts +25 -0
- package/dist/cases/target-ref.js +22 -0
- package/dist/cases/timeline-range.d.ts +6 -0
- package/dist/cases/timeline-range.js +322 -0
- package/dist/cases/tombstone-sweeper.d.ts +13 -0
- package/dist/cases/tombstone-sweeper.js +128 -0
- package/dist/cases/types.d.ts +244 -0
- package/dist/cases/types.js +47 -0
- package/dist/cases/wiki-page-index.d.ts +50 -0
- package/dist/cases/wiki-page-index.js +350 -0
- package/dist/connectors/event-index.d.ts +44 -0
- package/dist/connectors/event-index.js +366 -0
- package/dist/connectors/types.d.ts +69 -0
- package/dist/connectors/types.js +3 -0
- package/dist/db-manager.d.ts +4 -0
- package/dist/db-manager.js +10 -3
- package/dist/decision-formatter.d.ts +2 -0
- package/dist/decision-formatter.js +11 -8
- package/dist/embedding-server/mobile/daemon.d.ts +1 -0
- package/dist/embedding-server/mobile/daemon.js +11 -1
- package/dist/entities/audit-metrics.d.ts +73 -0
- package/dist/entities/audit-metrics.js +128 -0
- package/dist/entities/candidate-generator.d.ts +9 -0
- package/dist/entities/candidate-generator.js +436 -0
- package/dist/entities/entity-impact.d.ts +41 -0
- package/dist/entities/entity-impact.js +140 -0
- package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
- package/dist/entities/entity-linked-decision-counts.js +29 -0
- package/dist/entities/entity-list.d.ts +23 -0
- package/dist/entities/entity-list.js +99 -0
- package/dist/entities/entity-orphan-list.d.ts +26 -0
- package/dist/entities/entity-orphan-list.js +91 -0
- package/dist/entities/entity-search.d.ts +21 -0
- package/dist/entities/entity-search.js +150 -0
- package/dist/entities/errors.d.ts +65 -0
- package/dist/entities/errors.js +124 -0
- package/dist/entities/exact-merge-backfill.d.ts +11 -0
- package/dist/entities/exact-merge-backfill.js +161 -0
- package/dist/entities/lineage-backfill.d.ts +12 -0
- package/dist/entities/lineage-backfill.js +101 -0
- package/dist/entities/lineage-store.d.ts +51 -0
- package/dist/entities/lineage-store.js +308 -0
- package/dist/entities/normalization.d.ts +14 -0
- package/dist/entities/normalization.js +63 -0
- package/dist/entities/policy-store.d.ts +17 -0
- package/dist/entities/policy-store.js +263 -0
- package/dist/entities/policy-types.d.ts +72 -0
- package/dist/entities/policy-types.js +59 -0
- package/dist/entities/projection.d.ts +8 -0
- package/dist/entities/projection.js +82 -0
- package/dist/entities/provenance-query.d.ts +33 -0
- package/dist/entities/provenance-query.js +167 -0
- package/dist/entities/read-identity.d.ts +31 -0
- package/dist/entities/read-identity.js +264 -0
- package/dist/entities/recall-bridge.d.ts +5 -0
- package/dist/entities/recall-bridge.js +171 -0
- package/dist/entities/resolution-engine.d.ts +15 -0
- package/dist/entities/resolution-engine.js +69 -0
- package/dist/entities/rollback-preview.d.ts +39 -0
- package/dist/entities/rollback-preview.js +235 -0
- package/dist/entities/source-locator.d.ts +3 -0
- package/dist/entities/source-locator.js +32 -0
- package/dist/entities/store.d.ts +75 -0
- package/dist/entities/store.js +465 -0
- package/dist/entities/types.d.ts +162 -0
- package/dist/entities/types.js +30 -0
- package/dist/index.d.ts +47 -2
- package/dist/index.js +66 -25
- package/dist/mama-api.d.ts +22 -1
- package/dist/mama-api.js +272 -64
- package/dist/memory/api.d.ts +28 -1
- package/dist/memory/api.js +293 -31
- package/dist/memory/types.d.ts +11 -2
- package/dist/search/feedback-store.d.ts +59 -0
- package/dist/search/feedback-store.js +211 -0
- package/dist/search/question-type.d.ts +5 -0
- package/dist/search/question-type.js +41 -0
- package/dist/search/ranker-features.d.ts +23 -0
- package/dist/search/ranker-features.js +104 -0
- package/dist/search/ranker-rescore.d.ts +31 -0
- package/dist/search/ranker-rescore.js +226 -0
- package/dist/search/ranker-trainer.d.ts +94 -0
- package/dist/search/ranker-trainer.js +860 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +52 -0
- package/package.json +21 -3
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.featureSetVersion = exports.SEARCH_RANKER_FEATURE_SET_VERSION = exports.RANKER_QUALITY_FIXTURES = void 0;
|
|
4
|
+
exports.trainOfflineRanker = trainOfflineRanker;
|
|
5
|
+
exports.scoreWithRankerModel = scoreWithRankerModel;
|
|
6
|
+
exports.evaluateAgainstBaselines = evaluateAgainstBaselines;
|
|
7
|
+
exports.insertRankerModelVersion = insertRankerModelVersion;
|
|
8
|
+
exports.activateRankerModel = activateRankerModel;
|
|
9
|
+
exports.ndcgAtK = ndcgAtK;
|
|
10
|
+
exports.mrr = mrr;
|
|
11
|
+
exports.seedQualityFixtureFeedback = seedQualityFixtureFeedback;
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const feedback_store_js_1 = require("./feedback-store.js");
|
|
14
|
+
const ranker_features_js_1 = require("./ranker-features.js");
|
|
15
|
+
Object.defineProperty(exports, "featureSetVersion", { enumerable: true, get: function () { return ranker_features_js_1.featureSetVersion; } });
|
|
16
|
+
Object.defineProperty(exports, "SEARCH_RANKER_FEATURE_SET_VERSION", { enumerable: true, get: function () { return ranker_features_js_1.SEARCH_RANKER_FEATURE_SET_VERSION; } });
|
|
17
|
+
const question_type_js_1 = require("./question-type.js");
|
|
18
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
19
|
+
const LEARNING_RATE = 0.05;
|
|
20
|
+
const MINI_BATCH_SIZE = 32;
|
|
21
|
+
const MAX_EPOCHS = 500;
|
|
22
|
+
const EARLY_STOP_DELTA = 1e-5;
|
|
23
|
+
const MIN_NEUTRAL_PER_QUERY = 3;
|
|
24
|
+
// 0.01 is a small but meaningful margin across 8 fixture queries: it blocks
|
|
25
|
+
// tie/noise wins without demanding a large jump on a tiny evaluation set.
|
|
26
|
+
const RANKER_QUALITY_MARGIN = 0.01;
|
|
27
|
+
function seededShuffle(items, seed) {
|
|
28
|
+
const shuffled = [...items];
|
|
29
|
+
let state = seed >>> 0;
|
|
30
|
+
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
|
31
|
+
state = (state * 1664525 + 1013904223) >>> 0;
|
|
32
|
+
const swapIndex = state % (index + 1);
|
|
33
|
+
const current = shuffled[index];
|
|
34
|
+
shuffled[index] = shuffled[swapIndex];
|
|
35
|
+
shuffled[swapIndex] = current;
|
|
36
|
+
}
|
|
37
|
+
return shuffled;
|
|
38
|
+
}
|
|
39
|
+
function reorderFixtureCandidates(query, candidates) {
|
|
40
|
+
if (candidates.length <= 1) {
|
|
41
|
+
return [...candidates];
|
|
42
|
+
}
|
|
43
|
+
const seed = Array.from(query).reduce((total, char) => total + char.charCodeAt(0), 0);
|
|
44
|
+
return seededShuffle(candidates, seed);
|
|
45
|
+
}
|
|
46
|
+
const RAW_RANKER_QUALITY_FIXTURES = [
|
|
47
|
+
{
|
|
48
|
+
query: 'fix the stale case status correction',
|
|
49
|
+
question_type: 'correction',
|
|
50
|
+
candidates: [
|
|
51
|
+
{
|
|
52
|
+
id: 'correction-good-1',
|
|
53
|
+
source_type: 'decision',
|
|
54
|
+
bm25_score: 0.18,
|
|
55
|
+
vector_score: 0.25,
|
|
56
|
+
case_rollup_score: 0.3,
|
|
57
|
+
recency_score: 0.4,
|
|
58
|
+
relevance: 3,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'correction-bad-1',
|
|
62
|
+
source_type: 'wiki_page',
|
|
63
|
+
bm25_score: 0.95,
|
|
64
|
+
vector_score: 0.86,
|
|
65
|
+
case_rollup_score: 0.2,
|
|
66
|
+
recency_score: 0.2,
|
|
67
|
+
relevance: 0,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'correction-mid-1',
|
|
71
|
+
source_type: 'case',
|
|
72
|
+
bm25_score: 0.45,
|
|
73
|
+
vector_score: 0.42,
|
|
74
|
+
case_rollup_score: 0.35,
|
|
75
|
+
recency_score: 0.1,
|
|
76
|
+
relevance: 1,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'correction-bad-2',
|
|
80
|
+
source_type: 'connector_event',
|
|
81
|
+
bm25_score: 0.74,
|
|
82
|
+
vector_score: 0.71,
|
|
83
|
+
case_rollup_score: 0.25,
|
|
84
|
+
recency_score: 0.3,
|
|
85
|
+
relevance: 0,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'correction-bad-3',
|
|
89
|
+
source_type: 'checkpoint',
|
|
90
|
+
bm25_score: 0.61,
|
|
91
|
+
vector_score: 0.67,
|
|
92
|
+
case_rollup_score: 0.15,
|
|
93
|
+
recency_score: 0.2,
|
|
94
|
+
relevance: 0,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
query: 'revert the mistaken merge decision',
|
|
100
|
+
question_type: 'correction',
|
|
101
|
+
candidates: [
|
|
102
|
+
{
|
|
103
|
+
id: 'correction-good-2',
|
|
104
|
+
source_type: 'decision',
|
|
105
|
+
bm25_score: 0.22,
|
|
106
|
+
vector_score: 0.2,
|
|
107
|
+
case_rollup_score: 0.33,
|
|
108
|
+
recency_score: 0.2,
|
|
109
|
+
relevance: 3,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'correction-bad-4',
|
|
113
|
+
source_type: 'connector_event',
|
|
114
|
+
bm25_score: 0.92,
|
|
115
|
+
vector_score: 0.88,
|
|
116
|
+
case_rollup_score: 0.2,
|
|
117
|
+
recency_score: 0.3,
|
|
118
|
+
relevance: 0,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'correction-mid-2',
|
|
122
|
+
source_type: 'case',
|
|
123
|
+
bm25_score: 0.5,
|
|
124
|
+
vector_score: 0.49,
|
|
125
|
+
case_rollup_score: 0.38,
|
|
126
|
+
recency_score: 0.1,
|
|
127
|
+
relevance: 1,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'correction-bad-5',
|
|
131
|
+
source_type: 'wiki_page',
|
|
132
|
+
bm25_score: 0.7,
|
|
133
|
+
vector_score: 0.76,
|
|
134
|
+
case_rollup_score: 0.2,
|
|
135
|
+
recency_score: 0.2,
|
|
136
|
+
relevance: 0,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'correction-bad-6',
|
|
140
|
+
source_type: 'checkpoint',
|
|
141
|
+
bm25_score: 0.6,
|
|
142
|
+
vector_score: 0.57,
|
|
143
|
+
case_rollup_score: 0.19,
|
|
144
|
+
recency_score: 0.2,
|
|
145
|
+
relevance: 0,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
query: 'find the Obsidian doc for the case',
|
|
151
|
+
question_type: 'artifact',
|
|
152
|
+
candidates: [
|
|
153
|
+
{
|
|
154
|
+
id: 'artifact-good-1',
|
|
155
|
+
source_type: 'wiki_page',
|
|
156
|
+
bm25_score: 0.21,
|
|
157
|
+
vector_score: 0.28,
|
|
158
|
+
case_rollup_score: 0.3,
|
|
159
|
+
recency_score: 0.2,
|
|
160
|
+
relevance: 3,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'artifact-bad-1',
|
|
164
|
+
source_type: 'decision',
|
|
165
|
+
bm25_score: 0.96,
|
|
166
|
+
vector_score: 0.87,
|
|
167
|
+
case_rollup_score: 0.21,
|
|
168
|
+
recency_score: 0.4,
|
|
169
|
+
relevance: 0,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'artifact-mid-1',
|
|
173
|
+
source_type: 'case',
|
|
174
|
+
bm25_score: 0.44,
|
|
175
|
+
vector_score: 0.47,
|
|
176
|
+
case_rollup_score: 0.36,
|
|
177
|
+
recency_score: 0.2,
|
|
178
|
+
relevance: 1,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'artifact-bad-2',
|
|
182
|
+
source_type: 'connector_event',
|
|
183
|
+
bm25_score: 0.73,
|
|
184
|
+
vector_score: 0.74,
|
|
185
|
+
case_rollup_score: 0.18,
|
|
186
|
+
recency_score: 0.3,
|
|
187
|
+
relevance: 0,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'artifact-bad-3',
|
|
191
|
+
source_type: 'checkpoint',
|
|
192
|
+
bm25_score: 0.68,
|
|
193
|
+
vector_score: 0.62,
|
|
194
|
+
case_rollup_score: 0.19,
|
|
195
|
+
recency_score: 0.2,
|
|
196
|
+
relevance: 0,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
query: 'which pdf described the rollout',
|
|
202
|
+
question_type: 'artifact',
|
|
203
|
+
candidates: [
|
|
204
|
+
{
|
|
205
|
+
id: 'artifact-good-2',
|
|
206
|
+
source_type: 'wiki_page',
|
|
207
|
+
bm25_score: 0.19,
|
|
208
|
+
vector_score: 0.26,
|
|
209
|
+
case_rollup_score: 0.31,
|
|
210
|
+
recency_score: 0.2,
|
|
211
|
+
relevance: 3,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'artifact-bad-4',
|
|
215
|
+
source_type: 'decision',
|
|
216
|
+
bm25_score: 0.91,
|
|
217
|
+
vector_score: 0.89,
|
|
218
|
+
case_rollup_score: 0.21,
|
|
219
|
+
recency_score: 0.3,
|
|
220
|
+
relevance: 0,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'artifact-mid-2',
|
|
224
|
+
source_type: 'case',
|
|
225
|
+
bm25_score: 0.49,
|
|
226
|
+
vector_score: 0.43,
|
|
227
|
+
case_rollup_score: 0.35,
|
|
228
|
+
recency_score: 0.3,
|
|
229
|
+
relevance: 1,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'artifact-bad-5',
|
|
233
|
+
source_type: 'connector_event',
|
|
234
|
+
bm25_score: 0.76,
|
|
235
|
+
vector_score: 0.72,
|
|
236
|
+
case_rollup_score: 0.2,
|
|
237
|
+
recency_score: 0.2,
|
|
238
|
+
relevance: 0,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'artifact-bad-6',
|
|
242
|
+
source_type: 'checkpoint',
|
|
243
|
+
bm25_score: 0.63,
|
|
244
|
+
vector_score: 0.61,
|
|
245
|
+
case_rollup_score: 0.19,
|
|
246
|
+
recency_score: 0.2,
|
|
247
|
+
relevance: 0,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
query: 'when did the blocker first happen',
|
|
253
|
+
question_type: 'timeline',
|
|
254
|
+
candidates: [
|
|
255
|
+
{
|
|
256
|
+
id: 'timeline-good-1',
|
|
257
|
+
source_type: 'connector_event',
|
|
258
|
+
bm25_score: 0.2,
|
|
259
|
+
vector_score: 0.29,
|
|
260
|
+
case_rollup_score: 0.3,
|
|
261
|
+
recency_score: 0.2,
|
|
262
|
+
relevance: 3,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'timeline-bad-1',
|
|
266
|
+
source_type: 'decision',
|
|
267
|
+
bm25_score: 0.93,
|
|
268
|
+
vector_score: 0.91,
|
|
269
|
+
case_rollup_score: 0.18,
|
|
270
|
+
recency_score: 0.3,
|
|
271
|
+
relevance: 0,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'timeline-mid-1',
|
|
275
|
+
source_type: 'checkpoint',
|
|
276
|
+
bm25_score: 0.48,
|
|
277
|
+
vector_score: 0.46,
|
|
278
|
+
case_rollup_score: 0.34,
|
|
279
|
+
recency_score: 0.2,
|
|
280
|
+
relevance: 1,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: 'timeline-bad-2',
|
|
284
|
+
source_type: 'case',
|
|
285
|
+
bm25_score: 0.78,
|
|
286
|
+
vector_score: 0.74,
|
|
287
|
+
case_rollup_score: 0.18,
|
|
288
|
+
recency_score: 0.2,
|
|
289
|
+
relevance: 0,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: 'timeline-bad-3',
|
|
293
|
+
source_type: 'wiki_page',
|
|
294
|
+
bm25_score: 0.64,
|
|
295
|
+
vector_score: 0.63,
|
|
296
|
+
case_rollup_score: 0.19,
|
|
297
|
+
recency_score: 0.2,
|
|
298
|
+
relevance: 0,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
query: 'history before the regression',
|
|
304
|
+
question_type: 'timeline',
|
|
305
|
+
candidates: [
|
|
306
|
+
{
|
|
307
|
+
id: 'timeline-good-2',
|
|
308
|
+
source_type: 'connector_event',
|
|
309
|
+
bm25_score: 0.23,
|
|
310
|
+
vector_score: 0.27,
|
|
311
|
+
case_rollup_score: 0.31,
|
|
312
|
+
recency_score: 0.2,
|
|
313
|
+
relevance: 3,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'timeline-bad-4',
|
|
317
|
+
source_type: 'decision',
|
|
318
|
+
bm25_score: 0.94,
|
|
319
|
+
vector_score: 0.9,
|
|
320
|
+
case_rollup_score: 0.18,
|
|
321
|
+
recency_score: 0.3,
|
|
322
|
+
relevance: 0,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'timeline-mid-2',
|
|
326
|
+
source_type: 'checkpoint',
|
|
327
|
+
bm25_score: 0.47,
|
|
328
|
+
vector_score: 0.45,
|
|
329
|
+
case_rollup_score: 0.36,
|
|
330
|
+
recency_score: 0.2,
|
|
331
|
+
relevance: 1,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'timeline-bad-5',
|
|
335
|
+
source_type: 'case',
|
|
336
|
+
bm25_score: 0.75,
|
|
337
|
+
vector_score: 0.77,
|
|
338
|
+
case_rollup_score: 0.19,
|
|
339
|
+
recency_score: 0.2,
|
|
340
|
+
relevance: 0,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: 'timeline-bad-6',
|
|
344
|
+
source_type: 'wiki_page',
|
|
345
|
+
bm25_score: 0.62,
|
|
346
|
+
vector_score: 0.64,
|
|
347
|
+
case_rollup_score: 0.18,
|
|
348
|
+
recency_score: 0.2,
|
|
349
|
+
relevance: 0,
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
query: 'current status of the rollout',
|
|
355
|
+
question_type: 'status',
|
|
356
|
+
candidates: [
|
|
357
|
+
{
|
|
358
|
+
id: 'status-good-1',
|
|
359
|
+
source_type: 'case',
|
|
360
|
+
bm25_score: 0.2,
|
|
361
|
+
vector_score: 0.24,
|
|
362
|
+
case_rollup_score: 0.36,
|
|
363
|
+
recency_score: 0.2,
|
|
364
|
+
relevance: 3,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
id: 'status-bad-1',
|
|
368
|
+
source_type: 'decision',
|
|
369
|
+
bm25_score: 0.97,
|
|
370
|
+
vector_score: 0.9,
|
|
371
|
+
case_rollup_score: 0.2,
|
|
372
|
+
recency_score: 0.3,
|
|
373
|
+
relevance: 0,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: 'status-mid-1',
|
|
377
|
+
source_type: 'checkpoint',
|
|
378
|
+
bm25_score: 0.46,
|
|
379
|
+
vector_score: 0.47,
|
|
380
|
+
case_rollup_score: 0.34,
|
|
381
|
+
recency_score: 0.2,
|
|
382
|
+
relevance: 1,
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 'status-bad-2',
|
|
386
|
+
source_type: 'connector_event',
|
|
387
|
+
bm25_score: 0.74,
|
|
388
|
+
vector_score: 0.73,
|
|
389
|
+
case_rollup_score: 0.19,
|
|
390
|
+
recency_score: 0.2,
|
|
391
|
+
relevance: 0,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: 'status-bad-3',
|
|
395
|
+
source_type: 'wiki_page',
|
|
396
|
+
bm25_score: 0.65,
|
|
397
|
+
vector_score: 0.62,
|
|
398
|
+
case_rollup_score: 0.18,
|
|
399
|
+
recency_score: 0.2,
|
|
400
|
+
relevance: 0,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
query: 'latest progress on case assembly',
|
|
406
|
+
question_type: 'status',
|
|
407
|
+
candidates: [
|
|
408
|
+
{
|
|
409
|
+
id: 'status-good-2',
|
|
410
|
+
source_type: 'case',
|
|
411
|
+
bm25_score: 0.22,
|
|
412
|
+
vector_score: 0.23,
|
|
413
|
+
case_rollup_score: 0.37,
|
|
414
|
+
recency_score: 0.2,
|
|
415
|
+
relevance: 3,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
id: 'status-bad-4',
|
|
419
|
+
source_type: 'decision',
|
|
420
|
+
bm25_score: 0.92,
|
|
421
|
+
vector_score: 0.91,
|
|
422
|
+
case_rollup_score: 0.2,
|
|
423
|
+
recency_score: 0.3,
|
|
424
|
+
relevance: 0,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: 'status-mid-2',
|
|
428
|
+
source_type: 'checkpoint',
|
|
429
|
+
bm25_score: 0.48,
|
|
430
|
+
vector_score: 0.45,
|
|
431
|
+
case_rollup_score: 0.34,
|
|
432
|
+
recency_score: 0.2,
|
|
433
|
+
relevance: 1,
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
id: 'status-bad-5',
|
|
437
|
+
source_type: 'connector_event',
|
|
438
|
+
bm25_score: 0.77,
|
|
439
|
+
vector_score: 0.75,
|
|
440
|
+
case_rollup_score: 0.2,
|
|
441
|
+
recency_score: 0.2,
|
|
442
|
+
relevance: 0,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: 'status-bad-6',
|
|
446
|
+
source_type: 'wiki_page',
|
|
447
|
+
bm25_score: 0.66,
|
|
448
|
+
vector_score: 0.63,
|
|
449
|
+
case_rollup_score: 0.18,
|
|
450
|
+
recency_score: 0.2,
|
|
451
|
+
relevance: 0,
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
];
|
|
456
|
+
exports.RANKER_QUALITY_FIXTURES = RAW_RANKER_QUALITY_FIXTURES.map((fixture) => ({
|
|
457
|
+
...fixture,
|
|
458
|
+
candidates: reorderFixtureCandidates(fixture.query, fixture.candidates),
|
|
459
|
+
}));
|
|
460
|
+
function asSourceForFeedback(sourceType) {
|
|
461
|
+
switch (sourceType) {
|
|
462
|
+
case 'wiki_page':
|
|
463
|
+
return 'wiki_page';
|
|
464
|
+
case 'case':
|
|
465
|
+
return 'case';
|
|
466
|
+
case 'connector_event':
|
|
467
|
+
return 'connector_event';
|
|
468
|
+
case 'checkpoint':
|
|
469
|
+
return 'checkpoint';
|
|
470
|
+
default:
|
|
471
|
+
return 'decision';
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function parseDate(input, field) {
|
|
475
|
+
const date = new Date(input);
|
|
476
|
+
if (Number.isNaN(date.getTime())) {
|
|
477
|
+
throw new Error(`${field} must be a valid ISO timestamp`);
|
|
478
|
+
}
|
|
479
|
+
return date;
|
|
480
|
+
}
|
|
481
|
+
function iso(date) {
|
|
482
|
+
return date.toISOString();
|
|
483
|
+
}
|
|
484
|
+
function effectiveWindow(input) {
|
|
485
|
+
const retentionDays = (0, feedback_store_js_1.getFeedbackRetentionDays)(input.adapter);
|
|
486
|
+
const now = input.now ?? new Date();
|
|
487
|
+
const retentionCutoff = new Date(now.getTime() - retentionDays * MS_PER_DAY);
|
|
488
|
+
const until = input.until ? parseDate(input.until, 'until') : now;
|
|
489
|
+
const since = input.since ? parseDate(input.since, 'since') : retentionCutoff;
|
|
490
|
+
if (since.getTime() > until.getTime()) {
|
|
491
|
+
throw new Error('since must be before until');
|
|
492
|
+
}
|
|
493
|
+
const retentionWarning = Boolean(input.since && since.getTime() < retentionCutoff.getTime());
|
|
494
|
+
if (retentionWarning) {
|
|
495
|
+
console.warn(JSON.stringify({
|
|
496
|
+
event: 'training_window_exceeds_retention',
|
|
497
|
+
since: iso(since),
|
|
498
|
+
retention_cutoff_at: iso(retentionCutoff),
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
since: iso(since),
|
|
503
|
+
until: iso(until),
|
|
504
|
+
retention_cutoff_at: iso(retentionCutoff),
|
|
505
|
+
retention_days_at_train_time: retentionDays,
|
|
506
|
+
retention_warning: retentionWarning,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function labelForKind(kind) {
|
|
510
|
+
if (kind === 'accept' || kind === 'click') {
|
|
511
|
+
return 1;
|
|
512
|
+
}
|
|
513
|
+
if (kind === 'reject' || kind === 'hide' || kind === 'shown') {
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
function rowToExample(row) {
|
|
519
|
+
const label = labelForKind(row.feedback_kind);
|
|
520
|
+
if (label === null) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const features = (0, ranker_features_js_1.serializeFeatures)((0, ranker_features_js_1.extractFeatures)({
|
|
524
|
+
...row,
|
|
525
|
+
source_type: row.result_source_type,
|
|
526
|
+
rank_position: row.rank_position,
|
|
527
|
+
}, row.query, { question_type: row.question_type }));
|
|
528
|
+
return {
|
|
529
|
+
query: row.query,
|
|
530
|
+
question_type: row.question_type,
|
|
531
|
+
features,
|
|
532
|
+
label,
|
|
533
|
+
feedback_kind: row.feedback_kind,
|
|
534
|
+
created_at: row.created_at,
|
|
535
|
+
feedback_id: row.feedback_id,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function capNeutralRows(rows) {
|
|
539
|
+
const byQuery = new Map();
|
|
540
|
+
for (const row of rows) {
|
|
541
|
+
const key = row.query_hash_hex || row.query;
|
|
542
|
+
const current = byQuery.get(key) ?? [];
|
|
543
|
+
current.push(row);
|
|
544
|
+
byQuery.set(key, current);
|
|
545
|
+
}
|
|
546
|
+
const capped = [];
|
|
547
|
+
for (const groupRows of byQuery.values()) {
|
|
548
|
+
const explicit = groupRows.filter((row) => row.feedback_kind !== 'shown');
|
|
549
|
+
const shown = groupRows.filter((row) => row.feedback_kind === 'shown');
|
|
550
|
+
const neutralLimit = Math.max(explicit.length * 3, MIN_NEUTRAL_PER_QUERY);
|
|
551
|
+
capped.push(...explicit, ...shown.slice(0, neutralLimit));
|
|
552
|
+
}
|
|
553
|
+
capped.sort((left, right) => {
|
|
554
|
+
const byCreatedAt = left.created_at.localeCompare(right.created_at);
|
|
555
|
+
return byCreatedAt !== 0 ? byCreatedAt : left.feedback_id.localeCompare(right.feedback_id);
|
|
556
|
+
});
|
|
557
|
+
return capped;
|
|
558
|
+
}
|
|
559
|
+
function sigmoid(value) {
|
|
560
|
+
if (value >= 35) {
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
if (value <= -35) {
|
|
564
|
+
return 0;
|
|
565
|
+
}
|
|
566
|
+
return 1 / (1 + Math.exp(-value));
|
|
567
|
+
}
|
|
568
|
+
function dot(left, right) {
|
|
569
|
+
const length = Math.min(left.length, right.length);
|
|
570
|
+
let total = 0;
|
|
571
|
+
for (let index = 0; index < length; index += 1) {
|
|
572
|
+
total += left[index] * right[index];
|
|
573
|
+
}
|
|
574
|
+
return total;
|
|
575
|
+
}
|
|
576
|
+
function hasBothLabels(examples) {
|
|
577
|
+
const labels = new Set(examples.map((example) => example.label));
|
|
578
|
+
return labels.has(0) && labels.has(1);
|
|
579
|
+
}
|
|
580
|
+
function trainWeights(examples, featureCount) {
|
|
581
|
+
const coefficients = new Array(featureCount).fill(0);
|
|
582
|
+
let intercept = 0;
|
|
583
|
+
if (examples.length === 0 || !hasBothLabels(examples)) {
|
|
584
|
+
return { coefficients, intercept };
|
|
585
|
+
}
|
|
586
|
+
for (let epoch = 0; epoch < MAX_EPOCHS; epoch += 1) {
|
|
587
|
+
const shuffledExamples = seededShuffle(examples, epoch + 1);
|
|
588
|
+
let epochDelta = 0;
|
|
589
|
+
let batchCount = 0;
|
|
590
|
+
for (let batchStart = 0; batchStart < shuffledExamples.length; batchStart += MINI_BATCH_SIZE) {
|
|
591
|
+
const batch = shuffledExamples.slice(batchStart, batchStart + MINI_BATCH_SIZE);
|
|
592
|
+
const gradients = new Array(featureCount).fill(0);
|
|
593
|
+
let interceptGradient = 0;
|
|
594
|
+
for (const example of batch) {
|
|
595
|
+
const prediction = sigmoid(dot(coefficients, example.features) + intercept);
|
|
596
|
+
const error = prediction - example.label;
|
|
597
|
+
for (let index = 0; index < featureCount; index += 1) {
|
|
598
|
+
gradients[index] += error * example.features[index];
|
|
599
|
+
}
|
|
600
|
+
interceptGradient += error;
|
|
601
|
+
}
|
|
602
|
+
let delta = 0;
|
|
603
|
+
for (let index = 0; index < featureCount; index += 1) {
|
|
604
|
+
const update = (LEARNING_RATE * gradients[index]) / batch.length;
|
|
605
|
+
coefficients[index] -= update;
|
|
606
|
+
delta += Math.abs(update);
|
|
607
|
+
}
|
|
608
|
+
const interceptUpdate = (LEARNING_RATE * interceptGradient) / batch.length;
|
|
609
|
+
intercept -= interceptUpdate;
|
|
610
|
+
delta += Math.abs(interceptUpdate);
|
|
611
|
+
epochDelta += delta;
|
|
612
|
+
batchCount += 1;
|
|
613
|
+
}
|
|
614
|
+
if (batchCount > 0 && epochDelta / batchCount < EARLY_STOP_DELTA) {
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return { coefficients, intercept };
|
|
619
|
+
}
|
|
620
|
+
function questionWeights(examples, featureCount, fallback) {
|
|
621
|
+
const output = {};
|
|
622
|
+
for (const questionType of question_type_js_1.QUESTION_TYPES) {
|
|
623
|
+
const subset = examples.filter((example) => example.question_type === questionType);
|
|
624
|
+
output[questionType] = hasBothLabels(subset)
|
|
625
|
+
? trainWeights(subset, featureCount).coefficients
|
|
626
|
+
: [...fallback];
|
|
627
|
+
}
|
|
628
|
+
return output;
|
|
629
|
+
}
|
|
630
|
+
function modelIdFor(input) {
|
|
631
|
+
const compactTime = input.trainedAt.replace(/[^0-9]/g, '').slice(0, 17);
|
|
632
|
+
const uniqueSuffix = (0, node_crypto_1.randomBytes)(4).toString('hex');
|
|
633
|
+
return `ranker_${compactTime}_${input.rows}_${input.distinctQueries}_${uniqueSuffix}`;
|
|
634
|
+
}
|
|
635
|
+
async function trainOfflineRanker(input) {
|
|
636
|
+
const minFeedbackRows = input.minFeedbackRows ?? 1000;
|
|
637
|
+
const minDistinctQueries = input.minDistinctQueries ?? 10;
|
|
638
|
+
const window = effectiveWindow(input);
|
|
639
|
+
const rows = (0, feedback_store_js_1.listSearchFeedback)(input.adapter, {
|
|
640
|
+
since: window.since,
|
|
641
|
+
until: window.until,
|
|
642
|
+
});
|
|
643
|
+
const cappedRows = capNeutralRows(rows);
|
|
644
|
+
const examples = cappedRows
|
|
645
|
+
.map(rowToExample)
|
|
646
|
+
.filter((example) => Boolean(example));
|
|
647
|
+
const distinctQueries = new Set(examples.map((example) => example.query.trim().toLowerCase()))
|
|
648
|
+
.size;
|
|
649
|
+
const counts = {
|
|
650
|
+
feedbackRows: examples.length,
|
|
651
|
+
distinctQueries,
|
|
652
|
+
};
|
|
653
|
+
const examplesHaveBothLabels = hasBothLabels(examples);
|
|
654
|
+
if (counts.feedbackRows < minFeedbackRows ||
|
|
655
|
+
counts.distinctQueries < minDistinctQueries ||
|
|
656
|
+
!examplesHaveBothLabels) {
|
|
657
|
+
return {
|
|
658
|
+
status: 'insufficient_data',
|
|
659
|
+
effectiveWindow: window,
|
|
660
|
+
counts,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const featureCount = examples[0].features.length;
|
|
664
|
+
const trained = trainWeights(examples, featureCount);
|
|
665
|
+
const trainedAt = input.now ? input.now.toISOString() : new Date().toISOString();
|
|
666
|
+
const model = {
|
|
667
|
+
model_id: modelIdFor({
|
|
668
|
+
trainedAt,
|
|
669
|
+
rows: counts.feedbackRows,
|
|
670
|
+
distinctQueries: counts.distinctQueries,
|
|
671
|
+
}),
|
|
672
|
+
feature_set_version: input.featureSetVersion ?? (0, ranker_features_js_1.featureSetVersion)(),
|
|
673
|
+
trained_at: trainedAt,
|
|
674
|
+
training_rows_count: examples.length,
|
|
675
|
+
coefficients: trained.coefficients,
|
|
676
|
+
intercept: trained.intercept,
|
|
677
|
+
question_type_weights: questionWeights(examples, featureCount, trained.coefficients),
|
|
678
|
+
training_window: window,
|
|
679
|
+
};
|
|
680
|
+
return {
|
|
681
|
+
status: 'trained',
|
|
682
|
+
model,
|
|
683
|
+
effectiveWindow: window,
|
|
684
|
+
counts,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function rankByScore(items, score) {
|
|
688
|
+
return items
|
|
689
|
+
.map((item) => ({ item, score: score(item) }))
|
|
690
|
+
.sort((left, right) => {
|
|
691
|
+
const diff = right.score - left.score;
|
|
692
|
+
if (Math.abs(diff) > 1e-12) {
|
|
693
|
+
return diff;
|
|
694
|
+
}
|
|
695
|
+
return left.item.id.localeCompare(right.item.id);
|
|
696
|
+
})
|
|
697
|
+
.map((entry) => entry.item);
|
|
698
|
+
}
|
|
699
|
+
function reciprocalRankRanks(items, leftScore, rightScore) {
|
|
700
|
+
const left = rankByScore(items, leftScore);
|
|
701
|
+
const right = rankByScore(items, rightScore);
|
|
702
|
+
const leftRanks = new Map(left.map((item, index) => [item.id, index + 1]));
|
|
703
|
+
const rightRanks = new Map(right.map((item, index) => [item.id, index + 1]));
|
|
704
|
+
return rankByScore(items, (item) => {
|
|
705
|
+
const leftRank = leftRanks.get(item.id) ?? items.length;
|
|
706
|
+
const rightRank = rightRanks.get(item.id) ?? items.length;
|
|
707
|
+
return 1 / (60 + leftRank) + 1 / (60 + rightRank);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function scoreWithRankerModel(model, row, query, questionType) {
|
|
711
|
+
const features = (0, ranker_features_js_1.serializeFeatures)((0, ranker_features_js_1.extractFeatures)(row, query, { question_type: questionType }));
|
|
712
|
+
const coefficients = model.question_type_weights[questionType] ?? model.coefficients;
|
|
713
|
+
return sigmoid(dot(coefficients, features) + model.intercept);
|
|
714
|
+
}
|
|
715
|
+
function baselineMetrics(fixtures, ranker) {
|
|
716
|
+
const perQuery = {};
|
|
717
|
+
let ndcgTotal = 0;
|
|
718
|
+
let mrrTotal = 0;
|
|
719
|
+
for (const fixture of fixtures) {
|
|
720
|
+
const ranked = ranker(fixture);
|
|
721
|
+
const relevance = new Map(fixture.candidates.map((candidate) => [candidate.id, candidate.relevance]));
|
|
722
|
+
const relevantSet = new Set(fixture.candidates
|
|
723
|
+
.filter((candidate) => candidate.relevance > 0)
|
|
724
|
+
.map((candidate) => candidate.id));
|
|
725
|
+
const ndcg = ndcgAtK(ranked, relevance, 5);
|
|
726
|
+
const mrrScore = mrr(ranked, relevantSet);
|
|
727
|
+
perQuery[fixture.query] = { ndcg, mrr: mrrScore };
|
|
728
|
+
ndcgTotal += ndcg;
|
|
729
|
+
mrrTotal += mrrScore;
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
ndcg: ndcgTotal / fixtures.length,
|
|
733
|
+
mrr: mrrTotal / fixtures.length,
|
|
734
|
+
per_query: perQuery,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
async function evaluateAgainstBaselines(_input, model) {
|
|
738
|
+
const bm25 = baselineMetrics(exports.RANKER_QUALITY_FIXTURES, (fixture) => rankByScore(fixture.candidates, (candidate) => candidate.bm25_score));
|
|
739
|
+
const vector = baselineMetrics(exports.RANKER_QUALITY_FIXTURES, (fixture) => rankByScore(fixture.candidates, (candidate) => candidate.vector_score));
|
|
740
|
+
const rrf = baselineMetrics(exports.RANKER_QUALITY_FIXTURES, (fixture) => reciprocalRankRanks(fixture.candidates, (candidate) => candidate.bm25_score, (candidate) => candidate.vector_score));
|
|
741
|
+
const logistic = baselineMetrics(exports.RANKER_QUALITY_FIXTURES, (fixture) => rankByScore(fixture.candidates, (candidate) => 0.5 * candidate.bm25_score + 0.5 * candidate.vector_score));
|
|
742
|
+
const learned = baselineMetrics(exports.RANKER_QUALITY_FIXTURES, (fixture) => rankByScore(fixture.candidates, (candidate) => scoreWithRankerModel(model, candidate, fixture.query, fixture.question_type)));
|
|
743
|
+
const bestBaselineNdcg = Math.max(bm25.ndcg, vector.ndcg, rrf.ndcg, logistic.ndcg);
|
|
744
|
+
const bestBaselineMrr = Math.max(bm25.mrr, vector.mrr, rrf.mrr, logistic.mrr);
|
|
745
|
+
return {
|
|
746
|
+
bm25,
|
|
747
|
+
vector,
|
|
748
|
+
rrf,
|
|
749
|
+
logistic,
|
|
750
|
+
learned,
|
|
751
|
+
passes: learned.ndcg >= bestBaselineNdcg + RANKER_QUALITY_MARGIN &&
|
|
752
|
+
learned.mrr >= bestBaselineMrr + RANKER_QUALITY_MARGIN,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function runTransaction(adapter, fn) {
|
|
756
|
+
if (!adapter.transaction) {
|
|
757
|
+
return fn();
|
|
758
|
+
}
|
|
759
|
+
const result = adapter.transaction(fn);
|
|
760
|
+
if (typeof result === 'function') {
|
|
761
|
+
return result();
|
|
762
|
+
}
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
async function insertRankerModelVersion(adapter, model, evaluation) {
|
|
766
|
+
const coefficientsPayload = JSON.stringify({
|
|
767
|
+
coefficients: model.coefficients,
|
|
768
|
+
intercept: model.intercept,
|
|
769
|
+
question_type_weights: model.question_type_weights,
|
|
770
|
+
training_rows_count: model.training_rows_count,
|
|
771
|
+
});
|
|
772
|
+
const metricsPayload = JSON.stringify({
|
|
773
|
+
learned: evaluation.learned,
|
|
774
|
+
passes: evaluation.passes,
|
|
775
|
+
});
|
|
776
|
+
const baselinePayload = JSON.stringify({
|
|
777
|
+
bm25: evaluation.bm25,
|
|
778
|
+
vector: evaluation.vector,
|
|
779
|
+
rrf: evaluation.rrf,
|
|
780
|
+
logistic: evaluation.logistic,
|
|
781
|
+
});
|
|
782
|
+
const trainingWindowPayload = JSON.stringify(model.training_window ?? {
|
|
783
|
+
since: null,
|
|
784
|
+
until: null,
|
|
785
|
+
retention_cutoff_at: null,
|
|
786
|
+
retention_days_at_train_time: null,
|
|
787
|
+
retention_warning: false,
|
|
788
|
+
});
|
|
789
|
+
adapter
|
|
790
|
+
.prepare(`
|
|
791
|
+
INSERT INTO ranker_model_versions (
|
|
792
|
+
model_id, model_version, feature_set_version, coefficients_json, metrics_json,
|
|
793
|
+
training_window_json, baseline_metrics_json, quality_gate_status, trained_at,
|
|
794
|
+
trained_by, active
|
|
795
|
+
)
|
|
796
|
+
VALUES (?, 'offline-logistic-v1', ?, ?, ?, ?, ?, ?, ?, 'mama train-ranker', 0)
|
|
797
|
+
`)
|
|
798
|
+
.run(model.model_id, model.feature_set_version, coefficientsPayload, metricsPayload, trainingWindowPayload, baselinePayload, evaluation.passes ? 'passed' : 'failed', model.trained_at);
|
|
799
|
+
return { model_id: model.model_id, active: false };
|
|
800
|
+
}
|
|
801
|
+
async function activateRankerModel(adapter, model_id) {
|
|
802
|
+
const row = adapter
|
|
803
|
+
.prepare(`
|
|
804
|
+
SELECT model_id, feature_set_version, quality_gate_status
|
|
805
|
+
FROM ranker_model_versions
|
|
806
|
+
WHERE model_id = ?
|
|
807
|
+
`)
|
|
808
|
+
.get(model_id);
|
|
809
|
+
if (!row) {
|
|
810
|
+
return 'not_found';
|
|
811
|
+
}
|
|
812
|
+
if (row.feature_set_version !== ranker_features_js_1.SEARCH_RANKER_FEATURE_SET_VERSION) {
|
|
813
|
+
return 'feature_set_mismatch';
|
|
814
|
+
}
|
|
815
|
+
if (row.quality_gate_status !== 'passed') {
|
|
816
|
+
return 'quality_gate_failed';
|
|
817
|
+
}
|
|
818
|
+
runTransaction(adapter, () => {
|
|
819
|
+
adapter.prepare('UPDATE ranker_model_versions SET active = 0 WHERE active = 1').run();
|
|
820
|
+
adapter.prepare('UPDATE ranker_model_versions SET active = 1 WHERE model_id = ?').run(model_id);
|
|
821
|
+
});
|
|
822
|
+
return 'activated';
|
|
823
|
+
}
|
|
824
|
+
function ndcgAtK(results, relevanceMap, k) {
|
|
825
|
+
const gains = results.slice(0, k).map((result, index) => {
|
|
826
|
+
const relevance = relevanceMap.get(result.id) ?? 0;
|
|
827
|
+
return (2 ** relevance - 1) / Math.log2(index + 2);
|
|
828
|
+
});
|
|
829
|
+
const dcg = gains.reduce((sum, gain) => sum + gain, 0);
|
|
830
|
+
const ideal = Array.from(relevanceMap.values())
|
|
831
|
+
.sort((left, right) => right - left)
|
|
832
|
+
.slice(0, k)
|
|
833
|
+
.map((relevance, index) => (2 ** relevance - 1) / Math.log2(index + 2))
|
|
834
|
+
.reduce((sum, gain) => sum + gain, 0);
|
|
835
|
+
return ideal === 0 ? 0 : dcg / ideal;
|
|
836
|
+
}
|
|
837
|
+
function mrr(results, relevantSet) {
|
|
838
|
+
const index = results.findIndex((result) => relevantSet.has(result.id));
|
|
839
|
+
return index === -1 ? 0 : 1 / (index + 1);
|
|
840
|
+
}
|
|
841
|
+
function seedQualityFixtureFeedback(adapter, createdAt = '2026-04-18T00:00:00.000Z') {
|
|
842
|
+
let counter = 0;
|
|
843
|
+
for (const fixture of exports.RANKER_QUALITY_FIXTURES) {
|
|
844
|
+
for (const candidate of fixture.candidates) {
|
|
845
|
+
counter += 1;
|
|
846
|
+
const kind = candidate.relevance >= 3 ? 'accept' : 'reject';
|
|
847
|
+
adapter
|
|
848
|
+
.prepare(`
|
|
849
|
+
INSERT INTO search_feedback (
|
|
850
|
+
feedback_id, query, query_hash, question_type, result_source_type, result_source_id,
|
|
851
|
+
case_id, feedback_kind, rank_position, score_before, score_after, session_id,
|
|
852
|
+
actor, metadata_json, created_at, updated_at
|
|
853
|
+
)
|
|
854
|
+
VALUES (?, ?, zeroblob(32), ?, ?, ?, NULL, ?, ?, NULL, NULL, ?, 'test:ranker-fixture', '{}', ?, NULL)
|
|
855
|
+
`)
|
|
856
|
+
.run(`quality-${counter}`, fixture.query, fixture.question_type, asSourceForFeedback(candidate.source_type), candidate.id, kind, counter % 5, `fixture:${fixture.question_type}`, createdAt);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
//# sourceMappingURL=ranker-trainer.js.map
|