@delali/narsil 0.3.1-next.72 → 0.3.1-next.74

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 (70) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{capabilities-D6gjjQyA.d.ts → capabilities-CAMzmkAg.d.ts} +19 -4
  5. package/dist/{chunk-P2ZZN32C.mjs → chunk-2E35UM3G.mjs} +2 -12
  6. package/dist/chunk-3DDUNI6N.mjs +48 -0
  7. package/dist/{chunk-KZTI25D3.mjs → chunk-4MQ5F4IL.mjs} +1 -1
  8. package/dist/{chunk-HVJYTKK7.mjs → chunk-BVBIQQKG.mjs} +947 -1327
  9. package/dist/{chunk-2X2ZQOAB.mjs → chunk-CTOSGRM7.mjs} +101 -21
  10. package/dist/{chunk-7QMTA7AA.mjs → chunk-CUZOX6CN.mjs} +7784 -4276
  11. package/dist/chunk-I23G3IZC.mjs +593 -0
  12. package/dist/{chunk-RV77FR2F.mjs → chunk-I64LXKO3.mjs} +0 -1
  13. package/dist/chunk-KEHK2O6P.mjs +13 -0
  14. package/dist/{chunk-CVPA5Q7R.mjs → chunk-LX4JERXI.mjs} +9 -6
  15. package/dist/{chunk-EDMCZLSJ.mjs → chunk-NOZUQM7J.mjs} +193 -13
  16. package/dist/{chunk-LCCL6EEH.mjs → chunk-PO53FV4Q.mjs} +93 -94
  17. package/dist/chunk-T5ZWEWVD.mjs +658 -0
  18. package/dist/{chunk-Y52WZWAU.mjs → chunk-TVFLHM7M.mjs} +4 -4
  19. package/dist/{chunk-QR4OKRVY.mjs → chunk-VKX2BPXD.mjs} +147 -642
  20. package/dist/{chunk-3HQPUCCT.mjs → chunk-XW25X6AU.mjs} +66 -237
  21. package/dist/{chunk-M6XF5HDE.mjs → chunk-Z3DQPOJQ.mjs} +127 -30
  22. package/dist/{client-C2TiW29y.d.ts → client-C-I70ArH.d.ts} +77 -26
  23. package/dist/client.d.ts +9 -9
  24. package/dist/client.mjs +44 -22
  25. package/dist/{config-BLguB-om.d.ts → config-C6Iw5rCX.d.ts} +11 -4
  26. package/dist/{config-C7Ip8Ife.d.ts → config-DJPH3IQB.d.ts} +11 -2
  27. package/dist/direct-executor-KFAGILJC.mjs +17 -0
  28. package/dist/distribution/coordinator/etcd.d.ts +29 -16
  29. package/dist/distribution/coordinator/etcd.mjs +162 -75
  30. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  31. package/dist/distribution/transport/grpc.d.ts +2 -2
  32. package/dist/distribution/transport/grpc.mjs +22 -2
  33. package/dist/distribution/transport/in-memory.d.ts +2 -2
  34. package/dist/distribution/transport/in-memory.mjs +6 -3
  35. package/dist/distribution/transport/tcp.d.ts +2 -2
  36. package/dist/distribution/transport/tcp.mjs +40 -16
  37. package/dist/distribution.d.ts +9 -9
  38. package/dist/distribution.mjs +647 -366
  39. package/dist/embeddings/openai.mjs +1 -1
  40. package/dist/{engine-Dm4446j1.d.ts → engine-B8eRPO7m.d.ts} +97 -188
  41. package/dist/index.browser.mjs +12132 -8592
  42. package/dist/index.d.ts +27 -8
  43. package/dist/index.mjs +15 -14
  44. package/dist/{memory-CKHTU1CL.d.ts → memory-DJEwWoCi.d.ts} +1204 -968
  45. package/dist/partial-words-DTIHO8G4.d.ts +19 -0
  46. package/dist/partial-words.d.ts +22 -0
  47. package/dist/partial-words.mjs +11 -0
  48. package/dist/pattern.mjs +1 -1
  49. package/dist/persistence/durability/checkpoint-worker.d.ts +57 -29
  50. package/dist/persistence/durability/checkpoint-worker.mjs +26 -16
  51. package/dist/react.d.ts +7 -7
  52. package/dist/react.mjs +0 -1
  53. package/dist/{schema-BBOCdhND.d.ts → schema-DgyDijhf.d.ts} +6 -2
  54. package/dist/server.d.ts +11 -11
  55. package/dist/server.mjs +212 -32
  56. package/dist/{tasks-BECd0b__.d.ts → tasks-BVmAKYBh.d.ts} +3 -3
  57. package/dist/{thread-IBSIZZ6W.mjs → thread-W6DPKEF6.mjs} +82 -21
  58. package/dist/{types-BQDlIvfS.d.ts → types-B3b68nfF.d.ts} +1 -1
  59. package/dist/{types-D5OwhtsQ.d.ts → types-Cb9kiY5P.d.ts} +1 -1
  60. package/dist/{types-BiUmaiWT.d.ts → types-ChrVy4lH.d.ts} +1 -1
  61. package/dist/vector/search-worker.d.ts +1 -1
  62. package/dist/vector/search-worker.mjs +4 -3
  63. package/dist/workers/entry.mjs +4 -4
  64. package/package.json +15 -7
  65. package/dist/chunk-3L4U6EJQ.mjs +0 -49
  66. package/dist/chunk-IFBKLBGB.mjs +0 -7
  67. package/dist/chunk-XZ4PAREY.mjs +0 -25
  68. package/dist/direct-executor-SVCHXR74.mjs +0 -14
  69. package/dist/serialization/crc32-worker.d.ts +0 -20
  70. package/dist/serialization/crc32-worker.mjs +0 -62
@@ -1,1210 +1,1446 @@
1
- import { A as AnyDocument, S as SchemaDefinition, d as ScoringMode } from './schema-BBOCdhND.js';
1
+ import { A as AnyDocument, d as ScoringMode, S as SchemaDefinition } from './schema-DgyDijhf.js';
2
2
 
3
3
  /**
4
- * Every code a {@link NarsilError} carries, grouped by the part of the engine
5
- * that raises it.
6
- *
7
- * Match on the code rather than on the message, because a code is stable and a
8
- * message is not. The HTTP server maps each code to a status, so a code also
9
- * tells you whether the caller or the engine is at fault.
4
+ * Compares one field against a value. Setting several keys narrows the match,
5
+ * because a document has to satisfy all of them.
10
6
  *
11
7
  * @public
12
8
  */
13
- declare const ErrorCodes: {
14
- readonly SCHEMA_INVALID_TYPE: "SCHEMA_INVALID_TYPE";
15
- readonly SCHEMA_DEPTH_EXCEEDED: "SCHEMA_DEPTH_EXCEEDED";
16
- readonly SCHEMA_INVALID_VECTOR_DIMENSION: "SCHEMA_INVALID_VECTOR_DIMENSION";
17
- readonly SCHEMA_INVALID_GEOPOINT: "SCHEMA_INVALID_GEOPOINT";
18
- readonly DOC_NOT_FOUND: "DOC_NOT_FOUND";
19
- readonly DOC_ALREADY_EXISTS: "DOC_ALREADY_EXISTS";
20
- readonly DOC_VALIDATION_FAILED: "DOC_VALIDATION_FAILED";
21
- readonly INDEX_NOT_FOUND: "INDEX_NOT_FOUND";
22
- readonly INDEX_ALREADY_EXISTS: "INDEX_ALREADY_EXISTS";
23
- readonly INDEX_ORPHANED: "INDEX_ORPHANED";
24
- readonly INDEX_REOPEN_CAPACITY_EXHAUSTED: "INDEX_REOPEN_CAPACITY_EXHAUSTED";
25
- readonly INSTANCE_SHUT_DOWN: "INSTANCE_SHUT_DOWN";
26
- readonly PARTITION_CORRUPTED: "PARTITION_CORRUPTED";
27
- readonly PARTITION_REBALANCING_BACKPRESSURE: "PARTITION_REBALANCING_BACKPRESSURE";
28
- readonly WORKER_CRASHED: "WORKER_CRASHED";
29
- readonly WORKER_BUSY: "WORKER_BUSY";
30
- readonly WORKER_TIMEOUT: "WORKER_TIMEOUT";
31
- readonly WORKER_ACTION_FAILED: "WORKER_ACTION_FAILED";
32
- readonly PERSISTENCE_SAVE_FAILED: "PERSISTENCE_SAVE_FAILED";
33
- readonly PERSISTENCE_LOAD_FAILED: "PERSISTENCE_LOAD_FAILED";
34
- readonly PERSISTENCE_DELETE_FAILED: "PERSISTENCE_DELETE_FAILED";
35
- readonly PERSISTENCE_CRC_MISMATCH: "PERSISTENCE_CRC_MISMATCH";
36
- readonly PERSISTENCE_WAL_CORRUPT: "PERSISTENCE_WAL_CORRUPT";
37
- readonly PERSISTENCE_FSYNC_FAILED: "PERSISTENCE_FSYNC_FAILED";
38
- readonly SEARCH_INVALID_FIELD: "SEARCH_INVALID_FIELD";
39
- readonly VECTOR_DIMENSION_MISMATCH: "VECTOR_DIMENSION_MISMATCH";
40
- readonly SEARCH_INVALID_FILTER: "SEARCH_INVALID_FILTER";
41
- readonly SEARCH_INVALID_MODE: "SEARCH_INVALID_MODE";
42
- readonly SEARCH_INVALID_CURSOR: "SEARCH_INVALID_CURSOR";
43
- readonly SEARCH_RESULT_WINDOW_EXCEEDED: "SEARCH_RESULT_WINDOW_EXCEEDED";
44
- readonly SEARCH_WORK_CAP_EXCEEDED: "SEARCH_WORK_CAP_EXCEEDED";
45
- readonly LANGUAGE_NOT_SUPPORTED: "LANGUAGE_NOT_SUPPORTED";
46
- readonly ENVELOPE_VERSION_MISMATCH: "ENVELOPE_VERSION_MISMATCH";
47
- readonly ENVELOPE_INVALID_MAGIC: "ENVELOPE_INVALID_MAGIC";
48
- readonly PARTITION_CAPACITY_EXCEEDED: "PARTITION_CAPACITY_EXCEEDED";
49
- readonly EMBEDDING_FAILED: "EMBEDDING_FAILED";
50
- readonly EMBEDDING_DIMENSION_MISMATCH: "EMBEDDING_DIMENSION_MISMATCH";
51
- readonly EMBEDDING_NO_SOURCE: "EMBEDDING_NO_SOURCE";
52
- readonly EMBEDDING_CONFIG_INVALID: "EMBEDDING_CONFIG_INVALID";
53
- readonly DOC_MISSING_REQUIRED_FIELD: "DOC_MISSING_REQUIRED_FIELD";
54
- readonly CONFIG_INVALID: "CONFIG_INVALID";
55
- readonly QUERY_ROUTING_FAILED: "QUERY_ROUTING_FAILED";
56
- readonly QUERY_PARTIAL_FAILURE: "QUERY_PARTIAL_FAILURE";
57
- readonly QUERY_NODE_TIMEOUT: "QUERY_NODE_TIMEOUT";
58
- readonly QUERY_NO_ACTIVE_REPLICA: "QUERY_NO_ACTIVE_REPLICA";
59
- readonly CLUSTER_OPERATION_UNSUPPORTED: "CLUSTER_OPERATION_UNSUPPORTED";
60
- readonly ALLOCATION_NO_DATA_NODES: "ALLOCATION_NO_DATA_NODES";
61
- readonly ALLOCATION_INVALID_CONFIG: "ALLOCATION_INVALID_CONFIG";
62
- readonly ALLOCATION_FAILED: "ALLOCATION_FAILED";
63
- readonly CONTROLLER_METADATA_INVALID: "CONTROLLER_METADATA_INVALID";
64
- readonly NODE_BOOTSTRAP_FAILED: "NODE_BOOTSTRAP_FAILED";
65
- readonly NODE_ALREADY_JOINED: "NODE_ALREADY_JOINED";
66
- readonly NODE_NOT_JOINED: "NODE_NOT_JOINED";
67
- readonly NODE_NOT_READY: "NODE_NOT_READY";
68
- readonly NODE_NOT_CONTROLLER: "NODE_NOT_CONTROLLER";
69
- readonly COORDINATOR_DEPENDENCY_MISSING: "COORDINATOR_DEPENDENCY_MISSING";
70
- readonly TRANSPORT_DEPENDENCY_MISSING: "TRANSPORT_DEPENDENCY_MISSING";
71
- readonly REPLICATION_ENTRY_INVALID: "REPLICATION_ENTRY_INVALID";
72
- readonly REPLICATION_INSYNC_REMOVAL_FAILED: "REPLICATION_INSYNC_REMOVAL_FAILED";
73
- readonly REPLICATION_ROLLBACK_FAILED: "REPLICATION_ROLLBACK_FAILED";
74
- readonly REPLICATION_LOG_FULL: "REPLICATION_LOG_FULL";
75
- readonly REPLICATION_ENTRY_CORRUPT: "REPLICATION_ENTRY_CORRUPT";
76
- readonly REPLICATION_SNAPSHOT_CORRUPT: "REPLICATION_SNAPSHOT_CORRUPT";
77
- readonly REPLICATION_TERM_MISMATCH: "REPLICATION_TERM_MISMATCH";
78
- readonly REPLICATION_SYNC_FAILED: "REPLICATION_SYNC_FAILED";
79
- readonly PARTITION_NOT_PRIMARY: "PARTITION_NOT_PRIMARY";
80
- readonly PARTITION_UNASSIGNED: "PARTITION_UNASSIGNED";
81
- readonly INSUFFICIENT_REPLICAS: "INSUFFICIENT_REPLICAS";
82
- readonly SNAPSHOT_SYNC_UNAUTHORIZED: "SNAPSHOT_SYNC_UNAUTHORIZED";
83
- readonly SNAPSHOT_SYNC_REQUEST_INVALID: "SNAPSHOT_SYNC_REQUEST_INVALID";
84
- readonly SNAPSHOT_SYNC_INDEX_NOT_FOUND: "SNAPSHOT_SYNC_INDEX_NOT_FOUND";
85
- readonly SNAPSHOT_SYNC_TOO_LARGE: "SNAPSHOT_SYNC_TOO_LARGE";
86
- readonly SNAPSHOT_SYNC_CAPACITY_EXHAUSTED: "SNAPSHOT_SYNC_CAPACITY_EXHAUSTED";
87
- readonly SNAPSHOT_SYNC_SNAPSHOT_FAILED: "SNAPSHOT_SYNC_SNAPSHOT_FAILED";
88
- readonly SNAPSHOT_SYNC_DECODE_FAILED: "SNAPSHOT_SYNC_DECODE_FAILED";
89
- readonly SNAPSHOT_SYNC_FRAME_INVALID: "SNAPSHOT_SYNC_FRAME_INVALID";
90
- readonly SNAPSHOT_SYNC_HEADER_INVALID: "SNAPSHOT_SYNC_HEADER_INVALID";
91
- readonly SNAPSHOT_SYNC_HEADER_MISMATCH: "SNAPSHOT_SYNC_HEADER_MISMATCH";
92
- readonly SNAPSHOT_SYNC_CHUNK_OUT_OF_ORDER: "SNAPSHOT_SYNC_CHUNK_OUT_OF_ORDER";
93
- readonly SNAPSHOT_SYNC_CHUNK_OVERFLOW: "SNAPSHOT_SYNC_CHUNK_OVERFLOW";
94
- readonly SNAPSHOT_SYNC_CHUNK_SIZE_EXCEEDED: "SNAPSHOT_SYNC_CHUNK_SIZE_EXCEEDED";
95
- readonly SNAPSHOT_SYNC_CHUNK_MISSING: "SNAPSHOT_SYNC_CHUNK_MISSING";
96
- readonly SNAPSHOT_SYNC_END_MISSING: "SNAPSHOT_SYNC_END_MISSING";
97
- readonly SNAPSHOT_SYNC_CHECKSUM_MISMATCH: "SNAPSHOT_SYNC_CHECKSUM_MISMATCH";
98
- readonly SNAPSHOT_SYNC_PRIMARY_ERROR: "SNAPSHOT_SYNC_PRIMARY_ERROR";
99
- readonly SNAPSHOT_SYNC_NO_TARGETS: "SNAPSHOT_SYNC_NO_TARGETS";
100
- readonly SNAPSHOT_SYNC_TRANSPORT_FAILED: "SNAPSHOT_SYNC_TRANSPORT_FAILED";
101
- readonly SNAPSHOT_SYNC_SCHEMA_UNAVAILABLE: "SNAPSHOT_SYNC_SCHEMA_UNAVAILABLE";
102
- readonly SNAPSHOT_SYNC_RESTORE_FAILED: "SNAPSHOT_SYNC_RESTORE_FAILED";
103
- readonly SNAPSHOT_SYNC_RESTORE_CLEANUP_FAILED: "SNAPSHOT_SYNC_RESTORE_CLEANUP_FAILED";
104
- readonly SNAPSHOT_SYNC_TIMEOUT: "SNAPSHOT_SYNC_TIMEOUT";
105
- readonly SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE: "SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE";
106
- readonly SNAPSHOT_SYNC_NOT_ASSIGNED: "SNAPSHOT_SYNC_NOT_ASSIGNED";
107
- readonly SNAPSHOT_SYNC_ABORTED: "SNAPSHOT_SYNC_ABORTED";
9
+ type ComparisonFilter = {
10
+ /** This matches a field equal to the value, or a list field where one element equals it. */
11
+ eq?: number | string | boolean;
12
+ /**
13
+ * This matches a document that holds the field with a value other than this
14
+ * one, or a list field where every element differs from it, which an empty
15
+ * list does.
16
+ */
17
+ ne?: number | string | boolean;
18
+ /** This matches a field greater than the number. */
19
+ gt?: number;
20
+ /** This matches a field less than the number. */
21
+ lt?: number;
22
+ /** This matches a field greater than or equal to the number. */
23
+ gte?: number;
24
+ /** This matches a field less than or equal to the number. */
25
+ lte?: number;
26
+ /** This matches a field inside the inclusive range, given as `[low, high]`. */
27
+ between?: [number, number];
108
28
  };
109
29
  /**
110
- * Any one of the codes in {@link ErrorCodes}, which is the type to narrow on
111
- * when you branch on a failure the engine raised.
30
+ * The engine tests a text field against this filter for equality, for a text
31
+ * range, or with the set and prefix tests.
112
32
  *
113
- * @public
114
- */
115
- type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
116
- /**
117
- * Every code the HTTP layer raises for a failure that arises before or around
118
- * the engine call, such as parsing the body, enforcing a limit, or routing.
33
+ * The engine compares each element of a `string[]`, `verbatim[]`, or `enum[]` field, so
34
+ * it matches a document with `eq`, `in`, `startsWith`, `endsWith`, or
35
+ * `contains` where one element passes the test, while it matches a document
36
+ * with `ne` or `nin` where every element differs from the values.
119
37
  *
120
- * The engine raises none of these, so a failure carrying one of them says the
121
- * request never reached the engine.
38
+ * The engine applies the range tests `gt`, `gte`, `lt`, `lte`, and `between`
39
+ * to a single `string` or `verbatim` field whose type includes `sortable`,
40
+ * while it throws `SEARCH_INVALID_FILTER` for a text range on any other field.
41
+ * It orders the values as a sort on the field does, comparing their case folds
42
+ * first and their raw code points only where the folds are equal.
122
43
  *
123
- * @public
124
- */
125
- declare const ServerErrorCodes: {
126
- readonly INVALID_REQUEST: "INVALID_REQUEST";
127
- readonly INVALID_JSON: "INVALID_JSON";
128
- readonly EMPTY_BODY: "EMPTY_BODY";
129
- readonly PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
130
- readonly NOT_FOUND: "NOT_FOUND";
131
- readonly TASK_NOT_FOUND: "TASK_NOT_FOUND";
132
- readonly TASK_NOT_CANCELLABLE: "TASK_NOT_CANCELLABLE";
133
- readonly TASK_OWNED_BY_ANOTHER_INSTANCE: "TASK_OWNED_BY_ANOTHER_INSTANCE";
134
- readonly TASK_INTERRUPTED: "TASK_INTERRUPTED";
135
- readonly TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS";
136
- readonly HOOK_ERROR: "HOOK_ERROR";
137
- readonly INTERNAL_ERROR: "INTERNAL_ERROR";
138
- };
139
- /**
140
- * Any one of the codes in {@link ServerErrorCodes}.
44
+ * A pattern field is a `verbatim` or `verbatim[]` field, or a `string` or
45
+ * `string[]` field whose type includes `pattern`. On such a field, the engine
46
+ * answers `eq`, `ne`, `in`, `nin`, `startsWith`, `endsWith`, and `contains`
47
+ * from the field's pattern index. `caseFold` applies to each of those tests.
48
+ * On an `enum` or `enum[]` field, the engine answers `eq`, `ne`, `in`, `nin`,
49
+ * `startsWith`, and `endsWith` from the field's index of values. It throws
50
+ * `SEARCH_INVALID_FILTER` for `eq`, `ne`, `in`, `nin`, `startsWith`, or
51
+ * `endsWith` on a `string` or `string[]` field whose type leaves out
52
+ * `pattern`, because such a field keeps no index of whole values.
53
+ * The engine compares the code points of each value as stored, with no
54
+ * Unicode normalisation. It throws `SEARCH_INVALID_FILTER` for `contains` or
55
+ * `caseFold` on any other field, for a test text longer than 1,024 code points
56
+ * on a pattern field, and for a `caseFold` test whose text has a case fold
57
+ * longer than 2,048 code points.
141
58
  *
142
59
  * @public
143
60
  */
144
- type ServerErrorCode = (typeof ServerErrorCodes)[keyof typeof ServerErrorCodes];
61
+ type StringFilter = Pick<ComparisonFilter, 'eq' | 'ne'> & {
62
+ /** This matches a value that comes after the text in sort order. */
63
+ gt?: string;
64
+ /** This matches a value that comes before the text in sort order. */
65
+ lt?: string;
66
+ /** This matches a value equal to the text or after it in sort order. */
67
+ gte?: string;
68
+ /** This matches a value equal to the text or before it in sort order. */
69
+ lte?: string;
70
+ /** This matches a value inside the inclusive range in sort order, given as `[low, high]`. */
71
+ between?: [string, string];
72
+ /** This matches a field equal to any value in the list, which holds at most 65,536 values. */
73
+ in?: string[];
74
+ /** This matches a field that differs from every value in the list, which holds at most 65,536 values. */
75
+ nin?: string[];
76
+ /** This matches a field starting with the text. */
77
+ startsWith?: string;
78
+ /** This matches a field ending with the text. */
79
+ endsWith?: string;
80
+ /** This matches a pattern field whose value holds the text anywhere in it. */
81
+ contains?: string;
82
+ /**
83
+ * When this is true, the engine compares the case fold of each value of a
84
+ * pattern field with the case fold of the text, so a test for `error` matches
85
+ * `ERROR`. It applies to `eq`, `ne`, `in`, `nin`, `startsWith`, `endsWith`,
86
+ * and `contains`.
87
+ */
88
+ caseFold?: boolean;
89
+ };
145
90
  /**
146
- * Every code that the caller's own side raises, where a request never reaches
147
- * a server, or where the caller cannot read the answer.
91
+ * Tests the contents of a list field, which is a `number[]`, `boolean[]`,
92
+ * `enum[]`, `verbatim[]`, or `string[]` field.
148
93
  *
149
- * The HTTP client raises the first five. The React bindings raise
150
- * `CLIENT_UNEXPECTED_ERROR` alone, which reports a fault inside a hook. No
151
- * server sends any of them, so a failure under one means the exchange broke
152
- * before the operation ran. `STATUS_BY_CODE` in `src/server/errors.ts`
153
- * therefore maps none of them, because no request can arrive under one and no
154
- * HTTP status belongs to one.
94
+ * The engine throws `SEARCH_INVALID_FILTER` for these tests on any other
95
+ * field, for a value whose type differs from the type of the list's elements,
96
+ * and for `containsAll` or `matchesAny` on a `string[]` field whose type leaves
97
+ * out `pattern`.
155
98
  *
156
99
  * @public
157
100
  */
158
- declare const ClientErrorCodes: {
159
- readonly CLIENT_CONNECTION_FAILED: "CLIENT_CONNECTION_FAILED";
160
- readonly CLIENT_REQUEST_TIMEOUT: "CLIENT_REQUEST_TIMEOUT";
161
- readonly CLIENT_REQUEST_ABORTED: "CLIENT_REQUEST_ABORTED";
162
- readonly CLIENT_INVALID_RESPONSE: "CLIENT_INVALID_RESPONSE";
163
- readonly CLIENT_TASK_TIMEOUT: "CLIENT_TASK_TIMEOUT";
164
- readonly CLIENT_UNEXPECTED_ERROR: "CLIENT_UNEXPECTED_ERROR";
101
+ type ArrayFilter = {
102
+ /** This matches a list holding every one of the values, at most 65,536 of them, so an empty set of values matches every list. */
103
+ containsAll?: (string | number | boolean)[];
104
+ /** This matches a list holding at least one of the values, at most 65,536 of them. */
105
+ matchesAny?: (string | number | boolean)[];
106
+ /** This compares the number of elements in the list. */
107
+ size?: ComparisonFilter;
165
108
  };
166
109
  /**
167
- * Any one of the codes in {@link ClientErrorCodes}.
110
+ * Tests whether a field carries a value at all, without looking at what it
111
+ * holds.
168
112
  *
169
113
  * @public
170
114
  */
171
- type ClientErrorCode = (typeof ClientErrorCodes)[keyof typeof ClientErrorCodes];
115
+ type PresenceFilter = {
116
+ /** This matches a document where the field holds a value other than null. */
117
+ exists?: boolean;
118
+ /** This matches a document where the field is absent or null. */
119
+ notExists?: boolean;
120
+ /** This matches a document where the field is absent, null, an empty string, or an empty list. */
121
+ isEmpty?: boolean;
122
+ /** This matches a document where the field holds a value other than an empty string or an empty list. */
123
+ isNotEmpty?: boolean;
124
+ };
172
125
  /**
173
- * Every code a {@link NarsilError} can carry: one the engine raised, one the
174
- * HTTP layer raised, one the client raised, or any other string.
175
- *
176
- * The last arm exists for a server's `onRequest` hook, which rejects a request
177
- * under a code of its own, such as `UNAUTHORIZED`, and the client then passes
178
- * that code through unchanged. An editor still completes every code Narsil
179
- * defines.
126
+ * Matches a `geopoint` field against a circle drawn on the earth.
180
127
  *
181
128
  * @public
182
129
  */
183
- type NarsilErrorCode = ErrorCode | ServerErrorCode | ClientErrorCode | (string & {});
184
- /**
185
- * The error every part of the engine throws.
130
+ type GeoRadiusFilter = {
131
+ /** The engine tests the field against this circle. */
132
+ radius: {
133
+ /** The centre has this latitude, in degrees from -90 to 90. For any other value, the engine throws `SEARCH_INVALID_FILTER`. */
134
+ lat: number;
135
+ /** The centre has this longitude, in degrees from -180 to 180. For any other value, the engine throws `SEARCH_INVALID_FILTER`. */
136
+ lon: number;
137
+ /** The circle has this radius, measured in `unit`. */
138
+ distance: number;
139
+ /**
140
+ * The engine measures `distance` in kilometres under `'km'`, in miles
141
+ * under `'mi'`, and in metres under `'m'`. For any other unit, it throws
142
+ * `SEARCH_INVALID_FILTER`.
143
+ */
144
+ unit: 'km' | 'mi' | 'm';
145
+ /** The engine matches the points inside the circle while this is true, which is the default, and the points outside it while this is false. */
146
+ inside?: boolean;
147
+ /**
148
+ * The engine measures each distance with the Haversine formula on a sphere
149
+ * by default, and with Vincenty's formula on the WGS-84 ellipsoid while
150
+ * this is true. Vincenty's formula iterates, so the engine takes longer
151
+ * to compute each distance. The two formulas differ by less than 0.3% under about
152
+ * 100 km, while across a continent they can differ by up to 0.5%.
153
+ */
154
+ highPrecision?: boolean;
155
+ };
156
+ };
157
+ /**
158
+ * Matches a `geopoint` field against a polygon drawn on the earth.
186
159
  *
187
- * Catch it, read {@link NarsilError.code} to decide what to do, and read
188
- * {@link NarsilError.details} for the values that caused it. The engine lets
189
- * this error propagate unchanged, so the code you catch is the code the
190
- * failing operation raised.
160
+ * @public
161
+ */
162
+ type GeoPolygonFilter = {
163
+ /** The engine tests the field against this polygon. */
164
+ polygon: {
165
+ /**
166
+ * The engine closes the ring through these corners in the order that you
167
+ * list them, so you can leave the first point off the end. It throws
168
+ * `SEARCH_INVALID_FILTER` for a ring of fewer than three corners, which
169
+ * encloses no area, and for a corner whose latitude is outside -90 to 90
170
+ * or whose longitude is outside -180 to 180. It also throws that error for
171
+ * a ring of more than 65,536 corners. List the corners counter-clockwise
172
+ * around the area, as in an exterior ring of GeoJSON. Where the corners
173
+ * span 180 degrees of longitude or more, the engine matches the area on
174
+ * the left of that path, so a ring listed that way can cross the
175
+ * antimeridian or span more than half the globe.
176
+ */
177
+ points: Array<{
178
+ lat: number;
179
+ lon: number;
180
+ }>;
181
+ /** The engine matches the points inside the polygon while this is true, which is the default, and the points outside it while this is false. */
182
+ inside?: boolean;
183
+ };
184
+ };
185
+ /**
186
+ * Either shape a `geopoint` field is matched against.
191
187
  *
192
188
  * @public
193
189
  */
194
- declare class NarsilError extends Error {
195
- /** This says which failure it is. */
196
- readonly code: NarsilErrorCode;
197
- /** This carries the values behind the failure, such as the field, index, or limit involved. It is empty when the code says everything. */
198
- readonly details: Record<string, unknown>;
199
- /**
200
- * Builds an error carrying a code the caller can branch on.
201
- *
202
- * @param code - The failure this error reports, from {@link ErrorCodes} for
203
- * an engine failure, {@link ServerErrorCodes} for one the HTTP layer raised,
204
- * or {@link ClientErrorCodes} for one that stopped a request from reaching a
205
- * server.
206
- * @param message - Plain description, for a log or a person.
207
- * @param details - Values behind the failure, which reach
208
- * {@link NarsilError.details}.
209
- */
210
- constructor(code: NarsilErrorCode, message: string, details?: Record<string, unknown>);
211
- }
190
+ type GeoFilter = GeoRadiusFilter | GeoPolygonFilter;
212
191
  /**
213
- * Reports whether a caught value is a {@link NarsilError}, including one that
214
- * another entry point of this package raises.
192
+ * The filter one field accepts. Which of these forms applies follows from the
193
+ * field's declared type.
215
194
  *
216
- * Each browser bundle holds its own copy of the class, so an `instanceof` test
217
- * inside the React bindings returns false for an error that the client raises,
218
- * although that error still holds its code. This function tests for the marker
219
- * that the constructor writes, so it returns true for an error from any entry
220
- * point.
195
+ * @public
196
+ */
197
+ type FieldFilter = ComparisonFilter | StringFilter | ArrayFilter | PresenceFilter | GeoFilter;
198
+ /**
199
+ * The filter that a query runs, built from per-field tests and the boolean
200
+ * operators that combine them.
221
201
  *
222
- * @param value - The value that a `catch` block receives.
223
- * @returns True where the value holds a code in {@link NarsilError.code}.
202
+ * The engine answers every test from an index that it keeps for the field. It
203
+ * applies the filter before scoring, so a narrow filter makes a search
204
+ * cheaper. A filter nests at most 30 levels deep and holds at most 1,024
205
+ * tests, counted across every field and nested expression. Past either limit,
206
+ * the engine throws `SEARCH_INVALID_FILTER`, while it throws
207
+ * `SEARCH_INVALID_FIELD` for a test on a field that the schema leaves out.
224
208
  *
225
209
  * @public
226
210
  */
227
- declare function isNarsilError(value: unknown): value is NarsilError;
211
+ type FilterExpression = {
212
+ /** These per-field tests are keyed by field name, and a document has to satisfy every entry. */
213
+ fields?: Record<string, FieldFilter>;
214
+ /** Every nested expression has to match. */
215
+ and?: FilterExpression[];
216
+ /** At least one nested expression has to match. */
217
+ or?: FilterExpression[];
218
+ /** The nested expression has to fail. */
219
+ not?: FilterExpression;
220
+ };
228
221
 
229
222
  /**
230
- * What {@link Narsil.query} returns: the ranked hits and everything the query
231
- * asked for alongside them.
223
+ * Everything {@link Narsil.suggest} accepts.
232
224
  *
233
- * @typeParam T - Shape of the stored documents, which flows through to each
234
- * hit's `document`.
225
+ * @public
226
+ */
227
+ interface SuggestParams {
228
+ /** The returned terms complete this text. */
229
+ prefix: string;
230
+ /** The lookup returns this many completions, most widely used first, and 10 by default. */
231
+ limit?: number;
232
+ }
233
+ /**
234
+ * Everything {@link Narsil.listDocuments} accepts.
235
+ *
236
+ * {@link Narsil.listDocuments} reads the stored documents without ranking them,
237
+ * which is how you page through a whole index. Leave `cursor` out to start at
238
+ * the first document, then pass back the cursor each result carries until it
239
+ * comes back null.
240
+ *
241
+ * A cursor belongs to the sort it was made under, so pass the same `sort` back
242
+ * with it. Changing `sort` invalidates the cursor, and the engine then throws
243
+ * `SEARCH_INVALID_CURSOR` rather than returning a page from the wrong order.
235
244
  *
236
245
  * @public
237
246
  */
238
- interface QueryResult<T = AnyDocument> {
239
- /** These documents match, cut to the query's `limit`, best score first or in the query's sort order. */
240
- hits: Array<Hit<T>>;
247
+ interface ListParams {
241
248
  /**
242
- * This many documents match in total, before the engine applies `limit`
243
- * and `offset`. For a keyword search the engine counts every match. For a
244
- * vector search it counts every vector that passes the query's filter and
245
- * its `similarity` floor, which is every vector in the field where the query
246
- * sets neither of them. The engine also counts each pinned document that
247
- * the query does not match, since it places that document among the hits.
249
+ * This cursor comes from a previous result, and continues where it stopped.
250
+ * The engine ties a cursor to the sort that produced it, and it throws
251
+ * `SEARCH_INVALID_CURSOR` for a cursor sent back under a different sort.
248
252
  */
249
- count: number;
253
+ cursor?: string;
254
+ /** The page carries this many documents, and 10 by default. The engine raises a value below one to one. */
255
+ limit?: number;
256
+ /** This narrows the listing to the documents the filter accepts. */
257
+ filters?: FilterExpression;
250
258
  /**
251
- * `count` holds the exact number of matches where this is true, and a
252
- * number at or below the true total where it is false. Where a query sets a
253
- * `similarity` floor on a vector field that holds a graph, the engine
254
- * counts only the vectors that it fetches, because it compares the query
255
- * with only a fraction of that field's vectors. For a hybrid query the
256
- * engine fuses two rankings, so the count is exact only where each of those
257
- * rankings contains every matching document. Where you pin a document from
258
- * outside the hits that the engine fetches, the engine reports true only
259
- * where it holds every match of the query, because only then can it test
260
- * whether the query matches that document.
259
+ * This orders the listing by field value rather than by document id, and the
260
+ * engine applies the fields in the order they are listed. Pass an object
261
+ * keyed by field, or a list of fields in the order they apply. It breaks a
262
+ * tie on document id, and it sorts by at most eight fields. The engine uses
263
+ * document-id order when you leave this out.
264
+ *
265
+ * The engine reads every document the listing covers to build a sorted page,
266
+ * so a sorted listing costs more than the default order on a large index.
261
267
  */
262
- countExact: boolean;
263
- /** The engine spent this many milliseconds on the search. */
264
- elapsed: number;
265
- /** This opaque cursor reaches the next page. Pass it back as `searchAfter`. */
266
- cursor?: string;
267
- /** These facet counts are keyed by the field each facet was asked for. */
268
- facets?: Record<string, FacetResult>;
269
- /** These groups arrive when the query asked for grouping. */
270
- groups?: GroupResult[];
271
- /** This turns true when the index's terms came from an older analysis than its language module produces now. */
272
- analysisStale?: boolean;
273
- /** The search read this much of the index, which is how you spot a degraded answer. */
274
- coverage: QueryCoverage;
268
+ sort?: SortSpec;
269
+ /** This chooses how much of each stored document comes back, and the whole document by default. */
270
+ document?: DocumentProjection;
275
271
  }
272
+
276
273
  /**
277
- * How much of an index one search read, which is what you check before you
278
- * trust a count or a ranking.
279
- *
280
- * An embedded engine reads every partition it holds, so `queriedPartitions`
281
- * matches `totalPartitions` there and the other two stay zero. A search across
282
- * a cluster may find a node slow or unreachable, which it counts here while
283
- * still returning the hits the other nodes gave it.
274
+ * Which engine answers a query: keyword matching, vector similarity, or both
275
+ * merged into one ranking.
284
276
  *
285
277
  * @public
286
278
  */
287
- interface QueryCoverage {
288
- /** The search should have read this many partitions. */
289
- totalPartitions: number;
290
- /** The search read this many of them. */
291
- queriedPartitions: number;
292
- /** The search counts a partition here when the node holding it took longer than the query allowed. */
293
- timedOutPartitions: number;
294
- /** The search counts a partition here when the node holding it returned an error, and when no active replica served it. */
295
- failedPartitions: number;
296
- }
279
+ type SearchMode = 'fulltext' | 'vector' | 'hybrid';
297
280
  /**
298
- * One matching document, with the score that ranked it.
299
- *
300
- * @typeParam T - Shape of the stored document.
281
+ * How many of a query's terms a document has to carry. `all` demands every
282
+ * term, `any` accepts one, and a number demands that many.
301
283
  *
302
284
  * @public
303
285
  */
304
- interface Hit<T = AnyDocument> {
305
- /** The document is stored under this id. */
306
- id: string;
307
- /**
308
- * This score ranked the hit. Scores compare within one result set, never
309
- * across searches. A sorted query ranks by sort values instead and carries
310
- * no score unless it sets `includeScores`.
311
- */
312
- score?: number;
313
- /** This is the stored document. */
314
- document: T;
315
- /** These are the parts the score was built from, which arrive when the query asked for them. */
316
- scoreComponents?: ScoreComponents;
317
- /** These highlighted snippets are keyed by field, and arrive when the query asked for highlighting. */
318
- highlights?: Record<string, HighlightMatch>;
319
- }
286
+ type TermMatchPolicy = 'all' | 'any' | number;
320
287
  /**
321
- * The numbers BM25 combined into one hit's score, which is what you read when
322
- * a ranking surprises you.
288
+ * One field of a sort, and the direction that field orders by.
323
289
  *
324
290
  * @public
325
291
  */
326
- interface ScoreComponents {
292
+ interface SortField {
293
+ /** The field the engine reads, which may name a nested field with dots. */
294
+ field: string;
295
+ /** Ascending or descending order for this field. */
296
+ direction: 'asc' | 'desc';
327
297
  /**
328
- * Each query term appears in one field of this document this many times,
329
- * under a key of `field:term`. A term that appears in two fields therefore
330
- * has two entries here, while the keys of {@link ScoreComponents.idf} hold
331
- * the bare term.
298
+ * Where a document's field holds a list, the engine sorts that document by
299
+ * one value from the list: the smallest under `'min'`, the largest under
300
+ * `'max'`, and the mean or the median of the list's numbers under `'avg'`
301
+ * or `'median'`. Where you leave this out, the engine uses `'min'` for an
302
+ * ascending sort and `'max'` for a descending one. It sorts by a field that
303
+ * holds a single value as that value stands, under every mode. It throws
304
+ * `SEARCH_INVALID_FIELD` for `'avg'` or `'median'` on a field that the
305
+ * schema declares as anything other than `number` or `number[]`.
332
306
  */
333
- termFrequencies: Record<string, number>;
334
- /** Each field the query searched holds this many tokens, keyed by field. */
335
- fieldLengths: Record<string, number>;
336
- /** Each query term carries this inverse document frequency, which is what makes a rare term count for more. */
337
- idf: Record<string, number>;
307
+ mode?: 'min' | 'max' | 'avg' | 'median';
338
308
  }
339
309
  /**
340
- * A highlighted field, ready to render.
310
+ * A sort, either as an object keyed by field or as a list of fields in the
311
+ * order they apply.
312
+ *
313
+ * Both forms order by the first field, break a tie on the second, and carry on
314
+ * that way. Prefer the list where the order of the fields matters, because
315
+ * JavaScript moves an all-digit key such as `2024` to the front of an object
316
+ * and the list keeps the order you wrote.
341
317
  *
342
318
  * @public
343
319
  */
344
- interface HighlightMatch {
345
- /** This is the field's text around the match, wrapped in the query's tags. */
346
- snippet: string;
347
- /**
348
- * Each range gives the start and the end of one match in the original field,
349
- * so you can render the highlight yourself. Where the snippet covers part of
350
- * the field alone, the engine lists only the matches that the snippet
351
- * displays. Every offset is a character count from the start of the original
352
- * field, whatever the snippet contains.
353
- */
354
- positions: Array<{
355
- start: number;
356
- end: number;
357
- }>;
358
- }
320
+ type SortSpec = Record<string, 'asc' | 'desc'> | readonly SortField[];
359
321
  /**
360
- * Counts for one faceted field.
322
+ * Everything {@link Narsil.query} accepts.
323
+ *
324
+ * Set `term` for keyword search, `vector` for similarity search, and `mode`
325
+ * when you want both. The engine matches nothing for a query carrying neither
326
+ * of them, so page through an index with {@link Narsil.listDocuments}
327
+ * instead.
361
328
  *
362
329
  * @public
363
330
  */
364
- interface FacetResult {
365
- /** This many documents match per value, keyed by value. */
366
- values: Record<string, number>;
367
- /** This is the number of entries in `values`, which is at most the facet's `limit`. */
368
- count: number;
331
+ interface QueryParams {
332
+ /** The engine analyses this text with the index's language module and searches for the terms that result. */
333
+ term?: string;
334
+ /** The engine searches the term in these fields, and in every text field in the schema by default. */
335
+ fields?: string[];
369
336
  /**
370
- * Every count in `values` is at most this far below its true count. A value
371
- * that the engine leaves out of `values` because of `limit` has a true
372
- * count of at most this figure too. Where the engine counts a field on one
373
- * thread, it counts every value exactly, so this figure is the largest
374
- * count that it leaves out. Where it splits the count across worker copies
375
- * or cluster nodes, each of them returns only its own top values, so the
376
- * merged count of a value that is common overall but rare on one of them
377
- * can lack the matches on that one. A bound of 0 means that `values` holds
378
- * every matching value, each with its exact count.
337
+ * The engine scores only the documents that pass this filter. It throws
338
+ * `SEARCH_INVALID_FILTER` for an operand of the wrong shape, and for an
339
+ * operator on a field of another type, such as `startsWith` on a number
340
+ * field.
379
341
  */
380
- errorBound: number;
342
+ filters?: FilterExpression;
343
+ /**
344
+ * The engine multiplies the score from each named field by its weight. It
345
+ * throws `SEARCH_INVALID_FIELD` for any field other than a text field that
346
+ * the schema declares, and `CONFIG_INVALID` for a weight that is not a
347
+ * finite number.
348
+ */
349
+ boost?: Record<string, number>;
350
+ /** The engine gathers term statistics this way, and follows the index's `defaultScoring` where the query sets none. */
351
+ scoring?: ScoringMode;
352
+ /**
353
+ * The engine drops any hit scoring below this value. It ranks with BM25,
354
+ * which has no fixed upper bound, so a floor that suits one index and one
355
+ * query can reject every hit in another.
356
+ */
357
+ minScore?: number;
358
+ /**
359
+ * The engine keeps a document only where the document matches this many
360
+ * query terms, under the same `tolerance` and `prefixLength` that the search
361
+ * uses. One term is enough by default.
362
+ */
363
+ termMatch?: TermMatchPolicy;
364
+ /**
365
+ * The engine matches an indexed term within this edit distance of a query
366
+ * term, so it finds a document for a query with a typo. It counts each
367
+ * Unicode code point as one character. Set an integer from 0 to 10.
368
+ */
369
+ tolerance?: number;
370
+ /**
371
+ * The engine applies `tolerance` only to the indexed terms that share this
372
+ * many leading characters with the query term, or to the terms that start
373
+ * with the whole query term when the query term is shorter. Set an integer
374
+ * from 0 to 1,024; the default is 2.
375
+ */
376
+ prefixLength?: number;
377
+ /**
378
+ * With this set, the engine treats the last query term as an unfinished
379
+ * word, so `secur` matches `security`. The engine matches that last term by
380
+ * its stem or by completing it, while `tolerance` applies only to the earlier
381
+ * terms, which have to match whole. The engine scores completions against a
382
+ * shared document frequency. It also ranks them below whole-word matches.
383
+ * The engine ignores this setting where `exact` is true, and the setting is
384
+ * false by default.
385
+ */
386
+ prefix?: boolean;
387
+ /**
388
+ * With this set, the engine also matches each query word of three or more
389
+ * characters inside the longer words of every field whose type includes
390
+ * `partial`, so a search for `proof` matches a document with `waterproof`
391
+ * in such a field. The engine counts each Unicode code point as one
392
+ * character. It looks for the query word, before stemming, inside each word
393
+ * exactly as that word appears in a document.
394
+ *
395
+ * The engine adds at most 50 such words to a query, which it divides as
396
+ * evenly as it can among the query words. Within the share of each query
397
+ * word, it adds first the words that the most documents contain. It scores
398
+ * a match inside a longer word in proportion to the part of that word that
399
+ * the query word makes up, so it scores such a match below a whole-word
400
+ * match at the same frequency in a field of the same length. With
401
+ * `highlight`, it tags each whole word that contains a query word in a
402
+ * searched `partial` field.
403
+ *
404
+ * The engine applies no `tolerance` to these matches. It ignores this
405
+ * setting where `exact` is true or where the searched fields include no
406
+ * `partial` field. Where the setting is absent, the engine treats it as
407
+ * false, while it throws `CONFIG_INVALID` for any value other than `true`
408
+ * or `false`.
409
+ */
410
+ partialWords?: boolean;
411
+ /**
412
+ * With this set, the engine matches each query term only against an equal
413
+ * indexed term, so it turns off fuzzy matching, prefix completion, and
414
+ * partial-word matching. The engine still stems the query, because the
415
+ * index stores stemmed terms, so a search for `running` matches a document
416
+ * that contains `runs` through the stem that both words share.
417
+ */
418
+ exact?: boolean;
419
+ /**
420
+ * The engine counts the values of these fields across the matching
421
+ * documents, and each entry sets how it cuts and sorts that field's counts.
422
+ * For a field outside the schema, it counts the values that the documents
423
+ * store. It throws `SEARCH_INVALID_FIELD` for a `geopoint` or a vector
424
+ * field.
425
+ */
426
+ facets?: FacetConfig;
427
+ /**
428
+ * The engine sorts the hits by the values of these fields, in place of the
429
+ * relevance ranking. Pass an object keyed by field, or a list of fields in
430
+ * the order that the engine compares them. The engine sorts by a `number`,
431
+ * `boolean`, or `enum` field, by a text field whose type includes
432
+ * `sortable`, or by a list of any of these, which it reduces to one value by
433
+ * the entry's `mode`. It throws `SEARCH_INVALID_FIELD` for a sort on a
434
+ * `geopoint` or a vector field, because neither type has an order. It throws
435
+ * the same code for a sort on any other text field, since the engine keeps
436
+ * up to the first 512 code points of every document's value in memory to
437
+ * sort by a text field. The engine orders hybrid results by fusion, so it
438
+ * throws `SEARCH_INVALID_MODE` for a sort on a hybrid query. It also throws
439
+ * that code for a direction other than `asc` and `desc`.
440
+ */
441
+ sort?: SortSpec;
442
+ /** These settings collapse the hits into groups by field value. */
443
+ group?: GroupConfig;
444
+ /**
445
+ * The query returns this many hits, and 10 by default. Pass 0 to read the
446
+ * match count with no hits. The engine reads a value below 0 as 0. It falls
447
+ * back to the default for `NaN` and for an infinite value, so a page size
448
+ * that you compute from user input never fails the query.
449
+ */
450
+ limit?: number;
451
+ /** The query skips this many hits before returning. A deep offset costs more than a cursor. */
452
+ offset?: number;
453
+ /** This cursor comes from a previous result's `cursor`, and pages without an offset's cost. */
454
+ searchAfter?: string;
455
+ /** These settings name the fields that come back with highlighted snippets. */
456
+ highlight?: HighlightConfig;
457
+ /** These documents take fixed positions, ahead of the ranking. */
458
+ pinned?: Array<{
459
+ docId: string;
460
+ position: number;
461
+ }>;
462
+ /** This picks which engine answers the query. Keyword search runs by default. */
463
+ mode?: SearchMode;
464
+ /** These vector-search inputs are required in `vector` and `hybrid` mode. */
465
+ vector?: VectorQueryConfig;
466
+ /** These settings control how the keyword and vector rankings merge in `hybrid` mode. */
467
+ hybrid?: HybridConfig;
468
+ /**
469
+ * Setting this returns relevance scores on a sorted query. A query that
470
+ * names a sort ranks by sort values alone and computes no scores, so its
471
+ * hits carry none until this restores them. A query without a sort ignores
472
+ * this and always carries scores.
473
+ */
474
+ includeScores?: boolean;
475
+ /** Setting this returns the numbers behind each hit's score, which is what you read when a ranking surprises you. */
476
+ includeScoreComponents?: boolean;
477
+ /** This chooses how much of each stored document comes back, and the whole document by default. */
478
+ document?: DocumentProjection;
381
479
  }
382
480
  /**
383
- * One group of hits, produced by a query that grouped its results.
481
+ * How much of a stored document each hit carries back.
384
482
  *
385
- * @public
386
- */
387
- interface GroupResult {
388
- /** Each grouping field holds this value for the group, keyed by field. */
389
- values: Record<string, unknown>;
390
- /** These hits fall in the group, cut to the query's `maxPerGroup`. */
391
- hits: Array<Hit>;
392
- /** The query's reducer produced this value for the group, and only a query that set `group.reduce` carries it. */
393
- reduced?: unknown;
394
- /** The reducer threw, and this holds its message, so the group carries its hits and no `reduced` value. */
395
- reducerError?: string;
396
- }
397
- /**
398
- * What {@link Narsil.preflight} returns: how many documents a query matches,
399
- * without building or ranking a single hit.
483
+ * Pass `false` when the ids and scores are all you need, and every hit's
484
+ * `document` is then an empty object. Pass `include` to keep named fields
485
+ * alone, or `exclude` to drop named fields and keep the rest; naming both
486
+ * keeps the included fields and then drops the excluded ones from those. Use
487
+ * dots to name a nested field, so `author.name` addresses the `name` inside
488
+ * `author`, and a name that matches no field changes nothing.
489
+ *
490
+ * Drop a vector field on a similarity search, because the engine otherwise
491
+ * reads every hit's vector back out of the index and writes it into the
492
+ * response.
400
493
  *
401
494
  * @public
402
495
  */
403
- interface PreflightResult {
404
- /** The query matches this many documents. */
405
- count: number;
406
- /** `count` holds the exact number of matches where this is true, and a number at or below the true total where it is false, under the rule in {@link QueryResult.countExact}. */
407
- countExact: boolean;
408
- /** The count took this many milliseconds. */
409
- elapsed: number;
410
- /** This turns true when the index's terms came from an older analysis than its language module produces now. */
411
- analysisStale?: boolean;
412
- }
496
+ type DocumentProjection = boolean | {
497
+ /** Names the fields to keep, so every field left out of this list is dropped. */
498
+ include?: string[];
499
+ /** Names the fields to drop, so every other field is kept. */
500
+ exclude?: string[];
501
+ };
413
502
  /**
414
- * What a batch write returns. One bad document never abandons the rest, so
415
- * read both lists.
503
+ * Vector-search inputs passed under `QueryParams.vector`.
504
+ *
505
+ * Supply either a raw `value` array or a `text` string for auto-embedding;
506
+ * passing both throws `EMBEDDING_CONFIG_INVALID`. Result count is governed
507
+ * by the outer query's `limit`, not by any field on this object.
416
508
  *
417
509
  * @public
418
510
  */
419
- interface BatchResult {
420
- /** The engine wrote the documents under these ids. */
421
- succeeded: string[];
422
- /** The engine rejected each of these documents, and each entry carries the error that rejected it. */
423
- failed: Array<{
424
- docId: string;
425
- error: NarsilError;
426
- }>;
511
+ interface VectorQueryConfig {
512
+ /**
513
+ * Name of the schema field that holds the vector to compare against.
514
+ * Must reference a field declared as `vector[N]` in the index schema.
515
+ */
516
+ field: string;
517
+ /**
518
+ * Raw query vector, given as a number array or a `Float32Array`. Length
519
+ * must match the indexed field's dimension or the search rejects the
520
+ * request with `VECTOR_DIMENSION_MISMATCH`.
521
+ */
522
+ value?: number[] | Float32Array;
523
+ /**
524
+ * Text to embed at query time using the index or instance embedding
525
+ * adapter. Mutually exclusive with `value`; requires a configured adapter.
526
+ */
527
+ text?: string;
528
+ /**
529
+ * Score floor applied during ranking. Hits scoring below this value
530
+ * are dropped before `limit` is enforced, so the returned hit count
531
+ * can be smaller than `limit` even when more documents exist. The
532
+ * floor is interpreted in score space for every metric; for
533
+ * `euclidean`, distance is mapped to a similarity score of
534
+ * `1 / (1 + distance)` first. Defaults to no floor.
535
+ */
536
+ similarity?: number;
537
+ /**
538
+ * Similarity metric used for ranking. Defaults to `cosine`. Choose
539
+ * `dotProduct` for raw inner-product scoring on already-normalised
540
+ * vectors and `euclidean` for distance-based ordering.
541
+ */
542
+ metric?: 'cosine' | 'dotProduct' | 'euclidean';
543
+ /**
544
+ * HNSW exploration factor for approximate search. Higher values raise
545
+ * recall at the cost of latency. Ignored while the field is still
546
+ * served by the brute-force backend. Defaults to the engine's built-in
547
+ * value when omitted.
548
+ */
549
+ efSearch?: number;
550
+ /**
551
+ * How many times `limit` a quantized field re-scores against its
552
+ * full-precision vectors before it returns the best `limit`. A value below
553
+ * 1 or one that is not a finite number fails with `CONFIG_INVALID`. The
554
+ * engine ignores this on a field whose quantization is `none`. When a query
555
+ * omits it, the engine takes 3 for `osq1` and `osq2`, 2 for `osq8`, and 2
556
+ * for `osq4` at 1,024 dimensions and above, but 5 for `osq4` below 1,024,
557
+ * where a four-bit code ranks the true neighbours less reliably.
558
+ */
559
+ oversample?: number;
427
560
  }
428
561
  /**
429
- * What {@link Narsil.getStats} returns about one index.
562
+ * How a hybrid query merges its keyword ranking with its vector ranking.
563
+ *
564
+ * `rrf` combines the two by rank alone, which needs no tuning and handles
565
+ * scores that live on different scales. `linear` blends the scores directly,
566
+ * which gives you control once you know what each side's scores look like.
430
567
  *
431
568
  * @public
432
569
  */
433
- interface IndexStats {
434
- /** The index holds this many documents. A closed index reports its count as of the last checkpoint. */
435
- documentCount: number;
436
- /** The index is spread across this many partitions. */
437
- partitionCount: number;
438
- /**
439
- * Formula-based estimate of the index's main-thread memory footprint, summed
440
- * across every partition and vector index for this index. Computed from
441
- * structural counters (document count, posting counts, field-index sizes),
442
- * not measured. Per-object V8 overhead (hidden classes, map transitions,
443
- * GC headers) is not captured here, so this number routinely undershoots
444
- * the real heap by a meaningful factor. Use {@link Narsil.getMemoryStats}
445
- * when sizing host memory.
446
- */
447
- estimatedMemoryBytes: number;
448
- /** The index analyses text with this language module. */
449
- language: string;
450
- /** The index was created with this field layout. */
451
- schema: SchemaDefinition;
570
+ interface HybridConfig {
571
+ /** The engine merges the rankings this way, and by rank fusion by default. It throws `CONFIG_INVALID` for any other value. */
572
+ strategy?: 'rrf' | 'linear';
573
+ /** The engine uses this rank-fusion constant under `rrf` alone, where a larger value reduces the lead of the top ranks. Set a whole number of at least 1, or leave it out for 60. The engine throws `CONFIG_INVALID` for any other value. */
574
+ k?: number;
575
+ /** The engine uses this weight of the vector score, from 0 to 1, under `linear` alone, and 0.5 where you leave it out. It throws `CONFIG_INVALID` for a value outside that range. */
576
+ alpha?: number;
452
577
  }
453
578
  /**
454
- * A summary of one index, as {@link Narsil.listIndexes} reports it.
579
+ * Fields a query counts values for, keyed by field name.
455
580
  *
456
581
  * @public
457
582
  */
458
- interface IndexInfo {
459
- /** The index was created under this name. */
460
- name: string;
461
- /** The index holds this many documents. A closed index reports its count as of the last checkpoint. */
462
- documentCount: number;
463
- /** The index is spread across this many partitions. */
464
- partitionCount: number;
465
- /** The index analyses text with this language module. */
466
- language: string;
467
- /** This turns true when the index's terms came from an older analysis than its language module produces now. */
468
- analysisStale?: boolean;
469
- /** This is `open` in memory, `closed` after release, and `reopen-failed` after five recovery failures. */
470
- state: 'open' | 'closed' | 'reopen-failed';
471
- /** This counts successful loads since this engine started. */
472
- reopenCount: number;
583
+ interface FacetConfig {
584
+ /** Each key names a field the query counts values for. */
585
+ [field: string]: {
586
+ /** The engine returns this many values for the field, in the order of `sort`. An engine returns every value where you leave this out, while a cluster returns 10, or every range of a field that sets `ranges`. */
587
+ limit?: number;
588
+ /** The engine orders the returned values by their count, putting the highest first under `'desc'`, which is the default, and the lowest first under `'asc'`. Under `'asc'`, each cluster node sends the count of up to 10,000 values of the field, so the lowest counts are exact wherever no node holds more values than that. */
589
+ sort?: 'asc' | 'desc';
590
+ /** The engine counts a numeric field's matches into these ranges, with one count for each range. A cluster search counts up to 1,000 ranges on a field and throws `CONFIG_INVALID` for more. */
591
+ ranges?: Array<{
592
+ from: number;
593
+ to: number;
594
+ }>;
595
+ };
473
596
  }
474
597
  /**
475
- * What {@link Narsil.getPartitionStats} reports for one partition, which is
476
- * where you look when an index grows unevenly.
598
+ * How a query collapses its hits into groups.
477
599
  *
478
600
  * @public
479
601
  */
480
- interface PartitionStatsResult {
481
- /** This identifies the partition within its index. */
482
- partitionId: number;
483
- /** The partition holds this many documents. */
484
- documentCount: number;
602
+ interface GroupConfig {
603
+ /** The values of these fields define a group, and several fields group by their combination. */
604
+ fields: string[];
605
+ /** The engine returns this many hits from each group, best first, and one hit where you leave this out. */
606
+ maxPerGroup?: number;
607
+ /** The engine returns this many groups, best first, and every group where you leave this out. */
608
+ limit?: number;
485
609
  /**
486
- * Formula-based estimate of this partition's memory footprint. V8 has no
487
- * per-object heap accounting API, so this number is derived from structural
488
- * counters (document count, posting counts, field-index sizes) and does not
489
- * include per-object overhead. Treat as an estimate, not a measurement.
610
+ * The engine folds every hit of each group into one value, such as a sum or
611
+ * an average, including the hits beyond `maxPerGroup`. In cluster mode the
612
+ * coordinator fetches up to 10,000 hits of each group to fold.
490
613
  */
491
- estimatedMemoryBytes: number;
614
+ reduce?: GroupReducer;
492
615
  }
493
616
  /**
494
- * What {@link Narsil.suggest} returns: the indexed terms that complete a
495
- * prefix, ready to offer as you-type suggestions.
617
+ * Folds the hits of one group into a single value.
496
618
  *
497
619
  * @public
498
620
  */
499
- interface SuggestResult {
500
- /** These completions run most widely used first. */
501
- terms: Array<{
502
- /** This is the completed term, as the index holds it. */
503
- term: string;
504
- /** The term appears in this many documents, which is what ordered the list. */
505
- documentFrequency: number;
506
- }>;
507
- /** The lookup took this many milliseconds. */
508
- elapsed: number;
509
- /** This turns true when the index's terms came from an older analysis than its language module produces now. */
510
- analysisStale?: boolean;
511
- }
621
+ type GroupReducer = {
622
+ /**
623
+ * Folds one hit into the running value.
624
+ *
625
+ * @param accumulator - What the previous call returned, or what
626
+ * `initialValue` produced for the first hit.
627
+ * @param doc - The hit's stored document.
628
+ * @param score - The hit's relevance score.
629
+ * @returns The new running value.
630
+ */
631
+ reducer: (accumulator: unknown, doc: AnyDocument, score: number) => unknown;
632
+ /** Produces the starting value for each group. The engine calls it once per group. */
633
+ initialValue: () => unknown;
634
+ };
512
635
  /**
513
- * The state of one vector field, and what maintenance would cost, as
514
- * {@link Narsil.vectorMaintenanceStatus} reports it.
636
+ * This configuration names the fields that receive highlighted snippets. It
637
+ * also sets how the engine marks those snippets up.
515
638
  *
516
- * Removing a vector leaves a tombstone in the graph, and enough of them slow a
517
- * search down. Read this to decide when compaction earns its cost.
639
+ * The engine analyses the text of each returned field again to tag every word
640
+ * that it matched, so it tags the same words whatever the index's
641
+ * `trackPositions` setting is. It tags a word whose stem equals a query stem,
642
+ * and a word within the query's `tolerance` of a query stem. For a `prefix`
643
+ * query, it tags a word for the last query term only when that word completes
644
+ * the term or shares its stem.
518
645
  *
519
646
  * @public
520
647
  */
521
- interface VectorMaintenanceResult {
522
- /** This describes the named vector field. */
523
- fieldName: string;
524
- /**
525
- * Tombstones make up this share of the graph's nodes, from 0 to 1. Before
526
- * the first build, where {@link VectorMaintenanceResult.graphCount} reads 0,
527
- * the engine divides by the number of stored vectors, because no graph
528
- * exists yet.
529
- */
530
- tombstoneRatio: number;
531
- /** The field holds this many HNSW graphs: 0 before the first build and 1 once a graph exists. */
532
- graphCount: number;
533
- /** This many vectors wait outside the graph, which a search scans one by one. */
534
- bufferSize: number;
535
- /** This stays true while a graph build runs. */
536
- building: boolean;
537
- /** {@link Narsil.compactVectors} would take about this many milliseconds. */
538
- estimatedCompactMs: number;
539
- /** {@link Narsil.optimizeVectors} would take about this many milliseconds. */
540
- estimatedOptimizeMs: number;
648
+ interface HighlightConfig {
649
+ /** The engine highlights these fields. */
650
+ fields: string[];
651
+ /** The engine writes this before each match, and writes `<mark>` by default. */
652
+ preTag?: string;
653
+ /** The engine writes this after each match, and writes `</mark>` by default. */
654
+ postTag?: string;
655
+ /**
656
+ * The engine takes this many characters of the field around the densest run
657
+ * of matches, 200 by default. It returns a snippet longer than that number,
658
+ * because it then adds the opening and closing tags, and an ellipsis at each
659
+ * end where it cuts the field.
660
+ */
661
+ maxSnippetLength?: number;
541
662
  }
663
+
542
664
  /**
543
- * One stored document, as {@link Narsil.listDocuments} returns it.
665
+ * Every code a {@link NarsilError} carries, grouped by the part of the engine
666
+ * that raises it.
544
667
  *
545
- * @typeParam T - Shape of the stored document.
668
+ * Match on the code rather than on the message, because a code is stable and a
669
+ * message is not. The HTTP server maps each code to a status, so a code also
670
+ * tells you whether the caller or the engine is at fault.
546
671
  *
547
672
  * @public
548
673
  */
549
- interface ListedDocument<T = AnyDocument> {
550
- /** The document is stored under this id. */
551
- id: string;
552
- /** This holds the stored document, cut down to what the projection kept. */
553
- document: T;
554
- }
674
+ declare const ErrorCodes: {
675
+ readonly SCHEMA_INVALID_TYPE: "SCHEMA_INVALID_TYPE";
676
+ readonly SCHEMA_DEPTH_EXCEEDED: "SCHEMA_DEPTH_EXCEEDED";
677
+ readonly SCHEMA_INVALID_VECTOR_DIMENSION: "SCHEMA_INVALID_VECTOR_DIMENSION";
678
+ readonly SCHEMA_INVALID_GEOPOINT: "SCHEMA_INVALID_GEOPOINT";
679
+ readonly DOC_NOT_FOUND: "DOC_NOT_FOUND";
680
+ readonly DOC_ALREADY_EXISTS: "DOC_ALREADY_EXISTS";
681
+ readonly DOC_VALIDATION_FAILED: "DOC_VALIDATION_FAILED";
682
+ readonly INDEX_NOT_FOUND: "INDEX_NOT_FOUND";
683
+ readonly INDEX_ALREADY_EXISTS: "INDEX_ALREADY_EXISTS";
684
+ readonly INDEX_ORPHANED: "INDEX_ORPHANED";
685
+ readonly INDEX_REOPEN_CAPACITY_EXHAUSTED: "INDEX_REOPEN_CAPACITY_EXHAUSTED";
686
+ readonly INSTANCE_SHUT_DOWN: "INSTANCE_SHUT_DOWN";
687
+ readonly PARTITION_CORRUPTED: "PARTITION_CORRUPTED";
688
+ readonly PARTITION_REBALANCING_BACKPRESSURE: "PARTITION_REBALANCING_BACKPRESSURE";
689
+ readonly WORKER_CRASHED: "WORKER_CRASHED";
690
+ readonly WORKER_BUSY: "WORKER_BUSY";
691
+ readonly WORKER_TIMEOUT: "WORKER_TIMEOUT";
692
+ readonly WORKER_ACTION_FAILED: "WORKER_ACTION_FAILED";
693
+ readonly PERSISTENCE_SAVE_FAILED: "PERSISTENCE_SAVE_FAILED";
694
+ readonly PERSISTENCE_LOAD_FAILED: "PERSISTENCE_LOAD_FAILED";
695
+ readonly PERSISTENCE_DELETE_FAILED: "PERSISTENCE_DELETE_FAILED";
696
+ readonly PERSISTENCE_CRC_MISMATCH: "PERSISTENCE_CRC_MISMATCH";
697
+ readonly PERSISTENCE_WAL_CORRUPT: "PERSISTENCE_WAL_CORRUPT";
698
+ readonly PERSISTENCE_FSYNC_FAILED: "PERSISTENCE_FSYNC_FAILED";
699
+ readonly SEARCH_INVALID_FIELD: "SEARCH_INVALID_FIELD";
700
+ readonly VECTOR_DIMENSION_MISMATCH: "VECTOR_DIMENSION_MISMATCH";
701
+ readonly SEARCH_INVALID_FILTER: "SEARCH_INVALID_FILTER";
702
+ readonly SEARCH_INVALID_MODE: "SEARCH_INVALID_MODE";
703
+ readonly SEARCH_INVALID_CURSOR: "SEARCH_INVALID_CURSOR";
704
+ readonly SEARCH_RESULT_WINDOW_EXCEEDED: "SEARCH_RESULT_WINDOW_EXCEEDED";
705
+ readonly SEARCH_WORK_CAP_EXCEEDED: "SEARCH_WORK_CAP_EXCEEDED";
706
+ readonly LANGUAGE_NOT_SUPPORTED: "LANGUAGE_NOT_SUPPORTED";
707
+ readonly ENVELOPE_VERSION_MISMATCH: "ENVELOPE_VERSION_MISMATCH";
708
+ readonly ENVELOPE_INVALID_MAGIC: "ENVELOPE_INVALID_MAGIC";
709
+ readonly PARTITION_CAPACITY_EXCEEDED: "PARTITION_CAPACITY_EXCEEDED";
710
+ readonly EMBEDDING_FAILED: "EMBEDDING_FAILED";
711
+ readonly EMBEDDING_DIMENSION_MISMATCH: "EMBEDDING_DIMENSION_MISMATCH";
712
+ readonly EMBEDDING_NO_SOURCE: "EMBEDDING_NO_SOURCE";
713
+ readonly EMBEDDING_CONFIG_INVALID: "EMBEDDING_CONFIG_INVALID";
714
+ readonly DOC_MISSING_REQUIRED_FIELD: "DOC_MISSING_REQUIRED_FIELD";
715
+ readonly CONFIG_INVALID: "CONFIG_INVALID";
716
+ readonly QUERY_ROUTING_FAILED: "QUERY_ROUTING_FAILED";
717
+ readonly QUERY_PARTIAL_FAILURE: "QUERY_PARTIAL_FAILURE";
718
+ readonly QUERY_NODE_TIMEOUT: "QUERY_NODE_TIMEOUT";
719
+ readonly QUERY_NO_ACTIVE_REPLICA: "QUERY_NO_ACTIVE_REPLICA";
720
+ readonly CLUSTER_OPERATION_UNSUPPORTED: "CLUSTER_OPERATION_UNSUPPORTED";
721
+ readonly ALLOCATION_NO_DATA_NODES: "ALLOCATION_NO_DATA_NODES";
722
+ readonly ALLOCATION_INVALID_CONFIG: "ALLOCATION_INVALID_CONFIG";
723
+ readonly ALLOCATION_FAILED: "ALLOCATION_FAILED";
724
+ readonly CONTROLLER_METADATA_INVALID: "CONTROLLER_METADATA_INVALID";
725
+ readonly NODE_BOOTSTRAP_FAILED: "NODE_BOOTSTRAP_FAILED";
726
+ readonly NODE_ALREADY_JOINED: "NODE_ALREADY_JOINED";
727
+ readonly NODE_NOT_JOINED: "NODE_NOT_JOINED";
728
+ readonly NODE_NOT_READY: "NODE_NOT_READY";
729
+ readonly NODE_NOT_CONTROLLER: "NODE_NOT_CONTROLLER";
730
+ readonly COORDINATOR_DEPENDENCY_MISSING: "COORDINATOR_DEPENDENCY_MISSING";
731
+ readonly TRANSPORT_DEPENDENCY_MISSING: "TRANSPORT_DEPENDENCY_MISSING";
732
+ readonly REPLICATION_ENTRY_INVALID: "REPLICATION_ENTRY_INVALID";
733
+ readonly REPLICATION_INSYNC_REMOVAL_FAILED: "REPLICATION_INSYNC_REMOVAL_FAILED";
734
+ readonly REPLICATION_ROLLBACK_FAILED: "REPLICATION_ROLLBACK_FAILED";
735
+ readonly REPLICATION_LOG_FULL: "REPLICATION_LOG_FULL";
736
+ readonly REPLICATION_ENTRY_CORRUPT: "REPLICATION_ENTRY_CORRUPT";
737
+ readonly REPLICATION_SNAPSHOT_CORRUPT: "REPLICATION_SNAPSHOT_CORRUPT";
738
+ readonly REPLICATION_TERM_MISMATCH: "REPLICATION_TERM_MISMATCH";
739
+ readonly REPLICATION_SYNC_FAILED: "REPLICATION_SYNC_FAILED";
740
+ readonly PARTITION_NOT_PRIMARY: "PARTITION_NOT_PRIMARY";
741
+ readonly PARTITION_UNASSIGNED: "PARTITION_UNASSIGNED";
742
+ readonly INSUFFICIENT_REPLICAS: "INSUFFICIENT_REPLICAS";
743
+ readonly SNAPSHOT_SYNC_UNAUTHORIZED: "SNAPSHOT_SYNC_UNAUTHORIZED";
744
+ readonly SNAPSHOT_SYNC_REQUEST_INVALID: "SNAPSHOT_SYNC_REQUEST_INVALID";
745
+ readonly SNAPSHOT_SYNC_INDEX_NOT_FOUND: "SNAPSHOT_SYNC_INDEX_NOT_FOUND";
746
+ readonly SNAPSHOT_SYNC_CAPACITY_EXHAUSTED: "SNAPSHOT_SYNC_CAPACITY_EXHAUSTED";
747
+ readonly SNAPSHOT_SYNC_SNAPSHOT_FAILED: "SNAPSHOT_SYNC_SNAPSHOT_FAILED";
748
+ readonly SNAPSHOT_SYNC_DECODE_FAILED: "SNAPSHOT_SYNC_DECODE_FAILED";
749
+ readonly SNAPSHOT_SYNC_FRAME_INVALID: "SNAPSHOT_SYNC_FRAME_INVALID";
750
+ readonly SNAPSHOT_SYNC_HEADER_INVALID: "SNAPSHOT_SYNC_HEADER_INVALID";
751
+ readonly SNAPSHOT_SYNC_HEADER_MISMATCH: "SNAPSHOT_SYNC_HEADER_MISMATCH";
752
+ readonly SNAPSHOT_SYNC_CHUNK_OUT_OF_ORDER: "SNAPSHOT_SYNC_CHUNK_OUT_OF_ORDER";
753
+ readonly SNAPSHOT_SYNC_CHUNK_OVERFLOW: "SNAPSHOT_SYNC_CHUNK_OVERFLOW";
754
+ readonly SNAPSHOT_SYNC_CHUNK_SIZE_EXCEEDED: "SNAPSHOT_SYNC_CHUNK_SIZE_EXCEEDED";
755
+ readonly SNAPSHOT_SYNC_CHUNK_MISSING: "SNAPSHOT_SYNC_CHUNK_MISSING";
756
+ readonly SNAPSHOT_SYNC_END_MISSING: "SNAPSHOT_SYNC_END_MISSING";
757
+ readonly SNAPSHOT_SYNC_CHECKSUM_MISMATCH: "SNAPSHOT_SYNC_CHECKSUM_MISMATCH";
758
+ readonly SNAPSHOT_SYNC_PRIMARY_ERROR: "SNAPSHOT_SYNC_PRIMARY_ERROR";
759
+ readonly SNAPSHOT_SYNC_NO_TARGETS: "SNAPSHOT_SYNC_NO_TARGETS";
760
+ readonly SNAPSHOT_SYNC_TRANSPORT_FAILED: "SNAPSHOT_SYNC_TRANSPORT_FAILED";
761
+ readonly SNAPSHOT_SYNC_SCHEMA_UNAVAILABLE: "SNAPSHOT_SYNC_SCHEMA_UNAVAILABLE";
762
+ readonly SNAPSHOT_SYNC_RESTORE_FAILED: "SNAPSHOT_SYNC_RESTORE_FAILED";
763
+ readonly SNAPSHOT_SYNC_RESTORE_CLEANUP_FAILED: "SNAPSHOT_SYNC_RESTORE_CLEANUP_FAILED";
764
+ readonly SNAPSHOT_SYNC_TIMEOUT: "SNAPSHOT_SYNC_TIMEOUT";
765
+ readonly SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE: "SNAPSHOT_SYNC_ALLOCATION_UNAVAILABLE";
766
+ readonly SNAPSHOT_SYNC_NOT_ASSIGNED: "SNAPSHOT_SYNC_NOT_ASSIGNED";
767
+ readonly SNAPSHOT_SYNC_ABORTED: "SNAPSHOT_SYNC_ABORTED";
768
+ };
555
769
  /**
556
- * What {@link Narsil.listDocuments} returns: one page of stored documents and
557
- * the cursor that reaches the next one.
558
- *
559
- * @typeParam T - Shape of the stored documents.
770
+ * Any one of the codes in {@link ErrorCodes}, which is the type to narrow on
771
+ * when you branch on a failure the engine raised.
560
772
  *
561
773
  * @public
562
774
  */
563
- interface ListResult<T = AnyDocument> {
564
- /** These documents make up the page, in ascending document-id order. */
565
- documents: Array<ListedDocument<T>>;
566
- /** This opaque cursor reaches the next page, and is null once the listing is finished. */
567
- cursor: string | null;
568
- /** The listing covers this many documents in total, and any filter narrows that count. */
569
- total: number;
570
- /** The engine spent this many milliseconds building the page. */
571
- elapsed: number;
572
- }
573
-
775
+ type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
574
776
  /**
575
- * Compares one field against a value. Setting several keys narrows the match,
576
- * because a document has to satisfy all of them.
777
+ * Every code the HTTP layer raises for a failure that arises before or around
778
+ * the engine call, such as parsing the body, enforcing a limit, or routing.
779
+ *
780
+ * The engine raises none of these, so a failure carrying one of them says the
781
+ * request never reached the engine.
577
782
  *
578
783
  * @public
579
784
  */
580
- type ComparisonFilter = {
581
- /** This matches a field equal to the value, or a list field where one element equals it. */
582
- eq?: number | string | boolean;
583
- /**
584
- * This matches a document that holds the field with a value other than this
585
- * one, or a list field where every element differs from it, which an empty
586
- * list does.
587
- */
588
- ne?: number | string | boolean;
589
- /** This matches a field greater than the number. */
590
- gt?: number;
591
- /** This matches a field less than the number. */
592
- lt?: number;
593
- /** This matches a field greater than or equal to the number. */
594
- gte?: number;
595
- /** This matches a field less than or equal to the number. */
596
- lte?: number;
597
- /** This matches a field inside the inclusive range, given as `[low, high]`. */
598
- between?: [number, number];
785
+ declare const ServerErrorCodes: {
786
+ readonly INVALID_REQUEST: "INVALID_REQUEST";
787
+ readonly INVALID_JSON: "INVALID_JSON";
788
+ readonly EMPTY_BODY: "EMPTY_BODY";
789
+ readonly PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE";
790
+ readonly NOT_FOUND: "NOT_FOUND";
791
+ readonly TASK_NOT_FOUND: "TASK_NOT_FOUND";
792
+ readonly TASK_NOT_CANCELLABLE: "TASK_NOT_CANCELLABLE";
793
+ readonly TASK_OWNED_BY_ANOTHER_INSTANCE: "TASK_OWNED_BY_ANOTHER_INSTANCE";
794
+ readonly TASK_INTERRUPTED: "TASK_INTERRUPTED";
795
+ readonly TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS";
796
+ readonly HOOK_ERROR: "HOOK_ERROR";
797
+ readonly INTERNAL_ERROR: "INTERNAL_ERROR";
599
798
  };
600
799
  /**
601
- * The engine tests a text field against this filter for equality, for a text
602
- * range, or with the set and prefix tests.
603
- *
604
- * The engine compares each element of a `string[]`, `verbatim[]`, or `enum[]` field, so
605
- * it matches a document with `eq`, `in`, `startsWith`, `endsWith`, or
606
- * `contains` where one element passes the test, while it matches a document
607
- * with `ne` or `nin` where every element differs from the values.
608
- *
609
- * The engine applies the range tests `gt`, `gte`, `lt`, `lte`, and `between`
610
- * to a single `string` or `verbatim` field whose type includes `sortable`,
611
- * while it throws `SEARCH_INVALID_FILTER` for a text range on any other field.
612
- * It orders the values as a sort on the field does, comparing their case folds
613
- * first and their raw code points only where the folds are equal.
614
- *
615
- * A pattern field is a `verbatim` or `verbatim[]` field, or a `string` or
616
- * `string[]` field whose type includes `pattern`. On such a field, the engine
617
- * answers `eq`, `ne`, `in`, `nin`, `startsWith`, `endsWith`, and `contains`
618
- * from the field's pattern index. `caseFold` applies to each of those tests.
619
- * On an `enum` or `enum[]` field, the engine answers `eq`, `ne`, `in`, `nin`,
620
- * `startsWith`, and `endsWith` from the field's index of values. It throws
621
- * `SEARCH_INVALID_FILTER` for `eq`, `ne`, `in`, `nin`, `startsWith`, or
622
- * `endsWith` on a `string` or `string[]` field whose type leaves out
623
- * `pattern`, because such a field keeps no index of whole values.
624
- * The engine compares the code points of each value as stored, with no
625
- * Unicode normalisation. It throws `SEARCH_INVALID_FILTER` for `contains` or
626
- * `caseFold` on any other field, for a test text longer than 1,024 code points
627
- * on a pattern field, and for a `caseFold` test whose text has a case fold
628
- * longer than 2,048 code points.
800
+ * Any one of the codes in {@link ServerErrorCodes}.
629
801
  *
630
802
  * @public
631
803
  */
632
- type StringFilter = Pick<ComparisonFilter, 'eq' | 'ne'> & {
633
- /** This matches a value that comes after the text in sort order. */
634
- gt?: string;
635
- /** This matches a value that comes before the text in sort order. */
636
- lt?: string;
637
- /** This matches a value equal to the text or after it in sort order. */
638
- gte?: string;
639
- /** This matches a value equal to the text or before it in sort order. */
640
- lte?: string;
641
- /** This matches a value inside the inclusive range in sort order, given as `[low, high]`. */
642
- between?: [string, string];
643
- /** This matches a field equal to any value in the list, which holds at most 65,536 values. */
644
- in?: string[];
645
- /** This matches a field that differs from every value in the list, which holds at most 65,536 values. */
646
- nin?: string[];
647
- /** This matches a field starting with the text. */
648
- startsWith?: string;
649
- /** This matches a field ending with the text. */
650
- endsWith?: string;
651
- /** This matches a pattern field whose value holds the text anywhere in it. */
652
- contains?: string;
653
- /**
654
- * When this is true, the engine compares the case fold of each value of a
655
- * pattern field with the case fold of the text, so a test for `error` matches
656
- * `ERROR`. It applies to `eq`, `ne`, `in`, `nin`, `startsWith`, `endsWith`,
657
- * and `contains`.
658
- */
659
- caseFold?: boolean;
660
- };
804
+ type ServerErrorCode = (typeof ServerErrorCodes)[keyof typeof ServerErrorCodes];
661
805
  /**
662
- * Tests the contents of a list field, which is a `number[]`, `boolean[]`,
663
- * `enum[]`, `verbatim[]`, or `string[]` field.
806
+ * Every code that the caller's own side raises, where a request never reaches
807
+ * a server, or where the caller cannot read the answer.
664
808
  *
665
- * The engine throws `SEARCH_INVALID_FILTER` for these tests on any other
666
- * field, for a value whose type differs from the type of the list's elements,
667
- * and for `containsAll` or `matchesAny` on a `string[]` field whose type leaves
668
- * out `pattern`.
809
+ * The HTTP client raises the first five. The React bindings raise
810
+ * `CLIENT_UNEXPECTED_ERROR` alone, which reports a fault inside a hook. No
811
+ * server sends any of them, so a failure under one means the exchange broke
812
+ * before the operation ran. `STATUS_BY_CODE` in `src/server/errors.ts`
813
+ * therefore maps none of them, because no request can arrive under one and no
814
+ * HTTP status belongs to one.
669
815
  *
670
816
  * @public
671
817
  */
672
- type ArrayFilter = {
673
- /** This matches a list holding every one of the values, at most 65,536 of them, so an empty set of values matches every list. */
674
- containsAll?: (string | number | boolean)[];
675
- /** This matches a list holding at least one of the values, at most 65,536 of them. */
676
- matchesAny?: (string | number | boolean)[];
677
- /** This compares the number of elements in the list. */
678
- size?: ComparisonFilter;
818
+ declare const ClientErrorCodes: {
819
+ readonly CLIENT_CONNECTION_FAILED: "CLIENT_CONNECTION_FAILED";
820
+ readonly CLIENT_REQUEST_TIMEOUT: "CLIENT_REQUEST_TIMEOUT";
821
+ readonly CLIENT_REQUEST_ABORTED: "CLIENT_REQUEST_ABORTED";
822
+ readonly CLIENT_INVALID_RESPONSE: "CLIENT_INVALID_RESPONSE";
823
+ readonly CLIENT_TASK_TIMEOUT: "CLIENT_TASK_TIMEOUT";
824
+ readonly CLIENT_UNEXPECTED_ERROR: "CLIENT_UNEXPECTED_ERROR";
679
825
  };
680
826
  /**
681
- * Tests whether a field carries a value at all, without looking at what it
682
- * holds.
827
+ * Any one of the codes in {@link ClientErrorCodes}.
683
828
  *
684
829
  * @public
685
830
  */
686
- type PresenceFilter = {
687
- /** This matches a document where the field holds a value other than null. */
688
- exists?: boolean;
689
- /** This matches a document where the field is absent or null. */
690
- notExists?: boolean;
691
- /** This matches a document where the field is absent, null, an empty string, or an empty list. */
692
- isEmpty?: boolean;
693
- /** This matches a document where the field holds a value other than an empty string or an empty list. */
694
- isNotEmpty?: boolean;
695
- };
831
+ type ClientErrorCode = (typeof ClientErrorCodes)[keyof typeof ClientErrorCodes];
696
832
  /**
697
- * Matches a `geopoint` field against a circle drawn on the earth.
833
+ * Every code a {@link NarsilError} can carry: one the engine raised, one the
834
+ * HTTP layer raised, one the client raised, or any other string.
835
+ *
836
+ * The last arm exists for a server's `onRequest` hook, which rejects a request
837
+ * under a code of its own, such as `UNAUTHORIZED`, and the client then passes
838
+ * that code through unchanged. An editor still completes every code Narsil
839
+ * defines.
698
840
  *
699
841
  * @public
700
842
  */
701
- type GeoRadiusFilter = {
702
- /** The engine tests the field against this circle. */
703
- radius: {
704
- /** The centre has this latitude, in degrees from -90 to 90. For any other value, the engine throws `SEARCH_INVALID_FILTER`. */
705
- lat: number;
706
- /** The centre has this longitude, in degrees from -180 to 180. For any other value, the engine throws `SEARCH_INVALID_FILTER`. */
707
- lon: number;
708
- /** The circle has this radius, measured in `unit`. */
709
- distance: number;
710
- /**
711
- * The engine measures `distance` in kilometres under `'km'`, in miles
712
- * under `'mi'`, and in metres under `'m'`. For any other unit, it throws
713
- * `SEARCH_INVALID_FILTER`.
714
- */
715
- unit: 'km' | 'mi' | 'm';
716
- /** The engine matches the points inside the circle while this is true, which is the default, and the points outside it while this is false. */
717
- inside?: boolean;
718
- /**
719
- * The engine measures each distance with the Haversine formula on a sphere
720
- * by default, and with Vincenty's formula on the WGS-84 ellipsoid while
721
- * this is true. Vincenty's formula iterates, so the engine takes longer
722
- * to compute each distance. The two formulas differ by less than 0.3% under about
723
- * 100 km, while across a continent they can differ by up to 0.5%.
724
- */
725
- highPrecision?: boolean;
726
- };
727
- };
843
+ type NarsilErrorCode = ErrorCode | ServerErrorCode | ClientErrorCode | (string & {});
728
844
  /**
729
- * Matches a `geopoint` field against a polygon drawn on the earth.
845
+ * The error every part of the engine throws.
846
+ *
847
+ * Catch it, read {@link NarsilError.code} to decide what to do, and read
848
+ * {@link NarsilError.details} for the values that caused it. The engine lets
849
+ * this error propagate unchanged, so the code you catch is the code the
850
+ * failing operation raised.
730
851
  *
731
852
  * @public
732
853
  */
733
- type GeoPolygonFilter = {
734
- /** The engine tests the field against this polygon. */
735
- polygon: {
736
- /**
737
- * The engine closes the ring through these corners in the order that you
738
- * list them, so you can leave the first point off the end. It throws
739
- * `SEARCH_INVALID_FILTER` for a ring of fewer than three corners, which
740
- * encloses no area, and for a corner whose latitude is outside -90 to 90
741
- * or whose longitude is outside -180 to 180. It also throws that error for
742
- * a ring of more than 65,536 corners. List the corners counter-clockwise
743
- * around the area, as in an exterior ring of GeoJSON. Where the corners
744
- * span 180 degrees of longitude or more, the engine matches the area on
745
- * the left of that path, so a ring listed that way can cross the
746
- * antimeridian or span more than half the globe.
747
- */
748
- points: Array<{
749
- lat: number;
750
- lon: number;
751
- }>;
752
- /** The engine matches the points inside the polygon while this is true, which is the default, and the points outside it while this is false. */
753
- inside?: boolean;
754
- };
755
- };
854
+ declare class NarsilError extends Error {
855
+ /** This says which failure it is. */
856
+ readonly code: NarsilErrorCode;
857
+ /** This carries the values behind the failure, such as the field, index, or limit involved. It is empty when the code says everything. */
858
+ readonly details: Record<string, unknown>;
859
+ /**
860
+ * Builds an error carrying a code the caller can branch on.
861
+ *
862
+ * @param code - The failure this error reports, from {@link ErrorCodes} for
863
+ * an engine failure, {@link ServerErrorCodes} for one the HTTP layer raised,
864
+ * or {@link ClientErrorCodes} for one that stopped a request from reaching a
865
+ * server.
866
+ * @param message - Plain description, for a log or a person.
867
+ * @param details - Values behind the failure, which reach
868
+ * {@link NarsilError.details}.
869
+ */
870
+ constructor(code: NarsilErrorCode, message: string, details?: Record<string, unknown>);
871
+ }
756
872
  /**
757
- * Either shape a `geopoint` field is matched against.
873
+ * Reports whether a caught value is a {@link NarsilError}, including one that
874
+ * another entry point of this package raises.
875
+ *
876
+ * Each browser bundle holds its own copy of the class, so an `instanceof` test
877
+ * inside the React bindings returns false for an error that the client raises,
878
+ * although that error still holds its code. This function tests for the marker
879
+ * that the constructor writes, so it returns true for an error from any entry
880
+ * point.
881
+ *
882
+ * @param value - The value that a `catch` block receives.
883
+ * @returns True where the value holds a code in {@link NarsilError.code}.
758
884
  *
759
885
  * @public
760
886
  */
761
- type GeoFilter = GeoRadiusFilter | GeoPolygonFilter;
887
+ declare function isNarsilError(value: unknown): value is NarsilError;
888
+
762
889
  /**
763
- * The filter one field accepts. Which of these forms applies follows from the
764
- * field's declared type.
890
+ * This map lists every event that the engine emits, with the payload of each
891
+ * one.
892
+ *
893
+ * {@link Narsil.on} registers a listener under a key from this map, so that the
894
+ * engine can pass the matching payload to that listener. Most of these events
895
+ * report work that the engine does in the background, away from the call that
896
+ * started it, so a failure that such an event reports never rejects a promise
897
+ * that you are holding.
765
898
  *
766
899
  * @public
767
900
  */
768
- type FieldFilter = ComparisonFilter | StringFilter | ArrayFilter | PresenceFilter | GeoFilter;
901
+ type NarsilEventMap = {
902
+ /**
903
+ * The engine emits this event after a call changes what a read returns from
904
+ * an index. The calls are the inserts, updates, and removals, singly or in a
905
+ * batch, together with `clear`, `restore`, `rebalance`, `rebuildAnalysis`,
906
+ * `compactVectors`, `optimizeVectors`, `createIndex`, and `dropIndex`. The
907
+ * engine emits the event after a call that throws as well, because a call can
908
+ * change the index before it throws. It also emits the event after it reloads
909
+ * an index, once a shared invalidation adapter reports that another instance
910
+ * saved that index. The engine emits one event per batch, however many
911
+ * documents the batch holds.
912
+ *
913
+ * The engine emits the event once a query can see the change, which means
914
+ * after every worker copy applies the change and after any rebalance in
915
+ * progress replays it. A query that a listener sends from its handler
916
+ * therefore returns results that include the change. The engine waits for the
917
+ * change to become visible only while a listener is registered for the event.
918
+ */
919
+ write: {
920
+ /** This is the name of the index that changed. */
921
+ indexName: string;
922
+ };
923
+ /**
924
+ * A worker thread exited unexpectedly. The pool removes the thread and fails
925
+ * the requests that the thread was serving with `WORKER_CRASHED`. The engine
926
+ * then sends each of those queries again on the main thread, so the caller
927
+ * receives a result with no error. The remaining workers keep serving
928
+ * queries, because each of them holds a full worker copy of every promoted
929
+ * index.
930
+ *
931
+ * After a delay, the engine spawns a replacement and loads every copy onto
932
+ * it before it sends queries to the replacement. Once no worker is left, the
933
+ * main thread serves every query, because it holds every document. The first
934
+ * request after the delay then starts a new pool. The delay starts at one
935
+ * second and doubles up to a minute while replacements keep failing.
936
+ */
937
+ workerCrash: {
938
+ /** This is the id of the worker that exited. */
939
+ workerId: number;
940
+ /** These are the indexes that the worker held. */
941
+ indexNames: string[];
942
+ /** This is the error that ended the worker. */
943
+ error: Error;
944
+ };
945
+ /** An index gained worker copies, either because it reached the copy
946
+ * threshold or because a request loaded its copies again after an idle
947
+ * spell. */
948
+ workerPromote: {
949
+ /** This is the number of workers in the pool. */
950
+ workerCount: number;
951
+ /** This is the reason that the index gained its copies, such as the copy
952
+ * threshold that it reached. */
953
+ reason: string;
954
+ };
955
+ /** An index could not gain worker copies, so the main thread keeps serving
956
+ * its queries. */
957
+ workerPromoteFailure: {
958
+ /** This is the reason that the engine tried to load the copies. */
959
+ reason: string;
960
+ /** This is the error that stopped the load. */
961
+ error: Error;
962
+ /** This is true when the engine will try again at a later threshold. */
963
+ retryable: boolean;
964
+ };
965
+ /** An index finished spreading its documents across a new number of
966
+ * partitions. */
967
+ partitionRebalance: {
968
+ /** This is the name of the index that the engine rebalanced. */
969
+ indexName: string;
970
+ /** This is the number of partitions before the rebalance. */
971
+ oldCount: number;
972
+ /** This is the number of partitions after the rebalance. */
973
+ newCount: number;
974
+ };
975
+ /**
976
+ * An index passed its watermark, so its partitions are close to their
977
+ * capacity. On a cluster node, the engine reports the document count and the
978
+ * capacity of the one partition that `partitionId` identifies, which this
979
+ * node leads.
980
+ */
981
+ partitionWatermark: {
982
+ /** This is the name of the index that crossed the mark. */
983
+ indexName: string;
984
+ /** This is the number of documents in the index, or in the partition on a
985
+ * cluster node. */
986
+ documentCount: number;
987
+ /** This is the number of documents that the current partitions of the
988
+ * index can hold, or that the partition can hold on a cluster node. */
989
+ capacity: number;
990
+ /** This is the number of partitions in the index. */
991
+ partitionCount: number;
992
+ /** On a cluster node, this is the partition that crossed the mark, while
993
+ * outside a cluster the field is absent. */
994
+ partitionId?: number;
995
+ };
996
+ /**
997
+ * The process uses nine tenths of its heap, as measured during a write or a
998
+ * load, so the next large index can end the process with an out-of-memory
999
+ * error.
1000
+ *
1001
+ * Where `--max-old-space-size` or `--max-old-space-size-percentage` sets a
1002
+ * limit, on the command line or in `NODE_OPTIONS`, the engine measures the
1003
+ * used bytes against that limit, which `heapLimit` then reports. Where
1004
+ * neither flag sets a limit, the engine measures the share of the heap that
1005
+ * V8 reports as no longer available. V8 reports a limit about 192 MB above
1006
+ * the true ceiling, so a process on a default heap below about 2 GB can end before
1007
+ * this event fires.
1008
+ *
1009
+ * The engine emits the event once per crossing, then arms it again once the
1010
+ * free share of the heap recovers to two tenths. Raise the limit with
1011
+ * `--max-old-space-size-percentage` or `--max-old-space-size`, or close an
1012
+ * idle index; see {@link ProcessMemoryReport.heapLimit}.
1013
+ */
1014
+ heapPressure: {
1015
+ /** This is the index that a write or a load was touching when the heap
1016
+ * reached that point. */
1017
+ indexName: string;
1018
+ /** This is the heap that the process uses, in bytes. */
1019
+ heapUsed: number;
1020
+ /** This is the size in bytes that the heap can grow to. */
1021
+ heapLimit: number;
1022
+ /** This is the estimate of the bytes that the index holds. */
1023
+ estimatedMemoryBytes: number;
1024
+ };
1025
+ /** The engine started, completed, or failed a rebuild that brings the terms
1026
+ * of an index up to the current analysis of its language module. */
1027
+ analysisRebuild: {
1028
+ /** This is the name of the index under rebuild. */
1029
+ indexName: string;
1030
+ /** This is the stage that the rebuild has reached. */
1031
+ status: 'started' | 'completed' | 'failed';
1032
+ /** This is the number of partitions that the rebuild has covered so far. */
1033
+ partitionsRebuilt: number;
1034
+ /** This is the number of partitions that the rebuild covers in total. */
1035
+ partitionCount: number;
1036
+ /** This is the error that stopped the rebuild, which only a `failed`
1037
+ * status includes. */
1038
+ error?: Error;
1039
+ };
1040
+ /** The engine started, completed, or failed the rewrite of an index that you
1041
+ * saved with an earlier release. The engine rewrites the index as it
1042
+ * recovers it, before it serves the index's first request. Because the
1043
+ * engine writes the new manifest last, the earlier files stay intact through
1044
+ * a failed rewrite. The engine then starts the rewrite again the next time
1045
+ * it recovers the index. Without index lifecycle settings, the engine
1046
+ * recovers every index before {@link createNarsil} returns, so pass the
1047
+ * listener in the `events` setting of {@link createNarsil}. */
1048
+ layoutUpgrade: {
1049
+ /** This is the name of the index under rewrite. */
1050
+ indexName: string;
1051
+ /** This is the stage that the rewrite has reached. */
1052
+ status: 'started' | 'completed' | 'failed';
1053
+ /** This is `manifest` where the earlier index is a segment manifest with
1054
+ * its files, and `bundle` where it is a single snapshot bundle. */
1055
+ layout: 'manifest' | 'bundle';
1056
+ /** This is the error that stopped the rewrite, which only a `failed`
1057
+ * status includes. */
1058
+ error?: Error;
1059
+ };
1060
+ /** A write to the write-ahead log or a checkpoint failed, so the writes since
1061
+ * the last checkpoint are at risk. */
1062
+ durabilityError: {
1063
+ /** This is the error that the durability layer threw. */
1064
+ error: Error;
1065
+ };
1066
+ /** The invalidation channel failed, so this instance can serve partitions
1067
+ * that another instance has since changed. */
1068
+ invalidationError: {
1069
+ /** This is the error that the adapter threw. */
1070
+ error: Error;
1071
+ };
1072
+ };
769
1073
  /**
770
- * The filter that a query runs, built from per-field tests and the boolean
771
- * operators that combine them.
772
- *
773
- * The engine answers every test from an index that it keeps for the field. It
774
- * applies the filter before scoring, so a narrow filter makes a search
775
- * cheaper. A filter nests at most 30 levels deep and holds at most 1,024
776
- * tests, counted across every field and nested expression. Past either limit,
777
- * the engine throws `SEARCH_INVALID_FILTER`, while it throws
778
- * `SEARCH_INVALID_FIELD` for a test on a field that the schema leaves out.
1074
+ * This map holds a listener for any event of {@link NarsilEventMap}, so that
1075
+ * the engine can register each listener before it starts.
779
1076
  *
780
1077
  * @public
781
1078
  */
782
- type FilterExpression = {
783
- /** These per-field tests are keyed by field name, and a document has to satisfy every entry. */
784
- fields?: Record<string, FieldFilter>;
785
- /** Every nested expression has to match. */
786
- and?: FilterExpression[];
787
- /** At least one nested expression has to match. */
788
- or?: FilterExpression[];
789
- /** The nested expression has to fail. */
790
- not?: FilterExpression;
1079
+ type NarsilEventHandlers = {
1080
+ [K in keyof NarsilEventMap]?: (payload: NarsilEventMap[K]) => void;
791
1081
  };
792
1082
 
793
1083
  /**
794
- * Everything {@link Narsil.suggest} accepts.
1084
+ * What {@link Narsil.query} returns: the ranked hits and everything the query
1085
+ * asked for alongside them.
1086
+ *
1087
+ * @typeParam T - Shape of the stored documents, which flows through to each
1088
+ * hit's `document`.
795
1089
  *
796
1090
  * @public
797
1091
  */
798
- interface SuggestParams {
799
- /** The returned terms complete this text. */
800
- prefix: string;
801
- /** The lookup returns this many completions, most widely used first, and 10 by default. */
802
- limit?: number;
1092
+ interface QueryResult<T = AnyDocument> {
1093
+ /** These documents match, cut to the query's `limit`, best score first or in the query's sort order. */
1094
+ hits: Array<Hit<T>>;
1095
+ /**
1096
+ * This many documents match in total, before the engine applies `limit`
1097
+ * and `offset`. For a keyword search the engine counts every match. For a
1098
+ * vector search it counts every vector that passes the query's filter and
1099
+ * its `similarity` floor, which is every vector in the field where the query
1100
+ * sets neither of them. The engine also counts each pinned document that
1101
+ * the query does not match, since it places that document among the hits.
1102
+ */
1103
+ count: number;
1104
+ /**
1105
+ * `count` holds the exact number of matches where this is true, and a
1106
+ * number at or below the true total where it is false. Where a query sets a
1107
+ * `similarity` floor on a vector field that holds a graph, the engine
1108
+ * counts only the vectors that it fetches, because it compares the query
1109
+ * with only a fraction of that field's vectors. For a hybrid query the
1110
+ * engine fuses two rankings, so the count is exact only where each of those
1111
+ * rankings contains every matching document. Where you pin a document from
1112
+ * outside the hits that the engine fetches, the engine reports true only
1113
+ * where it holds every match of the query, because only then can it test
1114
+ * whether the query matches that document.
1115
+ */
1116
+ countExact: boolean;
1117
+ /** The engine spent this many milliseconds on the search. */
1118
+ elapsed: number;
1119
+ /** This opaque cursor reaches the next page. Pass it back as `searchAfter`. */
1120
+ cursor?: string;
1121
+ /** These facet counts are keyed by the field each facet was asked for. */
1122
+ facets?: Record<string, FacetResult>;
1123
+ /** These groups arrive when the query asked for grouping. */
1124
+ groups?: GroupResult[];
1125
+ /** This turns true when the index's terms came from an older analysis than its language module produces now. */
1126
+ analysisStale?: boolean;
1127
+ /** The search read this much of the index, which is how you spot a degraded answer. */
1128
+ coverage: QueryCoverage;
803
1129
  }
804
1130
  /**
805
- * Everything {@link Narsil.listDocuments} accepts.
1131
+ * How much of an index one search read, which is what you check before you
1132
+ * trust a count or a ranking.
806
1133
  *
807
- * {@link Narsil.listDocuments} reads the stored documents without ranking them,
808
- * which is how you page through a whole index. Leave `cursor` out to start at
809
- * the first document, then pass back the cursor each result carries until it
810
- * comes back null.
1134
+ * An embedded engine reads every partition it holds, so `queriedPartitions`
1135
+ * matches `totalPartitions` there and the other two stay zero. A search across
1136
+ * a cluster may find a node slow or unreachable, which it counts here while
1137
+ * still returning the hits the other nodes gave it.
811
1138
  *
812
- * A cursor belongs to the sort it was made under, so pass the same `sort` back
813
- * with it. Changing `sort` invalidates the cursor, and the engine then throws
814
- * `SEARCH_INVALID_CURSOR` rather than returning a page from the wrong order.
1139
+ * @public
1140
+ */
1141
+ interface QueryCoverage {
1142
+ /** The search should have read this many partitions. */
1143
+ totalPartitions: number;
1144
+ /** The search read this many of them. */
1145
+ queriedPartitions: number;
1146
+ /** The search counts a partition here when the node holding it took longer than the query allowed. */
1147
+ timedOutPartitions: number;
1148
+ /** The search counts a partition here when the node holding it returned an error, and when no active replica served it. */
1149
+ failedPartitions: number;
1150
+ }
1151
+ /**
1152
+ * One matching document, with the score that ranked it.
1153
+ *
1154
+ * @typeParam T - Shape of the stored document.
815
1155
  *
816
1156
  * @public
817
1157
  */
818
- interface ListParams {
819
- /**
820
- * This cursor comes from a previous result, and continues where it stopped.
821
- * The engine ties a cursor to the sort that produced it, and it throws
822
- * `SEARCH_INVALID_CURSOR` for a cursor sent back under a different sort.
823
- */
824
- cursor?: string;
825
- /** The page carries this many documents, and 10 by default. The engine raises a value below one to one. */
826
- limit?: number;
827
- /** This narrows the listing to the documents the filter accepts. */
828
- filters?: FilterExpression;
1158
+ interface Hit<T = AnyDocument> {
1159
+ /** The document is stored under this id. */
1160
+ id: string;
829
1161
  /**
830
- * This orders the listing by field value rather than by document id, and the
831
- * engine applies the fields in the order they are listed. Pass an object
832
- * keyed by field, or a list of fields in the order they apply. It breaks a
833
- * tie on document id, and it sorts by at most eight fields. The engine uses
834
- * document-id order when you leave this out.
835
- *
836
- * The engine reads every document the listing covers to build a sorted page,
837
- * so a sorted listing costs more than the default order on a large index.
1162
+ * This score ranked the hit. Scores compare within one result set, never
1163
+ * across searches. A sorted query ranks by sort values instead and carries
1164
+ * no score unless it sets `includeScores`.
838
1165
  */
839
- sort?: SortSpec;
840
- /** This chooses how much of each stored document comes back, and the whole document by default. */
841
- document?: DocumentProjection;
1166
+ score?: number;
1167
+ /** This is the stored document. */
1168
+ document: T;
1169
+ /** These are the parts the score was built from, which arrive when the query asked for them. */
1170
+ scoreComponents?: ScoreComponents;
1171
+ /** These highlighted snippets are keyed by field, and arrive when the query asked for highlighting. */
1172
+ highlights?: Record<string, HighlightMatch>;
842
1173
  }
843
-
844
1174
  /**
845
- * Which engine answers a query: keyword matching, vector similarity, or both
846
- * merged into one ranking.
1175
+ * The numbers BM25 combined into one hit's score, which is what you read when
1176
+ * a ranking surprises you.
847
1177
  *
848
1178
  * @public
849
1179
  */
850
- type SearchMode = 'fulltext' | 'vector' | 'hybrid';
1180
+ interface ScoreComponents {
1181
+ /**
1182
+ * Each query term appears in one field of this document this many times,
1183
+ * under a key of `field:term`. A term that appears in two fields therefore
1184
+ * has two entries here, while the keys of {@link ScoreComponents.idf} hold
1185
+ * the bare term.
1186
+ */
1187
+ termFrequencies: Record<string, number>;
1188
+ /** Each field the query searched holds this many tokens, keyed by field. */
1189
+ fieldLengths: Record<string, number>;
1190
+ /** Each query term carries this inverse document frequency, which is what makes a rare term count for more. */
1191
+ idf: Record<string, number>;
1192
+ }
851
1193
  /**
852
- * How many of a query's terms a document has to carry. `all` demands every
853
- * term, `any` accepts one, and a number demands that many.
1194
+ * A highlighted field, ready to render.
854
1195
  *
855
1196
  * @public
856
1197
  */
857
- type TermMatchPolicy = 'all' | 'any' | number;
1198
+ interface HighlightMatch {
1199
+ /** This is the field's text around the match, wrapped in the query's tags. */
1200
+ snippet: string;
1201
+ /**
1202
+ * Each range gives the start and the end of one match in the original field,
1203
+ * so you can render the highlight yourself. Where the snippet covers part of
1204
+ * the field alone, the engine lists only the matches that the snippet
1205
+ * displays. Every offset is a character count from the start of the original
1206
+ * field, whatever the snippet contains.
1207
+ */
1208
+ positions: Array<{
1209
+ start: number;
1210
+ end: number;
1211
+ }>;
1212
+ }
858
1213
  /**
859
- * One field of a sort, and the direction that field orders by.
1214
+ * Counts for one faceted field.
860
1215
  *
861
1216
  * @public
862
1217
  */
863
- interface SortField {
864
- /** The field the engine reads, which may name a nested field with dots. */
865
- field: string;
866
- /** Ascending or descending order for this field. */
867
- direction: 'asc' | 'desc';
1218
+ interface FacetResult {
1219
+ /** This many documents match per value, keyed by value. */
1220
+ values: Record<string, number>;
1221
+ /** This is the number of entries in `values`, which is at most the facet's `limit`. */
1222
+ count: number;
868
1223
  /**
869
- * Where a document's field holds a list, the engine sorts that document by
870
- * one value from the list: the smallest under `'min'`, the largest under
871
- * `'max'`, and the mean or the median of the list's numbers under `'avg'`
872
- * or `'median'`. Where you leave this out, the engine uses `'min'` for an
873
- * ascending sort and `'max'` for a descending one. It sorts by a field that
874
- * holds a single value as that value stands, under every mode. It throws
875
- * `SEARCH_INVALID_FIELD` for `'avg'` or `'median'` on a field that the
876
- * schema declares as anything other than `number` or `number[]`.
1224
+ * Every count in `values` is at most this far below its true count. A value
1225
+ * that the engine leaves out of `values` because of `limit` has a true
1226
+ * count of at most this figure too. Where the engine counts a field on one
1227
+ * thread, it counts every value exactly, so this figure is the largest
1228
+ * count that it leaves out. Where it splits the count across worker copies
1229
+ * or cluster nodes, each of them returns only its own top values, so the
1230
+ * merged count of a value that is common overall but rare on one of them
1231
+ * can lack the matches on that one. A bound of 0 means that `values` holds
1232
+ * every matching value, each with its exact count.
877
1233
  */
878
- mode?: 'min' | 'max' | 'avg' | 'median';
1234
+ errorBound: number;
879
1235
  }
880
1236
  /**
881
- * A sort, either as an object keyed by field or as a list of fields in the
882
- * order they apply.
883
- *
884
- * Both forms order by the first field, break a tie on the second, and carry on
885
- * that way. Prefer the list where the order of the fields matters, because
886
- * JavaScript moves an all-digit key such as `2024` to the front of an object
887
- * and the list keeps the order you wrote.
1237
+ * One group of hits, produced by a query that grouped its results.
888
1238
  *
889
1239
  * @public
890
1240
  */
891
- type SortSpec = Record<string, 'asc' | 'desc'> | readonly SortField[];
1241
+ interface GroupResult {
1242
+ /** Each grouping field holds this value for the group, keyed by field. */
1243
+ values: Record<string, unknown>;
1244
+ /** These hits fall in the group, cut to the query's `maxPerGroup`. */
1245
+ hits: Array<Hit>;
1246
+ /** The query's reducer produced this value for the group, and only a query that set `group.reduce` carries it. */
1247
+ reduced?: unknown;
1248
+ /** The reducer threw, and this holds its message, so the group carries its hits and no `reduced` value. */
1249
+ reducerError?: string;
1250
+ }
892
1251
  /**
893
- * Everything {@link Narsil.query} accepts.
1252
+ * What {@link Narsil.preflight} returns: how many documents a query matches,
1253
+ * without building or ranking a single hit.
894
1254
  *
895
- * Set `term` for keyword search, `vector` for similarity search, and `mode`
896
- * when you want both. The engine matches nothing for a query carrying neither
897
- * of them, so page through an index with {@link Narsil.listDocuments}
898
- * instead.
1255
+ * @public
1256
+ */
1257
+ interface PreflightResult {
1258
+ /** The query matches this many documents. */
1259
+ count: number;
1260
+ /** `count` holds the exact number of matches where this is true, and a number at or below the true total where it is false, under the rule in {@link QueryResult.countExact}. */
1261
+ countExact: boolean;
1262
+ /** The count took this many milliseconds. */
1263
+ elapsed: number;
1264
+ /** This turns true when the index's terms came from an older analysis than its language module produces now. */
1265
+ analysisStale?: boolean;
1266
+ }
1267
+ /**
1268
+ * What a batch write returns. One bad document never abandons the rest, so
1269
+ * read both lists.
899
1270
  *
900
1271
  * @public
901
1272
  */
902
- interface QueryParams {
903
- /** The engine analyses this text with the index's language module and searches for the terms that result. */
904
- term?: string;
905
- /** The engine searches the term in these fields, and in every text field in the schema by default. */
906
- fields?: string[];
907
- /**
908
- * The engine scores only the documents that pass this filter. It throws
909
- * `SEARCH_INVALID_FILTER` for an operand of the wrong shape, and for an
910
- * operator on a field of another type, such as `startsWith` on a number
911
- * field.
912
- */
913
- filters?: FilterExpression;
914
- /**
915
- * The engine multiplies the score from each named field by its weight. It
916
- * throws `SEARCH_INVALID_FIELD` for any field other than a text field that
917
- * the schema declares, and `CONFIG_INVALID` for a weight that is not a
918
- * finite number.
919
- */
920
- boost?: Record<string, number>;
921
- /** The engine gathers term statistics this way, and follows the index's `defaultScoring` where the query sets none. */
922
- scoring?: ScoringMode;
923
- /**
924
- * The engine drops any hit scoring below this value. It ranks with BM25,
925
- * which has no fixed upper bound, so a floor that suits one index and one
926
- * query can reject every hit in another.
927
- */
928
- minScore?: number;
929
- /**
930
- * The engine keeps a document only where the document matches this many
931
- * query terms, under the same `tolerance` and `prefixLength` that the search
932
- * uses. One term is enough by default.
933
- */
934
- termMatch?: TermMatchPolicy;
935
- /**
936
- * The engine matches an indexed term within this edit distance of a query
937
- * term, so it finds a document for a query with a typo. It counts each
938
- * Unicode code point as one character. Set an integer from 0 to 10.
939
- */
940
- tolerance?: number;
941
- /**
942
- * The engine applies `tolerance` only to the indexed terms that share this
943
- * many leading characters with the query term, or to the terms that start
944
- * with the whole query term when the query term is shorter. Set an integer
945
- * from 0 to 1,024; the default is 2.
946
- */
947
- prefixLength?: number;
948
- /**
949
- * With this set, the engine treats the last query term as an unfinished
950
- * word, so `secur` matches `security`. The engine matches that last term by
951
- * its stem or by completing it, while `tolerance` applies only to the earlier
952
- * terms, which have to match whole. The engine scores completions against a
953
- * shared document frequency. It also ranks them below whole-word matches.
954
- * The engine ignores this setting where `exact` is true, and the setting is
955
- * false by default.
956
- */
957
- prefix?: boolean;
958
- /**
959
- * Setting this makes the engine match each query term only against an equal
960
- * indexed term, which turns off fuzzy matching and prefix completion. The
961
- * engine still stems the query, because the index stores stemmed terms, so
962
- * `running` matches a document that holds `runs` through their shared stem.
963
- */
964
- exact?: boolean;
965
- /**
966
- * The engine counts the values of these fields across the matching
967
- * documents, and each entry sets how it cuts and sorts that field's counts.
968
- * For a field outside the schema, it counts the values that the documents
969
- * store. It throws `SEARCH_INVALID_FIELD` for a `geopoint` or a vector
970
- * field.
971
- */
972
- facets?: FacetConfig;
973
- /**
974
- * The engine sorts the hits by the values of these fields, in place of the
975
- * relevance ranking. Pass an object keyed by field, or a list of fields in
976
- * the order that the engine compares them. The engine sorts by a `number`,
977
- * `boolean`, or `enum` field, by a text field whose type includes
978
- * `sortable`, or by a list of any of these, which it reduces to one value by
979
- * the entry's `mode`. It throws `SEARCH_INVALID_FIELD` for a sort on a
980
- * `geopoint` or a vector field, because neither type has an order. It throws
981
- * the same code for a sort on any other text field, since the engine keeps
982
- * up to the first 512 code points of every document's value in memory to
983
- * sort by a text field. The engine orders hybrid results by fusion, so it
984
- * throws `SEARCH_INVALID_MODE` for a sort on a hybrid query. It also throws
985
- * that code for a direction other than `asc` and `desc`.
986
- */
987
- sort?: SortSpec;
988
- /** These settings collapse the hits into groups by field value. */
989
- group?: GroupConfig;
990
- /**
991
- * The query returns this many hits, and 10 by default. Pass 0 to read the
992
- * match count with no hits. The engine reads a value below 0 as 0. It falls
993
- * back to the default for `NaN` and for an infinite value, so a page size
994
- * that you compute from user input never fails the query.
995
- */
996
- limit?: number;
997
- /** The query skips this many hits before returning. A deep offset costs more than a cursor. */
998
- offset?: number;
999
- /** This cursor comes from a previous result's `cursor`, and pages without an offset's cost. */
1000
- searchAfter?: string;
1001
- /** These settings name the fields that come back with highlighted snippets. */
1002
- highlight?: HighlightConfig;
1003
- /** These documents take fixed positions, ahead of the ranking. */
1004
- pinned?: Array<{
1273
+ interface BatchResult {
1274
+ /** The engine wrote the documents under these ids. */
1275
+ succeeded: string[];
1276
+ /** The engine rejected each of these documents, and each entry carries the error that rejected it. */
1277
+ failed: Array<{
1005
1278
  docId: string;
1006
- position: number;
1279
+ error: NarsilError;
1007
1280
  }>;
1008
- /** This picks which engine answers the query. Keyword search runs by default. */
1009
- mode?: SearchMode;
1010
- /** These vector-search inputs are required in `vector` and `hybrid` mode. */
1011
- vector?: VectorQueryConfig;
1012
- /** These settings control how the keyword and vector rankings merge in `hybrid` mode. */
1013
- hybrid?: HybridConfig;
1014
- /**
1015
- * Setting this returns relevance scores on a sorted query. A query that
1016
- * names a sort ranks by sort values alone and computes no scores, so its
1017
- * hits carry none until this restores them. A query without a sort ignores
1018
- * this and always carries scores.
1019
- */
1020
- includeScores?: boolean;
1021
- /** Setting this returns the numbers behind each hit's score, which is what you read when a ranking surprises you. */
1022
- includeScoreComponents?: boolean;
1023
- /** This chooses how much of each stored document comes back, and the whole document by default. */
1024
- document?: DocumentProjection;
1025
1281
  }
1026
1282
  /**
1027
- * How much of a stored document each hit carries back.
1028
- *
1029
- * Pass `false` when the ids and scores are all you need, and every hit's
1030
- * `document` is then an empty object. Pass `include` to keep named fields
1031
- * alone, or `exclude` to drop named fields and keep the rest; naming both
1032
- * keeps the included fields and then drops the excluded ones from those. Use
1033
- * dots to name a nested field, so `author.name` addresses the `name` inside
1034
- * `author`, and a name that matches no field changes nothing.
1035
- *
1036
- * Drop a vector field on a similarity search, because the engine otherwise
1037
- * reads every hit's vector back out of the index and writes it into the
1038
- * response.
1283
+ * What {@link Narsil.importDocumentExport} returns. The engine continues past each
1284
+ * refused line, so read the refusals to learn which documents it skipped.
1039
1285
  *
1040
1286
  * @public
1041
1287
  */
1042
- type DocumentProjection = boolean | {
1043
- /** Names the fields to keep, so every field left out of this list is dropped. */
1044
- include?: string[];
1045
- /** Names the fields to drop, so every other field is kept. */
1046
- exclude?: string[];
1047
- };
1288
+ interface DocumentImportResult {
1289
+ /** The engine wrote this many documents, counting each one that it replaced. */
1290
+ imported: number;
1291
+ /** The engine refused this many lines in total. */
1292
+ refusedCount: number;
1293
+ /** These are the 1,000 refusals with the lowest line numbers. Each entry contains the line number, where the header is line 1, along with the error code and a message. */
1294
+ refused: Array<{
1295
+ line: number;
1296
+ code: NarsilErrorCode;
1297
+ message: string;
1298
+ }>;
1299
+ }
1048
1300
  /**
1049
- * Vector-search inputs passed under `QueryParams.vector`.
1050
- *
1051
- * Supply either a raw `value` array or a `text` string for auto-embedding;
1052
- * passing both throws `EMBEDDING_CONFIG_INVALID`. Result count is governed
1053
- * by the outer query's `limit`, not by any field on this object.
1301
+ * What {@link Narsil.getStats} returns about one index.
1054
1302
  *
1055
1303
  * @public
1056
1304
  */
1057
- interface VectorQueryConfig {
1058
- /**
1059
- * Name of the schema field that holds the vector to compare against.
1060
- * Must reference a field declared as `vector[N]` in the index schema.
1061
- */
1062
- field: string;
1063
- /**
1064
- * Raw query vector, given as a number array or a `Float32Array`. Length
1065
- * must match the indexed field's dimension or the search rejects the
1066
- * request with `VECTOR_DIMENSION_MISMATCH`.
1067
- */
1068
- value?: number[] | Float32Array;
1069
- /**
1070
- * Text to embed at query time using the index or instance embedding
1071
- * adapter. Mutually exclusive with `value`; requires a configured adapter.
1072
- */
1073
- text?: string;
1074
- /**
1075
- * Score floor applied during ranking. Hits scoring below this value
1076
- * are dropped before `limit` is enforced, so the returned hit count
1077
- * can be smaller than `limit` even when more documents exist. The
1078
- * floor is interpreted in score space for every metric; for
1079
- * `euclidean`, distance is mapped to a similarity score of
1080
- * `1 / (1 + distance)` first. Defaults to no floor.
1081
- */
1082
- similarity?: number;
1083
- /**
1084
- * Similarity metric used for ranking. Defaults to `cosine`. Choose
1085
- * `dotProduct` for raw inner-product scoring on already-normalised
1086
- * vectors and `euclidean` for distance-based ordering.
1087
- */
1088
- metric?: 'cosine' | 'dotProduct' | 'euclidean';
1089
- /**
1090
- * HNSW exploration factor for approximate search. Higher values raise
1091
- * recall at the cost of latency. Ignored while the field is still
1092
- * served by the brute-force backend. Defaults to the engine's built-in
1093
- * value when omitted.
1094
- */
1095
- efSearch?: number;
1305
+ interface IndexStats {
1306
+ /** The index holds this many documents. A closed index reports its count as of the last checkpoint. */
1307
+ documentCount: number;
1308
+ /** The index is spread across this many partitions. */
1309
+ partitionCount: number;
1096
1310
  /**
1097
- * How many times `limit` a quantized field re-scores against its
1098
- * full-precision vectors before it returns the best `limit`. A value below
1099
- * 1 or one that is not a finite number fails with `CONFIG_INVALID`. The
1100
- * engine ignores this on a field whose quantization is `none`. When a query
1101
- * omits it, the engine takes 3 for `osq1` and `osq2`, 2 for `osq8`, and 2
1102
- * for `osq4` at 1,024 dimensions and above, but 5 for `osq4` below 1,024,
1103
- * where a four-bit code ranks the true neighbours less reliably.
1311
+ * Formula-based estimate of the index's main-thread memory footprint, summed
1312
+ * across every partition and vector index for this index. Computed from
1313
+ * structural counters (document count, posting counts, field-index sizes),
1314
+ * not measured. Per-object V8 overhead (hidden classes, map transitions,
1315
+ * GC headers) is not captured here, so this number routinely undershoots
1316
+ * the real heap by a meaningful factor. Use {@link Narsil.getMemoryStats}
1317
+ * when sizing host memory.
1104
1318
  */
1105
- oversample?: number;
1319
+ estimatedMemoryBytes: number;
1320
+ /** The index analyses text with this language module. */
1321
+ language: string;
1322
+ /** The index was created with this field layout. */
1323
+ schema: SchemaDefinition;
1106
1324
  }
1107
1325
  /**
1108
- * How a hybrid query merges its keyword ranking with its vector ranking.
1326
+ * A summary of one index, as {@link Narsil.listIndexes} reports it.
1109
1327
  *
1110
- * `rrf` combines the two by rank alone, which needs no tuning and handles
1111
- * scores that live on different scales. `linear` blends the scores directly,
1112
- * which gives you control once you know what each side's scores look like.
1328
+ * @public
1329
+ */
1330
+ interface IndexInfo {
1331
+ /** The index was created under this name. */
1332
+ name: string;
1333
+ /** The index holds this many documents. A closed index reports its count as of the last checkpoint. */
1334
+ documentCount: number;
1335
+ /** The index is spread across this many partitions. */
1336
+ partitionCount: number;
1337
+ /** The index analyses text with this language module. */
1338
+ language: string;
1339
+ /** This turns true when the index's terms came from an older analysis than its language module produces now. */
1340
+ analysisStale?: boolean;
1341
+ /** This is `open` in memory, `closed` after release, and `reopen-failed` after five recovery failures. */
1342
+ state: 'open' | 'closed' | 'reopen-failed';
1343
+ /** This counts successful loads since this engine started. */
1344
+ reopenCount: number;
1345
+ }
1346
+ /**
1347
+ * What {@link Narsil.getPartitionStats} reports for one partition, which is
1348
+ * where you look when an index grows unevenly.
1113
1349
  *
1114
1350
  * @public
1115
1351
  */
1116
- interface HybridConfig {
1117
- /** The engine merges the rankings this way, and by rank fusion by default. It throws `CONFIG_INVALID` for any other value. */
1118
- strategy?: 'rrf' | 'linear';
1119
- /** The engine uses this rank-fusion constant under `rrf` alone, where a larger value reduces the lead of the top ranks. Set a whole number of at least 1, or leave it out for 60. The engine throws `CONFIG_INVALID` for any other value. */
1120
- k?: number;
1121
- /** The engine uses this weight of the vector score, from 0 to 1, under `linear` alone, and 0.5 where you leave it out. It throws `CONFIG_INVALID` for a value outside that range. */
1122
- alpha?: number;
1352
+ interface PartitionStatsResult {
1353
+ /** This identifies the partition within its index. */
1354
+ partitionId: number;
1355
+ /** The partition holds this many documents. */
1356
+ documentCount: number;
1357
+ /**
1358
+ * Formula-based estimate of this partition's memory footprint. V8 has no
1359
+ * per-object heap accounting API, so this number is derived from structural
1360
+ * counters (document count, posting counts, field-index sizes) and does not
1361
+ * include per-object overhead. Treat as an estimate, not a measurement.
1362
+ */
1363
+ estimatedMemoryBytes: number;
1123
1364
  }
1124
1365
  /**
1125
- * Fields a query counts values for, keyed by field name.
1366
+ * What {@link Narsil.suggest} returns: the indexed terms that complete a
1367
+ * prefix, ready to offer as you-type suggestions.
1126
1368
  *
1127
1369
  * @public
1128
1370
  */
1129
- interface FacetConfig {
1130
- /** Each key names a field the query counts values for. */
1131
- [field: string]: {
1132
- /** The engine returns this many values for the field, in the order of `sort`. An engine returns every value where you leave this out, while a cluster returns 10, or every range of a field that sets `ranges`. */
1133
- limit?: number;
1134
- /** The engine orders the returned values by their count, putting the highest first under `'desc'`, which is the default, and the lowest first under `'asc'`. Under `'asc'`, each cluster node sends the count of up to 10,000 values of the field, so the lowest counts are exact wherever no node holds more values than that. */
1135
- sort?: 'asc' | 'desc';
1136
- /** The engine counts a numeric field's matches into these ranges, with one count for each range. A cluster search counts up to 1,000 ranges on a field and throws `CONFIG_INVALID` for more. */
1137
- ranges?: Array<{
1138
- from: number;
1139
- to: number;
1140
- }>;
1141
- };
1371
+ interface SuggestResult {
1372
+ /** These completions run most widely used first. */
1373
+ terms: Array<{
1374
+ /** This is the completed term, as the index holds it. */
1375
+ term: string;
1376
+ /** The term appears in this many documents, which is what ordered the list. */
1377
+ documentFrequency: number;
1378
+ }>;
1379
+ /** The lookup took this many milliseconds. */
1380
+ elapsed: number;
1381
+ /** This turns true when the index's terms came from an older analysis than its language module produces now. */
1382
+ analysisStale?: boolean;
1142
1383
  }
1143
1384
  /**
1144
- * How a query collapses its hits into groups.
1385
+ * The state of one vector field, and what maintenance would cost, as
1386
+ * {@link Narsil.vectorMaintenanceStatus} reports it.
1387
+ *
1388
+ * Removing a vector leaves a tombstone in the graph, and enough of them slow a
1389
+ * search down. Read this to decide when compaction earns its cost.
1145
1390
  *
1146
1391
  * @public
1147
1392
  */
1148
- interface GroupConfig {
1149
- /** The values of these fields define a group, and several fields group by their combination. */
1150
- fields: string[];
1151
- /** The engine returns this many hits from each group, best first, and one hit where you leave this out. */
1152
- maxPerGroup?: number;
1153
- /** The engine returns this many groups, best first, and every group where you leave this out. */
1154
- limit?: number;
1393
+ interface VectorMaintenanceResult {
1394
+ /** This describes the named vector field. */
1395
+ fieldName: string;
1155
1396
  /**
1156
- * The engine folds every hit of each group into one value, such as a sum or
1157
- * an average, including the hits beyond `maxPerGroup`. In cluster mode the
1158
- * coordinator fetches up to 10,000 hits of each group to fold.
1397
+ * Tombstones make up this share of the graph's nodes, from 0 to 1. Before
1398
+ * the first build, where {@link VectorMaintenanceResult.graphCount} reads 0,
1399
+ * the engine divides by the number of stored vectors, because no graph
1400
+ * exists yet.
1159
1401
  */
1160
- reduce?: GroupReducer;
1402
+ tombstoneRatio: number;
1403
+ /** The field holds this many HNSW graphs: 0 before the first build and 1 once a graph exists. */
1404
+ graphCount: number;
1405
+ /** This many vectors wait outside the graph, which a search scans one by one. */
1406
+ bufferSize: number;
1407
+ /** This stays true while a graph build runs. */
1408
+ building: boolean;
1409
+ /** {@link Narsil.compactVectors} would take about this many milliseconds. */
1410
+ estimatedCompactMs: number;
1411
+ /** {@link Narsil.optimizeVectors} would take about this many milliseconds. */
1412
+ estimatedOptimizeMs: number;
1161
1413
  }
1162
1414
  /**
1163
- * Folds the hits of one group into a single value.
1415
+ * One stored document, as {@link Narsil.listDocuments} returns it.
1416
+ *
1417
+ * @typeParam T - Shape of the stored document.
1164
1418
  *
1165
1419
  * @public
1166
1420
  */
1167
- type GroupReducer = {
1168
- /**
1169
- * Folds one hit into the running value.
1170
- *
1171
- * @param accumulator - What the previous call returned, or what
1172
- * `initialValue` produced for the first hit.
1173
- * @param doc - The hit's stored document.
1174
- * @param score - The hit's relevance score.
1175
- * @returns The new running value.
1176
- */
1177
- reducer: (accumulator: unknown, doc: AnyDocument, score: number) => unknown;
1178
- /** Produces the starting value for each group. The engine calls it once per group. */
1179
- initialValue: () => unknown;
1180
- };
1421
+ interface ListedDocument<T = AnyDocument> {
1422
+ /** The document is stored under this id. */
1423
+ id: string;
1424
+ /** This holds the stored document, cut down to what the projection kept. */
1425
+ document: T;
1426
+ }
1181
1427
  /**
1182
- * This configuration names the fields that receive highlighted snippets. It
1183
- * also sets how the engine marks those snippets up.
1428
+ * What {@link Narsil.listDocuments} returns: one page of stored documents and
1429
+ * the cursor that reaches the next one.
1184
1430
  *
1185
- * The engine analyses the text of each returned field again to tag every word
1186
- * that it matched, so it tags the same words whatever the index's
1187
- * `trackPositions` setting is. It tags a word whose stem equals a query stem,
1188
- * and a word within the query's `tolerance` of a query stem. For a `prefix`
1189
- * query, it tags a word for the last query term only when that word completes
1190
- * the term or shares its stem.
1431
+ * @typeParam T - Shape of the stored documents.
1191
1432
  *
1192
1433
  * @public
1193
1434
  */
1194
- interface HighlightConfig {
1195
- /** The engine highlights these fields. */
1196
- fields: string[];
1197
- /** The engine writes this before each match, and writes `<mark>` by default. */
1198
- preTag?: string;
1199
- /** The engine writes this after each match, and writes `</mark>` by default. */
1200
- postTag?: string;
1201
- /**
1202
- * The engine takes this many characters of the field around the densest run
1203
- * of matches, 200 by default. It returns a snippet longer than that number,
1204
- * because it then adds the opening and closing tags, and an ellipsis at each
1205
- * end where it cuts the field.
1206
- */
1207
- maxSnippetLength?: number;
1435
+ interface ListResult<T = AnyDocument> {
1436
+ /** These documents make up the page, in ascending document-id order. */
1437
+ documents: Array<ListedDocument<T>>;
1438
+ /** This opaque cursor reaches the next page, and is null once the listing is finished. */
1439
+ cursor: string | null;
1440
+ /** The listing covers this many documents in total, and any filter narrows that count. */
1441
+ total: number;
1442
+ /** The engine spent this many milliseconds building the page. */
1443
+ elapsed: number;
1208
1444
  }
1209
1445
 
1210
1446
  /**
@@ -1293,4 +1529,4 @@ interface MemoryStats {
1293
1529
  }>;
1294
1530
  }
1295
1531
 
1296
- export { type ArrayFilter as A, type BatchResult as B, type ClientErrorCode as C, type DocumentProjection as D, type ErrorCode as E, type FacetConfig as F, type GeoFilter as G, type HighlightConfig as H, type IndexInfo as I, type QueryCoverage as J, type ScoreComponents as K, type ListParams as L, type MemoryStats as M, NarsilError as N, type SearchMode as O, type PartitionStatsResult as P, type QueryParams as Q, type SortField as R, type ServerErrorCode as S, type SortSpec as T, type StringFilter as U, type VectorMaintenanceResult as V, type TermMatchPolicy as W, type VectorQueryConfig as X, type WorkerCopyReport as Y, ClientErrorCodes as a, ErrorCodes as b, type NarsilErrorCode as c, ServerErrorCodes as d, type QueryResult as e, type IndexStats as f, type ListResult as g, type PreflightResult as h, isNarsilError as i, type SuggestParams as j, type SuggestResult as k, type ComparisonFilter as l, type FacetResult as m, type FieldFilter as n, type FilterExpression as o, type GeoPolygonFilter as p, type GeoRadiusFilter as q, type GroupConfig as r, type GroupReducer as s, type GroupResult as t, type HighlightMatch as u, type Hit as v, type HybridConfig as w, type ListedDocument as x, type PresenceFilter as y, type ProcessMemoryReport as z };
1532
+ export { type WorkerCopyReport as $, type ArrayFilter as A, type BatchResult as B, type ClientErrorCode as C, type DocumentImportResult as D, type ErrorCode as E, type FacetConfig as F, type GeoFilter as G, type HighlightConfig as H, type IndexInfo as I, type ListedDocument as J, type PresenceFilter as K, type ListParams as L, type MemoryStats as M, NarsilError as N, type ProcessMemoryReport as O, type PartitionStatsResult as P, type QueryParams as Q, type QueryCoverage as R, type ServerErrorCode as S, type ScoreComponents as T, type SearchMode as U, type VectorMaintenanceResult as V, type SortField as W, type SortSpec as X, type StringFilter as Y, type TermMatchPolicy as Z, type VectorQueryConfig as _, ClientErrorCodes as a, ErrorCodes as b, type NarsilErrorCode as c, ServerErrorCodes as d, type QueryResult as e, type NarsilEventHandlers as f, type IndexStats as g, type ListResult as h, isNarsilError as i, type PreflightResult as j, type SuggestParams as k, type SuggestResult as l, type NarsilEventMap as m, type ComparisonFilter as n, type DocumentProjection as o, type FacetResult as p, type FieldFilter as q, type FilterExpression as r, type GeoPolygonFilter as s, type GeoRadiusFilter as t, type GroupConfig as u, type GroupReducer as v, type GroupResult as w, type HighlightMatch as x, type Hit as y, type HybridConfig as z };