CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-medusa-interfaces

Core interfaces for Medusa e-commerce framework service implementations

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

search-service.mddocs/

Search Service

Interface for search service implementations providing full-text search capabilities including index management, document operations, and search queries across different search engines.

Deprecation Notice: Use AbstractSearchService from @medusajs/utils instead.

Capabilities

Static Methods

Type checking and identification methods for search services.

/**
 * Static property identifying this as a search service
 */
static _isSearchService: boolean;

/**
 * Checks if an object is a search service
 * @param {object} obj - Object to check
 * @returns {boolean} True if obj is a search service
 */
static isSearchService(obj: object): boolean;

Configuration

Access to service configuration options.

/**
 * Getter for service options configuration
 * @returns {object} Service options or empty object
 */
get options(): object;

Index Management

Methods for creating, retrieving, and configuring search indexes.

/**
 * Creates a search index with optional configuration
 * @param {string} indexName - The name of the index to create
 * @param {object} options - Optional index configuration
 * @returns {Promise<object>} Response from search engine provider
 * @throws {Error} If not overridden by child class
 */
createIndex(indexName: string, options?: object): Promise<object>;

/**
 * Retrieves information about an existing index
 * @param {string} indexName - The name of the index to retrieve
 * @returns {Promise<object>} Response from search engine provider
 * @throws {Error} If not overridden by child class
 */
getIndex(indexName: string): Promise<object>;

/**
 * Updates the settings of an existing index
 * @param {string} indexName - The name of the index to update
 * @param {object} settings - Settings object for index configuration
 * @returns {Promise<object>} Response from search engine provider
 * @throws {Error} If not overridden by child class
 */
updateSettings(indexName: string, settings: object): Promise<object>;

Document Operations

Methods for adding, replacing, and deleting documents in search indexes.

/**
 * Adds documents to a search index
 * @param {string} indexName - The name of the index
 * @param {array} documents - Array of document objects to be indexed
 * @param {string} type - Type of documents (e.g: products, regions, orders)
 * @returns {Promise<object>} Response from search engine provider
 * @throws {Error} If not overridden by child class
 */
addDocuments(indexName: string, documents: array, type: string): Promise<object>;

/**
 * Replaces documents in a search index
 * @param {string} indexName - The name of the index
 * @param {array} documents - Array of document objects to replace existing documents
 * @param {string} type - Type of documents to be replaced
 * @returns {Promise<object>} Response from search engine provider
 * @throws {Error} If not overridden by child class
 */
replaceDocuments(indexName: string, documents: array, type: string): Promise<object>;

/**
 * Deletes a single document from the index
 * @param {string} indexName - The name of the index
 * @param {string} document_id - The ID of the document to delete
 * @returns {Promise<object>} Response from search engine provider
 * @throws {Error} If not overridden by child class
 */
deleteDocument(indexName: string, document_id: string): Promise<object>;

/**
 * Deletes all documents from an index
 * @param {string} indexName - The name of the index
 * @returns {Promise<object>} Response from search engine provider
 * @throws {Error} If not overridden by child class
 */
deleteAllDocuments(indexName: string): Promise<object>;

Search Operations

Methods for performing search queries on indexed documents.

/**
 * Searches for documents in an index
 * @param {string} indexName - The name of the index to search
 * @param {string} query - The search query string
 * @param {object} options - Search options including pagination, filters, and provider-specific options
 * @returns {Promise<object>} Search results with hits array and metadata
 * @throws {Error} If not overridden by child class
 */
search(indexName: string, query: string, options?: SearchOptions): Promise<SearchResult>;

Types

/**
 * Search options configuration
 */
interface SearchOptions {
  paginationOptions?: {
    limit: number;
    offset: number;
  };
  filter?: any;
  additionalOptions?: any;
}

/**
 * Search result structure
 */
interface SearchResult {
  hits: any[];
  [key: string]: any;
}

Implementation Example

import { SearchService } from "medusa-interfaces";

// Elasticsearch implementation
class ElasticsearchService extends SearchService {
  constructor(options) {
    super();
    this.client = options.client;
    this.options_ = options;
  }

  async createIndex(indexName, options = {}) {
    const indexConfig = {
      index: indexName,
      body: {
        settings: {
          number_of_shards: options.shards || 1,
          number_of_replicas: options.replicas || 0,
          ...options.settings
        },
        mappings: options.mappings || {}
      }
    };

    try {
      const response = await this.client.indices.create(indexConfig);
      return {
        acknowledged: response.acknowledged,
        index: indexName,
        shards_acknowledged: response.shards_acknowledged
      };
    } catch (error) {
      throw new Error(`Failed to create index: ${error.message}`);
    }
  }

  async getIndex(indexName) {
    try {
      const response = await this.client.indices.get({ index: indexName });
      return response[indexName];
    } catch (error) {
      throw new Error(`Failed to get index: ${error.message}`);
    }
  }

  async updateSettings(indexName, settings) {
    try {
      const response = await this.client.indices.putSettings({
        index: indexName,
        body: { settings }
      });
      return { acknowledged: response.acknowledged };
    } catch (error) {
      throw new Error(`Failed to update settings: ${error.message}`);
    }
  }

  async addDocuments(indexName, documents, type) {
    const body = documents.flatMap(doc => [
      { index: { _index: indexName, _type: type, _id: doc.id } },
      doc
    ]);

    try {
      const response = await this.client.bulk({ body });
      return {
        took: response.took,
        errors: response.errors,
        items: response.items
      };
    } catch (error) {
      throw new Error(`Failed to add documents: ${error.message}`);
    }
  }

  async replaceDocuments(indexName, documents, type) {
    // Delete all documents of the type first
    await this.client.deleteByQuery({
      index: indexName,
      body: {
        query: { term: { _type: type } }
      }
    });

    // Add new documents
    return await this.addDocuments(indexName, documents, type);
  }

  async deleteDocument(indexName, document_id) {
    try {
      const response = await this.client.delete({
        index: indexName,
        id: document_id
      });
      return {
        result: response.result,
        version: response._version
      };
    } catch (error) {
      throw new Error(`Failed to delete document: ${error.message}`);
    }
  }

  async deleteAllDocuments(indexName) {
    try {
      const response = await this.client.deleteByQuery({
        index: indexName,
        body: { query: { match_all: {} } }
      });
      return {
        deleted: response.deleted,
        took: response.took
      };
    } catch (error) {
      throw new Error(`Failed to delete all documents: ${error.message}`);
    }
  }

  async search(indexName, query, options = {}) {
    const searchBody = {
      query: {
        multi_match: {
          query: query,
          fields: ["*"]
        }
      }
    };

    // Add filters if provided
    if (options.filter) {
      searchBody.query = {
        bool: {
          must: searchBody.query,
          filter: options.filter
        }
      };
    }

    const searchParams = {
      index: indexName,
      body: searchBody,
      from: options.paginationOptions?.offset || 0,
      size: options.paginationOptions?.limit || 20
    };

    // Add any additional provider-specific options
    if (options.additionalOptions) {
      Object.assign(searchParams, options.additionalOptions);
    }

    try {
      const response = await this.client.search(searchParams);
      return {
        hits: response.hits.hits.map(hit => ({
          id: hit._id,
          score: hit._score,
          source: hit._source
        })),
        total: response.hits.total.value,
        took: response.took,
        max_score: response.hits.max_score
      };
    } catch (error) {
      throw new Error(`Search failed: ${error.message}`);
    }
  }
}

// Algolia implementation example
class AlgoliaSearchService extends SearchService {
  constructor(options) {
    super();
    this.client = options.client;
    this.options_ = options;
  }

  async createIndex(indexName, options = {}) {
    const index = this.client.initIndex(indexName);
    
    if (options.settings) {
      await index.setSettings(options.settings);
    }

    return {
      acknowledged: true,
      index: indexName
    };
  }

  async getIndex(indexName) {
    const index = this.client.initIndex(indexName);
    const settings = await index.getSettings();
    return { settings };
  }

  async updateSettings(indexName, settings) {
    const index = this.client.initIndex(indexName);
    await index.setSettings(settings);
    return { acknowledged: true };
  }

  async addDocuments(indexName, documents, type) {
    const index = this.client.initIndex(indexName);
    
    // Add type field to documents
    const typedDocuments = documents.map(doc => ({ ...doc, _type: type }));
    
    const response = await index.saveObjects(typedDocuments);
    return {
      objectIDs: response.objectIDs,
      taskID: response.taskID
    };
  }

  async replaceDocuments(indexName, documents, type) {
    const index = this.client.initIndex(indexName);
    
    // Clear existing documents of this type
    await index.deleteBy({ filters: `_type:${type}` });
    
    // Add new documents
    return await this.addDocuments(indexName, documents, type);
  }

  async deleteDocument(indexName, document_id) {
    const index = this.client.initIndex(indexName);
    const response = await index.deleteObject(document_id);
    return { taskID: response.taskID };
  }

  async deleteAllDocuments(indexName) {
    const index = this.client.initIndex(indexName);
    const response = await index.clearObjects();
    return { taskID: response.taskID };
  }

  async search(indexName, query, options = {}) {
    const index = this.client.initIndex(indexName);
    
    const searchOptions = {
      offset: options.paginationOptions?.offset || 0,
      length: options.paginationOptions?.limit || 20,
      filters: options.filter || "",
      ...options.additionalOptions
    };

    const response = await index.search(query, searchOptions);
    
    return {
      hits: response.hits.map(hit => ({
        id: hit.objectID,
        score: hit._rankingInfo?.nbTypos || 0,
        source: hit
      })),
      total: response.nbHits,
      took: response.processingTimeMs,
      page: response.page
    };
  }
}

Usage in Medusa

Search services are typically used for:

  • Product Search: Full-text search across product catalog
  • Order Search: Finding orders by customer, product, or status
  • Customer Search: Admin panel customer lookup
  • Content Search: CMS content and documentation search

Basic Usage Pattern:

// In a Medusa service
class ProductSearchService {
  constructor({ searchService }) {
    this.searchService_ = searchService;
    this.productIndex = "products";
  }

  async indexProducts(products) {
    // Transform products for search indexing
    const searchDocuments = products.map(product => ({
      id: product.id,
      title: product.title,
      description: product.description,
      tags: product.tags?.map(tag => tag.value) || [],
      price: product.variants?.[0]?.prices?.[0]?.amount,
      categories: product.categories?.map(cat => cat.name) || []
    }));

    return await this.searchService_.addDocuments(
      this.productIndex,
      searchDocuments,
      "product"
    );
  }

  async searchProducts(query, options = {}) {
    const results = await this.searchService_.search(
      this.productIndex,
      query,
      {
        paginationOptions: {
          limit: options.limit || 20,
          offset: options.offset || 0
        },
        filter: options.categoryFilter ? `categories:${options.categoryFilter}` : null
      }
    );

    return {
      products: results.hits,
      total: results.total,
      page: Math.floor((options.offset || 0) / (options.limit || 20)) + 1
    };
  }

  async updateProduct(productId, productData) {
    await this.searchService_.replaceDocuments(
      this.productIndex,
      [{ id: productId, ...productData }],
      "product"
    );
  }

  async deleteProduct(productId) {
    await this.searchService_.deleteDocument(this.productIndex, productId);
  }
}

Error Handling

All abstract methods throw descriptive errors when not implemented:

  • "createIndex must be overridden by a child class"
  • "getIndex must be overridden by a child class"
  • "addDocuments must be overridden by a child class"
  • "updateDocument must be overridden by a child class" (for replaceDocuments)
  • "deleteDocument must be overridden by a child class"
  • "deleteAllDocuments must be overridden by a child class"
  • "search must be overridden by a child class"
  • "updateSettings must be overridden by a child class"

docs

base-service.md

file-service.md

fulfillment-service.md

index.md

notification-service.md

oauth-service.md

payment-service.md

search-service.md

tile.json