OpenSearch and Magento GraphQL: How Search Really Works Together
AI generated
{ }
type
GraphQL · Magento · OpenSearch · Search · Performance
OpenSearch and Magento GraphQL
How Search Really Works Together

When a Magento GraphQL product search is slow, the problem is rarely in the GraphQL layer itself. It is almost always in how search requests get routed through resolvers to OpenSearch, and whether indexing, filter mapping and sorting are configured correctly.

17 min read OpenSearch · Indexing · Filter Mapping · Resolvers Magento 2.4 · OpenSearch 2.x

1. The search architecture in Magento: from GraphQL request to OpenSearch

When a headless frontend sends a GraphQL product search (products(search: "...")), the request passes through several layers before the result comes back. The resolver receives the arguments, builds a SearchCriteria object and hands it to the product repository. As soon as a search argument is present, Magento automatically routes the request to the search adapter, which in Magento 2.4 is OpenSearch by default. This adapter translates the SearchCriteria object into an OpenSearch query, sends it to the OpenSearch cluster and returns the matching result IDs.

The returned IDs are then used in a second database query to load the full product data: attributes that are not in the OpenSearch index get loaded from the database afterward. That is an important point: OpenSearch does not return the full product data, only IDs and relevance scores primarily. Magento combines the OpenSearch results and the database data into the final response. This architecture is transparent to the GraphQL client, but for performance optimization you need to understand both layers.

The search adapter itself is swappable. Magento abstracts the concrete search service behind interfaces, so switching from OpenSearch to another search service is theoretically possible without changing the resolvers. In practice this promise is limited by adapter-specific configuration, but the underlying principle of layer separation holds up well.

2. Indexing: what OpenSearch knows about products

OpenSearch only knows what Magento has written into the index. The catalog index holds a set of standard fields for every product: SKU, name, description, price, status, visibility and every attribute configured as searchable and filterable. What is not in the index: complex EAV relationships, custom options, configurable variant details and any attribute that is not marked as Used in Layered Navigation or Searchable in the admin configuration.

This has direct consequences for GraphQL filters. A filter on an attribute that is not in the index forces Magento back onto a database fallback, and that fallback loses all the advantages of OpenSearch: relevance scoring, fast full-text search and aggregations for facet filters. That is why indexing configuration is not just an admin setting but an architectural decision that directly influences the performance of GraphQL search queries.

3. The product resolver: how it decides between DB and OpenSearch

Magento's internal product resolver checks whether the incoming query contains a search or a filter argument. With a search argument, the search adapter (OpenSearch) is always used. With a pure filter argument, it depends on whether the filters are based on indexed attributes or not. If all filters are based on filterable attributes, OpenSearch can handle the filter query as well, with substantial performance advantages over DB queries.

This switching behavior is not always transparent. Magento logs do not directly show which path was taken; you have to infer it from the query structure and the response times. A simple diagnostic test: send the same query with search: "" (an empty search string) and without a search argument, then compare the response times. If the difference is significant, one of the two paths is running through the database instead of OpenSearch.


# Query type 1: full-text search, always uses OpenSearch
query SearchProducts {
  products(
    search: "running shoes"
    filter: {
      price: { from: "30.00", to: "200.00" }
    }
    pageSize: 24
    currentPage: 1
    sort: { relevance: DESC }
  ) {
    total_count
    aggregations {
      attribute_code
      label
      count
      options {
        label
        value
        count
      }
    }
    items {
      sku
      name
      url_key
    }
  }
}

# Query type 2: filter-only, may or may not use OpenSearch
# depends on whether filter attributes are indexed
query FilterProducts {
  products(
    filter: {
      category_id: { eq: "5" }
      price: { from: "50.00", to: "150.00" }
    }
    pageSize: 24
  ) {
    total_count
    items {
      sku
      name
    }
  }
}

4. Filter mapping: translating GraphQL arguments into OpenSearch queries

When a GraphQL filter is forwarded to OpenSearch, Magento's search adapter translates the SearchCriteria object into an OpenSearch query DSL. Simple equality filters become term queries, range filters become range queries, and full-text searches become multi_match or query_string queries. This translation happens automatically, but only if the filtered attribute is correctly configured in the OpenSearch mapping.

The OpenSearch field mapping defines how Magento stores a product attribute in the index. Text fields can be stored as text (for full-text search with tokenization) or as keyword (for exact filter queries), often both at once as a multi-field. Filterable attributes are typically stored as keyword so they can be used for precise term queries. If an attribute is missing or mapped incorrectly, the OpenSearch filter returns no result, without an error message, just an empty result set.

5. Analyzing search queries: debugging and optimizing queries

The most effective tool for debugging Magento OpenSearch requests is OpenSearch query logging. Through the OpenSearch admin interface or directly via the REST API, you can inspect the actual query DSL objects that Magento sends to OpenSearch. This information shows which filters were translated, which relevance boosts are applied and whether the query produces the expected hits.

Another important diagnostic tool is Magento's own bin/magento indexer:status. If the catalog index is not up to date, OpenSearch returns stale or missing products, without an error message in the GraphQL response. Performance problems that show up as slow response times despite correct OpenSearch configuration often lie in the second phase: reloading product details from the database for attributes that are not in the index.


# Debug query: check if aggregations (facets) are returned correctly
# Aggregations are only available when using OpenSearch
query DebugSearchWithAggregations {
  products(
    search: "jacket"
    filter: {
      category_id: { eq: "12" }
    }
    pageSize: 1
    currentPage: 1
  ) {
    total_count
    # If aggregations are empty or missing, OpenSearch is not being used
    # or the indexed attributes are not configured as filterable
    aggregations {
      attribute_code
      label
      count
      options {
        label
        value
        count
      }
    }
    # page_info helps verify pagination works correctly with OpenSearch
    page_info {
      current_page
      page_size
      total_pages
    }
    items {
      sku
    }
  }
}

# Expected: aggregations contains price, color, size, etc.
# Missing aggregations = OpenSearch not engaged or attribute not indexed

6. Aggregations: how facet filters come from OpenSearch

The aggregations field in Magento's GraphQL schema is one of the most important features of the OpenSearch integration. For every filterable attribute, it returns a list of available values along with the number of matches per value, which is the foundation for dynamic facet filters in the headless frontend. This data comes directly from OpenSearch's aggregation feature and is part of a single query result, not a separate API call per filter attribute.

Aggregations only work when OpenSearch is used for the request. With a pure database fallback, the aggregations field returns an empty array. This is one of the most common mistakes when building headless frontends: the frontend expects aggregations for facet filters, but because a filter on a non-indexed attribute bypasses the OpenSearch path, no aggregations come back. The fix is always the same: enable the filter attribute as Used in Layered Navigation in the admin configuration and rebuild the index.

7. DB search vs. OpenSearch: what is really slower and why

Criterion Database Search OpenSearch Advantage
Full-text search LIKE query, slow Inverted index, fast OpenSearch: 10 to 100x faster
Facet aggregations Separate COUNT queries per attribute One aggregation, all values OpenSearch: O(1) instead of O(n)
EAV filtering JOIN across multiple tables Denormalized in the index OpenSearch: no joins needed
Relevance ranking Not available TF/IDF and BM25 Better search results
Scalability Linear degradation on large catalogs Horizontal scaling OpenSearch: cluster scalable

The table makes it clear: OpenSearch is significantly superior for every search scenario in Magento. The only scenario where the database still plays a role is reloading product detail data that is not in the index. This is exactly why the indexing strategy matters so much: the more relevant product data lives in the index, the fewer DB queries are needed for a complete GraphQL response.

8. Configuring custom attributes for search

Custom product attributes from custom modules are not included in the OpenSearch index by default. For a custom attribute to be usable in GraphQL filters and to return aggregations, three configuration steps are required. First: the attribute must be enabled as Used in Layered Navigation in the admin interface (for facet filters) and/or as Use in Search (for full-text search). Second: the attribute must be added to the module's schema.graphqls file so it becomes available in the GraphQL filter input. Third: the catalog index must be rebuilt (bin/magento indexer:reindex catalogsearch_fulltext).

A frequently overlooked fourth step: the OpenSearch field mapping must be checked. If an attribute gets stored in the index with an unsuitable data type, for instance a numeric attribute stored as a string, range filters will not work correctly. The mapping can be inspected via the OpenSearch REST API (GET /magento2_product_1/_mapping) and may need to be rebuilt after an index reconfiguration.

9. Common failure patterns: when search does not return what you expect

The most common failure pattern: a full-text search returns too many or too few results. The reason is almost always the weighting of search fields in the OpenSearch index. Magento configures default weights for name, description and SKU, but custom attributes configured as search fields often get weighted too high or too low. The result is irrelevant hits at the top positions, or the products you actually want simply do not appear.

A second failure pattern: category filters return incorrect results. In Magento, category membership is stored in the index as an array of category IDs, including all parent categories. If a GraphQL filter uses a category ID that is not present in the index in this form (for example because the category index is stale), the search returns empty results. The fix is simple: bin/magento indexer:reindex catalog_category_product followed by catalogsearch_fulltext.


# Diagnostic query: verify custom attribute is indexed and filterable
query CheckCustomAttributeFilter {
  products(
    filter: {
      # custom_material must be configured as:
      # - "Used in Layered Navigation: Filterable"
      # - "Use in Search: Yes" (if full-text searchable)
      custom_material: { eq: "cotton" }
    }
    pageSize: 5
  ) {
    total_count
    aggregations {
      attribute_code
      label
      # custom_material should appear here if correctly indexed
    }
    items {
      sku
      name
      # custom_material field must be added to schema.graphqls
      # and ProductInterface extension to appear here
    }
  }
}

# If total_count is 0 and you expect results:
# 1. Check attribute configuration in Magento Admin
# 2. Verify field mapping: GET /magento2_product_1/_mapping
# 3. Run: bin/magento indexer:reindex catalogsearch_fulltext
# 4. Check GraphQL schema for filter input registration

10. Summary

OpenSearch and Magento GraphQL work together across several abstraction layers: the resolver translates GraphQL arguments into SearchCriteria objects, the search adapter translates those into OpenSearch query DSL objects, and the result is combined with database data before it is returned as a GraphQL response. This architecture is powerful, but it requires knowledge at every level for correct configuration and performance optimization.

The most important levers for performant GraphQL search in Magento: configure all relevant filter attributes as filterable, keep the index up to date, use aggregations for facet filters and minimize the reload layer from the database by keeping as many fields as possible in the index. This is not a one-time setup step; indexing strategy and OpenSearch configuration need to be considered for every new attribute and every new custom module.

OpenSearch and Magento GraphQL: The Essentials at a Glance

Architecture

Resolver to SearchCriteria to search adapter to OpenSearch query to IDs to database query for details.

Indexing

Only indexed attributes can be used as GraphQL filters without a DB fallback. Aggregations require the OpenSearch path.

Configuration

Mark the attribute as filterable, extend schema.graphqls, rebuild the index. Three steps for every custom filter.

Diagnostics

Empty aggregations means OpenSearch is not active. Empty results mean a stale index or incorrect attribute mapping.

11. FAQ: OpenSearch and Magento GraphQL

1When does Magento use OpenSearch instead of the DB?
Always with a search argument. For filter-only: only when all filter attributes are configured as filterable in the index. Otherwise DB fallback.
2Why does aggregations return an empty array?
The OpenSearch path is not used, DB fallback instead. Cause: filter attribute not enabled as "Used in Layered Navigation." Rebuild the index.
3Does OpenSearch return full product data?
No. IDs and scores. Full details come from a second database query, that is Magento's two-stage search architecture.
4Configuring a custom attribute for GraphQL filters?
1) Mark the attribute as filterable. 2) Extend schema.graphqls. 3) Run indexer:reindex catalogsearch_fulltext.
5text vs. keyword in the OpenSearch mapping?
text is tokenized for full-text search. keyword is an exact value for filters. Many fields have both as a multi-field.
6Why does a category filter return empty results?
Stale index. Category IDs in the product index do not match the DB. Fix: indexer:reindex catalog_category_product and catalogsearch_fulltext.
7How to debug which OpenSearch query Magento sends?
OpenSearch slow log or dashboard. Xdebug in the search adapter. The sent query DSL shows filters, weights and aggregations.
8Impact of a stale index on GraphQL?
Missing or incorrect products, wrong aggregation counts. Scheduled indexing or mview for regular updates in production.
9Adjusting search field weights?
Yes, via admin configuration or search_request.xml. Controls which fields are weighted more heavily during full-text searches.
10catalogsearch_fulltext vs. catalog_category_product?
catalogsearch_fulltext is the OpenSearch product index. catalog_category_product is the DB mapping table. Both need to be current for correct category filters.