Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Berrylastic Laravel Package

imdhemy/berrylastic

Berrylastic is a Laravel/PHP package that simplifies integrating Elasticsearch into your applications, with a fluent query builder, model indexing, and configurable search support for building fast, scalable search experiences. It keeps search logic clean and idiomatic.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a v1.0.0-alpha release of an Eloquent-backed search package: models are indexed automatically, and you query them with match, multi-match, and match-phrase. Treat it as pre-stable — pin it explicitly, because Composer will not resolve an alpha by default:

composer require <vendor>/<package>:"^1.0@alpha"

Where to look first, in order:

  1. The README's quick-start — the trait name, the config key for index/connection, and the searchable-attributes array.
  2. config/<package>.php after php artisan vendor:publish — host/credentials, index prefix, and (if present) the queue setting for auto-indexing.
  3. src/ for the model trait (it holds the lifecycle hooks) and the query builder class (it holds the chainable match/multiMatch/matchPhrase methods and their argument order).

First use case: make one existing model searchable end-to-end.

use Vendor\Package\Concerns\Searchable; // name from the README

class Article extends Model
{
    use Searchable;

    public function searchableAs(): string { return 'articles'; }
    public function toSearchableArray(): array
    {
        return ['id' => $this->id, 'title' => $this->title, 'body' => $this->body];
    }
}

Then backfill and query:

Article::query()->chunkById(500, fn ($rows) => /* on-demand index each row */);

Article::search()            // exact method names per README
    ->match('title', 'laravel')
    ->get();

Confirm the index exists and the mapping is what you expect before backfilling a real table — a bad dynamic mapping means a reindex later.

Implementation Patterns

Let the trait do writes, but keep it off the request path. Auto-indexing and auto-updating are wired to created/updated/deleted. For anything user-facing, route those through a queue (ShouldQueue listeners or the package's queued option if it ships one) so a slow search cluster can't fail a form submission.

Split the two write paths deliberately. Use auto-indexing for normal CRUD; use on-demand indexing for backfills, imports, seeders, and replaying after a mapping change. Wrap backfills in chunkById + a queued job per chunk so a failure is retryable and memory stays flat.

Hide the query DSL behind an action or repository. Controllers shouldn't know about boost factors or multi-match types:

final class SearchArticles
{
    public function __invoke(string $term, int $page = 1): LengthAwarePaginator
    {
        return Article::search()
            ->multiMatch($term, ['title^3', 'body'])   // verify signature in README
            ->paginate(20, 'page', $page);
    }
}

That indirection is what makes an alpha survivable — when the fluent API shifts in beta, you edit one class.

Pick the query type by intent, not by habit.

  • match — one field, relevance-ranked; the default choice.
  • multi-match — one term across several fields; boost the fields that matter (title^3), and check which type the package defaults to (best_fields vs most_fields vs cross_fields behave very differently when the same term appears in multiple fields).
  • match-phrase — exact adjacency, so use it for titles/names/quoted user input, not for free-text search, or you'll get zero results for perfectly reasonable queries.

Denormalize what you search. The index is a read model: flatten relationships into toSearchableArray() rather than trying to join at query time. If a related model changes, you own re-indexing the parent (on-demand update from the child's observer, queued).

Test without a cluster, verify with one. Stub/disable indexing in feature tests so the suite isn't network-bound, then keep a small integration suite against a real index for the query layer. Every integration test needs a clean index and a refresh before asserting — otherwise you're testing timing, not behavior.

Per-tenant indexes (articles_tenant_{id}) are straightforward with a dynamic searchableAs(); just remember the index has to be created/mapped per tenant before first write.

Gotchas and Tips

  • Alpha means no API contract. Pin an exact version or commit, read the changelog before every bump, and assume method names, config keys, and toSearchableArray() semantics can change. Don't build a public API directly on the package's fluent builder.
  • Eloquent bulk writes never fire model events. Article::where(...)->update([...]), insert(), and raw SQL all skip updated — so auto-updating silently misses them. Any mass update needs an explicit on-demand reindex afterwards.
  • Read-after-write is not guaranteed. Search indexes are near-real-time; a document saved in this request may not be findable in the next line. Never assert "saved then searchable" in production code or in tests without an explicit refresh.
  • Mappings beat dynamic guessing. Strings get dynamically mapped as analyzed text (and often a .keyword subfield) — fine for match, but match-phrase and sorting need the analyzer and field shape to be right. Set mappings explicitly before the first index, or you'll be reindexing into a fresh index and swapping aliases.
  • Deletes and soft deletes. Confirm whether the package hard-deletes from the index on deleted; with SoftDeletes, a soft delete fires updated, not deleted — filter deleted_at in the query or exclude trashed records from toSearchableArray().
  • Debugging: get the raw query. Log the generated query body before sending it; almost every "wrong results" report is an analyzer mismatch, a wrong multi-match type, or a boost typo — none of which the fluent API makes visible. Also check index existence and cluster/index health first when results are empty rather than assuming a code bug.
  • Pagination depth. from/size pagination degrades on deep pages; use the package's cursor/search_after path if it exposes one. Cap size explicitly — an unbounded get() is a memory incident waiting to happen.
  • Extension points worth knowing now: the model trait's searchable-attribute method (override per model), any config flag toggling auto-indexing (turn it off in tests and seeders), and the query builder's underlying client — reaching past the builder for one unusual query is legitimate, but wrap it so it stays one file.
  • Observers in tests and factories. Model::factory()->create() triggers indexing. Disable it globally in the test bootstrap unless the test is specifically about indexing.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle