DEV Community

I Built a Search Engine With Zero Dependencies - Just Python 3.14's Standard Library

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. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.