Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/projects/bookmarks-semantic-search/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "Semantic Bookmark Search",
"position": 20
}
441 changes: 441 additions & 0 deletions docs/projects/bookmarks-semantic-search/index.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/projects/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ They're optional and ungraded. Browse them any time — each project's intro say

<ProjectChooser
projects={mergeProjectMeta([
{
id: 'bookmarks-semantic-search',
title: 'Build Semantic Search Over Your Browser Bookmarks',
summary:
"Parse your browser's bookmarks export, embed every bookmark locally, and find the page you saved months ago by describing what you remember — not by guessing the title.",
},
{
id: '2027-dependency-freshness-checker',
title: 'Build a Dependency-Freshness Checker',
Expand Down
1 change: 1 addition & 0 deletions examples/bookmarks-semantic-search/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GITHUB_TOKEN=your-key-here
6 changes: 6 additions & 0 deletions examples/bookmarks-semantic-search/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
__pycache__/
index.npy
records.json
build/
node_modules/
1 change: 1 addition & 0 deletions examples/bookmarks-semantic-search/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
60 changes: 60 additions & 0 deletions examples/bookmarks-semantic-search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Semantic Bookmark Search Example

[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/abderrahim-lectures/python-data-analysis-course)

The local companion to the course's [Build Semantic Search Over Your Browser Bookmarks](../../docs/projects/bookmarks-semantic-search/index.md) project — parse a standard Netscape bookmarks export, embed every bookmark locally, and search by *meaning* instead of guessing the title.

## What's here

- `parse_bookmarks.py` — parses a Netscape-format bookmarks HTML export (the format Chrome/Firefox/Edge all export) into records of `title`, `url`, and `folder` path, using only the standard library's `html.parser` — [Step 1](../../docs/projects/bookmarks-semantic-search/index.md#step-1-parse-the-export-into-records).
- `build_index.py` — embeds every bookmark title locally with `sentence-transformers` and saves the vectors (`index.npy`) and records (`records.json`) — [Step 2](../../docs/projects/bookmarks-semantic-search/index.md#step-2-build-a-searchable-index).
- `search.py` — `search(query, top_k)`: ranks bookmarks against a natural-language query using NumPy cosine similarity — [Step 3](../../docs/projects/bookmarks-semantic-search/index.md#step-3-search-by-meaning).
- `compare.py` — runs the same query through a simple keyword ranker and the semantic search side by side, so you can see where each wins — [Step 4](../../docs/projects/bookmarks-semantic-search/index.md#step-4-compare-against-keyword-search).
- `sample_bookmarks.html` — a small, realistic bookmarks export (ML, web dev, databases, productivity, news folders) so the pipeline runs out of the box with no setup.
- `notebook.ipynb` — a Colab/Kaggle/Binder-ready notebook that runs the whole pipeline over an embedded sample export. Launch it from the badges on the [lesson page](../../docs/projects/bookmarks-semantic-search/index.md#where-to-run-this), or open it directly: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/abderrahim-lectures/python-data-analysis-course/blob/main/examples/bookmarks-semantic-search/notebook.ipynb) [![Open In Kaggle](https://kaggle.com/static/images/open-in-kaggle.svg)](https://kaggle.com/kernels/welcome?src=https://github.com/abderrahim-lectures/python-data-analysis-course/blob/main/examples/bookmarks-semantic-search/notebook.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/abderrahim-lectures/python-data-analysis-course/main?filepath=examples%2Fbookmarks-semantic-search%2Fnotebook.ipynb)

## Running it

This project needs **no API key and no network access** — everything runs locally:

```bash
uv sync
uv run python parse_bookmarks.py sample_bookmarks.html # parse the export
uv run python build_index.py sample_bookmarks.html # embed locally
uv run python search.py "how do I split data into train and test sets"
uv run python compare.py "scikit learn train test split" # semantic vs keyword
```

`uv` reads `pyproject.toml`/`uv.lock` and creates an isolated environment for this project automatically on first run.

### Using your own bookmarks

1. **Export your bookmarks to HTML** from your browser (Chrome/Edge: `⋮` → Bookmarks → Bookmark manager → `⋮` → Export bookmarks; Firefox: Library → Bookmarks → Import and Backup → Export bookmarks to HTML).
2. Run the same three commands with your export file instead of `sample_bookmarks.html`:
```bash
uv run python parse_bookmarks.py /path/to/your/bookmarks.html
uv run python build_index.py /path/to/your/bookmarks.html
uv run python search.py "the page about the thing I forgot"
```

Save new bookmarks? Re-export the HTML and re-run `build_index.py` — the index is a snapshot, not a live view.

## Running it in GitHub Codespaces

Click the badge above, or go to the [repo's Codespaces page](https://github.com/abderrahim-lectures/python-data-analysis-course), for a ready-to-go cloud dev environment (Node + Python + `uv` preinstalled via [`.devcontainer/devcontainer.json`](../../.devcontainer/devcontainer.json)). Once it's open:

```bash
cd examples/bookmarks-semantic-search
uv run python build_index.py sample_bookmarks.html
uv run python search.py "train test split"
```

Your own bookmarks live on your own machine, so in a Codespace you'll be searching the bundled sample — perfect for trying the pipeline, less useful for finding your own lost pages.

## A note on staying current

Model names and library APIs in this space change fast. `all-MiniLM-L6-v2` was verified working while writing this example, but may have drifted by the time you read it — the embedding call is the only moving part, so if a version error appears, pinning a newer `sentence-transformers` release is usually enough.

## Built your own search tool?

See [`examples/student-projects/`](../student-projects/) for how to share it with the class via a pull request — no git experience required, it walks through every step.
43 changes: 43 additions & 0 deletions examples/bookmarks-semantic-search/build_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Embeds every bookmark from parse_bookmarks.py and saves the vectors +
records locally, so search.py (Step 3) doesn't re-embed at query time.

Run with: uv run python build_index.py bookmarks.html
Re-run this any time you add or edit bookmarks -- the saved index doesn't
update itself.
"""

import json
import sys
from pathlib import Path

import numpy as np
from sentence_transformers import SentenceTransformer

from parse_bookmarks import load_bookmarks

MODEL_NAME = "all-MiniLM-L6-v2"
INDEX_PATH = "index.npy"
RECORDS_PATH = "records.json"


def main() -> None:
records = load_bookmarks(Path(sys.argv[1]))
if not records:
print("No bookmarks parsed -- is this a Netscape-format export?")
return

print(f"Embedding {len(records)} bookmarks with {MODEL_NAME}...")
model = SentenceTransformer(MODEL_NAME)
texts = [r["title"] for r in records]
embeddings = model.encode(texts, normalize_embeddings=True)

np.save(INDEX_PATH, embeddings)
with open(RECORDS_PATH, "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2)

print(f"Saved {embeddings.shape[0]} vectors ({embeddings.shape[1]}-dim) to {INDEX_PATH}")
print(f"Saved bookmark records to {RECORDS_PATH}")


if __name__ == "__main__":
main()
41 changes: 41 additions & 0 deletions examples/bookmarks-semantic-search/compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Compares semantic search against simple keyword search on the same index.

Run with: uv run python compare.py "your query here"
"""

import json
import sys

from search import search


def keyword_search(query: str, records: list[dict], top_k: int = 5) -> list[dict]:
"""Ranks records by how many query words appear in their title, treating
it as the closest thing to what a browser's bookmark search does."""
words = [w.lower() for w in query.split() if len(w) > 2]
scored = []
for record in records:
title_lower = record["title"].lower()
hits = sum(1 for w in words if w in title_lower)
if hits:
scored.append({**record, "score": hits})
scored.sort(key=lambda r: r["score"], reverse=True)
return scored[:top_k]


def main() -> None:
query = " ".join(sys.argv[1:]) or "how do I split data into train and test"
with open("records.json", encoding="utf-8") as f:
records = json.load(f)

print("Keyword search:")
for r in keyword_search(query, records):
print(f" {r['score']} hit(s) [{r['folder']}] {r['title']}")

print("\nSemantic search:")
for r in search(query):
print(f" {r['score']:.3f} [{r['folder']}] {r['title']}")


if __name__ == "__main__":
main()
Loading
Loading