8000 Cut crawler memory by moving wiki scores out of Python dicts by daoudclarke · Pull Request #580 · mwmbl/mwmbl · GitHub
[go: up one dir, main page]

Skip to content

Cut crawler memory by moving wiki scores out of Python dicts - #580

Merged
daoudclarke merged 2 commits into
mainfrom
claude/elegant-wozniak-zsspln
Oct 31, 2026
Merged

daoudclarke merged 2 commits into
mainfrom
claude/elegant-wozniak-zsspln

Conversation

@daoudclarke
Copy link
Copy Markdown
Contributor

First step towards a crawler that fits on small machines (e.g. a Raspberry Pi).

Why

At import, mwmbl/tinysearchengine/rank.py loaded wiki_stats.json (14.6MB) and document_counts.json (14MB) into Python dicts. That cost about 150MB in every crawler process, and the crawler runs several: main, 2 crawl workers, the indexer and the blacklist-snapshot process. Because of reference counting, forked processes gradually copy those pages. It was the biggest single cost. Redis, the memory-mapped index and the bloom filters cost far less.

Changes

  • document_counts.json: rank.py no longer loads it. Its only Python user was get_tf_idf_features, whose one call was commented out, so the function is deleted.
  • wiki_stats.json: get_wiki_score now calls mwmbl_rank.wiki_score. A new mwmbl_rank/build.rs turns the JSON at build time into a sorted table embedded with include_bytes!. wiki.rs binary-searches it, so it needs no heap, all processes share it, and the OS can drop it from memory when short. This also replaces the Rust HashMap the server's ranking model used, which cost about 60MB once loaded.
  • CRAWLER_VERSION is bumped to 1.0.4.
  • New test test/test_wiki_score.py: scores match count / max_count from the JSON exactly, including non-ASCII titles.

Measurements

Python 3.11, mwmbl.settings_crawler, measured after scoring documents:

Before After
One process, RSS 290MB 147MB
Memory added by importing rank and scoring +196MB +46MB
Main process + 3 forked processes, PSS (real memory once shared pages are split) 473MB 331MB
Wiki score lookup ~230–600ns ~170ns

The fork saving is smaller than the single-process one because forked processes already shared some of those pages.

Testing

  • make check passes.
  • Full pytest suite: 1,739 passed, 10 skipped. test_wiki_score.py failed in that run on titles containing /, which a URL's last segment can never match (true before this change too). The test now skips those titles, and it passes with test_rust_features.py.
  • cargo test wiki passes (4 tests), including a check that every key in the table is found.

Local pitfall: uv run (including inside make check) can reinstall an old cached build of mwmbl_rank over a fresh maturin develop. Rebuild with uv run --no-sync maturin develop and run the tests with --no-sync. CI builds from scratch, so it isn't affected.

Next steps (not in this PR)

  1. Avoid the ~40MB still added by importing rank, mostly pandas and completer.py loading its CSV.
  2. A single-process mode for small machines, with crawling and indexing on threads.
  3. Skip heavy imports the crawler doesn't need: boto3, and Django ORM setup when HAS_DATABASE=False.
  4. Replace the crawler's Redis with SQLite, so volunteers don't need to run a separate daemon.

Generated by Claude Code

rank.py loaded wiki_stats.json and document_counts.json into dicts at import, about 150MB
in every crawler process. The document counts were only used by get_tf_idf_features,
which nothing calls, so they are gone. The wiki scores now come from mwmbl_rank, where
build.rs turns the JSON into a sorted table embedded in the library's read-only data:
no heap, shared between processes, and a lookup is a binary search (~170ns).

One process after scoring a document: 290MB RSS before, 147MB after. Main process plus
three forked workers: 473MB PSS before, 331MB after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTbu4o4Ebr1WH6ifqqfqNp
…docs

- build.rs asserts the counts have a positive max and checks the u32 casts, instead of
  silently falling back to zero scores or truncating.
- The serde_json build-dependency gets float_roundtrip, like the runtime one.
- idf.rs was never called (TF-IDF features are not in the model), so it goes too.
- Dockerfile comment and analyse/wiki_stats.py now say the table is built by build.rs,
  and that mwmbl_rank needs rebuilding after regenerating the stats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTbu4o4Ebr1WH6ifqqfqNp
@codecov
codecov Bot commented Oct 31, 2026
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@daoudclarke
daoudclarke merged commit 86376eb into main Oct 31, 2026
7 checks passed
@daoudclarke
daoudclarke deleted the claude/elegant-wozniak-zsspln branch October 31, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0