Skip to content

Search and Track with the JS SDK

Aurélien Peronnet edited this page May 22, 2026 · 2 revisions

Search your catalog

Your catalog is finally indexed, your are now ready to enhance your search experience.
The SearchManager service provides powerful search capabilities including full-text search, filtering, faceting, sorting, and pagination.

const baseUri = 'http://gally.localhost/api'
const searchManager = new SearchManager({ baseUri })
const response = await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  searchQuery: 'shoes',
  currentPage: 1,
  pageSize: 10,
})

Basic Search

Text Search

const response = await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  searchQuery: 'nike shoes',
  currentPage: 1,
  pageSize: 20,
})

console.log(`Found ${response.getTotalCount()} products`)

for (const product of response.getCollection()) {
  console.log(`- ${product.name} (${product.sku})`)
}

Browse All Products

const response = await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  searchQuery: '', // Empty query = browse all
  currentPage: 1,
  pageSize: 50,
})

Field Selection

Choose which fields to return:

const response = await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  selectedFields: ['id', 'sku', 'name', 'price', 'stock'],
  searchQuery: 'shoes',
})

for (const product of response.getCollection()) {
  console.log({
    id: product.id,
    sku: product.sku,
    name: product.name,
    price: product.price,
    stock: product.stock,
  })
}

Pagination

Basic Pagination

const response = await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  currentPage: 2,
  pageSize: 20,
})

console.log(`Page ${response.currentPage} of ${response.pageCount}`)
console.log(`Showing ${response.collection.length} of ${response.getTotalCount()} total`)

Iterate Through Pages

async function getAllProducts(searchManager, localizedCatalog) {
  const allProducts = []
  let currentPage = 1
  let hasMore = true
  
  while (hasMore) {
    const response = await searchManager.search({
      localizedCatalog,
      metadata: 'product',
      currentPage,
      pageSize: 100,
    })
    allProducts.push(...response.getCollection())
    
    hasMore = currentPage < response.pageCount
    currentPage++
  }
  
  return allProducts
}

Filtering

Single Filter

await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  filters: [
    {
      brand__value: {
        eq: 'nike'
      }
    }
  ]
})

Multiple Filters

await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  filters: [
    {
      brand__value: {
        eq: 'nike'
      },
    },
    {
      color__value: {
        in: ['blue', 'white']
      }
    },
    {
      price__price: {
        gte: 100,
        lte: 200
      }
    }
  ],
})

Sorting

Get Available Sort Options

const sortOptions = await searchManager.getProductSortingOptions()

for (const option of sortOptions) {
  console.log(`- ${option.code}: ${option.label}`)
}

// Example output:
// - _score: Relevance
// - name: Name
// - price__price: Price
// - stock__status: Stock status
// - category__position: Position

Apply Sorting

await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  sortField: 'price__price',
  sortDirection: 'asc'
})

// Sort by relevance (default for searches)
await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  searchQuery: 'shoes',
  sortField: '_score',
  sortDirection: 'desc',
})

Aggregations (Facets)

Get Aggregations

const response = await searchManager.search(request)

const aggregations = response.getAggregations()

for (const agg of aggregations) {
  console.log(`${agg.label} (${agg.type}):`)
  
  for (const option of agg.options) {
    console.log(`  - ${option.label}: ${option.count} products`)
  }
}

// Example output:
// Brand (checkbox):
//   - Nike: 42 products
//   - Adidas: 38 products
// Price (slider):
//   - 0-50: 25 products
//   - 50-100: 30 products

Facet Types

Type Description Example
checkbox Multiple selection facets Brand, Color
boolean Yes/No facets In Stock, On Sale
slider Range facets Price, Rating
category Hierarchical category facets Category Tree

Autocomplete

Autocomplete Search

const response = await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  searchQuery: 'sho',
  isAutocomplete: true,
  pageSize: 5,
})

for (const suggestion of response.getCollection()) {
  console.log(`- ${suggestion.name}`)
}

Complete Search Example

See a complete search example
import { SearchManager } from '@gally/sdk'

async function searchProducts({
  query = '',
  page = 1,
  pageSize = 20,
  brand = [],
  inStock = null,
  priceRange = null,
  sortBy = '_score',
  sortOrder = 'desc',
}) {
  const searchManager = new SearchManager(config)
  
  // Build filters
  const filters = []
  
  if (brand.length > 0) {
    filters.push({
      brand: { in: brand } 
    })
  }
  
  if (inStock !== null) {
    filters.push({
      stock__status: { eq: inStock } 
    })
  }
  
  if (priceRange) {
    const [minPrice, maxPrice] = priceRange.split('-') 
    filters.push({
      price__price: { gte: minPrice, lte: maxPrice } 
    })
  }
    
  // Execute search
  const response = await searchManager.search({
    localizedCatalog: 'sdk_test_shop_fr',
    metadata: 'product',
    searchQuery: query,
    currentPage: page,
    pageSize,
    filters,
    sortField: sortBy,
    sortDirection: sortOrder,
    selectedFields: ['id', 'sku', 'name', 'price', 'stock', 'brand', 'image'],
  })
  
  return {
    products: response.getCollection(),
    total: response.getTotalCount(),
    currentPage: response.currentPage,
    pageCount: response.pageCount,
    aggregations: response.getAggregations(),
  }
}

// Usage
const results = await searchProducts({
  query: 'running shoes',
  page: 1,
  pageSize: 20,
  brand: ['nike', 'adidas'],
  inStock: true,
  priceRange: '50-150',
  sortBy: 'price__price',
  sortOrder: 'asc',
})

console.log(`Found ${results.total} products`)
console.log(`Page ${results.currentPage} of ${results.pageCount}`)

for (const product of results.products) {
  console.log(`- ${product.name}: €${product.price[0].price}`)
}

Browser Usage

For client-side search (recommended for frontend apps):

import { SearchManager } from '@gally/sdk/browser'

const searchManager = new SearchManager({
  baseUri: 'https://your-gally-instance.com/api/',
})

// Use exactly as shown above
const response = await searchManager.search(request)

Advanced Features

Category Context

Search within a specific category:

await searchManager.search({
  localizedCatalog: 'sdk_test_shop_fr',
  metadata: 'product',
  categoryCode: 'cat_shoes',
  searchQuery: 'nike',
})

Boost Fields

Custom boosting is configured server-side via Gally's admin interface. The SDK automatically applies configured boost values.

Spell Correction

Spell correction is applied automatically by Gally when enabled.

Performance Tips

  1. Limit selected fields - Only request fields you need
  2. Use appropriate page sizes - 20-50 items is optimal
  3. Cache search results - Cache results on the client side
  4. Debounce autocomplete - Wait for user to finish typing
  5. Use browser export - Smaller bundle size for frontend

Error Handling

try {
  const response = await searchManager.search(request)
} catch (error) {
  if (error.response?.status === 404) {
    console.error('Catalog or index not found')
  } else if (error.response?.status === 400) {
    console.error('Invalid search request:', error.message)
  } else {
    console.error('Search failed:', error.message)
  }
}

Track user interactions

The Gally SDK tracks user interactions throughout the shopping journey via a single push() method. Two key features keep event calls minimal:

  • Automatic session managementsessionUid (session cookie) and sessionVid (long-term cookie) are generated and persisted automatically. You never need to pass them.
  • Automatic context inference — after a category view or search event, the SDK stores the context in sessionStorage and silently applies it to all subsequent events. You only declare the context once.

If your project already uses a bundler (Vite, Webpack, etc.), the snippet approach is not recommended — the SDK is already included in your bundle. Use TrackingEventManager.init() directly.

import { TrackingEventManager, TrackingEventType } from '@gally/sdk/browser'

const tracker = TrackingEventManager.init({
  baseUri: 'https://your-gally-instance.com/api',
})

init() is a singleton — calling it multiple times always returns the same instance. The tracker is also available globally as window.gallyEvent after the first call.

You can also include the tracker using an IIFE.


Event Types

1. View — Category

Viewing a category page sets the context for all subsequent events in the session.

await tracker.push({
  eventType: TrackingEventType.VIEW,
  metadataCode: 'category',
  localizedCatalogCode: 'my_shop_fr',
  entityCode: 'cat_shoes',
  payload: JSON.stringify({
    product_list: {
      item_count: 24,
      current_page: 1,
      page_count: 3,
      sort_order: 'position',
      sort_direction: 'asc',
      filters: [],
    },
  }),
})

After this call, contextType: 'category' and contextCode: 'cat_shoes' are automatically applied to all following events.


2. Display

Track the list of products rendered on screen. Context is injected automatically.

await tracker.push({
  eventType: TrackingEventType.DISPLAY,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  payload: JSON.stringify({
    items: [
      { entityCode: 'PROD-001', display: { position: 0 } },
      { entityCode: 'PROD-002', display: { position: 1 } },
      { entityCode: 'PROD-003', display: { position: 2 } },
    ],
  }),
})

3. View — Product

Context from the previous category view or search is applied automatically.

await tracker.push({
  eventType: TrackingEventType.VIEW,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  entityCode: 'PROD-001',
})

4. Search

A search event sets the context for all subsequent events (contextType: 'search', contextCode: <query>).

await tracker.push({
  eventType: TrackingEventType.SEARCH,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  payload: JSON.stringify({
    search_query: {
      is_spellchecked: false,
      query_text: 'running shoes',
    },
    product_list: {
      item_count: 12,
      current_page: 1,
      page_count: 2,
      sort_order: 'relevance',
      sort_direction: 'desc',
      filters: [],
    },
  }),
})

5. Add to Cart

await tracker.push({
  eventType: TrackingEventType.ADD_TO_CART,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  entityCode: 'PROD-001',
  payload: JSON.stringify({
    cart: { qty: 2 },
    child_sku: 'PROD-001-RED-XL'
  }),
})

⚠️ For entityCode use the parent product SKU, for child_sku prefer using the child product SKU if available


6. Order

await tracker.push({
  eventType: TrackingEventType.ORDER,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  payload: JSON.stringify({
    order: {
      order_id: 'ORD-12345',
      total: 259.98,
    },
    items: [
      {
        child_sku: 'PROD-001-RED-XL',
        entityCode: 'PROD-001',
        order: { price: 129.99, qty: 2, row_total: 259.98 }
      },
    ],
  }),
})

⚠️ For entityCode use the parent product SKU, for child_sku prefer using the child product SKU if available


Typical User Journey

Context propagates automatically across the session. You only set it once — on the first category view or search.

See a complete example of tracking a user journey
// 1. User lands on the "Shoes" category
await tracker.push({
  eventType: TrackingEventType.VIEW,
  metadataCode: 'category',
  localizedCatalogCode: 'my_shop_fr',
  entityCode: 'cat_shoes',         // → sets context: category / cat_shoes
})

// 2. Products are rendered
await tracker.push({
  eventType: TrackingEventType.DISPLAY,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  payload: JSON.stringify({        // context auto-applied ✅
    items: [
      { entityCode: 'PROD-001', display: { position: 0 } },
      { entityCode: 'PROD-002', display: { position: 1 } },
    ],
  }),
})

// 3. User clicks a product
await tracker.push({
  eventType: TrackingEventType.VIEW,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  entityCode: 'PROD-001',          // context auto-applied ✅
})

// 4. User adds to cart
await tracker.push({
  eventType: TrackingEventType.ADD_TO_CART,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  entityCode: 'PROD-001',          // context auto-applied ✅
  payload: JSON.stringify({ 
    child_sku: 'PROD-001-RED-XL',
    cart: { qty: 1 } 
  }),
})

// 5. User checks out
await tracker.push({
  eventType: TrackingEventType.ORDER,
  metadataCode: 'product',
  localizedCatalogCode: 'my_shop_fr',
  payload: JSON.stringify({        // context auto-applied ✅
    order: { order_id: 'ORD-42', total: 129.99 },
    items: [
      { 
        entityCode: 'PROD-001', 
        child_sku: 'PROD-001-RED-XL', 
        order: { price: 129.99, qty: 1, row_total: 129.99 } 
      }
    ],
  }),
})

Context Inference Rules

Event Sets context
VIEW with metadataCode: 'category' contextType: 'category', contextCode: entityCode
SEARCH contextType: 'search', contextCode: payload.search_query.query_text
VIEW (product), ORDER Updates sourceEventType / sourceMetadataCode only
DISPLAY, ADD_TO_CART Does not update context

Context is stored in sessionStorage and persists across page navigations within the same browser tab.


Session Identifiers

Cookie Type Lifetime Description
gally-session-uid Session cookie 1 hour Unique ID for the current browsing session
gally-session-vid Persistent cookie 1 year Unique ID for the visitor across sessions

Both are created on the first tracked event and never need to be passed manually.


Event Fields Reference

Field Required Description
eventType TrackingEventType.VIEW / DISPLAY / SEARCH / ADD_TO_CART / ORDER
metadataCode Entity type: 'product' or 'category'
localizedCatalogCode Catalog code, e.g. 'my_shop_fr'
entityCode Context-dependent Product SKU or category code
contextType Auto-inferred 'category' or 'search'
contextCode Auto-inferred Category code or search query
sourceEventType Auto-inferred Event type that set the current context
sourceMetadataCode Auto-inferred Metadata code of the context-setting event
sessionUid Auto-generated Omit — populated from cookie automatically
sessionVid Auto-generated Omit — populated from cookie automatically
payload Event-dependent JSON-encoded event-specific data (see below)

Payload Formats by events

VIEW (category) and SEARCH

product_list: {
  item_count: number,
  current_page: number,
  page_count: number,
  sort_order: string,
  sort_direction: 'asc' | 'desc',
  filters: Array<{ name: string, value: string }>
}

// Only for SEARCH events
search_query: {
  is_spellchecked: boolean,
  query_text: string
}

DISPLAY

items: Array<{
  entityCode: string,
  display: { position: number }
}>

ADD_TO_CART

cart: {
  qty: number
}
child_sku: string

ORDER

order: {
  order_id: string,
  total: number
}
items: Array<{
  entityCode: string,
  child_sku: string,
  order: { price: number, qty: number, row_total: number }
}>

Performance

Setting Default Description
batchSize 10 Max events per API request
debounceMs 300 Delay before flushing the queue
throttleMs 1000 Minimum time between requests
const tracker = TrackingEventManager.init({
  baseUri: 'https://your-gally-instance.com/api',
  batchSize: 20,
  debounceMs: 500,
  throttleMs: 2000,
})

Call tracker.flushPending() before page unload to ensure no events are lost:

window.addEventListener('beforeunload', () => tracker.flushPending())

Include the tracker using IIFE

As mentionned above the tracker can be used either by it importing directly or using an IIFE in your application head. Paste this in the <head> of every page, before any other script. Only sdkUrl and baseUri need to be changed.

<head> parses
│
├── Inline stub runs synchronously
│   └── window.gallyEvent = { push: writeToLocalStorage }
│
├── SDK script tag injected → starts downloading (non-blocking)
│
<body> renders — page is fully interactive
│
├── Code pushes events anytime
│   └── gallyEvent.push({...}) → localStorage queue
│
SDK finishes downloading
│
└── onload → window.gallyEvent.init({ baseUri })
    └── TrackingEventManager.replayPersistedEvents()
        └── Sends all queued events to the API  ✅

No events are lost, even if they are triggered during the very first paint before the SDK has had time to download.


Snippet

<script>
  const gallyOptions = {
    sdkUrl: 'https://cdn.jsdelivr.net/npm/@elastic-suite/gally-sdk@2.2.2-alpha.0/dist/browser/iife/gally-sdk.global.js',
    baseUri: 'https://your-gally-instance.com/api',
    uidCookieMaxAge: 60 * 60,
    vidCookieMaxAge: 365 * 24 * 60 * 60
  }
  ;(function(w,d,o){
    if(w.gallyEvent)return
    w.gallyEvent={push:function(i){try{const r=localStorage.getItem('gally-event-queue'),c=r?JSON.parse(r):[];c.push(i);localStorage.setItem('gally-event-queue',JSON.stringify(c))}catch(_){}}}
    const g=d.createElement('script'),s=d.scripts[0];g.async=1;g.src=o.sdkUrl;g.onload=function(){w.gallyEvent.init(o)};s.parentNode.insertBefore(g,s)
  })(window,document,gallyOptions)
</script>

Configuration

Constant Description Required Default
sdkUrl URL of the IIFE bundle, use either:
- CDN URL: https://cdn.jsdelivr.net/npm/@elastic-suite/gally-sdk@2.2.2-alpha.0/dist/browser/iife/gally-sdk.global.js
- Self-hosted: Self-hosting the sdk bundle
yes
baseUri Base URL of your Gally API instance yes
uidCookieMaxAge Duration of the session cookie (to identify a navigation session) in seconds no 60 * 60 (1 hour)
vidCookieMaxAge Duration of the visitor cookie (to identify a visitor on the long term) no 365 * 24 * 60 * 60 (1 year)

Pushing Events

Call window.gallyEvent.push() anywhere on the page, at any time — before or after the SDK loads.

Self-Hosting the browser SDK Bundle

Instead of relying on a CDN, you can serve the IIFE bundle from your own infrastructure.

Build it:

npm run build
# outputs dist/browser/iife/gally-sdk.global.js

Serve it and update sdkUrl to point to your server:

const gallyOptions = {
  sdkUrl: 'https://assets.your-domain.com/gally-sdk.global.js',
  ...other options
}

Loading both the npm bundle and the IIFE bundle on the same page creates two separate module instances that conflict over window.gallyEvent. Use one or the other.


Clone this wiki locally