Skip to main content

Two Search Boxes in One Reader

Updated 5 min readMarkdown

Type a word you know is in chapter three into the shelf search, and nothing comes back. Type a book's title into the search panel inside the reader, and nothing comes back either. Neither box is broken. Each one is looking in a place the other never touches, and the reader has two search boxes because it has two different questions to answer: which books, and where in this book.

The shelf search reads rows, not books

The shelf search never opens a book. A query goes to the book list endpoint with a search parameter, and the database answers it with a single SQL query against the books_meta table, the table that holds one row per book. Each word in the query becomes a LIKE condition matched against five columns: title, author, and three stored paths for where the book came from. All the words have to match, and the match is case-insensitive.

That is the whole extent of it. The search never reads chapter text, never parses a file, never looks at a description. A word that appears a hundred times in the body of a book scores exactly nothing here if it is not in the title, the author field, or a file path. What comes back is a page of books — twelve by default — with a total count, and the same query can be combined with the shelf's other filters: a category, a source, favorites, a sort order. Those filters narrow the same rows; none of them reach into content.

The shelf search is fast for the same reason it is blind to content: the data it needs was already written down when the book was imported. Nothing is parsed at query time.

The in-book search walks chapters, not rows

The search panel inside the reader starts from the opposite end. It takes a book id and a keyword, checks that the book belongs to the current user, and hands the book's reader to a search engine that walks the book's manifest in order, chapter by chapter. Only HTML chapters are scanned. Each chapter's HTML is parsed, elements containing the search terms are collected, and every hit is recorded with the chapter it came from and the element inside that chapter, so the result can point at a specific place: a chapter href plus an element id.

The results come back in two shapes at once: a flat list of hits, and the same hits grouped by chapter with a count per chapter. The excerpt shown for each hit is cut down to the words around the match, with the rest replaced by ellipses — the point is to show enough context to recognize the passage, not to reproduce the paragraph. The scan stops after 500 hits, and at most 500 are returned; when the book has more matches than that, the response says so, with a flag and a count of how many were scanned before the cutoff. When the reader opens a chapter, the same keyword is run over that chapter's HTML again and the matches are wrapped in place, so the hit the reader clicked is visible in the text itself.

Nothing here is precomputed. There is no search index standing between the query and the chapters; every search re-walks the book. And the same engine serves all four formats — EPUB, TXT, MOBI, and PDF each hand their reader to it — because by the time the search runs, each format has already been turned into chapters the engine can walk.

The same query box on both sides

The two searches look alike from the outside, and that resemblance is part of the confusion. Both split a query on whitespace and require every word to match. Both ignore case. A reader typing two words into either box gets the same kind of behavior, and could reasonably assume one pipeline is doing both jobs.

Underneath, nothing is shared. One is a SQL LIKE over metadata rows; the other is a live scan over parsed chapter HTML. They do not even return the same kind of thing: one returns books, the other returns places inside a book. Merging them would not simplify anything. A shelf search that also searched content would have to parse every book on the shelf for every query, and would still have to answer a different question — it would need to say which books matched, while the reader's panel needs to say where. An in-book search built on metadata could not return a location at all, because metadata rows contain no positions inside a book.

The split is the design: the shelf search answers from data that was written down at import time, and the in-book search answers from the book itself, at the moment it is asked.

The chat search is a third job

There is one more search-like thing in the reader, and it belongs to neither chain. When a reader asks the chat a question about a book, the system cuts the book into chunks, builds a block-level inverted index in memory, and scores chunks with BM25 to pick passages for an answer. That index exists to feed a reply, not to return hits a reader can click through. It shares no code with the search panel's engine, and its output is an answer, not a list of places. Counting it as a third kind of search keeps the other two honest: the panel points, the chat answers, the shelf lists.

About the author

  • Writing about ebook technology.

No comments yet. Be the first to comment.