@topgunbuild/core 0.8.0 → 0.9.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/dist/index.js CHANGED
@@ -133,6 +133,7 @@ __export(index_exports, {
133
133
  QuerySubMessageSchema: () => QuerySubMessageSchema,
134
134
  QueryUnsubMessageSchema: () => QueryUnsubMessageSchema,
135
135
  RESOLVER_FORBIDDEN_PATTERNS: () => RESOLVER_FORBIDDEN_PATTERNS,
136
+ ReciprocalRankFusion: () => ReciprocalRankFusion,
136
137
  RegisterResolverRequestSchema: () => RegisterResolverRequestSchema,
137
138
  RegisterResolverResponseSchema: () => RegisterResolverResponseSchema,
138
139
  Ringbuffer: () => Ringbuffer,
@@ -2171,6 +2172,95 @@ var Predicates = class {
2171
2172
  static containsAny(attribute, values) {
2172
2173
  return { op: "containsAny", attribute, value: values };
2173
2174
  }
2175
+ // ============== Full-Text Search Predicates (Phase 12) ==============
2176
+ /**
2177
+ * Create a 'match' predicate for full-text search.
2178
+ * Uses BM25 scoring to find relevant documents.
2179
+ *
2180
+ * @param attribute - Field to search in
2181
+ * @param query - Search query string
2182
+ * @param options - Match options (minScore, boost, operator, fuzziness)
2183
+ *
2184
+ * @example
2185
+ * ```typescript
2186
+ * // Simple match
2187
+ * Predicates.match('title', 'machine learning')
2188
+ *
2189
+ * // With options
2190
+ * Predicates.match('body', 'neural networks', { minScore: 1.0, boost: 2.0 })
2191
+ * ```
2192
+ */
2193
+ static match(attribute, query, options) {
2194
+ return { op: "match", attribute, query, matchOptions: options };
2195
+ }
2196
+ /**
2197
+ * Create a 'matchPhrase' predicate for exact phrase matching.
2198
+ * Matches documents containing the exact phrase (words in order).
2199
+ *
2200
+ * @param attribute - Field to search in
2201
+ * @param query - Phrase to match
2202
+ * @param slop - Word distance tolerance (0 = exact, 1 = allow 1 word between)
2203
+ *
2204
+ * @example
2205
+ * ```typescript
2206
+ * // Exact phrase
2207
+ * Predicates.matchPhrase('body', 'machine learning')
2208
+ *
2209
+ * // With slop (allows "machine deep learning")
2210
+ * Predicates.matchPhrase('body', 'machine learning', 1)
2211
+ * ```
2212
+ */
2213
+ static matchPhrase(attribute, query, slop) {
2214
+ return { op: "matchPhrase", attribute, query, slop };
2215
+ }
2216
+ /**
2217
+ * Create a 'matchPrefix' predicate for prefix matching.
2218
+ * Matches documents where field starts with the given prefix.
2219
+ *
2220
+ * @param attribute - Field to search in
2221
+ * @param prefix - Prefix to match
2222
+ * @param maxExpansions - Maximum number of term expansions
2223
+ *
2224
+ * @example
2225
+ * ```typescript
2226
+ * // Match titles starting with "mach"
2227
+ * Predicates.matchPrefix('title', 'mach')
2228
+ *
2229
+ * // Limit expansions for performance
2230
+ * Predicates.matchPrefix('title', 'mach', 50)
2231
+ * ```
2232
+ */
2233
+ static matchPrefix(attribute, prefix, maxExpansions) {
2234
+ return { op: "matchPrefix", attribute, prefix, maxExpansions };
2235
+ }
2236
+ /**
2237
+ * Create a multi-field match predicate.
2238
+ * Searches across multiple fields with optional per-field boosting.
2239
+ *
2240
+ * @param attributes - Fields to search in
2241
+ * @param query - Search query string
2242
+ * @param options - Options including per-field boost factors
2243
+ *
2244
+ * @example
2245
+ * ```typescript
2246
+ * // Search title and body
2247
+ * Predicates.multiMatch(['title', 'body'], 'machine learning')
2248
+ *
2249
+ * // With boosting (title 2x more important)
2250
+ * Predicates.multiMatch(['title', 'body'], 'machine learning', {
2251
+ * boost: { title: 2.0, body: 1.0 }
2252
+ * })
2253
+ * ```
2254
+ */
2255
+ static multiMatch(attributes, query, options) {
2256
+ const children = attributes.map((attr) => ({
2257
+ op: "match",
2258
+ attribute: attr,
2259
+ query,
2260
+ matchOptions: options?.boost?.[attr] ? { boost: options.boost[attr] } : void 0
2261
+ }));
2262
+ return { op: "or", children };
2263
+ }
2174
2264
  };
2175
2265
  function evaluatePredicate(predicate, data) {
2176
2266
  if (!data) return false;
@@ -3793,6 +3883,9 @@ function isSimpleQuery(query) {
3793
3883
  function isLogicalQuery(query) {
3794
3884
  return query.type === "and" || query.type === "or" || query.type === "not";
3795
3885
  }
3886
+ function isFTSQuery(query) {
3887
+ return query.type === "match" || query.type === "matchPhrase" || query.type === "matchPrefix";
3888
+ }
3796
3889
 
3797
3890
  // src/query/indexes/StandingQueryIndex.ts
3798
3891
  var _StandingQueryIndex = class _StandingQueryIndex {
@@ -6051,11 +6144,48 @@ var QueryOptimizer = class {
6051
6144
  if ("indexRegistry" in indexRegistryOrOptions) {
6052
6145
  this.indexRegistry = indexRegistryOrOptions.indexRegistry;
6053
6146
  this.standingQueryRegistry = indexRegistryOrOptions.standingQueryRegistry;
6147
+ this.fullTextIndexes = indexRegistryOrOptions.fullTextIndexes ?? /* @__PURE__ */ new Map();
6054
6148
  } else {
6055
6149
  this.indexRegistry = indexRegistryOrOptions;
6056
6150
  this.standingQueryRegistry = standingQueryRegistry;
6151
+ this.fullTextIndexes = /* @__PURE__ */ new Map();
6057
6152
  }
6058
6153
  }
6154
+ /**
6155
+ * Register a full-text index for a field (Phase 12).
6156
+ *
6157
+ * @param field - Field name
6158
+ * @param index - FullTextIndex instance
6159
+ */
6160
+ registerFullTextIndex(field, index) {
6161
+ this.fullTextIndexes.set(field, index);
6162
+ }
6163
+ /**
6164
+ * Unregister a full-text index (Phase 12).
6165
+ *
6166
+ * @param field - Field name
6167
+ */
6168
+ unregisterFullTextIndex(field) {
6169
+ this.fullTextIndexes.delete(field);
6170
+ }
6171
+ /**
6172
+ * Get registered full-text index for a field (Phase 12).
6173
+ *
6174
+ * @param field - Field name
6175
+ * @returns FullTextIndex or undefined
6176
+ */
6177
+ getFullTextIndex(field) {
6178
+ return this.fullTextIndexes.get(field);
6179
+ }
6180
+ /**
6181
+ * Check if a full-text index exists for a field (Phase 12).
6182
+ *
6183
+ * @param field - Field name
6184
+ * @returns True if FTS index exists
6185
+ */
6186
+ hasFullTextIndex(field) {
6187
+ return this.fullTextIndexes.has(field);
6188
+ }
6059
6189
  /**
6060
6190
  * Optimize a query and return an execution plan.
6061
6191
  *
@@ -6129,12 +6259,151 @@ var QueryOptimizer = class {
6129
6259
  optimizeNode(query) {
6130
6260
  if (isLogicalQuery(query)) {
6131
6261
  return this.optimizeLogical(query);
6262
+ } else if (isFTSQuery(query)) {
6263
+ return this.optimizeFTS(query);
6132
6264
  } else if (isSimpleQuery(query)) {
6133
6265
  return this.optimizeSimple(query);
6134
6266
  } else {
6135
6267
  return { type: "full-scan", predicate: query };
6136
6268
  }
6137
6269
  }
6270
+ /**
6271
+ * Optimize a full-text search query (Phase 12).
6272
+ */
6273
+ optimizeFTS(query) {
6274
+ const field = query.attribute;
6275
+ if (!this.hasFullTextIndex(field)) {
6276
+ return { type: "full-scan", predicate: query };
6277
+ }
6278
+ return this.buildFTSScanStep(query);
6279
+ }
6280
+ /**
6281
+ * Build an FTS scan step from a query node (Phase 12).
6282
+ */
6283
+ buildFTSScanStep(query) {
6284
+ const field = query.attribute;
6285
+ switch (query.type) {
6286
+ case "match":
6287
+ return {
6288
+ type: "fts-scan",
6289
+ field,
6290
+ query: query.query,
6291
+ ftsType: "match",
6292
+ options: query.options,
6293
+ returnsScored: true,
6294
+ estimatedCost: this.estimateFTSCost(field)
6295
+ };
6296
+ case "matchPhrase":
6297
+ return {
6298
+ type: "fts-scan",
6299
+ field,
6300
+ query: query.query,
6301
+ ftsType: "matchPhrase",
6302
+ options: query.slop !== void 0 ? { fuzziness: query.slop } : void 0,
6303
+ returnsScored: true,
6304
+ estimatedCost: this.estimateFTSCost(field)
6305
+ };
6306
+ case "matchPrefix":
6307
+ return {
6308
+ type: "fts-scan",
6309
+ field,
6310
+ query: query.prefix,
6311
+ ftsType: "matchPrefix",
6312
+ options: query.maxExpansions !== void 0 ? { fuzziness: query.maxExpansions } : void 0,
6313
+ returnsScored: true,
6314
+ estimatedCost: this.estimateFTSCost(field)
6315
+ };
6316
+ default:
6317
+ throw new Error(`Unknown FTS query type: ${query.type}`);
6318
+ }
6319
+ }
6320
+ /**
6321
+ * Estimate cost of FTS query based on index size (Phase 12).
6322
+ */
6323
+ estimateFTSCost(field) {
6324
+ const index = this.fullTextIndexes.get(field);
6325
+ if (!index) {
6326
+ return Number.MAX_SAFE_INTEGER;
6327
+ }
6328
+ const docCount = index.getSize();
6329
+ return 50 + Math.log2(docCount + 1) * 10;
6330
+ }
6331
+ /**
6332
+ * Classify predicates by type for hybrid query planning (Phase 12).
6333
+ *
6334
+ * @param predicates - Array of predicates to classify
6335
+ * @returns Classified predicates
6336
+ */
6337
+ classifyPredicates(predicates) {
6338
+ const result = {
6339
+ exactPredicates: [],
6340
+ rangePredicates: [],
6341
+ ftsPredicates: [],
6342
+ otherPredicates: []
6343
+ };
6344
+ for (const pred of predicates) {
6345
+ if (isFTSQuery(pred)) {
6346
+ result.ftsPredicates.push(pred);
6347
+ } else if (isSimpleQuery(pred)) {
6348
+ switch (pred.type) {
6349
+ case "eq":
6350
+ case "neq":
6351
+ case "in":
6352
+ result.exactPredicates.push(pred);
6353
+ break;
6354
+ case "gt":
6355
+ case "gte":
6356
+ case "lt":
6357
+ case "lte":
6358
+ case "between":
6359
+ result.rangePredicates.push(pred);
6360
+ break;
6361
+ default:
6362
+ result.otherPredicates.push(pred);
6363
+ }
6364
+ } else if (isLogicalQuery(pred)) {
6365
+ result.otherPredicates.push(pred);
6366
+ } else {
6367
+ result.otherPredicates.push(pred);
6368
+ }
6369
+ }
6370
+ return result;
6371
+ }
6372
+ /**
6373
+ * Determine fusion strategy based on step types (Phase 12).
6374
+ *
6375
+ * Strategy selection:
6376
+ * - All binary (exact/range with no scores) → 'intersection'
6377
+ * - All scored (FTS) → 'score-filter' (filter by score, sort by score)
6378
+ * - Mixed (binary + scored) → 'rrf' (Reciprocal Rank Fusion)
6379
+ *
6380
+ * @param steps - Plan steps to fuse
6381
+ * @returns Fusion strategy
6382
+ */
6383
+ determineFusionStrategy(steps) {
6384
+ const hasScored = steps.some((s) => this.stepReturnsScored(s));
6385
+ const hasBinary = steps.some((s) => !this.stepReturnsScored(s));
6386
+ if (hasScored && hasBinary) {
6387
+ return "rrf";
6388
+ } else if (hasScored) {
6389
+ return "score-filter";
6390
+ } else {
6391
+ return "intersection";
6392
+ }
6393
+ }
6394
+ /**
6395
+ * Check if a plan step returns scored results (Phase 12).
6396
+ */
6397
+ stepReturnsScored(step) {
6398
+ switch (step.type) {
6399
+ case "fts-scan":
6400
+ return true;
6401
+ case "fusion":
6402
+ return step.returnsScored;
6403
+ default:
6404
+ return false;
6405
+ }
6406
+ }
6138
6407
  /**
6139
6408
  * Optimize a simple (attribute-based) query.
6140
6409
  */
@@ -6390,6 +6659,18 @@ var QueryOptimizer = class {
6390
6659
  return this.estimateCost(step.source) + 10;
6391
6660
  case "not":
6392
6661
  return this.estimateCost(step.source) + 100;
6662
+ // Phase 12: FTS step types
6663
+ case "fts-scan":
6664
+ return step.estimatedCost;
6665
+ case "fusion":
6666
+ return step.steps.reduce((sum, s) => {
6667
+ const cost = this.estimateCost(s);
6668
+ if (cost === Number.MAX_SAFE_INTEGER) {
6669
+ return Number.MAX_SAFE_INTEGER;
6670
+ }
6671
+ return Math.min(sum + cost, Number.MAX_SAFE_INTEGER);
6672
+ }, 0) + 20;
6673
+ // Fusion overhead
6393
6674
  default:
6394
6675
  return Number.MAX_SAFE_INTEGER;
6395
6676
  }
@@ -6410,6 +6691,12 @@ var QueryOptimizer = class {
6410
6691
  return this.usesIndexes(step.source);
6411
6692
  case "not":
6412
6693
  return this.usesIndexes(step.source);
6694
+ // Phase 12: FTS step types
6695
+ case "fts-scan":
6696
+ return true;
6697
+ // FTS uses FullTextIndex
6698
+ case "fusion":
6699
+ return step.steps.some((s) => this.usesIndexes(s));
6413
6700
  default:
6414
6701
  return false;
6415
6702
  }
@@ -7958,6 +8245,131 @@ var DefaultIndexingStrategy = class {
7958
8245
  }
7959
8246
  };
7960
8247
 
8248
+ // src/search/ReciprocalRankFusion.ts
8249
+ var ReciprocalRankFusion = class {
8250
+ constructor(config) {
8251
+ this.k = config?.k ?? 60;
8252
+ }
8253
+ /**
8254
+ * Merge multiple ranked result lists using RRF.
8255
+ *
8256
+ * Formula: RRF_score(d) = Σ 1 / (k + rank_i(d))
8257
+ *
8258
+ * @param resultSets - Array of ranked result lists from different search methods
8259
+ * @returns Merged results sorted by RRF score (descending)
8260
+ */
8261
+ merge(resultSets) {
8262
+ const nonEmptySets = resultSets.filter((set) => set.length > 0);
8263
+ if (nonEmptySets.length === 0) {
8264
+ return [];
8265
+ }
8266
+ const scoreMap = /* @__PURE__ */ new Map();
8267
+ for (const resultSet of nonEmptySets) {
8268
+ for (let rank = 0; rank < resultSet.length; rank++) {
8269
+ const result = resultSet[rank];
8270
+ const { docId, score, source } = result;
8271
+ const rrfContribution = 1 / (this.k + rank + 1);
8272
+ const existing = scoreMap.get(docId);
8273
+ if (existing) {
8274
+ existing.rrfScore += rrfContribution;
8275
+ existing.sources.add(source);
8276
+ existing.originalScores[source] = score;
8277
+ } else {
8278
+ scoreMap.set(docId, {
8279
+ rrfScore: rrfContribution,
8280
+ sources: /* @__PURE__ */ new Set([source]),
8281
+ originalScores: { [source]: score }
8282
+ });
8283
+ }
8284
+ }
8285
+ }
8286
+ const merged = [];
8287
+ for (const [docId, data] of scoreMap) {
8288
+ merged.push({
8289
+ docId,
8290
+ score: data.rrfScore,
8291
+ source: Array.from(data.sources).sort().join("+"),
8292
+ originalScores: data.originalScores
8293
+ });
8294
+ }
8295
+ merged.sort((a, b) => b.score - a.score);
8296
+ return merged;
8297
+ }
8298
+ /**
8299
+ * Merge with weighted RRF for different method priorities.
8300
+ *
8301
+ * Weighted formula: RRF_score(d) = Σ weight_i * (1 / (k + rank_i(d)))
8302
+ *
8303
+ * @param resultSets - Array of ranked result lists
8304
+ * @param weights - Weights for each result set (same order as resultSets)
8305
+ * @returns Merged results sorted by weighted RRF score (descending)
8306
+ *
8307
+ * @example
8308
+ * ```typescript
8309
+ * const rrf = new ReciprocalRankFusion();
8310
+ *
8311
+ * // Prioritize exact matches (weight 2.0) over FTS (weight 1.0)
8312
+ * const merged = rrf.mergeWeighted(
8313
+ * [exactResults, ftsResults],
8314
+ * [2.0, 1.0]
8315
+ * );
8316
+ * ```
8317
+ */
8318
+ mergeWeighted(resultSets, weights) {
8319
+ if (weights.length !== resultSets.length) {
8320
+ throw new Error(
8321
+ `Weights array length (${weights.length}) must match resultSets length (${resultSets.length})`
8322
+ );
8323
+ }
8324
+ const nonEmptyPairs = [];
8325
+ for (let i = 0; i < resultSets.length; i++) {
8326
+ if (resultSets[i].length > 0) {
8327
+ nonEmptyPairs.push({ resultSet: resultSets[i], weight: weights[i] });
8328
+ }
8329
+ }
8330
+ if (nonEmptyPairs.length === 0) {
8331
+ return [];
8332
+ }
8333
+ const scoreMap = /* @__PURE__ */ new Map();
8334
+ for (const { resultSet, weight } of nonEmptyPairs) {
8335
+ for (let rank = 0; rank < resultSet.length; rank++) {
8336
+ const result = resultSet[rank];
8337
+ const { docId, score, source } = result;
8338
+ const rrfContribution = weight * (1 / (this.k + rank + 1));
8339
+ const existing = scoreMap.get(docId);
8340
+ if (existing) {
8341
+ existing.rrfScore += rrfContribution;
8342
+ existing.sources.add(source);
8343
+ existing.originalScores[source] = score;
8344
+ } else {
8345
+ scoreMap.set(docId, {
8346
+ rrfScore: rrfContribution,
8347
+ sources: /* @__PURE__ */ new Set([source]),
8348
+ originalScores: { [source]: score }
8349
+ });
8350
+ }
8351
+ }
8352
+ }
8353
+ const merged = [];
8354
+ for (const [docId, data] of scoreMap) {
8355
+ merged.push({
8356
+ docId,
8357
+ score: data.rrfScore,
8358
+ source: Array.from(data.sources).sort().join("+"),
8359
+ originalScores: data.originalScores
8360
+ });
8361
+ }
8362
+ merged.sort((a, b) => b.score - a.score);
8363
+ return merged;
8364
+ }
8365
+ /**
8366
+ * Get the k constant used for RRF calculation.
8367
+ */
8368
+ getK() {
8369
+ return this.k;
8370
+ }
8371
+ };
8372
+
7961
8373
  // src/IndexedLWWMap.ts
7962
8374
  var IndexedLWWMap = class extends LWWMap {
7963
8375
  constructor(hlc, options = {}) {
@@ -10638,6 +11050,7 @@ var IndexedORMap = class extends ORMap {
10638
11050
  QuerySubMessageSchema,
10639
11051
  QueryUnsubMessageSchema,
10640
11052
  RESOLVER_FORBIDDEN_PATTERNS,
11053
+ ReciprocalRankFusion,
10641
11054
  RegisterResolverRequestSchema,
10642
11055
  RegisterResolverResponseSchema,
10643
11056
  Ringbuffer,