lexis / lexis-php
Official PHP SDK for the Lexis search API (sync + search endpoints).
Requires
- php: ^7.4 || ^8.0
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^9.6
README
Official PHP client for the Lexis search API —
sync your catalog, query the index, done. Zero runtime dependencies beyond
ext-curl + ext-json.
Requirements
- PHP 7.4 or 8.x (all minors supported)
ext-curl,ext-json- A Lexis API key (Settings → API keys in the dashboard)
Examples below are written with positional arguments so they paste-run on every supported PHP version. On PHP 8.0+ you're free to use named arguments (
$client->search(index: 'products', query: 'x')) — the method signatures match.
Install
composer require lexis/lexis-php
Quickstart
<?php require 'vendor/autoload.php'; use Lexis\Client; // Managed cloud (default) $lexis = new Client(getenv('LEXIS_API_KEY')); // OR — self-hosted / enterprise install, point at your own dashboard URL: // $lexis = new Client(getenv('LEXIS_API_KEY'), 'https://search.my-company.internal'); // 1. Push your catalog // sync->start(indexSlug, indexName, primaryKey, source) $run = $lexis->sync->start('products', 'Products'); $run->push([ ['id' => 'sku-1', 'title' => 'Adidași Nike Air', 'price' => 349, 'brand' => 'Nike'], ['id' => 'sku-2', 'title' => 'Adidași Puma RS', 'price' => 299, 'brand' => 'Puma'], // ... up to millions of docs; SDK chunks into 1000-doc batches ]); $stats = $run->commit(); echo "Committed {$stats['documents']} docs (deleted {$stats['deleted']})\n"; // 2. Search — (index, query, limit?, offset?, filters?) $result = $lexis->search('products', 'adidasi'); foreach ($result->hits as $hit) { printf("- %s (%.2f) — %s\n", $hit->id, $hit->score, $hit->get('title'), ); }
Self-hosted / enterprise deployments
For installs on your own infrastructure, pass the dashboard URL as the second argument — that's the only change needed. Auth header, request / response shapes, retries, error codes: all identical across cloud and self-hosted.
use Lexis\Client; $lexis = new Client( getenv('LEXIS_API_KEY'), // key from *your* dashboard 'https://search.my-company.internal', // *your* base URL, no trailing slash );
Pin the URL and key in your app config (.env, Laravel config, Symfony
parameters — whatever fits) so each environment points at the right
dashboard: a staging SDK instance talks to the staging Lexis install, prod
to prod.
// .env LEXIS_API_KEY=lexis_live_... LEXIS_BASE_URL=https://search.my-company.internal // app code $lexis = new Client( getenv('LEXIS_API_KEY'), getenv('LEXIS_BASE_URL') ?: null, // null falls back to managed cloud );
Need more than just the URL (custom timeout, retries, user agent, injected
HTTP transport)? Build a Config instead — see Configuration
reference below.
Sync flow in detail
Full-replace semantics
A sync run is atomic: whatever you push between start() and commit()
becomes the entire index content. Documents that were in the index before
but aren't in this run are deleted. There's no incremental upsert — if you
want to add one product to a catalog of 100k, you still push all 100k in a
new run.
Batching
The API caps each /documents call at 1000 documents. The SDK handles this
for you: pass as many as you want to push(), they're chunked into 1000-doc
batches and POSTed sequentially. One failed chunk aborts the whole thing
with an exception — you can then call $run->abort() if you want to mark
the run cleanly (otherwise it auto-expires server-side in ~15 minutes).
// Streaming from a large catalog $run = $lexis->sync->start('products'); foreach (fetchProductsFromDb() as $page) { // $page = array of up to N docs $run->push($page); } $run->commit();
Aborting
Call abort() explicitly if your source query fails mid-sync and you don't
want to wait 15 minutes for the run to expire:
try { $run = $lexis->sync->start('products'); foreach ($source as $batch) { $run->push($batch); } $run->commit(); } catch (\Throwable $e) { $run->abort('source query failed: ' . $e->getMessage()); throw $e; }
Custom primary key
By default each document must have an id field. Override with the third
argument on the first start() call — it's locked at index creation and
ignored thereafter.
// start(indexSlug, indexName, primaryKey, source) $run = $lexis->sync->start('articles', 'Articles', 'slug');
Search
// search(index, query, limit, offset, filters) $result = $lexis->search('products', 'adidași nike', 20, 0);
Each hit carries the original document fields plus three synthetic ones —
id (the primary-key value), primaryKey, and score:
foreach ($result->hits as $hit) { $hit->id; // "sku-1" $hit->primaryKey; // "sku-1" (same; exposed for clarity) $hit->score; // 4.2 $hit->get('title'); // "Adidași Nike Air" $hit->get('price', 0); // 349 (with default if missing) $hit->document; // full associative array, clean of _ prefixes } $result->total; // total matches across all pages $result->tookMs; // server-side query time $result->expandedTerms; // ["adidas", "nike"] — stemmed/synonym-expanded $result->suggestion; // "adidași" when the engine has a did-you-mean
Error handling
All SDK exceptions extend \Lexis\Exception\LexisException. Catch that if
you want a single net, or one of the specifics for fine-grained recovery:
| Exception | HTTP | Retryable by SDK |
|---|---|---|
ValidationException |
400 | no |
AuthenticationException |
401 | no |
PlanLimitException |
402 | no |
NotFoundException |
404 | no |
ConflictException |
409 | no |
RateLimitException |
429 | yes (auto) |
ServerException |
5xx | yes (auto) |
NetworkException |
— | yes (auto) |
Retries are automatic on 429, 5xx, and transport errors — the SDK respects
Retry-After on 429 and falls back to exponential backoff (0.5s → 1s → 2s
→ …) otherwise. You only see the exception if the budget is exhausted.
use Lexis\Exception\AuthenticationException; use Lexis\Exception\LexisException; use Lexis\Exception\PlanLimitException; try { $lexis->search('products', 'shoes'); } catch (AuthenticationException $e) { // Rotate the key. } catch (PlanLimitException $e) { // Upgrade or free some headroom. } catch (LexisException $e) { // Log $e->getMessage(), $e->getStatusCode(), $e->getResponseBody(). }
Configuration reference
Config is a plain constructor — the positional order is:
apiKey, baseUrl, timeout, maxRetries, retryBaseDelay, transport, userAgent.
use Lexis\Client; use Lexis\Config; // PHP 7.4-compatible (positional): $lexis = new Client(new Config( 'lexis_live_...', // apiKey 'https://lexis.florentiu.me', // baseUrl (default — change for self-hosted) 30.0, // timeout (s) 3, // maxRetries on 429/5xx/network; 0 disables 0.5, // retryBaseDelay (s) — doubles each attempt null, // transport — null = built-in cURL 'my-app/1.0' // userAgent )); // PHP 8.0+ (named args, same thing): // $lexis = new Client(new Config( // apiKey: 'lexis_live_...', // timeout: 60.0, // maxRetries: 5, // ));
Custom HTTP transport
Inject anything implementing \Lexis\Http\Transport — useful for testing
(pass a fake) or for corporate proxies that need Guzzle/PSR-18:
$lexis = new Client(new Config( 'lexis_live_...', Config::DEFAULT_BASE_URL, 30.0, 3, 0.5, new MyGuzzleAdapter() ));
The default is \Lexis\Http\CurlTransport — pure ext-curl, no extra
dependencies.
Rate limits (server-side)
/search: 600 requests/minute per API key/sync/*: 30 write requests/minute per API key
Plan quotas (documents, indexes, monthly search calls) surface as
PlanLimitException. Check your dashboard for the current numbers.
Testing
composer install
composer test
License
MIT — see LICENSE.