@topgunbuild/core 0.7.0 → 0.8.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/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AuthMessageSchema: () => AuthMessageSchema,
34
+ BM25Scorer: () => BM25Scorer,
34
35
  BatchMessageSchema: () => BatchMessageSchema,
35
36
  BuiltInProcessors: () => BuiltInProcessors,
36
37
  BuiltInResolvers: () => BuiltInResolvers,
@@ -54,6 +55,7 @@ __export(index_exports, {
54
55
  DEFAULT_RESOLVER_RATE_LIMITS: () => DEFAULT_RESOLVER_RATE_LIMITS,
55
56
  DEFAULT_STOP_WORDS: () => DEFAULT_STOP_WORDS,
56
57
  DEFAULT_WRITE_CONCERN_TIMEOUT: () => DEFAULT_WRITE_CONCERN_TIMEOUT,
58
+ ENGLISH_STOPWORDS: () => ENGLISH_STOPWORDS,
57
59
  EntryProcessBatchRequestSchema: () => EntryProcessBatchRequestSchema,
58
60
  EntryProcessBatchResponseSchema: () => EntryProcessBatchResponseSchema,
59
61
  EntryProcessKeyResultSchema: () => EntryProcessKeyResultSchema,
@@ -63,8 +65,11 @@ __export(index_exports, {
63
65
  EntryProcessorSchema: () => EntryProcessorSchema,
64
66
  EventJournalImpl: () => EventJournalImpl,
65
67
  FORBIDDEN_PATTERNS: () => FORBIDDEN_PATTERNS,
68
+ FTSInvertedIndex: () => BM25InvertedIndex,
69
+ FTSTokenizer: () => BM25Tokenizer,
66
70
  FallbackIndex: () => FallbackIndex,
67
71
  FilteringResultSet: () => FilteringResultSet,
72
+ FullTextIndex: () => FullTextIndex,
68
73
  HLC: () => HLC,
69
74
  HashIndex: () => HashIndex,
70
75
  IndexRegistry: () => IndexRegistry,
@@ -128,9 +133,22 @@ __export(index_exports, {
128
133
  QuerySubMessageSchema: () => QuerySubMessageSchema,
129
134
  QueryUnsubMessageSchema: () => QueryUnsubMessageSchema,
130
135
  RESOLVER_FORBIDDEN_PATTERNS: () => RESOLVER_FORBIDDEN_PATTERNS,
136
+ ReciprocalRankFusion: () => ReciprocalRankFusion,
131
137
  RegisterResolverRequestSchema: () => RegisterResolverRequestSchema,
132
138
  RegisterResolverResponseSchema: () => RegisterResolverResponseSchema,
133
139
  Ringbuffer: () => Ringbuffer,
140
+ SearchMessageSchema: () => SearchMessageSchema,
141
+ SearchOptionsSchema: () => SearchOptionsSchema,
142
+ SearchPayloadSchema: () => SearchPayloadSchema,
143
+ SearchRespMessageSchema: () => SearchRespMessageSchema,
144
+ SearchRespPayloadSchema: () => SearchRespPayloadSchema,
145
+ SearchSubMessageSchema: () => SearchSubMessageSchema,
146
+ SearchSubPayloadSchema: () => SearchSubPayloadSchema,
147
+ SearchUnsubMessageSchema: () => SearchUnsubMessageSchema,
148
+ SearchUnsubPayloadSchema: () => SearchUnsubPayloadSchema,
149
+ SearchUpdateMessageSchema: () => SearchUpdateMessageSchema,
150
+ SearchUpdatePayloadSchema: () => SearchUpdatePayloadSchema,
151
+ SearchUpdateTypeSchema: () => SearchUpdateTypeSchema,
134
152
  SetResultSet: () => SetResultSet,
135
153
  SimpleAttribute: () => SimpleAttribute,
136
154
  SortedMap: () => SortedMap,
@@ -176,6 +194,7 @@ __export(index_exports, {
176
194
  isUsingNativeHash: () => isUsingNativeHash,
177
195
  isWriteConcernAchieved: () => isWriteConcernAchieved,
178
196
  multiAttribute: () => multiAttribute,
197
+ porterStem: () => porterStem,
179
198
  resetNativeHash: () => resetNativeHash,
180
199
  serialize: () => serialize,
181
200
  simpleAttribute: () => simpleAttribute,
@@ -2153,6 +2172,95 @@ var Predicates = class {
2153
2172
  static containsAny(attribute, values) {
2154
2173
  return { op: "containsAny", attribute, value: values };
2155
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
+ }
2156
2264
  };
2157
2265
  function evaluatePredicate(predicate, data) {
2158
2266
  if (!data) return false;
@@ -2596,6 +2704,66 @@ var JournalReadResponseSchema = import_zod3.z.object({
2596
2704
  events: import_zod3.z.array(JournalEventDataSchema),
2597
2705
  hasMore: import_zod3.z.boolean()
2598
2706
  });
2707
+ var SearchOptionsSchema = import_zod3.z.object({
2708
+ limit: import_zod3.z.number().optional(),
2709
+ minScore: import_zod3.z.number().optional(),
2710
+ boost: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.number()).optional()
2711
+ });
2712
+ var SearchPayloadSchema = import_zod3.z.object({
2713
+ requestId: import_zod3.z.string(),
2714
+ mapName: import_zod3.z.string(),
2715
+ query: import_zod3.z.string(),
2716
+ options: SearchOptionsSchema.optional()
2717
+ });
2718
+ var SearchMessageSchema = import_zod3.z.object({
2719
+ type: import_zod3.z.literal("SEARCH"),
2720
+ payload: SearchPayloadSchema
2721
+ });
2722
+ var SearchRespPayloadSchema = import_zod3.z.object({
2723
+ requestId: import_zod3.z.string(),
2724
+ results: import_zod3.z.array(import_zod3.z.object({
2725
+ key: import_zod3.z.string(),
2726
+ value: import_zod3.z.unknown(),
2727
+ score: import_zod3.z.number(),
2728
+ matchedTerms: import_zod3.z.array(import_zod3.z.string())
2729
+ })),
2730
+ totalCount: import_zod3.z.number(),
2731
+ error: import_zod3.z.string().optional()
2732
+ });
2733
+ var SearchRespMessageSchema = import_zod3.z.object({
2734
+ type: import_zod3.z.literal("SEARCH_RESP"),
2735
+ payload: SearchRespPayloadSchema
2736
+ });
2737
+ var SearchUpdateTypeSchema = import_zod3.z.enum(["ENTER", "UPDATE", "LEAVE"]);
2738
+ var SearchSubPayloadSchema = import_zod3.z.object({
2739
+ subscriptionId: import_zod3.z.string(),
2740
+ mapName: import_zod3.z.string(),
2741
+ query: import_zod3.z.string(),
2742
+ options: SearchOptionsSchema.optional()
2743
+ });
2744
+ var SearchSubMessageSchema = import_zod3.z.object({
2745
+ type: import_zod3.z.literal("SEARCH_SUB"),
2746
+ payload: SearchSubPayloadSchema
2747
+ });
2748
+ var SearchUpdatePayloadSchema = import_zod3.z.object({
2749
+ subscriptionId: import_zod3.z.string(),
2750
+ key: import_zod3.z.string(),
2751
+ value: import_zod3.z.unknown(),
2752
+ score: import_zod3.z.number(),
2753
+ matchedTerms: import_zod3.z.array(import_zod3.z.string()),
2754
+ type: SearchUpdateTypeSchema
2755
+ });
2756
+ var SearchUpdateMessageSchema = import_zod3.z.object({
2757
+ type: import_zod3.z.literal("SEARCH_UPDATE"),
2758
+ payload: SearchUpdatePayloadSchema
2759
+ });
2760
+ var SearchUnsubPayloadSchema = import_zod3.z.object({
2761
+ subscriptionId: import_zod3.z.string()
2762
+ });
2763
+ var SearchUnsubMessageSchema = import_zod3.z.object({
2764
+ type: import_zod3.z.literal("SEARCH_UNSUB"),
2765
+ payload: SearchUnsubPayloadSchema
2766
+ });
2599
2767
  var ConflictResolverSchema = import_zod3.z.object({
2600
2768
  name: import_zod3.z.string().min(1).max(100),
2601
2769
  code: import_zod3.z.string().max(5e4),
@@ -2727,7 +2895,14 @@ var MessageSchema = import_zod3.z.discriminatedUnion("type", [
2727
2895
  UnregisterResolverResponseSchema,
2728
2896
  MergeRejectedMessageSchema,
2729
2897
  ListResolversRequestSchema,
2730
- ListResolversResponseSchema
2898
+ ListResolversResponseSchema,
2899
+ // Phase 11.1: Full-Text Search
2900
+ SearchMessageSchema,
2901
+ SearchRespMessageSchema,
2902
+ // Phase 11.1b: Live Search Subscriptions
2903
+ SearchSubMessageSchema,
2904
+ SearchUpdateMessageSchema,
2905
+ SearchUnsubMessageSchema
2731
2906
  ]);
2732
2907
 
2733
2908
  // src/types/WriteConcern.ts
@@ -3708,6 +3883,9 @@ function isSimpleQuery(query) {
3708
3883
  function isLogicalQuery(query) {
3709
3884
  return query.type === "and" || query.type === "or" || query.type === "not";
3710
3885
  }
3886
+ function isFTSQuery(query) {
3887
+ return query.type === "match" || query.type === "matchPhrase" || query.type === "matchPrefix";
3888
+ }
3711
3889
 
3712
3890
  // src/query/indexes/StandingQueryIndex.ts
3713
3891
  var _StandingQueryIndex = class _StandingQueryIndex {
@@ -5966,11 +6144,48 @@ var QueryOptimizer = class {
5966
6144
  if ("indexRegistry" in indexRegistryOrOptions) {
5967
6145
  this.indexRegistry = indexRegistryOrOptions.indexRegistry;
5968
6146
  this.standingQueryRegistry = indexRegistryOrOptions.standingQueryRegistry;
6147
+ this.fullTextIndexes = indexRegistryOrOptions.fullTextIndexes ?? /* @__PURE__ */ new Map();
5969
6148
  } else {
5970
6149
  this.indexRegistry = indexRegistryOrOptions;
5971
6150
  this.standingQueryRegistry = standingQueryRegistry;
6151
+ this.fullTextIndexes = /* @__PURE__ */ new Map();
5972
6152
  }
5973
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
+ }
5974
6189
  /**
5975
6190
  * Optimize a query and return an execution plan.
5976
6191
  *
@@ -6044,12 +6259,151 @@ var QueryOptimizer = class {
6044
6259
  optimizeNode(query) {
6045
6260
  if (isLogicalQuery(query)) {
6046
6261
  return this.optimizeLogical(query);
6262
+ } else if (isFTSQuery(query)) {
6263
+ return this.optimizeFTS(query);
6047
6264
  } else if (isSimpleQuery(query)) {
6048
6265
  return this.optimizeSimple(query);
6049
6266
  } else {
6050
6267
  return { type: "full-scan", predicate: query };
6051
6268
  }
6052
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
+ }
6053
6407
  /**
6054
6408
  * Optimize a simple (attribute-based) query.
6055
6409
  */
@@ -6305,6 +6659,18 @@ var QueryOptimizer = class {
6305
6659
  return this.estimateCost(step.source) + 10;
6306
6660
  case "not":
6307
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
6308
6674
  default:
6309
6675
  return Number.MAX_SAFE_INTEGER;
6310
6676
  }
@@ -6325,6 +6691,12 @@ var QueryOptimizer = class {
6325
6691
  return this.usesIndexes(step.source);
6326
6692
  case "not":
6327
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));
6328
6700
  default:
6329
6701
  return false;
6330
6702
  }
@@ -7873,6 +8245,131 @@ var DefaultIndexingStrategy = class {
7873
8245
  }
7874
8246
  };
7875
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
+
7876
8373
  // src/IndexedLWWMap.ts
7877
8374
  var IndexedLWWMap = class extends LWWMap {
7878
8375
  constructor(hlc, options = {}) {
@@ -8629,49 +9126,1115 @@ var IndexedLWWMap = class extends LWWMap {
8629
9126
  }
8630
9127
  };
8631
9128
 
8632
- // src/IndexedORMap.ts
8633
- var IndexedORMap = class extends ORMap {
8634
- constructor(hlc, options = {}) {
8635
- super(hlc);
8636
- this.options = options;
8637
- this.indexRegistry = new IndexRegistry();
8638
- this.queryOptimizer = new QueryOptimizer({
8639
- indexRegistry: this.indexRegistry
8640
- });
8641
- this.indexRegistry.setFallbackIndex(
8642
- new FallbackIndex(
8643
- () => this.getAllCompositeKeys(),
8644
- (compositeKey) => this.getRecordByCompositeKey(compositeKey),
8645
- (record, query) => this.matchesIndexQuery(record, query)
8646
- )
8647
- );
8648
- this.queryTracker = new QueryPatternTracker();
8649
- this.indexAdvisor = new IndexAdvisor(this.queryTracker);
8650
- if (options.adaptiveIndexing?.autoIndex?.enabled) {
8651
- this.autoIndexManager = new AutoIndexManager(
8652
- this.queryTracker,
8653
- this.indexAdvisor,
8654
- options.adaptiveIndexing.autoIndex
8655
- );
8656
- this.autoIndexManager.setMap(this);
8657
- } else {
8658
- this.autoIndexManager = null;
9129
+ // src/query/tokenization/stopwords.ts
9130
+ var ENGLISH_STOPWORDS = /* @__PURE__ */ new Set([
9131
+ // Articles
9132
+ "a",
9133
+ "an",
9134
+ "the",
9135
+ // Pronouns
9136
+ "i",
9137
+ "me",
9138
+ "my",
9139
+ "myself",
9140
+ "we",
9141
+ "our",
9142
+ "ours",
9143
+ "ourselves",
9144
+ "you",
9145
+ "your",
9146
+ "yours",
9147
+ "yourself",
9148
+ "yourselves",
9149
+ "he",
9150
+ "him",
9151
+ "his",
9152
+ "himself",
9153
+ "she",
9154
+ "her",
9155
+ "hers",
9156
+ "herself",
9157
+ "it",
9158
+ "its",
9159
+ "itself",
9160
+ "they",
9161
+ "them",
9162
+ "their",
9163
+ "theirs",
9164
+ "themselves",
9165
+ "what",
9166
+ "which",
9167
+ "who",
9168
+ "whom",
9169
+ "this",
9170
+ "that",
9171
+ "these",
9172
+ "those",
9173
+ // Auxiliary verbs
9174
+ "am",
9175
+ "is",
9176
+ "are",
9177
+ "was",
9178
+ "were",
9179
+ "be",
9180
+ "been",
9181
+ "being",
9182
+ "have",
9183
+ "has",
9184
+ "had",
9185
+ "having",
9186
+ "do",
9187
+ "does",
9188
+ "did",
9189
+ "doing",
9190
+ "will",
9191
+ "would",
9192
+ "shall",
9193
+ "should",
9194
+ "can",
9195
+ "could",
9196
+ "may",
9197
+ "might",
9198
+ "must",
9199
+ "ought",
9200
+ // Prepositions
9201
+ "about",
9202
+ "above",
9203
+ "across",
9204
+ "after",
9205
+ "against",
9206
+ "along",
9207
+ "among",
9208
+ "around",
9209
+ "at",
9210
+ "before",
9211
+ "behind",
9212
+ "below",
9213
+ "beneath",
9214
+ "beside",
9215
+ "between",
9216
+ "beyond",
9217
+ "by",
9218
+ "down",
9219
+ "during",
9220
+ "except",
9221
+ "for",
9222
+ "from",
9223
+ "in",
9224
+ "inside",
9225
+ "into",
9226
+ "near",
9227
+ "of",
9228
+ "off",
9229
+ "on",
9230
+ "onto",
9231
+ "out",
9232
+ "outside",
9233
+ "over",
9234
+ "past",
9235
+ "since",
9236
+ "through",
9237
+ "throughout",
9238
+ "to",
9239
+ "toward",
9240
+ "towards",
9241
+ "under",
9242
+ "underneath",
9243
+ "until",
9244
+ "up",
9245
+ "upon",
9246
+ "with",
9247
+ "within",
9248
+ "without",
9249
+ // Conjunctions
9250
+ "and",
9251
+ "but",
9252
+ "or",
9253
+ "nor",
9254
+ "so",
9255
+ "yet",
9256
+ "both",
9257
+ "either",
9258
+ "neither",
9259
+ "not",
9260
+ "only",
9261
+ "as",
9262
+ "if",
9263
+ "than",
9264
+ "when",
9265
+ "while",
9266
+ "although",
9267
+ "because",
9268
+ "unless",
9269
+ "whether",
9270
+ // Adverbs
9271
+ "here",
9272
+ "there",
9273
+ "where",
9274
+ "when",
9275
+ "how",
9276
+ "why",
9277
+ "all",
9278
+ "each",
9279
+ "every",
9280
+ "any",
9281
+ "some",
9282
+ "no",
9283
+ "none",
9284
+ "more",
9285
+ "most",
9286
+ "other",
9287
+ "such",
9288
+ "own",
9289
+ "same",
9290
+ "too",
9291
+ "very",
9292
+ "just",
9293
+ "also",
9294
+ "now",
9295
+ "then",
9296
+ "again",
9297
+ "ever",
9298
+ "once",
9299
+ // Misc
9300
+ "few",
9301
+ "many",
9302
+ "much",
9303
+ "several",
9304
+ "s",
9305
+ "t",
9306
+ "d",
9307
+ "ll",
9308
+ "m",
9309
+ "ve",
9310
+ "re"
9311
+ ]);
9312
+
9313
+ // src/query/tokenization/porter-stemmer.ts
9314
+ function porterStem(word) {
9315
+ if (!word || word.length < 3) {
9316
+ return word;
9317
+ }
9318
+ let stem = word;
9319
+ if (stem.endsWith("sses")) {
9320
+ stem = stem.slice(0, -2);
9321
+ } else if (stem.endsWith("ies")) {
9322
+ stem = stem.slice(0, -2);
9323
+ } else if (!stem.endsWith("ss") && stem.endsWith("s")) {
9324
+ stem = stem.slice(0, -1);
9325
+ }
9326
+ const step1bRegex = /^(.+?)(eed|ed|ing)$/;
9327
+ const step1bMatch = stem.match(step1bRegex);
9328
+ if (step1bMatch) {
9329
+ const [, base, suffix] = step1bMatch;
9330
+ if (suffix === "eed") {
9331
+ if (getMeasure(base) > 0) {
9332
+ stem = base + "ee";
9333
+ }
9334
+ } else if (hasVowel(base)) {
9335
+ stem = base;
9336
+ if (stem.endsWith("at") || stem.endsWith("bl") || stem.endsWith("iz")) {
9337
+ stem = stem + "e";
9338
+ } else if (endsWithDoubleConsonant(stem) && !stem.match(/[lsz]$/)) {
9339
+ stem = stem.slice(0, -1);
9340
+ } else if (getMeasure(stem) === 1 && endsWithCVC(stem)) {
9341
+ stem = stem + "e";
9342
+ }
9343
+ }
9344
+ }
9345
+ if (stem.endsWith("y") && hasVowel(stem.slice(0, -1))) {
9346
+ stem = stem.slice(0, -1) + "i";
9347
+ }
9348
+ const step2Suffixes = [
9349
+ [/ational$/, "ate", 0],
9350
+ [/tional$/, "tion", 0],
9351
+ [/enci$/, "ence", 0],
9352
+ [/anci$/, "ance", 0],
9353
+ [/izer$/, "ize", 0],
9354
+ [/abli$/, "able", 0],
9355
+ [/alli$/, "al", 0],
9356
+ [/entli$/, "ent", 0],
9357
+ [/eli$/, "e", 0],
9358
+ [/ousli$/, "ous", 0],
9359
+ [/ization$/, "ize", 0],
9360
+ [/ation$/, "ate", 0],
9361
+ [/ator$/, "ate", 0],
9362
+ [/alism$/, "al", 0],
9363
+ [/iveness$/, "ive", 0],
9364
+ [/fulness$/, "ful", 0],
9365
+ [/ousness$/, "ous", 0],
9366
+ [/aliti$/, "al", 0],
9367
+ [/iviti$/, "ive", 0],
9368
+ [/biliti$/, "ble", 0]
9369
+ ];
9370
+ for (const [regex, replacement, minMeasure] of step2Suffixes) {
9371
+ if (regex.test(stem)) {
9372
+ const base = stem.replace(regex, "");
9373
+ if (getMeasure(base) > minMeasure) {
9374
+ stem = base + replacement;
9375
+ break;
9376
+ }
8659
9377
  }
8660
- if (options.defaultIndexing && options.defaultIndexing !== "none") {
8661
- this.defaultIndexingStrategy = new DefaultIndexingStrategy(options.defaultIndexing);
8662
- } else {
8663
- this.defaultIndexingStrategy = null;
9378
+ }
9379
+ const step3Suffixes = [
9380
+ [/icate$/, "ic", 0],
9381
+ [/ative$/, "", 0],
9382
+ [/alize$/, "al", 0],
9383
+ [/iciti$/, "ic", 0],
9384
+ [/ical$/, "ic", 0],
9385
+ [/ful$/, "", 0],
9386
+ [/ness$/, "", 0]
9387
+ ];
9388
+ for (const [regex, replacement, minMeasure] of step3Suffixes) {
9389
+ if (regex.test(stem)) {
9390
+ const base = stem.replace(regex, "");
9391
+ if (getMeasure(base) > minMeasure) {
9392
+ stem = base + replacement;
9393
+ break;
9394
+ }
8664
9395
  }
8665
9396
  }
8666
- // ==================== Index Management ====================
8667
- /**
8668
- * Add a hash index on an attribute.
8669
- *
8670
- * @param attribute - Attribute to index
8671
- * @returns Created HashIndex
8672
- */
8673
- addHashIndex(attribute) {
8674
- const index = new HashIndex(attribute);
9397
+ const step4Suffixes = [
9398
+ [/al$/, 1],
9399
+ [/ance$/, 1],
9400
+ [/ence$/, 1],
9401
+ [/er$/, 1],
9402
+ [/ic$/, 1],
9403
+ [/able$/, 1],
9404
+ [/ible$/, 1],
9405
+ [/ant$/, 1],
9406
+ [/ement$/, 1],
9407
+ [/ment$/, 1],
9408
+ [/ent$/, 1],
9409
+ [/ion$/, 1],
9410
+ [/ou$/, 1],
9411
+ [/ism$/, 1],
9412
+ [/ate$/, 1],
9413
+ [/iti$/, 1],
9414
+ [/ous$/, 1],
9415
+ [/ive$/, 1],
9416
+ [/ize$/, 1]
9417
+ ];
9418
+ for (const [regex, minMeasure] of step4Suffixes) {
9419
+ if (regex.test(stem)) {
9420
+ const base = stem.replace(regex, "");
9421
+ if (getMeasure(base) > minMeasure) {
9422
+ if (regex.source === "ion$") {
9423
+ if (base.match(/[st]$/)) {
9424
+ stem = base;
9425
+ }
9426
+ } else {
9427
+ stem = base;
9428
+ }
9429
+ break;
9430
+ }
9431
+ }
9432
+ }
9433
+ if (stem.endsWith("e")) {
9434
+ const base = stem.slice(0, -1);
9435
+ const measure = getMeasure(base);
9436
+ if (measure > 1 || measure === 1 && !endsWithCVC(base)) {
9437
+ stem = base;
9438
+ }
9439
+ }
9440
+ if (getMeasure(stem) > 1 && endsWithDoubleConsonant(stem) && stem.endsWith("l")) {
9441
+ stem = stem.slice(0, -1);
9442
+ }
9443
+ return stem;
9444
+ }
9445
+ function isVowel(char, prevChar) {
9446
+ if ("aeiou".includes(char)) {
9447
+ return true;
9448
+ }
9449
+ if (char === "y" && prevChar && !"aeiou".includes(prevChar)) {
9450
+ return true;
9451
+ }
9452
+ return false;
9453
+ }
9454
+ function hasVowel(str) {
9455
+ for (let i = 0; i < str.length; i++) {
9456
+ if (isVowel(str[i], i > 0 ? str[i - 1] : void 0)) {
9457
+ return true;
9458
+ }
9459
+ }
9460
+ return false;
9461
+ }
9462
+ function getMeasure(str) {
9463
+ let pattern = "";
9464
+ for (let i = 0; i < str.length; i++) {
9465
+ pattern += isVowel(str[i], i > 0 ? str[i - 1] : void 0) ? "v" : "c";
9466
+ }
9467
+ const matches = pattern.match(/vc/g);
9468
+ return matches ? matches.length : 0;
9469
+ }
9470
+ function endsWithDoubleConsonant(str) {
9471
+ if (str.length < 2) return false;
9472
+ const last = str[str.length - 1];
9473
+ const secondLast = str[str.length - 2];
9474
+ return last === secondLast && !"aeiou".includes(last);
9475
+ }
9476
+ function endsWithCVC(str) {
9477
+ if (str.length < 3) return false;
9478
+ const last3 = str.slice(-3);
9479
+ const c1 = !"aeiou".includes(last3[0]);
9480
+ const v = isVowel(last3[1], last3[0]);
9481
+ const c2 = !"aeiou".includes(last3[2]) && !"wxy".includes(last3[2]);
9482
+ return c1 && v && c2;
9483
+ }
9484
+
9485
+ // src/fts/Tokenizer.ts
9486
+ var BM25Tokenizer = class {
9487
+ /**
9488
+ * Create a new BM25Tokenizer.
9489
+ *
9490
+ * @param options - Configuration options
9491
+ */
9492
+ constructor(options) {
9493
+ this.options = {
9494
+ lowercase: true,
9495
+ stopwords: ENGLISH_STOPWORDS,
9496
+ stemmer: porterStem,
9497
+ minLength: 2,
9498
+ maxLength: 40,
9499
+ ...options
9500
+ };
9501
+ }
9502
+ /**
9503
+ * Tokenize text into an array of normalized tokens.
9504
+ *
9505
+ * @param text - Text to tokenize
9506
+ * @returns Array of tokens
9507
+ */
9508
+ tokenize(text) {
9509
+ if (!text || typeof text !== "string") {
9510
+ return [];
9511
+ }
9512
+ let processed = this.options.lowercase ? text.toLowerCase() : text;
9513
+ const words = processed.split(/[^\p{L}\p{N}]+/u).filter((w) => w.length > 0);
9514
+ const tokens = [];
9515
+ for (const word of words) {
9516
+ if (word.length < this.options.minLength) {
9517
+ continue;
9518
+ }
9519
+ if (this.options.stopwords.has(word)) {
9520
+ continue;
9521
+ }
9522
+ const stemmed = this.options.stemmer(word);
9523
+ if (stemmed.length < this.options.minLength) {
9524
+ continue;
9525
+ }
9526
+ if (stemmed.length > this.options.maxLength) {
9527
+ continue;
9528
+ }
9529
+ tokens.push(stemmed);
9530
+ }
9531
+ return tokens;
9532
+ }
9533
+ };
9534
+
9535
+ // src/fts/BM25InvertedIndex.ts
9536
+ var BM25InvertedIndex = class {
9537
+ constructor() {
9538
+ this.index = /* @__PURE__ */ new Map();
9539
+ this.docLengths = /* @__PURE__ */ new Map();
9540
+ this.docTerms = /* @__PURE__ */ new Map();
9541
+ this.idfCache = /* @__PURE__ */ new Map();
9542
+ this.totalDocs = 0;
9543
+ this.avgDocLength = 0;
9544
+ }
9545
+ /**
9546
+ * Add a document to the index.
9547
+ *
9548
+ * @param docId - Unique document identifier
9549
+ * @param tokens - Array of tokens (already tokenized/stemmed)
9550
+ */
9551
+ addDocument(docId, tokens) {
9552
+ const termFreqs = /* @__PURE__ */ new Map();
9553
+ const uniqueTerms = /* @__PURE__ */ new Set();
9554
+ for (const token of tokens) {
9555
+ termFreqs.set(token, (termFreqs.get(token) || 0) + 1);
9556
+ uniqueTerms.add(token);
9557
+ }
9558
+ for (const [term, freq] of termFreqs) {
9559
+ if (!this.index.has(term)) {
9560
+ this.index.set(term, []);
9561
+ }
9562
+ this.index.get(term).push({
9563
+ docId,
9564
+ termFrequency: freq
9565
+ });
9566
+ }
9567
+ this.docLengths.set(docId, tokens.length);
9568
+ this.docTerms.set(docId, uniqueTerms);
9569
+ this.totalDocs++;
9570
+ this.updateAvgDocLength();
9571
+ this.idfCache.clear();
9572
+ }
9573
+ /**
9574
+ * Remove a document from the index.
9575
+ *
9576
+ * @param docId - Document identifier to remove
9577
+ */
9578
+ removeDocument(docId) {
9579
+ const terms = this.docTerms.get(docId);
9580
+ if (!terms) {
9581
+ return;
9582
+ }
9583
+ for (const term of terms) {
9584
+ const termInfos = this.index.get(term);
9585
+ if (termInfos) {
9586
+ const filtered = termInfos.filter((info) => info.docId !== docId);
9587
+ if (filtered.length === 0) {
9588
+ this.index.delete(term);
9589
+ } else {
9590
+ this.index.set(term, filtered);
9591
+ }
9592
+ }
9593
+ }
9594
+ this.docLengths.delete(docId);
9595
+ this.docTerms.delete(docId);
9596
+ this.totalDocs--;
9597
+ this.updateAvgDocLength();
9598
+ this.idfCache.clear();
9599
+ }
9600
+ /**
9601
+ * Get all documents containing a term.
9602
+ *
9603
+ * @param term - Term to look up
9604
+ * @returns Array of TermInfo objects
9605
+ */
9606
+ getDocumentsForTerm(term) {
9607
+ return this.index.get(term) || [];
9608
+ }
9609
+ /**
9610
+ * Calculate IDF (Inverse Document Frequency) for a term.
9611
+ *
9612
+ * Uses BM25 IDF formula:
9613
+ * IDF = log((N - df + 0.5) / (df + 0.5) + 1)
9614
+ *
9615
+ * Where:
9616
+ * - N = total documents
9617
+ * - df = document frequency (docs containing term)
9618
+ *
9619
+ * @param term - Term to calculate IDF for
9620
+ * @returns IDF value (0 if term doesn't exist)
9621
+ */
9622
+ getIDF(term) {
9623
+ if (this.idfCache.has(term)) {
9624
+ return this.idfCache.get(term);
9625
+ }
9626
+ const termInfos = this.index.get(term);
9627
+ if (!termInfos || termInfos.length === 0) {
9628
+ return 0;
9629
+ }
9630
+ const docFreq = termInfos.length;
9631
+ const idf = Math.log((this.totalDocs - docFreq + 0.5) / (docFreq + 0.5) + 1);
9632
+ this.idfCache.set(term, idf);
9633
+ return idf;
9634
+ }
9635
+ /**
9636
+ * Get the length of a document (number of tokens).
9637
+ *
9638
+ * @param docId - Document identifier
9639
+ * @returns Document length (0 if not found)
9640
+ */
9641
+ getDocLength(docId) {
9642
+ return this.docLengths.get(docId) || 0;
9643
+ }
9644
+ /**
9645
+ * Get the average document length.
9646
+ *
9647
+ * @returns Average length across all documents
9648
+ */
9649
+ getAvgDocLength() {
9650
+ return this.avgDocLength;
9651
+ }
9652
+ /**
9653
+ * Get the total number of documents in the index.
9654
+ *
9655
+ * @returns Total document count
9656
+ */
9657
+ getTotalDocs() {
9658
+ return this.totalDocs;
9659
+ }
9660
+ /**
9661
+ * Get iterator for document lengths (useful for serialization).
9662
+ *
9663
+ * @returns Iterator of [docId, length] pairs
9664
+ */
9665
+ getDocLengths() {
9666
+ return this.docLengths.entries();
9667
+ }
9668
+ /**
9669
+ * Get the number of documents in the index (alias for getTotalDocs).
9670
+ *
9671
+ * @returns Number of indexed documents
9672
+ */
9673
+ getSize() {
9674
+ return this.totalDocs;
9675
+ }
9676
+ /**
9677
+ * Clear all data from the index.
9678
+ */
9679
+ clear() {
9680
+ this.index.clear();
9681
+ this.docLengths.clear();
9682
+ this.docTerms.clear();
9683
+ this.idfCache.clear();
9684
+ this.totalDocs = 0;
9685
+ this.avgDocLength = 0;
9686
+ }
9687
+ /**
9688
+ * Check if a document exists in the index.
9689
+ *
9690
+ * @param docId - Document identifier
9691
+ * @returns True if document exists
9692
+ */
9693
+ hasDocument(docId) {
9694
+ return this.docTerms.has(docId);
9695
+ }
9696
+ /**
9697
+ * Get all unique terms in the index.
9698
+ *
9699
+ * @returns Iterator of all terms
9700
+ */
9701
+ getTerms() {
9702
+ return this.index.keys();
9703
+ }
9704
+ /**
9705
+ * Get the number of unique terms in the index.
9706
+ *
9707
+ * @returns Number of unique terms
9708
+ */
9709
+ getTermCount() {
9710
+ return this.index.size;
9711
+ }
9712
+ /**
9713
+ * Update the average document length after add/remove.
9714
+ */
9715
+ updateAvgDocLength() {
9716
+ if (this.totalDocs === 0) {
9717
+ this.avgDocLength = 0;
9718
+ return;
9719
+ }
9720
+ let sum = 0;
9721
+ for (const length of this.docLengths.values()) {
9722
+ sum += length;
9723
+ }
9724
+ this.avgDocLength = sum / this.totalDocs;
9725
+ }
9726
+ };
9727
+
9728
+ // src/fts/BM25Scorer.ts
9729
+ var BM25Scorer = class {
9730
+ /**
9731
+ * Create a new BM25 scorer.
9732
+ *
9733
+ * @param options - BM25 configuration options
9734
+ */
9735
+ constructor(options) {
9736
+ this.k1 = options?.k1 ?? 1.2;
9737
+ this.b = options?.b ?? 0.75;
9738
+ }
9739
+ /**
9740
+ * Score documents against a query.
9741
+ *
9742
+ * @param queryTerms - Array of query terms (already tokenized/stemmed)
9743
+ * @param index - The inverted index to search
9744
+ * @returns Array of scored documents, sorted by relevance (descending)
9745
+ */
9746
+ score(queryTerms, index) {
9747
+ if (queryTerms.length === 0 || index.getTotalDocs() === 0) {
9748
+ return [];
9749
+ }
9750
+ const avgDocLength = index.getAvgDocLength();
9751
+ const docScores = /* @__PURE__ */ new Map();
9752
+ for (const term of queryTerms) {
9753
+ const idf = index.getIDF(term);
9754
+ if (idf === 0) {
9755
+ continue;
9756
+ }
9757
+ const termInfos = index.getDocumentsForTerm(term);
9758
+ for (const { docId, termFrequency } of termInfos) {
9759
+ const docLength = index.getDocLength(docId);
9760
+ const numerator = termFrequency * (this.k1 + 1);
9761
+ const denominator = termFrequency + this.k1 * (1 - this.b + this.b * (docLength / avgDocLength));
9762
+ const termScore = idf * (numerator / denominator);
9763
+ const current = docScores.get(docId) || { score: 0, terms: /* @__PURE__ */ new Set() };
9764
+ current.score += termScore;
9765
+ current.terms.add(term);
9766
+ docScores.set(docId, current);
9767
+ }
9768
+ }
9769
+ const results = [];
9770
+ for (const [docId, { score, terms }] of docScores) {
9771
+ results.push({
9772
+ docId,
9773
+ score,
9774
+ matchedTerms: Array.from(terms)
9775
+ });
9776
+ }
9777
+ results.sort((a, b) => b.score - a.score);
9778
+ return results;
9779
+ }
9780
+ /**
9781
+ * Score a single document against query terms.
9782
+ * Uses pre-computed IDF from index but calculates TF locally.
9783
+ *
9784
+ * Complexity: O(Q × D) where Q = query terms, D = document tokens
9785
+ *
9786
+ * @param queryTerms - Tokenized query terms
9787
+ * @param docTokens - Tokenized document terms
9788
+ * @param index - Inverted index for IDF and avgDocLength
9789
+ * @returns BM25 score (0 if no matching terms)
9790
+ */
9791
+ scoreSingleDocument(queryTerms, docTokens, index) {
9792
+ if (queryTerms.length === 0 || docTokens.length === 0) {
9793
+ return 0;
9794
+ }
9795
+ const avgDocLength = index.getAvgDocLength();
9796
+ const docLength = docTokens.length;
9797
+ if (avgDocLength === 0) {
9798
+ return 0;
9799
+ }
9800
+ const termFreqs = /* @__PURE__ */ new Map();
9801
+ for (const token of docTokens) {
9802
+ termFreqs.set(token, (termFreqs.get(token) || 0) + 1);
9803
+ }
9804
+ let score = 0;
9805
+ for (const term of queryTerms) {
9806
+ const tf = termFreqs.get(term) || 0;
9807
+ if (tf === 0) {
9808
+ continue;
9809
+ }
9810
+ const idf = index.getIDF(term);
9811
+ if (idf <= 0) {
9812
+ continue;
9813
+ }
9814
+ const numerator = tf * (this.k1 + 1);
9815
+ const denominator = tf + this.k1 * (1 - this.b + this.b * (docLength / avgDocLength));
9816
+ const termScore = idf * (numerator / denominator);
9817
+ score += termScore;
9818
+ }
9819
+ return score;
9820
+ }
9821
+ /**
9822
+ * Get the k1 parameter value.
9823
+ */
9824
+ getK1() {
9825
+ return this.k1;
9826
+ }
9827
+ /**
9828
+ * Get the b parameter value.
9829
+ */
9830
+ getB() {
9831
+ return this.b;
9832
+ }
9833
+ };
9834
+
9835
+ // src/fts/IndexSerializer.ts
9836
+ var IndexSerializer = class {
9837
+ /**
9838
+ * Serialize inverted index to a JSON-serializable object.
9839
+ * Note: In a real app, you might want to encoding this to binary (msgpack) later.
9840
+ */
9841
+ serialize(index) {
9842
+ const data = {
9843
+ version: 1,
9844
+ metadata: {
9845
+ totalDocs: index.getTotalDocs(),
9846
+ avgDocLength: index.getAvgDocLength(),
9847
+ createdAt: Date.now(),
9848
+ lastModified: Date.now()
9849
+ },
9850
+ terms: this.serializeTerms(index),
9851
+ docLengths: this.serializeDocLengths(index)
9852
+ };
9853
+ return data;
9854
+ }
9855
+ /**
9856
+ * Deserialize from object into a new BM25InvertedIndex.
9857
+ */
9858
+ deserialize(data) {
9859
+ if (data.version !== 1) {
9860
+ throw new Error(`Unsupported index version: ${data.version}`);
9861
+ }
9862
+ const index = new BM25InvertedIndex();
9863
+ this.loadIntoIndex(index, data);
9864
+ return index;
9865
+ }
9866
+ serializeTerms(index) {
9867
+ const terms = [];
9868
+ const indexMap = index.index;
9869
+ for (const term of index.getTerms()) {
9870
+ const termInfos = index.getDocumentsForTerm(term);
9871
+ terms.push({
9872
+ term,
9873
+ idf: index.getIDF(term),
9874
+ postings: termInfos.map((info) => ({
9875
+ docId: info.docId,
9876
+ termFrequency: info.termFrequency,
9877
+ positions: info.fieldPositions
9878
+ }))
9879
+ });
9880
+ }
9881
+ return terms;
9882
+ }
9883
+ serializeDocLengths(index) {
9884
+ const lengths = {};
9885
+ for (const [docId, length] of index.getDocLengths()) {
9886
+ lengths[docId] = length;
9887
+ }
9888
+ return lengths;
9889
+ }
9890
+ loadIntoIndex(index, data) {
9891
+ const idx = index;
9892
+ idx.totalDocs = data.metadata.totalDocs;
9893
+ idx.avgDocLength = data.metadata.avgDocLength;
9894
+ idx.docLengths = new Map(Object.entries(data.docLengths));
9895
+ for (const { term, idf, postings } of data.terms) {
9896
+ const termInfos = postings.map((p) => ({
9897
+ docId: p.docId,
9898
+ termFrequency: p.termFrequency,
9899
+ fieldPositions: p.positions
9900
+ }));
9901
+ idx.index.set(term, termInfos);
9902
+ idx.idfCache.set(term, idf);
9903
+ for (const info of termInfos) {
9904
+ if (!idx.docTerms.has(info.docId)) {
9905
+ idx.docTerms.set(info.docId, /* @__PURE__ */ new Set());
9906
+ }
9907
+ idx.docTerms.get(info.docId).add(term);
9908
+ }
9909
+ }
9910
+ }
9911
+ };
9912
+
9913
+ // src/fts/FullTextIndex.ts
9914
+ var FullTextIndex = class {
9915
+ /**
9916
+ * Create a new FullTextIndex.
9917
+ *
9918
+ * @param config - Index configuration
9919
+ */
9920
+ constructor(config) {
9921
+ this.fields = config.fields;
9922
+ this.tokenizer = new BM25Tokenizer(config.tokenizer);
9923
+ this.scorer = new BM25Scorer(config.bm25);
9924
+ this.fieldIndexes = /* @__PURE__ */ new Map();
9925
+ this.combinedIndex = new BM25InvertedIndex();
9926
+ this.indexedDocs = /* @__PURE__ */ new Set();
9927
+ this.serializer = new IndexSerializer();
9928
+ this.documentTokensCache = /* @__PURE__ */ new Map();
9929
+ for (const field of this.fields) {
9930
+ this.fieldIndexes.set(field, new BM25InvertedIndex());
9931
+ }
9932
+ }
9933
+ /**
9934
+ * Index a document (add or update).
9935
+ * Called when a document is set in the CRDT map.
9936
+ *
9937
+ * @param docId - Document identifier
9938
+ * @param document - Document data containing fields to index
9939
+ */
9940
+ onSet(docId, document) {
9941
+ if (!document || typeof document !== "object") {
9942
+ this.documentTokensCache.delete(docId);
9943
+ return;
9944
+ }
9945
+ if (this.indexedDocs.has(docId)) {
9946
+ this.removeFromIndexes(docId);
9947
+ }
9948
+ const allTokens = [];
9949
+ for (const field of this.fields) {
9950
+ const value = document[field];
9951
+ if (typeof value !== "string") {
9952
+ continue;
9953
+ }
9954
+ const tokens = this.tokenizer.tokenize(value);
9955
+ if (tokens.length > 0) {
9956
+ const fieldIndex = this.fieldIndexes.get(field);
9957
+ fieldIndex.addDocument(docId, tokens);
9958
+ allTokens.push(...tokens);
9959
+ }
9960
+ }
9961
+ if (allTokens.length > 0) {
9962
+ this.combinedIndex.addDocument(docId, allTokens);
9963
+ this.indexedDocs.add(docId);
9964
+ this.documentTokensCache.set(docId, allTokens);
9965
+ } else {
9966
+ this.documentTokensCache.delete(docId);
9967
+ }
9968
+ }
9969
+ /**
9970
+ * Remove a document from the index.
9971
+ * Called when a document is deleted from the CRDT map.
9972
+ *
9973
+ * @param docId - Document identifier to remove
9974
+ */
9975
+ onRemove(docId) {
9976
+ if (!this.indexedDocs.has(docId)) {
9977
+ return;
9978
+ }
9979
+ this.removeFromIndexes(docId);
9980
+ this.indexedDocs.delete(docId);
9981
+ this.documentTokensCache.delete(docId);
9982
+ }
9983
+ /**
9984
+ * Search the index with a query.
9985
+ *
9986
+ * @param query - Search query text
9987
+ * @param options - Search options (limit, minScore, boost)
9988
+ * @returns Array of search results, sorted by relevance
9989
+ */
9990
+ search(query, options) {
9991
+ const queryTerms = this.tokenizer.tokenize(query);
9992
+ if (queryTerms.length === 0) {
9993
+ return [];
9994
+ }
9995
+ const boost = options?.boost;
9996
+ let results;
9997
+ if (boost && Object.keys(boost).length > 0) {
9998
+ results = this.searchWithBoost(queryTerms, boost);
9999
+ } else {
10000
+ results = this.scorer.score(queryTerms, this.combinedIndex);
10001
+ }
10002
+ if (options?.minScore !== void 0) {
10003
+ results = results.filter((r) => r.score >= options.minScore);
10004
+ }
10005
+ if (options?.limit !== void 0 && options.limit > 0) {
10006
+ results = results.slice(0, options.limit);
10007
+ }
10008
+ return results.map((r) => ({
10009
+ docId: r.docId,
10010
+ score: r.score,
10011
+ matchedTerms: r.matchedTerms,
10012
+ source: "fulltext"
10013
+ }));
10014
+ }
10015
+ /**
10016
+ * Serialize the index state.
10017
+ *
10018
+ * @returns Serialized index data
10019
+ */
10020
+ serialize() {
10021
+ return this.serializer.serialize(this.combinedIndex);
10022
+ }
10023
+ /**
10024
+ * Load index from serialized state.
10025
+ *
10026
+ * @param data - Serialized index data
10027
+ */
10028
+ load(data) {
10029
+ this.combinedIndex = this.serializer.deserialize(data);
10030
+ this.indexedDocs.clear();
10031
+ for (const [docId] of this.combinedIndex.getDocLengths()) {
10032
+ this.indexedDocs.add(docId);
10033
+ }
10034
+ this.fieldIndexes.clear();
10035
+ for (const field of this.fields) {
10036
+ this.fieldIndexes.set(field, new BM25InvertedIndex());
10037
+ }
10038
+ this.documentTokensCache.clear();
10039
+ }
10040
+ /**
10041
+ * Build the index from an array of entries.
10042
+ * Useful for initial bulk loading.
10043
+ *
10044
+ * @param entries - Array of [docId, document] tuples
10045
+ */
10046
+ buildFromEntries(entries) {
10047
+ for (const [docId, document] of entries) {
10048
+ this.onSet(docId, document);
10049
+ }
10050
+ }
10051
+ /**
10052
+ * Clear all data from the index.
10053
+ */
10054
+ clear() {
10055
+ this.combinedIndex.clear();
10056
+ for (const fieldIndex of this.fieldIndexes.values()) {
10057
+ fieldIndex.clear();
10058
+ }
10059
+ this.indexedDocs.clear();
10060
+ this.documentTokensCache.clear();
10061
+ }
10062
+ /**
10063
+ * Get the number of indexed documents.
10064
+ *
10065
+ * @returns Number of documents in the index
10066
+ */
10067
+ getSize() {
10068
+ return this.indexedDocs.size;
10069
+ }
10070
+ /**
10071
+ * Tokenize a query string using the index's tokenizer.
10072
+ * Public method for external use (e.g., SearchCoordinator).
10073
+ *
10074
+ * @param query - Query text to tokenize
10075
+ * @returns Array of tokenized terms
10076
+ */
10077
+ tokenizeQuery(query) {
10078
+ return this.tokenizer.tokenize(query);
10079
+ }
10080
+ /**
10081
+ * Score a single document against query terms.
10082
+ * O(Q × D) complexity where Q = query terms, D = document tokens.
10083
+ *
10084
+ * This method is optimized for checking if a single document
10085
+ * matches a query, avoiding full index scan.
10086
+ *
10087
+ * @param docId - Document ID to score
10088
+ * @param queryTerms - Pre-tokenized query terms
10089
+ * @param document - Optional document data (used if not in cache)
10090
+ * @returns SearchResult with score and matched terms, or null if no match
10091
+ */
10092
+ scoreSingleDocument(docId, queryTerms, document) {
10093
+ if (queryTerms.length === 0) {
10094
+ return null;
10095
+ }
10096
+ let docTokens = this.documentTokensCache.get(docId);
10097
+ if (!docTokens && document) {
10098
+ docTokens = this.tokenizeDocument(document);
10099
+ }
10100
+ if (!docTokens || docTokens.length === 0) {
10101
+ return null;
10102
+ }
10103
+ const docTokenSet = new Set(docTokens);
10104
+ const matchedTerms = queryTerms.filter((term) => docTokenSet.has(term));
10105
+ if (matchedTerms.length === 0) {
10106
+ return null;
10107
+ }
10108
+ const score = this.scorer.scoreSingleDocument(
10109
+ queryTerms,
10110
+ docTokens,
10111
+ this.combinedIndex
10112
+ );
10113
+ if (score <= 0) {
10114
+ return null;
10115
+ }
10116
+ return {
10117
+ docId,
10118
+ score,
10119
+ matchedTerms,
10120
+ source: "fulltext"
10121
+ };
10122
+ }
10123
+ /**
10124
+ * Tokenize all indexed fields of a document.
10125
+ * Internal helper for scoreSingleDocument when document not in cache.
10126
+ *
10127
+ * @param document - Document data
10128
+ * @returns Array of all tokens from indexed fields
10129
+ */
10130
+ tokenizeDocument(document) {
10131
+ const allTokens = [];
10132
+ for (const field of this.fields) {
10133
+ const value = document[field];
10134
+ if (typeof value === "string") {
10135
+ const tokens = this.tokenizer.tokenize(value);
10136
+ allTokens.push(...tokens);
10137
+ }
10138
+ }
10139
+ return allTokens;
10140
+ }
10141
+ /**
10142
+ * Get the index name (for debugging/display).
10143
+ *
10144
+ * @returns Descriptive name including indexed fields
10145
+ */
10146
+ get name() {
10147
+ return `FullTextIndex(${this.fields.join(", ")})`;
10148
+ }
10149
+ /**
10150
+ * Remove document from all indexes (internal).
10151
+ */
10152
+ removeFromIndexes(docId) {
10153
+ this.combinedIndex.removeDocument(docId);
10154
+ for (const fieldIndex of this.fieldIndexes.values()) {
10155
+ fieldIndex.removeDocument(docId);
10156
+ }
10157
+ }
10158
+ /**
10159
+ * Search with field boosting.
10160
+ * Scores are computed per-field and combined with boost weights.
10161
+ */
10162
+ searchWithBoost(queryTerms, boost) {
10163
+ const docScores = /* @__PURE__ */ new Map();
10164
+ for (const field of this.fields) {
10165
+ const fieldIndex = this.fieldIndexes.get(field);
10166
+ const boostWeight = boost[field] ?? 1;
10167
+ const fieldResults = this.scorer.score(queryTerms, fieldIndex);
10168
+ for (const result of fieldResults) {
10169
+ const current = docScores.get(result.docId) || {
10170
+ score: 0,
10171
+ terms: /* @__PURE__ */ new Set()
10172
+ };
10173
+ current.score += result.score * boostWeight;
10174
+ for (const term of result.matchedTerms) {
10175
+ current.terms.add(term);
10176
+ }
10177
+ docScores.set(result.docId, current);
10178
+ }
10179
+ }
10180
+ const results = [];
10181
+ for (const [docId, { score, terms }] of docScores) {
10182
+ results.push({
10183
+ docId,
10184
+ score,
10185
+ matchedTerms: Array.from(terms)
10186
+ });
10187
+ }
10188
+ results.sort((a, b) => b.score - a.score);
10189
+ return results;
10190
+ }
10191
+ };
10192
+
10193
+ // src/IndexedORMap.ts
10194
+ var IndexedORMap = class extends ORMap {
10195
+ constructor(hlc, options = {}) {
10196
+ super(hlc);
10197
+ // Full-Text Search (Phase 11)
10198
+ this.fullTextIndex = null;
10199
+ this.options = options;
10200
+ this.indexRegistry = new IndexRegistry();
10201
+ this.queryOptimizer = new QueryOptimizer({
10202
+ indexRegistry: this.indexRegistry
10203
+ });
10204
+ this.indexRegistry.setFallbackIndex(
10205
+ new FallbackIndex(
10206
+ () => this.getAllCompositeKeys(),
10207
+ (compositeKey) => this.getRecordByCompositeKey(compositeKey),
10208
+ (record, query) => this.matchesIndexQuery(record, query)
10209
+ )
10210
+ );
10211
+ this.queryTracker = new QueryPatternTracker();
10212
+ this.indexAdvisor = new IndexAdvisor(this.queryTracker);
10213
+ if (options.adaptiveIndexing?.autoIndex?.enabled) {
10214
+ this.autoIndexManager = new AutoIndexManager(
10215
+ this.queryTracker,
10216
+ this.indexAdvisor,
10217
+ options.adaptiveIndexing.autoIndex
10218
+ );
10219
+ this.autoIndexManager.setMap(this);
10220
+ } else {
10221
+ this.autoIndexManager = null;
10222
+ }
10223
+ if (options.defaultIndexing && options.defaultIndexing !== "none") {
10224
+ this.defaultIndexingStrategy = new DefaultIndexingStrategy(options.defaultIndexing);
10225
+ } else {
10226
+ this.defaultIndexingStrategy = null;
10227
+ }
10228
+ }
10229
+ // ==================== Index Management ====================
10230
+ /**
10231
+ * Add a hash index on an attribute.
10232
+ *
10233
+ * @param attribute - Attribute to index
10234
+ * @returns Created HashIndex
10235
+ */
10236
+ addHashIndex(attribute) {
10237
+ const index = new HashIndex(attribute);
8675
10238
  this.indexRegistry.addIndex(index);
8676
10239
  this.buildIndexFromExisting(index);
8677
10240
  return index;
@@ -8713,6 +10276,104 @@ var IndexedORMap = class extends ORMap {
8713
10276
  this.indexRegistry.addIndex(index);
8714
10277
  this.buildIndexFromExisting(index);
8715
10278
  }
10279
+ // ==================== Full-Text Search (Phase 11) ====================
10280
+ /**
10281
+ * Enable BM25-based full-text search on specified fields.
10282
+ * This creates a FullTextIndex for relevance-ranked search.
10283
+ *
10284
+ * Note: This is different from addInvertedIndex which provides
10285
+ * boolean matching (contains/containsAll/containsAny). This method
10286
+ * provides BM25 relevance scoring for true full-text search.
10287
+ *
10288
+ * @param config - Full-text index configuration
10289
+ * @returns The created FullTextIndex
10290
+ *
10291
+ * @example
10292
+ * ```typescript
10293
+ * const map = new IndexedORMap(hlc);
10294
+ * map.enableFullTextSearch({
10295
+ * fields: ['title', 'body'],
10296
+ * tokenizer: { minLength: 2 },
10297
+ * bm25: { k1: 1.2, b: 0.75 }
10298
+ * });
10299
+ *
10300
+ * map.add('doc1', { title: 'Hello World', body: 'Test content' });
10301
+ * const results = map.search('hello');
10302
+ * // [{ key: 'doc1', tag: '...', value: {...}, score: 0.5, matchedTerms: ['hello'] }]
10303
+ * ```
10304
+ */
10305
+ enableFullTextSearch(config) {
10306
+ this.fullTextIndex = new FullTextIndex(config);
10307
+ const snapshot = this.getSnapshot();
10308
+ const entries = [];
10309
+ for (const [key, tagMap] of snapshot.items) {
10310
+ for (const [tag, record] of tagMap) {
10311
+ if (!snapshot.tombstones.has(tag)) {
10312
+ const compositeKey = this.createCompositeKey(key, tag);
10313
+ entries.push([compositeKey, record.value]);
10314
+ }
10315
+ }
10316
+ }
10317
+ this.fullTextIndex.buildFromEntries(entries);
10318
+ return this.fullTextIndex;
10319
+ }
10320
+ /**
10321
+ * Check if full-text search is enabled.
10322
+ *
10323
+ * @returns true if full-text search is enabled
10324
+ */
10325
+ isFullTextSearchEnabled() {
10326
+ return this.fullTextIndex !== null;
10327
+ }
10328
+ /**
10329
+ * Get the full-text index (if enabled).
10330
+ *
10331
+ * @returns The FullTextIndex or null
10332
+ */
10333
+ getFullTextIndex() {
10334
+ return this.fullTextIndex;
10335
+ }
10336
+ /**
10337
+ * Perform a BM25-ranked full-text search.
10338
+ * Results are sorted by relevance score (highest first).
10339
+ *
10340
+ * @param query - Search query text
10341
+ * @param options - Search options (limit, minScore, boost)
10342
+ * @returns Array of search results with scores, sorted by relevance
10343
+ *
10344
+ * @throws Error if full-text search is not enabled
10345
+ */
10346
+ search(query, options) {
10347
+ if (!this.fullTextIndex) {
10348
+ throw new Error("Full-text search is not enabled. Call enableFullTextSearch() first.");
10349
+ }
10350
+ const scoredDocs = this.fullTextIndex.search(query, options);
10351
+ const results = [];
10352
+ for (const { docId: compositeKey, score, matchedTerms } of scoredDocs) {
10353
+ const [key, tag] = this.parseCompositeKey(compositeKey);
10354
+ const records = this.getRecords(key);
10355
+ const record = records.find((r) => r.tag === tag);
10356
+ if (record) {
10357
+ results.push({
10358
+ key,
10359
+ tag,
10360
+ value: record.value,
10361
+ score,
10362
+ matchedTerms: matchedTerms ?? []
10363
+ });
10364
+ }
10365
+ }
10366
+ return results;
10367
+ }
10368
+ /**
10369
+ * Disable full-text search and release the index.
10370
+ */
10371
+ disableFullTextSearch() {
10372
+ if (this.fullTextIndex) {
10373
+ this.fullTextIndex.clear();
10374
+ this.fullTextIndex = null;
10375
+ }
10376
+ }
8716
10377
  /**
8717
10378
  * Remove an index.
8718
10379
  *
@@ -8866,6 +10527,9 @@ var IndexedORMap = class extends ORMap {
8866
10527
  const record = super.add(key, value, ttlMs);
8867
10528
  const compositeKey = this.createCompositeKey(key, record.tag);
8868
10529
  this.indexRegistry.onRecordAdded(compositeKey, value);
10530
+ if (this.fullTextIndex) {
10531
+ this.fullTextIndex.onSet(compositeKey, value);
10532
+ }
8869
10533
  return record;
8870
10534
  }
8871
10535
  /**
@@ -8878,6 +10542,9 @@ var IndexedORMap = class extends ORMap {
8878
10542
  for (const record of matchingRecords) {
8879
10543
  const compositeKey = this.createCompositeKey(key, record.tag);
8880
10544
  this.indexRegistry.onRecordRemoved(compositeKey, record.value);
10545
+ if (this.fullTextIndex) {
10546
+ this.fullTextIndex.onRemove(compositeKey);
10547
+ }
8881
10548
  }
8882
10549
  return result;
8883
10550
  }
@@ -8889,6 +10556,9 @@ var IndexedORMap = class extends ORMap {
8889
10556
  if (applied) {
8890
10557
  const compositeKey = this.createCompositeKey(key, record.tag);
8891
10558
  this.indexRegistry.onRecordAdded(compositeKey, record.value);
10559
+ if (this.fullTextIndex) {
10560
+ this.fullTextIndex.onSet(compositeKey, record.value);
10561
+ }
8892
10562
  }
8893
10563
  return applied;
8894
10564
  }
@@ -8911,6 +10581,9 @@ var IndexedORMap = class extends ORMap {
8911
10581
  if (removedValue !== void 0 && removedKey !== void 0) {
8912
10582
  const compositeKey = this.createCompositeKey(removedKey, tag);
8913
10583
  this.indexRegistry.onRecordRemoved(compositeKey, removedValue);
10584
+ if (this.fullTextIndex) {
10585
+ this.fullTextIndex.onRemove(compositeKey);
10586
+ }
8914
10587
  }
8915
10588
  }
8916
10589
  /**
@@ -8919,6 +10592,9 @@ var IndexedORMap = class extends ORMap {
8919
10592
  clear() {
8920
10593
  super.clear();
8921
10594
  this.indexRegistry.clear();
10595
+ if (this.fullTextIndex) {
10596
+ this.fullTextIndex.clear();
10597
+ }
8922
10598
  }
8923
10599
  // ==================== Helper Methods ====================
8924
10600
  /**
@@ -9272,6 +10948,7 @@ var IndexedORMap = class extends ORMap {
9272
10948
  // Annotate the CommonJS export names for ESM import in node:
9273
10949
  0 && (module.exports = {
9274
10950
  AuthMessageSchema,
10951
+ BM25Scorer,
9275
10952
  BatchMessageSchema,
9276
10953
  BuiltInProcessors,
9277
10954
  BuiltInResolvers,
@@ -9295,6 +10972,7 @@ var IndexedORMap = class extends ORMap {
9295
10972
  DEFAULT_RESOLVER_RATE_LIMITS,
9296
10973
  DEFAULT_STOP_WORDS,
9297
10974
  DEFAULT_WRITE_CONCERN_TIMEOUT,
10975
+ ENGLISH_STOPWORDS,
9298
10976
  EntryProcessBatchRequestSchema,
9299
10977
  EntryProcessBatchResponseSchema,
9300
10978
  EntryProcessKeyResultSchema,
@@ -9304,8 +10982,11 @@ var IndexedORMap = class extends ORMap {
9304
10982
  EntryProcessorSchema,
9305
10983
  EventJournalImpl,
9306
10984
  FORBIDDEN_PATTERNS,
10985
+ FTSInvertedIndex,
10986
+ FTSTokenizer,
9307
10987
  FallbackIndex,
9308
10988
  FilteringResultSet,
10989
+ FullTextIndex,
9309
10990
  HLC,
9310
10991
  HashIndex,
9311
10992
  IndexRegistry,
@@ -9369,9 +11050,22 @@ var IndexedORMap = class extends ORMap {
9369
11050
  QuerySubMessageSchema,
9370
11051
  QueryUnsubMessageSchema,
9371
11052
  RESOLVER_FORBIDDEN_PATTERNS,
11053
+ ReciprocalRankFusion,
9372
11054
  RegisterResolverRequestSchema,
9373
11055
  RegisterResolverResponseSchema,
9374
11056
  Ringbuffer,
11057
+ SearchMessageSchema,
11058
+ SearchOptionsSchema,
11059
+ SearchPayloadSchema,
11060
+ SearchRespMessageSchema,
11061
+ SearchRespPayloadSchema,
11062
+ SearchSubMessageSchema,
11063
+ SearchSubPayloadSchema,
11064
+ SearchUnsubMessageSchema,
11065
+ SearchUnsubPayloadSchema,
11066
+ SearchUpdateMessageSchema,
11067
+ SearchUpdatePayloadSchema,
11068
+ SearchUpdateTypeSchema,
9375
11069
  SetResultSet,
9376
11070
  SimpleAttribute,
9377
11071
  SortedMap,
@@ -9417,6 +11111,7 @@ var IndexedORMap = class extends ORMap {
9417
11111
  isUsingNativeHash,
9418
11112
  isWriteConcernAchieved,
9419
11113
  multiAttribute,
11114
+ porterStem,
9420
11115
  resetNativeHash,
9421
11116
  serialize,
9422
11117
  simpleAttribute,