@nixxie-cms/search 1.0.1
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/LICENSE +23 -0
- package/README.md +42 -0
- package/dist/declarations/src/AlgoliaSearch.d.ts +17 -0
- package/dist/declarations/src/AlgoliaSearch.d.ts.map +1 -0
- package/dist/declarations/src/ElasticsearchSearch.d.ts +15 -0
- package/dist/declarations/src/ElasticsearchSearch.d.ts.map +1 -0
- package/dist/declarations/src/InMemorySearch.d.ts +18 -0
- package/dist/declarations/src/InMemorySearch.d.ts.map +1 -0
- package/dist/declarations/src/MeilisearchSearch.d.ts +15 -0
- package/dist/declarations/src/MeilisearchSearch.d.ts.map +1 -0
- package/dist/declarations/src/TypesenseSearch.d.ts +22 -0
- package/dist/declarations/src/TypesenseSearch.d.ts.map +1 -0
- package/dist/declarations/src/index.d.ts +11 -0
- package/dist/declarations/src/index.d.ts.map +1 -0
- package/dist/declarations/src/types.d.ts +48 -0
- package/dist/declarations/src/types.d.ts.map +1 -0
- package/dist/nixxie-cms-search.cjs.d.ts +2 -0
- package/dist/nixxie-cms-search.cjs.js +433 -0
- package/dist/nixxie-cms-search.esm.js +424 -0
- package/package.json +33 -0
- package/src/AlgoliaSearch.ts +97 -0
- package/src/ElasticsearchSearch.ts +96 -0
- package/src/InMemorySearch.ts +101 -0
- package/src/MeilisearchSearch.ts +72 -0
- package/src/TypesenseSearch.ts +98 -0
- package/src/index.ts +44 -0
- package/src/types.ts +71 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
|
|
6
|
+
|
|
7
|
+
/** Algolia backend over the REST API (no SDK dependency). */
|
|
8
|
+
class AlgoliaSearch {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.appId = config.appId;
|
|
11
|
+
this.apiKey = config.apiKey;
|
|
12
|
+
}
|
|
13
|
+
headers() {
|
|
14
|
+
return {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'X-Algolia-API-Key': this.apiKey,
|
|
17
|
+
'X-Algolia-Application-Id': this.appId
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
writeHost() {
|
|
21
|
+
return `https://${this.appId}.algolia.net`;
|
|
22
|
+
}
|
|
23
|
+
readHost() {
|
|
24
|
+
return `https://${this.appId}-dsn.algolia.net`;
|
|
25
|
+
}
|
|
26
|
+
async index(indexName, documents) {
|
|
27
|
+
const docs = Array.isArray(documents) ? documents : [documents];
|
|
28
|
+
const requests = docs.map(d => ({
|
|
29
|
+
action: 'updateObject',
|
|
30
|
+
body: {
|
|
31
|
+
...d,
|
|
32
|
+
objectID: d.id
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
const res = await fetch(`${this.writeHost()}/1/indexes/${indexName}/batch`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: this.headers(),
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
requests
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) throw new Error(`Algolia index failed (${res.status}): ${await res.text()}`);
|
|
43
|
+
}
|
|
44
|
+
async remove(indexName, id) {
|
|
45
|
+
await fetch(`${this.writeHost()}/1/indexes/${indexName}/${encodeURIComponent(id)}`, {
|
|
46
|
+
method: 'DELETE',
|
|
47
|
+
headers: this.headers()
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async search(indexName, query) {
|
|
51
|
+
var _query$offset, _query$limit, _data$hits, _ref, _data$nbHits, _data$hits2;
|
|
52
|
+
const filters = query.filter ? Object.entries(query.filter).map(([k, v]) => `${k}:${JSON.stringify(v)}`).join(' AND ') : undefined;
|
|
53
|
+
const res = await fetch(`${this.readHost()}/1/indexes/${indexName}/query`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: this.headers(),
|
|
56
|
+
// Algolia's two pagination modes are mutually exclusive: page+hitsPerPage OR offset+length.
|
|
57
|
+
// Mixing them returns HTTP 400, so use offset/length only.
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
query: query.q,
|
|
60
|
+
offset: (_query$offset = query.offset) !== null && _query$offset !== void 0 ? _query$offset : 0,
|
|
61
|
+
length: (_query$limit = query.limit) !== null && _query$limit !== void 0 ? _query$limit : 20,
|
|
62
|
+
filters
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) throw new Error(`Algolia search failed (${res.status}): ${await res.text()}`);
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
return {
|
|
68
|
+
hits: ((_data$hits = data.hits) !== null && _data$hits !== void 0 ? _data$hits : []).map(h => {
|
|
69
|
+
var _h$_rankingInfo;
|
|
70
|
+
return {
|
|
71
|
+
document: {
|
|
72
|
+
...h,
|
|
73
|
+
id: h.objectID
|
|
74
|
+
},
|
|
75
|
+
score: (_h$_rankingInfo = h._rankingInfo) === null || _h$_rankingInfo === void 0 ? void 0 : _h$_rankingInfo.userScore
|
|
76
|
+
};
|
|
77
|
+
}),
|
|
78
|
+
total: (_ref = (_data$nbHits = data.nbHits) !== null && _data$nbHits !== void 0 ? _data$nbHits : (_data$hits2 = data.hits) === null || _data$hits2 === void 0 ? void 0 : _data$hits2.length) !== null && _ref !== void 0 ? _ref : 0,
|
|
79
|
+
tookMs: data.processingTimeMS
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async clear(indexName) {
|
|
83
|
+
await fetch(`${this.writeHost()}/1/indexes/${indexName}/clear`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: this.headers()
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async close() {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Elasticsearch backend over the REST API (no SDK dependency). */
|
|
92
|
+
class ElasticsearchSearch {
|
|
93
|
+
constructor(config) {
|
|
94
|
+
this.node = config.node.replace(/\/$/, '');
|
|
95
|
+
if (config.apiKey) this.authHeader = `ApiKey ${config.apiKey}`;else if (config.username && config.password) {
|
|
96
|
+
this.authHeader = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async request(path, method, body) {
|
|
100
|
+
const res = await fetch(`${this.node}${path}`, {
|
|
101
|
+
method,
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
...(this.authHeader ? {
|
|
105
|
+
Authorization: this.authHeader
|
|
106
|
+
} : {})
|
|
107
|
+
},
|
|
108
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok && res.status !== 404) {
|
|
111
|
+
throw new Error(`Elasticsearch request failed (${res.status}): ${await res.text()}`);
|
|
112
|
+
}
|
|
113
|
+
return res.status === 404 ? undefined : res.json();
|
|
114
|
+
}
|
|
115
|
+
async index(indexName, documents) {
|
|
116
|
+
const docs = Array.isArray(documents) ? documents : [documents];
|
|
117
|
+
const body = docs.map(d => `${JSON.stringify({
|
|
118
|
+
index: {
|
|
119
|
+
_index: indexName,
|
|
120
|
+
_id: d.id
|
|
121
|
+
}
|
|
122
|
+
})}\n${JSON.stringify(d)}`).join('\n') + '\n';
|
|
123
|
+
const res = await fetch(`${this.node}/_bulk?refresh=true`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/x-ndjson',
|
|
127
|
+
...(this.authHeader ? {
|
|
128
|
+
Authorization: this.authHeader
|
|
129
|
+
} : {})
|
|
130
|
+
},
|
|
131
|
+
body
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) throw new Error(`Elasticsearch bulk failed (${res.status}): ${await res.text()}`);
|
|
134
|
+
}
|
|
135
|
+
async remove(indexName, id) {
|
|
136
|
+
await this.request(`/${indexName}/_doc/${encodeURIComponent(id)}?refresh=true`, 'DELETE');
|
|
137
|
+
}
|
|
138
|
+
async search(indexName, query) {
|
|
139
|
+
var _query$sort, _query$offset, _query$limit, _res$hits$hits, _res$hits, _res$hits2, _res$hits$total, _res$hits3;
|
|
140
|
+
const must = [query.q ? {
|
|
141
|
+
multi_match: {
|
|
142
|
+
query: query.q,
|
|
143
|
+
fields: ['*']
|
|
144
|
+
}
|
|
145
|
+
} : {
|
|
146
|
+
match_all: {}
|
|
147
|
+
}];
|
|
148
|
+
const filter = query.filter ? Object.entries(query.filter).map(([k, v]) => ({
|
|
149
|
+
term: {
|
|
150
|
+
[k]: v
|
|
151
|
+
}
|
|
152
|
+
})) : [];
|
|
153
|
+
const sort = (_query$sort = query.sort) === null || _query$sort === void 0 ? void 0 : _query$sort.map(s => {
|
|
154
|
+
const [field, dir = 'asc'] = s.split(':');
|
|
155
|
+
return {
|
|
156
|
+
[field]: dir
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
const res = await this.request(`/${indexName}/_search`, 'POST', {
|
|
160
|
+
from: (_query$offset = query.offset) !== null && _query$offset !== void 0 ? _query$offset : 0,
|
|
161
|
+
size: (_query$limit = query.limit) !== null && _query$limit !== void 0 ? _query$limit : 20,
|
|
162
|
+
query: {
|
|
163
|
+
bool: {
|
|
164
|
+
must,
|
|
165
|
+
filter
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
sort
|
|
169
|
+
});
|
|
170
|
+
const hits = (_res$hits$hits = res === null || res === void 0 || (_res$hits = res.hits) === null || _res$hits === void 0 ? void 0 : _res$hits.hits) !== null && _res$hits$hits !== void 0 ? _res$hits$hits : [];
|
|
171
|
+
return {
|
|
172
|
+
hits: hits.map(h => ({
|
|
173
|
+
document: {
|
|
174
|
+
...h._source,
|
|
175
|
+
id: h._id
|
|
176
|
+
},
|
|
177
|
+
score: h._score
|
|
178
|
+
})),
|
|
179
|
+
total: typeof (res === null || res === void 0 || (_res$hits2 = res.hits) === null || _res$hits2 === void 0 ? void 0 : _res$hits2.total) === 'object' ? res.hits.total.value : (_res$hits$total = res === null || res === void 0 || (_res$hits3 = res.hits) === null || _res$hits3 === void 0 ? void 0 : _res$hits3.total) !== null && _res$hits$total !== void 0 ? _res$hits$total : 0,
|
|
180
|
+
tookMs: res === null || res === void 0 ? void 0 : res.took
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async clear(indexName) {
|
|
184
|
+
await this.request(`/${indexName}/_delete_by_query?refresh=true`, 'POST', {
|
|
185
|
+
query: {
|
|
186
|
+
match_all: {}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async close() {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Zero-dependency in-memory search. Tokenises stringified document values and ranks by the
|
|
195
|
+
* number of query terms matched. Intended for development, tests and small datasets — swap in a
|
|
196
|
+
* real engine (Meilisearch/Typesense/Algolia/Elasticsearch) for production.
|
|
197
|
+
*/
|
|
198
|
+
class InMemorySearch {
|
|
199
|
+
constructor() {
|
|
200
|
+
_defineProperty(this, "indexes", new Map());
|
|
201
|
+
}
|
|
202
|
+
indexFor(name) {
|
|
203
|
+
let idx = this.indexes.get(name);
|
|
204
|
+
if (!idx) {
|
|
205
|
+
idx = new Map();
|
|
206
|
+
this.indexes.set(name, idx);
|
|
207
|
+
}
|
|
208
|
+
return idx;
|
|
209
|
+
}
|
|
210
|
+
async index(indexName, documents) {
|
|
211
|
+
const idx = this.indexFor(indexName);
|
|
212
|
+
for (const doc of Array.isArray(documents) ? documents : [documents]) {
|
|
213
|
+
if (!doc.id) throw new Error('Search documents must have an `id`');
|
|
214
|
+
idx.set(doc.id, doc);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async remove(indexName, id) {
|
|
218
|
+
var _this$indexes$get;
|
|
219
|
+
(_this$indexes$get = this.indexes.get(indexName)) === null || _this$indexes$get === void 0 || _this$indexes$get.delete(id);
|
|
220
|
+
}
|
|
221
|
+
async search(indexName, query) {
|
|
222
|
+
var _query$sort, _query$offset, _query$limit;
|
|
223
|
+
const start = Date.now();
|
|
224
|
+
const idx = this.indexes.get(indexName);
|
|
225
|
+
const terms = query.q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
226
|
+
const matched = [];
|
|
227
|
+
for (const doc of (_idx$values = idx === null || idx === void 0 ? void 0 : idx.values()) !== null && _idx$values !== void 0 ? _idx$values : []) {
|
|
228
|
+
var _idx$values;
|
|
229
|
+
if (query.filter && !this.matchesFilter(doc, query.filter)) continue;
|
|
230
|
+
const haystack = JSON.stringify(doc).toLowerCase();
|
|
231
|
+
const score = terms.length === 0 ? 1 : terms.filter(t => haystack.includes(t)).length;
|
|
232
|
+
if (terms.length === 0 || score > 0) matched.push({
|
|
233
|
+
document: doc,
|
|
234
|
+
score
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
matched.sort((a, b) => b.score - a.score);
|
|
238
|
+
if ((_query$sort = query.sort) !== null && _query$sort !== void 0 && _query$sort.length) this.applySort(matched, query.sort);
|
|
239
|
+
const offset = (_query$offset = query.offset) !== null && _query$offset !== void 0 ? _query$offset : 0;
|
|
240
|
+
const limit = (_query$limit = query.limit) !== null && _query$limit !== void 0 ? _query$limit : 20;
|
|
241
|
+
const page = matched.slice(offset, offset + limit);
|
|
242
|
+
return {
|
|
243
|
+
hits: page.map(m => ({
|
|
244
|
+
document: m.document,
|
|
245
|
+
score: m.score
|
|
246
|
+
})),
|
|
247
|
+
total: matched.length,
|
|
248
|
+
tookMs: Date.now() - start
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async clear(indexName) {
|
|
252
|
+
var _this$indexes$get2;
|
|
253
|
+
(_this$indexes$get2 = this.indexes.get(indexName)) === null || _this$indexes$get2 === void 0 || _this$indexes$get2.clear();
|
|
254
|
+
}
|
|
255
|
+
async close() {
|
|
256
|
+
this.indexes.clear();
|
|
257
|
+
}
|
|
258
|
+
matchesFilter(doc, filter) {
|
|
259
|
+
return Object.entries(filter).every(([k, v]) => doc[k] === v);
|
|
260
|
+
}
|
|
261
|
+
applySort(items, sort) {
|
|
262
|
+
items.sort((a, b) => {
|
|
263
|
+
for (const spec of sort) {
|
|
264
|
+
const [attr, dir = 'asc'] = spec.split(':');
|
|
265
|
+
const av = a.document[attr];
|
|
266
|
+
const bv = b.document[attr];
|
|
267
|
+
if (av === bv) continue;
|
|
268
|
+
const cmp = av > bv ? 1 : -1;
|
|
269
|
+
return dir === 'desc' ? -cmp : cmp;
|
|
270
|
+
}
|
|
271
|
+
return 0;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Meilisearch backend over the REST API (no SDK dependency). */
|
|
277
|
+
class MeilisearchSearch {
|
|
278
|
+
constructor(config) {
|
|
279
|
+
this.base = config.url.replace(/\/$/, '');
|
|
280
|
+
this.apiKey = config.apiKey;
|
|
281
|
+
}
|
|
282
|
+
async request(path, method, body) {
|
|
283
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
284
|
+
method,
|
|
285
|
+
headers: {
|
|
286
|
+
'Content-Type': 'application/json',
|
|
287
|
+
...(this.apiKey ? {
|
|
288
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
289
|
+
} : {})
|
|
290
|
+
},
|
|
291
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
292
|
+
});
|
|
293
|
+
if (!res.ok && res.status !== 404) {
|
|
294
|
+
throw new Error(`Meilisearch request failed (${res.status}): ${await res.text()}`);
|
|
295
|
+
}
|
|
296
|
+
return res.status === 404 ? undefined : res.json();
|
|
297
|
+
}
|
|
298
|
+
async index(indexName, documents) {
|
|
299
|
+
const docs = Array.isArray(documents) ? documents : [documents];
|
|
300
|
+
await this.request(`/indexes/${indexName}/documents`, 'POST', docs);
|
|
301
|
+
}
|
|
302
|
+
async remove(indexName, id) {
|
|
303
|
+
await this.request(`/indexes/${indexName}/documents/${encodeURIComponent(id)}`, 'DELETE');
|
|
304
|
+
}
|
|
305
|
+
async search(indexName, query) {
|
|
306
|
+
var _query$limit, _query$offset, _res$hits, _ref, _res$estimatedTotalHi, _res$hits2;
|
|
307
|
+
const filter = query.filter ? Object.entries(query.filter).map(([k, v]) => `${k} = ${JSON.stringify(v)}`) : undefined;
|
|
308
|
+
const res = await this.request(`/indexes/${indexName}/search`, 'POST', {
|
|
309
|
+
q: query.q,
|
|
310
|
+
limit: (_query$limit = query.limit) !== null && _query$limit !== void 0 ? _query$limit : 20,
|
|
311
|
+
offset: (_query$offset = query.offset) !== null && _query$offset !== void 0 ? _query$offset : 0,
|
|
312
|
+
filter,
|
|
313
|
+
sort: query.sort
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
hits: ((_res$hits = res === null || res === void 0 ? void 0 : res.hits) !== null && _res$hits !== void 0 ? _res$hits : []).map(document => ({
|
|
317
|
+
document
|
|
318
|
+
})),
|
|
319
|
+
total: (_ref = (_res$estimatedTotalHi = res === null || res === void 0 ? void 0 : res.estimatedTotalHits) !== null && _res$estimatedTotalHi !== void 0 ? _res$estimatedTotalHi : res === null || res === void 0 || (_res$hits2 = res.hits) === null || _res$hits2 === void 0 ? void 0 : _res$hits2.length) !== null && _ref !== void 0 ? _ref : 0,
|
|
320
|
+
tookMs: res === null || res === void 0 ? void 0 : res.processingTimeMs
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async clear(indexName) {
|
|
324
|
+
await this.request(`/indexes/${indexName}/documents`, 'DELETE');
|
|
325
|
+
}
|
|
326
|
+
async close() {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Typesense backend over the REST API (no SDK dependency).
|
|
331
|
+
* Collections must already exist; documents are upserted. Typesense has no wildcard `query_by`,
|
|
332
|
+
* so configure `queryBy` to match your collection's searchable fields.
|
|
333
|
+
* Note: `clear()` drops the entire collection (Typesense has no schema-preserving truncate),
|
|
334
|
+
* so recreate the collection before re-indexing.
|
|
335
|
+
*/
|
|
336
|
+
class TypesenseSearch {
|
|
337
|
+
constructor(config) {
|
|
338
|
+
var _config$protocol, _config$port, _config$queryBy;
|
|
339
|
+
const protocol = (_config$protocol = config.protocol) !== null && _config$protocol !== void 0 ? _config$protocol : 'http';
|
|
340
|
+
const port = (_config$port = config.port) !== null && _config$port !== void 0 ? _config$port : 8108;
|
|
341
|
+
this.base = `${protocol}://${config.host}:${port}`;
|
|
342
|
+
this.apiKey = config.apiKey;
|
|
343
|
+
this.queryBy = (_config$queryBy = config.queryBy) !== null && _config$queryBy !== void 0 ? _config$queryBy : 'title,name,body,content';
|
|
344
|
+
}
|
|
345
|
+
headers() {
|
|
346
|
+
return {
|
|
347
|
+
'Content-Type': 'application/json',
|
|
348
|
+
'X-TYPESENSE-API-KEY': this.apiKey
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async index(indexName, documents) {
|
|
352
|
+
const docs = Array.isArray(documents) ? documents : [documents];
|
|
353
|
+
const body = docs.map(d => JSON.stringify(d)).join('\n');
|
|
354
|
+
const res = await fetch(`${this.base}/collections/${indexName}/documents/import?action=upsert`, {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
headers: this.headers(),
|
|
357
|
+
body
|
|
358
|
+
});
|
|
359
|
+
if (!res.ok) throw new Error(`Typesense import failed (${res.status}): ${await res.text()}`);
|
|
360
|
+
}
|
|
361
|
+
async remove(indexName, id) {
|
|
362
|
+
await fetch(`${this.base}/collections/${indexName}/documents/${encodeURIComponent(id)}`, {
|
|
363
|
+
method: 'DELETE',
|
|
364
|
+
headers: this.headers()
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async search(indexName, query) {
|
|
368
|
+
var _query$limit, _query$offset, _query$sort, _data$hits, _data$found;
|
|
369
|
+
// Use Typesense's `offset`/`limit` params (supported since v0.23) rather than `page`/`per_page`,
|
|
370
|
+
// so arbitrary, non-page-aligned offsets return the correct window.
|
|
371
|
+
const params = new URLSearchParams({
|
|
372
|
+
q: query.q,
|
|
373
|
+
query_by: this.queryBy,
|
|
374
|
+
limit: String((_query$limit = query.limit) !== null && _query$limit !== void 0 ? _query$limit : 20),
|
|
375
|
+
offset: String((_query$offset = query.offset) !== null && _query$offset !== void 0 ? _query$offset : 0)
|
|
376
|
+
});
|
|
377
|
+
if (query.filter) {
|
|
378
|
+
params.set('filter_by', Object.entries(query.filter).map(([k, v]) => `${k}:=${JSON.stringify(v)}`).join(' && '));
|
|
379
|
+
}
|
|
380
|
+
if ((_query$sort = query.sort) !== null && _query$sort !== void 0 && _query$sort.length) params.set('sort_by', query.sort.join(','));
|
|
381
|
+
const res = await fetch(`${this.base}/collections/${indexName}/documents/search?${params}`, {
|
|
382
|
+
headers: this.headers()
|
|
383
|
+
});
|
|
384
|
+
if (!res.ok) throw new Error(`Typesense search failed (${res.status}): ${await res.text()}`);
|
|
385
|
+
const data = await res.json();
|
|
386
|
+
return {
|
|
387
|
+
hits: ((_data$hits = data.hits) !== null && _data$hits !== void 0 ? _data$hits : []).map(h => ({
|
|
388
|
+
document: h.document,
|
|
389
|
+
score: h.text_match
|
|
390
|
+
})),
|
|
391
|
+
total: (_data$found = data.found) !== null && _data$found !== void 0 ? _data$found : 0,
|
|
392
|
+
tookMs: data.search_time_ms
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async clear(indexName) {
|
|
396
|
+
const res = await fetch(`${this.base}/collections/${indexName}`, {
|
|
397
|
+
method: 'DELETE',
|
|
398
|
+
headers: this.headers()
|
|
399
|
+
});
|
|
400
|
+
// A missing collection (404) is already "cleared"; anything else is a real failure.
|
|
401
|
+
if (!res.ok && res.status !== 404) {
|
|
402
|
+
throw new Error(`Typesense clear failed (${res.status}): ${await res.text()}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async close() {}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function createSearch(config) {
|
|
409
|
+
switch (config.driver) {
|
|
410
|
+
case 'memory':
|
|
411
|
+
return new InMemorySearch();
|
|
412
|
+
case 'meilisearch':
|
|
413
|
+
return new MeilisearchSearch(config);
|
|
414
|
+
case 'typesense':
|
|
415
|
+
return new TypesenseSearch(config);
|
|
416
|
+
case 'algolia':
|
|
417
|
+
return new AlgoliaSearch(config);
|
|
418
|
+
case 'elasticsearch':
|
|
419
|
+
return new ElasticsearchSearch(config);
|
|
420
|
+
default:
|
|
421
|
+
{
|
|
422
|
+
const exhaustive = config;
|
|
423
|
+
throw new Error(`Unknown search driver: ${exhaustive.driver}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
exports.AlgoliaSearch = AlgoliaSearch;
|
|
429
|
+
exports.ElasticsearchSearch = ElasticsearchSearch;
|
|
430
|
+
exports.InMemorySearch = InMemorySearch;
|
|
431
|
+
exports.MeilisearchSearch = MeilisearchSearch;
|
|
432
|
+
exports.TypesenseSearch = TypesenseSearch;
|
|
433
|
+
exports.createSearch = createSearch;
|