Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

Dev guideRecipesAPI ReferenceChangelog
Dev guideRecipesUser GuidesNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Product search with inventory

How to use Optimizely Graph to search and display products by keyword and price range.

You can use Optimizely Graph queries to search products by keyword, price range, and inventory status. You can test queries in the GraphQL Explorer.

Basic product search query

Use this query to search for products by keyword and filter them by price. It helps you build ecommerce experiences using the GenericProduct model.

The following query searches for products that match a keyword in their name or description and filters results by a price range, and returns the most relevant products first:

query ProductSearchWithInventory($searchTerm: String, $minPrice: Float, $maxPrice: Float, $skip: Int = 0, $limit: Int = 20) {
  GenericProduct(
    where: {
      _and: [
        {
          _or: [
            { _fulltext: { match: $searchTerm } }
            { Name: { match: $searchTerm, boost: 5 } }
            { Description: { match: $searchTerm, boost: 3 } }
          ]
        }
        { DefaultMarketPrice: { gte: $minPrice, lte: $maxPrice } }
      ]
    }
    skip: $skip
    limit: $limit
    orderBy: { _ranking: SEMANTIC }
  ) {
    total
    facets {
      DefaultMarketPrice(
        ranges: [
          { from: 0, to: 50 }
          { from: 50, to: 100 }
          { from: 100, to: 500 }
          { from: 500 }
        ]
      ) {
        name
        count
      }
    }
    items {
      _score
      Name
      RelativePath
      Description
      DefaultMarketPrice
      DefaultImageUrl
    }
  }
}

Advanced product search with inventory status

Use this query to refine your product search with boosted relevance and sorted pricing. It lets you sort available products in ascending order by price.

This query boosts matches in product names and descriptions and filters by price, and sorts results by relevance and price (lowest first):

query AdvancedProductSearch(
  $searchTerm: String,
  $minPrice: Float,
  $maxPrice: Float,
  $skip: Int = 0,
  $limit: Int = 20
) {
  GenericProduct(
    where: {
      _and: [
        {
          _or: [
            { _fulltext: { match: $searchTerm } }
            { Name: { match: $searchTerm, boost: 8 } }
            { Description: { match: $searchTerm, boost: 3 } }
          ]
        }
        { DefaultMarketPrice: { gte: $minPrice, lte: $maxPrice } }
      ]
    }
    skip: $skip
    limit: $limit
    orderBy: { _ranking: SEMANTIC, DefaultMarketPrice: ASC }
  ) {
    total
    facets {
      DefaultMarketPrice(ranges: [
        { from: 0, to: 50 }
        { from: 50, to: 100 }
        { from: 100, to: 500 }
        { from: 500 }
      ]) {
        name
        count
      }
    }
    items {
      _score
      Name
      RelativePath
      Description
      DefaultMarketPrice
      DefaultImageUrl
    }
  }
}

React implementation with filters

Use this React component to build a product search interface with keyword input, price filters, and pagination. It dynamically fetches and displays products based on user input.

This component lets users search for products by keyword and price and displays search results, handles loading, and error states, and supports pagination:

import { gql, useQuery } from '@apollo/client';
import { useState } from 'react';

const PRODUCT_SEARCH = gql`
  query ProductSearchWithInventory(
    $searchTerm: String,
    $minPrice: Float,
    $maxPrice: Float,
    $skip: Int = 0,
    $limit: Int = 20
  ) {
    GenericProduct(
      where: {
        _and: [
          {
            _or: [
              { _fulltext: { match: $searchTerm } }
              { Name: { match: $searchTerm, boost: 5 } }
              { Description: { match: $searchTerm, boost: 3 } }
            ]
          }
          { DefaultMarketPrice: { gte: $minPrice, lte: $maxPrice } }
        ]
      }
      skip: $skip
      limit: $limit
      orderBy: { _ranking: SEMANTIC }
    ) {
      total
      facets {
        DefaultMarketPrice(ranges: [
          { from: 0, to: 50 }
          { from: 50, to: 100 }
          { from: 100, to: 500 }
          { from: 500 }
        ]) {
          name
          count
        }
      }
      items {
        _score
        Name
        RelativePath
        Description
        DefaultMarketPrice
        DefaultImageUrl
      }
    }
  }
`;

export function ProductSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
  const [currentPage, setCurrentPage] = useState(1);
  const pageSize = 20;

  const { loading, error, data, refetch } = useQuery(PRODUCT_SEARCH, {
    variables: {
      searchTerm: searchTerm || undefined,
      minPrice: priceRange.min,
      maxPrice: priceRange.max,
      skip: (currentPage - 1) * pageSize,
      limit: pageSize
    },
    errorPolicy: 'partial'
  });

  const products = data?.GenericProduct?.items || [];
  const priceFacets = data?.GenericProduct?.facets?.DefaultMarketPrice || [];
  const totalResults = data?.GenericProduct?.total || 0;

  const handleSearch = (e) => {
    e.preventDefault();
    setCurrentPage(1);
    refetch();
  };

  return (
    <div className="product-search">
      <form onSubmit={handleSearch} className="search-form">
        <input
          type="text"
          placeholder="Search products..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      <p>{totalResults} products found</p>

      <div className="filters">
        <h3>Price Range</h3>
        <input
          type="number"
          placeholder="Min"
          value={priceRange.min}
          onChange={(e) => setPriceRange(prev => ({ ...prev, min: Number(e.target.value) }))}
        />
        <input
          type="number"
          placeholder="Max"
          value={priceRange.max}
          onChange={(e) => setPriceRange(prev => ({ ...prev, max: Number(e.target.value) }))}
        />
        <div className="price-facets">
          {priceFacets.map(facet => (
            <div key={facet.name}>
              {facet.name}: {facet.count} items
            </div>
          ))}
        </div>
      </div>

      <div className="products-grid">
        {products.map(product => (
          <div key={product.RelativePath} className="product-card">
            {product.DefaultImageUrl && (
              <img src={product.DefaultImageUrl} alt={product.Name} />
            )}
            <h3>
              <a href={product.RelativePath}>{product.Name}</a>
            </h3>
            {product.Description && (
              <p>{product.Description.substring(0, 100)}...</p>
            )}
            <p>${product.DefaultMarketPrice.toFixed(2)}</p>
            <p>Relevance: {(product._score * 100).toFixed(1)}%</p>
          </div>
        ))}
      </div>

      <div className="pagination">
        <button disabled={currentPage === 1} onClick={() => setCurrentPage(prev => prev - 1)}>Previous</button>
        <span>Page {currentPage} of {Math.ceil(totalResults / pageSize)}</span>
        <button disabled={currentPage * pageSize >= totalResults} onClick={() => setCurrentPage(prev => prev + 1)}>Next</button>
      </div>
    </div>
  );
}