Search by

codeartmk / opensearch-laravel

codeart

This package integrates the Opensearch client to work seamlessly with your Laravel Eloquent Model.

Package info

github.com/codeartmk/opensearch-laravel

pkg:composer/codeartmk/opensearch-laravel

Statistics

Installs: 7 998

Dependents: 0

Suggesters: 0

Stars: 12

Open Issues: 0

v1.1.0 2026-09-17 14:05 UTC

README

Latest Version on Packagist Total Downloads

Overview

This package integrates the Opensearch client to work seamlessly with your Laravel Eloquent Model.

Requirements

Laravel PHP
10.x 8.1 – 8.3
11.x 8.2 – 8.4
12.x 8.2 – 8.5
13.x 8.3 – 8.5

Installation

To install the Laravel OpenSearch Plugin, use Composer:

composer require codeartmk/opensearch-laravel

and then export the configuration with

php artisan vendor:publish --provider="Codeart\OpensearchLaravel\OpenSearchServiceProvider" --tag="config"

Basic usage

Setting up the model

Your models will need to implement the Codeart\OpensearchLaravel\OpenSearchable interface, and include the trait Codeart\OpensearchLaravel\Traits\HasOpenSearchDocuments.

use Codeart\OpensearchLaravel\OpenSearchable;
use Codeart\OpensearchLaravel\Traits\HasOpenSearchDocuments;

class User extends Authenticatable implements OpenSearchable
{
    use HasApiTokens, HasFactory, Notifiable, HasOpenSearchDocuments;
    
    //rest of the model
}

You can override the 3 functions openSearchMapping, openSearchArray, and openSearchIndexName to customize your mapping, the information stored and the index name.

For mapping options look at OpenSearch mapping documentation.

use Codeart\OpensearchLaravel\OpenSearchable;
use Codeart\OpensearchLaravel\Traits\HasOpenSearchDocuments;

class User extends Authenticatable implements OpenSearchable
{
    use HasApiTokens, HasFactory, Notifiable, HasOpenSearchDocuments;
    
    // Sent as the index's "mappings", so return its contents directly, starting with "properties".
    public function openSearchMapping(): array
    {
        return [
            "properties" => [
                "id" => [ "type" => "integer" ],
                "first_name" => [ "type" => "text" ],
                "last_name" => [ "type" => "text" ],
                // The keyword sub-field lets you aggregate and sort on name.keyword
                "name" => [ "type" => "text", "fields" => [ "keyword" => [ "type" => "keyword" ] ] ],
                "email" => [ "type" => "keyword" ],
                //...
            ]
        ];
    }
    
    public function openSearchArray(): array
    {
        return [
            "id" => $this->id,
            "first_name" => $this->first_name,
            "last_name" => $this->last_name,
            "name" => "{$this->first_name} {$this->last_name}",
            "email" => $this->email,
            //...
        ];
    }
    
    public function openSearchIndexName(): string
    {
        return "users";        
    }
    
    //rest of the model
}

Building queries and aggregations

Once the model is ready you can start building your queries and aggregation through the opensearch method on the class:

use App\Models\User;

User::opensearch()
    ->builder()
    ->search([
        Query::make([
            BoolQuery::make([
                Must::make([
                    MatchOne::make("first_name", "John"),
                    BoolQuery::make([
                        Should::make([
                            MatchOne::make('email', 'johndoe@example.com'),
                            MatchOne::make('last_name', 'johndoe@example.com'),
                        ]),
                        'minimum_should_match' => 1
                    ])
                ]),
            ])
        ]),
        Sort::make([
            'id' => 'desc',
        ])
    ])
    ->aggregations([
        Aggregation::make(
            name: "user_names",
            aggregationType: Terms::make(field: 'name.keyword',  size: 10000),
            aggregation: Aggregation::make(
                name: 'bucket_truncate',
                aggregationType: BucketSort::make('_key')
            )
        ),
    ])
    ->get();

search() takes a Query, a Sort, or one of each, in any order. Anything else, a second Query or Sort, or an empty list throws InvalidSearchParametersException. aggregations() takes an Aggregation or a list of them and throws InvalidAggregationParametersException for an empty list, an item that isn't an Aggregation, or two aggregations with the same name at the same level. Both exceptions implement Codeart\OpensearchLaravel\Exceptions\OpenSearchException.

Query::make() takes exactly one root query (a query type or a BoolQuery); combine several conditions inside a BoolQuery. An empty list, more than one item, or anything that isn't a query throws InvalidSearchParametersException.

BoolQuery::make() takes at most one each of Must, Should, MustNot and Filter, plus the optional minimum_should_match and boost keys. Anything else throws InvalidSearchParametersException. minimum_should_match is only sent when there is a Should clause, because without one it would match nothing.

Must, Should, MustNot and Filter each take a query type, a BoolQuery, or a list of them. A list item that isn't a query throws InvalidSearchParametersException.

Sub-aggregations

The aggregation parameter accepts a single Aggregation or an array of them, so a bucket can hold several sibling sub-aggregations:

Aggregation::make(
    name: 'categories',
    aggregationType: Terms::make('category'),
    aggregation: [
        Aggregation::make('average_price', Average::make('price')),
        Aggregation::make('max_price', Maximum::make('price')),
    ]
);

Pagination, source filtering, highlighting and total hits

size() defaults to 10000. If you only need aggregations, call ->size(0) so no documents are returned alongside them. The other options are only sent when you call them.

use App\Models\User;

User::opensearch()
    ->builder()
    ->search([
        Query::make([MatchOne::make('bio', 'laravel')]),
    ])
    ->size(20)
    ->from(40) // requires size(); from + size can't exceed the index's max_result_window (10000 by default)
    ->source(['name', 'email']) // or false, a single field, or ['includes' => [...], 'excludes' => [...]]
    ->highlight(['bio', 'title' => ['fragment_size' => 50]], ['pre_tags' => ['<b>'], 'post_tags' => ['</b>']])
    ->trackTotalHits() // true, false, or a number to count up to
    ->get();

Supported Query DSL queries

Match All

https://opensearch.org/docs/latest/query-dsl/match-all/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MatchAll::make();

Full-text queries

Match

https://opensearch.org/docs/latest/query-dsl/full-text/match/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MatchOne::make('name', 'john doe');

Match Bool Prefix

https://opensearch.org/docs/latest/query-dsl/full-text/match-bool-prefix/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MatchBoolPrefix::make('title', 'the wind rises');

Match Phrase

https://opensearch.org/docs/latest/query-dsl/full-text/match-phrase/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MatchPhrase::make('title', 'the wind rises');
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MatchPhrase::make('title', 'wind rises the', slop: 3);

Match Phrase Prefix

https://opensearch.org/docs/latest/query-dsl/full-text/match-phrase-prefix/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MatchPhrasePrefix::make('title', 'the rise');

Multi Match

https://opensearch.org/docs/latest/query-dsl/full-text/multi-match/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MultiMatch::make('wind', ['title^4', 'description']);
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MultiMatch::make('wind rises', ['title', 'description'], type: 'cross_fields', operator: 'and');

Query String

https://opensearch.org/docs/latest/query-dsl/full-text/query-string/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\QueryString::make('the wind AND (rises OR rising)');
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\QueryString::make('wind rises', fields: ['title', 'description'], defaultOperator: 'AND');

Simple Query String

https://opensearch.org/docs/latest/query-dsl/full-text/simple-query-string/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\SimpleQueryString::make('"rises wind" | windy');
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\SimpleQueryString::make('wind rises', fields: ['title'], defaultOperator: 'AND');

Term-level queries

Exists

https://opensearch.org/docs/latest/query-dsl/term/exists/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Exists::make('description');

Fuzzy

https://opensearch.org/docs/latest/query-dsl/term/fuzzy/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Fuzzy::make('speaker', 'HALET');

IDs

https://opensearch.org/docs/latest/query-dsl/term/ids/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Ids::make([34229, 91296]);

Prefix

https://opensearch.org/docs/latest/query-dsl/term/prefix/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Prefix::make('speaker', 'KING H');

Range

https://opensearch.org/docs/latest/query-dsl/term/range/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Range::make('line_id', ['gte' => 10, 'lte' => 20]);

Regexp

https://opensearch.org/docs/latest/query-dsl/term/regexp/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Regexp::make('play_name', '[a-zA-Z]amlet');

Term

https://opensearch.org/docs/latest/query-dsl/term/term/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Term::make('id', 1234);

Terms

https://opensearch.org/docs/latest/query-dsl/term/terms/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Terms::make('line_id', [61809, 61810]);

Terms Set

https://opensearch.org/docs/latest/query-dsl/term/terms-set/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\TermsSet::make('classes', ['CS101', 'CS102', 'MATH101'], minimumShouldMatchField: 'min_required');
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\TermsSet::make('classes', ['CS101', 'CS102'], minimumShouldMatchScript: 'Math.min(params.num_terms, 2)');

Wildcard

https://opensearch.org/docs/latest/query-dsl/term/wildcard/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Wildcard::make('speaker', 'H*Y');

Compound queries

Boosting

https://opensearch.org/docs/latest/query-dsl/compound/boosting/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Boosting::make(positive: MatchOne::make('title', 'wind'), negative: Term::make('genre', 'horror'), negativeBoost: 0.2);

Constant Score

https://opensearch.org/docs/latest/query-dsl/compound/constant-score/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\ConstantScore::make(Term::make('genre', 'drama'), boost: 1.2);

Disjunction Max

https://opensearch.org/docs/latest/query-dsl/compound/disjunction-max/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\DisMax::make([MatchOne::make('title', 'wind'), MatchOne::make('description', 'wind')], tieBreaker: 0.7);

Function Score

https://opensearch.org/docs/latest/query-dsl/compound/function-score/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\FunctionScore::make(
    functions: [
        ['filter' => Term::make('genre', 'drama'), 'weight' => 2],
        ['field_value_factor' => ['field' => 'likes', 'modifier' => 'log1p']],
    ],
    query: MatchOne::make('title', 'wind'),
    scoreMode: 'sum',
    boostMode: 'multiply'
);

Joining queries

Nested

https://opensearch.org/docs/latest/query-dsl/joining/nested/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Nested::make('comments', Term::make('comments.author', 'ana'));
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Nested::make('comments', Term::make('comments.author', 'ana'), scoreMode: 'max', innerHits: []);

Geographic queries

Geo Bounding Box

https://opensearch.org/docs/latest/query-dsl/geo-and-xy/geo-bounding-box/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\GeoBoundingBox::make('location', topLeft: ['lat' => 42.5, 'lon' => 20.5], bottomRight: ['lat' => 41.5, 'lon' => 21.5]);

Geo Distance

https://opensearch.org/docs/latest/query-dsl/geo-and-xy/geodistance/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\GeoDistance::make('location', lat: 41.99, lon: 21.43, distance: '50km');

Specialized queries

k-NN

https://opensearch.org/docs/latest/query-dsl/specialized/k-nn/index/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Knn::make('embedding', vector: [0.12, 0.45, 0.91], k: 10);
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Knn::make('embedding', vector: [0.12, 0.45, 0.91], k: 10, filter: Term::make('genre', 'drama'));

More Like This

https://opensearch.org/docs/latest/query-dsl/specialized/more-like-this/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MoreLikeThis::make(['title', 'description'], like: 'the wind rises', minTermFreq: 1, maxQueryTerms: 12);
\Codeart\OpensearchLaravel\Search\SearchQueries\Types\MoreLikeThis::make(['title'], like: [['_index' => 'movies', '_id' => '1']]);

Script

https://opensearch.org/docs/latest/query-dsl/specialized/script/

\Codeart\OpensearchLaravel\Search\SearchQueries\Types\Script::make("doc['likes'].value > params.min", params: ['min' => 100]);

Supported Aggregations

Metric aggregations

Average

https://opensearch.org/docs/latest/aggregations/metric/average/

\Codeart\OpensearchLaravel\Aggregations\Types\Average::make('taxful_total_price');

Cardinality

https://opensearch.org/docs/latest/aggregations/metric/cardinality/

\Codeart\OpensearchLaravel\Aggregations\Types\Cardinality::make('products.product_id');

Extended Stats

https://opensearch.org/docs/latest/aggregations/metric/extended-stats/

\Codeart\OpensearchLaravel\Aggregations\Types\ExtendedStats::make('taxful_total_price');
\Codeart\OpensearchLaravel\Aggregations\Types\ExtendedStats::make('taxful_total_price', sigma: 3);

Geo Bounds

https://opensearch.org/docs/latest/aggregations/metric/geobounds/

\Codeart\OpensearchLaravel\Aggregations\Types\GeoBounds::make('geoip.location');

Geo Centroid

https://opensearch.org/docs/latest/aggregations/metric/geocentroid/

\Codeart\OpensearchLaravel\Aggregations\Types\GeoCentroid::make('geoip.location');

Maximum

https://opensearch.org/docs/latest/aggregations/metric/maximum/

\Codeart\OpensearchLaravel\Aggregations\Types\Maximum::make('taxful_total_price');

Minimum

https://opensearch.org/docs/latest/aggregations/metric/minimum/

\Codeart\OpensearchLaravel\Aggregations\Types\Minimum::make('taxful_total_price');

Percentile

https://opensearch.org/docs/latest/aggregations/metric/percentile/

\Codeart\OpensearchLaravel\Aggregations\Types\Percentile::make('taxful_total_price');
\Codeart\OpensearchLaravel\Aggregations\Types\Percentile::make('taxful_total_price', percents: [50, 95, 99]);

Percentile Ranks

https://opensearch.org/docs/latest/aggregations/metric/percentile-ranks/

\Codeart\OpensearchLaravel\Aggregations\Types\PercentileRanks::make('taxful_total_price', values: [50, 100]);

Scripted Metric

https://opensearch.org/docs/latest/aggregations/metric/scripted-metric/

\Codeart\OpensearchLaravel\Aggregations\Types\ScriptedMetric::make(
    mapScript: "state.total += doc['taxful_total_price'].value",
    combineScript: 'return state.total',
    reduceScript: 'double sum = 0; for (t in states) { sum += t } return sum',
    initScript: 'state.total = 0'
);

Stats

https://opensearch.org/docs/latest/aggregations/metric/stats/

\Codeart\OpensearchLaravel\Aggregations\Types\Stats::make('taxful_total_price');

Sum

https://opensearch.org/docs/latest/aggregations/metric/sum/

\Codeart\OpensearchLaravel\Aggregations\Types\Sum::make('taxful_total_price');

Top Hits

https://opensearch.org/docs/latest/aggregations/metric/top-hits/

\Codeart\OpensearchLaravel\Aggregations\Types\TopHits::make(size: 3);
\Codeart\OpensearchLaravel\Aggregations\Types\TopHits::make(size: 1, sort: [['order_date' => ['order' => 'desc']]], source: ['customer_full_name', 'taxful_total_price']);

Value Count

https://opensearch.org/docs/latest/aggregations/metric/value-count/

\Codeart\OpensearchLaravel\Aggregations\Types\ValueCount::make('taxful_total_price');

Weighted Average

https://opensearch.org/docs/latest/aggregations/metric/weighted-avg/

\Codeart\OpensearchLaravel\Aggregations\Types\WeightedAvg::make(valueField: 'taxful_total_price', weightField: 'total_quantity');

Bucket aggregations

Composite

https://opensearch.org/docs/latest/aggregations/bucket/composite/

// A string source is a terms source on that field. Aggregations like Histogram and DateHistogram,
// or raw source arrays, can be used too. Pass the previous response's after_key as `after` to page.
\Codeart\OpensearchLaravel\Aggregations\Types\Composite::make(
    sources: ['category' => 'category.keyword', 'month' => DateHistogram::make('order_date', 'month')],
    size: 100,
    after: ['category' => "Men's Clothing", 'month' => 1672531200000]
);

Date Histogram

https://opensearch.org/docs/latest/aggregations/bucket/date-histogram/

\Codeart\OpensearchLaravel\Aggregations\Types\DateHistogram::make('order_date', 'month');
\Codeart\OpensearchLaravel\Aggregations\Types\DateHistogram::make('order_date', '30d', isIntervalFixed: true, format: 'yyyy-MM-dd');

Date Range

https://opensearch.org/docs/latest/aggregations/bucket/date-range/

\Codeart\OpensearchLaravel\Aggregations\Types\DateRange::make('order_date', [['to' => 'now-10M/M'], ['from' => 'now-10M/M']], format: 'MM-yyyy');

Filter

https://opensearch.org/docs/latest/aggregations/bucket/filter/

\Codeart\OpensearchLaravel\Aggregations\Types\Filter::make(Term::make('currency', 'EUR'));

Filters

https://opensearch.org/docs/latest/aggregations/bucket/filters/

\Codeart\OpensearchLaravel\Aggregations\Types\Filters::make(['eur' => Term::make('currency', 'EUR'), 'usd' => Term::make('currency', 'USD')], otherBucketKey: 'other');

Geo Distance

https://opensearch.org/docs/latest/aggregations/bucket/geo-distance/

\Codeart\OpensearchLaravel\Aggregations\Types\GeoDistance::make('geoip.location', lat: 41.99, lon: 21.43, ranges: [['to' => 100], ['from' => 100, 'to' => 500]], unit: 'km');

Geohash Grid

https://opensearch.org/docs/latest/aggregations/bucket/geohash-grid/

\Codeart\OpensearchLaravel\Aggregations\Types\GeohashGrid::make('geoip.location', precision: 4);

Global

https://opensearch.org/docs/latest/aggregations/bucket/global/

\Codeart\OpensearchLaravel\Aggregations\Types\GlobalBucket::make(); // named GlobalBucket because `global` is a reserved word in PHP

Histogram

https://opensearch.org/docs/latest/aggregations/bucket/histogram/

\Codeart\OpensearchLaravel\Aggregations\Types\Histogram::make('taxful_total_price', interval: 50, minDocCount: 1);

Missing

https://opensearch.org/docs/latest/aggregations/bucket/missing/

\Codeart\OpensearchLaravel\Aggregations\Types\Missing::make('discount');

Multi Terms

https://opensearch.org/docs/latest/aggregations/bucket/multi-terms/

\Codeart\OpensearchLaravel\Aggregations\Types\MultiTerms::make(['region', 'host'], size: 10);

Nested

https://opensearch.org/docs/latest/aggregations/bucket/nested/

\Codeart\OpensearchLaravel\Aggregations\Types\Nested::make('products');

Range

https://opensearch.org/docs/latest/aggregations/bucket/range/

\Codeart\OpensearchLaravel\Aggregations\Types\Range::make('taxful_total_price', [['to' => 50], ['from' => 50, 'to' => 100], ['from' => 100]]);

Reverse Nested

https://opensearch.org/docs/latest/aggregations/bucket/reverse-nested/

\Codeart\OpensearchLaravel\Aggregations\Types\ReverseNested::make();
\Codeart\OpensearchLaravel\Aggregations\Types\ReverseNested::make('products');

Significant Terms

https://opensearch.org/docs/latest/aggregations/bucket/significant-terms/

\Codeart\OpensearchLaravel\Aggregations\Types\SignificantTerms::make('manufacturer.keyword', size: 5);

Terms

https://opensearch.org/docs/latest/aggregations/bucket/terms/

\Codeart\OpensearchLaravel\Aggregations\Types\Terms::make('company.name', 100);
\Codeart\OpensearchLaravel\Aggregations\Types\Terms::make('company.name', 100, order: ['_count' => 'asc'], minDocCount: 5);

Pipeline aggregations

Average Bucket

https://opensearch.org/docs/latest/aggregations/pipeline/avg-bucket/

\Codeart\OpensearchLaravel\Aggregations\Types\AvgBucket::make('sales_per_month>sales');

Bucket Script

https://opensearch.org/docs/latest/aggregations/pipeline/bucket-script/

\Codeart\OpensearchLaravel\Aggregations\Types\BucketScript::make(['sales' => 'total_sales', 'count' => '_count'], script: 'params.sales / params.count');

Bucket Selector

https://opensearch.org/docs/latest/aggregations/pipeline/bucket-selector/

\Codeart\OpensearchLaravel\Aggregations\Types\BucketSelector::make(['sales' => 'total_sales'], script: 'params.sales > 1000');

Bucket Sort

https://opensearch.org/docs/latest/aggregations/pipeline/bucket-sort/

\Codeart\OpensearchLaravel\Aggregations\Types\BucketSort::make('company_id');
\Codeart\OpensearchLaravel\Aggregations\Types\BucketSort::make('total_sales', order: 'desc', size: 5, from: 0);

Cumulative Sum

https://opensearch.org/docs/latest/aggregations/pipeline/cumulative-sum/

\Codeart\OpensearchLaravel\Aggregations\Types\CumulativeSum::make('sales');

Derivative

https://opensearch.org/docs/latest/aggregations/pipeline/derivative/

\Codeart\OpensearchLaravel\Aggregations\Types\Derivative::make('sales', gapPolicy: 'skip');

Maximum Bucket

https://opensearch.org/docs/latest/aggregations/pipeline/max-bucket/

\Codeart\OpensearchLaravel\Aggregations\Types\MaxBucket::make('sales_per_month>sales');

Minimum Bucket

https://opensearch.org/docs/latest/aggregations/pipeline/min-bucket/

\Codeart\OpensearchLaravel\Aggregations\Types\MinBucket::make('sales_per_month>sales');

Moving Function

https://opensearch.org/docs/latest/aggregations/pipeline/moving-function/

\Codeart\OpensearchLaravel\Aggregations\Types\MovingFunction::make('sales', window: 3, script: 'MovingFunctions.unweightedAvg(values)');

Stats Bucket

https://opensearch.org/docs/latest/aggregations/pipeline/stats-bucket/

\Codeart\OpensearchLaravel\Aggregations\Types\StatsBucket::make('sales_per_month>sales');

Sum Bucket

https://opensearch.org/docs/latest/aggregations/pipeline/sum-bucket/

\Codeart\OpensearchLaravel\Aggregations\Types\SumBucket::make('sales_per_month>sales');

If something you need is missing, see Extending the functionality.

Working with indices and documents

We offer tools to help you work with Opensearch indices and documents.

Indices

We have the methods create, exists, and delete currently.

The optional $configuration parameter in the create method allows you to customize your settings for your index.

use App\Models\User;

User::opensearch()
    ->indices()
    ->create($configuration = []);

User::opensearch()
    ->indices()
    ->delete();

User::opensearch()
    ->indices()
    ->exists();

Documents

use App\Models\User;

User::opensearch()
    ->documents()
    ->createAll();

User::opensearch()
    ->documents()
    ->create($ids);

User::opensearch()
    ->documents()
    ->createOrUpdate($id);

User::opensearch()
    ->documents()
    ->delete($id);

Lazy Loading Relationship

The methods createAll, create, and createOrUpdate all accept a function as a second parameter to allow you to lazy load your relationship when creating documents.

use App\Models\User;

User::opensearch()
    ->documents()
    ->create($ids, fn($query) => $query->with('relationship'));

The client

The OpenSearch client is built from the opensearch-laravel config by OpensearchClientFactory, which is registered as a singleton in the container. The client is built on first use and reused for the rest of the request.

If you change the connection config at runtime (for example, per tenant), drop the reused client so the next call builds one from the new config:

use Codeart\OpensearchLaravel\Factories\OpensearchClientFactory;

config(['opensearch-laravel.host' => $tenant->opensearch_host]);

app(OpensearchClientFactory::class)->forgetClient();

Testing

Because Model::opensearch() resolves OpensearchClientFactory from the container, you can swap the client in your application's tests instead of sending requests to a real cluster:

use App\Models\User;
use Codeart\OpensearchLaravel\Factories\OpensearchClientFactory;
use Mockery\MockInterface;
use OpenSearch\Client;

public function test_it_searches_users(): void
{
    $client = Mockery::mock(Client::class);
    $client->shouldReceive('search')
        ->once()
        ->andReturn(['hits' => ['total' => ['value' => 0], 'hits' => []]]);

    $this->mock(OpensearchClientFactory::class, function (MockInterface $factory) use ($client) {
        $factory->shouldReceive('createClient')->andReturn($client);
    });

    // Code that calls User::opensearch() now uses the mocked client.
}

Extending the functionality

If we've missed a search query you need or an aggregation you need, you can easily implement your own and integrate it to work our core functionality.

Search Query

Create a custom class and implement the SearchQueryType and OpenSearchQuery interfaces. If you were to implement the Span term query it would look like the following:

use Codeart\OpensearchLaravel\Interfaces\OpenSearchQuery;
use Codeart\OpensearchLaravel\Search\SearchQueries\Types\SearchQueryType;

class SpanTerm implements OpenSearchQuery, SearchQueryType
{
    public function __construct(
        private readonly string $field,
        private readonly string $value
    ) {}

    public static function make(string $field, string $value): self
    {
        return new self($field, $value);
    }

    public function toOpenSearchQuery(): array
    {
        return [
            'span_term' => [
                $this->field => $this->value
            ]
        ];
    }
}

and then just call it.

use App\Models\User;
use MyNamespace\SpanTerm;

User::opensearch()
    ->builder()
    ->search([
        Query::make([
            SpanTerm::make('title', 'wind')
        ]),
    ])
    ->get();

Aggregations

You can achieve the same for aggregations but instead of SearchQueryType you need to implement the AggregationType inteface.

use Codeart\OpensearchLaravel\Interfaces\OpenSearchQuery;
use Codeart\OpensearchLaravel\Aggregations\Types\AggregationType;

class MyCustomAggregation implements OpenSearchQuery, AggregationType
{
    //aggregation logic
}

Contact Us

License

This project is licensed under the MIT License - see the LICENSE file for details.