log in
consulting hosting industries the daily tools about contact

Typesense: Real Search for 1/10th What Algolia Charges

I swapped Algolia for Typesense on a product catalog and cut the search bill by 90%. Here's what it took and what I'd warn you about.

Algolia is genuinely good software. I've also watched it quietly eat $800/month from a mid-size e-commerce client who sold outdoor gear and had maybe 400k product records. The search was fast. The bill was faster. When Typesense crossed my radar I was skeptical — "open source Algolia" is a bold claim — but I ran the migration anyway. That was eighteen months ago and I haven't looked back.

What Typesense Actually Is

At its core, Typesense is a search engine purpose-built for the same use case Algolia owns: instant, typo-tolerant, faceted search over structured records. Not Elasticsearch's document indexing kitchen sink. Not Solr from 2009. Something lean, written in C++, that does one thing — return relevant, fuzzy-matched results with facet counts in under 10ms — and does it without requiring a PhD in relevance tuning.

The pitch is straightforward: self-host it on a $20 DigitalOcean droplet, point your app at it, and stop writing checks to Algolia. Or use Typesense Cloud if you don't want to manage infrastructure. Their cloud pricing is still dramatically cheaper than Algolia's because they're not charging per search operation — they charge per node/hour, which is a much friendlier model once you have real traffic.

For the outdoor gear client I mentioned: we went from ~$820/month on Algolia (records + search ops) to $58/month on Typesense Cloud for equivalent performance. That's not a rounding error. That's a line-item that a business owner notices.

The Migration in Practice

Typesense has a Laravel Scout driver maintained by the community (typesense/typesense-php + a Scout provider). Setup is familiar if you've used Scout with Algolia.

First, the collection schema. This is where Typesense differs from Algolia in an important way — you define your schema upfront, including field types. Algolia infers everything. Typesense wants to know what you're indexing.

// config/typesense.php (relevant portion)
'model-settings' => [
    App\Models\Product::class => [
        'collection-schema' => [
            'name' => 'products',
            'num_documents' => 0,
            'fields' => [
                ['name' => 'id', 'type' => 'string'],
                ['name' => 'name', 'type' => 'string'],
                ['name' => 'description', 'type' => 'string'],
                ['name' => 'brand', 'type' => 'string', 'facet' => true],
                ['name' => 'category', 'type' => 'string', 'facet' => true],
                ['name' => 'price', 'type' => 'float', 'facet' => true],
                ['name' => 'in_stock', 'type' => 'bool', 'facet' => true],
                ['name' => 'rating', 'type' => 'float'],
                ['name' => 'created_at', 'type' => 'int64'],
            ],
            'default_sorting_field' => 'created_at',
        ],
    ],
],

The model itself is standard Scout:

class Product extends Model
{
    use Searchable;

    public function toSearchableArray(): array
    {
        return [
            'id'          => (string) $this->id,
            'name'        => $this->name,
            'description' => $this->description,
            'brand'       => $this->brand,
            'category'    => $this->category->name,
            'price'       => (float) $this->price,
            'in_stock'    => (bool) $this->stock_qty > 0,
            'rating'      => (float) ($this->avg_rating ?? 0),
            'created_at'  => $this->created_at->timestamp,
        ];
    }
}

Search with facets is where it gets interesting. Scout's fluent interface doesn't expose Typesense-specific parameters directly, so for faceted search you drop down to the raw client:

use Typesense\Client as TypesenseClient;

class ProductSearchService
{
    public function __construct(private TypesenseClient $client) {}

    public function search(string $query, array $filters = [], int $page = 1): array
    {
        $filterBy = collect($filters)
            ->map(fn($value, $field) => "{$field}:={$value}")
            ->implode(' && ');

        $results = $this->client->collections['products']->documents->search([
            'q'               => $query ?: '*',
            'query_by'        => 'name,description,brand',
            'query_by_weights' => '3,1,2',
            'facet_by'        => 'brand,category,price,in_stock',
            'max_facet_values' => 50,
            'filter_by'       => $filterBy ?: null,
            'sort_by'         => 'rating:desc',
            'per_page'        => 24,
            'page'            => $page,
            'typo_tokens_threshold' => 1,
            'num_typos'       => 2,
        ]);

        return [
            'hits'   => collect($results['hits'])->pluck('document'),
            'facets' => $results['facet_counts'],
            'total'  => $results['found'],
        ];
    }
}

The query_by_weights parameter is one of my favorite Typesense features. You're telling the engine that a match in name is three times more important than a match in description. Algolia has this too, but the Typesense syntax is dead simple and it actually changes results in ways you can reason about.

Gotchas That Will Bite You

Schema changes are destructive. This is the big one. In Algolia you just send new attributes and they show up. In Typesense, if you need to add a field that isn't optional: true, you have to drop and recreate the collection, then reindex. On 400k records that's a 20-minute window. Plan for it. I now add 'optional' => true to nearly every field except the ones I know will always exist.

Facet fields must be declared in the schema. You can't decide mid-query that you want to facet on something you didn't mark facet: true at collection creation. I've hit this twice — once when a client wanted to add a "material" facet post-launch. Minor annoyance if you're prepared for it, incident if you're not.

The filter_by syntax is its own little language. Range filters look like price:[20..150]. Multi-value filters look like brand:=[Nike,Adidas]. Boolean filters look like in_stock:=true. It's documented but not obvious, and the error messages when you get it wrong are not always helpful. Keep the docs open.

Typo tolerance is aggressive by default. Two typos allowed on short strings will sometimes surface genuinely wrong results. For a product name like "Arc'teryx" the fuzzy matching can get weird. I usually dial num_typos down to 1 for short query_by fields and only let it breathe on descriptions.

Self-hosted HA requires Typesense Cloud or careful cluster setup. If you're running on a single node and it goes down, search goes down. For most of my clients I use Typesense Cloud for production precisely because I don't want to think about this. The $40-80/month for a single cloud node is still a fraction of what Algolia charges.

When I'd Reach for Typesense

Product catalogs, real estate listings, restaurant menus, doctor directories — anything where you have structured records, users type imprecise queries, and you need faceted filtering. This is the exact sweet spot. I've used it for a healthcare directory (providers by specialty, insurance, location), a print management app's template library, and two e-commerce builds. It has held up in all three.

If your Algolia bill is over $200/month, do the migration math. It's probably worth a sprint.

I'd also reach for it over Elasticsearch for this use case every time. Elastic is powerful but it's operationally heavy and relevance tuning in it is a part-time job. Typesense gives you 90% of the search quality with 10% of the complexity.

When I Wouldn't

Full-text document search (think: searching through PDFs, legal docs, long-form content) is not what Typesense is optimized for. The lack of native aggregations beyond facet counts means analytics-heavy search — "how many products sold in this price range last quarter" — isn't its job either. Use Postgres or a real analytics store for that.

I also wouldn't use it if you need geo-search as a primary feature. Typesense has geo filtering and it works, but if location is the main search dimension (think Uber-style radius queries on millions of records) you're probably better served by PostGIS or a purpose-built geo index.

And if your team has zero capacity to manage infrastructure and zero tolerance for any operational overhead, Algolia's managed simplicity might still be worth the premium. I'm not going to pretend ops overhead is zero with self-hosted Typesense.

The Bottom Line

Typesense is the first open source search engine I've used that I'd recommend to a client without a bunch of asterisks. The schema-first approach feels like a constraint until you realize it's the reason it's so fast and predictable. Ninety percent of what makes Algolia worth paying for is here, for a fraction of the price. If you're signing checks to Algolia for a product catalog or directory app, spend an afternoon on the migration — you'll probably be done by dinner.

Need help shipping something like this? Get in touch.