-
Notifications
You must be signed in to change notification settings - Fork 0
Search and Track with the JS SDK
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,
})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})`)
}const response = await searchManager.search({
localizedCatalog: 'sdk_test_shop_fr',
metadata: 'product',
searchQuery: '', // Empty query = browse all
currentPage: 1,
pageSize: 50,
})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,
})
}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`)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
}await searchManager.search({
localizedCatalog: 'sdk_test_shop_fr',
metadata: 'product',
filters: [
{
brand__value: {
eq: 'nike'
}
}
]
})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
}
}
],
})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: Positionawait 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',
})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| 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 |
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}`)
}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}`)
}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)Search within a specific category:
await searchManager.search({
localizedCatalog: 'sdk_test_shop_fr',
metadata: 'product',
categoryCode: 'cat_shoes',
searchQuery: 'nike',
})Custom boosting is configured server-side via Gally's admin interface. The SDK automatically applies configured boost values.
Spell correction is applied automatically by Gally when enabled.
- Limit selected fields - Only request fields you need
- Use appropriate page sizes - 20-50 items is optimal
- Cache search results - Cache results on the client side
- Debounce autocomplete - Wait for user to finish typing
- Use browser export - Smaller bundle size for frontend
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)
}
}The Gally SDK tracks user interactions throughout the shopping journey via a single push() method. Two key features keep event calls minimal:
-
Automatic session management —
sessionUid(session cookie) andsessionVid(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
sessionStorageand 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.
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'andcontextCode: 'cat_shoes'are automatically applied to all following events.
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 } },
],
}),
})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',
})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: [],
},
}),
})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
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
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 }
}
],
}),
})| 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.
| 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.
| 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) |
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
}items: Array<{
entityCode: string,
display: { position: number }
}>cart: {
qty: number
}
child_sku: stringorder: {
order_id: string,
total: number
}
items: Array<{
entityCode: string,
child_sku: string,
order: { price: number, qty: number, row_total: number }
}>| 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())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.
<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>| 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) |
Call window.gallyEvent.push() anywhere on the page, at any time — before or after the SDK loads.
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.jsServe 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.
1. Getting started
2. Managing your content
3. Searching and tracking user interactions