@nebutra/search 0.1.2 → 3.0.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 +21 -77
- package/dist/factory.d.ts +3 -11
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +4 -58
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -6
- package/dist/types.d.ts +4 -35
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -21
- package/dist/providers/algolia.d.ts +0 -20
- package/dist/providers/algolia.d.ts.map +0 -1
- package/dist/providers/algolia.js +0 -297
- package/dist/providers/meilisearch.d.ts +0 -17
- package/dist/providers/meilisearch.d.ts.map +0 -1
- package/dist/providers/meilisearch.js +0 -279
- package/dist/providers/typesense.d.ts +0 -22
- package/dist/providers/typesense.d.ts.map +0 -1
- package/dist/providers/typesense.js +0 -352
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { logger } from "@nebutra/logger";
|
|
2
|
-
import { algoliasearch } from "algoliasearch";
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Algolia Provider — Managed SaaS, global CDN, zero-ops
|
|
5
|
-
// =============================================================================
|
|
6
|
-
export class AlgoliaProvider {
|
|
7
|
-
name = "algolia";
|
|
8
|
-
searchClient;
|
|
9
|
-
adminClient;
|
|
10
|
-
constructor(config) {
|
|
11
|
-
const _appId = config?.appId ?? process.env.ALGOLIA_APP_ID;
|
|
12
|
-
const searchKey = config?.searchKey ?? process.env.ALGOLIA_SEARCH_KEY;
|
|
13
|
-
const adminKey = config?.adminKey ?? process.env.ALGOLIA_ADMIN_KEY;
|
|
14
|
-
if (!_appId) {
|
|
15
|
-
throw new Error("[search:algolia] Missing ALGOLIA_APP_ID environment variable");
|
|
16
|
-
}
|
|
17
|
-
if (!searchKey) {
|
|
18
|
-
throw new Error("[search:algolia] Missing ALGOLIA_SEARCH_KEY environment variable");
|
|
19
|
-
}
|
|
20
|
-
if (!adminKey) {
|
|
21
|
-
throw new Error("[search:algolia] Missing ALGOLIA_ADMIN_KEY environment variable");
|
|
22
|
-
}
|
|
23
|
-
this.searchClient = algoliasearch(_appId, searchKey);
|
|
24
|
-
this.adminClient = algoliasearch(_appId, adminKey);
|
|
25
|
-
logger.info("[search:algolia] Initializing", { appId: _appId });
|
|
26
|
-
}
|
|
27
|
-
async indexDocument(index, doc) {
|
|
28
|
-
try {
|
|
29
|
-
const indexName = this._getIndexName(index, doc.tenantId);
|
|
30
|
-
const algoliaIndex = this.adminClient.initIndex(indexName);
|
|
31
|
-
const record = this._prepareRecord(doc);
|
|
32
|
-
await algoliaIndex.saveObject(record);
|
|
33
|
-
logger.debug("[search:algolia] Indexed document", {
|
|
34
|
-
index: indexName,
|
|
35
|
-
docId: doc.id,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
logger.error("[search:algolia] Failed to index document", {
|
|
40
|
-
index,
|
|
41
|
-
docId: doc.id,
|
|
42
|
-
error: error instanceof Error ? error.message : String(error),
|
|
43
|
-
});
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async indexDocuments(index, docs) {
|
|
48
|
-
if (docs.length === 0)
|
|
49
|
-
return;
|
|
50
|
-
try {
|
|
51
|
-
// Group documents by tenant
|
|
52
|
-
const byTenant = new Map();
|
|
53
|
-
for (const doc of docs) {
|
|
54
|
-
const tenant = doc.tenantId;
|
|
55
|
-
if (!byTenant.has(tenant)) {
|
|
56
|
-
byTenant.set(tenant, []);
|
|
57
|
-
}
|
|
58
|
-
byTenant.get(tenant)?.push(doc);
|
|
59
|
-
}
|
|
60
|
-
// Index each tenant's documents
|
|
61
|
-
for (const [tenantId, tenantDocs] of byTenant.entries()) {
|
|
62
|
-
const indexName = this._getIndexName(index, tenantId);
|
|
63
|
-
const algoliaIndex = this.adminClient.initIndex(indexName);
|
|
64
|
-
const records = tenantDocs.map((doc) => this._prepareRecord(doc));
|
|
65
|
-
await algoliaIndex.saveObjects(records);
|
|
66
|
-
logger.debug("[search:algolia] Indexed batch", {
|
|
67
|
-
index: indexName,
|
|
68
|
-
count: tenantDocs.length,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
logger.error("[search:algolia] Failed to index batch", {
|
|
74
|
-
index,
|
|
75
|
-
count: docs.length,
|
|
76
|
-
error: error instanceof Error ? error.message : String(error),
|
|
77
|
-
});
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async search(index, query) {
|
|
82
|
-
try {
|
|
83
|
-
const indexName = this._getIndexName(index, query.tenantId);
|
|
84
|
-
const algoliaIndex = this.searchClient.initIndex(indexName);
|
|
85
|
-
const page = query.page ?? 1;
|
|
86
|
-
const hitsPerPage = Math.min(query.hitsPerPage ?? 20, 100);
|
|
87
|
-
// Build facet filters
|
|
88
|
-
const facetFilters = [];
|
|
89
|
-
if (query.filters) {
|
|
90
|
-
for (const [key, value] of Object.entries(query.filters)) {
|
|
91
|
-
if (typeof value === "string") {
|
|
92
|
-
facetFilters.push(`${key}:${value}`);
|
|
93
|
-
}
|
|
94
|
-
else if (typeof value === "number") {
|
|
95
|
-
facetFilters.push(`${key}:${value}`);
|
|
96
|
-
}
|
|
97
|
-
else if (typeof value === "boolean") {
|
|
98
|
-
facetFilters.push(`${key}:${value}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (query.tenantId) {
|
|
103
|
-
facetFilters.push(`tenantId:${query.tenantId}`);
|
|
104
|
-
}
|
|
105
|
-
const startMs = performance.now();
|
|
106
|
-
const result = await algoliaIndex.search(query.query, {
|
|
107
|
-
page: page - 1, // Algolia uses 0-based pagination
|
|
108
|
-
hitsPerPage,
|
|
109
|
-
facets: query.facets,
|
|
110
|
-
facetFilters,
|
|
111
|
-
typoTolerance: query.typoTolerance !== false,
|
|
112
|
-
highlightPreTag: "<mark>",
|
|
113
|
-
highlightPostTag: "</mark>",
|
|
114
|
-
attributesToHighlight: query.highlightFields,
|
|
115
|
-
attributesToSnippet: query.highlightFields ? undefined : undefined,
|
|
116
|
-
});
|
|
117
|
-
const processingTimeMs = performance.now() - startMs;
|
|
118
|
-
const hits = result.hits.map((hit) => ({
|
|
119
|
-
doc: this._unprepareRecord(hit),
|
|
120
|
-
score: hit._rankingInfo?.nbExactMatches ?? hit._score ?? 1,
|
|
121
|
-
highlights: hit._highlightResult
|
|
122
|
-
? Object.fromEntries(Object.entries(hit._highlightResult).map(([key, val]) => [
|
|
123
|
-
key,
|
|
124
|
-
val.value,
|
|
125
|
-
]))
|
|
126
|
-
: undefined,
|
|
127
|
-
}));
|
|
128
|
-
const totalPages = Math.ceil(result.nbHits / hitsPerPage);
|
|
129
|
-
const facetDistribution = {};
|
|
130
|
-
if (result.facets) {
|
|
131
|
-
for (const [facetName, facetValues] of Object.entries(result.facets)) {
|
|
132
|
-
facetDistribution[facetName] = facetValues;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
logger.debug("[search:algolia] Search completed", {
|
|
136
|
-
index: indexName,
|
|
137
|
-
query: query.query,
|
|
138
|
-
hits: hits.length,
|
|
139
|
-
totalHits: result.nbHits,
|
|
140
|
-
processingTimeMs,
|
|
141
|
-
});
|
|
142
|
-
return {
|
|
143
|
-
hits,
|
|
144
|
-
totalHits: result.nbHits,
|
|
145
|
-
processingTimeMs,
|
|
146
|
-
facetDistribution: (Object.keys(facetDistribution).length > 0 ? facetDistribution : undefined) ?? {},
|
|
147
|
-
page,
|
|
148
|
-
hitsPerPage,
|
|
149
|
-
totalPages,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
logger.error("[search:algolia] Search failed", {
|
|
154
|
-
index,
|
|
155
|
-
query: query.query,
|
|
156
|
-
error: error instanceof Error ? error.message : String(error),
|
|
157
|
-
});
|
|
158
|
-
throw error;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
async deleteDocument(index, docId, tenantId) {
|
|
162
|
-
try {
|
|
163
|
-
const indexName = this._getIndexName(index, tenantId);
|
|
164
|
-
const algoliaIndex = this.adminClient.initIndex(indexName);
|
|
165
|
-
await algoliaIndex.deleteObject(docId);
|
|
166
|
-
logger.debug("[search:algolia] Deleted document", {
|
|
167
|
-
index: indexName,
|
|
168
|
-
docId,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
logger.error("[search:algolia] Failed to delete document", {
|
|
173
|
-
index,
|
|
174
|
-
docId,
|
|
175
|
-
error: error instanceof Error ? error.message : String(error),
|
|
176
|
-
});
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async deleteByFilter(index, filters) {
|
|
181
|
-
try {
|
|
182
|
-
const indexName = this._getIndexName(index);
|
|
183
|
-
const algoliaIndex = this.adminClient.initIndex(indexName);
|
|
184
|
-
// Build facet filters
|
|
185
|
-
const facetFilters = [];
|
|
186
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
187
|
-
if (typeof value === "string") {
|
|
188
|
-
facetFilters.push(`${key}:${value}`);
|
|
189
|
-
}
|
|
190
|
-
else if (typeof value === "number") {
|
|
191
|
-
facetFilters.push(`${key}:${value}`);
|
|
192
|
-
}
|
|
193
|
-
else if (typeof value === "boolean") {
|
|
194
|
-
facetFilters.push(`${key}:${value}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// Search for matching documents
|
|
198
|
-
const result = await this.searchClient.initIndex(indexName).search("", {
|
|
199
|
-
facetFilters,
|
|
200
|
-
hitsPerPage: 10000,
|
|
201
|
-
});
|
|
202
|
-
if (result.hits.length > 0) {
|
|
203
|
-
const docIds = result.hits.map((hit) => hit.objectID);
|
|
204
|
-
await algoliaIndex.deleteObjects(docIds);
|
|
205
|
-
logger.debug("[search:algolia] Deleted documents by filter", {
|
|
206
|
-
index: indexName,
|
|
207
|
-
filters,
|
|
208
|
-
deletedCount: docIds.length,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
logger.error("[search:algolia] Failed to delete by filter", {
|
|
214
|
-
index,
|
|
215
|
-
filters,
|
|
216
|
-
error: error instanceof Error ? error.message : String(error),
|
|
217
|
-
});
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
async createIndex(index, settings) {
|
|
222
|
-
try {
|
|
223
|
-
const indexName = this._getIndexName(index);
|
|
224
|
-
const algoliaIndex = this.adminClient.initIndex(indexName);
|
|
225
|
-
// Apply settings
|
|
226
|
-
await this._applySettings(algoliaIndex, settings);
|
|
227
|
-
logger.info("[search:algolia] Index created/configured", { index: indexName });
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
logger.error("[search:algolia] Failed to create index", {
|
|
231
|
-
index,
|
|
232
|
-
error: error instanceof Error ? error.message : String(error),
|
|
233
|
-
});
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
async updateSettings(index, settings) {
|
|
238
|
-
try {
|
|
239
|
-
const indexName = this._getIndexName(index);
|
|
240
|
-
const algoliaIndex = this.adminClient.initIndex(indexName);
|
|
241
|
-
await this._applySettings(algoliaIndex, settings);
|
|
242
|
-
logger.info("[search:algolia] Settings updated", { index: indexName });
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
logger.error("[search:algolia] Failed to update settings", {
|
|
246
|
-
index,
|
|
247
|
-
error: error instanceof Error ? error.message : String(error),
|
|
248
|
-
});
|
|
249
|
-
throw error;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
async close() {
|
|
253
|
-
logger.info("[search:algolia] Closing connection");
|
|
254
|
-
// Algolia doesn't require explicit connection cleanup
|
|
255
|
-
}
|
|
256
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
257
|
-
_getIndexName(baseIndex, tenantId) {
|
|
258
|
-
if (tenantId) {
|
|
259
|
-
return `${baseIndex}__${tenantId}`;
|
|
260
|
-
}
|
|
261
|
-
return baseIndex;
|
|
262
|
-
}
|
|
263
|
-
_prepareRecord(doc) {
|
|
264
|
-
return {
|
|
265
|
-
objectID: doc.id,
|
|
266
|
-
...doc,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
_unprepareRecord(record) {
|
|
270
|
-
const { objectID, ...rest } = record;
|
|
271
|
-
return {
|
|
272
|
-
id: objectID,
|
|
273
|
-
...rest,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
async _applySettings(index, settings) {
|
|
277
|
-
const algoliaSettings = {};
|
|
278
|
-
if (settings.searchableAttributes) {
|
|
279
|
-
algoliaSettings.searchableAttributes = settings.searchableAttributes;
|
|
280
|
-
}
|
|
281
|
-
if (settings.filterableAttributes) {
|
|
282
|
-
algoliaSettings.filterableAttributes = settings.filterableAttributes;
|
|
283
|
-
}
|
|
284
|
-
if (settings.facetableAttributes) {
|
|
285
|
-
algoliaSettings.facets = settings.facetableAttributes;
|
|
286
|
-
}
|
|
287
|
-
if (settings.rankingRules) {
|
|
288
|
-
algoliaSettings.ranking = settings.rankingRules;
|
|
289
|
-
}
|
|
290
|
-
if (settings.synonyms) {
|
|
291
|
-
// Algolia uses different synonym format, skip for now
|
|
292
|
-
}
|
|
293
|
-
if (Object.keys(algoliaSettings).length > 0) {
|
|
294
|
-
await index.setSettings(algoliaSettings);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { IndexSettings, MeilisearchConfig, SearchDocument, SearchProvider, SearchQuery, SearchResult } from "../types";
|
|
2
|
-
export declare class MeilisearchProvider implements SearchProvider {
|
|
3
|
-
readonly name = "meilisearch";
|
|
4
|
-
private client;
|
|
5
|
-
constructor(config?: MeilisearchConfig);
|
|
6
|
-
indexDocument<T extends SearchDocument>(index: string, doc: T): Promise<void>;
|
|
7
|
-
indexDocuments<T extends SearchDocument>(index: string, docs: T[]): Promise<void>;
|
|
8
|
-
search<T extends SearchDocument = SearchDocument>(index: string, query: SearchQuery): Promise<SearchResult<T>>;
|
|
9
|
-
deleteDocument(index: string, docId: string, tenantId?: string): Promise<void>;
|
|
10
|
-
deleteByFilter(index: string, filters: Record<string, string | number | boolean>): Promise<void>;
|
|
11
|
-
createIndex(index: string, settings: IndexSettings): Promise<void>;
|
|
12
|
-
updateSettings(index: string, settings: IndexSettings): Promise<void>;
|
|
13
|
-
close(): Promise<void>;
|
|
14
|
-
private _getTenantIndex;
|
|
15
|
-
private _applySettings;
|
|
16
|
-
}
|
|
17
|
-
//# sourceMappingURL=meilisearch.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"meilisearch.d.ts","sourceRoot":"","sources":["../../src/providers/meilisearch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,WAAW,EACX,YAAY,EACb,MAAM,UAAU,CAAC;AAMlB,qBAAa,mBAAoB,YAAW,cAAc;IACxD,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,OAAO,CAAC,MAAM,CAAc;gBAEhB,MAAM,CAAC,EAAE,iBAAiB;IAiBhC,aAAa,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7E,cAAc,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAoCjF,MAAM,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACpD,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAuFrB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB9E,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GACjD,OAAO,CAAC,IAAI,CAAC;IAyCV,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAclE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAarE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,OAAO,CAAC,eAAe;YAOT,cAAc;CAiC7B"}
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { logger } from "@nebutra/logger";
|
|
2
|
-
import { MeiliSearch } from "meilisearch";
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Meilisearch Provider — Developer-friendly, typo-tolerant search
|
|
5
|
-
// =============================================================================
|
|
6
|
-
export class MeilisearchProvider {
|
|
7
|
-
name = "meilisearch";
|
|
8
|
-
client;
|
|
9
|
-
constructor(config) {
|
|
10
|
-
const url = config?.url ?? process.env.MEILISEARCH_URL ?? "http://localhost:7700";
|
|
11
|
-
const _apiKey = config?.apiKey ?? process.env.MEILISEARCH_API_KEY;
|
|
12
|
-
const timeout = config?.timeout ?? 30000;
|
|
13
|
-
logger.info("[search:meilisearch] Initializing", { url });
|
|
14
|
-
const clientConfig = {
|
|
15
|
-
host: url,
|
|
16
|
-
timeout,
|
|
17
|
-
};
|
|
18
|
-
if (_apiKey) {
|
|
19
|
-
clientConfig.apiKey = _apiKey;
|
|
20
|
-
}
|
|
21
|
-
this.client = new MeiliSearch(clientConfig);
|
|
22
|
-
}
|
|
23
|
-
async indexDocument(index, doc) {
|
|
24
|
-
try {
|
|
25
|
-
const indexWithTenant = this._getTenantIndex(index, doc.tenantId);
|
|
26
|
-
await this.client.index(indexWithTenant).addDocuments([doc]);
|
|
27
|
-
logger.debug("[search:meilisearch] Indexed document", {
|
|
28
|
-
index: indexWithTenant,
|
|
29
|
-
docId: doc.id,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
logger.error("[search:meilisearch] Failed to index document", {
|
|
34
|
-
index,
|
|
35
|
-
docId: doc.id,
|
|
36
|
-
error: error instanceof Error ? error.message : String(error),
|
|
37
|
-
});
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
async indexDocuments(index, docs) {
|
|
42
|
-
if (docs.length === 0)
|
|
43
|
-
return;
|
|
44
|
-
try {
|
|
45
|
-
// Group documents by tenant for proper filtering
|
|
46
|
-
const byTenant = new Map();
|
|
47
|
-
for (const doc of docs) {
|
|
48
|
-
const tenant = doc.tenantId;
|
|
49
|
-
if (!byTenant.has(tenant)) {
|
|
50
|
-
byTenant.set(tenant, []);
|
|
51
|
-
}
|
|
52
|
-
byTenant.get(tenant)?.push(doc);
|
|
53
|
-
}
|
|
54
|
-
// Index each tenant's documents to their own index
|
|
55
|
-
for (const [tenantId, tenantDocs] of byTenant.entries()) {
|
|
56
|
-
const indexWithTenant = this._getTenantIndex(index, tenantId);
|
|
57
|
-
await this.client
|
|
58
|
-
.index(indexWithTenant)
|
|
59
|
-
.addDocuments(tenantDocs);
|
|
60
|
-
logger.debug("[search:meilisearch] Indexed batch", {
|
|
61
|
-
index: indexWithTenant,
|
|
62
|
-
count: tenantDocs.length,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
logger.error("[search:meilisearch] Failed to index batch", {
|
|
68
|
-
index,
|
|
69
|
-
count: docs.length,
|
|
70
|
-
error: error instanceof Error ? error.message : String(error),
|
|
71
|
-
});
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
async search(index, query) {
|
|
76
|
-
try {
|
|
77
|
-
const indexWithTenant = this._getTenantIndex(index, query.tenantId);
|
|
78
|
-
const page = query.page ?? 1;
|
|
79
|
-
const hitsPerPage = Math.min(query.hitsPerPage ?? 20, 100);
|
|
80
|
-
const offset = (page - 1) * hitsPerPage;
|
|
81
|
-
// Build filter string for Meilisearch
|
|
82
|
-
let filter;
|
|
83
|
-
if (query.filters || query.tenantId) {
|
|
84
|
-
const filterParts = [];
|
|
85
|
-
// Add tenant filter if present
|
|
86
|
-
if (query.tenantId) {
|
|
87
|
-
filterParts.push(`tenantId = "${query.tenantId}"`);
|
|
88
|
-
}
|
|
89
|
-
// Add additional filters
|
|
90
|
-
if (query.filters) {
|
|
91
|
-
for (const [key, value] of Object.entries(query.filters)) {
|
|
92
|
-
if (typeof value === "string") {
|
|
93
|
-
filterParts.push(`${key} = "${value}"`);
|
|
94
|
-
}
|
|
95
|
-
else if (typeof value === "number") {
|
|
96
|
-
filterParts.push(`${key} = ${value}`);
|
|
97
|
-
}
|
|
98
|
-
else if (typeof value === "boolean") {
|
|
99
|
-
filterParts.push(`${key} = ${value}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (filterParts.length > 0) {
|
|
104
|
-
filter = filterParts.join(" AND ");
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const startMs = performance.now();
|
|
108
|
-
const searchParams = {
|
|
109
|
-
offset,
|
|
110
|
-
limit: hitsPerPage,
|
|
111
|
-
highlightPreTag: "<mark>",
|
|
112
|
-
highlightPostTag: "</mark>",
|
|
113
|
-
};
|
|
114
|
-
if (filter !== undefined)
|
|
115
|
-
searchParams.filter = filter;
|
|
116
|
-
if (query.facets !== undefined)
|
|
117
|
-
searchParams.facets = query.facets;
|
|
118
|
-
if (query.sort !== undefined)
|
|
119
|
-
searchParams.sort = query.sort;
|
|
120
|
-
if (query.highlightFields !== undefined)
|
|
121
|
-
searchParams.attributesToHighlight = query.highlightFields;
|
|
122
|
-
const result = await this.client.index(indexWithTenant).search(query.query, searchParams);
|
|
123
|
-
const processingTimeMs = performance.now() - startMs;
|
|
124
|
-
const hits = (result.hits || []).map((hit) => ({
|
|
125
|
-
doc: hit,
|
|
126
|
-
score: hit._rankingScore ?? 1,
|
|
127
|
-
highlights: hit._formatted ?? undefined,
|
|
128
|
-
}));
|
|
129
|
-
const totalHits = result.estimatedTotalHits ?? 0;
|
|
130
|
-
const totalPages = Math.ceil(totalHits / hitsPerPage);
|
|
131
|
-
logger.debug("[search:meilisearch] Search completed", {
|
|
132
|
-
index: indexWithTenant,
|
|
133
|
-
query: query.query,
|
|
134
|
-
hits: hits.length,
|
|
135
|
-
totalHits,
|
|
136
|
-
processingTimeMs,
|
|
137
|
-
});
|
|
138
|
-
return {
|
|
139
|
-
hits,
|
|
140
|
-
totalHits,
|
|
141
|
-
processingTimeMs,
|
|
142
|
-
facetDistribution: result.facetDistribution ?? {},
|
|
143
|
-
page,
|
|
144
|
-
hitsPerPage,
|
|
145
|
-
totalPages,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
logger.error("[search:meilisearch] Search failed", {
|
|
150
|
-
index,
|
|
151
|
-
query: query.query,
|
|
152
|
-
error: error instanceof Error ? error.message : String(error),
|
|
153
|
-
});
|
|
154
|
-
throw error;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
async deleteDocument(index, docId, tenantId) {
|
|
158
|
-
try {
|
|
159
|
-
const indexWithTenant = this._getTenantIndex(index, tenantId);
|
|
160
|
-
await this.client.index(indexWithTenant).deleteDocument(docId);
|
|
161
|
-
logger.debug("[search:meilisearch] Deleted document", {
|
|
162
|
-
index: indexWithTenant,
|
|
163
|
-
docId,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
logger.error("[search:meilisearch] Failed to delete document", {
|
|
168
|
-
index,
|
|
169
|
-
docId,
|
|
170
|
-
error: error instanceof Error ? error.message : String(error),
|
|
171
|
-
});
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
async deleteByFilter(index, filters) {
|
|
176
|
-
try {
|
|
177
|
-
// Build filter string
|
|
178
|
-
const filterParts = [];
|
|
179
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
180
|
-
if (typeof value === "string") {
|
|
181
|
-
filterParts.push(`${key} = "${value}"`);
|
|
182
|
-
}
|
|
183
|
-
else if (typeof value === "number") {
|
|
184
|
-
filterParts.push(`${key} = ${value}`);
|
|
185
|
-
}
|
|
186
|
-
else if (typeof value === "boolean") {
|
|
187
|
-
filterParts.push(`${key} = ${value}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const filter = filterParts.join(" AND ");
|
|
191
|
-
// Meilisearch doesn't have a direct deleteByFilter, so we search then delete
|
|
192
|
-
const result = await this.client.index(index).search("", {
|
|
193
|
-
filter,
|
|
194
|
-
limit: 10000,
|
|
195
|
-
});
|
|
196
|
-
if (result.hits.length > 0) {
|
|
197
|
-
const docIds = result.hits.map((hit) => hit.id);
|
|
198
|
-
await this.client.index(index).deleteDocuments(docIds);
|
|
199
|
-
logger.debug("[search:meilisearch] Deleted documents by filter", {
|
|
200
|
-
index,
|
|
201
|
-
filters,
|
|
202
|
-
deletedCount: docIds.length,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
logger.error("[search:meilisearch] Failed to delete by filter", {
|
|
208
|
-
index,
|
|
209
|
-
filters,
|
|
210
|
-
error: error instanceof Error ? error.message : String(error),
|
|
211
|
-
});
|
|
212
|
-
throw error;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
async createIndex(index, settings) {
|
|
216
|
-
try {
|
|
217
|
-
// Meilisearch creates indices on-demand, so we update settings instead
|
|
218
|
-
await this._applySettings(index, settings);
|
|
219
|
-
logger.info("[search:meilisearch] Index created/configured", { index });
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
logger.error("[search:meilisearch] Failed to create index", {
|
|
223
|
-
index,
|
|
224
|
-
error: error instanceof Error ? error.message : String(error),
|
|
225
|
-
});
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
async updateSettings(index, settings) {
|
|
230
|
-
try {
|
|
231
|
-
await this._applySettings(index, settings);
|
|
232
|
-
logger.info("[search:meilisearch] Settings updated", { index });
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
logger.error("[search:meilisearch] Failed to update settings", {
|
|
236
|
-
index,
|
|
237
|
-
error: error instanceof Error ? error.message : String(error),
|
|
238
|
-
});
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
async close() {
|
|
243
|
-
logger.info("[search:meilisearch] Closing connection");
|
|
244
|
-
// Meilisearch doesn't require explicit connection cleanup
|
|
245
|
-
}
|
|
246
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
247
|
-
_getTenantIndex(baseIndex, tenantId) {
|
|
248
|
-
if (tenantId) {
|
|
249
|
-
return `${baseIndex}__${tenantId}`;
|
|
250
|
-
}
|
|
251
|
-
return baseIndex;
|
|
252
|
-
}
|
|
253
|
-
async _applySettings(index, settings) {
|
|
254
|
-
const indexObj = this.client.index(index);
|
|
255
|
-
if (settings.searchableAttributes) {
|
|
256
|
-
await indexObj.updateSearchableAttributes(settings.searchableAttributes);
|
|
257
|
-
}
|
|
258
|
-
if (settings.filterableAttributes) {
|
|
259
|
-
await indexObj.updateFilterableAttributes(settings.filterableAttributes);
|
|
260
|
-
}
|
|
261
|
-
if (settings.facetableAttributes) {
|
|
262
|
-
// Note: updateFacetedSearch doesn't exist on Index, skip this step
|
|
263
|
-
// await indexObj.updateFacetedSearch({ facets: settings.facetableAttributes });
|
|
264
|
-
}
|
|
265
|
-
if (settings.sortableAttributes) {
|
|
266
|
-
await indexObj.updateSortableAttributes(settings.sortableAttributes);
|
|
267
|
-
}
|
|
268
|
-
if (settings.rankingRules) {
|
|
269
|
-
await indexObj.updateRankingRules(settings.rankingRules);
|
|
270
|
-
}
|
|
271
|
-
if (settings.synonyms) {
|
|
272
|
-
await indexObj.updateSynonyms(settings.synonyms);
|
|
273
|
-
}
|
|
274
|
-
if (settings.primaryKey) {
|
|
275
|
-
// Primary key cannot be changed after index creation in Meilisearch
|
|
276
|
-
// This is a no-op for existing indices
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { IndexSettings, SearchDocument, SearchProvider, SearchQuery, SearchResult, TypesenseConfig } from "../types";
|
|
2
|
-
export declare class TypesenseProvider implements SearchProvider {
|
|
3
|
-
readonly name = "typesense";
|
|
4
|
-
private client;
|
|
5
|
-
constructor(config?: TypesenseConfig);
|
|
6
|
-
indexDocument<T extends SearchDocument>(index: string, doc: T): Promise<void>;
|
|
7
|
-
indexDocuments<T extends SearchDocument>(index: string, docs: T[]): Promise<void>;
|
|
8
|
-
search<T extends SearchDocument = SearchDocument>(index: string, query: SearchQuery): Promise<SearchResult<T>>;
|
|
9
|
-
deleteDocument(index: string, docId: string, tenantId?: string): Promise<void>;
|
|
10
|
-
deleteByFilter(index: string, filters: Record<string, string | number | boolean>): Promise<void>;
|
|
11
|
-
createIndex(index: string, settings: IndexSettings): Promise<void>;
|
|
12
|
-
updateSettings(index: string, _settings: IndexSettings): Promise<void>;
|
|
13
|
-
close(): Promise<void>;
|
|
14
|
-
private _getCollectionName;
|
|
15
|
-
private _prepareDocument;
|
|
16
|
-
private _buildSchema;
|
|
17
|
-
private _getQueryByFields;
|
|
18
|
-
private _parseHostFromUrl;
|
|
19
|
-
private _parsePortFromUrl;
|
|
20
|
-
private _parseProtocolFromUrl;
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=typesense.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"typesense.d.ts","sourceRoot":"","sources":["../../src/providers/typesense.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,cAAc,EACd,WAAW,EACX,YAAY,EACZ,eAAe,EAChB,MAAM,UAAU,CAAC;AAMlB,qBAAa,iBAAkB,YAAW,cAAc;IACtD,QAAQ,CAAC,IAAI,eAAe;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,MAAM,CAAC,EAAE,eAAe;IAoB9B,aAAa,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB7E,cAAc,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCjF,MAAM,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACpD,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAqHrB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB9E,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GACjD,OAAO,CAAC,IAAI,CAAC;IA0CV,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBlE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBtE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,YAAY;IA+BpB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,qBAAqB;CAQ9B"}
|