Building search with Server-Side Swift
21 Aug 2022
Recently I migrated all the sites I run (this one, my personal site, and the one pertaining to my guitar building) from their various disparate homes to all be hosted in the same way in the same place and in the same way. This was in part to make my life easier with respect to managing all three as they were, but also so I could have fun trying out some new ideas.
One of these was my little weekend project: writing a small search engine for all three sites as a way to play with using Swift on the server. The project is called Guilty Spark (after the librarian character in the Halo videogames), and you can find the source here. It’s a very naive search engine as these things go, but that’s not really the point, it was just an excuse to learn some new things, both about information retrieval, but mostly about using Swift both as a server language and how it fairs being hosted on Linux.
Normally I’d use Go for projects like this, but given I’d been chatting a lot with my friend Jason of mine about Swift language intricacies of late, it seemed a fun excuse to give Swift a try in this context. It’s also the kind of project that suits building something rudimentary as the first deployed version, and then slowly enhancing over time (another behaviour encouraged by discussion with Jason over projects). This weekend I got the indexer and basic service working just processing titles and tags on articles, and having a very minimal UI in terms of displaying results. But now that I’ve managed to get that beachhead working, I can slowly make it more feature rich at my leisure as free time allows. It’s a nice way to work when the project allows it.
This isn’t the first time I’ve tried kicking the tyres on server-side Swift, but in the past when following the regular tutorials, I was immediately put off by the reliance on heavy-weight frameworks like Vapor. Whilst I’m sure these are very good at what they do, one of the things I like about Golang is that for experimentation and exploration you can bootstrap a new service with just a single file of code - you can feel that the language was aimed to support this use-case. Heavy-weight frameworks like Django (and I assume Vapor) are absolutely the right choice if you’re setting out to build something bigger, but I like to start small, and reading the tutorials for Swift it felt like this wasn’t what people do; and thus I’ve stuck to Go in general for this sort of thing.
But, this time I decided to not look for tutorials on server-side Swift, but instead started with the network frameworks: I knew that under the hood most of the new server-side Swift frameworks are using SwiftNIO, a lightweight, event driven networking framework (partly written by another friend and former colleague). I found a nice tutorial series by Helge Heß on making a small HTTP server using SwiftNIO, which I can recommend if you want to understand how it works in this context. What’s even better is that at the end of the tutorial, just when you’re thinking “right, now I just need to wrap all this up into a small minimal library I can use”, it turns out Helge has already done that for you!
As per the README for MicroExpress, Helge’s library makes it very easy to write very little code to start responding to web requests:
import MicroExpress
let app = Express()
app.get("/moo") { req, res, next in
res.send("Muhhh")
}
app.get("/json") { _, res, _ in
res.json([ "a": 42, "b": 1337 ])
}
app.get("/") { _, res, _ in
res.send("Homepage")
}
app.listen(1337)
But it supports JSON response, query parsing, and adding your own middleware out the box. I don’t need folders for app code, and route code, etc., I now have my one file starter for my next idea. I guess technically two, as you do need a Package.swift to download the library, but I needed go.mod too, so I feel I have friction parity between Swift and Go at least.
And whilst I do recommend reading Helge’s tutorial to understand what it’s doing under the hood and how SwiftNIO is working, you don’t need to do that just to use this simple framework.
So, having found a nice starting place for my server-side Swift noodling, how does it compare to getting things running on my server versus Go?
I was a bit worried about the Linux side support. Whilst Swift is supported on a [small number of Linux distributions], they didn’t happen to support the one that I use, Debian, which feels like a slightly odd omission. However, I did find instructions on how to get it running on Debian which involved building my own version of Python so that it matches Ubuntu and then using the Ubuntu version.
One of the really nice things that’s happened to Swift in the last few years has been the introduction of Swift Package Manager which means that there is now one “true” way to support libraries and it feels a lot like how simple it to use libraries with Go. The one think I have noted is that many people making packages don’t test them on Linux, which isn’t surprising, but for my project here I pulled in a few libraries, such as one to parse YML and another to do word stemming, neither of which advertised them selves as having been tested on Linux but worked just fine.
I didn’t really push this project in the direction of explicit parallelism or concurrency, one of Go’s strong points, so I can’t yet compare them on that, but it was lovely to be reminded of how Swift’s functional like data manipulation using map (and it’s variations compactMap and flatMap), reduce, and how you can compose them in a single statement really suits how I think of problems. I guess it’s also why I like Swift’s Combine library too, which is the same patterns but for data streams rather than static collections.
public static func tokeniseString(_ query: String) -> Set<String> {
let seperators = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters)
return Set(
query.components(separatedBy: seperators)
.filter{$0.count > 0}
.map{normaliseString($0)}
)
}
I appreciate this style of writing code isn’t to everyone’s tastes, but like I say, it works well in my head for whatever reason. After having to write a lot of python and typescript of late for work, it was nice to come back to my happy place for this kind of data processing code.
Thus, just a few hours per day over the weekend I now have a service running on my Linux VPS written in Swift that feels like a nice language to use for this kind of problem, and at a project scale that suits this sort of fun little exploration (and that includes having test coverage!). It’s certainly something I’ll continue to tinker on, and I’d happily use server-side Swift in the future based on this little exploration.
I’m not sure that I’d yet recommend it to clients over Go when I’m asked to write backend services, but perhaps I would if they were a heavily iOS or macOS shop already, so they had in house Swift expertise. Go just feels like it’s easier to know that when I finish my part with a client contract they’re going to be able to support it long term without trying to find the few people who write Swift in this context and Go definitely has the python-like advantage of a strong library community that we know will work on Linux (the common hosting platform for such projects). But outside those concerns, I’d definitely recommend giving Swift a try.