I Compressed 10 Years of Bookmarks Into One Searchable AI Brain

Photo: Unsplash

AI Power User

I Compressed 10 Years of Bookmarks Into One Searchable AI Brain

Eleven thousand saved links became a local semantic archive I can actually search by meaning
semantic searchbookmarkslocal AIknowledge management

I had 11,400 bookmarks. Safari, Chrome, an old Firefox profile, a Pocket export from before Pocket shut down, and an Instapaper account I’d forgotten the password to. Ten years of “I’ll need this later” — and functionally zero ability to find anything. The folders were archaeology: a “Read later” inside “2017” inside “Old imports”. When I actually needed that article about salary negotiation I saved around 2019, I spent twenty minutes failing to find it and gave up.

One weekend on a Mac Studio fixed it. The result is a local, searchable archive where I type “the article about negotiating by naming a range first” and get the right page in two seconds — found by meaning, not by remembering which folder past-me invented. Here’s the whole pipeline.

Step 1: Get everything out

Every source exports to HTML or CSV:

  • Safari: File → Export → Bookmarks → Safari Bookmarks.html
  • Chrome: Bookmark Manager (⌥⌘B) → ⋮ → Export bookmarks
  • Firefox: Bookmarks → Manage → Import and Backup → Export to HTML
  • Pocket/Instapaper: both offered CSV/HTML exports (if you grabbed your Pocket export before the 2025 shutdown, it’s a CSV of URLs and titles)

The bookmark HTML format is ancient Netscape-era markup, which is good news — it’s trivial to parse. A few lines of Python with BeautifulSoup flattens everything into one list:

from bs4 import BeautifulSoup
import glob, csv

links = []
for f in glob.glob("exports/*.html"):
    soup = BeautifulSoup(open(f, encoding="utf-8"), "html.parser")
    for a in soup.find_all("a"):
        links.append((a.get("href"), a.text.strip(),
                      a.get("add_date", "")))

# dedupe by URL
links = list({u: (u, t, d) for u, t, d in links if u}.values())
print(len(links))
with open("all_links.csv", "w") as out:
    csv.writer(out).writerows(links)

After deduplication my 11,400 became 9,800 unique URLs. Already a lesson: I’d re-bookmarked the same pages across browsers for years.

Before archiving anything, find out what still exists. This script checks every URL with reasonable politeness:

import csv, concurrent.futures, requests

def check(url):
    try:
        r = requests.head(url, timeout=10, allow_redirects=True,
                          headers={"User-Agent": "Mozilla/5.0"})
        if r.status_code >= 400:  # some servers reject HEAD
            r = requests.get(url, timeout=10, stream=True,
                             headers={"User-Agent": "Mozilla/5.0"})
        return url, r.status_code
    except Exception:
        return url, 0

urls = [row[0] for row in csv.reader(open("all_links.csv"))]
with concurrent.futures.ThreadPoolExecutor(20) as ex:
    results = list(ex.map(check, urls))

dead = [u for u, s in results if s == 0 or s >= 400]
print(f"{len(dead)}/{len(urls)} dead")

My result: 3,420 of 9,800 dead — 35%. That’s right in line with link-rot research (Harvard’s studies found ~38% of links from a decade ago broken, and Pew measured 38% of 2013-era pages gone by 2023). A third of what I “saved” over ten years no longer exists. Some came back via the Wayback Machine (https://web.archive.org/web/2019/<url>), but the lesson burned in: a bookmark is a pointer, not a copy, and pointers rot. Everything from here on fixes that.

Step 3: Archive the survivors as single files

For the ~6,400 live links, I fetched actual content. The tool that makes this painless is monolith — it saves a page as one self-contained HTML file with images and CSS inlined:

brew install monolith
monolith "https://example.com/article" -o archive/example-article.html

Wrapped in a loop with a 2-second sleep and a retry pass, the full crawl ran overnight (about nine hours) and produced 14 GB of HTML. For the embedding step you also want clean text, so I ran each file through trafilatura, which strips navigation and ads and outputs readable plaintext:

pip install trafilatura
trafilatura -i filelist.txt --output-dir text/ --no-comments

Roughly 800 pages were paywalls, login walls, or JavaScript husks with no extractable text — accepted losses. Final corpus: ~5,600 readable articles, 310 MB of plaintext. Ten years of reading interests, finally in one folder.

This is where it becomes more than a backup. The easy path: AnythingLLM (free, native Mac app). Create a workspace, point it at the text/ folder, pick a local embedding model, and let it index — about 40 minutes for my corpus on an M2 Ultra, entirely offline. Then you just ask: “that article about negotiation where you should name a range, not a number” — and it returned the 2019 piece I’d failed to find manually, plus two related ones I’d forgotten I saved.

If you’d rather own the pipeline, it’s ~30 lines with a local embedding model:

import ollama, glob, numpy as np

docs, vecs = [], []
for f in glob.glob("text/*.txt"):
    body = open(f).read()[:8000]
    e = ollama.embed(model="nomic-embed-text", input=body)
    docs.append(f); vecs.append(e["embeddings"][0])
V = np.array(vecs)
np.save("vecs.npy", V)

def search(q, k=5):
    qv = np.array(ollama.embed(model="nomic-embed-text",
                               input=q)["embeddings"][0])
    sims = V @ qv / (np.linalg.norm(V, axis=1) * np.linalg.norm(qv))
    return [docs[i] for i in sims.argsort()[::-1][:k]]

nomic-embed-text via Ollama embeds the whole corpus in under 20 minutes and handles my mixed Czech/English archive surprisingly well — I can query in Czech and find English articles, because good embedding models map both languages into the same meaning-space. For a decade of bilingual bookmarking, that’s the killer feature: no cloud translation API ever sees what I read.

The insight: stop organizing, start indexing

Here’s what the project actually taught me. Those ten years of folder taxonomies — “Dev/JS/Performance”, “Career”, “Recipes maybe” — contributed nothing to the final system. Semantic search ignores them and wins anyway, because folders encode what a link was about to you on the day you saved it, while embeddings encode what it’s about, period. The article I needed was filed under “Work stuff 2019”. Meaning-search found it from the word “negotiation,” which appears nowhere in my folder names.

So the organizational advice is liberating: stop curating, start capturing. Every minute spent deciding which folder a bookmark belongs in is wasted; every page whose content you archive is searchable forever, by any future question, in any language you think in.

Maintenance: the monthly habit that doesn’t rot

The system stays alive with one small loop. New saves go to a single flat inbox.txt of URLs — a Shortcut on Mac and iPhone appends the current tab with one tap, zero filing decisions. Then a monthly cron-style ritual (first Sunday, ~10 minutes of attention):

monolith-batch inbox.txt archive/   # fetch full pages
trafilatura -i new.txt --output-dir text/
python embed_new.py                  # append to the index
> inbox.txt

Because pages are archived within weeks of saving instead of years, the 35% rot rate drops to near zero — I capture content while it’s alive. The Mac Studio does the work; I just keep the habit.

Total cost of the whole project: zero software dollars, one weekend, and 15 GB of disk. What I got back is the thing bookmarks always promised and never delivered — a memory that actually remembers. If you’ve got a decade of “I’ll need this later” rotting in browser menus, this is your sign: a third of it is already gone, and the clock is running on the rest.

Get the next live webinar in your inbox

One email a month: the upcoming live event + free recording access for subscribers. No spam, unsubscribe anytime.