Semantic search in WooCommerce with XEYE
To add semantic search to WooCommerce a small plugin is enough: it keeps your API key in wp-config.php, exposes a route in the WordPress REST API that calls XEYE and renders a search box with a shortcode. First you have to export the products to JSON, import them into XEYE and train them.
By Joan Martorell
What do I need before I start?
- An XEYE account and an API key, created on the API keys page of the console.
- Access to the files of your WordPress site to create a plugin and edit
wp-config.php. - WP-CLI to export the products. If you do not have it, you can use the WooCommerce REST API.
The code on this page follows the official WordPress and WooCommerce documentation and passes the PHP syntax check, but we have not run it on a real store. Try it first in a test environment.
Step 1: how do I export the products?
XEYE imports a JSON file with a list of objects: a required text, and optionally a description and some params. Save this file as xeye-export.php and run it with WP-CLI:
<?php
// wp eval-file xeye-export.php > products.json
$out = array();
foreach ( wc_get_products( array( 'status' => 'publish', 'limit' => -1 ) ) as $product ) {
$out[] = array(
'text' => $product->get_name(),
'description' => wp_strip_all_tags( $product->get_short_description() ?: $product->get_description() ),
'params' => array(
'id' => $product->get_id(),
'sku' => $product->get_sku(),
'price' => $product->get_price(),
'url' => $product->get_permalink(),
'image' => wp_get_attachment_url( $product->get_image_id() ),
),
);
}
echo wp_json_encode( $out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
wp eval-file xeye-export.php > products.jsonThe id stored in params is the identifier of the product in WooCommerce. The optional step at the end uses it to order the native results.
Step 2: how do I load them into XEYE?
- In the console, create a list called
productsand mark it as public. - Open it and use “Import from file” with the
products.jsonfrom the previous step. - On the Trainings tab, choose a model and launch the training.
- When it finishes, try a few searches in the playground. It is free.
Step 3: how do I call XEYE from WordPress?
First, keep the key where it is not published. Add this line to wp-config.php:
define( 'XEYE_API_KEY', 'your-api-key' );Then create the plugin. Save this file as wp-content/plugins/xeye-search/xeye-search.php and activate it in the admin:
<?php
/**
* Plugin Name: XEYE Search
* Description: Semantic product search through the XEYE API.
*/
defined( 'ABSPATH' ) || exit;
// In wp-config.php: define( 'XEYE_API_KEY', 'your-api-key' );
add_action( 'rest_api_init', function () {
register_rest_route( 'xeye/v1', '/search', array(
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => function ( WP_REST_Request $request ) {
$results = xeye_search( $request['q'], $request['limit'] );
return is_wp_error( $results )
? $results
: rest_ensure_response( array( 'results' => $results ) );
},
'args' => array(
'q' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ( $value ) {
return is_string( $value ) && strlen( trim( $value ) ) >= 2 && strlen( $value ) <= 200;
},
),
'limit' => array( 'default' => 5, 'sanitize_callback' => 'absint' ),
),
) );
} );
function xeye_search( $term, $limit = 5 ) {
if ( ! defined( 'XEYE_API_KEY' ) ) {
return new WP_Error( 'xeye_not_configured', 'Search is not configured.', array( 'status' => 500 ) );
}
// Cache each query for five minutes: repeated searches spend no credit.
$limit = max( 1, min( 20, (int) $limit ) );
$cache_key = 'xeye_' . md5( strtolower( $term ) . '|' . $limit );
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$response = wp_remote_post( 'https://search.xeye.es/api/v1/search', array(
'timeout' => 5,
'headers' => array( 'Content-Type' => 'application/json', 'X-API-Key' => XEYE_API_KEY ),
'body' => wp_json_encode( array(
'list_name' => 'products',
'search_term' => $term,
'limit' => $limit,
) ),
) );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return new WP_Error( 'xeye_unavailable', 'Search is unavailable.', array( 'status' => 502 ) );
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
$results = isset( $data['results'] ) && is_array( $data['results'] ) ? $data['results'] : array();
set_transient( $cache_key, $results, 5 * MINUTE_IN_SECONDS );
return $results;
}
The plugin registers the route /wp-json/xeye/v1/search. It validates the query, calls XEYE with the key and caches each response for five minutes, so repeated searches spend no credit.
Step 4: how do I render the search box?
With a shortcode. Add this block to the same plugin and write [xeye_search] on the page where you want the search box:
// Add to the plugin. Use it in any page with the shortcode [xeye_search].
add_shortcode( 'xeye_search', function () {
ob_start();
?>
<input type="search" id="xeye-q" placeholder="Search products" autocomplete="off">
<ul id="xeye-results"></ul>
<script>
(() => {
const ENDPOINT = <?php echo wp_json_encode( rest_url( 'xeye/v1/search' ) ); ?>;
const input = document.getElementById('xeye-q');
const list = document.getElementById('xeye-results');
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
const q = input.value.trim();
if (q.length < 2) { list.replaceChildren(); return; }
// Wait until the user stops typing: one search, not one per keystroke.
timer = setTimeout(async () => {
const response = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ q, limit: 5 }),
});
const { results = [] } = await response.json();
list.replaceChildren(...results.map((result) => {
const item = document.createElement('li');
const link = document.createElement('a');
link.href = result.params?.url || '#';
link.textContent = result.item;
item.append(link);
return item;
}));
}, 300);
});
})();
</script>
<?php
return ob_get_clean();
} );
It waits 300 milliseconds after the last keystroke before searching, and links each result to the URL stored in params. Styling is up to you.
Optional: can I use XEYE for the native product search?
Yes. This block intercepts the WooCommerce product search, asks XEYE for the results and makes WordPress show those products in the same order. If XEYE does not respond, the native search is used:
// Optional: make the native product search (?s=…&post_type=product) use XEYE.
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() || ! $query->is_search()
|| 'product' !== $query->get( 'post_type' ) ) {
return;
}
$results = xeye_search( $query->get( 's' ), 20 );
if ( is_wp_error( $results ) ) {
return; // fall back to the native search
}
$ids = array_values( array_filter( array_map( function ( $result ) {
return absint( $result['params']['id'] ?? 0 );
}, $results ) ) );
// An empty post__in would return every product.
$query->set( 'post__in', $ids ? $ids : array( 0 ) );
$query->set( 'orderby', 'post__in' ); // keep XEYE's order
$query->set( 'xeye', true );
}, 20 ); // after WooCommerce's own pre_get_posts
// Drop WordPress's LIKE '%term%' clause, which would discard semantic matches.
add_filter( 'posts_search', function ( $search, $query ) {
return $query->get( 'xeye' ) ? '' : $search;
}, 10, 2 );
- The results are limited to the 20 products that XEYE returns.
- If the customer chooses another order, such as price, WooCommerce reorders the results.
- In block themes, the product collection block may not go through the main query. Check it in yours.
How do I keep the catalogue up to date?
There is no automatic sync. When you add products, export them, import only the new ones and launch a training. Importing adds elements and does not replace the existing ones, so if you import the whole catalogue again you will get duplicates. Changes to existing products are edited in the console.
Frequently asked questions
Why not call XEYE directly from JavaScript?
- Because the API key would be visible in the browser. The REST route of the plugin acts as an intermediary: the browser talks to your WordPress site and only your server knows the key.
Is the REST route open to anyone?
- Yes, like any public search box. The five-minute cache reduces usage, and it is worth adding rate limiting on your server or your CDN and a balance alert in XEYE.
Does it work with variable products?
- The script exports one element per product, with the price WooCommerce returns for the parent product. If you want to search by variation, export one element per variation.
Do I have to disable the WooCommerce search?
- No. The shortcode adds an independent search box. The optional block is the only one that changes the native search, and it falls back to it if XEYE does not respond.
Try it with your own data
Create an account, upload a list and run your first search in about five minutes. You start with €5 of credit, no card required.
Keep reading
- Semantic search for your online shop: how to set it upHow to add search that understands synonyms, intent and typos to an online shop: the data it needs, the steps to set it up and what it does not do.
- Semantic search in Shopify with XEYEAdd a search box that understands synonyms and typos to your Shopify store: export the products, train them in XEYE and connect the theme through a small proxy.
- XEYE pricingXEYE costs €0.001 per API search and €0.30 per training, plus €0.0053 per AI description. No fees, and €5 of starting credit.