Dev.to · 4 min read

Store and search chunks in Laravel with Meilisearch and Larameili

Store and search chunks in Laravel with Meilisearch and Larameili

Many times, you have to split documents into chunks, embed them, and push them into Meilisearch. Now the chunks live in the index and only in the index. There is no reason to keep a copy in your database: nothing joins a chunk, nothing edits one by hand, and the only question you ever ask is which chunks are relevant to this query, within this filter. The awkward part is querying them from Laravel. Scout wants to mirror an Eloquent model into the index, which is backwards here. So you end up talking to the raw Meilisearch client, building filter strings by hand and mapping arrays back into something usable. Larameili is for exactly this. It gives a Meilisearch index an Active Record model, so the chunks read like Eloquent even though they never touch your database. How to install As usually, use composer, and the service provider auto-registers: composer require edulazaro/larameili It reads the same MEILISEARCH_HOST and MEILISEARCH_KEY your app already uses. The model is the index A model maps to one index. Declare the index name, the fields to filter on, and an embedder if you want hybrid search. namespace App\Meili; use EduLazaro\Larameili\Meili; /** * @property string $id * @property int $document_id * @property string $content * @property string $type */ class Chunk extends Meili { protected static string $index = 'chunks'; protected static array $searchable = ['content']; protected static array $filterable = ['document_id', 'type']; protected static array $embedders = [ 'default' => ['source' => 'openAi', 'model' => 'text-embedding-3-small'], ]; } Push the settings The $filterable and $embedders you declared live in code until you sync them to the engine. List the model in config/larameili.php and run the command, which creates the index and applies the settings. php artisan meili:sync Import the chunks in bulk An importer sends thousands of chunks. import() streams them in batches and waits for each one, so a long import never outruns the engine, and it is memory-safe over a generator. Chunk::import($documentChunks, batchSize: 500); There are also insert(), updateMany() for partial updates, and deleteWhere() to drop a document's chunks by filter. Chunk::deleteWhere("document_id = {$document->id}"); Search, keyword then hybrid The query builder compiles to Meilisearch parameters and hydrates the hits back into Chunk models. Start with a keyword search inside a filter. $hits = Chunk::query() ->where('type', 'body') ->search('cancellation policy'); Because the index has an embedder, semantic() turns it into a hybrid search: Meilisearch runs the keyword and the vector search together and fuses the rankings. 0 is keyword only, 1 is vector only. $hits = Chunk::query() ->where('type', 'body') ->semantic(0.7) ->search('how do I get my money back'); Filter and paginate Filters map to Meilisearch's syntax, and paginate() returns a Laravel LengthAwarePaginator with an exact total, so it drops straight into a Blade view. $page = Chunk::query() ->whereIn('document_id', [1, 2, 3]) ->where('type', 'body') ->paginate(20); $page->total(); // exact $page->links(); Back to the Eloquent record A chunk belongs to a Document that does live in your database. Meilisearch has no joins, so this is a resolver: it looks the Eloquent model up by the foreign key stored on the chunk. use EduLazaro\Larameili\Relations\BelongsToEloquent; class Chunk extends Meili { public function document(): BelongsToEloquent { return $this->belongsToEloquent(Document::class, foreignKey: 'document_id', ownerKey: 'id'); } } Read it as a property and it resolves lazily. Eager-load it on a search with with(), so every hit is resolved in one query instead of one per hit. $hits = Chunk::query() ->semantic(0.7) ->with('document') // one whereIn for every hit ->search('how do I get my money back'); $hits->first()->document->title; // a normal Eloquent model And that's it The chunks stay in Meilisearch, where they belong, and you query them with a model instead of a raw client: filters, hybrid search, pagination, and a link back to the records that do live in your database. Your relational schema keeps the entities that earn a table, and the search documents stop pretending to be one of them. 👉 Package on Packagist: packagist.org/packages/edulazaro/larameili 👉 Source on GitHub: github.com/edulazaro/larameili

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News