datalumo

Laravel Package

The Datalumo Laravel package provides a Scout-inspired integration for syncing Eloquent models to Datalumo collections. Models are automatically indexed when created, updated, or deleted — and can be searched with a fluent API.

Requirements

  • PHP 8.2 or later
  • Laravel 11, 12, or 13
  • A Datalumo account with an API token

Installation

Install via Composer:

composer require datalumo/laravel

Publish the configuration file:

php artisan vendor:publish --tag=datalumo-config

Add your API token to .env:

DATALUMO_TOKEN=your-api-token

You can find your API token in your organisation settings.

Configuration

The published config file (config/datalumo.php) includes the following options:

Option Default Description
token env('DATALUMO_TOKEN') Your API token
url https://datalumo.app API base URL
queue true Queue indexing operations
queue_connection null Queue connection name
queue_name null Queue name
chunk_size 50 Records per chunk when importing

Making models searchable

Add the Searchable trait to any Eloquent model and implement the toSearchableText() method:

use Datalumo\Laravel\Searchable;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use Searchable;

    protected string $datalumoCollectionId = 'your-collection-uuid';
    protected string $datalumoIntegrationId = 'your-integration-uuid';

    public function toSearchableText(): string
    {
        return $this->title . "\n\n" . $this->body;
    }
}

The model requires two properties:

  • $datalumoCollectionId — the collection where entries are synced to (used for indexing)
  • $datalumoIntegrationId — the integration used for search, summarise, and chat

The toSearchableText() method defines the content that gets indexed.

Customising indexed data

Override these optional methods to enrich your entries with additional data:

class Article extends Model
{
    use Searchable;

    protected string $datalumoCollectionId = 'your-collection-uuid';
    protected string $datalumoIntegrationId = 'your-integration-uuid';

    public function toSearchableText(): string
    {
        return $this->title . "\n\n" . $this->body;
    }

    public function toSearchableTitle(): ?string
    {
        return $this->title;
    }

    public function toSearchableMeta(): ?array
    {
        return [
            'author' => $this->author->name,
            'published_at' => $this->published_at->toIso8601String(),
            'tags' => $this->tags->pluck('name')->all(),
        ];
    }

    public function toSearchableSourceUrl(): ?string
    {
        return route('articles.show', $this);
    }
}

All of these methods return null by default and are optional.

Conditional indexing

Control which models get indexed by overriding shouldBeSearchable():

public function shouldBeSearchable(): bool
{
    return $this->is_published;
}

When a model returns false, it is automatically removed from Datalumo if it was previously indexed. This means toggling a model from published to draft will remove it from search results.

Custom source type and key

By default, the database table name is used as the source_type and the primary key as the identifier. You can override both:

public function searchableSourceType(): string
{
    return 'blog_posts';
}

public function getScoutKey(): mixed
{
    return $this->uuid;
}

public function getScoutKeyName(): string
{
    return 'uuid';
}

Automatic syncing

Once a model uses the Searchable trait, it automatically syncs to Datalumo on every model event:

  • Create — the model is indexed
  • Update — the model is re-indexed with new content
  • Delete — the model is removed from Datalumo
  • Soft delete — respects shouldBeSearchable() (removed if it returns false)
  • Restore — the model is re-indexed

By default, all sync operations are dispatched to the queue. Set DATALUMO_QUEUE=false in your .env for synchronous indexing during development.

Manual syncing

You can also trigger indexing manually:

// Index a single model
$article->searchable();

// Remove a single model
$article->unsearchable();

// Index a collection of models
Article::where('is_published', true)->get()->searchable();

// Remove a collection of models
Article::where('is_draft', true)->get()->unsearchable();

Searching

$articles = Article::search('how do refunds work')->get();

This performs a semantic search via your Datalumo integration and returns an Eloquent Collection of matching Article models, hydrated from your database. The integration can search across multiple collections at once.

Pagination

$articles = Article::search('refund policy')->paginate(15);

Returns a standard Laravel LengthAwarePaginator, fully compatible with Blade views and API responses.

Similarity threshold

Control how strict the matching is. A value of 0 matches everything, 1 requires near-exact matches:

$articles = Article::search('refund policy')
    ->threshold(0.4)
    ->get();

The default threshold is 0.25.

Filter by metadata

Narrow results to entries matching specific metadata values:

$articles = Article::search('refund policy')
    ->meta(['category' => 'billing'])
    ->get();

// Multiple filters (AND logic)
$articles = Article::search('refund policy')
    ->meta(['category' => 'billing', 'author' => 'John'])
    ->get();

Chaining

All builder methods are fluent and can be chained:

$articles = Article::search('return policy')
    ->threshold(0.3)
    ->meta(['category' => 'billing'])
    ->paginate(10);

Raw results

Get the raw Datalumo Entry objects without mapping to Eloquent models:

$entries = Article::search('refund policy')->raw();

foreach ($entries as $entry) {
    echo $entry->title;
    echo $entry->rawText;
    echo $entry->sourceId;
}

This is useful when you need the full entry data or don't need to hydrate models.

AI features

Summarise

Get an AI-generated summary of search results:

$summary = Article::search('explain the refund policy')->summarise();

echo $summary->summary;      // markdown summary
echo $summary->references;   // source references

Control the output format and language:

$summary = Article::search('explain the refund policy')
    ->summarise(format: 'html', locale: 'nl');

Chat

Have a conversation grounded in your collection's content:

$response = Article::search('What is your refund policy?')->chat();

echo $response->message;          // AI response
echo $response->conversationId;   // use to continue

Continue an existing conversation:

$followUp = Article::search('How long do I have?')
    ->chat($response->conversationId);

The AI generates answers based on the content in your integration's connected collections. If the answer isn't in your content, it will say so rather than making something up.

Streaming

The summarise and chat methods also have streaming variants that return text chunks as they are generated. This is useful for building real-time chat interfaces.

Stream a chat response

$stream = Article::search('What is your refund policy?')->streamChat();

foreach ($stream->text() as $chunk) {
    echo $chunk; // "Refunds", " are", " available", ...
    flush();
}

Stream a summary

$stream = Article::search('explain the refund policy')
    ->streamSummarise(format: 'html');

foreach ($stream->text() as $chunk) {
    echo $chunk;
    flush();
}

Get the full text

If you want streaming transport but still need the complete response:

$stream = Article::search('hello')->streamChat();

$fullResponse = $stream->fullText();
$conversationId = $stream->conversationId();

Continue a streamed conversation

$stream = Article::search('What is your refund policy?')->streamChat();
$text = $stream->fullText();
$conversationId = $stream->conversationId();

// Continue with the conversation ID
$followUp = Article::search('How long do I have?')
    ->streamChat($conversationId);

Laravel HTTP streaming

Combine with Laravel's response()->stream() for real-time responses:

Route::get('/chat', function () {
    $stream = Article::search(request('message'))->streamChat();

    return response()->stream(function () use ($stream) {
        foreach ($stream->text() as $chunk) {
            echo $chunk;
            ob_flush();
            flush();
        }
    }, 200, ['Content-Type' => 'text/plain']);
});

Access raw stream events

For full control over the stream, iterate over StreamEvent objects:

$stream = Article::search('hello')->streamChat();

foreach ($stream as $event) {
    if ($event->isTextDelta()) {
        echo $event->data;
    } elseif ($event->isCitation()) {
        // handle citation: $event->citation()
    }
}

Artisan commands

Import

Bulk import all existing models into Datalumo:

php artisan datalumo:import "App\Models\Article"

This processes models in chunks (default 50, configurable via datalumo.chunk_size in your config) and respects shouldBeSearchable(). Models that return false are skipped.

Flush

Remove all models of a given type from Datalumo:

php artisan datalumo:flush "App\Models\Article"

This will ask for confirmation before proceeding.

Queue configuration

By default, all indexing operations are dispatched to the queue so your application stays responsive. Configure the connection and queue name via environment variables:

DATALUMO_QUEUE=true
DATALUMO_QUEUE_CONNECTION=redis
DATALUMO_QUEUE_NAME=indexing

Set DATALUMO_QUEUE=false for synchronous indexing. This is useful during development or when you need entries to be immediately searchable.

Testing

In your application tests, mock the Engine to prevent actual API calls:

use Datalumo\Laravel\Engine;

$engine = Mockery::mock(Engine::class);
$engine->shouldReceive('update')->andReturnNull();
$engine->shouldReceive('delete')->andReturnNull();

$this->app->instance(Engine::class, $engine);

This lets you test model creation and updates without hitting the Datalumo API.

Blade components

The package includes Blade components for embedding Datalumo widgets directly in your views. No JavaScript setup needed.

Chatbot

Add a floating chatbot widget to any page:

<x-datalumo::chatbot id="your-integration-id" />

Place this before the closing </body> tag or in your layout. The chatbot appears as a floating button that opens a chat window when clicked.

Embed an inline search box with results:

<x-datalumo::search id="your-integration-id" />

To render results in a specific container, use the target attribute:

<div id="search-results"></div>

<x-datalumo::search id="your-integration-id" target="#search-results" />

Bind to your own form

Connect the search widget to an existing form instead of rendering its own:

<form id="my-search-form">
    <input type="text" id="my-search-input" placeholder="Search...">
    <button type="submit">Search</button>
</form>

<div id="results"></div>

<x-datalumo::search
    id="your-integration-id"
    form="#my-search-form"
    input="#my-search-input"
    target="#results"
/>

Search modal

Add a search modal that opens with Ctrl+K (or Cmd+K on Mac):

<x-datalumo::search-modal id="your-integration-id" />

Publishing views

To customise the component templates, publish them to your application:

php artisan vendor:publish --tag=datalumo-views

The views will be published to resources/views/vendor/datalumo/.

Further reading