@delali/narsil 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -560,13 +560,13 @@ await narsil.optimizeVectors('docs', 'embedding')
560
560
 
561
561
  ## Hybrid search
562
562
 
563
- Hybrid mode runs the full-text and vector searches in one query and fuses the two rankings.
563
+ Hybrid mode runs the full-text and vector searches in one query and fuses the two rankings. The vector side needs a query vector: pass a precomputed `value` array, or a `text` string that the index or instance embedding adapter turns into a vector. The `text` form needs an embedding adapter configured first, so passing `text` without one fails with `EMBEDDING_CONFIG_INVALID`; see [Embedding adapters](#embedding-adapters).
564
564
 
565
565
  ```ts
566
566
  const results = await narsil.query('docs', {
567
567
  mode: 'hybrid',
568
568
  term: 'how do search engines scale',
569
- vector: { field: 'embedding', text: 'how do search engines scale' },
569
+ vector: { field: 'embedding', value: myQueryVector },
570
570
  hybrid: { strategy: 'rrf', k: 60 },
571
571
  limit: 10,
572
572
  })
@@ -581,14 +581,14 @@ Two fusion strategies are available:
581
581
  const weighted = await narsil.query('docs', {
582
582
  mode: 'hybrid',
583
583
  term: 'partition rebalancing',
584
- vector: { field: 'embedding', text: 'partition rebalancing' },
584
+ vector: { field: 'embedding', value: myQueryVector },
585
585
  hybrid: { strategy: 'linear', alpha: 0.7 },
586
586
  })
587
587
  ```
588
588
 
589
589
  ## Geosearch
590
590
 
591
- Declare a `geopoint` field and insert documents with `{ lat, lon }` values. Geo conditions are filters, so they compose with terms, other filters, facets, and every other query feature.
591
+ Declare a `geopoint` field and insert documents with `{ lat, lon }` values. Geo conditions are filters that refine a search, so a geo query pairs a `term` (or a vector query) with a location filter, and it composes with other filters, facets, and every other query feature. A query with only a location filter and no `term` matches nothing, the same way any other filter refines a term search.
592
592
 
593
593
  ```ts
594
594
  await narsil.createIndex('stores', {
@@ -604,6 +604,7 @@ await narsil.insert('stores', {
604
604
  })
605
605
 
606
606
  const nearby = await narsil.query('stores', {
607
+ term: 'market',
607
608
  filters: {
608
609
  fields: {
609
610
  location: {
@@ -614,6 +615,7 @@ const nearby = await narsil.query('stores', {
614
615
  })
615
616
 
616
617
  const inArea = await narsil.query('stores', {
618
+ term: 'market',
617
619
  filters: {
618
620
  fields: {
619
621
  location: {
@@ -707,7 +709,7 @@ engine.registerEmbeddingAdapter('openai-small', myReplacementAdapter)
707
709
 
708
710
  `registerEmbeddingAdapter` rebinds every index referencing that name, which lets you rotate credentials or swap models without recreating indexes.
709
711
 
710
- ### Shipped adapters
712
+ ### Bundled adapters
711
713
 
712
714
  | Adapter | Package | Dependencies |
713
715
  | --- | --- | --- |
@@ -733,7 +735,7 @@ When `embedBatch` is missing, Narsil falls back to parallel `embed()` calls with
733
735
 
734
736
  ## Persistence
735
737
 
736
- Persistence stores serialized partitions through a pluggable adapter. Three adapters ship with the package:
738
+ Persistence stores serialized partitions through a pluggable adapter. The package includes three adapters:
737
739
 
738
740
  | Adapter | Import | Environment |
739
741
  | --- | --- | --- |
@@ -848,7 +850,7 @@ Promotion emits the `workerPromote` event, and a crashed worker emits `workerCra
848
850
 
849
851
  ## Multi-instance invalidation
850
852
 
851
- When several engine instances share one persistence backend, the invalidation adapter tells the others which partitions changed so they evict stale cache instead of serving old data. Two adapters ship with the package, and `@delali/narsil/invalidation/noop` stubs the interface for single-instance deployments:
853
+ When several engine instances share one persistence backend, the invalidation adapter tells the others which partitions changed so they evict stale cache instead of serving old data. The package includes two adapters, and `@delali/narsil/invalidation/noop` stubs the interface for single-instance deployments:
852
854
 
853
855
  | Adapter | Import | Use case |
854
856
  | --- | --- | --- |
@@ -986,7 +988,7 @@ console.log(memory.workers)
986
988
 
987
989
  ## Language support
988
990
 
989
- Narsil ships with language modules for tokenization, stemming (Snowball algorithm), and stop word removal.
991
+ Narsil includes language modules for tokenization, stemming (Snowball algorithm), and stop word removal.
990
992
 
991
993
  ### Full support (tokenizer + stemmer + stop words)
992
994
 
@@ -998,17 +1000,25 @@ Chinese (Mandarin), Japanese
998
1000
 
999
1001
  ### African language support
1000
1002
 
1001
- Full stemmer support ships for Swahili. Tokenization and stop word support is available for Dagbani, Ewe, Ga, Hausa, Igbo, Twi (Akan), Yoruba, and Zulu.
1003
+ Swahili has full stemmer support. Tokenization and stop word support is available for Dagbani, Ewe, Ga, Hausa, Igbo, Twi (Akan), Yoruba, and Zulu.
1002
1004
 
1003
- Each language module is a separate entry point, so you only bundle the languages your application needs:
1005
+ Each language module is a separate entry point, so you only bundle the languages your application needs. Import the module and register it before you create an index that names it:
1004
1006
 
1005
1007
  ```ts
1006
- import '@delali/narsil/languages/french'
1007
- import '@delali/narsil/languages/swahili'
1008
- import '@delali/narsil/languages/twi'
1008
+ import { createNarsil, registerLanguage } from '@delali/narsil'
1009
+ import { french } from '@delali/narsil/languages/french'
1010
+ import { swahili } from '@delali/narsil/languages/swahili'
1011
+ import { twi } from '@delali/narsil/languages/twi'
1012
+
1013
+ registerLanguage(french)
1014
+ registerLanguage(swahili)
1015
+ registerLanguage(twi)
1016
+
1017
+ const narsil = await createNarsil()
1018
+ await narsil.createIndex('articles', { schema: { title: 'string' }, language: 'french' })
1009
1019
  ```
1010
1020
 
1011
- Import the module before creating an index that names it; naming a language that was never imported fails with `LANGUAGE_NOT_SUPPORTED`. English is always available. `registerLanguage(module)` adds a language of your own: a module carries a name, a tokenizer, a stemmer, and a stop word set, and any of the shipped modules serves as a reference.
1021
+ English is registered by default, so it needs no import. Naming a language you have not registered fails with `LANGUAGE_NOT_SUPPORTED`. `registerLanguage(module)` also adds a language of your own: a module carries a name, a tokenizer, a stemmer, and a stop word set, and any of the built-in language modules serves as a reference.
1012
1022
 
1013
1023
  ## HTTP server
1014
1024
 
@@ -1032,7 +1042,7 @@ const server = createServer(engine, {
1032
1042
  await server.listen()
1033
1043
  ```
1034
1044
 
1035
- `ServerOptions` also accepts `cors`, an `onRequest` hook for authentication, `limits` for body-size and concurrency caps, `embeddingAdapters` that JSON index configs reference by name, a `taskStore` that keeps long-running task status across restarts, an `instanceId` for task recovery, and `allowInsecure` for trusted private networks. The server refuses to bind a non-loopback address without an `onRequest` hook, because the admin endpoints can destroy data.
1045
+ `ServerOptions` also accepts `cors`, an `onRequest` hook for authentication, `limits` for body-size, concurrency, result-window, and fetch-count caps, `embeddingAdapters` that JSON index configs reference by name, a `taskStore` that keeps long-running task status across restarts, an `instanceId` for task recovery, and `allowInsecure` for trusted private networks. The server refuses to bind a non-loopback address without an `onRequest` hook, because the admin endpoints can destroy data.
1036
1046
 
1037
1047
  The full surface:
1038
1048
 
package/dist/server.d.ts CHANGED
@@ -45,6 +45,14 @@ interface ServerLimits {
45
45
  importBatchSize?: number;
46
46
  /** Maximum requests executing engine work at once; excess is shed with 503. Omit or 0 to disable. */
47
47
  maxConcurrentRequests?: number;
48
+ /** Ceiling for a search's `limit`, `offset`, and `group.maxPerGroup`, so one
49
+ * request cannot ask for an unbounded result set. Excess → 400. Defaults to
50
+ * 10000, matching the cluster query result window. */
51
+ maxResultWindow?: number;
52
+ /** Ceiling for the number of document ids in one multi-get request, so one
53
+ * request cannot pull an unbounded number of documents. Excess → 400.
54
+ * Defaults to 10000. */
55
+ maxFetchDocuments?: number;
48
56
  }
49
57
  interface ServerOptions {
50
58
  host?: string;
package/dist/server.mjs CHANGED
@@ -118,11 +118,31 @@ var STATUS_TEXT = {
118
118
  500: "500 Internal Server Error",
119
119
  503: "503 Service Unavailable"
120
120
  };
121
+ var STREAM_THRESHOLD = 64 * 1024;
121
122
  function statusLine(status) {
122
123
  return STATUS_TEXT[status] ?? `${status}`;
123
124
  }
124
- function sendJson(res, data, status = 200) {
125
+ function streamBody(res, abort, body, status, contentType) {
126
+ if (abort.aborted) return;
127
+ const totalSize = body.byteLength;
128
+ res.cork(() => {
129
+ res.writeStatus(statusLine(status)).writeHeader("Content-Type", contentType);
130
+ const startOffset = res.getWriteOffset();
131
+ const [, done] = res.tryEnd(body, totalSize);
132
+ if (done) return;
133
+ res.onWritable((offset) => {
134
+ if (abort.aborted) return true;
135
+ const [retryOk] = res.tryEnd(body.subarray(offset - startOffset), totalSize);
136
+ return retryOk;
137
+ });
138
+ });
139
+ }
140
+ function sendJson(res, data, status = 200, abort) {
125
141
  const payload = JSON.stringify(data);
142
+ if (abort && payload !== void 0 && Buffer.byteLength(payload) >= STREAM_THRESHOLD) {
143
+ streamBody(res, abort, Buffer.from(payload, "utf8"), status, "application/json");
144
+ return;
145
+ }
126
146
  res.cork(() => {
127
147
  res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/json").end(payload);
128
148
  });
@@ -134,10 +154,8 @@ function sendError(res, status, code, message, details) {
134
154
  res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/json").end(payload);
135
155
  });
136
156
  }
137
- function sendBinary(res, data, status = 200) {
138
- res.cork(() => {
139
- res.writeStatus(statusLine(status)).writeHeader("Content-Type", "application/octet-stream").end(data);
140
- });
157
+ function sendBinary(res, data, abort, status = 200) {
158
+ streamBody(res, abort, data, status, "application/octet-stream");
141
159
  }
142
160
 
143
161
  // src/server/handler-utils.ts
@@ -171,11 +189,15 @@ function respondError(ctx, err) {
171
189
  }
172
190
  function respondJson(ctx, data, status = 200) {
173
191
  if (ctx.abort.aborted) return;
174
- sendJson(ctx.res, data, status);
192
+ sendJson(ctx.res, data, status, ctx.abort);
175
193
  }
176
194
  function badRequest(res, message, details) {
177
195
  sendError(res, 400, ServerErrorCodes.INVALID_REQUEST, message, details);
178
196
  }
197
+ function rejectInvalid(ctx, failure) {
198
+ if (ctx.abort.aborted) return;
199
+ badRequest(ctx.res, failure.message, failure.details);
200
+ }
179
201
  function serializeBatchResult(result) {
180
202
  return {
181
203
  succeeded: result.succeeded,
@@ -186,6 +208,98 @@ function serializeBatchResult(result) {
186
208
  };
187
209
  }
188
210
 
211
+ // src/server/validation.ts
212
+ function isPlainObject(value) {
213
+ return typeof value === "object" && value !== null && !Array.isArray(value);
214
+ }
215
+ function overWindow(field, value, maxWindow) {
216
+ if (typeof value === "number" && value > maxWindow) {
217
+ return {
218
+ message: `Field "${field}" exceeds the maximum result window of ${maxWindow}`,
219
+ details: { value, limit: maxWindow }
220
+ };
221
+ }
222
+ return null;
223
+ }
224
+ function validateQuery(params, maxWindow) {
225
+ if (params.group && typeof params.group === "object" && "reduce" in params.group) {
226
+ return {
227
+ message: 'Custom group reducers are not available over HTTP; use "group.fields" and "group.maxPerGroup" only'
228
+ };
229
+ }
230
+ const rowBound = overWindow("limit", params.limit, maxWindow) ?? overWindow("offset", params.offset, maxWindow);
231
+ if (rowBound) return rowBound;
232
+ if (params.group && typeof params.group === "object") {
233
+ const grouped = overWindow("group.maxPerGroup", params.group.maxPerGroup, maxWindow);
234
+ if (grouped) return grouped;
235
+ }
236
+ if (params.facets && typeof params.facets === "object") {
237
+ for (const [field, facet] of Object.entries(params.facets)) {
238
+ if (facet && typeof facet === "object") {
239
+ const faceted = overWindow(`facets.${field}.limit`, facet.limit, maxWindow);
240
+ if (faceted) return faceted;
241
+ }
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ function validateSuggest(params) {
247
+ if (typeof params.prefix !== "string") {
248
+ return { message: 'Field "prefix" is required and must be a string' };
249
+ }
250
+ return null;
251
+ }
252
+ function validateDocumentBody(body) {
253
+ if (!isPlainObject(body.document)) {
254
+ return { message: 'Field "document" is required and must be an object' };
255
+ }
256
+ return null;
257
+ }
258
+ function validateMultiGet(body, maxFetch) {
259
+ if (!Array.isArray(body.docIds)) {
260
+ return { message: 'Field "docIds" is required and must be an array of strings' };
261
+ }
262
+ if (body.docIds.length > maxFetch) {
263
+ return {
264
+ message: `Field "docIds" exceeds the maximum of ${maxFetch} ids per request`,
265
+ details: { count: body.docIds.length, limit: maxFetch }
266
+ };
267
+ }
268
+ return null;
269
+ }
270
+ function validateBatch(body) {
271
+ const action = body.action ?? "insert";
272
+ if (action === "insert") {
273
+ if (!Array.isArray(body.documents)) return { message: 'Batch insert requires a "documents" array' };
274
+ return null;
275
+ }
276
+ if (action === "update") {
277
+ if (!Array.isArray(body.updates)) return { message: 'Batch update requires an "updates" array' };
278
+ return null;
279
+ }
280
+ if (action === "delete") {
281
+ if (!Array.isArray(body.docIds)) return { message: 'Batch delete requires a "docIds" array' };
282
+ return null;
283
+ }
284
+ return { message: 'Field "action" must be one of "insert", "update", or "delete"' };
285
+ }
286
+ function validateCreateIndex(body) {
287
+ if (typeof body.name !== "string" || body.name.length === 0) {
288
+ return { message: 'Field "name" is required and must be a non-empty string' };
289
+ }
290
+ if (!isPlainObject(body.config)) {
291
+ return { message: 'Field "config" is required and must be an object' };
292
+ }
293
+ return null;
294
+ }
295
+ function validateRebalance(body) {
296
+ const target = body.targetPartitionCount;
297
+ if (typeof target !== "number" || !Number.isInteger(target) || target <= 0) {
298
+ return { message: 'Field "targetPartitionCount" is required and must be a positive integer' };
299
+ }
300
+ return null;
301
+ }
302
+
189
303
  // src/server/handlers/admin.ts
190
304
  function taskResponse(record) {
191
305
  return { taskId: record.id, type: record.type, status: record.status };
@@ -204,7 +318,7 @@ function createAdminHandlers(deps) {
204
318
  try {
205
319
  const bytes = await engine.snapshot(ctx.params[0]);
206
320
  if (ctx.abort.aborted) return;
207
- sendBinary(ctx.res, bytes);
321
+ sendBinary(ctx.res, bytes, ctx.abort);
208
322
  } catch (err) {
209
323
  respondError(ctx, err);
210
324
  }
@@ -262,11 +376,12 @@ function createAdminHandlers(deps) {
262
376
  async function rebalance(ctx) {
263
377
  const body = parseJsonOptional(ctx);
264
378
  if (!body) return;
265
- const target = body.targetPartitionCount;
266
- if (typeof target !== "number" || !Number.isInteger(target) || target <= 0) {
267
- badRequest(ctx.res, 'Field "targetPartitionCount" is required and must be a positive integer');
379
+ const failure = validateRebalance(body);
380
+ if (failure) {
381
+ rejectInvalid(ctx, failure);
268
382
  return;
269
383
  }
384
+ const target = body.targetPartitionCount;
270
385
  const name = ctx.params[0];
271
386
  try {
272
387
  requireIndexExists(name);
@@ -315,16 +430,14 @@ function createAdminHandlers(deps) {
315
430
  }
316
431
 
317
432
  // src/server/handlers/documents.ts
318
- function isPlainObject(value) {
319
- return typeof value === "object" && value !== null && !Array.isArray(value);
320
- }
321
433
  function createDocumentHandlers(deps) {
322
- const { engine } = deps;
434
+ const { engine, limits } = deps;
323
435
  async function insert(ctx) {
324
436
  const body = parseJson(ctx);
325
437
  if (!body) return;
326
- if (!isPlainObject(body.document)) {
327
- badRequest(ctx.res, 'Field "document" is required and must be an object');
438
+ const failure = validateDocumentBody(body);
439
+ if (failure) {
440
+ rejectInvalid(ctx, failure);
328
441
  return;
329
442
  }
330
443
  try {
@@ -356,8 +469,9 @@ function createDocumentHandlers(deps) {
356
469
  async function put(ctx) {
357
470
  const body = parseJson(ctx);
358
471
  if (!body) return;
359
- if (!isPlainObject(body.document)) {
360
- badRequest(ctx.res, 'Field "document" is required and must be an object');
472
+ const failure = validateDocumentBody(body);
473
+ if (failure) {
474
+ rejectInvalid(ctx, failure);
361
475
  return;
362
476
  }
363
477
  const [name, id] = ctx.params;
@@ -376,8 +490,9 @@ function createDocumentHandlers(deps) {
376
490
  async function patch(ctx) {
377
491
  const body = parseJson(ctx);
378
492
  if (!body) return;
379
- if (!isPlainObject(body.document)) {
380
- badRequest(ctx.res, 'Field "document" is required and must be an object');
493
+ const failure = validateDocumentBody(body);
494
+ if (failure) {
495
+ rejectInvalid(ctx, failure);
381
496
  return;
382
497
  }
383
498
  try {
@@ -405,8 +520,9 @@ function createDocumentHandlers(deps) {
405
520
  async function multiGet(ctx) {
406
521
  const body = parseJson(ctx);
407
522
  if (!body) return;
408
- if (!Array.isArray(body.docIds)) {
409
- badRequest(ctx.res, 'Field "docIds" is required and must be an array of strings');
523
+ const failure = validateMultiGet(body, limits.maxFetchDocuments);
524
+ if (failure) {
525
+ rejectInvalid(ctx, failure);
410
526
  return;
411
527
  }
412
528
  try {
@@ -421,28 +537,22 @@ function createDocumentHandlers(deps) {
421
537
  async function batch(ctx) {
422
538
  const body = parseJson(ctx);
423
539
  if (!body) return;
540
+ const failure = validateBatch(body);
541
+ if (failure) {
542
+ rejectInvalid(ctx, failure);
543
+ return;
544
+ }
424
545
  const action = body.action ?? "insert";
425
546
  try {
426
- if (action === "insert") {
427
- if (!Array.isArray(body.documents)) {
428
- badRequest(ctx.res, 'Batch insert requires a "documents" array');
429
- return;
430
- }
431
- respondJson(ctx, serializeBatchResult(await engine.insertBatch(ctx.params[0], body.documents, body.options)));
432
- } else if (action === "update") {
433
- if (!Array.isArray(body.updates)) {
434
- badRequest(ctx.res, 'Batch update requires an "updates" array');
435
- return;
436
- }
437
- respondJson(ctx, serializeBatchResult(await engine.updateBatch(ctx.params[0], body.updates)));
547
+ if (action === "update") {
548
+ respondJson(ctx, serializeBatchResult(await engine.updateBatch(ctx.params[0], body.updates ?? [])));
438
549
  } else if (action === "delete") {
439
- if (!Array.isArray(body.docIds)) {
440
- badRequest(ctx.res, 'Batch delete requires a "docIds" array');
441
- return;
442
- }
443
- respondJson(ctx, serializeBatchResult(await engine.removeBatch(ctx.params[0], body.docIds)));
550
+ respondJson(ctx, serializeBatchResult(await engine.removeBatch(ctx.params[0], body.docIds ?? [])));
444
551
  } else {
445
- badRequest(ctx.res, 'Field "action" must be one of "insert", "update", or "delete"');
552
+ respondJson(
553
+ ctx,
554
+ serializeBatchResult(await engine.insertBatch(ctx.params[0], body.documents ?? [], body.options))
555
+ );
446
556
  }
447
557
  } catch (err) {
448
558
  respondError(ctx, err);
@@ -627,12 +737,9 @@ function createIndexHandlers(deps) {
627
737
  async function create(ctx) {
628
738
  const body = parseJson(ctx);
629
739
  if (!body) return;
630
- if (typeof body.name !== "string" || body.name.length === 0) {
631
- badRequest(ctx.res, 'Field "name" is required and must be a non-empty string');
632
- return;
633
- }
634
- if (typeof body.config !== "object" || body.config === null) {
635
- badRequest(ctx.res, 'Field "config" is required and must be an object');
740
+ const failure = validateCreateIndex(body);
741
+ if (failure) {
742
+ rejectInvalid(ctx, failure);
636
743
  return;
637
744
  }
638
745
  try {
@@ -684,22 +791,16 @@ function createIndexHandlers(deps) {
684
791
  }
685
792
 
686
793
  // src/server/handlers/search.ts
687
- function rejectsCustomReducer(ctx, params) {
688
- if (params.group && typeof params.group === "object" && "reduce" in params.group) {
689
- badRequest(
690
- ctx.res,
691
- 'Custom group reducers are not available over HTTP; use "group.fields" and "group.maxPerGroup" only'
692
- );
693
- return true;
694
- }
695
- return false;
696
- }
697
794
  function createSearchHandlers(deps) {
698
- const { engine } = deps;
795
+ const { engine, limits } = deps;
699
796
  async function search(ctx) {
700
797
  const params = parseJson(ctx);
701
798
  if (!params) return;
702
- if (rejectsCustomReducer(ctx, params)) return;
799
+ const failure = validateQuery(params, limits.maxResultWindow);
800
+ if (failure) {
801
+ rejectInvalid(ctx, failure);
802
+ return;
803
+ }
703
804
  try {
704
805
  respondJson(ctx, await engine.query(ctx.params[0], params));
705
806
  } catch (err) {
@@ -709,7 +810,11 @@ function createSearchHandlers(deps) {
709
810
  async function preflight(ctx) {
710
811
  const params = parseJson(ctx);
711
812
  if (!params) return;
712
- if (rejectsCustomReducer(ctx, params)) return;
813
+ const failure = validateQuery(params, limits.maxResultWindow);
814
+ if (failure) {
815
+ rejectInvalid(ctx, failure);
816
+ return;
817
+ }
713
818
  try {
714
819
  respondJson(ctx, await engine.preflight(ctx.params[0], params));
715
820
  } catch (err) {
@@ -719,8 +824,9 @@ function createSearchHandlers(deps) {
719
824
  async function suggest(ctx) {
720
825
  const params = parseJson(ctx);
721
826
  if (!params) return;
722
- if (typeof params.prefix !== "string") {
723
- badRequest(ctx.res, 'Field "prefix" is required and must be a string');
827
+ const failure = validateSuggest(params);
828
+ if (failure) {
829
+ rejectInvalid(ctx, failure);
724
830
  return;
725
831
  }
726
832
  try {
@@ -1007,7 +1113,9 @@ function resolveLimits(limits) {
1007
1113
  maxImportBytes: limits?.maxImportBytes ?? 100 * MB,
1008
1114
  maxLineBytes: limits?.maxLineBytes ?? 4 * MB,
1009
1115
  importBatchSize: limits?.importBatchSize ?? 1e3,
1010
- maxConcurrentRequests: limits?.maxConcurrentRequests ?? 0
1116
+ maxConcurrentRequests: limits?.maxConcurrentRequests ?? 0,
1117
+ maxResultWindow: limits?.maxResultWindow ?? 1e4,
1118
+ maxFetchDocuments: limits?.maxFetchDocuments ?? 1e4
1011
1119
  };
1012
1120
  }
1013
1121
  function resolveBuild(build) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delali/narsil",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "A distributed full-text search engine with cross-language portable binary format.",
6
6
  "author": "Delali (https://sondelali.com)",