internetarchive / internetarchive/openlibrary

Prototype: "Nearby Books" carousel — DDC shelf-adjacency browsing (Lit component)

Open
#13,159 5 comments 0 reactions 1 assignee Claimed by @Sravan1011 View on GitHub
Affects: UI Lead: @lokesh Module: Carousels Module: JavaScript Module: Solr Team: Front-end Theme: CSS Theme: Search Type: Feature Request
Dominant language
Python
Stars
6.7k
Forks
2k
Avg merge
2d 19h
Merged PRs (30d)
138

Description

## Summary

Physical library patrons browse by "shelf adjacency" — pull one book, glance left/right, find something related by call number. We'd like a small **prototype**: a **"Nearby Books" carousel**, a Lit web component that, given an edition, shows other available editions with numerically/lexically close Dewey Decimal Classification (DDC) numbers — the digital analog of scanning the shelf. Ideally it also filters by the edition's language, so you're not shown a book that's physically adjacent but unreadable to you.

This is explicitly a **prototype**: small, exploratory, not a production epic.

## Feasibility findings

### 1. DDC/LCC exist in Solr today, but as `string` fields, aggregated per-work (not per-edition)

- Schema: `conf/solr/conf/managed-schema.xml:175-176`
```xml


```
`lcc` is the same shape (`managed-schema.xml:173`).
- Populated in `openlibrary/solr/updater/work.py:483-490` (`ddc`/`ddc_sort`) and `:471-478` (`lcc`/`lcc_sort`). Both are computed by **aggregating across all editions of a work** (`get_edition_ddcs()`, `work.py:691-705`) and normalizing via `openlibrary/utils/ddc.py::normalize_ddc()` (zero-pads the integer part so lexical string sort approximates numeric order) and `choose_sorting_ddc()` picks one representative value for `ddc_sort`.
- **Implication**: DDC/LCC are per-**work** Solr fields, not per-edition. There's an open issue (#897, filed by @cdrini, still open) proposing to formally move classification from editions to works specifically *because* it "will also enable better UX suggestions for related works, (akin to shelf browsing)" — i.e., this prototype's premise already has a named precedent/blessing, but also means a "nearby edition" query is really a "nearby work" query today.

### 2. Nearest-neighbor is NOT a native numeric query today — but there's working precedent for a lexical-range approximation

- `ddc`/`ddc_sort`/`lcc`/`lcc_sort` are Solr `type="string"`, not numeric/float. There is no `sort=abs(sub(ddc,813.54)) asc`-style function query available without a schema change (new numeric field + reindex) — real risk, not hidden.
- However, **Library Explorer already solves this exact problem** with lexical range queries, and it's running in production:
- `openlibrary/components/LibraryExplorer/utils.js:69` — `decrementStringSolr(string, caseSensitive, numeric)` computes the lexically-previous string value.
- `openlibrary/components/LibraryExplorer/components/BookRoom.vue:224-231` uses it to find "position nearest a target classification": `${field}_sort:[* TO ${predecessor}]` counts items before a target, to scroll a shelf to the right offset.
- `openlibrary/components/LibraryExplorer/components/OLCarousel.vue:200-254` fetches `search.json` with `fields: 'key,title,author_name,cover_i,ddc,lcc,...'`, sorted by `ddc_sort`/`lcc_sort` (`LibraryToolbar.vue:486-487`), paginated.
- **Recommendation**: reuse this pattern rather than inventing one — two range queries against `ddc_sort` (`[* TO X]` desc-limited and `[X TO *]` asc-limited) to get N works just "before" and "after" a target DDC. Because of the zero-padding in `normalize_ddc()`, this reproduces correct numeric ordering for practical purposes. No schema change needed for a prototype.

### 3. Language filtering — straightforward

- `language` field: `managed-schema.xml:158`, `type="string" multiValued="true"`, also per-**work** (aggregated in `work.py:623-624`: `{lang for ed in self._solr_editions for lang in ed.language}`).
- A simple `fq=language:eng`-style filter works today, no schema risk. Same per-work caveat as DDC applies: filtering matches works that have *any* edition in that language, not necessarily the specific edition being viewed.

## Component approach

- **Correction to initial framing**: Lit is *not* unprecedented in this codebase. `openlibrary/components/lit/` has ~17 existing Lit components (`OLButton.js`, `OlDialog.js`, `OlPopover.js`, `OlToast.js`, `OpenLibraryOTP.js`, etc.), built via `vite-lit.config.mjs` into `ol-components.js`. So the tooling/build pattern already exists — no new build infra needed.
- **What actually would be new**: none of the existing Lit components query Solr and render a list of book cards. That pattern — fetch results, paginate, render book covers/metadata — exists today only in Vue (`LibraryExplorer/components/OLCarousel.vue`) and in the server-rendered template path (`openlibrary/templates/books/custom_carousel.html` + `custom_carousel_card.html`, mounted via `.carousel--progressively-enhanced` + `data-config` JSON blob). This prototype would be the **first Lit component that is a Solr-backed book carousel** — a smaller novelty than "first Lit component ever," but still a first for this specific pattern, worth flagging as a scope note.
- **Design decision for Mek/Lokesh**: render inside the existing carousel shell (`custom_carousel.html`'s `.carousel-section` / `.carousel-section-header` markup and CSS, card partial reused) for visual consistency with other book-page carousels — vs. a fully independent Lit web component with its own markup. Given the ask specifically frames this as "a Lit web component," leaning toward independent — but flagging the visual-consistency tradeoff explicitly.

## Suggested scope for the prototype

**In scope:**
- Given an edition key: resolve its work's DDC (`ddc_sort`) and language.
- Query Solr for N works with `ddc_sort` immediately below + N immediately above the target, filtered by matching `language`, excluding the current work — reusing the Library Explorer range-query pattern (no schema change).
- Render as a horizontal-scrolling Lit component, one card per result (cover, title, author).
- Works with no DDC: skip the component entirely (no placeholder, no fallback classification).

**Out of scope for v1:**
- LCC-based nearby (DDC only for the first pass).
- Any Solr schema/reindex work (numeric DDC field, true nearest-neighbor function queries).
- Genre/subject-adjacency reasoning (that's a separate, already-filed prototype: #13158, Genre Explorer).
- Availability-aware sorting beyond what Solr naturally returns (e.g., not prioritizing "currently lendable" editions).
- Edition-level (vs. work-level) classification — would require the #897 migration.

## Open questions for Mek/Lokesh

1. Is showing "nearby **works**" (not literally nearby **editions**, since DDC/language are per-work in Solr today) acceptable for a prototype, or does this need edition-level data first (blocked on #897)?
2. Is the lexical string range-query approach (reusing Library Explorer's `decrementStringSolr`/`_sort` pattern) sufficient, or is a numeric DDC field + true nearest-neighbor sort worth exploring as a v2 (schema change + reindex, non-trivial infra cost)?
3. Should this reuse the existing `custom_carousel.html` shell/CSS for visual consistency, or be a fully independent Lit component?
4. Where does it render — work page, edition page, or both?
5. Should LCC be a fallback classification for editions lacking DDC, or explicitly deferred to a later pass?

---
*Related/sibling prototype: #13158 (Genre Explorer) — both are Library-Explorer-adjacent shelf-browsing ideas, though this one is scoped to a single book-page carousel rather than a full explorer UI.*

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.