@topgunbuild/core 0.6.0 → 0.8.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
@@ -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,
@@ -131,6 +136,18 @@ __export(index_exports, {
131
136
  RegisterResolverRequestSchema: () => RegisterResolverRequestSchema,
132
137
  RegisterResolverResponseSchema: () => RegisterResolverResponseSchema,
133
138
  Ringbuffer: () => Ringbuffer,
139
+ SearchMessageSchema: () => SearchMessageSchema,
140
+ SearchOptionsSchema: () => SearchOptionsSchema,
141
+ SearchPayloadSchema: () => SearchPayloadSchema,
142
+ SearchRespMessageSchema: () => SearchRespMessageSchema,
143
+ SearchRespPayloadSchema: () => SearchRespPayloadSchema,
144
+ SearchSubMessageSchema: () => SearchSubMessageSchema,
145
+ SearchSubPayloadSchema: () => SearchSubPayloadSchema,
146
+ SearchUnsubMessageSchema: () => SearchUnsubMessageSchema,
147
+ SearchUnsubPayloadSchema: () => SearchUnsubPayloadSchema,
148
+ SearchUpdateMessageSchema: () => SearchUpdateMessageSchema,
149
+ SearchUpdatePayloadSchema: () => SearchUpdatePayloadSchema,
150
+ SearchUpdateTypeSchema: () => SearchUpdateTypeSchema,
134
151
  SetResultSet: () => SetResultSet,
135
152
  SimpleAttribute: () => SimpleAttribute,
136
153
  SortedMap: () => SortedMap,
@@ -176,6 +193,7 @@ __export(index_exports, {
176
193
  isUsingNativeHash: () => isUsingNativeHash,
177
194
  isWriteConcernAchieved: () => isWriteConcernAchieved,
178
195
  multiAttribute: () => multiAttribute,
196
+ porterStem: () => porterStem,
179
197
  resetNativeHash: () => resetNativeHash,
180
198
  serialize: () => serialize,
181
199
  simpleAttribute: () => simpleAttribute,
@@ -2596,6 +2614,66 @@ var JournalReadResponseSchema = import_zod3.z.object({
2596
2614
  events: import_zod3.z.array(JournalEventDataSchema),
2597
2615
  hasMore: import_zod3.z.boolean()
2598
2616
  });
2617
+ var SearchOptionsSchema = import_zod3.z.object({
2618
+ limit: import_zod3.z.number().optional(),
2619
+ minScore: import_zod3.z.number().optional(),
2620
+ boost: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.number()).optional()
2621
+ });
2622
+ var SearchPayloadSchema = import_zod3.z.object({
2623
+ requestId: import_zod3.z.string(),
2624
+ mapName: import_zod3.z.string(),
2625
+ query: import_zod3.z.string(),
2626
+ options: SearchOptionsSchema.optional()
2627
+ });
2628
+ var SearchMessageSchema = import_zod3.z.object({
2629
+ type: import_zod3.z.literal("SEARCH"),
2630
+ payload: SearchPayloadSchema
2631
+ });
2632
+ var SearchRespPayloadSchema = import_zod3.z.object({
2633
+ requestId: import_zod3.z.string(),
2634
+ results: import_zod3.z.array(import_zod3.z.object({
2635
+ key: import_zod3.z.string(),
2636
+ value: import_zod3.z.unknown(),
2637
+ score: import_zod3.z.number(),
2638
+ matchedTerms: import_zod3.z.array(import_zod3.z.string())
2639
+ })),
2640
+ totalCount: import_zod3.z.number(),
2641
+ error: import_zod3.z.string().optional()
2642
+ });
2643
+ var SearchRespMessageSchema = import_zod3.z.object({
2644
+ type: import_zod3.z.literal("SEARCH_RESP"),
2645
+ payload: SearchRespPayloadSchema
2646
+ });
2647
+ var SearchUpdateTypeSchema = import_zod3.z.enum(["ENTER", "UPDATE", "LEAVE"]);
2648
+ var SearchSubPayloadSchema = import_zod3.z.object({
2649
+ subscriptionId: import_zod3.z.string(),
2650
+ mapName: import_zod3.z.string(),
2651
+ query: import_zod3.z.string(),
2652
+ options: SearchOptionsSchema.optional()
2653
+ });
2654
+ var SearchSubMessageSchema = import_zod3.z.object({
2655
+ type: import_zod3.z.literal("SEARCH_SUB"),
2656
+ payload: SearchSubPayloadSchema
2657
+ });
2658
+ var SearchUpdatePayloadSchema = import_zod3.z.object({
2659
+ subscriptionId: import_zod3.z.string(),
2660
+ key: import_zod3.z.string(),
2661
+ value: import_zod3.z.unknown(),
2662
+ score: import_zod3.z.number(),
2663
+ matchedTerms: import_zod3.z.array(import_zod3.z.string()),
2664
+ type: SearchUpdateTypeSchema
2665
+ });
2666
+ var SearchUpdateMessageSchema = import_zod3.z.object({
2667
+ type: import_zod3.z.literal("SEARCH_UPDATE"),
2668
+ payload: SearchUpdatePayloadSchema
2669
+ });
2670
+ var SearchUnsubPayloadSchema = import_zod3.z.object({
2671
+ subscriptionId: import_zod3.z.string()
2672
+ });
2673
+ var SearchUnsubMessageSchema = import_zod3.z.object({
2674
+ type: import_zod3.z.literal("SEARCH_UNSUB"),
2675
+ payload: SearchUnsubPayloadSchema
2676
+ });
2599
2677
  var ConflictResolverSchema = import_zod3.z.object({
2600
2678
  name: import_zod3.z.string().min(1).max(100),
2601
2679
  code: import_zod3.z.string().max(5e4),
@@ -2727,7 +2805,14 @@ var MessageSchema = import_zod3.z.discriminatedUnion("type", [
2727
2805
  UnregisterResolverResponseSchema,
2728
2806
  MergeRejectedMessageSchema,
2729
2807
  ListResolversRequestSchema,
2730
- ListResolversResponseSchema
2808
+ ListResolversResponseSchema,
2809
+ // Phase 11.1: Full-Text Search
2810
+ SearchMessageSchema,
2811
+ SearchRespMessageSchema,
2812
+ // Phase 11.1b: Live Search Subscriptions
2813
+ SearchSubMessageSchema,
2814
+ SearchUpdateMessageSchema,
2815
+ SearchUnsubMessageSchema
2731
2816
  ]);
2732
2817
 
2733
2818
  // src/types/WriteConcern.ts
@@ -8629,10 +8714,1076 @@ var IndexedLWWMap = class extends LWWMap {
8629
8714
  }
8630
8715
  };
8631
8716
 
8717
+ // src/query/tokenization/stopwords.ts
8718
+ var ENGLISH_STOPWORDS = /* @__PURE__ */ new Set([
8719
+ // Articles
8720
+ "a",
8721
+ "an",
8722
+ "the",
8723
+ // Pronouns
8724
+ "i",
8725
+ "me",
8726
+ "my",
8727
+ "myself",
8728
+ "we",
8729
+ "our",
8730
+ "ours",
8731
+ "ourselves",
8732
+ "you",
8733
+ "your",
8734
+ "yours",
8735
+ "yourself",
8736
+ "yourselves",
8737
+ "he",
8738
+ "him",
8739
+ "his",
8740
+ "himself",
8741
+ "she",
8742
+ "her",
8743
+ "hers",
8744
+ "herself",
8745
+ "it",
8746
+ "its",
8747
+ "itself",
8748
+ "they",
8749
+ "them",
8750
+ "their",
8751
+ "theirs",
8752
+ "themselves",
8753
+ "what",
8754
+ "which",
8755
+ "who",
8756
+ "whom",
8757
+ "this",
8758
+ "that",
8759
+ "these",
8760
+ "those",
8761
+ // Auxiliary verbs
8762
+ "am",
8763
+ "is",
8764
+ "are",
8765
+ "was",
8766
+ "were",
8767
+ "be",
8768
+ "been",
8769
+ "being",
8770
+ "have",
8771
+ "has",
8772
+ "had",
8773
+ "having",
8774
+ "do",
8775
+ "does",
8776
+ "did",
8777
+ "doing",
8778
+ "will",
8779
+ "would",
8780
+ "shall",
8781
+ "should",
8782
+ "can",
8783
+ "could",
8784
+ "may",
8785
+ "might",
8786
+ "must",
8787
+ "ought",
8788
+ // Prepositions
8789
+ "about",
8790
+ "above",
8791
+ "across",
8792
+ "after",
8793
+ "against",
8794
+ "along",
8795
+ "among",
8796
+ "around",
8797
+ "at",
8798
+ "before",
8799
+ "behind",
8800
+ "below",
8801
+ "beneath",
8802
+ "beside",
8803
+ "between",
8804
+ "beyond",
8805
+ "by",
8806
+ "down",
8807
+ "during",
8808
+ "except",
8809
+ "for",
8810
+ "from",
8811
+ "in",
8812
+ "inside",
8813
+ "into",
8814
+ "near",
8815
+ "of",
8816
+ "off",
8817
+ "on",
8818
+ "onto",
8819
+ "out",
8820
+ "outside",
8821
+ "over",
8822
+ "past",
8823
+ "since",
8824
+ "through",
8825
+ "throughout",
8826
+ "to",
8827
+ "toward",
8828
+ "towards",
8829
+ "under",
8830
+ "underneath",
8831
+ "until",
8832
+ "up",
8833
+ "upon",
8834
+ "with",
8835
+ "within",
8836
+ "without",
8837
+ // Conjunctions
8838
+ "and",
8839
+ "but",
8840
+ "or",
8841
+ "nor",
8842
+ "so",
8843
+ "yet",
8844
+ "both",
8845
+ "either",
8846
+ "neither",
8847
+ "not",
8848
+ "only",
8849
+ "as",
8850
+ "if",
8851
+ "than",
8852
+ "when",
8853
+ "while",
8854
+ "although",
8855
+ "because",
8856
+ "unless",
8857
+ "whether",
8858
+ // Adverbs
8859
+ "here",
8860
+ "there",
8861
+ "where",
8862
+ "when",
8863
+ "how",
8864
+ "why",
8865
+ "all",
8866
+ "each",
8867
+ "every",
8868
+ "any",
8869
+ "some",
8870
+ "no",
8871
+ "none",
8872
+ "more",
8873
+ "most",
8874
+ "other",
8875
+ "such",
8876
+ "own",
8877
+ "same",
8878
+ "too",
8879
+ "very",
8880
+ "just",
8881
+ "also",
8882
+ "now",
8883
+ "then",
8884
+ "again",
8885
+ "ever",
8886
+ "once",
8887
+ // Misc
8888
+ "few",
8889
+ "many",
8890
+ "much",
8891
+ "several",
8892
+ "s",
8893
+ "t",
8894
+ "d",
8895
+ "ll",
8896
+ "m",
8897
+ "ve",
8898
+ "re"
8899
+ ]);
8900
+
8901
+ // src/query/tokenization/porter-stemmer.ts
8902
+ function porterStem(word) {
8903
+ if (!word || word.length < 3) {
8904
+ return word;
8905
+ }
8906
+ let stem = word;
8907
+ if (stem.endsWith("sses")) {
8908
+ stem = stem.slice(0, -2);
8909
+ } else if (stem.endsWith("ies")) {
8910
+ stem = stem.slice(0, -2);
8911
+ } else if (!stem.endsWith("ss") && stem.endsWith("s")) {
8912
+ stem = stem.slice(0, -1);
8913
+ }
8914
+ const step1bRegex = /^(.+?)(eed|ed|ing)$/;
8915
+ const step1bMatch = stem.match(step1bRegex);
8916
+ if (step1bMatch) {
8917
+ const [, base, suffix] = step1bMatch;
8918
+ if (suffix === "eed") {
8919
+ if (getMeasure(base) > 0) {
8920
+ stem = base + "ee";
8921
+ }
8922
+ } else if (hasVowel(base)) {
8923
+ stem = base;
8924
+ if (stem.endsWith("at") || stem.endsWith("bl") || stem.endsWith("iz")) {
8925
+ stem = stem + "e";
8926
+ } else if (endsWithDoubleConsonant(stem) && !stem.match(/[lsz]$/)) {
8927
+ stem = stem.slice(0, -1);
8928
+ } else if (getMeasure(stem) === 1 && endsWithCVC(stem)) {
8929
+ stem = stem + "e";
8930
+ }
8931
+ }
8932
+ }
8933
+ if (stem.endsWith("y") && hasVowel(stem.slice(0, -1))) {
8934
+ stem = stem.slice(0, -1) + "i";
8935
+ }
8936
+ const step2Suffixes = [
8937
+ [/ational$/, "ate", 0],
8938
+ [/tional$/, "tion", 0],
8939
+ [/enci$/, "ence", 0],
8940
+ [/anci$/, "ance", 0],
8941
+ [/izer$/, "ize", 0],
8942
+ [/abli$/, "able", 0],
8943
+ [/alli$/, "al", 0],
8944
+ [/entli$/, "ent", 0],
8945
+ [/eli$/, "e", 0],
8946
+ [/ousli$/, "ous", 0],
8947
+ [/ization$/, "ize", 0],
8948
+ [/ation$/, "ate", 0],
8949
+ [/ator$/, "ate", 0],
8950
+ [/alism$/, "al", 0],
8951
+ [/iveness$/, "ive", 0],
8952
+ [/fulness$/, "ful", 0],
8953
+ [/ousness$/, "ous", 0],
8954
+ [/aliti$/, "al", 0],
8955
+ [/iviti$/, "ive", 0],
8956
+ [/biliti$/, "ble", 0]
8957
+ ];
8958
+ for (const [regex, replacement, minMeasure] of step2Suffixes) {
8959
+ if (regex.test(stem)) {
8960
+ const base = stem.replace(regex, "");
8961
+ if (getMeasure(base) > minMeasure) {
8962
+ stem = base + replacement;
8963
+ break;
8964
+ }
8965
+ }
8966
+ }
8967
+ const step3Suffixes = [
8968
+ [/icate$/, "ic", 0],
8969
+ [/ative$/, "", 0],
8970
+ [/alize$/, "al", 0],
8971
+ [/iciti$/, "ic", 0],
8972
+ [/ical$/, "ic", 0],
8973
+ [/ful$/, "", 0],
8974
+ [/ness$/, "", 0]
8975
+ ];
8976
+ for (const [regex, replacement, minMeasure] of step3Suffixes) {
8977
+ if (regex.test(stem)) {
8978
+ const base = stem.replace(regex, "");
8979
+ if (getMeasure(base) > minMeasure) {
8980
+ stem = base + replacement;
8981
+ break;
8982
+ }
8983
+ }
8984
+ }
8985
+ const step4Suffixes = [
8986
+ [/al$/, 1],
8987
+ [/ance$/, 1],
8988
+ [/ence$/, 1],
8989
+ [/er$/, 1],
8990
+ [/ic$/, 1],
8991
+ [/able$/, 1],
8992
+ [/ible$/, 1],
8993
+ [/ant$/, 1],
8994
+ [/ement$/, 1],
8995
+ [/ment$/, 1],
8996
+ [/ent$/, 1],
8997
+ [/ion$/, 1],
8998
+ [/ou$/, 1],
8999
+ [/ism$/, 1],
9000
+ [/ate$/, 1],
9001
+ [/iti$/, 1],
9002
+ [/ous$/, 1],
9003
+ [/ive$/, 1],
9004
+ [/ize$/, 1]
9005
+ ];
9006
+ for (const [regex, minMeasure] of step4Suffixes) {
9007
+ if (regex.test(stem)) {
9008
+ const base = stem.replace(regex, "");
9009
+ if (getMeasure(base) > minMeasure) {
9010
+ if (regex.source === "ion$") {
9011
+ if (base.match(/[st]$/)) {
9012
+ stem = base;
9013
+ }
9014
+ } else {
9015
+ stem = base;
9016
+ }
9017
+ break;
9018
+ }
9019
+ }
9020
+ }
9021
+ if (stem.endsWith("e")) {
9022
+ const base = stem.slice(0, -1);
9023
+ const measure = getMeasure(base);
9024
+ if (measure > 1 || measure === 1 && !endsWithCVC(base)) {
9025
+ stem = base;
9026
+ }
9027
+ }
9028
+ if (getMeasure(stem) > 1 && endsWithDoubleConsonant(stem) && stem.endsWith("l")) {
9029
+ stem = stem.slice(0, -1);
9030
+ }
9031
+ return stem;
9032
+ }
9033
+ function isVowel(char, prevChar) {
9034
+ if ("aeiou".includes(char)) {
9035
+ return true;
9036
+ }
9037
+ if (char === "y" && prevChar && !"aeiou".includes(prevChar)) {
9038
+ return true;
9039
+ }
9040
+ return false;
9041
+ }
9042
+ function hasVowel(str) {
9043
+ for (let i = 0; i < str.length; i++) {
9044
+ if (isVowel(str[i], i > 0 ? str[i - 1] : void 0)) {
9045
+ return true;
9046
+ }
9047
+ }
9048
+ return false;
9049
+ }
9050
+ function getMeasure(str) {
9051
+ let pattern = "";
9052
+ for (let i = 0; i < str.length; i++) {
9053
+ pattern += isVowel(str[i], i > 0 ? str[i - 1] : void 0) ? "v" : "c";
9054
+ }
9055
+ const matches = pattern.match(/vc/g);
9056
+ return matches ? matches.length : 0;
9057
+ }
9058
+ function endsWithDoubleConsonant(str) {
9059
+ if (str.length < 2) return false;
9060
+ const last = str[str.length - 1];
9061
+ const secondLast = str[str.length - 2];
9062
+ return last === secondLast && !"aeiou".includes(last);
9063
+ }
9064
+ function endsWithCVC(str) {
9065
+ if (str.length < 3) return false;
9066
+ const last3 = str.slice(-3);
9067
+ const c1 = !"aeiou".includes(last3[0]);
9068
+ const v = isVowel(last3[1], last3[0]);
9069
+ const c2 = !"aeiou".includes(last3[2]) && !"wxy".includes(last3[2]);
9070
+ return c1 && v && c2;
9071
+ }
9072
+
9073
+ // src/fts/Tokenizer.ts
9074
+ var BM25Tokenizer = class {
9075
+ /**
9076
+ * Create a new BM25Tokenizer.
9077
+ *
9078
+ * @param options - Configuration options
9079
+ */
9080
+ constructor(options) {
9081
+ this.options = {
9082
+ lowercase: true,
9083
+ stopwords: ENGLISH_STOPWORDS,
9084
+ stemmer: porterStem,
9085
+ minLength: 2,
9086
+ maxLength: 40,
9087
+ ...options
9088
+ };
9089
+ }
9090
+ /**
9091
+ * Tokenize text into an array of normalized tokens.
9092
+ *
9093
+ * @param text - Text to tokenize
9094
+ * @returns Array of tokens
9095
+ */
9096
+ tokenize(text) {
9097
+ if (!text || typeof text !== "string") {
9098
+ return [];
9099
+ }
9100
+ let processed = this.options.lowercase ? text.toLowerCase() : text;
9101
+ const words = processed.split(/[^\p{L}\p{N}]+/u).filter((w) => w.length > 0);
9102
+ const tokens = [];
9103
+ for (const word of words) {
9104
+ if (word.length < this.options.minLength) {
9105
+ continue;
9106
+ }
9107
+ if (this.options.stopwords.has(word)) {
9108
+ continue;
9109
+ }
9110
+ const stemmed = this.options.stemmer(word);
9111
+ if (stemmed.length < this.options.minLength) {
9112
+ continue;
9113
+ }
9114
+ if (stemmed.length > this.options.maxLength) {
9115
+ continue;
9116
+ }
9117
+ tokens.push(stemmed);
9118
+ }
9119
+ return tokens;
9120
+ }
9121
+ };
9122
+
9123
+ // src/fts/BM25InvertedIndex.ts
9124
+ var BM25InvertedIndex = class {
9125
+ constructor() {
9126
+ this.index = /* @__PURE__ */ new Map();
9127
+ this.docLengths = /* @__PURE__ */ new Map();
9128
+ this.docTerms = /* @__PURE__ */ new Map();
9129
+ this.idfCache = /* @__PURE__ */ new Map();
9130
+ this.totalDocs = 0;
9131
+ this.avgDocLength = 0;
9132
+ }
9133
+ /**
9134
+ * Add a document to the index.
9135
+ *
9136
+ * @param docId - Unique document identifier
9137
+ * @param tokens - Array of tokens (already tokenized/stemmed)
9138
+ */
9139
+ addDocument(docId, tokens) {
9140
+ const termFreqs = /* @__PURE__ */ new Map();
9141
+ const uniqueTerms = /* @__PURE__ */ new Set();
9142
+ for (const token of tokens) {
9143
+ termFreqs.set(token, (termFreqs.get(token) || 0) + 1);
9144
+ uniqueTerms.add(token);
9145
+ }
9146
+ for (const [term, freq] of termFreqs) {
9147
+ if (!this.index.has(term)) {
9148
+ this.index.set(term, []);
9149
+ }
9150
+ this.index.get(term).push({
9151
+ docId,
9152
+ termFrequency: freq
9153
+ });
9154
+ }
9155
+ this.docLengths.set(docId, tokens.length);
9156
+ this.docTerms.set(docId, uniqueTerms);
9157
+ this.totalDocs++;
9158
+ this.updateAvgDocLength();
9159
+ this.idfCache.clear();
9160
+ }
9161
+ /**
9162
+ * Remove a document from the index.
9163
+ *
9164
+ * @param docId - Document identifier to remove
9165
+ */
9166
+ removeDocument(docId) {
9167
+ const terms = this.docTerms.get(docId);
9168
+ if (!terms) {
9169
+ return;
9170
+ }
9171
+ for (const term of terms) {
9172
+ const termInfos = this.index.get(term);
9173
+ if (termInfos) {
9174
+ const filtered = termInfos.filter((info) => info.docId !== docId);
9175
+ if (filtered.length === 0) {
9176
+ this.index.delete(term);
9177
+ } else {
9178
+ this.index.set(term, filtered);
9179
+ }
9180
+ }
9181
+ }
9182
+ this.docLengths.delete(docId);
9183
+ this.docTerms.delete(docId);
9184
+ this.totalDocs--;
9185
+ this.updateAvgDocLength();
9186
+ this.idfCache.clear();
9187
+ }
9188
+ /**
9189
+ * Get all documents containing a term.
9190
+ *
9191
+ * @param term - Term to look up
9192
+ * @returns Array of TermInfo objects
9193
+ */
9194
+ getDocumentsForTerm(term) {
9195
+ return this.index.get(term) || [];
9196
+ }
9197
+ /**
9198
+ * Calculate IDF (Inverse Document Frequency) for a term.
9199
+ *
9200
+ * Uses BM25 IDF formula:
9201
+ * IDF = log((N - df + 0.5) / (df + 0.5) + 1)
9202
+ *
9203
+ * Where:
9204
+ * - N = total documents
9205
+ * - df = document frequency (docs containing term)
9206
+ *
9207
+ * @param term - Term to calculate IDF for
9208
+ * @returns IDF value (0 if term doesn't exist)
9209
+ */
9210
+ getIDF(term) {
9211
+ if (this.idfCache.has(term)) {
9212
+ return this.idfCache.get(term);
9213
+ }
9214
+ const termInfos = this.index.get(term);
9215
+ if (!termInfos || termInfos.length === 0) {
9216
+ return 0;
9217
+ }
9218
+ const docFreq = termInfos.length;
9219
+ const idf = Math.log((this.totalDocs - docFreq + 0.5) / (docFreq + 0.5) + 1);
9220
+ this.idfCache.set(term, idf);
9221
+ return idf;
9222
+ }
9223
+ /**
9224
+ * Get the length of a document (number of tokens).
9225
+ *
9226
+ * @param docId - Document identifier
9227
+ * @returns Document length (0 if not found)
9228
+ */
9229
+ getDocLength(docId) {
9230
+ return this.docLengths.get(docId) || 0;
9231
+ }
9232
+ /**
9233
+ * Get the average document length.
9234
+ *
9235
+ * @returns Average length across all documents
9236
+ */
9237
+ getAvgDocLength() {
9238
+ return this.avgDocLength;
9239
+ }
9240
+ /**
9241
+ * Get the total number of documents in the index.
9242
+ *
9243
+ * @returns Total document count
9244
+ */
9245
+ getTotalDocs() {
9246
+ return this.totalDocs;
9247
+ }
9248
+ /**
9249
+ * Get iterator for document lengths (useful for serialization).
9250
+ *
9251
+ * @returns Iterator of [docId, length] pairs
9252
+ */
9253
+ getDocLengths() {
9254
+ return this.docLengths.entries();
9255
+ }
9256
+ /**
9257
+ * Get the number of documents in the index (alias for getTotalDocs).
9258
+ *
9259
+ * @returns Number of indexed documents
9260
+ */
9261
+ getSize() {
9262
+ return this.totalDocs;
9263
+ }
9264
+ /**
9265
+ * Clear all data from the index.
9266
+ */
9267
+ clear() {
9268
+ this.index.clear();
9269
+ this.docLengths.clear();
9270
+ this.docTerms.clear();
9271
+ this.idfCache.clear();
9272
+ this.totalDocs = 0;
9273
+ this.avgDocLength = 0;
9274
+ }
9275
+ /**
9276
+ * Check if a document exists in the index.
9277
+ *
9278
+ * @param docId - Document identifier
9279
+ * @returns True if document exists
9280
+ */
9281
+ hasDocument(docId) {
9282
+ return this.docTerms.has(docId);
9283
+ }
9284
+ /**
9285
+ * Get all unique terms in the index.
9286
+ *
9287
+ * @returns Iterator of all terms
9288
+ */
9289
+ getTerms() {
9290
+ return this.index.keys();
9291
+ }
9292
+ /**
9293
+ * Get the number of unique terms in the index.
9294
+ *
9295
+ * @returns Number of unique terms
9296
+ */
9297
+ getTermCount() {
9298
+ return this.index.size;
9299
+ }
9300
+ /**
9301
+ * Update the average document length after add/remove.
9302
+ */
9303
+ updateAvgDocLength() {
9304
+ if (this.totalDocs === 0) {
9305
+ this.avgDocLength = 0;
9306
+ return;
9307
+ }
9308
+ let sum = 0;
9309
+ for (const length of this.docLengths.values()) {
9310
+ sum += length;
9311
+ }
9312
+ this.avgDocLength = sum / this.totalDocs;
9313
+ }
9314
+ };
9315
+
9316
+ // src/fts/BM25Scorer.ts
9317
+ var BM25Scorer = class {
9318
+ /**
9319
+ * Create a new BM25 scorer.
9320
+ *
9321
+ * @param options - BM25 configuration options
9322
+ */
9323
+ constructor(options) {
9324
+ this.k1 = options?.k1 ?? 1.2;
9325
+ this.b = options?.b ?? 0.75;
9326
+ }
9327
+ /**
9328
+ * Score documents against a query.
9329
+ *
9330
+ * @param queryTerms - Array of query terms (already tokenized/stemmed)
9331
+ * @param index - The inverted index to search
9332
+ * @returns Array of scored documents, sorted by relevance (descending)
9333
+ */
9334
+ score(queryTerms, index) {
9335
+ if (queryTerms.length === 0 || index.getTotalDocs() === 0) {
9336
+ return [];
9337
+ }
9338
+ const avgDocLength = index.getAvgDocLength();
9339
+ const docScores = /* @__PURE__ */ new Map();
9340
+ for (const term of queryTerms) {
9341
+ const idf = index.getIDF(term);
9342
+ if (idf === 0) {
9343
+ continue;
9344
+ }
9345
+ const termInfos = index.getDocumentsForTerm(term);
9346
+ for (const { docId, termFrequency } of termInfos) {
9347
+ const docLength = index.getDocLength(docId);
9348
+ const numerator = termFrequency * (this.k1 + 1);
9349
+ const denominator = termFrequency + this.k1 * (1 - this.b + this.b * (docLength / avgDocLength));
9350
+ const termScore = idf * (numerator / denominator);
9351
+ const current = docScores.get(docId) || { score: 0, terms: /* @__PURE__ */ new Set() };
9352
+ current.score += termScore;
9353
+ current.terms.add(term);
9354
+ docScores.set(docId, current);
9355
+ }
9356
+ }
9357
+ const results = [];
9358
+ for (const [docId, { score, terms }] of docScores) {
9359
+ results.push({
9360
+ docId,
9361
+ score,
9362
+ matchedTerms: Array.from(terms)
9363
+ });
9364
+ }
9365
+ results.sort((a, b) => b.score - a.score);
9366
+ return results;
9367
+ }
9368
+ /**
9369
+ * Score a single document against query terms.
9370
+ * Uses pre-computed IDF from index but calculates TF locally.
9371
+ *
9372
+ * Complexity: O(Q × D) where Q = query terms, D = document tokens
9373
+ *
9374
+ * @param queryTerms - Tokenized query terms
9375
+ * @param docTokens - Tokenized document terms
9376
+ * @param index - Inverted index for IDF and avgDocLength
9377
+ * @returns BM25 score (0 if no matching terms)
9378
+ */
9379
+ scoreSingleDocument(queryTerms, docTokens, index) {
9380
+ if (queryTerms.length === 0 || docTokens.length === 0) {
9381
+ return 0;
9382
+ }
9383
+ const avgDocLength = index.getAvgDocLength();
9384
+ const docLength = docTokens.length;
9385
+ if (avgDocLength === 0) {
9386
+ return 0;
9387
+ }
9388
+ const termFreqs = /* @__PURE__ */ new Map();
9389
+ for (const token of docTokens) {
9390
+ termFreqs.set(token, (termFreqs.get(token) || 0) + 1);
9391
+ }
9392
+ let score = 0;
9393
+ for (const term of queryTerms) {
9394
+ const tf = termFreqs.get(term) || 0;
9395
+ if (tf === 0) {
9396
+ continue;
9397
+ }
9398
+ const idf = index.getIDF(term);
9399
+ if (idf <= 0) {
9400
+ continue;
9401
+ }
9402
+ const numerator = tf * (this.k1 + 1);
9403
+ const denominator = tf + this.k1 * (1 - this.b + this.b * (docLength / avgDocLength));
9404
+ const termScore = idf * (numerator / denominator);
9405
+ score += termScore;
9406
+ }
9407
+ return score;
9408
+ }
9409
+ /**
9410
+ * Get the k1 parameter value.
9411
+ */
9412
+ getK1() {
9413
+ return this.k1;
9414
+ }
9415
+ /**
9416
+ * Get the b parameter value.
9417
+ */
9418
+ getB() {
9419
+ return this.b;
9420
+ }
9421
+ };
9422
+
9423
+ // src/fts/IndexSerializer.ts
9424
+ var IndexSerializer = class {
9425
+ /**
9426
+ * Serialize inverted index to a JSON-serializable object.
9427
+ * Note: In a real app, you might want to encoding this to binary (msgpack) later.
9428
+ */
9429
+ serialize(index) {
9430
+ const data = {
9431
+ version: 1,
9432
+ metadata: {
9433
+ totalDocs: index.getTotalDocs(),
9434
+ avgDocLength: index.getAvgDocLength(),
9435
+ createdAt: Date.now(),
9436
+ lastModified: Date.now()
9437
+ },
9438
+ terms: this.serializeTerms(index),
9439
+ docLengths: this.serializeDocLengths(index)
9440
+ };
9441
+ return data;
9442
+ }
9443
+ /**
9444
+ * Deserialize from object into a new BM25InvertedIndex.
9445
+ */
9446
+ deserialize(data) {
9447
+ if (data.version !== 1) {
9448
+ throw new Error(`Unsupported index version: ${data.version}`);
9449
+ }
9450
+ const index = new BM25InvertedIndex();
9451
+ this.loadIntoIndex(index, data);
9452
+ return index;
9453
+ }
9454
+ serializeTerms(index) {
9455
+ const terms = [];
9456
+ const indexMap = index.index;
9457
+ for (const term of index.getTerms()) {
9458
+ const termInfos = index.getDocumentsForTerm(term);
9459
+ terms.push({
9460
+ term,
9461
+ idf: index.getIDF(term),
9462
+ postings: termInfos.map((info) => ({
9463
+ docId: info.docId,
9464
+ termFrequency: info.termFrequency,
9465
+ positions: info.fieldPositions
9466
+ }))
9467
+ });
9468
+ }
9469
+ return terms;
9470
+ }
9471
+ serializeDocLengths(index) {
9472
+ const lengths = {};
9473
+ for (const [docId, length] of index.getDocLengths()) {
9474
+ lengths[docId] = length;
9475
+ }
9476
+ return lengths;
9477
+ }
9478
+ loadIntoIndex(index, data) {
9479
+ const idx = index;
9480
+ idx.totalDocs = data.metadata.totalDocs;
9481
+ idx.avgDocLength = data.metadata.avgDocLength;
9482
+ idx.docLengths = new Map(Object.entries(data.docLengths));
9483
+ for (const { term, idf, postings } of data.terms) {
9484
+ const termInfos = postings.map((p) => ({
9485
+ docId: p.docId,
9486
+ termFrequency: p.termFrequency,
9487
+ fieldPositions: p.positions
9488
+ }));
9489
+ idx.index.set(term, termInfos);
9490
+ idx.idfCache.set(term, idf);
9491
+ for (const info of termInfos) {
9492
+ if (!idx.docTerms.has(info.docId)) {
9493
+ idx.docTerms.set(info.docId, /* @__PURE__ */ new Set());
9494
+ }
9495
+ idx.docTerms.get(info.docId).add(term);
9496
+ }
9497
+ }
9498
+ }
9499
+ };
9500
+
9501
+ // src/fts/FullTextIndex.ts
9502
+ var FullTextIndex = class {
9503
+ /**
9504
+ * Create a new FullTextIndex.
9505
+ *
9506
+ * @param config - Index configuration
9507
+ */
9508
+ constructor(config) {
9509
+ this.fields = config.fields;
9510
+ this.tokenizer = new BM25Tokenizer(config.tokenizer);
9511
+ this.scorer = new BM25Scorer(config.bm25);
9512
+ this.fieldIndexes = /* @__PURE__ */ new Map();
9513
+ this.combinedIndex = new BM25InvertedIndex();
9514
+ this.indexedDocs = /* @__PURE__ */ new Set();
9515
+ this.serializer = new IndexSerializer();
9516
+ this.documentTokensCache = /* @__PURE__ */ new Map();
9517
+ for (const field of this.fields) {
9518
+ this.fieldIndexes.set(field, new BM25InvertedIndex());
9519
+ }
9520
+ }
9521
+ /**
9522
+ * Index a document (add or update).
9523
+ * Called when a document is set in the CRDT map.
9524
+ *
9525
+ * @param docId - Document identifier
9526
+ * @param document - Document data containing fields to index
9527
+ */
9528
+ onSet(docId, document) {
9529
+ if (!document || typeof document !== "object") {
9530
+ this.documentTokensCache.delete(docId);
9531
+ return;
9532
+ }
9533
+ if (this.indexedDocs.has(docId)) {
9534
+ this.removeFromIndexes(docId);
9535
+ }
9536
+ const allTokens = [];
9537
+ for (const field of this.fields) {
9538
+ const value = document[field];
9539
+ if (typeof value !== "string") {
9540
+ continue;
9541
+ }
9542
+ const tokens = this.tokenizer.tokenize(value);
9543
+ if (tokens.length > 0) {
9544
+ const fieldIndex = this.fieldIndexes.get(field);
9545
+ fieldIndex.addDocument(docId, tokens);
9546
+ allTokens.push(...tokens);
9547
+ }
9548
+ }
9549
+ if (allTokens.length > 0) {
9550
+ this.combinedIndex.addDocument(docId, allTokens);
9551
+ this.indexedDocs.add(docId);
9552
+ this.documentTokensCache.set(docId, allTokens);
9553
+ } else {
9554
+ this.documentTokensCache.delete(docId);
9555
+ }
9556
+ }
9557
+ /**
9558
+ * Remove a document from the index.
9559
+ * Called when a document is deleted from the CRDT map.
9560
+ *
9561
+ * @param docId - Document identifier to remove
9562
+ */
9563
+ onRemove(docId) {
9564
+ if (!this.indexedDocs.has(docId)) {
9565
+ return;
9566
+ }
9567
+ this.removeFromIndexes(docId);
9568
+ this.indexedDocs.delete(docId);
9569
+ this.documentTokensCache.delete(docId);
9570
+ }
9571
+ /**
9572
+ * Search the index with a query.
9573
+ *
9574
+ * @param query - Search query text
9575
+ * @param options - Search options (limit, minScore, boost)
9576
+ * @returns Array of search results, sorted by relevance
9577
+ */
9578
+ search(query, options) {
9579
+ const queryTerms = this.tokenizer.tokenize(query);
9580
+ if (queryTerms.length === 0) {
9581
+ return [];
9582
+ }
9583
+ const boost = options?.boost;
9584
+ let results;
9585
+ if (boost && Object.keys(boost).length > 0) {
9586
+ results = this.searchWithBoost(queryTerms, boost);
9587
+ } else {
9588
+ results = this.scorer.score(queryTerms, this.combinedIndex);
9589
+ }
9590
+ if (options?.minScore !== void 0) {
9591
+ results = results.filter((r) => r.score >= options.minScore);
9592
+ }
9593
+ if (options?.limit !== void 0 && options.limit > 0) {
9594
+ results = results.slice(0, options.limit);
9595
+ }
9596
+ return results.map((r) => ({
9597
+ docId: r.docId,
9598
+ score: r.score,
9599
+ matchedTerms: r.matchedTerms,
9600
+ source: "fulltext"
9601
+ }));
9602
+ }
9603
+ /**
9604
+ * Serialize the index state.
9605
+ *
9606
+ * @returns Serialized index data
9607
+ */
9608
+ serialize() {
9609
+ return this.serializer.serialize(this.combinedIndex);
9610
+ }
9611
+ /**
9612
+ * Load index from serialized state.
9613
+ *
9614
+ * @param data - Serialized index data
9615
+ */
9616
+ load(data) {
9617
+ this.combinedIndex = this.serializer.deserialize(data);
9618
+ this.indexedDocs.clear();
9619
+ for (const [docId] of this.combinedIndex.getDocLengths()) {
9620
+ this.indexedDocs.add(docId);
9621
+ }
9622
+ this.fieldIndexes.clear();
9623
+ for (const field of this.fields) {
9624
+ this.fieldIndexes.set(field, new BM25InvertedIndex());
9625
+ }
9626
+ this.documentTokensCache.clear();
9627
+ }
9628
+ /**
9629
+ * Build the index from an array of entries.
9630
+ * Useful for initial bulk loading.
9631
+ *
9632
+ * @param entries - Array of [docId, document] tuples
9633
+ */
9634
+ buildFromEntries(entries) {
9635
+ for (const [docId, document] of entries) {
9636
+ this.onSet(docId, document);
9637
+ }
9638
+ }
9639
+ /**
9640
+ * Clear all data from the index.
9641
+ */
9642
+ clear() {
9643
+ this.combinedIndex.clear();
9644
+ for (const fieldIndex of this.fieldIndexes.values()) {
9645
+ fieldIndex.clear();
9646
+ }
9647
+ this.indexedDocs.clear();
9648
+ this.documentTokensCache.clear();
9649
+ }
9650
+ /**
9651
+ * Get the number of indexed documents.
9652
+ *
9653
+ * @returns Number of documents in the index
9654
+ */
9655
+ getSize() {
9656
+ return this.indexedDocs.size;
9657
+ }
9658
+ /**
9659
+ * Tokenize a query string using the index's tokenizer.
9660
+ * Public method for external use (e.g., SearchCoordinator).
9661
+ *
9662
+ * @param query - Query text to tokenize
9663
+ * @returns Array of tokenized terms
9664
+ */
9665
+ tokenizeQuery(query) {
9666
+ return this.tokenizer.tokenize(query);
9667
+ }
9668
+ /**
9669
+ * Score a single document against query terms.
9670
+ * O(Q × D) complexity where Q = query terms, D = document tokens.
9671
+ *
9672
+ * This method is optimized for checking if a single document
9673
+ * matches a query, avoiding full index scan.
9674
+ *
9675
+ * @param docId - Document ID to score
9676
+ * @param queryTerms - Pre-tokenized query terms
9677
+ * @param document - Optional document data (used if not in cache)
9678
+ * @returns SearchResult with score and matched terms, or null if no match
9679
+ */
9680
+ scoreSingleDocument(docId, queryTerms, document) {
9681
+ if (queryTerms.length === 0) {
9682
+ return null;
9683
+ }
9684
+ let docTokens = this.documentTokensCache.get(docId);
9685
+ if (!docTokens && document) {
9686
+ docTokens = this.tokenizeDocument(document);
9687
+ }
9688
+ if (!docTokens || docTokens.length === 0) {
9689
+ return null;
9690
+ }
9691
+ const docTokenSet = new Set(docTokens);
9692
+ const matchedTerms = queryTerms.filter((term) => docTokenSet.has(term));
9693
+ if (matchedTerms.length === 0) {
9694
+ return null;
9695
+ }
9696
+ const score = this.scorer.scoreSingleDocument(
9697
+ queryTerms,
9698
+ docTokens,
9699
+ this.combinedIndex
9700
+ );
9701
+ if (score <= 0) {
9702
+ return null;
9703
+ }
9704
+ return {
9705
+ docId,
9706
+ score,
9707
+ matchedTerms,
9708
+ source: "fulltext"
9709
+ };
9710
+ }
9711
+ /**
9712
+ * Tokenize all indexed fields of a document.
9713
+ * Internal helper for scoreSingleDocument when document not in cache.
9714
+ *
9715
+ * @param document - Document data
9716
+ * @returns Array of all tokens from indexed fields
9717
+ */
9718
+ tokenizeDocument(document) {
9719
+ const allTokens = [];
9720
+ for (const field of this.fields) {
9721
+ const value = document[field];
9722
+ if (typeof value === "string") {
9723
+ const tokens = this.tokenizer.tokenize(value);
9724
+ allTokens.push(...tokens);
9725
+ }
9726
+ }
9727
+ return allTokens;
9728
+ }
9729
+ /**
9730
+ * Get the index name (for debugging/display).
9731
+ *
9732
+ * @returns Descriptive name including indexed fields
9733
+ */
9734
+ get name() {
9735
+ return `FullTextIndex(${this.fields.join(", ")})`;
9736
+ }
9737
+ /**
9738
+ * Remove document from all indexes (internal).
9739
+ */
9740
+ removeFromIndexes(docId) {
9741
+ this.combinedIndex.removeDocument(docId);
9742
+ for (const fieldIndex of this.fieldIndexes.values()) {
9743
+ fieldIndex.removeDocument(docId);
9744
+ }
9745
+ }
9746
+ /**
9747
+ * Search with field boosting.
9748
+ * Scores are computed per-field and combined with boost weights.
9749
+ */
9750
+ searchWithBoost(queryTerms, boost) {
9751
+ const docScores = /* @__PURE__ */ new Map();
9752
+ for (const field of this.fields) {
9753
+ const fieldIndex = this.fieldIndexes.get(field);
9754
+ const boostWeight = boost[field] ?? 1;
9755
+ const fieldResults = this.scorer.score(queryTerms, fieldIndex);
9756
+ for (const result of fieldResults) {
9757
+ const current = docScores.get(result.docId) || {
9758
+ score: 0,
9759
+ terms: /* @__PURE__ */ new Set()
9760
+ };
9761
+ current.score += result.score * boostWeight;
9762
+ for (const term of result.matchedTerms) {
9763
+ current.terms.add(term);
9764
+ }
9765
+ docScores.set(result.docId, current);
9766
+ }
9767
+ }
9768
+ const results = [];
9769
+ for (const [docId, { score, terms }] of docScores) {
9770
+ results.push({
9771
+ docId,
9772
+ score,
9773
+ matchedTerms: Array.from(terms)
9774
+ });
9775
+ }
9776
+ results.sort((a, b) => b.score - a.score);
9777
+ return results;
9778
+ }
9779
+ };
9780
+
8632
9781
  // src/IndexedORMap.ts
8633
9782
  var IndexedORMap = class extends ORMap {
8634
9783
  constructor(hlc, options = {}) {
8635
9784
  super(hlc);
9785
+ // Full-Text Search (Phase 11)
9786
+ this.fullTextIndex = null;
8636
9787
  this.options = options;
8637
9788
  this.indexRegistry = new IndexRegistry();
8638
9789
  this.queryOptimizer = new QueryOptimizer({
@@ -8713,6 +9864,104 @@ var IndexedORMap = class extends ORMap {
8713
9864
  this.indexRegistry.addIndex(index);
8714
9865
  this.buildIndexFromExisting(index);
8715
9866
  }
9867
+ // ==================== Full-Text Search (Phase 11) ====================
9868
+ /**
9869
+ * Enable BM25-based full-text search on specified fields.
9870
+ * This creates a FullTextIndex for relevance-ranked search.
9871
+ *
9872
+ * Note: This is different from addInvertedIndex which provides
9873
+ * boolean matching (contains/containsAll/containsAny). This method
9874
+ * provides BM25 relevance scoring for true full-text search.
9875
+ *
9876
+ * @param config - Full-text index configuration
9877
+ * @returns The created FullTextIndex
9878
+ *
9879
+ * @example
9880
+ * ```typescript
9881
+ * const map = new IndexedORMap(hlc);
9882
+ * map.enableFullTextSearch({
9883
+ * fields: ['title', 'body'],
9884
+ * tokenizer: { minLength: 2 },
9885
+ * bm25: { k1: 1.2, b: 0.75 }
9886
+ * });
9887
+ *
9888
+ * map.add('doc1', { title: 'Hello World', body: 'Test content' });
9889
+ * const results = map.search('hello');
9890
+ * // [{ key: 'doc1', tag: '...', value: {...}, score: 0.5, matchedTerms: ['hello'] }]
9891
+ * ```
9892
+ */
9893
+ enableFullTextSearch(config) {
9894
+ this.fullTextIndex = new FullTextIndex(config);
9895
+ const snapshot = this.getSnapshot();
9896
+ const entries = [];
9897
+ for (const [key, tagMap] of snapshot.items) {
9898
+ for (const [tag, record] of tagMap) {
9899
+ if (!snapshot.tombstones.has(tag)) {
9900
+ const compositeKey = this.createCompositeKey(key, tag);
9901
+ entries.push([compositeKey, record.value]);
9902
+ }
9903
+ }
9904
+ }
9905
+ this.fullTextIndex.buildFromEntries(entries);
9906
+ return this.fullTextIndex;
9907
+ }
9908
+ /**
9909
+ * Check if full-text search is enabled.
9910
+ *
9911
+ * @returns true if full-text search is enabled
9912
+ */
9913
+ isFullTextSearchEnabled() {
9914
+ return this.fullTextIndex !== null;
9915
+ }
9916
+ /**
9917
+ * Get the full-text index (if enabled).
9918
+ *
9919
+ * @returns The FullTextIndex or null
9920
+ */
9921
+ getFullTextIndex() {
9922
+ return this.fullTextIndex;
9923
+ }
9924
+ /**
9925
+ * Perform a BM25-ranked full-text search.
9926
+ * Results are sorted by relevance score (highest first).
9927
+ *
9928
+ * @param query - Search query text
9929
+ * @param options - Search options (limit, minScore, boost)
9930
+ * @returns Array of search results with scores, sorted by relevance
9931
+ *
9932
+ * @throws Error if full-text search is not enabled
9933
+ */
9934
+ search(query, options) {
9935
+ if (!this.fullTextIndex) {
9936
+ throw new Error("Full-text search is not enabled. Call enableFullTextSearch() first.");
9937
+ }
9938
+ const scoredDocs = this.fullTextIndex.search(query, options);
9939
+ const results = [];
9940
+ for (const { docId: compositeKey, score, matchedTerms } of scoredDocs) {
9941
+ const [key, tag] = this.parseCompositeKey(compositeKey);
9942
+ const records = this.getRecords(key);
9943
+ const record = records.find((r) => r.tag === tag);
9944
+ if (record) {
9945
+ results.push({
9946
+ key,
9947
+ tag,
9948
+ value: record.value,
9949
+ score,
9950
+ matchedTerms: matchedTerms ?? []
9951
+ });
9952
+ }
9953
+ }
9954
+ return results;
9955
+ }
9956
+ /**
9957
+ * Disable full-text search and release the index.
9958
+ */
9959
+ disableFullTextSearch() {
9960
+ if (this.fullTextIndex) {
9961
+ this.fullTextIndex.clear();
9962
+ this.fullTextIndex = null;
9963
+ }
9964
+ }
8716
9965
  /**
8717
9966
  * Remove an index.
8718
9967
  *
@@ -8866,6 +10115,9 @@ var IndexedORMap = class extends ORMap {
8866
10115
  const record = super.add(key, value, ttlMs);
8867
10116
  const compositeKey = this.createCompositeKey(key, record.tag);
8868
10117
  this.indexRegistry.onRecordAdded(compositeKey, value);
10118
+ if (this.fullTextIndex) {
10119
+ this.fullTextIndex.onSet(compositeKey, value);
10120
+ }
8869
10121
  return record;
8870
10122
  }
8871
10123
  /**
@@ -8878,6 +10130,9 @@ var IndexedORMap = class extends ORMap {
8878
10130
  for (const record of matchingRecords) {
8879
10131
  const compositeKey = this.createCompositeKey(key, record.tag);
8880
10132
  this.indexRegistry.onRecordRemoved(compositeKey, record.value);
10133
+ if (this.fullTextIndex) {
10134
+ this.fullTextIndex.onRemove(compositeKey);
10135
+ }
8881
10136
  }
8882
10137
  return result;
8883
10138
  }
@@ -8889,6 +10144,9 @@ var IndexedORMap = class extends ORMap {
8889
10144
  if (applied) {
8890
10145
  const compositeKey = this.createCompositeKey(key, record.tag);
8891
10146
  this.indexRegistry.onRecordAdded(compositeKey, record.value);
10147
+ if (this.fullTextIndex) {
10148
+ this.fullTextIndex.onSet(compositeKey, record.value);
10149
+ }
8892
10150
  }
8893
10151
  return applied;
8894
10152
  }
@@ -8911,6 +10169,9 @@ var IndexedORMap = class extends ORMap {
8911
10169
  if (removedValue !== void 0 && removedKey !== void 0) {
8912
10170
  const compositeKey = this.createCompositeKey(removedKey, tag);
8913
10171
  this.indexRegistry.onRecordRemoved(compositeKey, removedValue);
10172
+ if (this.fullTextIndex) {
10173
+ this.fullTextIndex.onRemove(compositeKey);
10174
+ }
8914
10175
  }
8915
10176
  }
8916
10177
  /**
@@ -8919,6 +10180,9 @@ var IndexedORMap = class extends ORMap {
8919
10180
  clear() {
8920
10181
  super.clear();
8921
10182
  this.indexRegistry.clear();
10183
+ if (this.fullTextIndex) {
10184
+ this.fullTextIndex.clear();
10185
+ }
8922
10186
  }
8923
10187
  // ==================== Helper Methods ====================
8924
10188
  /**
@@ -9272,6 +10536,7 @@ var IndexedORMap = class extends ORMap {
9272
10536
  // Annotate the CommonJS export names for ESM import in node:
9273
10537
  0 && (module.exports = {
9274
10538
  AuthMessageSchema,
10539
+ BM25Scorer,
9275
10540
  BatchMessageSchema,
9276
10541
  BuiltInProcessors,
9277
10542
  BuiltInResolvers,
@@ -9295,6 +10560,7 @@ var IndexedORMap = class extends ORMap {
9295
10560
  DEFAULT_RESOLVER_RATE_LIMITS,
9296
10561
  DEFAULT_STOP_WORDS,
9297
10562
  DEFAULT_WRITE_CONCERN_TIMEOUT,
10563
+ ENGLISH_STOPWORDS,
9298
10564
  EntryProcessBatchRequestSchema,
9299
10565
  EntryProcessBatchResponseSchema,
9300
10566
  EntryProcessKeyResultSchema,
@@ -9304,8 +10570,11 @@ var IndexedORMap = class extends ORMap {
9304
10570
  EntryProcessorSchema,
9305
10571
  EventJournalImpl,
9306
10572
  FORBIDDEN_PATTERNS,
10573
+ FTSInvertedIndex,
10574
+ FTSTokenizer,
9307
10575
  FallbackIndex,
9308
10576
  FilteringResultSet,
10577
+ FullTextIndex,
9309
10578
  HLC,
9310
10579
  HashIndex,
9311
10580
  IndexRegistry,
@@ -9372,6 +10641,18 @@ var IndexedORMap = class extends ORMap {
9372
10641
  RegisterResolverRequestSchema,
9373
10642
  RegisterResolverResponseSchema,
9374
10643
  Ringbuffer,
10644
+ SearchMessageSchema,
10645
+ SearchOptionsSchema,
10646
+ SearchPayloadSchema,
10647
+ SearchRespMessageSchema,
10648
+ SearchRespPayloadSchema,
10649
+ SearchSubMessageSchema,
10650
+ SearchSubPayloadSchema,
10651
+ SearchUnsubMessageSchema,
10652
+ SearchUnsubPayloadSchema,
10653
+ SearchUpdateMessageSchema,
10654
+ SearchUpdatePayloadSchema,
10655
+ SearchUpdateTypeSchema,
9375
10656
  SetResultSet,
9376
10657
  SimpleAttribute,
9377
10658
  SortedMap,
@@ -9417,6 +10698,7 @@ var IndexedORMap = class extends ORMap {
9417
10698
  isUsingNativeHash,
9418
10699
  isWriteConcernAchieved,
9419
10700
  multiAttribute,
10701
+ porterStem,
9420
10702
  resetNativeHash,
9421
10703
  serialize,
9422
10704
  simpleAttribute,