NAV
curl Ruby PHP Postman

Introduction

Welcome to the Luigi's Box Live API! You can use our API to access Luigi's Box Live features such as autocomplete, search, and recommender.

We have examples in multiple languages. You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right.

Authentication

Most of the available endpoints use HMAC authentication to restrict access. To use the API you'll need a

If you need help contact our support.

Our API expects you to include these HTTP headers:

HTTP Header Comment
Content-Type e.g., application/json; charset=utf-8
Standard HTTP header. Some endpoints allow you to POST JSON data, while some are purely GET-based
Authorization e.g., ApiAuth 1292-9381:sd73hdh881gfop228
The general format is client tracker_id:digest. The client part is not used, it's best to provide your application name, or a generic name, such as "ApiAuth". You must compute the digest using the method described below.
date e.g., Thu, 29 Jun 2017 12:11:16 GMT
Request date in the HTTP format. Note that this is cross-validated on our servers and if your clock is very inaccurate, your requests will be rejected. We tolerate ±5 second inaccuracies. You will be including this timestamp in the digest computation, so what this means in plain terms is that you cannot cache the digest and must recompute it for each request.
Content-Encodingoptional e.g., gzip
Optional HTTP header. Content updates endpoint allows you to use gzip or deflate request payload compression methods to send large payloads effectively.

Digest computation

require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


date = Time.now.httpdate
digest("secret", "POST", "/v1/content", date)
<?php

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');
digest("secret", "POST", "/v1/content", $date);
// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestUri = request.url.replace(/^.*\/\/[^\/]+/, '').replace(/\?.*$/, '');
var timestamp = new Date().toUTCString();
var signature = [request.method, "application/json; charset=utf-8", timestamp, requestUri].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
echo $(digest "secret", "GET", "/v1/content", $date)

You must use HMAC SHA256 to compute the digest and it must be Base64 encoded.

Payload to be signed is computed as a newline-d concatenation of

Make sure that you are using the same values for digest computation as for the actual request. For example, if you compute digest from Content-Type: application/json; charset=utf-8, make sure you send the request with the exact same Content-Type (and not e.g. Content-Type: application/json).

Most programming languages provide crypto libraries which let you compute the HMAC digest with minimal effort. When the particular endpoint requires HMAC, we provide examples in several languages in the right column in its documentation.

The pseudocode for HMAC computation is:

signature = [request_method, content_type, timestamp, request_path].join("\n")
digest = base64encode(hmacsha256(signature, your_private_key))

Look for examples in the right column. You can find examples for other languages online, however, those were not tested by us. See the following external links for more examples:

Server will return HTTP 401 Not Authorized if your authentication fails. If this happens, look inside the response body. We include a diagnostics output which will tell you what was wrong with your request.

Diagnostics output as shown by Postman

Throttling

There are different limits for requests per time period in place for endpoints.

Content Updates

Autocomplete

If you exceed any of the limits, you'll get a 429 Too Many Requests response for subsequent requests. Check the Retry-After header to see how many seconds to wait before retrying the request.

If you find these limits unsufficient for your site, please contact us and we can put exceptions for higher limits in place.

Error handling

The API may return errors under specific circumstances. The table below summarizes some of the errors that you may encounter and a recommended way to handle them.

HTTP Status Reason Transient Recommended handlingg
408 Timeout The request is taking too long to process. This may mean a temporary overload on our side. Yes Since this is a transient error, you may safely retry the request. For autocomplete requests, retrying is not necessary, since the user will keep typing and a new request will be made naturally. For search and recommender, retry the request once. Avoid retrying the request more than once.
429 Too Many Requests You are breaching one of the throttling limits. No The default throttling limits are generous and you should not encounter this error. If you are seeing this error in production, for non-realtime APIs (such as Content Updates), obey the Retry-After header and retry the request. For realtime APIs (autocomplete, search, recommender), fail the request, raise an alarm and get in touch with us to investigate the reason for throttling.
500 Internal Server Error This is a bug on our side. No Fail the request, raise an alarm and get in touch with us to investigate the problem.
502 Bad Gateway Seeing this error may mean an operational incident on our side. No Fail the request, raise an alarm and check luigisboxstatus.com to see if there is an active incident and find out more details.
503 Service Unavailable Seeing this error may mean an operational incident on our side. No Fail the request, raise an alarm and check luigisboxstatus.com to see if there is an active incident and find out more details.

Development mode

While you are developing the integration, your API connection may be put into a development mode, where a small percentage of requests will return deliberate errors. The goal of the development mode is to expose you to errors that you will normally not see and help you write an error handling code.

These errors will be clearly marked as the development mode errors. Note that we will not enable development mode without notifying you, so unless you received an explicit notification, your API connection is not in the development mode.

To disable the development mode, contact support@luigisbox.com to switch your API connection into the production mode.

Importing your data

To use our Autocomplete and Search APIs, we need a way to synchronize your product catalog with our servers.

Once we have your catalog, we continuosly and automatically match the products from the catalog with our search analytics data and adjust their ranking.

We support two ways of catalog synchronization;

  1. The preferred way of synchronization is via our Content Updates API. Content updates enable near real-time synchronization of your database and make your search results accurate and up-to-date.

  2. We also support synchronization via XML or CSV feeds. We can setup regular processing of your feed and use the feed data to build your search index. However, be aware that even though we can process the feed several times a day, there will be periods of time where your search index is stale. For example, we process your feed at 8:00, and then, at 8:32 some of your products go out of stock. Your search index will be stale for several hours until we process your feed again. If you want to avoid stale search index, you need to implement the Content Updates API.

Content updates

When implementing Luigi's Box Search or Autocomplete service, you need to synchronize your product catalog (your database) with our search index. You should call the Content updates API in any of these cases:

Purpose Example trigger Endpoint
Make product searchable
  • New product gets in stock
  • Product which was unavailable becomes available again
Content update
Update product attributes
  • Product price changes
  • Someone updates product description
  • New product review was posted and product rating changes
Content update or Partial content update or Update by query
Remove project from search results
  • Product has sold out and will not be restocked
  • Product should be temporarily removed from all offerings
Content removal

Content update

HTTP Request

require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.post("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "POST", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "POST" "/v1/content" "$date")

curl -i -XPOST --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'POST', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['POST', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Blue Socks",
        "description": "Comfortable socks",
        "price": "2.9 €",
        "color": "blue",
        "material": "wool"
      },
      "nested": [
        {
          "title": "socks",
          "type": "category",
          "url": "https://myshop.example/categories/socks"
        }
      ]
    },
    {
      "url": "https://myshop.example/category/apparel",
      "type": "category",
      "fields": {
        "title": "Apparel"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "type": "article",
      "fields": {
        "title": "Contact us"
      }
    }
  ]
}

POST https://live.luigisbox.com/v1/content

This endpoint requires a JSON input which specifies the objects that should be updated in Luigi's Box search index. This API accepts an array of objects - each item in the objects array is a single object with its attributes which should be updated or inserted into Luigi's Box index. This allows you to index several objects with a single API call. This is mainly useful for initial import when you can send many objects at once and speed up the indexing process. The optimal number of objects to send in a single batch depends on many factors, mainly your objects size. We recommend that you send around 100 objects in a single batch.

Be aware that updates to object attributes are not incremental. The object in Luigi's Box index is always replaced with the attributes you send. If you send all object attributes in the first call and then send just a single attribute in another call, your object will retain only the single attribute from the second call - all other attributes will be lost. In practice this means that you must always send all object attributes with each API call. If you would like to update only part of the object, see Partial content update.

The object's JSON has following top-level attributes.

Attribute Description
urlREQUIRED Canonical URL of the object. It also serves as unique identifier of your object.
typeREQUIRED You can have several searchable types (e.g. products and categories) and search them separately. Note, that we automatically build a special type with the name query which contains queries recorded on your site. You can use this type to build an autocomplete widget which suggests queries.
autocomplete_typeoptional If you wish to override type for the purpose of autocomplete, override it here. This can be either single value, or an array of autocomplete_types. Usually it is used to define a more narrow scope of a type (aka filter for autocomplete). For instance, you can define an item type with in promotion autocomplete_type, to be able to perform autocomplete solely within items in promotion.
generationREQUIRED Object generation, see Generations documentation below
active_fromoptional The date/time at which this object should become searchable. This allows you to schedule search activation in advance. The date/time must be formatted in the ISO 8601 format, e.g. 2019-05-17T21:12:35+00:00
active_tooptional The date/time at which this object should stop being searchable. This allows you to schedule search deactivation in advance. The date/time must be formatted in the ISO 8601 format, e.g. 2019-05-17T21:12:35+00:00. To prevent accumulation of expired items, we will periodically delete expired items from our data stores. If you are issuing a partial update for an expired item, the partial update may fail because the item is no longer present.
fieldsREQUIRED Object attributes. Every field that you send will be searchable and can be used for filtering. You must send a field named title which we use as the object's display name. We automatically construct filtering facets out of the fields of your products. For instance, if you send a field called color with your product objects, we can show a color facet next to your search results and your users can filter the search results to only those products that have a specific color. Some fields are special, see Special fields below for more details.
nestedoptional Array of nested items, each having type, url and title. Ideal for categories, brands, variants and other objects, which are linked to the current object, but can also be addressed as a standalone object. For instance, to send products along with categories they belong to - the categories might be included as nested items. These are extracted server-side and stored also separately. You can also opt for including fields structure instead of title to include several attributes, not just the title.

There are no hard rules about field names, or which fields you have to send (except "title"), but when thinking about the fields, consider the following recommendations. If you are planning to use our Autocomplete widget see the note on Autocomplete widget integration.

There are few technical recommendations when dealing with fields:

Example of correct use of json field (only one level of nesting)

"fields": {
  "special": {
    "identifier": "X6454",
    "material": "metal"
  }
}

Example of wrong use of json field (multiple levels of nesting)

"fields": {
  "special": {
    "data": {
      "identifier": "X6454",
      "material": "metal"
    }
  }
}

Special fields

There are several fields which have special behavior:

Field name Description
title Required field. If you are using our Autocomplete widget, the title field will be automatically used by the widget as the result title.
availability If you send this field, it must have numeric value of 1 - meaning the product is available or 0 - product is unavailable. We are automatically sorting available results first. If an object does not have this field, we treat it as if it was available.
availability_rank This is a more advanced and granular version of the availability field. While availability is binary — a product is either available or not, availability_rank allows you to encode various availability "degrees". If you send this field, it must have a numeric value between 1 and 15. The semantics of this field is that the lower the number, the more available the product. It is up to you to devise an encoding between your domain availability semantics and availability_rank field. For example, you may set availability_rank: 1 for products which are ready for immediate shipment, availability_rank: 2 for products which will ship within 2 days, availability_rank: 3 for products which will ship within a week and availability_rank: 15 for products which are no longer available. We are automatically sorting "more available" results first.
_* Any attributes starting with underscore character _ are treated as hidden. They are searchable, but we will not expose them in autocomplete or search API responses. This is useful if you don't want to expose some private attributes to the world, but still want to be able to search them.
price* The price attribute should be a fully formatted string, including currency. Feel free to use formatting that is acceptable for the specific locale where the price will be displayed. Some valid values for price attribute are 1,232.60 €, kr12,341 or 8 129 zł. When we encounter a price attribute, we will do a best-effort extraction of a corresponding float value into a field called price_amount. If you are using an unusual price format or you want to have complete control over the extracted value, send the price_amount as part of the payload. When we encounter an existing _amount field, the auto-extraction will be skipped. Note that this behavior also applies to any field starting with price_ prefix, e.g., price_eur, or price_czk. For every price_-prefixed field, a corresponding _amount field will be auto-extracted, unless you send its value explicitely, e.g. price_eur_amount or price_czk_amount.
geo_* Any attributes starting with geo_ are considered as geographical location points, e.g., "geo_location" => {"lat" => 49.0448, "lon" => 18.5530}. If possible, use geo_location name as your first choice.
image_link Picked by our autocomplete.js, search.js and recco.js libraries to show an image for this record.
_margin If you send this field, it must have a float value of <0;1> (e.g., 0.42) - meaning the relative margin (e.g., margin is 42% of product price). If an object does not have this field, we treat it as if there is no margin. This information is used to prefer items with higher margin when sorting search results.
introduced_at If you send this field, it must have a date/timestamp value in ISO 8601 format - meaning the novelty of a product or a date when product will start / started to sell. If an object does not have this field, we ignore the novelty. When available, this information is used to prefer newer items when sorting search results.
category_path Reserved for filtering based on paths within categories hierarchy. Do not send the field named category_path on your own as you won't be able to filter upon them properly.

Nested categories / ancestors

Nested category with ancestors for "T-shirt" which belongs to "Apparel > Men > T-shirts".

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "T-shirt",
      },
      "nested": [
        {
          "type": "category",
          "url": "https://myshop.example/categories/apparel/men/t-shirts",
          "fields": {
            "title": "T-shirts",
            "ancestors": [{
                "title": "Apparel",
                "type": "category",
                "url": "https://myshop.example/categories/apparel"
              }, {
                "title": "Men",
                "type": "category",
                "url": "https://myshop.example/categories/apparel/men"
              }
            ]
          }
        }
      ]
    }
  ]
}

Some objects have a natural hierarchy, which you may want to capture in the data. Most often, the products belong to a category which is part of a hierarchy, e.g. a product called "White plain T-Shirt" belongs to a category "T-Shirts", which belongs to a category "Men", which belongs to a category "Apparel". Naturally, the leaf category in the hierarchy (the one at the bottom of the hierarchy), is most specific for the product, but it is useful to send data about other categories in the hierarchy as well. To differentiate between the product-specific category and other categories, higher in the category hierarchy, use a special ancestors field in the nested object.

See the example to the right for a simple case of a product belonging to a single category.

If the product directly belongs to more than one category, send multiple nested categories, each with its own category hierarchy. See the example to the right for a case of a product which belongs to two categories. The example also shows an alternative approach of declaring ancestors which allows you to store more attributes than just a title.

If you decide to utilize this way of assigning products to categories, please look at searching within full category hierarchy to make sure you get the best results when using search service.

The product "Cheddar Cheese" belongs to categories "Dairy > Cow milk" and "Wine > Snacks"

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "Cheddar cheese",
      },
      "nested": [
        {
          "type": "category",
          "url": "https://myshop.example/categories/dairy/cow-milk",
          "fields": {
            "title": "Cow milk",
            "image_link": "https://myshop.example/images/cow-milk.png",
            "ancestors": [{
              "fields": {
                 "title": "Dairy",
                 "image_link": "https://myshop.example/images/dairy.png"
              },
              "type": "category",
              "url": "https://myshop.example/categories/dairy"
            }]
          }
        },
        {
          "type": "category",
          "url": "https://myshop.example/categories/wine/snacks",
          "fields": {
            "title": "Snacks",
            "image_link": "https://myshop.example/images/snacks.png",
            "ancestors": [{
              "fields": {
                "title": "Wine",
                "image_link": "https://myshop.example/images/wine.png"
              },
              "type": "category",
              "url": "https://myshop.example/categories/wine"
            }]
          }
        }
      ]
    }
  ]
}

Nested variants

Simple "T-shirt" object with nested variants in medium and large red and small white.

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "T-shirt",
      },
      "nested": [
        {
          "type": "variant",
          "url": "https://myshop.example/products/1?variant=red-m",
          "fields": {
            "title": "Red T-shirt M",
            "color": "red",
            "size": "M"
          }
        },
        {
          "type": "variant",
          "url": "https://myshop.example/products/1?variant=red-l",
          "fields": {
            "title": "Red T-shirt L",
            "color": "red",
            "size": "L"
          }
        },
        {
          "type": "variant",
          "url": "https://myshop.example/products/1?variant=white-s",
          "fields": {
            "title": "White T-shirt S",
            "color": "white",
            "size": "S"
          }
        }
      ]
    }
  ]
}

Depending on your domain it might be sufficient to nest variants based on color and save size as just another field for the purposes of displaying search results.

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "fields": {
        "title": "T-shirt",
      },
      "nested": [
        {
          "type": "variant",
          "url": "https://myshop.example/products/1?variant=red",
          "fields": {
            "title": "Red T-shirt",
            "color": "red",
            "size": ["M", "L"]
          }
        },
        {
          "type": "variant",
          "url": "https://myshop.example/products/1?variant=white",
          "fields": {
            "title": "White T-shirt",
            "color": "white",
            "size": ["S"]
          }
        }
      ]
    }
  ]
}

Some objects come in many different variants based on size or color like a simple shirt for example, so you need to capture that in the data. However at the same time, you might not want want to display the same white medium and large shirt next to each other in the search results or when searching for "red shirt" display the red shirt image even though it is available in different colors. That is where variants come in handy.

See the example to the right for a simple case of a shirt with different variants.

The only requirement is that variant has to have a unique URL even if it is different only by a hash (e.g., /products/1#variant-red) or GET paramters (e.g., /products/1?variant=red). Note, that you can save any other key-value pairs in variant's fields that might help your users find specific variant or the object in general.

If you would like to leverage variants in your search or autocomplete, contact us at support@luigisbox.com so we can optimize your services for variants.

Files processing

If you POST an item with a type _file, we will schedule a process of downloading and processing a real file located on a supplied url. Its fields will be enriched by a content attribute to make the file searchable by its content. Please note, that at the end, the final item will get assigned a type file (no underscore).

Autocomplete results filtering

Unlike search endpoint, autocomplete, by default, does not allow filtering results by explicit key/value filters (the f[] parameter in search). If you need to filter autocomplete results, you can use autocomplete_type parameter when indexing content. Imagine that your web shop should list all products, but you also have a mobile app which must only list a subset of all products. You can implement this requirement by using autocomplete_type field. All items will be indexed with type: 'product', but the products that should be available from the mobile app will get an additional autocomplete_type: 'mobile'. When querying from the web store, you will send an autocomplete request such as type=product:6, but when querying from the mobile app, you will ask for type=mobile:6. It is also possible to index items with several autocomplete_types, such as ['mobile', 'partners']. If for some reasons autocomplete_type does not fit your needs and you need more dynamic kinds of filters, contact us to make sure you have traditional search-like filtering enabled.

Note, that the autocomplete_type is only relevant for autocomplete requests. To filter search requests, use search filter via the f[] parameter.

Autocomplete widget integration

Our Autocomplete widget expects certain fields in the object structure. If you send them, you will unlock specific features in the autocomplete dropdown.

Field name Required Description
title This is the absolute minimum that you must send.
image_link URL of the product preview image. The preview image should be small to load fast.
price Fully formatted price string, including currency.
price_old Fully formatted old price string which was discounted, including currency (Grid Layout).
itemgroup Groups items of identical type with different variants (Example: 1L milk, 5L milk, 10L milk).

Error handling

Example response for a batch with single failure

{
  "ok_count": 99,
  "errors_count": 1,
  "errors": {
    "http://example.org/products/1": {
      "type": "malformed_input",
      "reason": "incorrect object format",
      "caused_by": {
        "title": ["must be filled"]
      }
    }
  }
}

There are several failure modes:

HTTP Status Description
400 Bad Request Your request as a whole has invalid structure (e.g., missing the "objects" field) or the JSON has syntax errors. Look for more details in response body, fix the error and retry the request.
400 Bad Request Importing some of the objects failed. The response body will be JSON where you can extract the URLs of failed objects from "errors".
413 Payload Too Large You are sending batch larger than 5 megabytes. Try sending a smaller batch size, or try to compress your request body. Note: we are checking the length of request content in bytes. If payload is compressed, you can send batch up to 10 megabytes in size when decompressed. Refer to the Payload compression section for details.

Paylaod compression

The content updates api is compatible with optional Content-Encoding HTTP header, accepting gzip or deflate compression methods on request body. When in use, you can send batch up to 10 megabytes in size.

Checking your Data

If you ever want to know, which types you have pushed into our content endpoint, just open the following URL in any browser and check the facets key in JSON response.

https://live.luigisbox.com/search?tracker_id=<YOUR-TRACKER-ID>&facets=type&size=0

If you want to examine fields of your items of a specific type, it is also very easy, just use a filter:

https://live.luigisbox.com/search?tracker_id=<YOUR-TRACKER-ID>&f[]=type:<YOUR-TYPE>

Finally, you can search for virtually anything right from the location bar of your browser:

https://live.luigisbox.com/search?tracker_id=<YOUR-TRACKER-ID>&q=<YOUR-SEARCH-QUERY>

Partial content update

HTTP Request

require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.patch("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "PATCH", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "PATCH" "/v1/content" "$date")

curl -i -XPATCH --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'PATCH', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('PATCH', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['PATCH', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "fields": {
        "description": "The most comfortable socks"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "fields": {
        "price": "14.99 €"
      }
    },
    {
      "url": "https://myshop.example/contact",
      "fields": {
        "title": "Contacts"
      }
    }
  ]
}

PATCH https://live.luigisbox.com/v1/content

This endpoint requires a JSON input which specifies the objects that should be updated in Luigi's Box search index. This API accepts an array of up to 50 objects in a single request - each item in the objects array is an object with its attributes which should be updated in Luigi's Box index.

Every object requires url – it is the url of the object passed to the Content update API. Apart from that you only send what you would like to update. This is mainly useful for small real time updates of single objects or small batches of objects. Note when updating nested attribute that it replaces all previous content.

Limitations

Error handling

Example response for a batch with 48 successful updates, one object without URL and another with URL specified, but not found in the catalog.

{
  "ok_count": 48,
  "errors_count": 2,
  "errors": {
    "object #31": {
      "type": "malformed_input",
      "reason": "incorrect object format",
      "caused_by": {
        "url": ["is missing"]
      }
    },
    "http://example.org/products/99": {
      "type": "not_found",
      "reason": "URL not in catalog"
    }
  }
}

There are several failure modes:

HTTP Status Description
400 Bad Request Your request as a whole has invalid structure (e.g., missing the "objects" field) or the JSON has syntax errors. Look for more details in response body, fix the error and retry the request.
400 Bad Request Importing some of the objects failed. The response body will be JSON where you can extract the URLs of failed objects from "errors".
413 Payload Too Large You are sending more than 50 items in a single request. Try sending a smaller batch size.

Update by query

Additional way of keeping the product catalog up to date. Enables updates of item, that match search criteria. This endpoint works asynchronously, meaning that after you call it, it will start a job that will complete in time. After the first initializing call, you can check the state of the job.

HTTP Request

require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.patch("/v1/update_by_query") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "PATCH", "/v1/update_by_query", date)}"
  req.body = '{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "PATCH" "/v1/update_by_query" "$date")

curl -i -XPATCH --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/update_by_query" -d '{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'PATCH', '/v1/update_by_query', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('PATCH', "https://live.luigisbox.com/v1/update_by_query", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/update_by_query'
var timestamp = new Date().toUTCString();
var signature = ['PATCH', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "search": {
    "types": [
      "product"
    ],
    "partial": {
      "fields": {
        "color": "olive"
      }
    }
  },
  "update": {
    "fields": {
      "color": "green"
    }
  }
}

PATCH https://live.luigisbox.com/v1/update_by_query

This endpoint requires a JSON input consisting of two parts. search part consists of requirements, that item needs to fulfill to be updated. update part specifies how the product should be updated.

Search requirements for now work on principle of partial match, meaning that if product has attribute color: ['olive', 'red'] and you provide search requirement color: 'olive', the mentioned product will be a match and will be updated. Even though the field requirements are in partial object, it does not mean they will find fuzzy matches. Only partial matches on arrays of multiple values. On top of that, these requirements are also case sensitive. Meaning, that if product has category: Jeans and search requirement is category: jeans, the product won't be found.

Be aware, that updates to object attributes are not incremental. The attributes for found products in Luigi's Box index are always replaced with the attributes you send.

Required structrure of the request.

{
  "search": {
    "types": [], -> Array of strings, specifying the types of products we will include in search
    "partial": {
      "fields": {}, -> Hash of attribtues and their values, specifying the search criteria
    }
  },
  "update": {
    "fields": {} -> Hash of attribtues and their values, specifying the the attributes to be updated
  }
}

There are few technical recommendations when dealing with partial in search and fields in update part:

Special fields

There are several fields which have special behavior, specifically availability and availability_rank. Their behavior is described here

Checking state of job

If the asynchronous job was enqueued, API response will consist of url. Call GET method on this url to get the state of update job

Example response for request that enqueued async job (PATCH)

{
  "status_url": "/v1/update_by_query?job_id=1"
}
require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.get("/v1/update_by_query?job_id=1") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/v1/update_by_query", date)}"
  req.body = '{
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/v1/update_by_query" "$date")

curl -i -XGET --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/update_by_query?job_id=1" -d '{
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/v1/update_by_query', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/v1/update_by_query?job_id=1", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/update_by_query'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
}

GET /v1/update_by_query?job_id=1

Response to this GET request is the state of the job. Job can be in one of theses states:

If job is not complete yet, only status and tracker id will be present in the response.

Example response for request to get the state of async job (GET)

  {
    "tracker_id": 1111-2222,
    "status": "complete",
    "updates_count": 5,
    "failures_count": 0,
    "failures": {}
  }

If some updates failed during the job execution, these failures are also reported.

  {
    "tracker_id": 1111-2222,
    "status": "complete",
    "updates_count": 0,
    "failures_count": 2,
    "failures": {
      "/products/1" => {
        "type" => "data_schema_mismatch",
        "reason" => "failed to parse [attributes.price]",
        "caused_by" => {"type" => "number_format_exception", "reason" => "For input string: \"wrong sale price\""}
      }
    }
  }

Error handling

There are several failure modes:

HTTP Status Description
400 Bad Request Your request as a whole has invalid structure (e.g., missing the "fields" field) or the JSON has syntax errors. Look for more details in response body, fix the error and retry the request.
403 API not allowed You don't have API request allowed for your site in Luigi's Box.
405 Method not allowed Unsupported HTTP method.
413 Payload Too Large You are sending request larger than 0.5 megabytes. Try sending a smaller request. Note: we are checking the length of request content in bytes.

Content removal

require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.delete("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "DELETE", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "type": "item",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "item",
      "url": "https://myshop.example/products/2"
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "DELETE" "/v1/content" "$date")

curl -i -XDELETE --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "type": "item",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "item",
      "url": "https://myshop.example/products/2"
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'DELETE', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('DELETE', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "type": "item",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "item",
      "url": "https://myshop.example/products/2"
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['DELETE', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "type": "item",
      "url": "https://myshop.example/products/1"
    },
    {
      "type": "item",
      "url": "https://myshop.example/products/2"
    }
  ]
}

DELETE https://live.luigisbox.com/v1/content

This endpoint requires the url of the object along with its type. Those must be the same which were used to store the object through Content update API. Calling this API will remove the object from Luigi's Box search index and the object with specified url will no longer appear in search results or in autocomplete results.

Generations

In some cases, you cannot effectively determine which objects were changed on your part, often because they are managed in some external system and only loaded to your own system through a periodic batch job. While you could reimport all objects through the Content Update API, you may end up with objects that are indexed in our system but are no longer present in your system. You can solve a scenario like this with Content Generations.

Content Generations allow you to import objects associated with a generation marker and then commit that generation and remove the past generation. An example:

  1. You have objects indexed in Luigi's Box which mirror your application database at some point in the past
URL Generation Fields
example.org/1 X color: red
example.org/2 X color: black
  1. A periodic job imported data to your own system, which you now need to sync with Luigi's Box. Your application database now contains product 2 which has a new color (changed from black to yellow) and product 3, which is a new product which was not present in your database before. Product 1 was deleted by the job.
  2. You iterate through all objects in your application database and build a Content Update batch. You assign a special generation attribute to each object in batch, e.g. 'generation': 'Y'
  3. We import your objects and since we are using URLs as unique identifiers, we will find existing object for the given URL and update the object, or create a new object with that URL if it does not exist
URL Generation Fields
example.org/1 X color: red
example.org/2 Y color: yellow
example.org/3 Y color: blue
  1. At this point, your Luigi's Box index can contain objects which are no longer present in your application database (product 1 in this example)
  2. You commit the generation Y via an API call to Luigi's Box and we will delete all objects that are from a different generation than what you specified. Your Luigi's Box index is now synced with your application database.
URL Generation Fields
example.org/2 Y color: yellow
example.org/3 Y color: blue

Marking objects with generation marker

require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.post("/v1/content") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "POST", "/v1/content", date)}"
  req.body = '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "POST" "/v1/content" "$date")

curl -i -XPOST --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content" -d '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'POST', '/v1/content', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/content", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content'
var timestamp = new Date().toUTCString();
var signature = ['POST', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// Example request body

{
  "objects": [
    {
      "url": "https://myshop.example/products/1",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "blue"
      }
    },
    {
      "url": "https://myshop.example/products/2",
      "type": "item",
      "generation": "1534199032554",
      "fields": {
        "color": "black"
      }
    }
  ]
}

You use the Content Update API, but put a generation attribute inside each object top-level attributes. See the example on the right.

Note that the value of the generation marker is up to you to generate and can be any arbitrary string value. It is your responsibility to generate it and to make sure that the value is used consistently for all objects in the same generation.

We recommend that you use unix timestamp (cast to string) as the generation marker that you generate before initiating the content update process and use it for all subsequent objects.

POST https://live.luigisbox.com/v1/content

Committing a generation

require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.post("/v1/content/commit?type=item&generation=1534199032554") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "POST", "/v1/content/commit", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "POST" "/v1/content/commit" "$date")

curl -i -XPOST --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content/commit?type=item&generation=1534199032554"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'POST', '/v1/content/commit', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/content/commit?type=item&generation=1534199032554", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content/commit'
var timestamp = new Date().toUTCString();
var signature = ['POST', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

Committing a generation ensures that only objects from the specified generation remain in the index. Committing is always type specific and will commit only the generation from the specified type.

To prevent unintentional deletes, the commit API makes sure that at least one object from the committed generation is present in the index. This is done to prevent simple mistakes/typos in generation marker and to prevent you from accidentally deleting all objects of a single type.

POST https://live.luigisbox.com/v1/content/commit?type=item&generation=1534199032554

Note, that when you use nested items, you need to commit their respective types separately. E.g., when you index item with type 'product', which has nested items with type 'category' and 'brand', then you need to issue 3 separate commit calls: commit for 'product', commit for 'category' and commit for 'brand'. The nested types are using the same generation marker as their parent.

Feeds

Each of your searchable type must have a separate feed in XML or CSV format.

Product feeds

Product feed is the source of data about your products and the quality of the data in the product feed will have the biggest impact on the search quality. There are 2 aspects of a product feed: structure and contents.

Structure

We are very flexible regarding feed structure and support any valid XML or CSV file. However, to make the processing easier for us (and reduce the time to integration), please adhere to following guidelines:

We frequently encounter these common problems when dealing with XML files, if you can avoid these, it will make the process much faster:

Contents

Make sure that the feed contains all products that you have, even those that are not in stock. For product attributes, it's useful to think about four aspects of the data in the feed:

Luigi's Box product XML feed

Example of XML feed if you are not using product variants

<?xml version="1.0" encoding="UTF-8"?>
<items>
  <item>
    <title><![CDATA[Black Nike Shirt]]></title>
    <url>https://example.org/2172-black-nike-shirt</url>
    <availability>1</availability>
    <availability_rank>3</availability_rank>
    <availability_rank_text><![CDATA[In external warehouse / Ships within 5 days]]></availability_rank_text>
    <category primary="true"><![CDATA[Apparel | Men | Shirts]]></category>
    <!-- If the product is in multiple categories, feel free to add each category+hierarchy as separate tag. -->
    <!-- Make sure that the primary category is marked with primary="true" attribute -->
    <category><![CDATA[Seasons | Summer | Shirts]]></category>
    <brand><![CDATA[Nike]]></brand>
    <price>$78.9</price>
    <price_old>$81.9</price_old>
    <price_eur>65 EUR</price> <!-- Use only if you want to index prices in several currencies -->
    <price_eur_old>67.50 EUR</price_old> <!-- Use only if you want to index prices in several currencies -->
    <image_link_s>https://example.org/images/thumbs/100x100/2172.png</image_link_s>
    <image_link_m>https://example.org/images/thumbs/200x200/2172.png</image_link_m>
    <image_link_l>https://example.org/images/thumbs/600x600/2172.png</image_link_l>
    <description><![CDATA[A nice & comfortable shirt. It's ok to use <strong>html tags</strong> in description, as long as you wrap the contents inside CDATA directive.]]></description>
    <labels><![CDATA[Summer sale, Free shipping]]></labels>
    <product_code>NK8277S</product_code>
    <ean>8288881881818</ean>
    <to_cart_id>2172</to_cart_id>
    <margin>0.42</margin>
    <introduced_at>2021-07-15</introduced_at>
    <parameters>
      <param>
        <name><![CDATA[Size]]></name> <!-- avoid using dots in names, e.g. "N. Size" is not a valid name -->
        <value><![CDATA[XS, M, L, XL]]></value>
      </param>
      <param>
        <name><![CDATA[Material]]></name>
        <value><![CDATA[Cotton (80%), Polyester (20%)]]></value>
      </param>
    </parameters>
  </item>
  <!-- more products/items -->
</items>

Luigi's Box accepts data from an arbitrary data feed structure, though to make the integration process smooth and fast, we recommend that you prepare the data in the Luigi's Box feed structure. Every ecommerce store is different, has different data and requirements, so keep the above information in mind when deciding what to put into the feed.

The Luigi's Box product feed format contains a set of required "tags" (attributes), which should be applicable regardless of the kind of products that you sell. For each product, you should also supply parameters in the flexible name/value format. You can use arbitrary names with a single rule - avoid dots in the parameter name.

Attribute Description
title Product title, make sure the encode the title with entities if necessary, e.g. "Black &amp; Decker" or simply wrap the data in CDATA directive.
url Canonical URL of the product. Often a single product can have several URLs, use the canonical URL for this feed
availability Binary attribute. 1 = Product is orderable, 0 = Product is not orderable. To distinguish variation in availability, use availability_rank attribute
availability_rank Numeric, strictly within <1, 15>, which encodes the availability of the product. The higher the number, the less available the product is. If your store uses 4 availabilities: "In stock", "Ships within 7 days", "Ships within 14 days", "Out of stock", then a good encoding scheme for availability_rank would be 1, 3, 7, 15. For unavailable products (where availability: 0), the availability_rank must be 15.
availability_rank_text The exact availability text as it should appear in the product tile, e.g. "Ships within 14 days"
category Product category with full hierarchy. This exact category with hierarchy must appear in the category feed. If the product is categorized within multiple categories, simply use the <category> tag several times, for each hierarchy and mark the primary category with primary="true" attribute.
brand Product brand. This exact brand must appear in the brand feed.
price The fully formatted price as you want it to appear in the product tile, including currency. This is the price for which the product is available for purchase.
price_old If the product is in sale, then this is the original price.
price_$currency The fully formatted price as you want it to appear in the product tile, in the specific $currency. E.g., if you need to display price in USD and EUR, include the "main" price in the <price> attribute, and the price in EUR in <price_eur>. Add as many per-currency prices as you need.
price_$currency_old The same rules apply as for <price_$currency> tag. E.g., to include original price in EUR, use <price_eur_old> tag.
image_link_s Link to the tiny image used for variants previews. Best size is around 100x100px
image_link_m Link to the image suitable for autocomplete, around 200x200 px.
image_link_l Link to image suitable for product tile in search results. Best size si 600x600 px. Use the same image that you use for rendering your present search result tile if possible.
description Long-form description of the product. Feel free to use formatted HTML, but make sure it is correctly encoded, or enclosed in CDATA section.
labels Comma separated list of labels, such as "Free shipping" or "Sale".
product_code SKU code. Based on our experience, people often search for products using their codes, especially in B2B segments.
ean EAN of the product, if you have it available.
to_cart_id If you want the ability to add products to cart directly from search results page, make sure to include the internal ID of the product necessary. This may not be applicable in all cases, but include all attributes necessary to add product to cart. See Add to cart for more details.
geo_location If you include this field, it must have a geo_point format "geo_location": { "lat": 49.0448, "lon": 18.553} - meaning the location of a product. If an object does not have this field, we treat it as if there is no location. This information is used to prefer items within certain distance from user when sorting search results.
margin If you include this field, it must have a float value of <0;1> (e.g., 0.42) - meaning the relative margin (e.g., margin is 42% of product price). If an object does not have this field, we treat it as if there is no margin. This information is used to prefer items with higher margin when sorting search results.
introduced_at If you include this field, it must have a date/timestamp value in ISO 8601 format - meaning the novelty of a product or a date when product will start / started to sell. If an object does not have this field, we ignore the novelty. When available, this information is used to prefer newer items when sorting search results.
parameters Arbitrary product parameters in name/value format

Lugi's Box Search has a mode called "variants search", where we aggregate individual variants, and only show best match for each variant group. To activate this feature, we require that the individual variants are linked in the feed using item_group_id attribute and that they are listed in the feed together, i.e., they go one after another without being separated by any other unrelated product.

Example of XML feed if you are using product variants

<?xml version="1.0" encoding="UTF-8"?>
<items>
  <!-- Each item contains extra item_group_id attribute which links variants -->
  <item>
    <title>Black Nike Shirt</title>
    <url>https://example.org/2172-black-nike-shirt</url>
    <item_group_id>8272</item_group_id>
    <!-- other attributes, excluded for brevity -->
  </item>
  <item>
    <title>White Nike Shirt</title>
    <url>https://example.org/2173-white-nike-shirt</url>
    <item_group_id>8272</item_group_id>
  </item>
  <item>
    <title>Red Nike Shirt</title>
    <url>https://example.org/2174-red-nike-shirt</url>
    <item_group_id>8272</item_group_id>
  </item>
  <!-- All variants of the same product MUST follow each other, variant by variant in the feed -->
  <item>
    <title>Black Hoodie</title>
    <url>https://example.org/2175-black-hoodie</url>
    <item_group_id>8273</item_group_id>
  </item>
  <!-- more products/items -->
</items>

Your existing feeds

You may already have some data feeds ready that you use elsewhere and they may be useable for search. Below is a table of feeds that we frequently encounter and their usability.

Type Usable Comments
Heureka No Uses heureka-specific categories and usually contains only a subset of products
Google Merchant Yes Usually usable, or requires slight modifications

Category feeds

This feed lives on your site, e.g. at https://example.org/feeds/categories.xml

<?xml version="1.0" encoding="UTF-8"?>
<categories>
  <category>
    <name>Shirts</name>
    <url>https://example.org/categories/shirts</url>
  </category>
  <category>
    <name>Blazers</name>
    <url>https://example.org/categories/blazers-men</url>
    <hierarchy>Apparel | Men</hierarchy>
  </category>
  <category>
    <name>Blazers</name>
    <url>https://example.org/categories/blazers-women</url>
    <hierarchy>Apparel | Women</hierarchy>
  </category>
  <!-- more categories -->
</categories>

Category feeds are simpler than product feeds and we only require 2 attributes: title and URL.

The feed must be flat, no nesting is allowed (i.e., <category> nested in another <category> tag). If your categories are nested in a category tree, use a separate hierarchy tag to denote parent categories. Make sure that the name + hierarchy matches the categories that you use in the product feed to ensure correct pairing.

Attribute Description
nameREQUIRED Category name.
urlREQUIRED Canonical URL pointing to category listing. This is the URL where you want to take your users when they click on category suggestion in autocomplete.
hierarchyoptional Category hierarchy, where the | character serves as a category delimiter and does not include the category itself (only the path to it). We will automatically convert the hierarchy into an array that you can use to display category hierarchy in the autocomplete UI.
image_urloptional URL pointing to an image to show in the autocomplete UI. Make sure that the image is sized appropriately. We recommend that the images are not larger than 100x100 pixels.

We can also process feeds in other formats (even your custom format), contact us to discuss.

Brand feeds

This feed lives on your site, e.g. at https://example.org/feeds/brands.xml

<?xml version="1.0" encoding="UTF-8"?>
<brands>
  <brand>
    <name>NiceShirts</name>
    <url>https://example.org/brands/nice-shirts</url>
  </brand>
  <brand>
    <name>Blue</name>
    <url>https://example.org/brands/blue</url>
  </brand>
</brands>

Brand feeds are very similar to categories. Make sure that the brand name matches the brand that you use in the product feed to ensure correct pairing.

Attribute Description
nameREQUIRED Brand name.
urlREQUIRED Canonical URL pointing to brand listing. This is the URL where you want to take your users when they click on brand suggestion in autocomplete.
image_urloptional URL pointing to an image to show in the autocomplete UI. Make sure that the image is sized appropriately. We recommend that the images are not larger than 100x100 pixels.

Articles feeds

This feed lives on your site, e.g. at https://example.org/feeds/articles.xml

<?xml version="1.0" encoding="UTF-8"?>
<articles>
  <article>
    <name>Lorem ipsum title of the blog post</name>
    <annotation>Short description, perex</annotation>
    <text>Text of the article</text>
    <url>https://example.org/article/blog-post-lorem</url>
  </article>
  <article>
    <name>Lorem ipsum title of the article</name>
    <annotation>Short description, perex</annotation>
    <text>Text of the article</text>
    <url>https://example.org/brands/blog-post-ipsum</url>
  </article>
</articles>

Articles feeds are very similar to categories and brands.

Attribute Description
nameREQUIRED Article name.
urlREQUIRED Canonical URL pointing to the article. This is the URL where you want to take your users when they click on article suggestion in autocomplete.
annotationoptional Short annotation or perex of the article.
textoptional Complete text of the article.

We can also process feeds in other formats (even your custom format), contact us to discuss.

Add to cart

If you are planning to use managed Luigi's Box Search interface and want to enable "Add to cart" functionality directly from the search results page, we need an "interface" on your side to manage the actual adding to cart and UI interaction it involves.

We need either:

LBAddToCart function signature

LBAddToCart = function (productId, quantity) {
  // call cart API via XHR
  // update cart indicator
  // show confirmation dialog or notification
}
  1. A JavaScript function name and signature, that we can call when user clicks on "Add to cart" button on search results page. This function must manage the cart API call, update the cart state and UI, and handle any additional interaction, such as rendering confirmation modal dialog.
    The function can take any of the attributes present in your product feed. Usually, it will be product ID and product quantity.
    If you adhere to the function name and signature and make the function accept the to_cart_id attribute from the feed, then adding to cart will work out of the box.

  2. Specification of HTML data attributes and/or CSS classes required to trigger the cart functionality. For example <a data-product-id="123" class="js-add-to-cart">Add to cart</a>.
    In this case, we expect that you will handle the click events on the specific selector (.js-add-to-cart) and handle the cart interaction. To use this method, you must listen to click events on some parent element, because the search results will be added dynamically. In other words, do not rely on ready event to attach the listeners, because the "Add to cart" will be added later, after the ready event has fired.

  3. We are open to any other "Add to cart" interface, upon agreement.

Exporting your data

Content export

require 'faraday'
require 'faraday_middleware'
require 'json'
require 'time'
require 'openssl'
require 'base64'

def digest(key, method, endpoint, date)
  content_type = 'application/json; charset=utf-8'

  data = "#{method}\n#{content_type}\n#{date}\n#{endpoint}"

  dg = OpenSSL::Digest.new('sha256')
  Base64.strict_encode64(OpenSSL::HMAC.digest(dg, key, data)).strip
end


public_key = "<your-public-key>"
private_key = "<your-private-key>"

date = Time.now.httpdate

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.get("/v1/content_export") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.headers['Date'] = date
  req.headers['Authorization'] = "faraday #{public_key}:#{digest(private_key, "GET", "/v1/content_export", date)}"
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

digest() {
  KEY=$1
  METHOD=$2
  CONTENT_TYPE="application/json; charset=utf-8"
  ENDPOINT=$3
  DATE=$4

  DATA="$METHOD\n$CONTENT_TYPE\n$DATE\n$ENDPOINT"

  printf "$DATA" | openssl dgst -sha256 -hmac "$KEY" -binary | base64
}


public_key="<your-public-key>"
private_key="<your-private-key>"

date=$(env LC_ALL=en_US date -u '+%a, %d %b %Y %H:%M:%S GMT')
signature=$(digest "$private_key" "GET" "/v1/content_export" "$date")

curl -i -XGET --compressed\
  -H "Date: $date" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "Authorization: curl $public_key:$signature" \
  "https://live.luigisbox.com/v1/content_export"

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';

function digest($key, $method, $endpoint, $date) {
  $content_type = 'application/json; charset=utf-8';

  $data = "{$method}\n{$content_type}\n{$date}\n{$endpoint}";

  $signature = trim(base64_encode(hash_hmac('sha256', $data, $key, true)));

  return $signature;
}


$date = gmdate('D, d M Y H:i:s T');

$public_key = "<your-public-key>";
$private_key = "<your-private-key>";

$signature = digest($private_key, 'GET', '/v1/content_export', $date);

$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/v1/content_export", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
]);

echo $res->getStatusCode();
echo $res->getBody();

// This configuration and code work with the Postman tool
// https://www.getpostman.com/
//
// Start by creating the required HTTP headers in the "Headers" tab
//  - Accept-Encoding: gzip, deflate
//  - Content-Type: application/json; charset=utf-8
//  - Authorization: {{authorization}}
//  - Date: {{date}}
//
// The {{variable}} is a postman variable syntax. It will be replaced
// by values precomputed by the following pre-request script.

var privateKey = "your-secret";
var publicKey = "your-tracker-id";

var requestPath = '/v1/content_export'
var timestamp = new Date().toUTCString();
var signature = ['GET', "application/json; charset=utf-8", timestamp, requestPath].join("\n");

var encryptedSignature = CryptoJS.HmacSHA256(signature, privateKey).toString(CryptoJS.enc.Base64);

postman.setGlobalVariable("authorization", "ApiAuth " + publicKey + ":" + encryptedSignature);
postman.setGlobalVariable("date", timestamp);

// This endpoint requires no body

The above command returns JSON structured like this.

{
  "total": 14256,
  "objects": [
    {
      "url": "/item/1",
      "attributes":{
        "title": "Super product 1",
        ...
      },
      "nested": [],
      "type": "item",
      "exact": true
    },
    ...
  ],
  "links": [
    {
      "rel": "next",
      "href": "https://live.luigisbox.com/v1/content_export?cursor=23937182663"
    }
  ]
}

The content export endpoint returns all objects stored in our catalog in no particular order. It returns a list of products identified by their canonical URLs (relative ones) along with their attributes and nested fields.

The results returned by this API endpoint are paginated. To get to the next page, use the href attribute in the links section, where "rel": "next". When you receive a response which contains no link with "rel": "next", it means that there are no more pages to scroll and you have downloaded the full export.

HTTP Request

GET https://live.luigisbox.com/v1/content_export

Query Parameters

Parameter Description
size Number of results in one response / page. Optional, with a default value of 300. Is limited to 500 if a greater value is requested.
hit_fields Optional. A comma separated list of fields. Only these fields (in addition to record identifier) will be retrieved and present in results. If not provided, all fields will be present in results.

Request Headers

Consider sending request header of Accept-Encoding as well with values for supported encoding methods of your HTTP client, e.g. gzip or br, gzip, deflate for multiple supported methods. Encodings make the response from Content export endpoint considerably smaller and thus faster to transfer.

Searching & Autocomplete

Autocomplete

You can use our autocomplete endpoint to get perfect search-as-you-type functionality.

To use this feature, we need to synchronize your product database with our search index. See Importing your data for more details.

Luigi's Box Autocomplete can learn the best results ordering. In order to enable learning, you need to integrate Luigi's Box Search Analytics service with your website by following the instructions.

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Autocomplete.js library which allows you to build and autocomplete widget with minimum programming effort.

JSON API

To invoke it, use this code:

require 'faraday'
require 'faraday_middleware'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.get("/autocomplete/v2?q=harry+potter&tracker_id=1234-5678")

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XGET --compressed\
  "https://live.luigisbox.com/autocomplete/v2?q=harry+potter&tracker_id=1234-5678"\



<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/autocomplete/v2?q=harry+potter&tracker_id=1234-5678", [
  'headers' => [
    'Accept-Encoding' => 'gzip'
  ]
]);

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// This endpoint requires no body

The above command returns JSON structured like this. The exact content of attributes field depends on the content of your product catalog.

{
    "exact_match_hits_count": 6,
    "partial_match_hits_count": 0,
    "partial_match_terms": [],
    "hits": [
        {
            "url": "http://www.e-shop.com/products/123456",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "<em>Product</em> X",
                "title.untouched": "Product X",
                "availability": "true",
                "price": "5.52 EUR",
                "condition": "new"
            },
            "type": "item"
        },
        {
            "url": "http://www.e-shop.com/products/456789",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "Product Y",
                "title.untouched": "<em>Product</em> Y",
                "availability": "preorder",
                "price": "12.14 EUR",
                "condition": "new"
            },
            "type": "item"
        }
    ],
    "suggested_url": "http://www.e-shop.com/special_landing_site/HP?lb_redirected_from=harry+potter"
}

GET https://live.luigisbox.com/autocomplete/v2

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Autocomplete.js library which allows you to build and autocomplete widget with minimum programming effort.

If you choose to implement the JSON API, we recommend that you consume it on the frontend side, i.e., directly from the HTML page. This API was designed for this use case and no sensitive information is required to call it (e.g., your private key). Do not proxy calls to Luigi's Box Autocomplete API via your backend servers as this will introduce additional latency.

We also recommend that you add DNS prefetch instruction to your HTML code to avoid the DNS lookup penalty on the first autocomplete request. Add the following code anywhere to your <head> to instruct browser to do the DNS lookup in advance.

<link rel="dns-prefetch" href="//live.luigisbox.com">

Query Parameters

Parameter Description
q User input
type Comma separated list of required types and their quantity, e.g. items:6,category:3
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.
unroll_variants Specifies whether multiple variants of the same product should be unrolled to fit the requested number of items (default), or if all variants of the same product should always be rolled to a single suggestion (value "never").
user_id Optional. If supplied and is equal to user id collected into our analytics, it can drive personalization of search results. Contact us at support@luigisbox.com to get into more details.
f_type[] Filter, where type part of the parameter name is a name of a requested type. The value itself is using key:value syntax. E.g., use f_item[]=color:green to filter hits of item type which have an attribute color with a value green. You can use several filters in one request, e.g., f_item[]=color:green together with f_brand[]=promoted:true. Filtering on top of numerical and date attributes supports ranges, using pipe as a separator, e.g., f_item[]=price:5|7. This range can be left open from either side, e.g., f_item[]=price:6|. If a combination of filters for the same field is provided, they are applied with OR. E.g., filters f_item[]=color:green&f_item[]=color:blue will retrieve products, that have either green OR blue color. This feature is an alternative to autocomplete_style type of filtering and is for selected clients only. Contact us at support@luigisbox.com to make sure you are among them.
hit_fields Optional. A comma separated list of attributes and product parameters. Only these fields (in addition to some default ones) will be retrieved and present in results. If not provided, all fields will be present in results.
context Optional. See context parameter options in Search documentation for the details.
ctx[] Define popularity context, using key:value syntax e.g., ctx[]=warehouses:1. You can provide multiple key:value pairs, that are combined into one context definition. Order of key:value pairs in request is not important. However, please note that key:value pairs must match one of the contexts which are being reported into Luigi's Box Search Analytics.

Request Headers

Consider sending request header of Accept-Encoding as well with values for supported encoding methods of your HTTP client, e.g. gzip or br, gzip, deflate for multiple supported methods. Encodings make the response from the JSON API considerably smaller and thus faster to transfer.

Fixits in Autocomplete

Apart from regular results, nested under hits, autocomplete will also return a URL of a Fixit redirect if a such a rule for this query exists. Look for suggested_url in response. The entered search query must be an exact match with a query set in the Fixit rule for the suggested_url to appear in the response.

Autocomplete widget

We provide autocomplete widget which works directly with the JSON API. No programming is necessary, just include the script and CSS into your webpage and provide a simple configuration.

See standalone Autocomplete widget documentation for instructions and various configuration and styling examples.

Top items

You can use our Top items endpoint to get the list of most popular items of any type in the same output manner as with Autocomplete.

HTTP Request

require 'faraday'
require 'faraday_middleware'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.get("/v1/top_items")

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XGET --compressed\
  "https://live.luigisbox.com/v1/top_items"\



<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/v1/top_items", [
  'headers' => [
    'Accept-Encoding' => 'gzip'
  ]
]);

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// This endpoint requires no body

The above command returns JSON structured like this. The exact content of attributes field depends on the content of your product catalog.

{
    "hits": [
        {
            "url": "http://www.e-shop.com/products/123456",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "Product X",
                "availability": "true",
                "price": "5.52 EUR",
                "condition": "new"
            },
            "type": "item",
            "exact": true
        },
        {
            "url": "http://www.e-shop.com/products/456789",
            "attributes": {
                "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
                "description": "Description field from your product catalog",
                "categories": [
                    "Gadgets",
                    "Kids"
                ],
                "title": "Product Y",
                "availability": "preorder",
                "price": "12.14 EUR",
                "condition": "new"
            },
            "type": "item",
            "exact": true
        }
    ]
}

You can use the raw search endpoint and integrate it with your backend or frontend or as part of Luigi's Box Autocomplete widget (see Types option in Autocomple widget section).

GET https://live.luigisbox.com/v1/top_items

Query Parameters

Parameter Description
type Comma separated list of required types and their quantity, e.g. items:6,category:3
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.
f_type[] Filter, where type part of the parameter name is a name of a requested type. The value itself is using key:value syntax. E.g., use f_item[]=color:green to filter hits of item type which have an attribute color with a value green. You can use several filters in one request, e.g., f_item[]=color:green together with f_brand[]=promoted:true. Filtering on top of numerical and date attributes supports ranges, using pipe as a separator, e.g., f_item[]=price:5|7. This range can be left open from either side, e.g., f_item[]=price:6|. If a combination of filters for the same field is provided, they are applied with OR. E.g., filters f_item[]=color:green&f_item[]=color:blue will retrieve products, that have either green OR blue color.
ctx[] Define popularity context, using key:value syntax e.g., ctx[]=warehouses:1. You can provide multiple key:value pairs, that are combined into one context definition. Order of key:value pairs in request is not important. However, please note that key:value pairs must match one of the contexts which are being reported into Luigi's Box Search Analytics.

Request Headers

Consider sending request header of Accept-Encoding as well with values for supported encoding methods of your HTTP client, e.g. gzip or br, gzip, deflate for multiple supported methods. Encodings make the response from Top items endpoint considerably smaller and thus faster to transfer.

Search as a Service

You can use our search endpoint to get a perfect fulltext search functionality with advanced filtering options.

To use this feature, we need to synchronize your product database with our search index. See Importing your data for more details.

Luigi's Box Search as a Service can learn the best results ordering. In order to enable learning, you need to integrate Luigi's Box Search Analytics service with your website by following the instructions.

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Search.js library which allows you to build a search interface with minimum programming effort.

JSON API

You can use the raw search endpoint and integrate it with your backend or frontend.

HTTP Request

GET https://live.luigisbox.com/search

Query Parameters

Parameter Description
q User input - query. Optional, if you do not send q parameter, the API will only apply filters (f[] parameter). This is useful for generating listing pages.
qu Allows to control query understanding process. Use qu=1 or qu=0 to turn it on or off. This feature is currently off by default. Important: if you want to use this feature, you must also include user_id parameter with the value of _lb cookie from your site. Look for suggested_url in response to find out whether our system indicates that a redirect should be performed and what should be the destination (based on results of the query understanding process).
f[] Filter, using key:value syntax e.g., f[]=categories:Gadgets to filter hits according to chosen criteria. Filtering on top of numerical and date attributes supports ranges, using pipe as a separator, e.g., f[]=price:5|7. This range can be left open from either side, e.g., f[]=price:6|. If a combination of filters for the same field is provided, they are applied with OR. E.g., filters f[]=categories:jackets&f[]=categories:coats will retrieve products, that have either jackets OR coats category.
f_must[] Optional. Explicitly required filter, using key:value syntax e.g., f_must[]=categories:Gadgets to filter hits according to chosen criteria. Same rules aply here as for normal Filter in sense of possible filter values. The main difference is, that if a combination of filters for the same field is provided, they are applied with AND, not OR. E.g., filters f_must[]=categories:jackets&f_must[]=categories:windproof will retrieve only products, that have both jackets AND windproof category.
size How many hits you want the endpoint to return. Defaults to 10.
sort Allows you to specify ordering of the results, using attr:{asc|desc} syntax, e.g., sort=created_at:desc. In the case of sorting by geo field (e.g., sort=geo_location:asc), search request needs to contain also context[geo_location] representing visitors location.
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.
quicksearch_types A comma separated list of other content types (e.g., category, brand, helpdesk content), which should be (also) searched for alongside the main type (products). These will be without any facets though.
facets A comma separated list of facets you want to have included in the response. OPTIONAL - can be provided as coma separated list, where any value can be provided as facet_name:values_count, e.g. facets=category,material:5 (default values count is 30).
dynamic_facets_size Optional. If you wish our service to include additional, dynamically identified facets in the response, send the maximum number of such facets in this parameter. Defaults to 0 , i.e., no dynamically identified facets are returned. Dynamic identification of facets is based mainly on categories of retrieved items and their interesting attributes.
page Which page of the results you want the endpoint to return. Defaults to 1.
from Optional. If you prefer to use an equivalent of offset instead of page number, you can pass it as from parameter, which should be a non-negative integer. An equivalent of page=1 would be from=0.
use_fixits Optional. Allows to control use of fixit rules. Use use_fixits=1 or use_fixits=true to explicitly enable usage of fixit rules. Use other values (such as use_fixits=false) to disable fixit rules for current request. Default value is true, so fixit rules are enabled by default. Look for suggested_url in response to find out whether our system indicates that a redirect should be performed and what should be the destination (based on a matched fixit rule).
prefer[] Optional. Soft filter, using key:value syntax e.g., prefer[]=category:Gadgets to prefer hits according to chosen criteria. Use context to indicate preference of autocomplete_type as described in autocomplete results filtering.
hit_fields Optional. A comma separated list of attributes and product parameters. Only these fields (in addition to some default ones) will be retrieved and present in results. If not provided, all fields will be present in results.
remove_fields Optional. A comma separated list of attributes and product parameters. If provided, these fields will be ommited from the results. If not provided, all fields will be present in results.
context[geo_location] Optional. A coma separated list of geographical coordinates (lat, lon) representing visitors location, e.g., context[geo_location]=49.0448,18.5530. Allows to consider distance between a visitor and the items she is searching for. To be able to consider geographical context in search, catalog objects also need to contain an attribute which holds geo coordinates. By default, we assume that these are stored at geo_location.
context[geo_location_field] Optional. A definition of a custom field with geo coordinates to be used for geo search by context[geo_location]. If not defined, we assume that these are stored at geo_location field but you can override this by specifying context['geo_location_field']=my_field.
context[availability field] Optional. Allows to change or disable consideration of item availability on results ranking. Without context definition, the default availability field is considered for ranking. Supply context[availability_field]=my_custom_field parameter to override this to your custom field. This field must contain integer value (0 for unavailable items or 1 for available items). If you want to disable influence of items availability on results ranking, set this context explicitly to nil: context[availability_field]=nil.
context[boost_field] Optional. Allows to change the default field used for boosting or disable boosting on results ranking. Without context definition, the default boost field is considered for ranking. Provide context[boost_field]=my_custom_field to change this to your custom field. Make sure that your custom field contains integer values from the interval 0-3 (where higher number means higher boosting priority). If you want to disable influence of boosting on results ranking, set this context explicitly to nil: context[boost_field]=nil.
context[freshness_field] Optional. Allows to change or disable consideration of item freshness (boosting of new items) on results ranking. Without context definition, the default freshness field is considered for ranking. Provide context[freshness_field]=my_custom_field to change this to your custom field. Make sure that your custom field holds date/timestamp value in ISO 8601 format. If you want to disable influence of freshness on results ranking, set this context explicitly to nil: context[freshness_field]=nil.
user_id Optional. If supplied and is equal to user id collected into our analytics, it can drive personalization of search results. Contact us at support@luigisbox.com to get into more details.
ctx[] Define popularity context, using key:value syntax e.g., ctx[]=warehouses:1. You can provide multiple key:value pairs, that are combined into one context definition. Order of key:value pairs in request is not important. However, please note that key:value pairs must match one of the contexts which are being reported into Luigi's Box Search Analytics.

Request Headers

Consider sending request header of Accept-Encoding as well with values for supported encoding methods of your HTTP client, e.g. gzip or br, gzip, deflate for multiple supported methods. Encodings make the response from the JSON API considerably smaller and thus faster to transfer.

By default when searching, filters of same type are applied with OR and filters of different types are applied with AND. E.g., request with filters f[]=category:jackets&f[]=category:windproof will find products, that have category jackets OR category windproof OR both, and request with filters f[]=category:jackets&f[]=protection:windproof will find products, that have category jackets AND protection windproof.

If you want to combine two filters of same type in AND like fashion, use f_must[] instead of f[]. E.g., you want to find only products that have category jackets and category windproof matching query 'adidas'. So instead of using this request

GET https://live.luigisbox.com/search?tracker_id=*your_tracker_id*&f[]=type:item&f[]=category:jackets&f[]=category:windproof&query=adidas

you must use this request

GET https://live.luigisbox.com/search?tracker_id=*your_tracker_id*&f[]=type:item&f_must[]=category:jackets&f_must[]=category:windproof&query=adidas

The reason why we use this model, is that filters are automatically mapped onto facets. This way, you as a user of our API service, don't have to do think about it. You only provide filters, and we take care of the rest.

Filtering within full category hierarchy

Sometimes, when dealing with (hierarchical) categories, filtering by standalone category names might not be enough and you need to filter by whole paths in the hierarchy. You can use a special filter category_path for this purpose, while separating individual steps (categories in the hierarchy) by a double pipe ||, e.g., f[]=category_path:Women||Footwear||Sandals. As with other filters, you can use multiple category_path filters together to create OR (by repeating the f[]=...) or AND (using f_must[]) combinations.

Filtering with geographical distance

To filter results based on geographical distance from the user's current location, for example to to find result within 50km, use f[]=geo_range:|50km. This way, all results with geo location farther than 50km will be filtered out. (For this filter to work, you must have a geo field indexed within your data, and provide geo location context in search parameters.)

The pattern for value of geo range filter is lower_range|upper_range, and lower and upper range need to match the pattern of /\d+km/. You can also ommit the lower or upper range to achieve an open interval.

Tips

HTTP Response

The response to search request is a structured json. Two top-level fields are results and next_page. The resultsfield contains all information about requested results. Thenext_page` field contains link used for pagination to second page of result.

Results fields

Field Description
query Requested query (q request parameter) as a string.
corrected_query Optional. This field is returned only if Luigi's Box altered the requested query. See corrected_query.
total_hits Number of hits found for requested type.
hits A list of results for requested type. Content of each result item depends on data stored in catalog.
facets A list of facets (requested or automatically identified) calculated for matched items.
filters A list of filters used for matching results.
quicksearch_types A list of results for all requested quicksearch_types.
suggested_facet Optional. Indicates one facet with its facet values which Luigi's Box evaluated as most useful for the current situation. Can be used to provide an "assistent-like" user interface, where a user is presented with one question in each step, allowing her to efficiently narrow-down the result set.
suggested_url Optional. In case when LB algorithm recognizes the posibility to redirect the requested query (query understanding of fixit), it returns this url for redirect in this field.
offset Deprecated, please ignore.

Corrected_query

Luigi's Box search endpoint offers optional functionality that allows it to avoid no-results or low-relevance results for a given search query. If it recognizes that the requested query would end by a no-result state, it automatically augments the query to provide higher chances of finding results. There are two ways a query can be augmented, depending on the type of entered query. If a query includes a typo, such as requesting sheos is requested instead of shoes, Luigi's Box can "fix" the typo prior the actual search, in order to avoid fuzzy search with uncertain results.

In this case, the corrected_query would be a string looking like this:

<strike>sheos</strike> <b>shoes</b>

Other case might be, if there is no typo, but part of query is word that is causing no result state. For example if there is no whiskey or whiskey shoes in catalog and query would be shoes whiskey, the corrected query would be this:

shoes <strike>whiskey</strike>

The last case is a search query consisting of a code. For example, 6834a88asc. But, there is no product in catalog with this code. There is only one with 6834a77asb. Since Luigi's Box is strict with codes and does not allow fuzziness for them, the query would end in no result state. But Luigi's Box can try to get a match with corrected query, in which case it would look like this:

6834a<strike>88asc</strike>

In every case, the corrected query is a html representation of the augmented query, that can be used to inform the user on the site, that the original query was in fact altered in some way.

require 'faraday'
require 'faraday_middleware'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.get("/search?q=harry+potter&tracker_id=1234-5678")

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XGET --compressed\
  "https://live.luigisbox.com/search?q=harry+potter&tracker_id=1234-5678"\



<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('GET', "https://live.luigisbox.com/search?q=harry+potter&tracker_id=1234-5678", [
  'headers' => [
    'Accept-Encoding' => 'gzip'
  ]
]);

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// This endpoint requires no body

The above command returns JSON structured like this. The exact content of attributes field depends on the content of your product catalog.

{
  "results": {
    "total_hits": 223,
    "hits": [
      {
        "url": "http://www.e-shop.com/products/123456",
        "attributes": {
          "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
          "description": "Description field from your product catalog",
          "categories": [
            "Gadgets",
            "Kids"
          ],
          "categories_count": 2,
          "title": "<em>Product</em> X",
          "title.untouched": "Product X",
          "availability": "true",
          "price": "5.52 EUR",
          "condition": "new"
        },
        "type": "item"
      },
      {
        "url": "http://www.e-shop.com/products/456789",
        "attributes": {
          "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
          "description": "Description field from your product catalog",
          "categories": [
            "Gadgets",
            "Kids"
          ],
          "categories_count": 2,
          "title": "Product Y",
          "title.untouched": "<em>Product</em> Y",
          "availability": "preorder",
          "price": "12.14 EUR",
          "condition": "new"
        },
        "type": "item"
      }
    ],
    "facets": [
      {
        "name": "type",
        "type": "text",
        "values": [
          {
            "value": "item",
            "hits_count": 123
          },
          {
            "value": "article",
            "hits_count": 14
          }
        ]
      },
      {
        "name": "price",
        "type": "float",
        "values": [
          {
            "value": "0.0|9.0",
            "hits_count": 1
          },
          {
            "value": "9.0|18.0",
            "hits_count": 1
          }
        ]
      },
      {
        "name": "categories_count",
        "type": "float",
        "values": [
          {
            "value": "1.0|2.0",
            "hits_count": 147
          },
          {
            "value": "2.0|3.0",
            "hits_count": 71
          }
        ]
      },
      {
        "name": "created_at",
        "type": "date",
        "values": [
          {
            "value": "2017-10-23T00:00:00+00:00|2017-11-23T00:00:00+00:00",
            "hits_count": 18
          },
          {
            "value": "2017-11-23T00:00:00+00:00|2017-12-23T00:00:00+00:00",
            "hits_count": 80
          }
        ]
      }
    ],
    "offset": "20"
  },
  "next_page": "https://live.luigisbox.com/search?q=harry+potter&tracker_id=1234-5678&page=2"
}

Recommender

Recommender

You can use our recommendation endpoint to get a plethora of item recommendations from your catalog based on popularity, similarity, users' interaction history and much more.

To use this feature, we need to synchronize your product database with our search index. See Importing your data for more details.

To take advantage of personalized and other advanced recommendations, you need to integrate Luigi's Box Search Analytics service with your website by following the instructions.

We strongly recommend that you do not implement the JSON API directly, but instead use our integrated Recco.js library which allows you to build a recommendation interface with minimum programming effort.

Recommendation types

To be able to recommend items Luigi's Box Recommender requires your product details in our databases and trained recommender models. The training takes place within the Recommender itself, but the configuration has to be crafted based on your specific use case. Once everything is set up you will be given the recommender_type to use in recommendation requests described below. Please contact our support to discuss the options in detail.

JSON API

You can use the raw recommendation endpoint and integrate it with your backend or frontend.

If you would like to integrate this endpoint with your backend, consider supplying your own unique user identifiers to Luigi's Box Search Analytics script for best experience. See User identifiers for more details.

require 'faraday'
require 'faraday_middleware'
require 'json'

connection = Faraday.new(url: 'https://live.luigisbox.com') do |conn|
  conn.use FaradayMiddleware::Gzip
end

response = connection.post("/v1/recommend?tracker_id=1234-5678") do |req|
  req.headers['Content-Type'] = "application/json; charset=utf-8"
  req.body = '[
  {
    "blacklisted_item_ids": [
      "/products/789012"
    ],
    "item_ids": [
      "/products/012345",
      "/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000",
    "recommendation_context": {
      "gender": {
        "value": [
          "woman",
          "unisex"
        ],
        "operator": "or"
      },
      "price": {
        "value": [
          "|150"
        ]
      }
    },
    "settings_override": {
      "availability_field": "warehouse_2_availability"
    },
    "hit_fields": [
      "size",
      "color"
    ]
  }
]'
end

if response.success?
  puts JSON.pretty_generate(JSON.parse(response.body))
else
  puts "Error, HTTP status #{response.status}"
  puts response.body
end

#!/bin/bash

curl -i -XPOST --compressed\
  "https://live.luigisbox.com/v1/recommend?tracker_id=1234-5678"\
   -H "Content-Type: application/json; charset=utf-8"\
   -d '[
  {
    "blacklisted_item_ids": [
      "/products/789012"
    ],
    "item_ids": [
      "/products/012345",
      "/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000",
    "recommendation_context": {
      "gender": {
        "value": [
          "woman",
          "unisex"
        ],
        "operator": "or"
      },
      "price": {
        "value": [
          "|150"
        ]
      }
    },
    "settings_override": {
      "availability_field": "warehouse_2_availability"
    },
    "hit_fields": [
      "size",
      "color"
    ]
  }
]'

<?php

// Using Guzzle (http://guzzle.readthedocs.io/en/latest/overview.html#installation)
require 'GuzzleHttp/autoload.php';


$client = new GuzzleHttp\Client();
$res = $client->request('POST', "https://live.luigisbox.com/v1/recommend?tracker_id=1234-5678", [
  'headers' => [
    'Accept-Encoding' => 'gzip, deflate',
    'Content-Type' => 'application/json; charset=utf-8',
    'Date' => $date,
    'Authorization' => "guzzle {$public_key}:{$signature}",
  ],
  'body' => '[
  {
    "blacklisted_item_ids": [
      "/products/789012"
    ],
    "item_ids": [
      "/products/012345",
      "/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000",
    "recommendation_context": {
      "gender": {
        "value": [
          "woman",
          "unisex"
        ],
        "operator": "or"
      },
      "price": {
        "value": [
          "|150"
        ]
      }
    },
    "settings_override": {
      "availability_field": "warehouse_2_availability"
    },
    "hit_fields": [
      "size",
      "color"
    ]
  }
]'
]);

echo $res->getStatusCode();
echo $res->getBody();

// This endpoint requires no authentication

// Example request body

[
  {
    "blacklisted_item_ids": [
      "/products/789012"
    ],
    "item_ids": [
      "/products/012345",
      "/products/345678"
    ],
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "size": 2,
    "user_id": "6822981852855588000",
    "recommendation_context": {
      "gender": {
        "value": [
          "woman",
          "unisex"
        ],
        "operator": "or"
      },
      "price": {
        "value": [
          "|150"
        ]
      }
    },
    "settings_override": {
      "availability_field": "warehouse_2_availability"
    },
    "hit_fields": [
      "size",
      "color"
    ]
  }
]

The above command returns JSON structured like this. The exact content of attributes field depends on the time of the request and content of your product catalog.

[
  {
    "generated_at": "2020-05-05T12:44:22+00:00",
    "model_version": 1588682662,
    "recommendation_id": "a24588e9-0664-4637-91d5-165313a6eac8",
    "recommendation_type": "complementary",
    "recommender_client_identifier": "basket-sidebar",
    "recommender": "c01",
    "recommender_version": "b36705710",
    "user_id": "6822981852855588000",
    "hits": [
      {
        "url": "/products/123456",
        "attributes": {
          "image_link": "http://www.e-shop.com/assets/imgs/products/123456.jpg",
          "description": "Description field from your product catalog",
          "categories": [
            "Gadgets",
            "Kids"
          ],
          "title": "Product X",
          "availability": "true",
          "price": "5.52 EUR",
          "condition": "new"
        },
        "type": "item"
      },
      {
        "url": "/products/456789",
        "attributes": {
            "image_link": "http://www.e-shop.com/assets/imgs/products/456789.jpg",
            "description": "Description field from your product catalog",
            "categories": [
                "Gadgets",
                "Kids"
            ],
            "title": "Product Y",
            "availability": "preorder",
            "price": "12.14 EUR",
            "condition": "new"
        },
        "type": "item",
        "exact": true
      }
    ]
  }
]

HTTP Request

POST https://live.luigisbox.com/v1/recommend

Query Parameters

Parameter Description
tracker_id Identifier of your site within Luigi's Box. You can see this identifier in every URL in our app once you are logged-in.

Request Headers

Header Content
Content-Type application/json; charset=utf-8

Consider sending request header of Accept-Encoding as well with values for supported encoding methods of your HTTP client, e.g. gzip or br, gzip, deflate for multiple supported methods. Encodings make the response from the JSON API considerably smaller and thus faster to transfer.

Request Body

Request body is a JSON array of recommendation request objects. Each recommendation request object contains following attributes:

Attribute Description
recommendation_type Unique identifier of a requested recommendation type. See Recommendation types for more details.
user_id Unique user identifier. Send the value of _lb cookie from your site or supply your own value. See User identifiers for more details.
item_ids A list of items to base the recommendation on. Depending on the type of recommendation and placement it might be a list of URLs of products in a shopping cart or category, current URL of a product user is exploring, etc.
blacklisted_item_ids Optional. List of product URLs that must not be recommended, e.g., different product variants that are very similar to one in item_ids.
recommender_client_identifier Optional. Arbitrary identifier by which you distinguish between different recommendations. Use when issuing multiple recommendation requests in a single API call.
size Optional. How many recommended items you want to return. Defaults to 10.
recommendation_context Optional. Dict allowing to define request-time restrictions on results being recommended (e.g., filters used by user). It allows to define restrictions using OR operator {"gender": {"value": ["woman", "unisex"], "operator": "or"}}, AND operator {"color": {"value": ["black", "blue"], "operator": "and"}}, NOT operator {"allergens": {"value": ["gluten", "soya"], "operator": "not"}}. Use the syntax known from search, using a pipe `
settings_override Optional. Dict allowing to override selected settings in request time. The most common use case is to define custom availability field for a multi-warehouse catalog, e.g., when a user selects his location, then a particular warehouse is selected and thus an appropriate availability field should be used: {"availability_field": "warehouse_2_availability"}.
hit_fields Optional. A comma separated list of fields. Only these fields (in addition to record identifier and type) will be retrieved and present in results. If not provided, all fields will be present in results.