@memrosetta/core 0.2.21 → 0.2.22

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/index.d.ts CHANGED
@@ -169,6 +169,17 @@ declare function rrfMergeWeighted(ftsResults: readonly {
169
169
  readonly memory: Memory;
170
170
  readonly rank: number;
171
171
  }[], k?: number, limit?: number, ftsWeight?: number, vecWeight?: number): readonly SearchResult[];
172
+ /**
173
+ * Score-level fusion: alpha * normalizedVecSim + (1 - alpha) * normalizedFtsSim.
174
+ *
175
+ * Unlike RRF (which discards score magnitude), this preserves score information.
176
+ * Both FTS and vector scores are min-max normalized within the result set so
177
+ * neither modality dominates due to scale differences.
178
+ *
179
+ * Items found by both sources get the combined score (strongest signal).
180
+ * Items found by only one source get at most alpha or (1-alpha) of max.
181
+ */
182
+ declare function convexCombinationMerge(ftsResults: readonly SearchResult[], vecResults: readonly VectorSearchResult[], alpha?: number, limit?: number): readonly SearchResult[];
172
183
  /**
173
184
  * Generative Agents-inspired 3-factor reranking.
174
185
  * final_score = w_recency * recency + w_importance * importance + w_relevance * relevance
@@ -209,7 +220,7 @@ declare function updateAccessTracking(db: Database.Database, memoryIds: readonly
209
220
  *
210
221
  * When queryVec is not provided, performs FTS-only search (backward compatible).
211
222
  * When queryVec is provided, performs hybrid search combining FTS + vector
212
- * results via Reciprocal Rank Fusion.
223
+ * results via convex combination score fusion.
213
224
  *
214
225
  * Results are weighted by activation score and access tracking is updated.
215
226
  */
@@ -331,4 +342,4 @@ declare function determineTier(memory: {
331
342
  */
332
343
  declare function estimateTokens(content: string): number;
333
344
 
334
- export { DEFAULT_TIER_CONFIG, type MemoryRow, type PreparedStatements, type RelationStatements, type SchemaOptions, type SearchSqlResult, type SqliteEngineOptions, SqliteMemoryEngine, type VectorSearchResult, applyKeywordBoost, applyThreeFactorReranking, bruteForceVectorSearch, buildFtsQuery, buildSearchSql, computeActivation, computeEbbinghaus, createEngine, createPreparedStatements, createRelation, createRelationStatements, deduplicateResults, deriveMemoryState, determineTier, ensureSchema, estimateTokens, extractQueryTokens, ftsSearch, generateMemoryId, getRelationsByMemory, keywordsToString, normalizeScores, nowIso, rowToMemory, rrfMerge, rrfMergeWeighted, searchMemories, serializeEmbedding, storeBatchAsync, storeBatchInTransaction, storeMemory, storeMemoryAsync, stringToKeywords, updateAccessTracking, vectorSearch };
345
+ export { DEFAULT_TIER_CONFIG, type MemoryRow, type PreparedStatements, type RelationStatements, type SchemaOptions, type SearchSqlResult, type SqliteEngineOptions, SqliteMemoryEngine, type VectorSearchResult, applyKeywordBoost, applyThreeFactorReranking, bruteForceVectorSearch, buildFtsQuery, buildSearchSql, computeActivation, computeEbbinghaus, convexCombinationMerge, createEngine, createPreparedStatements, createRelation, createRelationStatements, deduplicateResults, deriveMemoryState, determineTier, ensureSchema, estimateTokens, extractQueryTokens, ftsSearch, generateMemoryId, getRelationsByMemory, keywordsToString, normalizeScores, nowIso, rowToMemory, rrfMerge, rrfMergeWeighted, searchMemories, serializeEmbedding, storeBatchAsync, storeBatchInTransaction, storeMemory, storeMemoryAsync, stringToKeywords, updateAccessTracking, vectorSearch };
package/dist/index.js CHANGED
@@ -934,6 +934,42 @@ function rrfMergeWeighted(ftsResults, vecResults, k = 20, limit = 10, ftsWeight
934
934
  matchType: "hybrid"
935
935
  }));
936
936
  }
937
+ function convexCombinationMerge(ftsResults, vecResults, alpha = 0.5, limit = 10) {
938
+ const vecSims = /* @__PURE__ */ new Map();
939
+ for (const vr of vecResults) {
940
+ vecSims.set(vr.memory.memoryId, {
941
+ sim: 1 - vr.distance,
942
+ memory: vr.memory
943
+ });
944
+ }
945
+ const vecValues = [...vecSims.values()].map((v) => v.sim);
946
+ const vecMin = vecValues.length > 0 ? Math.min(...vecValues) : 0;
947
+ const vecMax = vecValues.length > 0 ? Math.max(...vecValues) : 1;
948
+ const vecRange = vecMax - vecMin || 1;
949
+ const ftsValues = ftsResults.map((r) => r.score);
950
+ const ftsMin = ftsValues.length > 0 ? Math.min(...ftsValues) : 0;
951
+ const ftsMax = ftsValues.length > 0 ? Math.max(...ftsValues) : 1;
952
+ const ftsRange = ftsMax - ftsMin || 1;
953
+ const merged = /* @__PURE__ */ new Map();
954
+ for (const fr of ftsResults) {
955
+ const normFts = (fr.score - ftsMin) / ftsRange;
956
+ const vecEntry = vecSims.get(fr.memory.memoryId);
957
+ const normVec = vecEntry ? (vecEntry.sim - vecMin) / vecRange : 0;
958
+ const score = alpha * normVec + (1 - alpha) * normFts;
959
+ merged.set(fr.memory.memoryId, { score, memory: fr.memory });
960
+ }
961
+ for (const [id, entry] of vecSims) {
962
+ if (merged.has(id)) continue;
963
+ const normVec = (entry.sim - vecMin) / vecRange;
964
+ const score = alpha * normVec;
965
+ merged.set(id, { score, memory: entry.memory });
966
+ }
967
+ return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit).map(({ score, memory }) => ({
968
+ memory,
969
+ score,
970
+ matchType: "hybrid"
971
+ }));
972
+ }
937
973
  function cosineSimilarity(a, b) {
938
974
  let dot = 0;
939
975
  let normA = 0;
@@ -1072,34 +1108,10 @@ function searchMemories(db, query, queryVec, useVecTable = true, skipAccessTrack
1072
1108
  finalResults = deduplicateResults(boosted);
1073
1109
  } else {
1074
1110
  const limit = query.limit ?? DEFAULT_LIMIT;
1075
- const ftsHasEnough = ftsResults.length >= limit;
1076
- if (ftsHasEnough) {
1077
- const vecIds = new Set(vecResults.map((r) => r.memory.memoryId));
1078
- const reranked = ftsResults.map((r) => ({
1079
- ...r,
1080
- score: r.score * (vecIds.has(r.memory.memoryId) ? 1.3 : 1)
1081
- }));
1082
- reranked.sort((a, b) => b.score - a.score);
1083
- const weighted = applyThreeFactorReranking(reranked);
1084
- const boosted = applyKeywordBoost(weighted, queryTokens);
1085
- finalResults = deduplicateResults(boosted);
1086
- } else {
1087
- const ftsIds = new Set(ftsResults.map((r) => r.memory.memoryId));
1088
- const ftsItems = [...ftsResults];
1089
- for (const vr of vecResults) {
1090
- if (ftsItems.length >= limit) break;
1091
- if (ftsIds.has(vr.memory.memoryId)) continue;
1092
- ftsItems.push({
1093
- memory: vr.memory,
1094
- score: (1 - vr.distance) * 0.5,
1095
- // Lower score than FTS items
1096
- matchType: "hybrid"
1097
- });
1098
- }
1099
- const weighted = applyThreeFactorReranking(ftsItems);
1100
- const boosted = applyKeywordBoost(weighted, queryTokens);
1101
- finalResults = deduplicateResults(boosted);
1102
- }
1111
+ const merged = convexCombinationMerge(ftsResults, vecResults, 0.3, limit);
1112
+ const weighted = applyThreeFactorReranking(merged);
1113
+ const boosted = applyKeywordBoost(weighted, queryTokens);
1114
+ finalResults = deduplicateResults(boosted);
1103
1115
  }
1104
1116
  updateAccessTracking(db, finalResults.map((r) => r.memory.memoryId));
1105
1117
  return {
@@ -1722,6 +1734,7 @@ export {
1722
1734
  buildSearchSql,
1723
1735
  computeActivation,
1724
1736
  computeEbbinghaus,
1737
+ convexCombinationMerge,
1725
1738
  createEngine,
1726
1739
  createPreparedStatements,
1727
1740
  createRelation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memrosetta/core",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",