Laravel package
datalumo/laravel connects your Laravel app to Datalumo. Add a trait to a model and it stays in sync, search it from PHP and get Eloquent models back, and drop the search or chat widget into a Blade view.
If you have used Laravel Scout, this will feel familiar.
Requirements: PHP 8.2 or newer, Laravel 11 or 12.
#Install
composer require datalumo/laravel
php artisan datalumo:install
datalumo:install publishes the config file and, once your credentials are set, checks them for you. It prints the organisation it reached, the permissions on your API key, and the sources you can write to, and it warns when something is missing.
#Configure
In Datalumo, create a source of type API and an API key (see Authentication). Then fill in your .env:
DATALUMO_ORG=your-organisation-id
DATALUMO_TOKEN=your-api-key
DATALUMO_SOURCE=docs
DATALUMO_SEARCH_WIDGET={organisation-id}/{search-widget-id}
DATALUMO_CHAT_WIDGET={organisation-id}/{chat-widget-id}
DATALUMO_ORG is your organisation's id, not its name. DATALUMO_SOURCE is the slug of the source your models are pushed into.
Search and chat are separate widgets in Datalumo, so set the one (or both) you use. Their keys are safe to render in a page; your API key never leaves the server.
Which permissions your key needs:
| Permission | Needed for |
|---|---|
pages.write |
Syncing models, importing, flushing |
search |
Searching from PHP with datalumoSearch() |
pages.read |
Cleaning up orphans with datalumo:reconcile --prune |
Self-hosting Datalumo? Point DATALUMO_BASE_URL at your own install.
#Make a model searchable
Add the Searchable trait and describe what should be indexed:
use Datalumo\Laravel\Searchable;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use Searchable;
public function toDatalumoArray(): array
{
return [
'external_id' => (string) $this->getKey(),
'name' => $this->title,
'content' => $this->body,
'content_mime' => 'text/markdown',
'source_url' => route('articles.show', $this),
'meta' => ['author' => $this->author->name],
];
}
}
| Field | What it is |
|---|---|
external_id |
Your own id for the record. Defaults to the primary key, and is what makes a re-sync update instead of duplicate. |
name |
Title shown in results |
content |
The text to index |
content_mime |
text/plain, text/markdown, or text/html |
source_url |
Where the result links to |
meta |
Anything else you want stored with the page |
That is all the setup. From here on, saving a model pushes it and deleting one removes it. The work is queued by default, so requests never wait on the API. Set DATALUMO_QUEUE=false to run it inline (handy in tests and small apps).
#Only index some records
public function shouldBeSearchable(): bool
{
return $this->published_at !== null;
}
Unpublish an article and it is removed from Datalumo on the next save. Soft deleted models are removed too, and come back when restored.
#Sync by hand
$article->searchable(); // push this one
$article->unsearchable(); // remove this one
Article::where('type', 'guide')->get()->searchable();
#More than one model
Point each model at its own source, either in config/datalumo.php:
'model_sources' => [
\App\Models\Article::class => 'articles',
\App\Models\Product::class => 'products',
],
or by overriding datalumoSource() on the model. Anything not listed uses DATALUMO_SOURCE.
#Import what you already have
php artisan datalumo:import "App\Models\Article"
Rows are pushed in batches of 50. Remember that pushing is not indexing: Datalumo reads and embeds everything it received afterwards, so a large import keeps making progress on the source page in your dashboard after the command finishes.
Two more commands for keeping things tidy:
# Push only what changed since last time
php artisan datalumo:reconcile "App\Models\Article"
# See what would happen, without writing anything
php artisan datalumo:reconcile "App\Models\Article" --dry-run
# Also delete pages whose model is gone
php artisan datalumo:reconcile "App\Models\Article" --prune
# Remove every page for a model
php artisan datalumo:flush "App\Models\Article"
Reconcile remembers a fingerprint of each record, so a second run pushes almost nothing. Pass --force to push everything again. Only use --prune on a source that nothing else writes to, since it deletes anything it does not recognise.
#Search from PHP
$articles = Article::datalumoSearch('how to deploy')->get();
You get an Eloquent collection back, in Datalumo's relevance order, loaded from your own database. Everything you normally do with models still works: relationships, accessors, policies, your Blade partials.
Article::datalumoSearch('refund policy')
->where('author', 'jane') // match on meta you sent
->limit(10)
->get();
// Simple pagination
$results = Article::datalumoSearch($request->input('q'))->paginate(15);
// The raw response, with snippets and scores
$result = Article::datalumoSearch('refunds')->raw();
$result->sessionId; // pass this to the analytics component
$result->data[0]->name;
$result->data[0]->snippet;
$result->data[0]->score;
Search goes through your search widget, so the key must be set and the widget must cover the sources your models live in.
#Blade components
<x-datalumo::search />
<x-datalumo::chat />
Add the stack once in your layout, just before </body>:
@stack('datalumo-scripts')
Both components use the widget keys from your config, and accept a widget attribute when one page needs a different one:
<x-datalumo::search widget="ORG_ID/OTHER_WIDGET_ID" />
The widget's websites list must include your domain. That is what authorises it in the browser.
#Analytics for your own result pages
When you search in PHP and render the results yourself, add the analytics component with the session id from that search, so clicks show up in your Datalumo dashboard next to everything else:
<x-datalumo::analytics :session="$result->sessionId" />
See the analytics beacon for marking up the result links.
#Calling the API directly
Anything the package does not wrap is one facade call away:
use Datalumo\Laravel\Facades\Datalumo;
Datalumo::me();
Datalumo::pages('docs')->list(['status' => 'failed']);
Datalumo::widgets($widgetId)->search(['query' => 'refunds']);
#Configuration reference
| Key | Env | Default | What it does |
|---|---|---|---|
base_url |
DATALUMO_BASE_URL |
https://datalumo.app |
Where the API lives |
organisation |
DATALUMO_ORG |
Your organisation id | |
token |
DATALUMO_TOKEN |
Your API key | |
source |
DATALUMO_SOURCE |
Default source for synced models | |
search_widget |
DATALUMO_SEARCH_WIDGET |
Search widget key | |
chat_widget |
DATALUMO_CHAT_WIDGET |
Chat widget key | |
queue |
DATALUMO_QUEUE |
true |
Sync in the background |
queue_connection |
DATALUMO_QUEUE_CONNECTION |
Connection for those jobs | |
queue_name |
DATALUMO_QUEUE_NAME |
Queue for those jobs | |
chunk_size |
DATALUMO_CHUNK_SIZE |
50 |
Records per push (50 max) |
search.limit |
DATALUMO_SEARCH_LIMIT |
15 |
Default number of results |
model_sources |
[] |
Per-model source overrides |
#Troubleshooting
| Symptom | Check |
|---|---|
datalumo:install cannot verify |
DATALUMO_ORG and DATALUMO_TOKEN belong to the same organisation |
| Models save but nothing arrives | A queue worker is running, or set DATALUMO_QUEUE=false |
| Pushed but not searchable yet | Indexing runs after the push; watch progress on the source page |
| Search returns nothing | The search widget key is set, the key has the search permission, and the widget covers the model's source |
| Search returns fewer rows than expected | Hits are loaded from your database, so records deleted locally drop out |
| Widgets do not appear | @stack('datalumo-scripts') is in your layout, and your domain is on the widget's websites list |
#Related
- Introduction for the other ways to integrate
- Widget SDK for embedding search and chat by hand
- Authentication for API keys, permissions, and widget access
- Pages for the endpoints the package uses under the hood