Skip to content

Prototype: Add Nearby Books carousel Lit component (#13159) - #13526

Merged
lokesh merged 16 commits into
internetarchive:masterfrom
Sravan1011:nearby-books-prototype
Oct 5, 2026
Merged

lokesh merged 16 commits into
internetarchive:masterfrom
Sravan1011:nearby-books-prototype

Conversation

@Sravan1011

@Sravan1011 Sravan1011 commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR implements a "Nearby Books" carousel, addressing issue #13159.

The feature simulates the digital equivalent of physical shelf-browsing ("shelf adjacency"): when viewing a work's page, it shows other works with numerically/lexically close Dewey Decimal Classification (ddc_sort) values, filtered by the edition's language.

Key Implementation Details

Neighbour lookup (openlibrary/plugins/openlibrary/partials.py)

gather_nearby_books_async(work_key, language, limit):

  • Anchors on the work's indexed ddc_sort (fetched from Solr via solr.get_async), not a DDC recomputed from whichever single edition happens to be viewed -- ddc_sort is picked as the longest DDC string across all of a work's editions (see work.py), so the shelf position needs to come from that same value or the browsed position won't match the shelf's own sort order.
  • Skips the carousel if the work has no ddc_sort, or it isn't numeric (e.g. [Fic], [E]) -- those sort lexically after every number and would otherwise show up as false neighbours.
  • Runs three Solr queries concurrently: a few exact matches at ddc_sort, plus strict/exclusive ({/}) ranges below and above it. The exclusive bounds keep a popular ddc_sort value (tens of thousands of works can share one) from filling both the "before" and "after" buckets with ties on itself.
  • Both ranges are capped to numeric ddc_sort values, filtered by type:work, the edition's language, the standard safe-mode filter (-subject:"content_warning:cover"), and a fields allowlist.

Lazy-loaded FastAPI partial (openlibrary/fastapi/partials.py, modeled on lazy_carousel_partial)

GET /partials/NearbyBooks.json?work_key=...&language=... fetches the neighbours and renders them through the existing macros.RawQueryCarousel(lazy=False, books_data=...) -- the same primitive books/custom_carousel.html carousel used elsewhere, so safe-mode, the analytics key, and $_() translation all come for free. Returns {"partials": ""} when there's nothing to show.

This work is not done in prepare_book_page/on the book-page request path, since that runs before every book page renders (our busiest page) and would add several synchronous Solr calls to it. Instead it's fetched asynchronously after the page loads.

Template integration (openlibrary/templates/type/edition/view.html, openlibrary/macros/NearbyBooksLoadingIndicator.html)

Renders a lightweight placeholder (work key + the edition's language, no Solr call needed) below RelatedWorksCarousel. A small JS module (openlibrary/plugins/openlibrary/js/nearby-books.js, mirroring lazy-carousel.js) fetches the real carousel HTML from the partial once the placeholder scrolls into view and swaps it in, with a retry affordance on failure.

RawQueryCarousel.html / custom_carousel.html fix

RawQueryCarousel previously always built a title-link/load-more config from query, even when a caller passes pre-fetched books_data with no query of its own (our case). Fixed so that when there's no real query behind the carousel, the title renders as plain (non-link) text and load-more is omitted, instead of both pointing at an unrelated, empty search.

Screenshot

Nearby Books carousel on an edition page in local dev, showing real neighbouring works ordered by ddc_sort around 818.402 (Mark Twain):

Nearby Books carousel

Testing & Verification

  • ruff check / ruff format --diff: clean on all changed Python files.
  • npx eslint: clean on the new/changed JS.
  • npx stylelint: clean on the changed CSS.
  • New/updated unit tests in openlibrary/plugins/openlibrary/tests/test_partials.py cover: no ddc_sort -> empty, non-numeric ddc_sort -> empty, and the anchor/strict-bounds query shape.
  • Verified locally against the dev Solr index: /partials/NearbyBooks.json returns real neighbours anchored on the work's ddc_sort, and the carousel renders on the edition page (screenshot above).

@Sravan1011
Sravan1011 force-pushed the nearby-books-prototype branch from e4a16d7 to 2fa6561 Compare September 4, 2026 04:15
@Sravan1011

Copy link
Copy Markdown
Contributor Author

@lokesh @RayBB can anyone review this pr

@RayBB
RayBB requested a review from lokesh September 6, 2026 06:41
@github-actions github-actions Bot added the Needs: Response Issues which require feedback from lead label Sep 6, 2026
@Sravan1011

Sravan1011 commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor Author

@RayBB, @lokesh could you let openlibrary-bot review

@Sravan1011

Copy link
Copy Markdown
Contributor Author

@lokesh could you review this

@lokesh

lokesh commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@Sravan1011 Thanks for the writeup. Directionally this is a good idea, but I want to redirect the effort before you build more.

On the component. I'd push back on the original issue's lean toward creating a new Lit component. We have a primitive UI ol-carousel close to completion that will be available for use in the near future, and we can port this and other carousels at that time.

For now we should go through macros.QueryCarousel / books/custom_carousel.html instead. You get safe_mode, lazy loading, CacheableMacro, the analytics key, $_() on the strings and the real card for free. RawQueryCarousel.html already takes a books_data param for pre-fetched docs (plugins/openlibrary/partials.py:514 is the precedent), so the shape I'd want is a Python helper that owns the neighbour query, feeding the existing carousel. That deletes almost all 289 lines of OlNearbyBooks.js.

Which leaves the part actually worth prototyping. The neighbour logic needs work.

Issues

1. Wrong anchor — survives the switch

A DDC number is a shelf position. For "show me what's next to this book" to mean anything, two numbers have to agree: the spot you're standing on, and the number the shelf is sorted by.

The shelf is sorted by Solr's ddc_sort. That value lives on the work, and it's picked by gathering the DDCs of every edition of that work and taking the longest string (work.py:492-503).

The PR ignores that and computes its own number from the one edition you happen to be viewing.

So take a work whose editions carry 813, 813.52, and 813.52 F. Solr files that work on the shelf at 813.52 F. You open the edition catalogued as plain 813, and the PR asks "what sits either side of 813?" It gets a real answer, just not this book's neighbours. You're standing in the wrong aisle.

The fix is to stop computing anything: read the work's indexed ddc_sort straight from Solr and anchor on that. Then the anchor is on the same axis as the sort by definition, and normalize_ddc / choose_sorting_ddc leave the request path entirely.

2. Inclusive bounds — survives the switch

The two queries are "everything up to and including X" and "everything from X onward." X is a member of both.

If X is a rare classification with three works, nobody notices. But 813.54 (20th-century American fiction) has tens of thousands of works sitting at exactly that value. So:

  • before query: sort descending, take 10 → ten works, all at exactly 813.54
  • after query: sort ascending, take 10 → ten works, all at exactly 813.54

Neither query ever escapes the bucket. You never see 813.53 or 813.6. And because Solr breaks ties by internal doc id, reversing the descending list doesn't line it up with the ascending one, so the two sets are different arbitrary tens. Dedupe by key doesn't merge them. Net result: twenty essentially random books that share one number, presented as a shelf.

It looks like it's working, which is exactly why it needs catching now. And it fails hardest on the classifications the most books have.

The fix has two parts, and the second is a design decision rather than a bug fix:

  1. Make the bounds strict: strictly below X, strictly above X. decrementStringSolr (LibraryExplorer/utils.js:69) already computes "the largest string just below this one" for this purpose, and Prototype: "Nearby Books" carousel — DDC shelf-adjacency browsing (Lit component) #13159 pointed at it.
  2. Then decide what to do with the exact-X matches. Those are literally the books shelved right against this one, arguably the most nearby, so excluding them outright throws away the best results. Something like "a few at exactly X, then fan out" is probably right, but it should be a deliberate choice rather than a side effect.

--

Let me know if you have any questions on this feedback. Once we work through it, I'll get his up on a testing server so we can see what it looks like with real data.

Thanks!

@lokesh lokesh added Lead: @lokesh Issues pertaining to front-end design system [css, js, components] Type: Feature Request Issue describes a feature or enhancement we'd like to implement. [managed] Module: Carousels Module: JavaScript Issues related to the JavaScript functionality. [managed] Module: Solr Issues related to the configuration or use of the Solr subsystem. [managed] Theme: Book Page Issues related to the redesign of the books page Affects: UI Issues with the web site's user interface. [managed] Team: Front-end Issues belonging to the Front-end team [experimental tag] Needs: Review This issue/PR needs to be reviewed in order to be closed or merged (see comments). [managed] and removed Needs: Response Issues which require feedback from lead Needs: Review This issue/PR needs to be reviewed in order to be closed or merged (see comments). [managed] labels Sep 14, 2026
@lokesh

lokesh commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

@Sravan1011 let me know if you have any questions on my feedback and if you are still interested on iterating on this PR

@Sravan1011

Copy link
Copy Markdown
Contributor Author

Ya ya Im

@Sravan1011 Sravan1011 closed this Sep 18, 2026
@Sravan1011 Sravan1011 reopened this Sep 18, 2026
@Sravan1011

Sravan1011 commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor Author

I'm working on this will update in some time, I have done it just need to push those

@github-actions github-actions Bot added the Needs: Response Issues which require feedback from lead label Sep 18, 2026
@Sravan1011
Sravan1011 force-pushed the nearby-books-prototype branch from ca7b203 to aac32c7 Compare September 18, 2026 12:23
@Sravan1011

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review -- confirmed working locally now, screenshot below. Pushed a fix for all the blocking items:

1. Solr syntax error in the "after" query. Fixed -- switched to Solr's exclusive {/} range bounds instead of parens (ddc_sort:{"..." TO ...] / ddc_sort:[... TO "..."}), and dropped decrement_string_solr and its tests since the exclusive bound makes it unnecessary.

2. Carousel ignored the fetched books / title-link & load-more pointed at an unrelated empty query. Fixed at the root in RawQueryCarousel.html: when a caller passes pre-fetched books_data with no real query behind it (our case), the title now renders as plain non-link text and load-more is omitted, instead of both defaulting to an empty/unrelated search. custom_carousel.html was updated to support a title without a link.

3. Extra Solr calls on every book-page render. Fixed -- nearby_books/get_nearby_books are gone from BookPageContext/prepare_book_page entirely. The neighbour lookup now lives behind a new lazy-loaded FastAPI partial, GET /partials/NearbyBooks.json (modeled on lazy_carousel_partial), fetched by a small JS module (mirroring lazy-carousel.js) once the placeholder scrolls into view. The book page itself only renders a lightweight placeholder with the work key + edition language -- no Solr I/O before the page ships.

4. Non-numeric DDCs ([Fic], [E]) showing up as neighbours. Fixed -- both ranges are capped to numeric ddc_sort values, and the carousel is skipped if the work's own ddc_sort isn't numeric. Also added type:work, the safe-mode filter (-subject:"content_warning:cover"), and a fields allowlist to all three queries, since books_data bypasses RawQueryCarousel's own safe-mode/field handling.

5. PR description. Updated -- no more references to the <ol-nearby-books> Lit component; it describes the current Python-helper + RawQueryCarousel architecture.

On your earlier feedback (wrong anchor / inclusive bounds): both addressed as part of the above --

  • The anchor is now the work's indexed ddc_sort (fetched from Solr via solr.get_async), not a value recomputed from whichever single edition is being viewed. normalize_ddc/choose_sorting_ddc have been removed from the request path entirely.
  • The before/after ranges are now strict (exclusive {/}), so a popular ddc_sort value can no longer fill both ranges with ties on itself. Exact matches at the anchor value are pulled as their own small, deliberate bucket (4 exact + 8 before + 8 after) rather than being excluded or leaking into the ranges.

Screenshot

Nearby Books carousel rendering on /books/OL8011987M (The Wit & Wisdom of Mark Twain, ddc_sort 818.402) in local dev, showing real neighbours ordered by DDC:

Nearby Books carousel

Happy to make any further adjustments -- let me know if you'd like anything else changed before this goes up on the testing server.

@Sravan1011

Copy link
Copy Markdown
Contributor Author

@lokesh can you check now

Port NearbyBooksPartial onto the Jinja carousel (RawQueryCarousel and
custom_carousel were converted upstream in internetarchive#13590).
- Validate work_key/language/limit with pydantic patterns and bounds, and
  escape both values before they reach Solr
- Read the anchor ddc_sort through /select (docValues) instead of /get
- Memoize gather_nearby_books_async for 5 minutes; Solr failures return
  None so they are not cached
- Only render the placeholder when a loaded edition has a numeric DDC
- Reuse lazy-carousel.js and RawQueryCarouselPlaceholder.html.jinja via a
  config 'partial' key; drop nearby-books.js, the Templetor macro and the
  duplicated CSS
- Drop the no-op convert_iso_to_marc call and the stray helpers.py change
The heading class painted every heading in link blue, including the plain
Nearby Books heading and the home page's Welcome heading. Move the link
colour onto the anchor so unlinked headings use the heading ink.
Apply the same borrowable-or-public cut the other book-page carousels
use. The range queries are open-ended and distance-sorted, so Solr just
walks further along the shelf; on production the nearest readable
neighbours still sit within the same call-number neighbourhood.

The clause is joined with AND on purpose. A bare clause after an AND
chain is only a SHOULD for Solr's classic parser and would boost readable
works rather than filter to them.
The template gated the placeholder on a numeric DDC among the editions
loaded on the page, but the partial anchors on the work's Solr ddc_sort,
which aggregates every edition. Popular works with many editions often
keep their Dewey numbers on editions outside the page's slice, so the
carousel vanished on testing for books like The Mystery of the Blue Train
even though the partial had a full shelf for them.

Let the partial decide: it answers empty after one memoized key lookup
for works with no shelf position, and the loader removes the box.
The carousel's docs come straight from Solr, so they carried no lending
state and every card fell through to "Not in Library" even though Solr
said each work was borrowable. The other book-page carousels go through
work_search_async, which attaches archive.org availability to each doc;
do the same here, outside the memoized fetch so lending state is never
pinned for five minutes.

@lokesh lokesh left a comment

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.

After putting it on our testing server, I made one big change to filter to readable books. Neighbours must be borrowable or public, matching the other carousels on the book page.

Otherwise, looking good! We'll do some more testing within the team and make a final call. Thanks!

@lokesh lokesh added On Testing and removed Needs: Response Issues which require feedback from lead labels Sep 22, 2026
@lokesh

lokesh commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

@mekarpeles cc: @cdrini

Original issue: #13526
On testing server: https://testing.openlibrary.org/books/OL51733472M/The_Mystery_of_the_Blue_Train

This exploration is ready for vetting.

image

@lokesh lokesh assigned mekarpeles and unassigned lokesh Sep 22, 2026
@Sravan1011

Copy link
Copy Markdown
Contributor Author

@lokesh so I need to any more addition to this or good to go for a merge then ?

@github-actions github-actions Bot added the Needs: Response Issues which require feedback from lead label Sep 23, 2026
@lokesh

lokesh commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

@Sravan1011 No more changes at this time. Thanks for all your work so far! Staff will test out the experience and report back.

@Sravan1011

Copy link
Copy Markdown
Contributor Author

@lokesh @mekarpeles any further update on this issue

@Sravan1011

Copy link
Copy Markdown
Contributor Author

@lokesh @mekarpeles any update on the latest tests happend with this pr

@Sravan1011

Copy link
Copy Markdown
Contributor Author

@lokesh @mekarpeles any update on testing pr and merge this

lokesh added 2 commits October 4, 2026 21:35
Fires BookCarousel|Impression|<key> once, when half of a loaded carousel
is on screen, so clicks can be compared per impression across carousels.
Empty partials and fallback messages report nothing.
…otype

# Conflicts:
#	openlibrary/plugins/openlibrary/partials.py
#	openlibrary/templates/books/custom_carousel.html.jinja
@lokesh

lokesh commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

@Sravan1011 The PR looks good. I put it on the testing server and everything is working as expected. I'm going to go ahead and merge it!

I did pushed two commits to your branch:

  1. A merge of upstream/master.
  2. Impression tracking for the book carousels. Each carousel now sends BookCarousel|Impression| once, when at least half of it is on screen.

The impression tracking is there so we can judge whether Nearby Books earns its place on the book page. The existing click events (CoverClick, BorrowClick, etc.) are already reported per carousel key. With impressions alongside them, we can look at clicks per impression for nearby-books and compare it fairly with "Books like this one" (related-subjects-carousel) and "More by this author" (related-authors-carousel). It sits lowest on the page, so raw click counts would undersell it.

@Sravan1011

Copy link
Copy Markdown
Contributor Author

Yes @lokesh sir can you merge this. I'll be working on another issue too

"Nearby" read as geographic; the carousel shows Dewey shelf neighbours.
@lokesh

lokesh commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

I pushed a small change on top: renamed the carousel from "Nearby Books" to "On the Same Shelf" (9ae81fa). "Nearby" read a little like it might be geographic, books near you, so the new name makes the library shelf idea clearer.

@lokesh
lokesh merged commit feac96d into internetarchive:master Oct 5, 2026
6 checks passed
@Sravan1011

Copy link
Copy Markdown
Contributor Author

I pushed a small change on top: renamed the carousel from "Nearby Books" to "On the Same Shelf" (9ae81fa). "Nearby" read a little like it might be geographic, books near you, so the new name makes the library shelf idea clearer.

@lokesh thank you so much sir for guiding and correcting me in while working on this issue . It was great working with you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Affects: UI Issues with the web site's user interface. [managed] Lead: @lokesh Issues pertaining to front-end design system [css, js, components] Module: Carousels Module: JavaScript Issues related to the JavaScript functionality. [managed] Module: Solr Issues related to the configuration or use of the Solr subsystem. [managed] Needs: Response Issues which require feedback from lead On Testing Team: Front-end Issues belonging to the Front-end team [experimental tag] Theme: Book Page Issues related to the redesign of the books page Type: Feature Request Issue describes a feature or enhancement we'd like to implement. [managed]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants