0
# Search Service
1
2
Interface for search service implementations providing full-text search capabilities including index management, document operations, and search queries across different search engines.
3
4
**Deprecation Notice**: Use `AbstractSearchService` from @medusajs/utils instead.
5
6
## Capabilities
7
8
### Static Methods
9
10
Type checking and identification methods for search services.
11
12
```javascript { .api }
13
/**
14
* Static property identifying this as a search service
15
*/
16
static _isSearchService: boolean;
17
18
/**
19
* Checks if an object is a search service
20
* @param {object} obj - Object to check
21
* @returns {boolean} True if obj is a search service
22
*/
23
static isSearchService(obj: object): boolean;
24
```
25
26
### Configuration
27
28
Access to service configuration options.
29
30
```javascript { .api }
31
/**
32
* Getter for service options configuration
33
* @returns {object} Service options or empty object
34
*/
35
get options(): object;
36
```
37
38
### Index Management
39
40
Methods for creating, retrieving, and configuring search indexes.
41
42
```javascript { .api }
43
/**
44
* Creates a search index with optional configuration
45
* @param {string} indexName - The name of the index to create
46
* @param {object} options - Optional index configuration
47
* @returns {Promise<object>} Response from search engine provider
48
* @throws {Error} If not overridden by child class
49
*/
50
createIndex(indexName: string, options?: object): Promise<object>;
51
52
/**
53
* Retrieves information about an existing index
54
* @param {string} indexName - The name of the index to retrieve
55
* @returns {Promise<object>} Response from search engine provider
56
* @throws {Error} If not overridden by child class
57
*/
58
getIndex(indexName: string): Promise<object>;
59
60
/**
61
* Updates the settings of an existing index
62
* @param {string} indexName - The name of the index to update
63
* @param {object} settings - Settings object for index configuration
64
* @returns {Promise<object>} Response from search engine provider
65
* @throws {Error} If not overridden by child class
66
*/
67
updateSettings(indexName: string, settings: object): Promise<object>;
68
```
69
70
### Document Operations
71
72
Methods for adding, replacing, and deleting documents in search indexes.
73
74
```javascript { .api }
75
/**
76
* Adds documents to a search index
77
* @param {string} indexName - The name of the index
78
* @param {array} documents - Array of document objects to be indexed
79
* @param {string} type - Type of documents (e.g: products, regions, orders)
80
* @returns {Promise<object>} Response from search engine provider
81
* @throws {Error} If not overridden by child class
82
*/
83
addDocuments(indexName: string, documents: array, type: string): Promise<object>;
84
85
/**
86
* Replaces documents in a search index
87
* @param {string} indexName - The name of the index
88
* @param {array} documents - Array of document objects to replace existing documents
89
* @param {string} type - Type of documents to be replaced
90
* @returns {Promise<object>} Response from search engine provider
91
* @throws {Error} If not overridden by child class
92
*/
93
replaceDocuments(indexName: string, documents: array, type: string): Promise<object>;
94
95
/**
96
* Deletes a single document from the index
97
* @param {string} indexName - The name of the index
98
* @param {string} document_id - The ID of the document to delete
99
* @returns {Promise<object>} Response from search engine provider
100
* @throws {Error} If not overridden by child class
101
*/
102
deleteDocument(indexName: string, document_id: string): Promise<object>;
103
104
/**
105
* Deletes all documents from an index
106
* @param {string} indexName - The name of the index
107
* @returns {Promise<object>} Response from search engine provider
108
* @throws {Error} If not overridden by child class
109
*/
110
deleteAllDocuments(indexName: string): Promise<object>;
111
```
112
113
### Search Operations
114
115
Methods for performing search queries on indexed documents.
116
117
```javascript { .api }
118
/**
119
* Searches for documents in an index
120
* @param {string} indexName - The name of the index to search
121
* @param {string} query - The search query string
122
* @param {object} options - Search options including pagination, filters, and provider-specific options
123
* @returns {Promise<object>} Search results with hits array and metadata
124
* @throws {Error} If not overridden by child class
125
*/
126
search(indexName: string, query: string, options?: SearchOptions): Promise<SearchResult>;
127
```
128
129
## Types
130
131
```javascript { .api }
132
/**
133
* Search options configuration
134
*/
135
interface SearchOptions {
136
paginationOptions?: {
137
limit: number;
138
offset: number;
139
};
140
filter?: any;
141
additionalOptions?: any;
142
}
143
144
/**
145
* Search result structure
146
*/
147
interface SearchResult {
148
hits: any[];
149
[key: string]: any;
150
}
151
```
152
153
## Implementation Example
154
155
```javascript
156
import { SearchService } from "medusa-interfaces";
157
158
// Elasticsearch implementation
159
class ElasticsearchService extends SearchService {
160
constructor(options) {
161
super();
162
this.client = options.client;
163
this.options_ = options;
164
}
165
166
async createIndex(indexName, options = {}) {
167
const indexConfig = {
168
index: indexName,
169
body: {
170
settings: {
171
number_of_shards: options.shards || 1,
172
number_of_replicas: options.replicas || 0,
173
...options.settings
174
},
175
mappings: options.mappings || {}
176
}
177
};
178
179
try {
180
const response = await this.client.indices.create(indexConfig);
181
return {
182
acknowledged: response.acknowledged,
183
index: indexName,
184
shards_acknowledged: response.shards_acknowledged
185
};
186
} catch (error) {
187
throw new Error(`Failed to create index: ${error.message}`);
188
}
189
}
190
191
async getIndex(indexName) {
192
try {
193
const response = await this.client.indices.get({ index: indexName });
194
return response[indexName];
195
} catch (error) {
196
throw new Error(`Failed to get index: ${error.message}`);
197
}
198
}
199
200
async updateSettings(indexName, settings) {
201
try {
202
const response = await this.client.indices.putSettings({
203
index: indexName,
204
body: { settings }
205
});
206
return { acknowledged: response.acknowledged };
207
} catch (error) {
208
throw new Error(`Failed to update settings: ${error.message}`);
209
}
210
}
211
212
async addDocuments(indexName, documents, type) {
213
const body = documents.flatMap(doc => [
214
{ index: { _index: indexName, _type: type, _id: doc.id } },
215
doc
216
]);
217
218
try {
219
const response = await this.client.bulk({ body });
220
return {
221
took: response.took,
222
errors: response.errors,
223
items: response.items
224
};
225
} catch (error) {
226
throw new Error(`Failed to add documents: ${error.message}`);
227
}
228
}
229
230
async replaceDocuments(indexName, documents, type) {
231
// Delete all documents of the type first
232
await this.client.deleteByQuery({
233
index: indexName,
234
body: {
235
query: { term: { _type: type } }
236
}
237
});
238
239
// Add new documents
240
return await this.addDocuments(indexName, documents, type);
241
}
242
243
async deleteDocument(indexName, document_id) {
244
try {
245
const response = await this.client.delete({
246
index: indexName,
247
id: document_id
248
});
249
return {
250
result: response.result,
251
version: response._version
252
};
253
} catch (error) {
254
throw new Error(`Failed to delete document: ${error.message}`);
255
}
256
}
257
258
async deleteAllDocuments(indexName) {
259
try {
260
const response = await this.client.deleteByQuery({
261
index: indexName,
262
body: { query: { match_all: {} } }
263
});
264
return {
265
deleted: response.deleted,
266
took: response.took
267
};
268
} catch (error) {
269
throw new Error(`Failed to delete all documents: ${error.message}`);
270
}
271
}
272
273
async search(indexName, query, options = {}) {
274
const searchBody = {
275
query: {
276
multi_match: {
277
query: query,
278
fields: ["*"]
279
}
280
}
281
};
282
283
// Add filters if provided
284
if (options.filter) {
285
searchBody.query = {
286
bool: {
287
must: searchBody.query,
288
filter: options.filter
289
}
290
};
291
}
292
293
const searchParams = {
294
index: indexName,
295
body: searchBody,
296
from: options.paginationOptions?.offset || 0,
297
size: options.paginationOptions?.limit || 20
298
};
299
300
// Add any additional provider-specific options
301
if (options.additionalOptions) {
302
Object.assign(searchParams, options.additionalOptions);
303
}
304
305
try {
306
const response = await this.client.search(searchParams);
307
return {
308
hits: response.hits.hits.map(hit => ({
309
id: hit._id,
310
score: hit._score,
311
source: hit._source
312
})),
313
total: response.hits.total.value,
314
took: response.took,
315
max_score: response.hits.max_score
316
};
317
} catch (error) {
318
throw new Error(`Search failed: ${error.message}`);
319
}
320
}
321
}
322
323
// Algolia implementation example
324
class AlgoliaSearchService extends SearchService {
325
constructor(options) {
326
super();
327
this.client = options.client;
328
this.options_ = options;
329
}
330
331
async createIndex(indexName, options = {}) {
332
const index = this.client.initIndex(indexName);
333
334
if (options.settings) {
335
await index.setSettings(options.settings);
336
}
337
338
return {
339
acknowledged: true,
340
index: indexName
341
};
342
}
343
344
async getIndex(indexName) {
345
const index = this.client.initIndex(indexName);
346
const settings = await index.getSettings();
347
return { settings };
348
}
349
350
async updateSettings(indexName, settings) {
351
const index = this.client.initIndex(indexName);
352
await index.setSettings(settings);
353
return { acknowledged: true };
354
}
355
356
async addDocuments(indexName, documents, type) {
357
const index = this.client.initIndex(indexName);
358
359
// Add type field to documents
360
const typedDocuments = documents.map(doc => ({ ...doc, _type: type }));
361
362
const response = await index.saveObjects(typedDocuments);
363
return {
364
objectIDs: response.objectIDs,
365
taskID: response.taskID
366
};
367
}
368
369
async replaceDocuments(indexName, documents, type) {
370
const index = this.client.initIndex(indexName);
371
372
// Clear existing documents of this type
373
await index.deleteBy({ filters: `_type:${type}` });
374
375
// Add new documents
376
return await this.addDocuments(indexName, documents, type);
377
}
378
379
async deleteDocument(indexName, document_id) {
380
const index = this.client.initIndex(indexName);
381
const response = await index.deleteObject(document_id);
382
return { taskID: response.taskID };
383
}
384
385
async deleteAllDocuments(indexName) {
386
const index = this.client.initIndex(indexName);
387
const response = await index.clearObjects();
388
return { taskID: response.taskID };
389
}
390
391
async search(indexName, query, options = {}) {
392
const index = this.client.initIndex(indexName);
393
394
const searchOptions = {
395
offset: options.paginationOptions?.offset || 0,
396
length: options.paginationOptions?.limit || 20,
397
filters: options.filter || "",
398
...options.additionalOptions
399
};
400
401
const response = await index.search(query, searchOptions);
402
403
return {
404
hits: response.hits.map(hit => ({
405
id: hit.objectID,
406
score: hit._rankingInfo?.nbTypos || 0,
407
source: hit
408
})),
409
total: response.nbHits,
410
took: response.processingTimeMs,
411
page: response.page
412
};
413
}
414
}
415
```
416
417
## Usage in Medusa
418
419
Search services are typically used for:
420
421
- **Product Search**: Full-text search across product catalog
422
- **Order Search**: Finding orders by customer, product, or status
423
- **Customer Search**: Admin panel customer lookup
424
- **Content Search**: CMS content and documentation search
425
426
**Basic Usage Pattern:**
427
428
```javascript
429
// In a Medusa service
430
class ProductSearchService {
431
constructor({ searchService }) {
432
this.searchService_ = searchService;
433
this.productIndex = "products";
434
}
435
436
async indexProducts(products) {
437
// Transform products for search indexing
438
const searchDocuments = products.map(product => ({
439
id: product.id,
440
title: product.title,
441
description: product.description,
442
tags: product.tags?.map(tag => tag.value) || [],
443
price: product.variants?.[0]?.prices?.[0]?.amount,
444
categories: product.categories?.map(cat => cat.name) || []
445
}));
446
447
return await this.searchService_.addDocuments(
448
this.productIndex,
449
searchDocuments,
450
"product"
451
);
452
}
453
454
async searchProducts(query, options = {}) {
455
const results = await this.searchService_.search(
456
this.productIndex,
457
query,
458
{
459
paginationOptions: {
460
limit: options.limit || 20,
461
offset: options.offset || 0
462
},
463
filter: options.categoryFilter ? `categories:${options.categoryFilter}` : null
464
}
465
);
466
467
return {
468
products: results.hits,
469
total: results.total,
470
page: Math.floor((options.offset || 0) / (options.limit || 20)) + 1
471
};
472
}
473
474
async updateProduct(productId, productData) {
475
await this.searchService_.replaceDocuments(
476
this.productIndex,
477
[{ id: productId, ...productData }],
478
"product"
479
);
480
}
481
482
async deleteProduct(productId) {
483
await this.searchService_.deleteDocument(this.productIndex, productId);
484
}
485
}
486
```
487
488
## Error Handling
489
490
All abstract methods throw descriptive errors when not implemented:
491
492
- `"createIndex must be overridden by a child class"`
493
- `"getIndex must be overridden by a child class"`
494
- `"addDocuments must be overridden by a child class"`
495
- `"updateDocument must be overridden by a child class"` (for replaceDocuments)
496
- `"deleteDocument must be overridden by a child class"`
497
- `"deleteAllDocuments must be overridden by a child class"`
498
- `"search must be overridden by a child class"`
499
- `"updateSettings must be overridden by a child class"`