@nebutra/search 0.1.0 → 0.1.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.
@@ -0,0 +1,39 @@
1
+ import type { SearchConfig, SearchProvider } from "./types";
2
+ /**
3
+ * Create a search provider instance.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * // Auto-detect from environment
8
+ * const search = await createSearch();
9
+ *
10
+ * // Explicit Meilisearch
11
+ * const search = await createSearch({
12
+ * provider: "meilisearch",
13
+ * url: "http://localhost:7700",
14
+ * });
15
+ *
16
+ * // Explicit Algolia
17
+ * const search = await createSearch({
18
+ * provider: "algolia",
19
+ * appId: "YOUR_APP_ID",
20
+ * searchKey: "YOUR_SEARCH_KEY",
21
+ * adminKey: "YOUR_ADMIN_KEY",
22
+ * });
23
+ * ```
24
+ */
25
+ export declare function createSearch(config?: SearchConfig): Promise<SearchProvider>;
26
+ /**
27
+ * Get or create the default (singleton) search provider.
28
+ * Uses lazy initialisation so import-time side effects are avoided.
29
+ */
30
+ export declare function getSearch(): Promise<SearchProvider>;
31
+ /**
32
+ * Replace the default search provider (useful in tests).
33
+ */
34
+ export declare function setSearch(provider: SearchProvider): void;
35
+ /**
36
+ * Gracefully shut down the default search provider.
37
+ */
38
+ export declare function closeSearch(): Promise<void>;
39
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAsB,MAAM,SAAS,CAAC;AA4BhF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAmEjF;AAED;;;GAGG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,cAAc,CAAC,CAKzD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAExD;AAED;;GAEG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjD"}
@@ -0,0 +1,138 @@
1
+ import { logger } from "@nebutra/logger";
2
+ // =============================================================================
3
+ // Search Factory — Provider-agnostic search creation
4
+ // =============================================================================
5
+ // The factory resolves the correct provider at runtime based on:
6
+ // 1. Explicit config passed to `createSearch()`
7
+ // 2. `SEARCH_PROVIDER` environment variable
8
+ // 3. Auto-detection based on available env vars
9
+ //
10
+ // This lets customers switch backends without changing application code.
11
+ // =============================================================================
12
+ let defaultProvider = null;
13
+ /**
14
+ * Detect which provider to use based on available environment variables.
15
+ */
16
+ function detectProvider() {
17
+ if (process.env.MEILISEARCH_URL)
18
+ return "meilisearch";
19
+ if (process.env.TYPESENSE_URL)
20
+ return "typesense";
21
+ if (process.env.ALGOLIA_APP_ID)
22
+ return "algolia";
23
+ // Only fall back to pgvector when DATABASE_URL is set — otherwise default to
24
+ // meilisearch (developer-friendly local default).
25
+ if (process.env.SEARCH_PROVIDER === "pgvector" && process.env.DATABASE_URL)
26
+ return "pgvector";
27
+ return "meilisearch";
28
+ }
29
+ /**
30
+ * Create a search provider instance.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * // Auto-detect from environment
35
+ * const search = await createSearch();
36
+ *
37
+ * // Explicit Meilisearch
38
+ * const search = await createSearch({
39
+ * provider: "meilisearch",
40
+ * url: "http://localhost:7700",
41
+ * });
42
+ *
43
+ * // Explicit Algolia
44
+ * const search = await createSearch({
45
+ * provider: "algolia",
46
+ * appId: "YOUR_APP_ID",
47
+ * searchKey: "YOUR_SEARCH_KEY",
48
+ * adminKey: "YOUR_ADMIN_KEY",
49
+ * });
50
+ * ```
51
+ */
52
+ export async function createSearch(config) {
53
+ const providerType = config?.provider ??
54
+ process.env.SEARCH_PROVIDER ??
55
+ detectProvider();
56
+ logger.info("[search] Creating provider", { provider: providerType });
57
+ switch (providerType) {
58
+ case "meilisearch": {
59
+ const { MeilisearchProvider } = await import("./providers/meilisearch");
60
+ const meilisearchConfig = config;
61
+ const configObj = { provider: "meilisearch" };
62
+ if (meilisearchConfig?.url !== undefined)
63
+ configObj.url = meilisearchConfig.url;
64
+ if (meilisearchConfig?.apiKey !== undefined)
65
+ configObj.apiKey = meilisearchConfig.apiKey;
66
+ if (meilisearchConfig?.timeout !== undefined)
67
+ configObj.timeout = meilisearchConfig.timeout;
68
+ return new MeilisearchProvider(configObj);
69
+ }
70
+ case "typesense": {
71
+ const { TypesenseProvider } = await import("./providers/typesense");
72
+ const typesenseConfig = config;
73
+ const configObj = { provider: "typesense" };
74
+ if (typesenseConfig?.url !== undefined)
75
+ configObj.url = typesenseConfig.url;
76
+ if (typesenseConfig?.apiKey !== undefined)
77
+ configObj.apiKey = typesenseConfig.apiKey;
78
+ if (typesenseConfig?.timeout !== undefined)
79
+ configObj.timeout = typesenseConfig.timeout;
80
+ return new TypesenseProvider(configObj);
81
+ }
82
+ case "algolia": {
83
+ const { AlgoliaProvider } = await import("./providers/algolia");
84
+ const algoliaConfig = config;
85
+ const configObj = { provider: "algolia" };
86
+ if (algoliaConfig?.appId !== undefined)
87
+ configObj.appId = algoliaConfig.appId;
88
+ if (algoliaConfig?.searchKey !== undefined)
89
+ configObj.searchKey = algoliaConfig.searchKey;
90
+ if (algoliaConfig?.adminKey !== undefined)
91
+ configObj.adminKey = algoliaConfig.adminKey;
92
+ return new AlgoliaProvider(configObj);
93
+ }
94
+ case "pgvector": {
95
+ const { PgvectorProvider } = await import("./providers/pgvector");
96
+ const pgvectorConfig = config;
97
+ return new PgvectorProvider({
98
+ provider: "pgvector",
99
+ ...(pgvectorConfig?.connectionString !== undefined
100
+ ? { connectionString: pgvectorConfig.connectionString }
101
+ : {}),
102
+ ...(pgvectorConfig?.embeddingDim !== undefined
103
+ ? { embeddingDim: pgvectorConfig.embeddingDim }
104
+ : {}),
105
+ ...(pgvectorConfig?.tablePrefix !== undefined
106
+ ? { tablePrefix: pgvectorConfig.tablePrefix }
107
+ : {}),
108
+ });
109
+ }
110
+ default:
111
+ throw new Error(`Unknown search provider: ${providerType}`);
112
+ }
113
+ }
114
+ /**
115
+ * Get or create the default (singleton) search provider.
116
+ * Uses lazy initialisation so import-time side effects are avoided.
117
+ */
118
+ export async function getSearch() {
119
+ if (!defaultProvider) {
120
+ defaultProvider = await createSearch();
121
+ }
122
+ return defaultProvider;
123
+ }
124
+ /**
125
+ * Replace the default search provider (useful in tests).
126
+ */
127
+ export function setSearch(provider) {
128
+ defaultProvider = provider;
129
+ }
130
+ /**
131
+ * Gracefully shut down the default search provider.
132
+ */
133
+ export async function closeSearch() {
134
+ if (defaultProvider) {
135
+ await defaultProvider.close();
136
+ defaultProvider = null;
137
+ }
138
+ }
@@ -0,0 +1,7 @@
1
+ export { closeSearch, createSearch, getSearch, setSearch } from "./factory";
2
+ export { AlgoliaProvider } from "./providers/algolia";
3
+ export { MeilisearchProvider } from "./providers/meilisearch";
4
+ export { TypesenseProvider } from "./providers/typesense";
5
+ export type { AlgoliaConfig, IndexSettings, MeilisearchConfig, SearchConfig, SearchDocument, SearchHit, SearchProvider, SearchProviderType, SearchQuery, SearchResult, TypesenseConfig, } from "./types";
6
+ export { SearchDocumentSchema, SearchQuerySchema } from "./types";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG5E,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG1D,YAAY,EACV,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,SAAS,EACT,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,eAAe,GAChB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ // =============================================================================
2
+ // @nebutra/search — Provider-agnostic full-text search
3
+ // =============================================================================
4
+ // Supports:
5
+ // - Meilisearch (self-hosted, developer-friendly)
6
+ // - Typesense (self-hosted, geo + instant faceting)
7
+ // - Algolia (managed SaaS, zero-ops)
8
+ //
9
+ // Usage:
10
+ // import { getSearch } from "@nebutra/search";
11
+ //
12
+ // const search = await getSearch(); // auto-detects provider
13
+ // await search.indexDocument("products", { id: "123", name: "Widget" });
14
+ // const results = await search.search("products", { query: "widget" });
15
+ // =============================================================================
16
+ // ── Factory ─────────────────────────────────────────────────────────────────
17
+ export { closeSearch, createSearch, getSearch, setSearch } from "./factory";
18
+ // ── Providers (tree-shakable direct imports) ────────────────────────────────
19
+ export { AlgoliaProvider } from "./providers/algolia";
20
+ export { MeilisearchProvider } from "./providers/meilisearch";
21
+ export { TypesenseProvider } from "./providers/typesense";
22
+ export { SearchDocumentSchema, SearchQuerySchema } from "./types";
@@ -0,0 +1,20 @@
1
+ import type { AlgoliaConfig, IndexSettings, SearchDocument, SearchProvider, SearchQuery, SearchResult } from "../types";
2
+ export declare class AlgoliaProvider implements SearchProvider {
3
+ readonly name = "algolia";
4
+ private searchClient;
5
+ private adminClient;
6
+ constructor(config?: AlgoliaConfig);
7
+ indexDocument<T extends SearchDocument>(index: string, doc: T): Promise<void>;
8
+ indexDocuments<T extends SearchDocument>(index: string, docs: T[]): Promise<void>;
9
+ search<T extends SearchDocument = SearchDocument>(index: string, query: SearchQuery): Promise<SearchResult<T>>;
10
+ deleteDocument(index: string, docId: string, tenantId?: string): Promise<void>;
11
+ deleteByFilter(index: string, filters: Record<string, string | number | boolean>): Promise<void>;
12
+ createIndex(index: string, settings: IndexSettings): Promise<void>;
13
+ updateSettings(index: string, settings: IndexSettings): Promise<void>;
14
+ close(): Promise<void>;
15
+ private _getIndexName;
16
+ private _prepareRecord;
17
+ private _unprepareRecord;
18
+ private _applySettings;
19
+ }
20
+ //# sourceMappingURL=algolia.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algolia.d.ts","sourceRoot":"","sources":["../../src/providers/algolia.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,EACd,WAAW,EACX,YAAY,EACb,MAAM,UAAU,CAAC;AAMlB,qBAAa,eAAgB,YAAW,cAAc;IACpD,QAAQ,CAAC,IAAI,aAAa;IAC1B,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,WAAW,CAAM;gBAEb,MAAM,CAAC,EAAE,aAAa;IAuB5B,aAAa,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB7E,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;IAyFrB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB9E,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;IAiBlE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAerE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,gBAAgB;YAQV,cAAc;CA2B7B"}
@@ -0,0 +1,297 @@
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
+ }
@@ -0,0 +1,17 @@
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
@@ -0,0 +1 @@
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"}