CLOUD
development

Smart Arabic Search in Your App: Self-Hosted Meilisearch

CLOUD|October 19, 2026|7 min read|130 views
Smart Arabic Search in Your App: Self-Hosted Meilisearch

LIKE queries miss Arabic results because of hamza forms, taa marbuta and diacritics. Self-hosted Meilisearch normalizes Arabic, tolerates typos and answers in under 50 ms, with 1 million records needing about 2 to 4 GB of RAM.

If your app searches Arabic text with LIKE '%term%', your users are missing results every day. A customer types مدرسه and the record says مدرسة, or types مستشفى while the data has مستشفي, and the query returns nothing. The fix that works for most teams is Meilisearch running on your own server: its tokenizer normalizes Arabic letter variants and strips diacritics, it tolerates typos out of the box, it returns ranked results in under 50 ms for millions of records, and it plugs into Laravel through Scout with about ten lines of configuration. A catalog of 1 million product records of around 1 KB each typically needs 3 to 6 GB of disk for the index and runs well with 2 to 4 GB of RAM available for the page cache, so a 4 vCPU / 8 GB server holds it alongside your app. The non negotiable part is security: Meilisearch must listen on 127.0.0.1, run with a master key of at least 16 bytes, and the browser should only ever see a search only key or a tenant token. Below we explain why SQL search fails in Arabic, how Meilisearch handles it, the Scout setup, memory sizing, key handling, and how it compares with MySQL FULLTEXT and OpenSearch.

Why LIKE fails on Arabic

Arabic has several letters that people write interchangeably, and SQL treats each as a different character. The common trouble spots:

  • Hamza forms: أ and إ and آ and ا are often typed the same way on a phone keyboard. Some MySQL collations fold a few of them, others do not, and the behavior changes between utf8mb4_general_ci and utf8mb4_0900_ai_ci.
  • Taa marbuta and haa: ة and ه at the end of a word, as in مدرسة and مدرسه. No standard collation treats them as equal.
  • Alef maqsura and yaa: ى and ي, as in مستشفى and مستشفي.
  • Diacritics and tatweel: a title stored as مُحاسَبة or مـحاسبة will not match a plain search unless the collation ignores them.
  • The definite article: users search for سيارة while the text says السيارة, and you get no ranking that rewards the closer match.

On top of matching, LIKE '%term%' cannot use a B-tree index, so every search scans the whole table. On 500,000 rows that is often 300 to 800 ms per query, and it gets worse as the table grows. There is also no typo tolerance and no relevance ranking, so a product called exactly what the user typed can appear on page 4.

How Meilisearch handles Arabic

Meilisearch tokenizes text with its Charabia library, which detects the script and applies Arabic specific normalizers. In current versions it folds alef variants to a bare alef, treats ى like ي and ة like ه at the matching stage, removes diacritics and tatweel, and segments the definite article so that السيارة can match سيارة. You do not configure any of this, because it applies to every Arabic field automatically.

Typo tolerance is on by default. Words of 5 characters or more accept 1 typo, and words of 9 or more accept 2, so مستشفيات typed as مستشفيت still matches. You can tune both thresholds per index, and disable typos on fields such as SKUs or national ID numbers where an approximate match would be wrong.

Ranking follows a clear rule chain: number of matched words, typos, proximity, which attribute matched, then exactness. Put name before description in searchableAttributes and a match in the product name always wins, which is usually what users expect.

Running it on your server

A single binary or one Docker container is all you need. A minimal production setup with Docker:

docker run -d --name meili --restart unless-stopped \
  -p 127.0.0.1:7700:7700 \
  -e MEILI_ENV=production \
  -e MEILI_MASTER_KEY="$(openssl rand -base64 32)" \
  -e MEILI_NO_ANALYTICS=true \
  -v /srv/meili/data:/meili_data \
  getmeili/meilisearch:v1.11

Store the generated master key in your secrets file before you lose it. Binding to 127.0.0.1 means only processes on the same server can reach it, and if the frontend needs direct access, put Nginx in front with TLS and a rate limit of around 20 requests per second per IP on the search route.

Connecting Laravel Scout

Install the packages and point Scout at your instance:

composer require laravel/scout meilisearch/meilisearch-php http-interop/http-factory-guzzle

SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=your-admin-api-key

Add the Searchable trait to your model and return only the fields you search or filter on from toSearchableArray(). Sending whole rows with long HTML descriptions inflates the index 2 to 3 times for no gain. Define settings in config/scout.php under index-settings, for example searchable attributes in order, filterableAttributes such as city and category_id, and sortableAttributes such as price. Then run php artisan scout:sync-index-settings and php artisan scout:import "App\Models\Product". Importing 1 million records in batches of 1,000 usually takes 10 to 25 minutes on 4 vCPU. After that, Scout keeps the index in sync on every save through your queue.

Add synonyms for the words your market uses: جوال and موبايل and هاتف, or تكييف and مكيف. Synonyms in Meilisearch are a simple JSON map, and 30 good entries often fix more failed searches than any ranking tweak.

A settings block you can start from

'index-settings' => [
    App\Models\Product::class => [
        'searchableAttributes' => ['name', 'brand', 'description'],
        'filterableAttributes' => ['city', 'category_id', 'in_stock'],
        'sortableAttributes'   => ['price', 'created_at'],
        'typoTolerance' => [
            'disableOnAttributes' => ['sku'],
        ],
    ],
],

Keep filterable attributes to what the UI actually filters on, because each one adds to index size and indexing time, often 5 to 10% per attribute on large catalogs.

How much memory per million records

Meilisearch stores its index on disk in LMDB and relies on the operating system's page cache for speed. It does not need the whole index in RAM, but search gets slower when the hot part does not fit.

Records (about 1 KB each)Index on diskRAM for smooth searchTypical search time
100,0000.4 to 0.8 GB0.5 to 1 GB5 to 15 ms
1,000,0003 to 6 GB2 to 4 GB15 to 40 ms
5,000,00015 to 30 GB8 GB or more30 to 80 ms

Indexing is the heavy moment, since a full reindex can use most of your cores for several minutes, so schedule big imports outside business hours. For a catalog of up to 1 million records next to a Laravel app, a Business cloud server with 4 vCPU and 8 GB at 11,500 SAR a year is a comfortable fit. If the index grows, +2 GB of RAM costs 600 SAR a year and +100 GB SSD costs 500 SAR, so you scale exactly the resource that is short.

Protecting the master key

The master key can create indexes, delete them and mint new keys, so it never leaves the server. Use it once to list the default keys, then use the Default Admin API Key in your backend and the Default Search API Key anywhere public. For multi tenant apps, where each company should only see its own records, generate tenant tokens on the backend with a filter such as company_id = 42 and an expiry of 1 hour. The browser can search directly and fast, but a modified request still cannot read another tenant's data. Rotate the admin key every 90 days and on any staff change.

Meilisearch, MySQL FULLTEXT or OpenSearch

CriterionMySQL FULLTEXTMeilisearchOpenSearch
Arabic normalizationDepends on collation, partialBuilt inBuilt in Arabic analyzer
Typo toleranceNoneDefaultFuzzy queries, needs tuning
Minimum RAMUses MySQL buffer poolAbout 512 MB2 to 4 GB of JVM heap
Setup effortOne indexOne containerCluster and mappings
Best forSmall internal toolsProduct and content searchLogs, analytics, complex queries

MySQL FULLTEXT is acceptable for an internal tool with 20,000 rows, while OpenSearch earns its weight when you need aggregations across hundreds of millions of documents. For user facing search in an Arabic app, Meilisearch gives you most of the quality with a fraction of the operational work.

Your next steps

  1. Collect 50 real searches from your logs that returned zero results, which becomes your test set.
  2. Run Meilisearch in Docker bound to 127.0.0.1 with a strong master key.
  3. Connect Scout, slim down toSearchableArray(), and import your data.
  4. Add searchable attribute order, filters and 30 local synonyms.
  5. Re-run the 50 searches and compare, then expose only a search key or tenant token to the browser.

Check the plans on our pricing page to size the server, and keep both the app and the index in Riyadh, about 8 ms from your users.

Share