Jonathan Bird Web Development

Migrating from Algolia to Typesense: Laravel & Vue InstantSearch Guide

Last updated: December 1, 2025

I've always loved using Algolia for its InstantSearch, ease of use and integration. My clients love the functionality such as synonyms, stop words, AI recommendations, merchandising, and the InstantSearch frontend. It's been a reliable part of my stack for years.

However, in early 2025, Algolia decided to change their pricing model. New customers who would previously have been paying around $6K annually were now looking at ~2.5x that amount. For my client who is a B2B e-commerce platform with over 15K products and 1.6M search events per month, this was a substantial increase I couldn't ignore.

Evaluating the Alternatives

This pricing change led me to evaluate all the major alternatives:

  • Meilisearch - Great option with InstantSearch adapter and Laravel support, but lacks native support for rules (promoting, pinning, or boosting documents)
  • Elasticsearch - Powerful but complex, overkill for this project's needs
  • Typesense - Clear winner with its competitive pricing and direct adapter for Algolia InstantSearch and seamless Laravel Scout integration

Typesense stood out for several reasons:

  1. Drop-in InstantSearch compatibility via the typesense-instantsearch-adapter
  2. First-class Laravel Scout support with the official Typesense driver
  3. Significant cost savings - Typesense Cloud with high availability (3 production servers) is roughly 1/3 the cost of Algolia, including server costs and bandwidth
  4. Self-hosting option - You can save even more by running it yourself, though I opted for the fully managed Typesense Cloud service

A Word of Warning: Typesense Cloud UI Limitations

Before diving in, be aware that Typesense Cloud's dashboard is not as feature-rich as Algolia's. Key limitations:

  • No import/export for Curation rules - You can't bulk import query rules (called "Curation" in Typesense) through the UI. Migration requires using their API directly.
  • Limited Curation options - The UI only supports pinning and hiding specific documents. Advanced features like query-time brand/category boosting (Algolia's optionalFilters) aren't available in the UI - you'll need to use the API with sort_by overrides or build a custom admin panel.
  • No visual query rule builder - Algolia's drag-and-drop rule builder doesn't exist in Typesense.

If your team heavily relies on Algolia's merchandising UI to manage search rules, factor in the time to either build a custom admin interface or accept that some features will require API/config management.

The Setup: By the Numbers

For context, here's what I was working with:

  • ~15,000 products in the search index
  • Average record size: ~3.5KB per document
  • Total index size: ~55MB
  • Monthly searches: ~160,000 (aggregated - as InstantSearch is search-as-you-type, the real search events are closer to 16M)
  • Bandwidth calculation: I estimated bandwidth based on average record size × expected results per query × monthly searches

Typesense Cloud's pricing is straightforward - you pay for the cluster size and bandwidth, making it easy to predict costs.

The Migration Process

1. Laravel Scout Configuration

Follow the Laravel Scout Typesense documentation for the initial setup. Install the required packages:

composer require typesense/typesense-php

Update your .env file with Typesense credentials:

SCOUT_DRIVER=typesense
TYPESENSE_API_KEY=your-admin-api-key
TYPESENSE_API_KEY_SEARCH=your-search-only-api-key
TYPESENSE_HOST=your-cluster.a1.typesense.net
TYPESENSE_PORT=443
TYPESENSE_PROTOCOL=https

2. Define Your Schema

The biggest difference from Algolia is that Typesense requires a predefined schema. While this requires more upfront work, it's not a significant hurdle - you likely already have this structure defined in your database migrations.

In config/scout.php, define your collection schema:

<?php
// ... rest of the array
'typesense' => [
'client-settings' => [
'api_key' => env('TYPESENSE_API_KEY'),
'search_api_key' => env('TYPESENSE_API_KEY_SEARCH'),
'nodes' => [
[
'host' => env('TYPESENSE_HOST'),
'port' => env('TYPESENSE_PORT', '443'),
'protocol' => env('TYPESENSE_PROTOCOL', 'https'),
],
],
],
'model-settings' => [
\App\Models\Product::class => [
'collection-schema' => [
'fields' => [
// Basic fields
[
'name' => 'id',
'type' => 'string',
],
[
'name' => 'sku',
'type' => 'string',
],
[
'name' => 'slug',
'type' => 'string',
],
[
'name' => 'title',
'type' => 'string',
],
// Optional text fields
[
'name' => 'description',
'type' => 'string',
'optional' => true,
],
[
'name' => 'meta_description',
'type' => 'string',
'optional' => true,
],
// Faceted fields for filtering
[
'name' => 'brand',
'type' => 'string',
'facet' => true,
],
[
'name' => 'categories',
'type' => 'object',
'facet' => true,
],
// Numeric fields for sorting/filtering
[
'name' => 'price',
'type' => 'float',
'facet' => true,
'sort' => true,
],
[
'name' => 'popularity',
'type' => 'int32',
'sort' => true,
],
// Boolean flags
[
'name' => 'in_stock',
'type' => 'bool',
],
[
'name' => 'is_featured',
'type' => 'bool',
],
// Arrays
[
'name' => 'tags',
'type' => 'string[]',
'facet' => true,
],
// Required: created_at as UNIX timestamp
[
'name' => 'created_at',
'type' => 'int64',
],
],
'enable_nested_fields' => true,
],
'search-parameters' => (function () {
// Define searchable fields with their relevance weights
$fields = [
'title' => 4, // Most important
'sku' => 3, // Product codes often searched directly
'brand' => 2, // Brand searches common
'description' => 1, // Least weight but still searchable
'tags' => 1,
];
return [
'query_by' => implode(',', array_keys($fields)),
'query_by_weights' => implode(',', array_values($fields)),
'sort_by' => '_text_match:desc,popularity:desc',
];
})(),
],
],
],

Pro tip: Define your searchable fields and their weights together in an associative array. This keeps them in sync automatically - no more counting comma-separated values to ensure they match!

3. Update Your Model's toSearchableArray

Your model's toSearchableArray() method defines what gets indexed. Make sure it matches your schema:

<?php
// app/Models/Product.php
public function toSearchableArray(): array
{
return [
// Required: ID must be cast to string
'id' => (string) $this->id,
// Required: created_at must be a UNIX timestamp
'created_at' => $this->created_at->timestamp,
// Basic fields
'sku' => $this->sku,
'slug' => $this->slug,
'title' => $this->title,
'description' => $this->description ?? '',
'meta_description' => $this->meta_description ?? '',
'brand' => $this->brand?->name ?? '',
// Numeric fields
'price' => (float) $this->price,
'popularity' => (int) $this->sales_count,
// Boolean fields
'in_stock' => $this->stock_quantity > 0,
'is_featured' => (bool) $this->is_featured,
// Array fields
'tags' => $this->tags->pluck('name')->toArray(),
// Nested object for hierarchical categories (for InstantSearch's hierarchical menu)
// Build dynamically based on actual depth - only include levels that exist
'categories' => $this->buildCategoryHierarchy(),
// Pre-computed URLs for display
'image_url' => $this->getPublicImageUrl(),
'product_url' => route('products.show', $this->slug),
];
}
/**
* Build hierarchical category structure for InstantSearch.
*
* Format required by ais-hierarchical-menu widget:
* "lvl0": ["Books"],
* "lvl1": ["Books > Science Fiction"],
* "lvl2": ["Books > Science Fiction > Time Travel"]
*/
private function buildCategoryHierarchy(): array
{
$category = $this->category;
if (!$category) {
return [];
}
// Build array of category names from root to leaf
$ancestors = [];
$current = $category;
while ($current) {
array_unshift($ancestors, $current->name);
$current = $current->parent;
}
// Build hierarchical levels
$hierarchy = [];
$path = [];
foreach ($ancestors as $index => $name) {
$path[] = $name;
$hierarchy['lvl' . $index][] = implode(' > ', $path);
}
return $hierarchy;
}

4. Set Up Typesense Cloud

  1. Create a cluster at cloud.typesense.org
  2. Choose your region and cluster size based on your data volume
  3. Note your API keys:
    • Admin API Key: For indexing operations (keep server-side only)
    • Search-only API Key: For frontend search requests (safe to expose)

5. Import Your Data

Flush and re-import via Laravel Scout:

php artisan scout:flush "App\Models\Product"
php artisan scout:import "App\Models\Product"

For large datasets, you may want to chunk the import:

php artisan scout:import "App\Models\Product" --chunk=500

6. Update Your Vue InstantSearch Frontend

Install the Typesense InstantSearch adapter:

npm install --save typesense-instantsearch-adapter @babel/runtime

Pass Configuration from Laravel to Frontend

In your Blade template, expose the Typesense configuration:

{{-- resources/views/search.blade.php --}}
@push('before-scripts')
<script>
window.typesense = {
search_api_key: '{{ config('scout.typesense.client-settings.search_api_key') }}',
nodes: @json(config('scout.typesense.client-settings.nodes')),
query_by: '{{ config('scout.typesense.model-settings.' . \App\Models\Product::class . '.search-parameters.query_by') }}',
query_by_weights: '{{ config('scout.typesense.model-settings.' . \App\Models\Product::class . '.search-parameters.query_by_weights') }}',
sort_by: '{{ config('scout.typesense.model-settings.' . \App\Models\Product::class . '.search-parameters.sort_by') }}',
};
window.index = '{{ \App\Models\Product::class }}';
</script>
@endpush

Update Your Vue Component

Here's a complete example of the InstantSearch component migration:

<template>
<ais-instant-search
:search-client="searchClient"
:index-name="indexName"
:insights="false"
>
<div class="search-layout">
<!-- Search Box -->
<ais-search-box placeholder="Search products..." />
<!-- Filters Sidebar -->
<aside class="filters">
<h3>Brand</h3>
<ais-refinement-list attribute="brand" :limit="10" />
<h3>Categories</h3>
<ais-hierarchical-menu
:attributes="[
'categories.lvl0',
'categories.lvl1',
'categories.lvl2',
]"
/>
<h3>Price Range</h3>
<ais-range-input attribute="price" />
<h3>Availability</h3>
<ais-toggle-refinement
attribute="in_stock"
label="In Stock Only"
/>
</aside>
<!-- Results -->
<main class="results">
<!-- Sort Options -->
<ais-sort-by :items="sortOptions" />
<!-- Stats -->
<ais-stats />
<!-- Product Grid -->
<ais-hits>
<template v-slot:item="{ item }">
<article class="product-card">
<a :href="item.product_url">
<img :src="item.image_url" :alt="item.title" />
<h2>{{ item.title }}</h2>
<p class="brand">{{ item.brand }}</p>
<p class="price">${{ item.price.toFixed(2) }}</p>
<span v-if="!item.in_stock" class="out-of-stock">
Out of Stock
</span>
</a>
</article>
</template>
</ais-hits>
<!-- Pagination -->
<ais-pagination />
</main>
</div>
</ais-instant-search>
</template>
<script>
import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter';
import {
AisInstantSearch,
AisSearchBox,
AisHits,
AisPagination,
AisRefinementList,
AisHierarchicalMenu,
AisRangeInput,
AisToggleRefinement,
AisSortBy,
AisStats,
} from 'vue-instantsearch/vue3/es';
export default {
components: {
AisInstantSearch,
AisSearchBox,
AisHits,
AisPagination,
AisRefinementList,
AisHierarchicalMenu,
AisRangeInput,
AisToggleRefinement,
AisSortBy,
AisStats,
},
data() {
const indexName = window.index;
// Initialize Typesense adapter
const typesenseAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: window.typesense.search_api_key,
nodes: window.typesense.nodes,
cacheSearchResultsForSeconds: 120,
},
additionalSearchParameters: {
query_by: window.typesense.query_by,
query_by_weights: window.typesense.query_by_weights,
sort_by: window.typesense.sort_by,
},
});
return {
searchClient: typesenseAdapter.searchClient,
indexName: indexName,
// Sort options use different format than Algolia
sortOptions: [
{ value: indexName, label: 'Relevance' },
{ value: `${indexName}/sort/price:asc`, label: 'Price: Low to High' },
{ value: `${indexName}/sort/price:desc`, label: 'Price: High to Low' },
{ value: `${indexName}/sort/popularity:desc`, label: 'Most Popular' },
],
};
},
};
</script>

Key Differences and Gotchas

Sort-by Widget Format

This is one of the biggest syntax changes. Algolia uses replica indices, while Typesense uses a path-based format:

// Algolia format - requires separate replica indices
const sortOptions = [
{ value: 'products', label: 'Relevance' },
{ value: 'products_price_asc', label: 'Price: Low to High' },
{ value: 'products_price_desc', label: 'Price: High to Low' },
];
// Typesense format - no replicas needed!
const sortOptions = [
{ value: 'products', label: 'Relevance' },
{ value: 'products/sort/price:asc', label: 'Price: Low to High' },
{ value: 'products/sort/price:desc', label: 'Price: High to Low' },
];

Bonus: Typesense doesn't require maintaining separate replica indices for each sort order, which also saves on costs and complexity.

Filter Syntax in Backend Queries

If you're using Laravel Scout for backend searches, the filter syntax differs:

// Algolia syntax
$results = Product::search($query)
->where('is_featured', true)
->where('brand', 'Apple')
->get();
// Typesense syntax
$results = Product::search($query)
->options([
'filter_by' => 'is_featured:=true && brand:=Apple'
])
->get();

Here's a more complex example with OR conditions:

// Filtering for exclusive products OR products available to specific customers
$filterBy = 'is_exclusive:=false';
if ($allowedCustomerCodes->isNotEmpty()) {
$customerFilters = $allowedCustomerCodes
->map(fn($code) => "customer_codes:={$code}")
->implode(' || ');
$filterBy .= ' || ' . $customerFilters;
}
$results = Product::search($query)
->options(['filter_by' => $filterBy])
->take(20)
->get();

Field Weights

Algolia allows inline weights, but Typesense requires a separate query_by_weights parameter:

// This WON'T work in Typesense
'query_by' => 'title:4,sku:3,brand:2,description:1'
// This is the correct approach
'query_by' => 'title,sku,brand,description',
'query_by_weights' => '4,3,2,1',

Maximum Sort Fields

Typesense limits sorting to 3 fields maximum. Choose your most important ranking factors:

// This will error - too many sort fields
'sort_by' => '_text_match:desc,popularity:desc,price:asc,created_at:desc'
// Pick your top 3
'sort_by' => '_text_match:desc,popularity:desc,price:asc'

Analytics/Insights Not Supported

Algolia's analytics widgets aren't supported by the Typesense adapter. Make sure to disable them:

<!-- This will cause errors with Typesense -->
<ais-instant-search :insights="true">
<!-- Disable insights -->
<ais-instant-search :insights="false">

Nested Fields

For hierarchical data like categories, enable nested fields in your schema:

'collection-schema' => [
'fields' => [
[
'name' => 'categories',
'type' => 'object',
'facet' => true,
],
],
'enable_nested_fields' => true, // Required for nested objects
],

Handling User-Specific Data

One challenge specific to my client's B2B platform was displaying user-specific information like custom pricing and wishlist status. With Algolia, you might be tempted to bake this into the index, but that doesn't scale when you have personalised pricing per customer.

My solution: fetch search results from Typesense for fast, relevant matching, then enrich the results with user-specific data from the backend.

The Search Controller

// app/Http/Controllers/SearchController.php
public function search(Request $request): JsonResponse
{
$query = $request->input('q', '');
$customer = auth()->user()?->customer;
// Get base results from Typesense
$products = Product::search($query)
->options(['filter_by' => $this->buildFilterString($customer)])
->take(20)
->get();
// Enrich with user-specific data
$enrichedResults = $products->map(function ($product) use ($customer) {
return [
'id' => $product->id,
'title' => $product->title,
'slug' => $product->slug,
'image_url' => $product->image_url,
// User-specific pricing
'price' => $this->priceCalculator->getCustomerPrice($product, $customer),
'original_price' => $product->price,
'has_discount' => $this->priceCalculator->hasDiscount($product, $customer),
// User-specific wishlist status
'in_wishlist' => $customer
? $customer->wishlists()->whereHas('items', fn($q) => $q->where('product_id', $product->id))->exists()
: false,
// Stock for customer's location
'stock_status' => $this->stockService->getStatusForCustomer($product, $customer),
];
});
return response()->json(['results' => $enrichedResults]);
}
private function buildFilterString(?Customer $customer): string
{
$filters = ['is_active:=true'];
// Exclude products not available to this customer
if ($customer) {
$filters[] = "(is_exclusive:=false || customer_codes:={$customer->code})";
} else {
$filters[] = 'is_exclusive:=false';
}
return implode(' && ', $filters);
}

Yes, this adds an extra processing step, but it's necessary for this use case where:

  • Each customer can have unique negotiated pricing
  • Wishlist status needs to be accurate per-user
  • Stock availability varies by customer location

This hybrid approach gives me the best of both worlds - Typesense's lightning-fast search with the application's business logic applied on top.

Migrating Synonyms from Algolia to Typesense Format

Algolia's synonym export is JSON format, while Typesense uses CSV for bulk imports. There is no online converter or code online so I wanted to share a standalone PHP script that you can use to convert them that worked for me.

Here's an Artisan command to convert between formats:

<?php
// app/Console/Commands/ConvertAlgoliaSynonyms.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ConvertAlgoliaSynonyms extends Command
{
protected $signature = 'typesense:convert-synonyms
{input : Path to Algolia synonyms JSON file}
{output : Path for output CSV file}';
protected $description = 'Convert Algolia synonyms JSON to Typesense CSV format';
public function handle(): int
{
$inputFile = $this->argument('input');
$outputFile = $this->argument('output');
if (!file_exists($inputFile)) {
$this->error("File not found: {$inputFile}");
return self::FAILURE;
}
$json = file_get_contents($inputFile);
$json = preg_replace('/^\xEF\xBB\xBF/', '', $json); // Remove BOM
$algoliaSynonyms = json_decode($json, true);
if (!$algoliaSynonyms) {
$this->error('Failed to parse JSON file');
return self::FAILURE;
}
$fp = fopen($outputFile, 'w');
fputcsv($fp, ['id', 'synonyms', 'root', 'delete?', 'locale', 'symbols_to_index']);
$converted = 0;
foreach ($algoliaSynonyms as $synonym) {
$type = $synonym['type'] ?? '';
switch ($type) {
case 'synonym':
// Multi-way: all words are equivalent
$words = array_map('trim', $synonym['synonyms'] ?? []);
if (count($words) >= 2) {
fputcsv($fp, ['', implode(',', $words), '', '', '', '']);
$converted++;
}
break;
case 'onewaysynonym':
// One-way: input -> synonyms
$input = trim($synonym['input'] ?? '');
$targets = array_map('trim', $synonym['synonyms'] ?? []);
if ($input && count($targets) > 0) {
fputcsv($fp, ['', $input, implode(',', $targets), '', '', '']);
$converted++;
}
break;
case 'altcorrection1':
case 'altcorrection2':
// Typo corrections
$word = trim($synonym['word'] ?? '');
$corrections = array_map('trim', $synonym['corrections'] ?? []);
if ($word && count($corrections) > 0) {
fputcsv($fp, ['', $word, implode(',', $corrections), '', '', '']);
$converted++;
}
break;
}
}
fclose($fp);
$this->info("Converted {$converted} synonyms to {$outputFile}");
return self::SUCCESS;
}
}

Run it with:

php artisan typesense:convert-synonyms storage/algolia_synonyms.json storage/typesense_synonyms.csv

The CSV format for Typesense:

  • Multi-way synonyms: All words in synonyms column, root empty
  • One-way synonyms: Source word in synonyms, target word(s) in root

Migrating Query Rules (Curation) from Algolia

This was the trickiest part of the migration. Algolia's "Query Rules" are called "Curation" or "Overrides" in Typesense, and there's no import feature in the Typesense Cloud UI - you must use the API.

Step 1: Export from Algolia

Export your query rules from Algolia's dashboard as JSON.

Step 2: Convert to Typesense Format

Use the official conversion tool:

npx algolia-query-rules-to-typesense@latest \
--input algolia_rules.json \
--output converted_typesense_rules.json

Step 3: Import via API

Since there's no bulk import in the UI, I created an Artisan command to import the rules:

<?php
// app/Console/Commands/ImportTypesenseOverrides.php
namespace App\Console\Commands;
use App\Models\Product;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class ImportTypesenseOverrides extends Command
{
protected $signature = 'typesense:import-overrides
{file : Path to the converted rules JSON file}
{--collection= : Collection name (defaults to Product searchableAs)}';
protected $description = 'Import curation rules (overrides) into Typesense';
public function handle(): int
{
$file = $this->argument('file');
$config = config('scout.typesense.client-settings');
$collection = $this->option('collection') ?? (new Product())->searchableAs();
if (!file_exists($file)) {
$this->error("File not found: {$file}");
return self::FAILURE;
}
$rules = json_decode(file_get_contents($file), true);
if (!$rules) {
$this->error('Failed to parse JSON file');
return self::FAILURE;
}
$this->info("Importing " . count($rules) . " rules to collection: {$collection}");
$baseUrl = sprintf(
'%s://%s:%s/collections/%s/overrides',
$config['nodes'][0]['protocol'],
$config['nodes'][0]['host'],
$config['nodes'][0]['port'],
urlencode($collection)
);
$success = 0;
$failed = 0;
$progressBar = $this->output->createProgressBar(count($rules));
foreach ($rules as $rule) {
$overrideId = $rule['id'];
unset($rule['id']);
$response = Http::withHeaders([
'X-TYPESENSE-API-KEY' => $config['api_key'],
])->put("{$baseUrl}/" . urlencode($overrideId), $rule);
if ($response->successful()) {
$success++;
} else {
$failed++;
$this->newLine();
$this->warn("Failed: {$overrideId} - {$response->body()}");
}
$progressBar->advance();
}
$progressBar->finish();
$this->newLine(2);
$this->info("Import complete! Success: {$success}, Failed: {$failed}");
return $failed > 0 ? self::FAILURE : self::SUCCESS;
}
}

Run it with:

php artisan typesense:import-overrides storage/converted_typesense_rules.json
# Or specify a collection name:
php artisan typesense:import-overrides storage/converted_typesense_rules.json --collection=my_products

Important: What Won't Convert

Not all Algolia rules have Typesense equivalents. The conversion tool handles:

  • Pinned results (promoteincludes) - Pin specific products at positions
  • Hidden results (hideexcludes) - Hide specific products from results

But these Algolia features cannot be converted:

  • Dynamic filters - Closest equivalent to boosting a column (eg boost a brand or category higher for a search term). More info
  • optionalFilters - Brand/category boosting like brand:Nike<score=2> has no direct equivalent, dynamic filters are close but not exactly the same as boosting.
  • Query rewrites - Replacing or modifying the search query

In my migration, 98 of 145 rules converted successfully. The 47 that failed all used optionalFilters for brand boosting. I ended up using the dynamic filters to achieve this for my scenarios, but am awaiting confirmation from Typesense support as to whether this is the recommended solution. Otherwise there's also sort_by with _eval to achieve boosting, such as: sort_by=_eval(brand:nike):desc,_text_match:desc, or if you wish to boost multiple brands at once, you can use an array with weighting: _eval([ (brand:=ABC):4, (brand:=DEF):3, (brand:=GHI):2, (brand:=JKL):1 ]):desc,_text_match(buckets: 5):desc,avg3mth:desc

The Result

After migration:

  • Cost reduction: ~65% savings compared to new Algolia pricing
  • Performance: Comparable search speeds, often faster for complex queries
  • Developer experience: Minimal frontend code changes thanks to the InstantSearch adapter
  • Reliability: Typesense Cloud's 3-node HA cluster has been rock solid
  • No more replica indices: Sorting doesn't require maintaining separate indices

The migration took approximately one week of development time, including testing and refinement. The hardest part was understanding Typesense's schema requirements and filter syntax differences - once past that learning curve, everything fell into place.

Quick Reference: Algolia vs Typesense

Feature Algolia Typesense
Schema Schema-less Predefined schema required
Sort indices Replica indices Path-based (index/sort/field:dir)
Filter syntax ->where('field', value) ->options(['filter_by' => 'field:=value'])
Field weights Inline (field:weight) Separate parameter
Max sort fields Unlimited 3
Analytics Built-in widgets Not supported in adapter
Query rules UI Full visual builder Basic pin/hide only
Rules import/export JSON via dashboard API only
Brand boosting optionalFilters with scores Dynamic filtering or sort_by with _eval()
Pricing Per-search + records Cluster size + bandwidth

Conclusion

If you're facing similar Algolia pricing concerns, Typesense is a worthy alternative. The InstantSearch adapter makes migration straightforward, and the cost savings are substantial. While it requires a bit more configuration upfront with the schema definition, the trade-off is well worth it for the long-term savings and comparable functionality.

The key is understanding the syntax differences early - filter formats, sort-by patterns, and field weights. Once you've got those down, the rest is smooth sailing.

Useful Resources


Considering migrating from Algolia to Typesense, or need help implementing search in your Laravel application? I specialise in Laravel development and search integrations. Get in touch to discuss your project.

Syntax highlighting by Torchlight

More articles

Livewire vs Intertia in Laravel: Do you need the extra complexity?

At Laracon AU 2025, the debate between Livewire and JavaScript frameworks was a hot topic. Here's why Livewire might be all you need for real-time reactivity in your Laravel applications.

Read article

Talk to me about your website project