Wolfite.dev / Highlights

Obby Wiki•

Manticore Search in MediaWiki

Obby WikiMediaWiki

TL;DR: Experimental Manticore Search support in MediaWiki at less than 80 MiB for our use case. Skip to #takeaway to ignore the testing and benchmarking (more interesting).

This could either be a post I look back on fondly, or one I forget about entirely. At this moment, I'm completely unsure!

For a few days now I've been experimenting with Manticore Search support inside MediaWiki, mostly as a much lighterweight alternative to CirrusSearch and OpenSearch/ElasticSearch. In fact, my first rough benchmark of a small wiki database (709 articles) showed Manticore at around 79 MiB (109 MiB peak) with OpenSearch around 2-3 GiB.

I was initially skeptical, but this is completely accurate to how I've experimented further now.

At the start I was looking for an alternative to OpenSearch in many places, and Manticore stood out because of its long-lasting presence (it's a maintained fork of Sphinx) and large feature-set (pretty much everything MediaWiki/Cirrus used and more). Essentially, I couldn't find a single better option, and Manticore being so lightweight just sealed the deal for me. However, I did explore other options like Vespa (likely heavier than OpenSearch), Tantivy (which came close, but lacked vectors), Bleve (outperformed by Manticore), Meilisearch/Typesense (close but lacked regex and proper weighting).

Things like SQLite FTS5 and PostgreSQL or Xapian were also looked at, but didn't make much sense to me.

So, in the end, Manticore won, but there were close contenders, and I still had yet to verify the claims.

Test 1

These benchmarks are just rough numbers, essentially. Nothing is remotely concrete, and just gives you a vague idea. Except, that is, for the real 709 pages (corpus A).

The first benchmark was lazy, it didn't really test much; it showed Manticore performing well and under much less resources, but only tested full text search. The test ran both the real corpus (corpus A) of (then) 334 docs, as well as corpus B (100k docs, repeated from the original 334 x 300).

OpenSearch was run with a 2 GiB heap for this test. This test didn't run much but was worth mentioning quickly.

Test 2

Anyway, test 2 was much more refined. Keep in mind this was just through querying inside the container, there was no HTTP overhead that you'd see in actual production.

The next test was refined by:

  • Testing Manticore memory at idle with the real corpus, with both the helper on/off at a 384 MiB container limits
  • Testing typeahead with typo tolerance and a snippet query
  • OpenSearch with a 512 MiB heap.

Sparing some details, the test run smoothly and got the answer out pretty definitively. Even with a 512 MB heap, OpenSearch measured around 1 GiB idle and 1.6-2.0 GiB after load. While, on the other hand, Manticore measured at about 37 MiB idle and 115-125 MiB after use (helper on), while without the helper, it measured 13 MiB idle.

Typo-tolerant typeahead latency was measured somewhere around 1.8-2.7ms, while snippet query latency was 0.75-2.3ms.

Just to be genuine for a second, this completely shocked me, honestly; expectations were much more modest. Considering the amount of RAM left on my server, it wasn't even an option to continue with OpenSearch. I've already accidentally OOM'd my production host with it (by enabling two at the same time), but even then it'd come close. Not to mention, huge pain to deal with (I'm talking about ElasticSearch, mainly, but I assume OpenSearch inherits a lot of the problems).

There is a trade-off though, about 30 MB extra in disk space for Manticore throughout the test, but personally, I think that's a worthwhile trade-off. Another gap is that Manticore's typeahead suggester (when tested with typos) peaked at about 72% titles found, while OpenSearch found 86%. This was without tuning, however, so it remains to be seen the real effects of this.

There are some more shortcomings with it, but at this point OpenSearch wasn't even an option anymore so I decided to press forward.

For more details:

Roughly, Manticore uses a tenth of the memory that OpenSearch does, and is level or faster with everything but typo-tolerant typeahead.

Memory

StateManticoreOpenSearchRatio
Idle (334 docs)37 MiB (13 MiB with helper off)1,060 MiB~28x
After typeahead, snippet and query runs (334 docs)~113-117 MiB, peak ~125 MiB~1,615-1,817 MiB~15x
After 100k-doc load and queries384 MiB (at its limit; 344 MiB unconstrained in round 1)2,037 MiB, peak 2,240~5.5x (roughly)

Speed

TestManticoreOpenSearchAhead
Keyword query (334 docs) (average)0.35-0.38 ms0.55-0.91 ms (2 GB heap run)Manticore
Selective queries (100k docs)0.63-0.76 ms1.20-1.46 ms (2 GB heap run)Manticore, ~2x so
Broad queries (100k docs)1.09-1.21 ms0.89-1.20 ms (2 GB heap run)Level
Throughput at 16 connections (334 docs)16,014 qps8,947 qpsManticore, +79%
Throughput at 16 connections (100k docs)4,557 qps6,005 qpsOpenSearch, +32%
Snippet queries (average)0.75-2.30 ms1.08-3.15 msManticore on 3 of 4 types
Snippet throughput at 16 connections5,361 qps3,508 qpsManticore, +53%
Bulk indexing (100k docs)11,461 docs/s8,913-13,930 docs/sLevel
Index size on disk (100k docs)107.5 MB77.9 MBOpenSearch, 28% smaller

Typeahead (334 titles)

MethodManticoreOpenSearchAhead
Prefix, no typo tolerance: found / latency82% / 0.34 ms85% / 0.84 msManticore on speed, results are level
Best typo-tolerant method, clean input90% / 2.73 ms86% / 0.59 msOpenSearch on speed
Best typo-tolerant method, typo input72% / 2.65 ms86% / 0.58 msOpenSearch
Typo-tolerant throughput at 16 connections2,581 qps9,186 qpsOpenSearch, 3.6x
Slowest single request1,035 ms7.9 msOpenSearch

Not perfect by any means, but definitely worth pursuing, at least I thought so. Anyway, it took a bit, but I eventually replaced core-search with this at least on Special:Search:. It's not exactly perfect, and lacks pretty much every search operator Cirrus has, but I still thought it was a good PoC at that point!

I didn't stop there, though.

Everything in MediaWiki's core search UI is visible now, but I'm going to spin off a new extension to revamp search's UI, in my ideal world, at least. So most of everything cool that Manticore Search does has yet to be seen on MediaWiki, but I hope that shortly it will.

Not sure if I'm going to open source this extension, since I don't think it stands to benefit that many people. We'll see in the future, though.

Cross-wiki benchmarking

What's a good way to say that I did test this with a make-shift farm of 20 wikis? I love wasting time experimenting, and sometimes it pulls through. You just don't hear about the times it doesn't.

Environment and workload

For some context first: I have two testing databases, my_wiki and kowiki. Very little content is on kowiki, as I mainly used it to test cross-wiki sign-in with NewAuth. Every page is Korean, however.

  • Existing MediaWiki 1.46 / PHP 8.3.32 and Manticore 29.9.0 / Buddy 4.4.3 containers (from my local PC, under WSL2)
  • Manticore retained its two-CPU quota, 256 MiB memory cap, and was given no swap allowance
  • Each fixture copied all 790 indexed documents from my_wiki, including rendered body, source text/raw source, completion, redirects, and exact membership data. This was from a modern import of the Obby Wiki, containing pages in the Obby category, as well as Player and Studio
  • Every fixture table used the current schema/tokenization/morphology, a 32 MiB per-table RAM limit, and an additional wiki_id string attribute. Each table was first flushed to disk after insertion. Twenty copies occupied 134,337,383 disk bytes (~128.1 MiB) of search data
  • Synthetic document IDs were globally unique (wiki_number * 1000000 + page_id) while retaining the original database-local page_id value. This is required to keep all copied hits distinct in this prototype, as the future backend will still need a durable identity scheme and origin-wiki validation
  • Equal rotations of broad rendered-text such as tower, the exact title phrase Tower of Sound, the exact source phrase of Button Labs, and a nonexistent word. Half of the requests used offset 0, half used offset 100. The result limit was 20

The rest aren't really important as much, so you can choose to read or skip them without missing much context (a lot of it is explained inline anyway):

  • Queries selected wiki identity, page metadata, weighted scores, and highlighted title/body snippets. Each measured query included SELECT, SHOW META, and SHOW WARNINGS over an established connection. Search options matched the main weights 10:5:1:, max_matches 10000, and max_query_time 1000 ms. Highlight markers used backend defaults rather than the production HTML-preservation markers
  • One, four, and eight concurrent clients each performed 200 requests. Mixed phases ran four cross-wiki readers, one local reader, and a writer for five seconds. The writer performed 100 full document replacements at a target of 20/s, rotating across tables, with ~10.4 KiB body and ~8.8 KiB source payloads. It used a fresh client/schema check per replacement, approximating independent edit requests; delays could produce catch-up bursts
  • Totals were checked against the original corpus and multiplied by table count. Expected broad-query totals were 136, 680, 1360, and 2720. Phrase/source totals were 1, 5, 10, and 20. Nonexistent-word totals were zero
  • Ran with no PHP bootstrap

Searches without concurrent writes

Latency combines all four query types. Throughput is an unpaced backend load test and not a supported HTTP request rate (HTTP overhead was not accounted for). Peaks below are sampled cgroup usage during the phase, which includes reclaimable memory. Fixture-loading peaks are separate.

# of fixture tablesDocuments (Pages)ClientsMedian msp95 msRequests/sPhase peak MiB
179011.07.3446102.9
179042.010.9983103.3
179087.219.3950104.2
5395011.711.9289162.8
5395045.116.9539161.9
53950813.327.3547162.2
10790012.514.5218211.7
10790048.526.9334211.9
107900820.340.2355212.4
20 (setup trimmed)1580014.416.6151209.1
20 (setup trimmed)15800414.231.8238214.5
20 (setup trimmed)15800831.655.6240222.1

Broad queries were slower than the other types, coming in at twenty tables with eight clients, their median was 43.0 ms and p95 67.0 ms. The all-workload maximum was around 102.0 ms.

Concurrent reads and writes

Tested with four cross-wiki clients and one local client, with 100 paced replacements:

TablesCross-wiki p95 msCross-wiki requests/sLocal p95 msWrite median / p95 msPhase peak MiB
112.975613.12.7 / 403.3105.4
523.137210.313.9 / 22.9165.1
1029.826111.921.7 / 38.7214.5
20, setup trimmed40.717714.324.1 / 51.3228.3

The first mixed phase had substantial write outliers, with a 1359 ms maximum. Later write phases were much faster. The cause was not established, but I guess it could've been something related to running under Windows. Regardless, as such, this benchmark does not promise a write-latency bound. Twenty-table write maximum was 120 ms.

Across both runs, 22,665 measured search requests and 400 replacements returned no query errors, incomplete results, warnings, or incorrect totals.

Memory findings

The ordinary run began at 69.5 MiB and completed all one/five/ten-table phases, and then completed the twenty-table single-client phase (p95 @ 16.3 ms). It stopped before twenty-table concurrent phases when the admission guard saw usage above 224 MiB in usage. The guard is checked before loading another table or admitting the ordinary search phases, but it does not cancel work already running or guarantee that transient allocations stay below that same threshold.

During fixture loading, sampled usage reached exactly 255.9 MiB and cgroup max increased by 473 events. oom and oom_kill stayed at zero. Dropping the fixtures left usage at 166.7 MiB, mostly consisting of anonymous memory (157.4 MiB), rather than file cache. Manticore's DEBUG malloc_trim then returned 1 and reduced usage to 71.1 MiB without restarting or changing daemon settings. This command releases allocator-retained memory and was introduced in v2.7.3 in September of 2018 (see the Changelog).

Anyway, a separate twenty-table run released allocator memory after each fixture insertion (only during setup). It performed all timed search/write phases without trimming or changing the cap. This is the twenty-table row set you see above. In no way is it a repeat of the first run.

Despite the amount of stress placed on Manticore, the container never OOM'd and stayed fully operational, rejecting no requests.

Search latency was acceptable at the tested counts, but bulk-loading/rebuild memory was a larger concern. As such, twenty is feasible for this environment, but I cannot promise that it would actually work within the 256 MiB cap for larger or busier wikis.

This benchmark did NOT evaluate corpora with sustained edits/rebuilds. There is no evidence here for capacity beyond twenty copies.

Takeaway

All in all, I believe in its potential, but currently I do not believe sustained cross-wiki farm searching is possible at 256 MiB reliably. If this took place on a real farm, I think the results would've been way different. It is worth remembering the corpus was essentially the same thing just duplicated. I would love to retry this with a real corpus later.

Focus

This extension's goal is not to reach parity with CirrusSearch. Not because I don't think Manticore itself can support it, but because Cirrus has so many features. I can't imagine that more than 1% of users use the missing features. Anyway, I think it'd be nice. If I make the extension open source in the future, feel free to add CirrusSearch features.

For the things I care about, however, I do hope it ends up being compelling and even better than Cirrus is. This extension is mainly for the crowd with smaller servers that want powerful (-enough) search at a much lesser cost in terms of RAM. Search for the rest of us! I guess.

I used to use ElasticSearch in production, but it was the first to go. Honestly, I could've kept my server at 2 GB of RAM if I never used ElasticSearch, it's rather bad.

While in terms of CirrusSearch parity, this extension does lack a lot, it also has some positives in where functionality exceeds Cirrus. Some other parts are finicky, like ManticoreSearch only working on a fork of RelatedArticles. I doubt that could go upstream, considering I have a minor patch still pending on the repo since February 2026, I consider it unmaintained.

There's also a few things we can never beat CirrusSearch on, such as GeoData search and WikibaseCirrusSearch; we are, however, looking into a new extension that's sort of SearchVue and AdvancedSearch that embeds some of Manticore's features inside the UI (ManticoreSearch is just the backend). I believe Manticore itself also supports facets, too, which could probably be of use.

Features

Just a rapid-fire of some existing features below. Keep in mind this is for the current extension ManticoreSearch, not Manticore itself.

  • Stemming
  • Field weights, weights also added to by TrendingArticles page_views (7d)
  • Typo-tolerant typeahead
  • morelike: and a few other operators like intitle:, incategory:, hastemplate:, and insource:
  • Filters with Cargo (my hope is Bucket one day, but it isn't close to parity)
  • Related articles support (again, on the fork only)
  • A few other QoL features like -word negation- or or|operators, as well as "exact phrase matching" or 'phrase matching with stemming'

Probably a little more I missed. Cross-wiki search is currently experimental and only on the backend. Again, no UI modifications.

$wgManticoreSearchCrossWikiEnabled = true; // default false
$wgManticoreSearchCrossWikiPublicWikis = [ 'my_wiki', 'kowiki' ]; // default []

So, I guess, it's better than core search, slightly worse than Cirrus in some ways. I think OpenSearch has better support for other languages, but I do believe Manticore will work regardless, just not as well for languages other than English. Along with Cargo, I'll also be implementing the new search UI. Will likely have facets as well. I believe Manticore supports search with LLMs, but I don't think that'd be used much.

Anyway, if it's interesting I can definitely open source it. Of course, my pipedream is that it's better than Cirrus in almost every way, but that's not feasible. Although, technically speaking, I do believe it is possible.