Sysadmin Hell

Legacy Labs 2026

Intro

This is a short build log for my LegacyLabs experiment: taking an old, abandoned search engine and seeing whether it can still be useful in a modern small web app. The constraint is deliberate: modern OS, tiny VPS, old-school search stack, and no heavyweight Elasticsearch/SOLR style of machinery.

Day 1: Swish-e

For my LegacyLabs pick, I want to revive an abandoned piece of software: Swish-e.

I used Swish-e on a customer project many years ago, but only at a surface level. It did some basic site-search work, and I remember admiring the idea of it, but I never really had the chance to understand it properly.

That makes it a good candidate for this project. This is not random retro software for me, it is unfinished business.

The motivation is simple: I want adequate website search, and possibly document search, without needing a large modern search stack sitting there with 32GB of RAM allocated to it. Swish-e comes from a time when search had to be smaller, simpler, and more directly operated by the sysadmin.

So let’s begin.

The first decision: this will not run on an entirely retro platform.

I briefly considered running an old Linux distribution, but running a 20-year-old operating system on the public Internet, with decades of unpatched security holes, gives me hives. I also do not have physical server hardware or an adequate home connection for hosting it myself.

Instead, I will use a small paid VPS: one CPU, 512MB RAM, and Debian 12 Bookworm.

That feels like the right compromise. The operating system will be modern and patched, but the application layer will still be deliberately small and old-school. Debian also makes sense because I will need build tools and compatibility with older C software. A minimal distribution like Alpine is attractive in general, but for this particular task it creates the wrong kind of difficulty.

Day 2: Actual sysadmin

I found the archived GitHub repo: https://github.com/swish-e/swish-e.

Before that, I found out there is still a Debian package for Swish-e. That would have been the easy option, but I decided not to use it.

If I am going to use an abandoned search engine, I at least want to know whether I can still build it from source on a modern system. So I pulled the latest archived code and tried to compile it on Debian 12 Bookworm.

It did not build cleanly.

The first issue was a name clash around uncompress2. I did not spend too much time making it beautiful. This is not library development, and I am not planning to maintain a public Swish-e fork (yet!).

I just renamed the symbol.

find src -type f \( -name '*.c' -o -name '*.h' \) -exec perl -pi -e 's/\buncompress2\b/swish_uncompress2/g' {} \;

After that, the build got further and eventually produced a working binary.

I made a couple of small test documents, indexed them, and queried them back. That confirmed the important part: Swish-e still works on a modern Debian box if you are willing to give it a small shove.

The next question is how to use it programmatically.

Swish-e comes with C and Perl interfaces. I do not want to use either. For this project, it will probably be treated as a command-line tool called from the application layer.

Day 3: Docs

Reading the Swish-e documentation. It may as well be French. I am no longer used to documentation like that and I don't usually shy away from man pages. The capabilities that I want to achieve are as follows:

  • My documents will live in database, not flat files
  • I want to have them converted and consumed by the search engine.
  • I want to programmatically query it and return relevant information back to scripts.
  • I want to be able to index one document at a time
  • I want to be able to index everything in bulk
  • I want to be able to update / delete index if document changed or removed.

Finally, once I have all that, I'd like to proliferate documents to something uncomfortable (like at least 100,000) and see if it still stands on 1CPU / 512RAM and whether the results I get from it are adequate.

Day 4: Armature

I like thinking with my hands, and to properly play with the search engine, I need real-ish data.

So today I put together a small app: a hobby web app for posting memories about old computers. It is a hybrid of two apps I worked on before but never published. The important part is that the search layer will rely on user-generated content, not carefully prepared test fixtures.

In the spirit of keeping it economical, I went with FlightPHP and SQLite for a simple backend, ported over some of my slick PixiJS components, and started planning the search document structure.

The search documents will look something like this:

<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<title>Apple IIe</title>

	<meta name="computer_id" content="2">
	<meta name="user_id" content="1">
	<meta name="username" content="demo">
	<meta name="slug" content="apple-iie">
	<meta name="url" content="/u/demo/apple-iie">
</head>
<body>
<h1>Apple IIe</h1>

<div class="story">
	The Apple IIe was the workhorse of our school computer lab.
    We learned Logo programming and played Oregon Trail.
    The green screen and the satisfying click of the keyboard are forever 
    etched in my memory.</div>
</body>
</html>

The idea is simple: Swish-e indexes the full text, while the metadata gives the app enough pointers to route search results back to the right computer, user, and URL.

For the first iteration, I will be happy with basic full-text search, as long as it works reliably.

Day 5: Local environment

I got all machinery for the indexing and search to run on my FlightPHP app. I hit a few obstacles, like I couldn't get Swish-e to work on my MacBook, as it was failing with segfaults. I asked ChatGPT for help and it paid off. Basically, ignore the library soup provided by brew and build it without libxml2

./configure --prefix="$HOME/local/swish-e" --disable-libxml2

Now full text works, indexing works, deletions / insertions / updates are working too. Next thing is to host it on my underpowered machine and stress test.

Day 6: Prod

The production environment is intentionally lean: a single 3.8 GHz CPU and 512 MB of RAM.

I installed my app and indexed all the documents.

Search is working fairly well in full-text mode, but full-text search itself is not the interesting part. I do not need a dedicated search tool just to match words. I could have achieved that with a MySQL full-text index, or the equivalent in Postgres.

Before exploring the more interesting features, I wanted to answer a simpler question: can I index a relatively large corpus of data on an underpowered server?

Seeding

I was tempted to use cheap AI to generate the content, but I felt bad about all the wasted energy, so instead I built a seeder that generates text from randomly stitched permutations and some Faker content.

The content only needed to be semi-coherent, and it turned out fairly okay.

I also added some genuine content, so the idea is to search for a needle in a haystack: genuine personal computer stories hidden inside a sea of generated filler.

Command

I created a command to index the search corpus.

It is a two-step process:

  • Convert all database records into HTML files containing metadata and the document body.
  • Feed that directory of generated HTML documents into Swish-e.

Results

The full database of 100,000 seeded records took about 30 minutes to process.

Most of that time was spent generating the actual documents. I had to process them in batches and use explicit garbage collection to stay economical with memory, so that probably contributed to the runtime.

But the results are good.

Search queries take roughly 100–300 milliseconds and return accurate full-text results.

That is more than acceptable for a tiny production box.

Day 7: Search features

The goal of this exercise is not just to prove that search works. Full-text search alone is not much of an achievement.

What I actually want is to see how far I can push an old-school search engine like Swish-e.

When I think about what I expect from search, three things come to mind:

  • Stemming
  • Phonetic search
  • Synonyms

After digging some documentation I found that the first two are actually available through fuzzy indexing. Let's try it out.

Stemming reduces words to their root form, so searches for "install", "installed", and "installing" can find the same family of results.

DoubleMetaphone does phonetic matching, so words that sound similar can still match even if they are spelled differently.

To enable stemming, all I had to do was add this to swish.conf:

FuzzyIndexingMode Stemming_en1

I re-indexed the corpus and the build time was almost unchanged:

Without stemming: 1.24 seconds
With stemming:    1.30 seconds

And together with DoubleMetaphone

FuzzyIndexingMode DoubleMetaphone

That build took:

DoubleMetaphone: 1.84 seconds

The benchmarks above were made on 10,000 not 100,000 corpus.

Results

Both indexes worked surprisingly well.

Stemming handled root variations nicely, and DoubleMetaphone did a good job with phonetic searches and spelling-adjacent queries.

Searching for installing found stories containing installed.

Searching for a misspelled or sound-alike term still surfaced the expected document.

Next stop: synonyms.

Day 8: Synonyms

It's day 8 and built myself a tiny CI routine. Because I can only do that many git pulls over ssh. With that out of the way, I moved on to synonyms, which is one of the parts of search engines I find genuinely interesting. Swish-e does not support synonyms out of the box, but I found a few old Internet articles on the subject, and for v1 I settled on what feels like the appropriate option for the era: WordNet.

WordNet is basically a lexical database of English. It groups words into sets of related meanings, so instead of just knowing that “red” is a word, it can tell you that in some contexts it is related to words like “crimson”, “ruby”, and “scarlet”. It is old, free, still available for download, and feels very much in the spirit of this whole experiment. For the first version, I want to keep the implementation fairly basic.

  • I will ignore and, not, or, and anything else under 3 characters.

  • I will also ignore anything inside quotes, or anything using wildcard-style search with ? or *.

Then I will take the remaining words, run them through WordNet, and expand the query into grouped synonym expressions:

(word OR synonym1 OR synonym2)

That should give me a working first pass. This approach already has a couple of obvious problems. The first one is ranking. Synonyms will be treated the same as the original search term. So if someone searches for red, words like scarlet and ruby may be treated as equally important, even though they are only expanded terms. Swish-e will not know that these were synonyms rather than words the user actually typed.

The second problem is meaning. WordNet needs to know which part of speech a word is. For example, if you treat red as a noun, you can get meanings like redness, Marxist, or loss. But if you treat red as an adjective, you get much more useful colour-related synonyms like crimson, ruby, and scarlet.

After a few tries, I got it to work. "Red computer" turned into

Raw output lines: ["# SWISH format: 2.5.8","# Search words: (red or redness or \"chromatic color\" or \"chromatic colour\" or \"spectral color\" or \"spectral colour\" or reddish or ruddy or blood-red or carmine or cerise or cherry or cherry-red or crimson or ruby or ruby-red or scarlet or chromatic) (computer or \"computing machine\" or \"computing device\" or \"data processor\" or \"electronic computer\" or \"information processing system\" or machine)","# Removed stopwords:","# Number of hits: 1","# Search time: 0.001 seconds","# Run time: 0.017 seconds","3\t1000","."]

Looks like god knows what, but I do want v1 to be greedy, just for demo purposes. That would do for now.

Another problem become evident, Swish-e has an issue with plurals: pioneer would not find "pioneers". I have to look into that too.