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
- public key — this is a
tracker_id
assigned to your site in Luigi's Box search analytics. You can see thetracker_id
in every URL in Luigi's Box application once you are logged in. - private key — you can find your private key in the API Keys section in Luigi's Box application.
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-Encoding optional |
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
- HTTP request method - e.g.,
GET
- Content-type header - e.g.,
application/json; charset=utf-8
- Date (in HTTP-date format) - the same value you are sending in
date
header - path (without query string) - e.g.
/filters
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.
Throttling ¶
There are different limits for requests per time period in place for endpoints.
Content Updates ¶
- limit of 500 request per 1 minute and 5 concurrent requests for
tracker_id
- see more about Content Updates API
Autocomplete ¶
- limit of 800 request per 1 minute for
tracker_id
- limit of 30 request per 5 seconds for same
IP address
- see more about Autocomplete
Search ¶
- limit of 350 request per 1 minute for same
tracker_id
- limit of 30 request per 5 seconds for same
IP address
- see more about Search
If you exceed any of the limits, you'll get a 429 Too Many Requests
response for subsequent requests. Check the Retry-After
header if present to see how many seconds to wait before retrying the request.
If you find these limits insufficient 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;
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.
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 |
|
Content update |
Update product attributes |
|
Content update or Partial content update or Update by query |
Remove project from search results |
|
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_type s. 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.
- When calling the Search API, for each object found, you will get back the same JSON-encoded information that you sent via the Content update API. Consider how your search frontend should look like. If you need certain information to appear in the search results display, you should send the necessary object attributes as fields. For example, if you want to display "Christmas promotion" label with certain products, then you must send a special field, e.g. "promotion": "Christmas" with the relevant products. Then, in the search part, you will get back the object JSON containing the field and you can display a special Christmas label.
- Think about how you want your users to find your content. If your product is called "Comfortable Shoes" and you track the color attribute separately, would you like your users to find the products via "red shoes" query? If yes, then you should send a "color" field.
- Would you like to allow your users to drill down to search results and use facets to filter by attributes? If yes, you should send all fields which you want to use for faceted search.
There are few technical recommendations when dealing with fields
:
- Make sure that numeric fields are formatted as numbers, not as strings. E.g.,
"height": "70.5"
is wrong."height": 70.5
is correct. - Make sure that date fields are formatted using ISO 8601 standard with
T
used to delimit time -yyyyMMddTHHmmssZ
, e.g."active_from": "2018-07-15T13:23:50+00:00"
- It is possible to send an array of value for arbitrary field, e.g.
"color": ["red", "black", "blue"]
- Make sure that you are not using deeply nested objects, i.e., use only one level of nesting (see example).
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. |
availability_rank_text |
The exact availability text as it should appear in the product tile, e.g. "Ships within 14 days" |
_* |
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_type
s, 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 ¶
- This endpoint cannot be used to create new objects and you cannot send multiple objects identified by the same
url
twice within one request. type
of object is considered immutable by this endpoint. Requests attempting to change it will thus result in error.- All other notes, recommendations and considerations from Content update API apply here as well.
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:
- Make sure that numeric fields are formatted as numbers, not as strings. E.g.,
"height": "70.5"
is wrong."height": 70.5
is correct. - Make sure that date fields are formatted using ISO 8601 standard with
T
used to delimit time -yyyyMMddTHHmmssZ
, e.g."active_from": "2018-07-15T13:23:50+00:00"
- It is possible to send an array of value for arbitrary field, e.g.
"color": ["red", "black", "blue"]
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:
- in progress
- complete
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:
- 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 |
- 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.
- 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'
- 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 |
- 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)
- 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 prefer XML over CSV.
- If using XML, avoid using tag attributes. Instead of
<product id="123">...
use<product><id>123</id></product>
- If using XML, keep the structure as flat as possible. Nesting tags is ok, but don't use complex nesting if possible. In many cases, nesting is unavoidable and it's ok.
- Name the attributes in a way that makes sense to you, there's no prescribed naming convention.
We frequently encounter these common problems when dealing with XML files, if you can avoid these, it will make the process much faster:
- No encoding of special characters, e.g.
&
. Make sure that all special characters are encoded as entities, or escaped viaCDATA
directive.<name>Black & White</name>
is not a valid XML.<name>Black & White</name>
or<name><![CDATA[Black & White]]></name>
is valid. - Double encoding of special character using both
CDATA
directive and entities. E.g.,<name><![CDATA[Black & White]]></name>
is syntatically valid, but theCDATA
directive will prevent special handling of XML entities, and the text will be parsed verbatim, in this caseBlack & White
. Use eitherCDATA
or entities, not both,
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:
- Required attributes: We only require two attributes for each product: title and URL. They must be present in the feed for each product (the tag names do not matter) as the bare minimum.
- Visual attributes: Look at the product tile in the category listing and all the information that it shows. If you want the product tile in search results to show the same information, it must be present in the feed. This usually includes product image, product price, discounts, various labels. Keep in mind that some information is only visible under certain circumstances, so it's best to check the code that renders the product tile and make sure that all information is included in the feed.
- Behavioral attributes: These attributes are not required to render the product tile, but they are essential for some features related to the product tile. For example, if your product tile shows an "Add to cart" button, and this button will trigger an XHR request which sends the internal product ID, make sure that this internal ID is present in the feed.
- Searchable attributes: All attributes that your product should be findable by. E.g., if you want your products to be searchable by EAN, you must include product EAN code in the feed. In this regard: more is more, and the more data you provide in the feed, the better the search will be.
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>
<boost>1</boost> <!-- You can control boosting via the boost tag -->
<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 & 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 optional |
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 optional |
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 optional |
Link to the tiny image used for variants previews. Best size is around 100x100px |
image_link_m optional |
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 optional |
EAN of the product, if you have it available. |
to_cart_id optional |
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 optional |
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 optional |
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 optional |
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. |
boost optional |
You can use the boost field to manage boosting level for a particular product. Luigi's Box support 3 boosting levels, encoded as 1, 2 and 3. The higher the number, the stronger the boosting effect. Use this field judiciously to avoid interfering with the core ranking algorithm too much. You can always set up boosting in the Luigi's Box application, boosting via feed data is primarily intended for synchronization purposes where you propagate boosting rules from another system. |
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 |
---|---|
name REQUIRED |
Category name. |
url REQUIRED |
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. |
hierarchy optional |
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_url optional |
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 |
---|---|
name REQUIRED |
Brand name. |
url REQUIRED |
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_url optional |
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 |
---|---|
name REQUIRED |
Article name. |
url REQUIRED |
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. |
annotation optional |
Short annotation or perex of the article. |
text optional |
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
}
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 theto_cart_id
attribute from the feed, then adding to cart will work out of the box.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 onready
event to attach the listeners, because the "Add to cart" will be added later, after theready
event has fired.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.
- Output of the API is not sorted.
- This API is not designed for real-time consumption. If you wish to search within the catalog, use our autocomplete and search endpoints.
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"
}
],
"campaigns": [
{
"id": 9,
"target_url": "https://www.e-shop.com/harry-potter",
"banners": {
"autocomplete_list": {
"desktop_url": "https://www.e-shop.com/harry-potter-1.jpg",
"mobile_url": "https://www.e-shop.com/harry-potter-2.jpg"
}
}
},
{
"id": 12,
"target_url": "https://www.e-shop.com/rowling",
"banners": {
"autocomplete_top": {
"desktop_url": "https://www.e-shop.com/rowling-1.jpg",
"mobile_url": "https://www.e-shop.com/rowling-2.jpg"
}
}
}
],
"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. item: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. |
prefer[] | Optional. Soft filter, using key:value syntax e.g., prefer[]=category:Gadgets to prefer hits according to chosen criteria. Results matching the specified attribute and its value will be sorted higher. Note that unlike f_type , the prefer is applied to all requested types. |
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[availability_rank_field] | Optional. Allows to change or disable consideration of item availability_rank on results ranking. Without context definition, the default availability_rank field is considered for ranking. Supply context[availability_rank_field]=my_custom_field parameter to override this to your custom field. This field must contain integer value (15 for unavailable items or 1-14 for available items with descending priority (1 is most available)). If you want to disable influence of items availability_rank on results ranking, set this context explicitly to nil: context[availability_rank_field]=nil . In case of both availability_rank_field and availability_field are defined, availability_rank_field has priority. If either attribute is set to nil, availability will be disabled. |
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.
How to use filters with search ¶
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.
Filtering and allowing missing values ¶
By default, when filter is used, items that have the required attribute missing are filtered out. However, if you don't want to filter out items that have the required attribute missing, you can use special value 'value_missing' for the filter.
So for example, if you would want to get all the items that have the color
attribute set to red
OR they don't have the color attribute specified at all, you could use this combination of filters.
f[]=color:red&f[]=color:value_missing
This special filter value is allowed for numeric
, date
, boolean
and text
filters.
Tips ¶
Make sure that you are requesting only the type that you want to search in. The API will search in all types by default — you send a request with a query and we will return a mix of results from all types. Even if you are not explicitely indexing multiple types, we are always automatically indexing your users' queries (type = queries), so you will always get mixed results by default. We sometimes see that clients are requesting large numbers of results and then filter only the relevant types locally, but there is a much simpler and more efficient way to do this. Simply request search results only for the relevant type by adding a type filter:
f[]=type:item
.If you omit query and only use filters, you will get back all products matching the filter(s) sorted by Luigi's Box rank. This is useful for generating listing pages. E.g., to get products for Apparel section, issue a request with
f[]=category:Apparel
and leave out the query.
HTTP Response ¶
The response to search request is a structured json.
Two top-level fields are results
and next_page
. The results
field contains all information
about requested results. The
next_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. |
campaigns | Optional. A list of campaigns using given query. See banner campaigns |
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",
"campaigns": [
{
"id": 13,
"target_url": "https://www.e-shop.com/harry-potter",
"banners": {
"search_header": {
"desktop_url": "https://www.e-shop.com/harry-potter-1.jpg",
"mobile_url": "https://www.e-shop.com/harry-potter-2.jpg"
},
"search_footer": {
"desktop_url": "https://www.e-shop.com/harry-potter-3.jpg",
"mobile_url": "https://www.e-shop.com/harry-potter-4.jpg"
}
}
}
]
},
"next_page": "https://live.luigisbox.com/search?q=harry+potter&tracker_id=1234-5678&page=2"
}
Banner Campaigns ¶
Campaigns are a way to customize your search and autocomplete results with banners.
Every campaign has at least one search query assigned, for which its banners are displayed within or along the results.
Banners are images, which take the customer to the specified URL when clicked upon.
Banners are referenced by their URL and as of now, they are not hosted by Luigi's Box.
We support (and advertise in autocomplete/search responses) following names of banner positions:
-
Autocomplete
- autocomplete_list
- autocomplete_top
-
Search
- search_header
- search_panel
- search_list
- search_footer
If you are using our frontend libraries for autocomplete & search results rendering,
we assume that those positions are displayed as follows, with the stated dimensions of banners.
Banner Positions ¶
Autocomplete ¶
Hero and Heromobile layout support only
-
Banner within List of brands in autocomplete - autocomplete_list
-
Computer devices
*310x240px (JPG, 620x480, max 600kb)* -
Mobile devices
*420x240px (JPG, 840x480, max 600kb)*
-
Computer devices
-
Banner within TOP product in autocomplete - autocomplete_top
-
Computer devices
*240x450px (JPG, 480x900, max 600kb)* -
Mobile devices
*420x240px (JPG, 840x480, max 600kb)*
-
Computer devices
Search ¶
-
Banner within Header in search results - search_header
-
Computer devices
*1024x160px (JPG, 2048x320, max 600kb)* -
Mobile devices
*420x240px (JPG, 840x480, max 600kb)*
-
Computer devices
-
Banner within Side panel in search results - search_panel
-
Computer devices
*240x280px (JPG, 480x460, max 600kb)* -
Mobile devices
*420x240px (JPG, 840x480, max 600kb)*
-
Computer devices
-
Banner within Product in the list in search results - search_list
-
Computer devices
*340x490px (JPG, 680x980, max 600kb)* -
Mobile devices
*340x490px (JPG, 680x980, max 600kb)*
-
Computer devices
-
Banner within Footer in search results - search_footer
-
Computer devices
*1024x160px (JPG, 2048x320, max 600kb)* -
Mobile devices
*420x240px (JPG, 840x480, max 600kb)*
-
Computer devices
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": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 2,
"user_id": "6822981852855588000",
"recommendation_context": {
"gender": {
"value": [
"woman",
"unisex"
],
"operator": "or"
},
"price_amount": {
"value": [
"|150"
]
}
},
"settings_override": {
"availability_field": "warehouse_2_availability"
},
"hit_fields": [
"url",
"title"
]
}
]'
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": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 2,
"user_id": "6822981852855588000",
"recommendation_context": {
"gender": {
"value": [
"woman",
"unisex"
],
"operator": "or"
},
"price_amount": {
"value": [
"|150"
]
}
},
"settings_override": {
"availability_field": "warehouse_2_availability"
},
"hit_fields": [
"url",
"title"
]
}
]'
<?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": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 2,
"user_id": "6822981852855588000",
"recommendation_context": {
"gender": {
"value": [
"woman",
"unisex"
],
"operator": "or"
},
"price_amount": {
"value": [
"|150"
]
}
},
"settings_override": {
"availability_field": "warehouse_2_availability"
},
"hit_fields": [
"url",
"title"
]
}
]'
]);
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": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 2,
"user_id": "6822981852855588000",
"recommendation_context": {
"gender": {
"value": [
"woman",
"unisex"
],
"operator": "or"
},
"price_amount": {
"value": [
"|150"
]
}
},
"settings_override": {
"availability_field": "warehouse_2_availability"
},
"hit_fields": [
"url",
"title"
]
}
]
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. Max of 10 ids is considered, the rest is ignored. |
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, max is 50. |
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 | , to define range criteria for numerical or date attributes, e.g., {"price_amount": {"value": [4.2|]}} . Multiple restrictions can be used within recommendation_context {"gender": {"value": ["woman", "unisex"], "operator": "or"}, "price_amount": {"value": 150}} . Single value restrictions can be submitted also in a simplified format {"size": "M", "category": "Summer shoes"} . |
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. |
mark_fallback_results | Optional. Boolean allowing to mark whether the response hits were recommended by a primary strategy of by a fallback. By default disabled. |
In case of multiple recommenders used on the same page, we suggest to join all their request objects into one request body to lower overall response time and to avoid the possibility of recommending the overlapping items.
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": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
},
{
"blacklisted_item_ids": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "bestsellers",
"recommender_client_identifier": "item_detail_bestsellers",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
}
]'
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": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
},
{
"blacklisted_item_ids": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "bestsellers",
"recommender_client_identifier": "item_detail_bestsellers",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
}
]'
<?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": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
},
{
"blacklisted_item_ids": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "bestsellers",
"recommender_client_identifier": "item_detail_bestsellers",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
}
]'
]);
echo $res->getStatusCode();
echo $res->getBody();
// This endpoint requires no authentication
// Example request body
[
{
"blacklisted_item_ids": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "item_detail_alternatives",
"recommender_client_identifier": "item_detail_alternatives",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
},
{
"blacklisted_item_ids": [
],
"item_ids": [
"/products/012345"
],
"recommendation_type": "bestsellers",
"recommender_client_identifier": "item_detail_bestsellers",
"size": 4,
"user_id": "6822981852855588000",
"hit_fields": [
"url",
"title"
]
}
]