@nxgt/mongo-meilisearch 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +201 -90
- package/dist/index.js.map +10 -9
- package/dist/{follow.d.ts → run/follow.d.ts} +2 -2
- package/dist/run/follow.d.ts.map +1 -0
- package/dist/{lease.d.ts → run/lease.d.ts} +2 -2
- package/dist/run/lease.d.ts.map +1 -0
- package/dist/{reindex.d.ts → run/reindex.d.ts} +2 -2
- package/dist/run/reindex.d.ts.map +1 -0
- package/dist/sync/batch.d.ts.map +1 -0
- package/dist/{context.d.ts → sync/context.d.ts} +1 -1
- package/dist/sync/context.d.ts.map +1 -0
- package/dist/sync/create-search-sync.d.ts.map +1 -0
- package/dist/sync/documents.d.ts.map +1 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/running.d.ts.map +1 -0
- package/dist/sync/start.d.ts.map +1 -0
- package/dist/sync/state.d.ts.map +1 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/syncs/create-search-syncs.d.ts +13 -0
- package/dist/syncs/create-search-syncs.d.ts.map +1 -0
- package/dist/syncs/types.d.ts +89 -0
- package/dist/syncs/types.d.ts.map +1 -0
- package/docs/README.md +3 -1
- package/docs/guide/search-syncs/lifecycle.md +301 -0
- package/docs/guide/search-syncs/wiring.md +188 -0
- package/docs/roadmap.md +18 -0
- package/docs/troubleshooting.md +213 -1
- package/package.json +4 -4
- package/dist/batch.d.ts.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/create-search-sync.d.ts.map +0 -1
- package/dist/documents.d.ts.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/follow.d.ts.map +0 -1
- package/dist/lease.d.ts.map +0 -1
- package/dist/reindex.d.ts.map +0 -1
- package/dist/running.d.ts.map +0 -1
- package/dist/start.d.ts.map +0 -1
- package/dist/state.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- /package/dist/{batch.d.ts → sync/batch.d.ts} +0 -0
- /package/dist/{create-search-sync.d.ts → sync/create-search-sync.d.ts} +0 -0
- /package/dist/{documents.d.ts → sync/documents.d.ts} +0 -0
- /package/dist/{errors.d.ts → sync/errors.d.ts} +0 -0
- /package/dist/{running.d.ts → sync/running.d.ts} +0 -0
- /package/dist/{start.d.ts → sync/start.d.ts} +0 -0
- /package/dist/{state.d.ts → sync/state.d.ts} +0 -0
- /package/dist/{types.d.ts → sync/types.d.ts} +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# The search syncs' lifecycle
|
|
2
|
+
|
|
3
|
+
One `syncIndexes`, one `reindexAll`, one `start`, one `close` for every sync the
|
|
4
|
+
[config](wiring.md) named — and one promise to watch while they run.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { bindIndex } from '@nxgt/meilisearch';
|
|
8
|
+
import { openMongo, defineMongo } from '@nxgt/mongo';
|
|
9
|
+
import { createSearchSyncs } from '@nxgt/mongo-meilisearch';
|
|
10
|
+
import * as collections from './models'; // the defineCollections
|
|
11
|
+
import { meili } from './meili'; // a Meilisearch client
|
|
12
|
+
import { articleIndex, authorIndex } from './search'; // the defineIndexes
|
|
13
|
+
|
|
14
|
+
const mongo = await openMongo(
|
|
15
|
+
defineMongo({ uri: process.env.MONGO_URI!, collections }),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const search = createSearchSyncs(mongo, {
|
|
19
|
+
articles: {
|
|
20
|
+
index: bindIndex(meili, articleIndex),
|
|
21
|
+
transform: (article) => ({ id: String(article._id), title: article.title }),
|
|
22
|
+
},
|
|
23
|
+
authors: {
|
|
24
|
+
index: bindIndex(meili, authorIndex),
|
|
25
|
+
transform: (author) => ({ id: String(author._id), name: author.name }),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await search.syncIndexes(); // create every index, apply its settings
|
|
30
|
+
await search.reindexAll(); // fill every index
|
|
31
|
+
const running = await search.start(); // follow every collection
|
|
32
|
+
void running.failed.catch(() => process.exit(1)); // await it or catch it
|
|
33
|
+
|
|
34
|
+
// …
|
|
35
|
+
|
|
36
|
+
await running.close(); // flush and stop every sync
|
|
37
|
+
await mongo.close(); // the Mongo is still yours
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## `syncs`
|
|
41
|
+
|
|
42
|
+
Each sync as `@nxgt/mongo-meilisearch` built it, under the key the config
|
|
43
|
+
used — so anything `createSearchSyncs` does not wrap is still reachable:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
search.syncs.articles.name; // 'articles:articles'
|
|
47
|
+
await search.syncs.articles.reindex(); // just this one
|
|
48
|
+
const one = await search.syncs.authors.start();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## `state()`
|
|
52
|
+
|
|
53
|
+
Where each sync stands, under its key; `undefined` for one that never
|
|
54
|
+
reindexed:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
await search.state();
|
|
58
|
+
// { articles: undefined, authors: undefined }
|
|
59
|
+
|
|
60
|
+
await search.reindexAll();
|
|
61
|
+
(await search.state()).articles?.reindexedAt; // a Date
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## `syncIndexes(options?)`
|
|
65
|
+
|
|
66
|
+
Every index the config names brought in line with its definition — created
|
|
67
|
+
with its primary key when it is missing, and only the settings that differ
|
|
68
|
+
updated — one after another, each report under its key. It is
|
|
69
|
+
`@nxgt/meilisearch`'s `syncIndex` per entry, so the options and the report
|
|
70
|
+
are that package's:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const reports = await search.syncIndexes();
|
|
74
|
+
// { articles: { uid: 'articles', created: true, changed: [ 'searchableAttributes' ], … },
|
|
75
|
+
// authors: { uid: 'authors', created: false, changed: [], … } }
|
|
76
|
+
|
|
77
|
+
await search.syncIndexes({ dryRun: true }); // what it would send, sends nothing
|
|
78
|
+
await search.syncIndexes({ wait: { timeout: 120_000 } }); // the SDK's WaitOptions, per task
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Run it twice and the second run sends nothing. The first index that throws
|
|
82
|
+
stops the rest, and the indexes after it are not looked at: a
|
|
83
|
+
`SearchIndexError` from `@nxgt/meilisearch` — `PRIMARY_KEY_MISMATCH` or
|
|
84
|
+
`TASK_FAILED` — or the SDK's own error for a Meilisearch that refused or is
|
|
85
|
+
not there. `dryRun` shows every **settings** difference at once, but not past
|
|
86
|
+
a primary-key mismatch: that one throws in a dry run too, since no setting
|
|
87
|
+
could make the index right.
|
|
88
|
+
|
|
89
|
+
It is a **deployment step**, like the Mongo's `sync()`, and it comes
|
|
90
|
+
first: `reindexAll` and `start` write documents, and an index a document write
|
|
91
|
+
creates gets the primary key but none of the definition's settings until a
|
|
92
|
+
sync runs — its filters and sorts are refused until then.
|
|
93
|
+
|
|
94
|
+
## `reindexAll()`
|
|
95
|
+
|
|
96
|
+
Every collection, one after another, each report under its key:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const reports = await search.reindexAll();
|
|
100
|
+
// { articles: { indexed: 1, skipped: 1, removed: 0 }, authors: { … } }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**The first that throws stops the rest, and the reports already collected go
|
|
104
|
+
with the rejection.** A reindex removes what a collection no longer gives, so
|
|
105
|
+
a half-finished run is not a state to keep going from: read the error, fix
|
|
106
|
+
it, run it again — a reindex that succeeded is idempotent.
|
|
107
|
+
|
|
108
|
+
It cannot run beside a follower: `@nxgt/mongo-meilisearch` throws
|
|
109
|
+
`SearchSyncError` with the code `RUNNING` for a sync of the same object that
|
|
110
|
+
is already following, or whose name another process holds the lease on, and
|
|
111
|
+
`createSearchSyncs` passes it through.
|
|
112
|
+
|
|
113
|
+
Each reindex holds its sync's lease while it runs. One that finds the lease
|
|
114
|
+
taken over — the process stalled past `leaseMs`, or the lease was removed —
|
|
115
|
+
rejects with `LEASE_LOST`, having recorded nothing, and removed nothing unless
|
|
116
|
+
the lease went while the removal itself ran; the pages it already sent stay in
|
|
117
|
+
the index. `reindexAll()` passes that through, and stops there like any other
|
|
118
|
+
failure.
|
|
119
|
+
|
|
120
|
+
## `start()`
|
|
121
|
+
|
|
122
|
+
Starts every sync and resolves once they are **all** hearing changes:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const running = await search.start();
|
|
126
|
+
running.running.articles.ready; // each RunningSearchSync is reachable
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A sync that will not start closes the ones already started before the error
|
|
130
|
+
comes back, so a caller that catches it owns nothing:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
try {
|
|
134
|
+
await search.start();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Nothing is running: no sync to close, no leak.
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A sync with nothing recorded reindexes as it starts, which on a real
|
|
141
|
+
collection is minutes — the syncs started before it are following changes
|
|
142
|
+
throughout, which is why a failure closes them rather than leaving them
|
|
143
|
+
behind.
|
|
144
|
+
|
|
145
|
+
Each sync takes the lease on its own name as it starts, so a second process
|
|
146
|
+
starting the same syncs is refused at the first name the other holds — a
|
|
147
|
+
`SearchSyncError` with the code `RUNNING`, naming the holder and when its
|
|
148
|
+
lease ends — and lets go of the names it had already taken:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { SearchSyncError } from '@nxgt/mongo-meilisearch';
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await search.start();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const held =
|
|
157
|
+
error instanceof SearchSyncError &&
|
|
158
|
+
(error.code === 'RUNNING' || error.code === 'LEASE_LOST');
|
|
159
|
+
if (held) {
|
|
160
|
+
// Another process follows these collections, or took one over while
|
|
161
|
+
// this start reindexed: wait, and try again. On RUNNING,
|
|
162
|
+
// `error.expiresAt` says until when that name's lease runs.
|
|
163
|
+
} else throw error;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A `RUNNING` the lease refused carries its `holder` and its `expiresAt`, a
|
|
168
|
+
`Date` on MongoDB's clock, so a standby can wait exactly until that lease
|
|
169
|
+
lapses; the loop is in the [troubleshooting entry](../../troubleshooting.md#search-sync-articlesarticles-is-held-by--until--wait-for-it-to-close-or-for-its-lease-to-lapse-before-you-start-it).
|
|
170
|
+
Both are `undefined` on a `LEASE_LOST`, and on the refusal of syncs that are
|
|
171
|
+
already running in this process.
|
|
172
|
+
|
|
173
|
+
A lease lasts each entry's `leaseMs` (30 s) unrenewed, so a process that
|
|
174
|
+
died holds its names that long at most. `start()` itself rejects with
|
|
175
|
+
`LEASE_LOST` when a sync's first reindex finds its lease taken over, and
|
|
176
|
+
closes the syncs already started, as for any failure. A running sync whose
|
|
177
|
+
lease another process took over stops, and `failed` rejects with
|
|
178
|
+
`LEASE_LOST`. The lease
|
|
179
|
+
itself is the single sync's: see [Following changes](../following-changes.md).
|
|
180
|
+
|
|
181
|
+
## `failed`
|
|
182
|
+
|
|
183
|
+
| Member | Type | Effect |
|
|
184
|
+
| --- | --- | --- |
|
|
185
|
+
| `running` | `{ [K in keyof I]: RunningSearchSync }` | Each running sync, with its own `ready`, `closed` and `flush` |
|
|
186
|
+
| `failed` | `Promise<never>` | Rejects with the **first** sync that stops on an error. Never resolves |
|
|
187
|
+
| `flush()` | `Promise<void>` | Sends what every sync holds and records where each one is. Stops at the first that fails |
|
|
188
|
+
| `close()` | `Promise<void>` | Flushes, then stops every sync. Idempotent |
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
running.failed.catch((error: SearchSyncError) => {
|
|
192
|
+
console.error({ sync: error.sync, code: error.code, cause: error.cause });
|
|
193
|
+
process.exit(1); // let the supervisor restart the process
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- **It must be taken.** A rejection nobody handles ends the process. `createSearchSyncs`
|
|
198
|
+
already takes each sync's own `closed`, so `failed` is the only one left to
|
|
199
|
+
you — handle it.
|
|
200
|
+
- **It never resolves.** A clean stop is not an event to wait for, so
|
|
201
|
+
`await running.failed` after `close()` waits forever. It is for `catch`, or
|
|
202
|
+
for racing against your own shutdown.
|
|
203
|
+
- **A dropped collection leaves it silent.** `@nxgt/mongo-meilisearch` treats
|
|
204
|
+
an invalidated stream as a clean stop — that sync's `closed` *resolves*
|
|
205
|
+
with `'invalidated'` — and `createSearchSyncs` forwards failures only. Watch
|
|
206
|
+
`running.running.articles.closed` when a drop has to be noticed.
|
|
207
|
+
|
|
208
|
+
## `flush()` and `close()`
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
await running.flush(); // every sync sends what it holds, and records it
|
|
212
|
+
await running.close(); // flushes, then stops; or `await using running = …`
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`flush()` stops at the first sync that fails, like `reindexAll()`: the syncs
|
|
216
|
+
after it in the config are neither sent nor recorded, and re-read those
|
|
217
|
+
changes on the next start — safe, but not free.
|
|
218
|
+
|
|
219
|
+
`close()` does not share that: it closes each sync in turn and keeps going
|
|
220
|
+
past one that fails. It swallows the failure `failed` already carried — a
|
|
221
|
+
caller should not have to wrap `close` to hear the same thing twice — and
|
|
222
|
+
throws anything else, including a **second** sync that fell over after
|
|
223
|
+
`failed` had settled, and anything that goes wrong while closing. If more
|
|
224
|
+
than one throws, it reports the first.
|
|
225
|
+
|
|
226
|
+
`close()` is idempotent, and `RunningSearchSyncs` is `AsyncDisposable`.
|
|
227
|
+
|
|
228
|
+
## A worker process
|
|
229
|
+
|
|
230
|
+
The Mongo and the search syncs, started in order and closed in reverse:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { openMongo } from '@nxgt/mongo';
|
|
234
|
+
import { createSearchSyncs } from '@nxgt/mongo-meilisearch';
|
|
235
|
+
import type { SearchSyncError } from '@nxgt/mongo-meilisearch';
|
|
236
|
+
import { config } from './db';
|
|
237
|
+
import { searchConfig } from './search';
|
|
238
|
+
|
|
239
|
+
const mongo = await openMongo(config);
|
|
240
|
+
const search = createSearchSyncs(mongo, searchConfig(meili));
|
|
241
|
+
|
|
242
|
+
const running = await search.start();
|
|
243
|
+
|
|
244
|
+
const stop = async () => {
|
|
245
|
+
await running.close(); // flushes and records where each sync is
|
|
246
|
+
await mongo.close(); // the search syncs never close it
|
|
247
|
+
process.exit(0);
|
|
248
|
+
};
|
|
249
|
+
process.on('SIGTERM', stop);
|
|
250
|
+
process.on('SIGINT', stop);
|
|
251
|
+
|
|
252
|
+
// The only promise left to handle: the first sync that falls over.
|
|
253
|
+
await running.failed.catch(async (error: SearchSyncError) => {
|
|
254
|
+
console.error({ sync: error.sync, code: error.code, cause: error.cause });
|
|
255
|
+
await running.close().catch(() => undefined);
|
|
256
|
+
await mongo.close();
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
A deployment step is the same two objects, without `start`:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
await using mongo = await openMongo(config);
|
|
265
|
+
await mongo.sync(); // the collections
|
|
266
|
+
const search = createSearchSyncs(mongo, searchConfig(meili));
|
|
267
|
+
await search.syncIndexes(); // the indexes
|
|
268
|
+
const reports = await search.reindexAll();
|
|
269
|
+
for (const [key, report] of Object.entries(reports)) {
|
|
270
|
+
console.log(`${key}: ${report.indexed} indexed, ${report.removed} removed`);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Signatures
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
interface SearchSyncs<S> {
|
|
278
|
+
readonly syncs: ByKey<S, SearchSync>;
|
|
279
|
+
state(): Promise<ByKey<S, SearchSyncState | undefined>>;
|
|
280
|
+
syncIndexes(options?: SyncOptions): Promise<ByKey<S, SyncReport>>; // @nxgt/meilisearch's
|
|
281
|
+
reindexAll(): Promise<ByKey<S, ReindexReport>>;
|
|
282
|
+
start(): Promise<RunningSearchSyncs<S>>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface RunningSearchSyncs<S> extends AsyncDisposable {
|
|
286
|
+
readonly running: ByKey<S, RunningSearchSync>;
|
|
287
|
+
readonly failed: Promise<never>;
|
|
288
|
+
flush(): Promise<void>;
|
|
289
|
+
close(): Promise<void>;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
type ByKey<S, T> = { readonly [K in keyof S]: T };
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`SearchSync`, `RunningSearchSync`, `SearchSyncState`, `ReindexReport` and
|
|
296
|
+
`SearchSyncError` are `@nxgt/mongo-meilisearch`'s, unchanged.
|
|
297
|
+
|
|
298
|
+
## Next
|
|
299
|
+
|
|
300
|
+
- [Wiring it over a Mongo](wiring.md) — the config behind all of this.
|
|
301
|
+
- [Troubleshooting](../../troubleshooting.md) — the messages, with their fixes.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Wiring it over a Mongo
|
|
2
|
+
|
|
3
|
+
`createSearchSyncs(mongo, config)` takes what
|
|
4
|
+
[`@nxgt/mongo`](https://www.npmjs.com/package/@nxgt/mongo)'s `openMongo`
|
|
5
|
+
returned and one entry per collection to follow — an index and a transform — and gives back
|
|
6
|
+
one object holding every sync.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { bindIndex } from '@nxgt/meilisearch';
|
|
10
|
+
import { createSearchSyncs } from '@nxgt/mongo-meilisearch';
|
|
11
|
+
import { mongo } from './db'; // the app's openMongo
|
|
12
|
+
import { meili } from './meili'; // a Meilisearch client
|
|
13
|
+
import { articleIndex, authorIndex } from './search'; // its defineIndexes
|
|
14
|
+
|
|
15
|
+
const search = createSearchSyncs(mongo, {
|
|
16
|
+
articles: {
|
|
17
|
+
index: bindIndex(meili, articleIndex),
|
|
18
|
+
transform: (article) =>
|
|
19
|
+
article.draft ? null : { id: String(article._id), title: article.title },
|
|
20
|
+
},
|
|
21
|
+
authors: {
|
|
22
|
+
index: bindIndex(meili, authorIndex),
|
|
23
|
+
transform: (author) => ({ id: String(author._id), name: author.name }),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`createSearchSyncs` sends nothing: it builds the syncs.
|
|
29
|
+
[`reindexAll` and `start`](lifecycle.md) are what write.
|
|
30
|
+
|
|
31
|
+
## The keys are the Mongo's own
|
|
32
|
+
|
|
33
|
+
A key is the name a collection is **exported** under — the same one
|
|
34
|
+
`mongo.db.articles` answers to, not the collection's name on the server. That
|
|
35
|
+
is what lets the transform be written inline: its argument is typed by the
|
|
36
|
+
collection the key names, and its result by the index that entry carries.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
createSearchSyncs(mongo, {
|
|
40
|
+
nowhere: { index: bindIndex(meili, articleIndex), transform: () => null },
|
|
41
|
+
});
|
|
42
|
+
// Type error: index: 'createSearchSyncs: this Mongo wires no collection called "nowhere"'
|
|
43
|
+
// TypeError at run time: createSearchSyncs: this Mongo wires no collection called "nowhere"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A key that is a member of the driver's `Db` — `command`, `watch` — is refused
|
|
47
|
+
the same way: the Mongo never wires a collection under one.
|
|
48
|
+
|
|
49
|
+
Only the collections you name are followed. A Mongo wiring twenty collections
|
|
50
|
+
and a config naming two builds two syncs.
|
|
51
|
+
|
|
52
|
+
## One database
|
|
53
|
+
|
|
54
|
+
`createSearchSyncs` follows the collections of **one** database, as `mongo.db` itself
|
|
55
|
+
does:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const mongo = await openMongo(
|
|
59
|
+
defineMongo({
|
|
60
|
+
databases: {
|
|
61
|
+
main: { uri, collections },
|
|
62
|
+
analytics: { uri, database: 'analytics', collections: events },
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
createSearchSyncs(mongo, { articles: { index, transform } });
|
|
68
|
+
// TypeError: createSearchSyncs: this Mongo holds 2 databases (main, analytics),
|
|
69
|
+
// and createSearchSyncs follows the collections of one. Build one
|
|
70
|
+
// `createSearchSyncs` per database, from a Mongo that wires that database alone
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A Mongo of several has no sole collections, so its keys are `never` and the
|
|
74
|
+
config does not compile either — every entry is refused, with the same
|
|
75
|
+
`wires no collection called "…"` message. Build one Mongo per database,
|
|
76
|
+
and one `createSearchSyncs` over each.
|
|
77
|
+
|
|
78
|
+
## What an entry takes
|
|
79
|
+
|
|
80
|
+
Everything [`createSearchSync`](../sync-lifecycle.md)
|
|
81
|
+
takes **except `collection`**, which the Mongo already holds:
|
|
82
|
+
|
|
83
|
+
| Option | Type | Default | Effect |
|
|
84
|
+
| --- | --- | --- | --- |
|
|
85
|
+
| `index` | `TypedIndex<I>` | — | Where the documents go: from `bindIndex` |
|
|
86
|
+
| `transform` | `Transform<Col, I>` | — | The document as the index holds it; `null` keeps it out, and takes it out |
|
|
87
|
+
| `toIndexId` | `ToIndexId<Col, I>` | `String` | The index id of a `_id`. Optional while the index's ids are strings, required otherwise |
|
|
88
|
+
| `name` | `string` | `'<collection>:<index uid>'` | What this sync's resume point is recorded under |
|
|
89
|
+
| `stateCollection` | `string` | `'nxgt_search_sync'` | Where resume points are kept, in the Mongo's database |
|
|
90
|
+
| `batchSize` | `number` | `500` | Changes, or documents, sent at once |
|
|
91
|
+
| `flushIntervalMs` | `number` | `1000` | How long a change waits for others |
|
|
92
|
+
| `positionIntervalMs` | `number` | `60000` | How often a quiet sync records where its stream is |
|
|
93
|
+
| `leaseMs` | `number` | `30000` | How long the lease on this sync's name lasts unrenewed; a running sync renews it every third of that |
|
|
94
|
+
| `pageSize` | `number` | `100` | Documents a reindex reads per page |
|
|
95
|
+
| `onHistoryLost` | `'reindex' \| 'fail'` | `'reindex'` | What `start` does when a resume point is older than the server's history |
|
|
96
|
+
|
|
97
|
+
Each entry sets its own: a large collection can take a bigger `pageSize`
|
|
98
|
+
while a small one keeps the default.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const search = createSearchSyncs(mongo, {
|
|
102
|
+
articles: {
|
|
103
|
+
index: bindIndex(meili, articleIndex),
|
|
104
|
+
transform: toArticleHit,
|
|
105
|
+
pageSize: 500,
|
|
106
|
+
onHistoryLost: 'fail',
|
|
107
|
+
},
|
|
108
|
+
authors: {
|
|
109
|
+
index: bindIndex(meili, authorIndex),
|
|
110
|
+
transform: toAuthorHit,
|
|
111
|
+
name: 'authors:public',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Two `createSearchSyncs` built from one config share their resume points**, since
|
|
117
|
+
the default `name` is `'<collection>:<index uid>'`. Give `name` when two are
|
|
118
|
+
meant to be different.
|
|
119
|
+
|
|
120
|
+
## The whole wiring
|
|
121
|
+
|
|
122
|
+
The clients, the Mongo, the index settings, the syncs:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// src/search/index.ts
|
|
126
|
+
import { bindIndex } from '@nxgt/meilisearch';
|
|
127
|
+
import { openMongo } from '@nxgt/mongo';
|
|
128
|
+
import { createSearchSyncs } from '@nxgt/mongo-meilisearch';
|
|
129
|
+
import { Meilisearch } from 'meilisearch';
|
|
130
|
+
import { config } from '../db';
|
|
131
|
+
import { articleIndex, authorIndex, toArticleHit, toAuthorHit } from './indexes';
|
|
132
|
+
|
|
133
|
+
export const meili = new Meilisearch({
|
|
134
|
+
host: process.env.MEILI_HOST!,
|
|
135
|
+
apiKey: process.env.MEILI_KEY!,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export async function buildSearch() {
|
|
139
|
+
const mongo = await openMongo(config);
|
|
140
|
+
|
|
141
|
+
const search = createSearchSyncs(mongo, {
|
|
142
|
+
articles: { index: bindIndex(meili, articleIndex), transform: toArticleHit },
|
|
143
|
+
authors: { index: bindIndex(meili, authorIndex), transform: toAuthorHit },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Every index created and its settings applied, before anything writes
|
|
147
|
+
// documents: a deployment step, beside `mongo.sync()`.
|
|
148
|
+
await search.syncIndexes();
|
|
149
|
+
|
|
150
|
+
return { mongo, search };
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`createSearchSyncs` **does not own the Mongo**: closing its syncs stops them
|
|
155
|
+
and nothing else, and `mongo.close()` stays the caller's to make.
|
|
156
|
+
|
|
157
|
+
## Signatures
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
function createSearchSyncs<C, const I extends IndexMap<I>>(
|
|
161
|
+
mongo: Mongo<C>,
|
|
162
|
+
config: SearchSyncsConfig<C, I>,
|
|
163
|
+
): SearchSyncs<I>;
|
|
164
|
+
|
|
165
|
+
type SearchSyncEntry<Col, I> = Omit<SearchSyncOptions<Col, I>, 'collection'>;
|
|
166
|
+
|
|
167
|
+
type SearchSyncsConfig<C, I extends IndexMap<I>> = {
|
|
168
|
+
[K in keyof I]: K extends keyof SoleCollections<C>
|
|
169
|
+
? SoleCollections<C>[K] extends infer Col extends AnyCollectionDefinition
|
|
170
|
+
? SearchSyncEntry<Col, I[K]>
|
|
171
|
+
: never
|
|
172
|
+
: {
|
|
173
|
+
index: `createSearchSyncs: this Mongo wires no collection called "${K & string}"`;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
type SoleCollections<C>; // what a Mongo's sole database holds, by export name
|
|
178
|
+
type IndexMap<I> = { [K in keyof I]: AnyIndexDefinition };
|
|
179
|
+
type ByKey<S, T> = { readonly [K in keyof S]: T };
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`I` is inferred from each entry's `index` alone, which is what lets a
|
|
183
|
+
`transform` be written inline.
|
|
184
|
+
|
|
185
|
+
## Next
|
|
186
|
+
|
|
187
|
+
- [The search syncs' lifecycle](lifecycle.md) — `reindexAll`, `start`, `failed`,
|
|
188
|
+
`flush` and `close`.
|
package/docs/roadmap.md
CHANGED
|
@@ -26,12 +26,30 @@ _Nothing queued._
|
|
|
26
26
|
but a change to one of them does not reach the index: the sync follows the
|
|
27
27
|
collection it was given. Run a sync per collection that has to move the
|
|
28
28
|
index.
|
|
29
|
+
- **`createSearchSyncs` over several databases** — a Mongo holding more than
|
|
30
|
+
one gives `never` for its keys, and `createSearchSyncs` throws naming them.
|
|
31
|
+
Build one `createSearchSyncs` per database, from a Mongo that wires that
|
|
32
|
+
database alone.
|
|
33
|
+
- **`createSearchSyncs` owning the Mongo** — closing the search syncs stops
|
|
34
|
+
them and nothing else: the clients, the databases and the collections
|
|
35
|
+
belong to the Mongo, and `mongo.close()` stays the caller's.
|
|
36
|
+
- **A lock of its own for `createSearchSyncs`** — the lease already provides
|
|
37
|
+
it: each sync takes a lease on its name, so a second process starting the
|
|
38
|
+
same syncs is refused with `RUNNING`.
|
|
29
39
|
- **One index fed by two collections, or by another writer** — a reindex
|
|
30
40
|
removes every document the collection does not give it, whoever wrote it.
|
|
31
41
|
The sync owns its index.
|
|
32
42
|
|
|
33
43
|
## Shipped
|
|
34
44
|
|
|
45
|
+
- **Several collections from one config** — `createSearchSyncs(mongo, config)`
|
|
46
|
+
over what `@nxgt/mongo`'s `openMongo` returns: one entry per collection,
|
|
47
|
+
under the key the Mongo wires it as, and one `syncIndexes`, `reindexAll`,
|
|
48
|
+
`start` and `close` for all of them, with `failed` for the first sync that
|
|
49
|
+
stops. It is `@nxgt/mongo-search-kit`, folded in and renamed
|
|
50
|
+
(`createSearchKit` is `createSearchSyncs`), whose own roadmap is carried
|
|
51
|
+
here: `syncIndexes()` and one follower per sync name across processes came
|
|
52
|
+
with its 0.2.0, and the package is now a deprecated re-export — 0.5.0.
|
|
35
53
|
- **A standby waits exactly for the holder** — a `RUNNING` the lease refused
|
|
36
54
|
carries `holder` and `expiresAt`, read from the lease document, so a process
|
|
37
55
|
waiting for the name sleeps until the holder's lease lapses instead of a
|