# 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

```bash
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](/docs/authentication)). Then fill in your `.env`:

```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:

```php
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

```php
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

```php
$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`:

```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

```bash
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:

```bash
# 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

```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.

```php
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

```blade
<x-datalumo::search />
<x-datalumo::chat />
```

Add the stack once in your layout, just before `</body>`:

```blade
@stack('datalumo-scripts')
```

Both components use the widget keys from your config, and accept a `widget` attribute when one page needs a different one:

```blade
<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:

```blade
<x-datalumo::analytics :session="$result->sessionId" />
```

See [the analytics beacon](/docs/sdk#analytics-beacon-server-rendered-results) for marking up the result links.

## Calling the API directly

Anything the package does not wrap is one facade call away:

```php
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](/docs/introduction) for the other ways to integrate
- [Widget SDK](/docs/sdk) for embedding search and chat by hand
- [Authentication](/docs/authentication) for API keys, permissions, and widget access
- [Pages](/docs/pages) for the endpoints the package uses under the hood
