Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e84146c
Find all locations of web.ctx.site.get() and whether they are called …
Winterhuli May 7, 2026
0910ea0
replace per-loan site.get with site.get_many
Winterhuli May 8, 2026
8036a9b
fix(api): replace list comprehension web.ctx.site.get(key) with get_m…
WeiGuang-2099 May 9, 2026
c4a1a99
Merge pull request #1 from WeiGuang-2099/Cheng
Winterhuli May 11, 2026
bb14d0b
Delete N+1 queries.txt
Winterhuli May 11, 2026
0af2fef
Merge pull request #2 from WeiGuang-2099/yuheng
Winterhuli May 11, 2026
ef3d073
Batch load works in get_observations using get_many
Hussain5001 May 12, 2026
11fed38
Merge pull request #3 from WeiGuang-2099/hussain
WeiGuang-2099 May 13, 2026
3dd00c0
Batch note page lookups in mybooks
MuchiniGun May 15, 2026
6c42073
Merge pull request #4 from WeiGuang-2099/owen
Hussain5001 May 15, 2026
b97ff2e
Merge branch 'master' into finalsubmit
WeiGuang-2099 May 16, 2026
c0478d9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2026
53172e8
Update openlibrary/plugins/openlibrary/api.py
WeiGuang-2099 May 19, 2026
7e2dbd8
Implemented the mybooks suggestions:
MuchiniGun May 21, 2026
6d96026
Updating the remaining changes
MuchiniGun May 21, 2026
8286b6a
fix: avoid relying on get_many result order in mybooks
Winterhuli May 22, 2026
a4807d6
Avoid N+1 work fetches in bulk tag and reading log stats
WeiGuang-2099 May 23, 2026
22e2a59
Merge pull request #7 from WeiGuang-2099/yuheng
WeiGuang-2099 May 23, 2026
54ae068
Merge branch 'master' into finalsubmit
WeiGuang-2099 May 23, 2026
17a9045
Merge branch 'finalsubmit' into Cheng
WeiGuang-2099 May 23, 2026
a457177
Merge pull request #8 from WeiGuang-2099/Cheng
WeiGuang-2099 May 23, 2026
85822dc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2026
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
3 changes: 1 addition & 2 deletions openlibrary/plugins/openlibrary/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,7 @@ def POST(self):
if not edition_keys:
raise web.HTTPError("404 Not Found", {"Content-Type": "application/json"})

editions = [web.ctx.site.get(key) for key in edition_keys]
logger.info(f"Disassociating {ocaid} from the following editions: {', '.join(edition_keys)}")
editions = web.ctx.site.get_many(edition_keys)

# Update records
try:
Expand Down
7 changes: 6 additions & 1 deletion openlibrary/plugins/openlibrary/bulk_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ def POST(self):
tags_to_add = json.loads(i.tags_to_add or "{}")
tags_to_remove = json.loads(i.tags_to_remove or "{}")

work_keys = [f"/works/{work}" for work in works]
work_docs = {doc.key: doc for doc in web.ctx.site.get_many(work_keys) if doc}

docs_to_update = []
# Number of tags added per work:
docs_adding = 0
# Number of tags removed per work:
docs_removing = 0

for work in works:
w = web.ctx.site.get(f"/works/{work}")
w = work_docs.get(f"/works/{work}")
if not w:
continue

current_subjects = {
# XXX : Should an empty list be the default for these?
Expand Down
69 changes: 51 additions & 18 deletions openlibrary/plugins/upstream/mybooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,26 @@ def render_template(self, mb: "MyBooksTemplate") -> TemplateResult:
if mb.me:
myloans = get_loans_of_user(mb.me.key)
loans = web.Storage({"docs": [], "total_results": len(myloans)})
# TODO: should do in one web.ctx.get_many fetch

# Collect all book keys first so we can batch-load them in one call.
book_keys = [loan["book"] for loan in myloans]

# Batch fetch all books instead of calling site.get(...) inside the loop.
books = web.ctx.site.get_many(book_keys)

# Build a mapping from book key -> book object.
# This avoids relying on get_many() returning results in the same order
# as the original book_keys / myloans list.
books_by_key = {book.key: book for book in books if book}

for loan in myloans:
# Book will be None if no OL edition exists for the book
if book := web.ctx.site.get(loan["book"]):
# Book will be None if no OL edition exists for the book.
# Match each loan to its book using the book key, not list position.

if book := books_by_key.get(loan["book"]):
book.loan = loan
loans.docs.append(book)

docs["loans"] = loans

if mb.me or mb.is_public:
Expand Down Expand Up @@ -575,38 +589,57 @@ def __init__(self, user: User) -> None:

def get_notes(self, limit: int = RESULTS_PER_PAGE, page: int = 1) -> list:
notes = Booknotes.get_notes_grouped_by_work(self.username, limit=limit, page=page)

for entry in notes:
entry["work_key"] = f"/works/OL{entry['work_id']}W"
entry["work"] = self._get_work(entry["work_key"])
entry["work_details"] = self._get_work_details(entry["work"])
if not notes:
return notes
Comment on lines +592 to +593
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial idea was just to avoid making empty get_many([]) calls when there are no notes, but I see it's unnecessary since the normal batch flow handles empty notes. I will remove the guard.


work_keys = [f"/works/OL{entry['work_id']}W" for entry in notes]
works = web.ctx.site.get_many(work_keys)
works_by_key = {work.key: work for work in works}
author_keys = list(dict.fromkeys(a.author.key for work_key in work_keys for a in works_by_key[work_key].get("authors", [])))
authors_by_key = {author.key: author for author in web.ctx.site.get_many(author_keys)} if author_keys else {}
work_details_by_key = {work_key: self._get_work_details(works_by_key[work_key], authors_by_key=authors_by_key) for work_key in work_keys}

edition_ids = list(dict.fromkeys(note["edition_id"] for entry in notes for note in entry["notes"] if note["edition_id"] != Booknotes.NULL_EDITION_VALUE))
edition_keys_by_id = {edition_id: f"/books/OL{edition_id}M" for edition_id in edition_ids}
editions_by_key = {edition.key: edition for edition in web.ctx.site.get_many(list(edition_keys_by_id.values()))} if edition_keys_by_id else {}

for entry, work_key in zip(notes, work_keys):
entry["work_key"] = work_key
entry["work"] = works_by_key[work_key]
entry["work_details"] = work_details_by_key[work_key]
entry["notes"] = {i["edition_id"]: i["notes"] for i in entry["notes"]}
entry["editions"] = {k: web.ctx.site.get(f"/books/OL{k}M") for k in entry["notes"] if k != Booknotes.NULL_EDITION_VALUE}
entry["editions"] = {k: editions_by_key[edition_keys_by_id[k]] for k in entry["notes"] if k != Booknotes.NULL_EDITION_VALUE}
return notes

def get_observations(self, limit: int = RESULTS_PER_PAGE, page: int = 1) -> list:
observations = Observations.get_observations_grouped_by_work(self.username, limit=limit, page=page)

for entry in observations:
entry["work_key"] = f"/works/OL{entry['work_id']}W"
entry["work"] = self._get_work(entry["work_key"])
entry["work_details"] = self._get_work_details(entry["work"])
work_keys = [f"/works/OL{entry['work_id']}W" for entry in observations]
works = web.ctx.site.get_many(work_keys)
works_by_key = {work.key: work for work in works}

for entry, work_key in zip(observations, work_keys):
work = works_by_key[work_key]
entry["work_key"] = work_key
entry["work"] = work
entry["work_details"] = self._get_work_details(work)

ids = {}
for item in entry["observations"]:
ids[item["observation_type"]] = item["observation_values"]
entry["observations"] = convert_observation_ids(ids)
return observations

def _get_work(self, work_key: str) -> "Work | None":
return web.ctx.site.get(work_key)
return observations

def _get_work_details(self, work: "Work") -> dict[str, list[str] | str | int | None]:
def _get_work_details(self, work: "Work", authors_by_key: dict[str, Any] | None = None) -> dict[str, list[str] | str | int | None]:
author_keys = [a.author.key for a in work.get("authors", [])]
if authors_by_key is None:
authors_by_key = {a.key: a for a in web.ctx.site.get_many(author_keys)}

return {
"cover_url": (work.get_cover_url("S") or "https://openlibrary.org/static/images/icons/avatar_book-sm.png"),
"title": work.get("title"),
"authors": [a.name for a in web.ctx.site.get_many(author_keys)],
"authors": [authors_by_key[key].name for key in author_keys if key in authors_by_key],
"first_publish_year": work.first_publish_year or None,
}

Expand Down
14 changes: 8 additions & 6 deletions openlibrary/views/loanstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,18 @@ def GET(self):

stats = get_cached_reading_log_stats(limit)

solr_docs = get_solr_works({f"/works/OL{item['work_id']}W" for leaderboard in stats["leaderboard"].values() for item in leaderboard})
all_keys = {f"/works/OL{item['work_id']}W" for leaderboard in stats["leaderboard"].values() for item in leaderboard}
solr_docs = get_solr_works(all_keys)

# Fetch works from solr and inject into leaderboard
# Batch-fetch any works missing from Solr
missed_keys = all_keys - set(solr_docs.keys())
missed_docs = {doc.key: doc for doc in web.ctx.site.get_many(list(missed_keys)) if doc} if missed_keys else {}

# Inject works into leaderboard
for leaderboard in stats["leaderboard"].values():
for item in leaderboard:
key = f"/works/OL{item['work_id']}W"
if key in solr_docs:
item["work"] = solr_docs[key]
else:
item["work"] = web.ctx.site.get(key)
item["work"] = solr_docs.get(key) or missed_docs.get(key)

works = [item["work"] for leaderboard in stats["leaderboard"].values() for item in leaderboard]

Expand Down