@dikolab/kbdb 0.1.5 → 0.2.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.
Files changed (193) hide show
  1. package/README.md +30 -9
  2. package/dist/cli.cjs +181 -50
  3. package/dist/cli.cjs.map +3 -3
  4. package/dist/cli.mjs +181 -50
  5. package/dist/cli.mjs.map +3 -3
  6. package/dist/kbdb-worker.cjs +283 -29
  7. package/dist/kbdb-worker.cjs.map +4 -4
  8. package/dist/mod.cjs +13 -2
  9. package/dist/mod.cjs.map +2 -2
  10. package/dist/mod.mjs +13 -2
  11. package/dist/mod.mjs.map +2 -2
  12. package/dist/src/shared/cli/functions/format-output.function.d.ts +7 -7
  13. package/dist/src/shared/cli/functions/run-search.function.d.ts +25 -16
  14. package/dist/src/shared/cli/index.d.ts +1 -1
  15. package/dist/src/shared/cli/typings/cli-options.interface.d.ts +2 -0
  16. package/dist/src/shared/cli/typings/search-display.model.d.ts +25 -7
  17. package/dist/src/shared/mcp/functions/handle-recall.function.d.ts +29 -0
  18. package/dist/src/shared/mcp/typings/mcp-client.interface.d.ts +30 -11
  19. package/dist/src/shared/version/constants/version.constant.d.ts +1 -1
  20. package/dist/src/shared/wasm-codec/functions/decode-recall-result.function.d.ts +28 -0
  21. package/dist/src/shared/wasm-codec/functions/decode-scored-results.function.d.ts +14 -7
  22. package/dist/src/shared/wasm-codec/functions/encode-recall-params.function.d.ts +21 -0
  23. package/dist/src/shared/wasm-codec/functions/encode-search-params.function.d.ts +1 -1
  24. package/dist/src/shared/wasm-codec/index.d.ts +5 -2
  25. package/dist/src/shared/worker-client/classes/worker-client.class.d.ts +13 -3
  26. package/dist/src/shared/worker-client/index.d.ts +2 -1
  27. package/dist/src/shared/worker-client/typings/recall-result.model.d.ts +94 -0
  28. package/dist/src/shared/worker-client/typings/search-params.model.d.ts +2 -0
  29. package/dist/src/shared/worker-client/typings/search-result.model.d.ts +23 -7
  30. package/dist/src/shared/worker-daemon/classes/worker-daemon.class.d.ts +1 -0
  31. package/dist/wasm/fs-database/kbdb_fs_database.d.ts +18 -0
  32. package/dist/wasm/fs-database/kbdb_fs_database.js +30 -0
  33. package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
  34. package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +2 -0
  35. package/dist/wasm/kb-worker/kbdb_worker.d.ts +38 -0
  36. package/dist/wasm/kb-worker/kbdb_worker.js +56 -0
  37. package/dist/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
  38. package/dist/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +3 -0
  39. package/dist/worker.mjs +283 -29
  40. package/dist/worker.mjs.map +4 -4
  41. package/docs/details/README.md +26 -0
  42. package/docs/details/cli.md +608 -0
  43. package/docs/details/library-api.md +406 -0
  44. package/docs/details/mcp-server.md +430 -0
  45. package/docs/details/search-and-ranking.md +457 -0
  46. package/docs/details/storage.md +529 -0
  47. package/docs/goals/agents.md +751 -0
  48. package/docs/goals/architecture.svg +198 -0
  49. package/docs/goals/cli.md +2308 -0
  50. package/docs/goals/content-composer.md +609 -0
  51. package/docs/goals/content-parser.md +279 -0
  52. package/docs/goals/database.md +1452 -0
  53. package/docs/goals/document.md +368 -0
  54. package/docs/goals/mcp.md +1467 -0
  55. package/docs/goals/overview.md +124 -0
  56. package/docs/goals/query-parser.md +271 -0
  57. package/docs/goals/query-result.md +693 -0
  58. package/docs/goals/skills.md +738 -0
  59. package/docs/goals/worker-client.md +944 -0
  60. package/docs/goals/worker-daemon.md +1447 -0
  61. package/docs/modules/overview.md +298 -0
  62. package/docs/modules/rust/fs-database/functions/document.md +48 -0
  63. package/docs/modules/rust/fs-database/functions/integrity.md +68 -0
  64. package/docs/modules/rust/fs-database/functions/io.md +87 -0
  65. package/docs/modules/rust/fs-database/functions/module.md +22 -0
  66. package/docs/modules/rust/fs-database/functions/query-exec.md +44 -0
  67. package/docs/modules/rust/fs-database/functions/section.md +59 -0
  68. package/docs/modules/rust/fs-database/indexes/btree.md +35 -0
  69. package/docs/modules/rust/fs-database/indexes/corpus.md +61 -0
  70. package/docs/modules/rust/fs-database/indexes/inverted.md +60 -0
  71. package/docs/modules/rust/fs-database/indexes/module.md +64 -0
  72. package/docs/modules/rust/fs-database/indexes/positional.md +39 -0
  73. package/docs/modules/rust/fs-database/module.md +149 -0
  74. package/docs/modules/rust/fs-database/types/catalog.md +53 -0
  75. package/docs/modules/rust/fs-database/types/module.md +12 -0
  76. package/docs/modules/rust/fs-database/types/section.md +101 -0
  77. package/docs/modules/rust/kb-worker/functions/compose.md +57 -0
  78. package/docs/modules/rust/kb-worker/functions/module.md +20 -0
  79. package/docs/modules/rust/kb-worker/functions/retrieve.md +194 -0
  80. package/docs/modules/rust/kb-worker/functions/search.md +173 -0
  81. package/docs/modules/rust/kb-worker/functions/write.md +172 -0
  82. package/docs/modules/rust/kb-worker/module.md +171 -0
  83. package/docs/modules/rust/kb-worker/types/cache.md +90 -0
  84. package/docs/modules/rust/kb-worker/types/context.md +26 -0
  85. package/docs/modules/rust/kb-worker/types/module.md +16 -0
  86. package/docs/modules/rust/kb-worker/types/status.md +29 -0
  87. package/docs/modules/rust/kb-worker/types/token.md +50 -0
  88. package/docs/modules/rust/overview.md +495 -0
  89. package/docs/modules/rust/query-parser/functions/extract.md +56 -0
  90. package/docs/modules/rust/query-parser/functions/module.md +16 -0
  91. package/docs/modules/rust/query-parser/functions/parse.md +33 -0
  92. package/docs/modules/rust/query-parser/functions/pipeline.md +103 -0
  93. package/docs/modules/rust/query-parser/functions/rank.md +85 -0
  94. package/docs/modules/rust/query-parser/module.md +136 -0
  95. package/docs/modules/rust/query-parser/types/cache.md +39 -0
  96. package/docs/modules/rust/query-parser/types/keyphrase.md +60 -0
  97. package/docs/modules/rust/query-parser/types/module.md +17 -0
  98. package/docs/modules/rust/query-parser/types/token.md +42 -0
  99. package/docs/modules/rust/shared/content-composer/functions.md +114 -0
  100. package/docs/modules/rust/shared/content-composer/module.md +147 -0
  101. package/docs/modules/rust/shared/content-composer/types.md +93 -0
  102. package/docs/modules/rust/shared/content-parser/functions.md +72 -0
  103. package/docs/modules/rust/shared/content-parser/module.md +99 -0
  104. package/docs/modules/rust/shared/content-parser/types.md +71 -0
  105. package/docs/modules/rust/shared/corpus/module.md +24 -0
  106. package/docs/modules/rust/shared/corpus/types.md +141 -0
  107. package/docs/modules/rust/shared/document/module.md +25 -0
  108. package/docs/modules/rust/shared/document/types.md +154 -0
  109. package/docs/modules/rust/shared/encode/module.md +22 -0
  110. package/docs/modules/rust/shared/encode/traits.md +304 -0
  111. package/docs/modules/rust/shared/error/module.md +28 -0
  112. package/docs/modules/rust/shared/error/types.md +302 -0
  113. package/docs/modules/rust/shared/kbid/module.md +24 -0
  114. package/docs/modules/rust/shared/kbid/types.md +147 -0
  115. package/docs/modules/rust/shared/memory/functions.md +209 -0
  116. package/docs/modules/rust/shared/memory/module.md +24 -0
  117. package/docs/modules/rust/shared/module.md +168 -0
  118. package/docs/modules/rust/shared/query/module.md +27 -0
  119. package/docs/modules/rust/shared/query/types.md +236 -0
  120. package/docs/modules/rust/shared/section/module.md +25 -0
  121. package/docs/modules/rust/shared/section/types.md +294 -0
  122. package/docs/modules/rust/shared/term/module.md +25 -0
  123. package/docs/modules/rust/shared/term/types.md +139 -0
  124. package/docs/modules/typescript/cli.md +129 -0
  125. package/docs/modules/typescript/kbdb-worker.md +135 -0
  126. package/docs/modules/typescript/overview.md +378 -0
  127. package/docs/modules/typescript/shared/cli/constants.md +23 -0
  128. package/docs/modules/typescript/shared/cli/functions.md +630 -0
  129. package/docs/modules/typescript/shared/cli/module.md +125 -0
  130. package/docs/modules/typescript/shared/cli/typings.md +172 -0
  131. package/docs/modules/typescript/shared/hash/functions.md +20 -0
  132. package/docs/modules/typescript/shared/hash/module.md +21 -0
  133. package/docs/modules/typescript/shared/mcp/classes.md +179 -0
  134. package/docs/modules/typescript/shared/mcp/functions.md +386 -0
  135. package/docs/modules/typescript/shared/mcp/module.md +160 -0
  136. package/docs/modules/typescript/shared/mcp/typings.md +522 -0
  137. package/docs/modules/typescript/shared/module.md +94 -0
  138. package/docs/modules/typescript/shared/platform/functions.md +42 -0
  139. package/docs/modules/typescript/shared/platform/module.md +25 -0
  140. package/docs/modules/typescript/shared/runtime/functions.md +26 -0
  141. package/docs/modules/typescript/shared/runtime/module.md +27 -0
  142. package/docs/modules/typescript/shared/runtime/typings.md +13 -0
  143. package/docs/modules/typescript/shared/version/module.md +27 -0
  144. package/docs/modules/typescript/shared/wasm-codec/functions.md +391 -0
  145. package/docs/modules/typescript/shared/wasm-codec/module.md +98 -0
  146. package/docs/modules/typescript/shared/worker-client/classes.md +264 -0
  147. package/docs/modules/typescript/shared/worker-client/functions.md +175 -0
  148. package/docs/modules/typescript/shared/worker-client/module.md +84 -0
  149. package/docs/modules/typescript/shared/worker-client/typings.md +508 -0
  150. package/docs/modules/typescript/shared/worker-daemon/classes.md +239 -0
  151. package/docs/modules/typescript/shared/worker-daemon/constants.md +37 -0
  152. package/docs/modules/typescript/shared/worker-daemon/functions.md +221 -0
  153. package/docs/modules/typescript/shared/worker-daemon/module.md +112 -0
  154. package/docs/modules/typescript/shared/worker-daemon/typings.md +126 -0
  155. package/docs/overview.md +188 -0
  156. package/docs/release-notes/0.1.0.md +189 -0
  157. package/docs/release-notes/0.1.1.md +46 -0
  158. package/docs/release-notes/0.1.2.md +38 -0
  159. package/docs/release-notes/0.1.3.md +42 -0
  160. package/docs/release-notes/0.2.0.md +147 -0
  161. package/docs/release-notes/README.md +9 -0
  162. package/package.json +8 -2
  163. package/src/shared/cli/functions/dispatch-command.function.ts +3 -3
  164. package/src/shared/cli/functions/format-output.function.ts +41 -34
  165. package/src/shared/cli/functions/parse-args.function.ts +7 -0
  166. package/src/shared/cli/functions/run-search.function.ts +53 -27
  167. package/src/shared/cli/index.ts +4 -1
  168. package/src/shared/cli/typings/cli-options.interface.ts +2 -0
  169. package/src/shared/cli/typings/search-display.model.ts +26 -7
  170. package/src/shared/mcp/functions/create-tool-definitions.function.ts +64 -4
  171. package/src/shared/mcp/functions/handle-recall.function.ts +66 -0
  172. package/src/shared/mcp/functions/handle-tool-call.function.ts +11 -1
  173. package/src/shared/mcp/typings/mcp-client.interface.ts +33 -15
  174. package/src/shared/version/constants/version.constant.ts +1 -1
  175. package/src/shared/wasm-codec/functions/decode-recall-result.function.ts +267 -0
  176. package/src/shared/wasm-codec/functions/decode-scored-results.function.ts +81 -27
  177. package/src/shared/wasm-codec/functions/encode-recall-params.function.ts +68 -0
  178. package/src/shared/wasm-codec/functions/encode-search-params.function.ts +4 -1
  179. package/src/shared/wasm-codec/index.ts +11 -3
  180. package/src/shared/worker-client/classes/worker-client.class.ts +24 -4
  181. package/src/shared/worker-client/index.ts +11 -1
  182. package/src/shared/worker-client/typings/recall-result.model.ts +98 -0
  183. package/src/shared/worker-client/typings/search-params.model.ts +2 -0
  184. package/src/shared/worker-client/typings/search-result.model.ts +24 -7
  185. package/src/shared/worker-daemon/classes/worker-daemon.class.ts +25 -4
  186. package/src/wasm/fs-database/kbdb_fs_database.d.ts +18 -0
  187. package/src/wasm/fs-database/kbdb_fs_database.js +30 -0
  188. package/src/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
  189. package/src/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +2 -0
  190. package/src/wasm/kb-worker/kbdb_worker.d.ts +38 -0
  191. package/src/wasm/kb-worker/kbdb_worker.js +56 -0
  192. package/src/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
  193. package/src/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +3 -0
@@ -0,0 +1,693 @@
1
+ # Query Result -- Ranking and Ordering
2
+
3
+ *How search results are scored, combined, ordered,
4
+ and shaped into the three result modes.*
5
+
6
+ This document is the authoritative specification for
7
+ the ranking pipeline -- the path from raw index
8
+ matches to ordered, scored results. It consolidates
9
+ the algorithms, formulas, parameters, and data
10
+ structures that determine result relevance. For the
11
+ tokenization pipeline that produces the indexed terms,
12
+ see [Query Parser](query-parser.md). For the storage
13
+ engine that holds the indexes, see
14
+ [Database](database.md).
15
+
16
+ ## Ranking Pipeline Overview
17
+
18
+ The full ranking path, from query string to final
19
+ result set:
20
+
21
+ ```
22
+ query string
23
+ -> tokenize (query-parser.wasm)
24
+ -> parse query -> QueryPlan (query-parser.wasm)
25
+ -> execute plan -> Vec<RawMatch> (fs-database.wasm)
26
+ -> fetch CorpusStats (fs-database.wasm)
27
+ -> score: BM25F (query-parser.wasm)
28
+ -> score: proximity (query-parser.wasm)
29
+ -> score: zone (query-parser.wasm)
30
+ -> score: freshness (query-parser.wasm, optional)
31
+ -> combine scores
32
+ -> sort descending
33
+ -> truncate to limit
34
+ -> shape into result mode
35
+ -> return
36
+ ```
37
+
38
+ `kb-worker.wasm` orchestrates the entire flow. It
39
+ calls `query-parser.wasm` for tokenization, parsing,
40
+ and scoring, and `fs-database.wasm` for index lookups
41
+ and corpus statistics. Neither standalone module calls
42
+ the other directly.
43
+
44
+ ## Searchable Content Sources
45
+
46
+ Not all section data contributes equally to search
47
+ and ranking. Content is tokenized and pre-computed
48
+ at section insertion time (not at query time) and
49
+ stored in the section header and inverted index for
50
+ fast retrieval. See [Database](database.md) for the
51
+ storage format of token counts and per-field term
52
+ frequencies.
53
+
54
+ ### Text Content (`text/*` Sections)
55
+
56
+ Sections with MIME type `text/*` (e.g.
57
+ `text/markdown`, `text/typescript`,
58
+ `text/javascript`) are the primary searchable
59
+ content. Their text body is tokenized, stemmed, and
60
+ indexed. During indexing, the system extracts and
61
+ stores the per-field term frequencies, positions,
62
+ and token counts needed for later search and
63
+ ranking.
64
+
65
+ Text content maps to the **body** field in BM25F
66
+ (`FieldWeights.body`, default 1.0).
67
+
68
+ ### Title Attribute (All Sections)
69
+
70
+ Any section with a `title` attribute has its title
71
+ tokenized and indexed alongside (or instead of) the
72
+ body content. Title terms receive the **heading**
73
+ field weight in BM25F (`FieldWeights.heading`,
74
+ default 2.0) -- giving title terms **2x the ranking
75
+ influence** compared to body text.
76
+
77
+ For binary sections (e.g. `image/*`,
78
+ `application/octet-stream`) that have no searchable
79
+ text body, the `title` is **required** and serves as
80
+ the primary searchable content. Without a title,
81
+ binary sections are not searchable and cannot appear
82
+ in ranked results.
83
+
84
+ ### Description Attribute (Optional)
85
+
86
+ Any section with a `description` attribute has its
87
+ description tokenized and indexed. Description terms
88
+ receive the **body** field weight in BM25F
89
+ (`FieldWeights.body`, default 1.0) -- the same
90
+ ranking weight as `text/*` section body content.
91
+
92
+ Description is optional for all section types. When
93
+ present, it provides additional searchable text that
94
+ supplements the body content (for text sections) or
95
+ the title (for binary sections).
96
+
97
+ ### Binary Sections
98
+
99
+ Sections with binary MIME types (e.g. `image/*`,
100
+ `application/octet-stream`) contain non-textual data
101
+ (base64-encoded) that cannot be tokenized or indexed
102
+ directly. Their searchability relies entirely on
103
+ metadata attributes:
104
+
105
+ 1. **`title`** (required for searchability) -- indexed
106
+ with heading weight (2.0x). A binary section
107
+ without a title is stored but invisible to search.
108
+ 2. **`description`** (optional) -- indexed with body
109
+ weight (1.0x). Provides additional search surface
110
+ for binary content.
111
+
112
+ A well-titled image section can appear in search
113
+ results ranked by its title relevance, even though
114
+ its pixel data is not searchable.
115
+
116
+ ### Code Sections
117
+
118
+ Sections with code-related MIME types (e.g.
119
+ `text/typescript`, `text/javascript`) have their
120
+ source code tokenized with identifier splitting
121
+ (camelCase, snake_case, PascalCase). Code content
122
+ maps to the **code** field weight in BM25F
123
+ (`FieldWeights.code`, default 1.5) -- giving code
124
+ terms **1.5x the ranking influence** compared to
125
+ plain body text.
126
+
127
+ If a code section also has `title` and/or
128
+ `description`, those are indexed with their
129
+ respective weights (heading 2.0x, body 1.0x) in
130
+ addition to the code body.
131
+
132
+ ### Field Weight Mapping
133
+
134
+ | Content source | BM25F field | Default weight |
135
+ |-------------------------|-------------|----------------|
136
+ | `title` attribute | `heading` | 2.0 |
137
+ | `text/*` body content | `body` | 1.0 |
138
+ | `description` attribute | `body` | 1.0 |
139
+ | Code section body | `code` | 1.5 |
140
+ | Binary content (raw) | -- | not indexed |
141
+
142
+ ### Pre-Computed Data at Insertion Time
143
+
144
+ When a section is added or updated, `kb-worker.wasm`
145
+ calls `query-parser.wasm` to tokenize the content
146
+ and compute the following data, which is then stored
147
+ by `fs-database.wasm` for use at query time:
148
+
149
+ - **Per-field term frequencies** -- for each term,
150
+ the term frequency in each field (heading, body,
151
+ code) is stored in the inverted index as separate
152
+ values rather than a single aggregated weight. This
153
+ lets BM25F apply field weights at query time,
154
+ making them tunable without rebuilding indexes.
155
+ - **Token counts** -- total and per-field token
156
+ counts are stored in the section header. Used for
157
+ BM25F length normalization and zone scoring.
158
+ - **Term positions** -- zero-based token offsets
159
+ stored in the positional index. Used for proximity
160
+ and zone scoring.
161
+ - **Corpus statistics updates** -- total sections,
162
+ total terms, per-term document frequencies, and
163
+ per-field average lengths are updated in
164
+ `corpus.dat`.
165
+
166
+ ## Raw Match (Pre-Ranking Data)
167
+
168
+ A `RawMatch` is the data that comes out of
169
+ `fs-database.wasm` after index lookups, before any
170
+ scoring is applied:
171
+
172
+ ```rust
173
+ pub struct RawMatch {
174
+ pub kbid: KbId,
175
+ pub field_tfs: Vec<(Term, FieldTf)>,
176
+ pub positions: Vec<(Term, Vec<u32>)>,
177
+ pub token_count: u32,
178
+ pub field_token_counts: FieldTokenCounts,
179
+ }
180
+
181
+ pub struct FieldTf {
182
+ pub heading: f32,
183
+ pub body: f32,
184
+ pub code: f32,
185
+ }
186
+
187
+ pub struct FieldTokenCounts {
188
+ pub heading: u32,
189
+ pub body: u32,
190
+ pub code: u32,
191
+ }
192
+ ```
193
+
194
+ - `field_tfs` -- per-term, per-field term frequency
195
+ values retrieved from the inverted index. Each
196
+ entry contains the TF for the heading, body, and
197
+ code fields separately.
198
+ - `positions` -- zero-based token offsets per term,
199
+ retrieved from the positional index. Fed to
200
+ proximity and zone scoring.
201
+ - `token_count` -- total tokens in the section,
202
+ retrieved from the section header. Used for BM25F
203
+ length normalization (`|d|`) and zone scoring (20%
204
+ heading zone boundary).
205
+ - `field_token_counts` -- per-field token counts,
206
+ retrieved from the section header. Used for
207
+ per-field BM25 length normalization.
208
+
209
+ ## Corpus Statistics
210
+
211
+ `CorpusStats` provides corpus-wide aggregates needed
212
+ for IDF computation and length normalization:
213
+
214
+ ```rust
215
+ pub struct CorpusStats {
216
+ pub total_sections: u32,
217
+ pub total_terms: u32,
218
+ pub avg_section_length: f32,
219
+ pub avg_heading_length: f32,
220
+ pub avg_body_length: f32,
221
+ pub avg_code_length: f32,
222
+ }
223
+ ```
224
+
225
+ - `total_sections` -- `N` in the IDF formula. Counts
226
+ only sections with at least one indexed term
227
+ (excludes binary sections with no title).
228
+ - `avg_section_length` -- average total token count
229
+ per indexed section. Used for overall BM25F length
230
+ normalization.
231
+ - `avg_heading_length` -- average heading field token
232
+ count. Used for per-field BM25 normalization of
233
+ title terms.
234
+ - `avg_body_length` -- average body field token
235
+ count. Used for per-field BM25 normalization of
236
+ body and description terms.
237
+ - `avg_code_length` -- average code field token
238
+ count. Used for per-field BM25 normalization of
239
+ code terms.
240
+
241
+ Per-field averages enable accurate per-field BM25
242
+ normalization -- a 3-word title is not penalized
243
+ against a 500-word average body length.
244
+
245
+ ## Scoring Functions
246
+
247
+ ### BM25F (Primary Ranking)
248
+
249
+ Field-Weighted Okapi BM25. The primary ranking
250
+ function that produces the base relevance score.
251
+
252
+ **Function signature:**
253
+
254
+ ```rust
255
+ pub fn score_bm25f(
256
+ params: &BM25FParams,
257
+ raw_match: &RawMatch,
258
+ corpus: &CorpusStats,
259
+ ) -> f32;
260
+ ```
261
+
262
+ **Parameters:**
263
+
264
+ ```rust
265
+ pub struct BM25FParams {
266
+ pub k1: f32, // default 1.2
267
+ pub b: f32, // default 0.75
268
+ pub field_weights: FieldWeights,
269
+ }
270
+
271
+ pub struct FieldWeights {
272
+ pub heading: f32, // default 2.0
273
+ pub body: f32, // default 1.0
274
+ pub code: f32, // default 1.5
275
+ }
276
+ ```
277
+
278
+ **IDF formula:**
279
+
280
+ ```
281
+ idf(t) = ln((N - df(t) + 0.5) / (df(t) + 0.5) + 1)
282
+ ```
283
+
284
+ Where `N` = `corpus.total_sections` and `df(t)` =
285
+ number of sections containing term `t`.
286
+
287
+ **Per-field BM25 score:**
288
+
289
+ For each field `f` (heading, body, code), the
290
+ per-term score uses the field-specific TF and the
291
+ field-specific average length:
292
+
293
+ ```
294
+ score_f(t, d) = idf(t)
295
+ * (tf_f(t,d) * (k1 + 1))
296
+ / (tf_f(t,d) + k1 * (1 - b + b * |d_f| / avgdl_f))
297
+ ```
298
+
299
+ Where:
300
+ - `tf_f(t,d)` = term frequency of `t` in field `f`
301
+ of section `d` (from `FieldTf`)
302
+ - `|d_f|` = token count of field `f` in section `d`
303
+ (from `FieldTokenCounts`)
304
+ - `avgdl_f` = average token count of field `f`
305
+ across the corpus (from `CorpusStats`)
306
+
307
+ **Field weighting:**
308
+
309
+ The final BM25F score for a term is the weighted sum
310
+ of per-field scores:
311
+
312
+ ```
313
+ bm25f(t, d) = w_heading * score_heading(t, d)
314
+ + w_body * score_body(t, d)
315
+ + w_code * score_code(t, d)
316
+ ```
317
+
318
+ The total BM25F score for a section is the sum of
319
+ `bm25f(t, d)` across all matched query terms.
320
+
321
+ **Parameter explanations:**
322
+
323
+ - `k1` (1.2) -- term saturation. Higher values let
324
+ term frequency continue to increase the score;
325
+ lower values flatten the curve faster.
326
+ - `b` (0.75) -- length normalization. 0 = no length
327
+ normalization; 1 = full normalization. At 0.75,
328
+ longer sections are moderately penalized relative
329
+ to average-length sections.
330
+ - `field_weights` -- per-field multipliers. Because
331
+ per-field TFs are stored separately in the inverted
332
+ index, field weights can be tuned at query time
333
+ without rebuilding indexes.
334
+
335
+ ### Proximity Scoring (Supplementary)
336
+
337
+ Boosts sections where query terms appear close
338
+ together.
339
+
340
+ **Function signature:**
341
+
342
+ ```rust
343
+ pub fn score_proximity(
344
+ positions: &[(Term, Vec<u32>)],
345
+ query_terms: &[Term],
346
+ ) -> f32;
347
+ ```
348
+
349
+ **Algorithm:**
350
+
351
+ 1. For each pair of query terms that both appear in
352
+ the section, compute the minimum distance between
353
+ any of their positions.
354
+ 2. The proximity score is the inverse of the minimum
355
+ span across all query terms.
356
+ 3. Sections where all query terms appear within a
357
+ small window get the highest boost.
358
+ 4. Single-term queries receive a proximity score of 0
359
+ (no boost).
360
+
361
+ ### Zone Scoring (Supplementary)
362
+
363
+ Structural weighting. Terms appearing in high-value
364
+ positions within a section get boosted.
365
+
366
+ **Function signature:**
367
+
368
+ ```rust
369
+ pub fn score_zone(
370
+ positions: &[(Term, Vec<u32>)],
371
+ section_length: u32,
372
+ ) -> f32;
373
+ ```
374
+
375
+ **Algorithm:**
376
+
377
+ - Terms in the first ~20% of a section (the "heading
378
+ zone") receive higher weight than terms in the
379
+ remaining body.
380
+ - The heading zone boundary is computed as
381
+ `0.2 * token_count`, using the pre-computed
382
+ `token_count` from the section header.
383
+ - The heading zone approximates headings and first
384
+ paragraphs, which typically carry more topical
385
+ signal.
386
+ - The zone score is added to the combined score as a
387
+ supplementary boost.
388
+
389
+ ### Freshness Weighting (Optional)
390
+
391
+ Time-decay boost based on section insertion
392
+ timestamp.
393
+
394
+ **Function signature:**
395
+
396
+ ```rust
397
+ pub fn score_freshness(
398
+ created_at: u64,
399
+ now: u64,
400
+ half_life_days: f32,
401
+ ) -> f32;
402
+ ```
403
+
404
+ **Algorithm:**
405
+
406
+ - Exponential decay: newer sections score higher.
407
+ - `half_life_days` controls how fast the boost
408
+ decays. After one half-life, the freshness score
409
+ is halved.
410
+ - This is an **optional** signal -- it can be
411
+ disabled by omitting the freshness weight from
412
+ score combination.
413
+
414
+ ## Score Combination
415
+
416
+ The four individual scores are combined into a final
417
+ ranking score:
418
+
419
+ ```
420
+ final_score = bm25f_score
421
+ + proximity_weight * proximity_score
422
+ + zone_weight * zone_score
423
+ + freshness_weight * freshness_score
424
+ ```
425
+
426
+ BM25F is the primary signal. Proximity, zone, and
427
+ freshness are supplementary signals added to the
428
+ BM25F base. Their weights control how much influence
429
+ each supplementary signal has relative to the primary
430
+ BM25F score.
431
+
432
+ Results are sorted by `final_score` in **descending
433
+ order** (highest relevance first). Ties are broken
434
+ by kbid lexicographic order for deterministic
435
+ ordering.
436
+
437
+ ## Result Truncation and Pagination
438
+
439
+ After sorting, results are windowed by `offset` and
440
+ `limit`. The ranking pipeline scores and sorts the
441
+ full result set, then skips the first `offset`
442
+ entries and takes the next `limit` entries. Both
443
+ parameters are optional: `offset` defaults to `0`,
444
+ `limit` defaults to `20`.
445
+
446
+ The result is a paged envelope, not a bare array:
447
+
448
+ ```ts
449
+ interface PagedSearchResult {
450
+ items: SectionMatch[] | DocumentMatch[];
451
+ total: number;
452
+ offset: number;
453
+ limit: number;
454
+ has_more: boolean;
455
+ }
456
+ ```
457
+
458
+ | Field | Description |
459
+ |------------|-----------------------------------|
460
+ | `items` | The windowed result records |
461
+ | `total` | Full match count before windowing |
462
+ | `offset` | Echo of requested offset (or 0) |
463
+ | `limit` | Echo of effective limit (or 20) |
464
+ | `has_more` | `true` when `offset + items.length < total` |
465
+
466
+ `has_more` is a convenience boolean so the caller
467
+ does not need to compute whether more pages exist.
468
+ The AI agent can page forward by calling `search`
469
+ again with `offset` incremented by `limit`.
470
+
471
+ Stats mode uses the same envelope with
472
+ `items: []` and aggregate data in a separate
473
+ `stats` field alongside the paging fields.
474
+
475
+ ## Result Modes
476
+
477
+ Three result modes shape the ranked results
478
+ differently.
479
+
480
+ ### Sections Mode
481
+
482
+ Returns individual `SectionMatch` records, each with
483
+ its own score:
484
+
485
+ ```ts
486
+ interface SectionMatch {
487
+ kbid: string;
488
+ docids: string[];
489
+ type: string;
490
+ heading: string;
491
+ score: number;
492
+ snippet: string;
493
+ matched_terms: string[];
494
+ }
495
+ ```
496
+
497
+ `heading` is `string | null` -- `null` when the
498
+ section has no title. `snippet` is always populated
499
+ for text and code sections using KWIC
500
+ (keyword-in-context): ~150 characters centered on
501
+ the highest-scoring matched term position, with
502
+ `...` ellipsis at boundaries. If the best match is
503
+ in the heading (not body), the snippet is the first
504
+ ~150 characters of the body content. For image
505
+ sections and sections with no body content,
506
+ `snippet` is an empty string.
507
+
508
+ Sections are ordered by `score` descending. Each
509
+ section appears at most once.
510
+
511
+ ### Documents Mode
512
+
513
+ Returns `DocumentMatch` records. Aggregates
514
+ section-level scores into a document-level score:
515
+
516
+ ```ts
517
+ interface DocumentMatch {
518
+ docid: string;
519
+ title: string;
520
+ score: number;
521
+ matching_sections: Array<{
522
+ kbid: string;
523
+ type: string;
524
+ heading: string;
525
+ score: number;
526
+ snippet: string;
527
+ }>;
528
+ }
529
+ ```
530
+
531
+ **Document score aggregation:** A document's score is
532
+ the sum (or max, depending on config) of its matching
533
+ sections' scores. Documents are ordered by their
534
+ aggregated score descending. Within each document,
535
+ matching sections are ordered by their individual
536
+ scores descending.
537
+
538
+ Sections not belonging to any document are returned
539
+ as single-section documents.
540
+
541
+ ### Stats Mode
542
+
543
+ Returns aggregate statistics without content bodies:
544
+
545
+ ```ts
546
+ interface StatsSearchResult {
547
+ total_matches: number;
548
+ query_time_ms: number;
549
+ top_terms: TermStat[];
550
+ query: string;
551
+ }
552
+
553
+ interface TermStat {
554
+ term: string;
555
+ frequency: number;
556
+ sections_matched: number;
557
+ }
558
+ ```
559
+
560
+ No individual scores. `top_terms` are ordered by
561
+ `frequency` descending.
562
+
563
+ ## MCP Search Projection
564
+
565
+ When search results are returned via the MCP
566
+ `search` tool, the ranking engine produces full
567
+ `SectionMatch` records but the MCP layer projects
568
+ a subset of fields optimized for AI agent
569
+ consumption:
570
+
571
+ ```ts
572
+ interface McpSearchResult {
573
+ kbid: string;
574
+ heading: string | null;
575
+ type: string;
576
+ docids: string[];
577
+ snippet: string;
578
+ matched_terms: string[];
579
+ content?: string;
580
+ }
581
+ ```
582
+
583
+ The `score` field from `SectionMatch` is
584
+ intentionally omitted. Results are ordered by
585
+ relevance descending; the position in the array
586
+ is the ranking signal. Exposing uncalibrated BM25F
587
+ scores provides no actionable information to the
588
+ agent and risks confusing score magnitudes across
589
+ different queries.
590
+
591
+ `matched_terms` is a flat `string[]` of the query
592
+ terms that matched -- no per-term frequency counts.
593
+ The agent needs to know *which* terms matched, not
594
+ *how often*.
595
+
596
+ `heading` is `string | null`. When a section has
597
+ no title, `heading` is `null` (not empty string).
598
+ This lets the AI agent distinguish "no title" from
599
+ "empty title."
600
+
601
+ The `content` field is present only when the MCP
602
+ search tool's `content` parameter is `true`. When
603
+ present, it contains the full section text (or
604
+ base64-encoded data for image sections).
605
+
606
+ The CLI `search` command continues to expose
607
+ `score` via the `SearchDisplay` type for human
608
+ inspection and debugging.
609
+
610
+ The MCP result uses the same `PagedSearchResult`
611
+ envelope with `items`, `total`, `offset`, `limit`,
612
+ and `has_more`.
613
+
614
+ ## References Resolution
615
+
616
+ After ranking and truncation, the search flow
617
+ resolves content marker references for the returned
618
+ sections. Each result set includes a `references`
619
+ array sourced from the bidirectional reference
620
+ graph index (`references.idx`). Each reference
621
+ entry includes:
622
+
623
+ - `kbid` or `docid` of the referenced entity.
624
+ - `marker_type` (Image, Code, Text, Document,
625
+ Link, or DocLink).
626
+ - `context_snippet` (~150 chars of the surrounding
627
+ text where the marker appears).
628
+ - `direction` (`forward` or `back`).
629
+
630
+ Forward references come from markers in the
631
+ returned sections' content. Back-references come
632
+ from markers in *other* sections that point to
633
+ the returned sections. Both directions are looked
634
+ up from the reference graph index in O(1) rather
635
+ than re-parsing content at query time. See
636
+ [Database -- Reference Graph](database.md) for the
637
+ index structure and [Content Parser](content-parser.md)
638
+ for how markers are extracted at indexing time.
639
+
640
+ ## Index Structures Supporting Ranking
641
+
642
+ The ranking pipeline consumes data from three index
643
+ structures maintained by `fs-database.wasm`:
644
+
645
+ - **Inverted index** (`indexes/terms.idx`) -- maps
646
+ normalized terms to posting lists of
647
+ `(kbid, heading_tf, body_tf, code_tf)` tuples.
648
+ Supports term lookup, IDF computation, and
649
+ per-field term frequency retrieval.
650
+ - **Positional index** (`indexes/positions.idx`) --
651
+ maps `(term, kbid)` pairs to position arrays
652
+ (zero-based token offsets). Supports proximity
653
+ scoring, zone scoring, and phrase verification.
654
+ - **Corpus statistics** (`indexes/corpus.dat`) --
655
+ stores `total_sections`, `total_terms`,
656
+ `avg_section_length`, `avg_heading_length`,
657
+ `avg_body_length`, `avg_code_length`, and per-term
658
+ document frequencies. Supports IDF and per-field
659
+ length normalization.
660
+
661
+ See [Database](database.md) for full index
662
+ specifications.
663
+
664
+ ## WASM Exports
665
+
666
+ The query-parser WASM exports used for ranking:
667
+
668
+ | Export | Role |
669
+ |------------------------|--------------------------|
670
+ | `qp_score_bm25f` | BM25F primary scoring |
671
+ | `qp_score_proximity` | Proximity boost |
672
+ | `qp_score_zone` | Zone/structural boost |
673
+ | `qp_score_freshness` | Freshness decay boost |
674
+
675
+ These are called by `kb-worker.wasm` after
676
+ `fs-database.wasm` returns raw matches.
677
+
678
+ ## Keyword Extraction and Term Weighting
679
+
680
+ Terms are weighted during indexing -- the other side
681
+ of the ranking coin:
682
+
683
+ - **RAKE** (Rapid Automatic Keyword Extraction) --
684
+ identifies candidate keyphrases by splitting on
685
+ stop words, scores by word degree / frequency
686
+ ratio.
687
+ - **TF-IDF** -- per-section, per-field term frequency
688
+ combined with corpus-wide inverse document
689
+ frequency. Per-field TFs are stored in the inverted
690
+ index as separate values per posting entry.
691
+
692
+ See [Query Parser](query-parser.md) for the full
693
+ tokenization pipeline and extraction algorithms.