Every modern web app eventually hits the moment it needs search. And almost universally, the playbook looks the same:
pip install elasticsearch whoosh fastapi uvicorn nltk click
A few minutes later you have an 80MB virtual environment, a daemon process running somewhere, network socket overhead, and a software supply chain that just grew five links longer — all to search a few hundred markdown files or API docs.
For the Zero Dependency Hackathon 2026 (Track F: Wildcard), I set out to prove something simpler: the Python 3.14 standard library already has everything you need to build a fast, local, embeddable search engine from scratch.
The result is SwiftSearch — no pip installs, no lockfiles, no external daemons. Just python -m swiftsearch serve and it runs.
Here's what it actually took to replace the standard stack, the stdlib corner that saved the weekend, and the quirks the docs conveniently don't mention.
TL;DR
| Normally you'd reach for... | SwiftSearch uses... |
|---|---|
| Elasticsearch / Whoosh |
collections.defaultdict inverted index |
| nltk (tokenization) | unicodedata.normalize() |
| FastAPI + uvicorn | http.server |
| BM25 ranking config | A 4-term linear scoring formula |
requirements.txt |
Nothing. It's empty. |
1. Replacing Whoosh & Elasticsearch: The Inverted Index
Under the hood, full-text retrieval boils down to two primitives: tokenizing text and building an inverted index. That's it — the rest is optimization.
Instead of pulling in a dedicated indexing library, the core engine runs entirely on collections.defaultdict:
from collections import defaultdict
from dataclasses import dataclass
@dataclass
class Posting:
doc_id: str
term_frequency: int
class InvertedIndex:
def __init__(self):
# term -> list of Postings
self.index = defaultdict(list)
self.documents = {}
self.doc_frequency = defaultdict(int)
By keeping postings and document frequencies in memory, a query like python backend never scans every document sequentially. The engine jumps straight to the relevant postings lists and computes the union/intersection in sub-millisecond time.
Transparent ranking beats a black box
Rather than wiring up BM25 configuration knobs, SwiftSearch scores results with a formula you can read in one line:
score = (title_match × 5) + (exact_match × 4) + (content_match × 2) + frequency
No tuning a k1 or b parameter you don't fully understand. A term in the title will always outrank the same term buried on page four — and you can explain why to anyone who asks.
2. The Stdlib Corner That Saved the Weekend: unicodedata
The usual excuse for reaching for nltk or a heavy regex tokenizer library is Unicode handling — accents, umlauts, non-ASCII input that breaks naive string slicing.
Turns out unicodedata, sitting quietly in the standard library, handles all of it:
import unicodedata
def tokenize(text: str) -> list[str]:
# Normalize to canonical composed form
normalized = unicodedata.normalize('NFC', text).lower()
tokens = []
current = []
for char in normalized:
if char.isalnum():
current.append(char)
else:
if current:
tokens.append("".join(current))
current = []
if current:
tokens.append("".join(current))
return tokens
Twenty lines. No compiled regex, no backtracking overhead, no external dependency — and it splits cleanly on punctuation while staying Unicode-safe.
3. What Turned Out Harder Than the Docs Made It Look
http.server has sharp edges
Everyone reaches for FastAPI or Flask when they think "Python web API." http.server has a reputation for being "not for production" — and building an embeddable engine on top of it surfaced exactly why, in two specific ways.
Dual routing (API + static assets). SimpleHTTPRequestHandler serves files; BaseHTTPRequestHandler handles custom routes. Getting GET /search?q=... to return raw JSON while GET / serves the command-palette demo — without tripping CORS — meant manually intercepting paths and setting headers by hand:
if self.path == "/" or self.path == "/demo.html":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
with open("web/demo.html", "rb") as f:
self.wfile.write(f.read())
The shortcut collision. In the frontend, pressing / was supposed to focus the search palette — a common command-palette pattern. But browsers fire keydown before the input gains focus, so the literal / character leaked straight into the query string. Every shortcut press sent a request like /search?q=%2Fauth. The fix was one line — e.preventDefault() on the global listener — but finding it wasn't.
4. The Zero-Dependency Receipt
At submission time, the dependency check was one command:
# Verify no external imports exist across the codebase
grep -rE "^import |^from " swiftsearch/ | sort -u
Every import resolved to a Python 3.14 built-in: argparse, collections, dataclasses, html, http.server, json, pathlib, time, unicodedata, urllib.parse.
No virtualenv. No pip install. No lockfile to audit for vulnerabilities. Clone the repo, run python -m swiftsearch serve, and it's already searching.
Key Takeaway
We reach for packages out of habit more often than out of necessity. Building SwiftSearch was a reminder that the standard library in a modern runtime is more capable than most of us give it credit for — and that stripping away the abstraction layers to build an inverted index from first principles isn't just feasible, it's genuinely satisfying.
If you've replaced a "you obviously need a library for that" tool with nothing but the standard library, I'd like to hear about it in the comments.
This article was originally published by DEV Community and written by Rushikesh.
Read original article on DEV Community