Search: Firestore's Own Real Query Limitations & Working Around Them
Personal Catalogue: React & Firebase
Chapter 6 · Search: Firestore's Own Real Query Limitations & Working Around Them
Chapter 1's own original question — "do I already own this?" — needs an answer across any item,
matched by title or by whichever identifying field that item's own type actually has: author for a Book,
artist for a Cd, director for a Dvd or Bluray. The MongoDB sibling answered this with one regex-powered
$or query and two real crashes along the way. This chapter answers the same question with a
genuinely different tool, because Firestore doesn't have the one the MongoDB sibling reached for at all.
The Real Limitation: No Substring Search, Anywhere
Firestore's own query engine supports equality, range comparisons (<, <=,
>, >=) against an ordered field, array-contains, and
in/not-in — and nothing else. There is no regex, no LIKE, and no
built-in way to ask "does this field contain this substring anywhere." The MongoDB sibling's own
$or: [{ title: regex }, { author: regex }, ...] has no Firestore equivalent to translate to —
not a harder version of the same query, a genuinely different capability Firestore's query language doesn't
have.
The Closest Native Tool: Prefix Range Queries
Firestore does support one real, commonly-used trick for "starts with" matching, exploiting the fact that strings are compared byte by byte: querying for everything between a prefix and that same prefix followed by a very high Unicode character finds every string starting with it.
Tried against a real book titled "Clean Code": searchByTitlePrefix("Clean") finds it —
"Clean" is genuinely a prefix. searchByTitlePrefix("Code") finds nothing at all,
even though "Code" is right there in the title, because it doesn't sit at the start of the string.
"clean" wouldn't correctly bound a
query meant to match "Clean Code" without also storing a lowercase mirror field), and scoped to
one field at a time — even combining four of these prefix filters across title/author/artist/director with
Firestore's own newer or() query composition would still only catch a term sitting at the very
start of one of those fields, never a term like "Martin" appearing partway through "Robert Martin."
The Real Answer at This App's Scale: Fetch, Then Filter
The MongoDB sibling's own honest scale note already applies here just as directly: at a few hundred to a few thousand items, a personal catalogue doesn't need a real search engine — it needs a search that works correctly. Since Chapter 3 already committed this course to no backend server of its own, the practical answer is the same idea the MongoDB sibling calls a "full collection scan," just run in the browser instead of on a database server: fetch every item, then filter with plain JavaScript string matching:
This one function does everything the MongoDB sibling's own $or query does — title, author,
artist, or director, matched anywhere in the string, case-insensitively — genuinely more flexible than the
prefix trick above, at the real cost of doing the matching after every document has already been fetched.
/search
route shadowed by an earlier-registered /:id route) and an unescaped-regex crash (a raw
parenthesis breaking new RegExp()). Neither can happen here — not because they were fixed, but
because neither ingredient exists in this app's own architecture. Chapter 3 already decided this course has no
Express-style router with path segments to order incorrectly, and searchItems() above never
constructs a regex from user input at all, so there's no special-character syntax to ever need escaping.
"Mission: Impossible (" is just a plain string being compared with .includes() — the
parenthesis means nothing special to it.
The Real Cost the MongoDB Sibling Never Has to Think About
Fetching every document to search it isn't just slower at scale — it has a real, direct cost that a
self-hosted MongoDB collection simply doesn't. Firestore bills by the number of documents actually read, and
getDocs(collection(db, "items")) reads and counts every single document in the
collection, on every search, regardless of how few actually match. A self-run MongoDB instance has
no equivalent per-document billing at all — its own "full collection scan" cost is CPU time, not a metered
line item.
searchItems() —
is a genuine, worthwhile mitigation at this app's scale, built directly into the search component below.
A Small, Debounced Search Bar
Each keystroke resets the 300ms timer rather than immediately triggering a real Firestore read — only once
typing genuinely pauses does searchItems() actually run, keeping the real, metered read cost
tied to how often someone stops to look at results, not to how many characters they typed.
Trying It End to End
Hands-On Exercises
Build searchItems() and confirm it returns the correct Book for a search matching only its author field, and returns a clean empty array (not an error) for a term matching nothing at all.
📄 View solutionBuild searchByTitlePrefix() and run it twice against a real book titled "Clean Code" — once with the query "Clean" and once with "Code" — recording both real results and explaining precisely why one succeeds and the other returns nothing.
📄 View solutionExplain why this course's own search implementation is structurally immune to both real crashes the MongoDB sibling's own Chapter 6 found and fixed — the route-ordering bug and the unescaped-regex SyntaxError — tying your answer directly back to the architecture decision made in Chapter 3.
📄 View solutionChapter 6 Quick Reference
- The real limitation — Firestore has no substring/regex search of any kind; only equality, range, array-contains, and in/not-in
- The native prefix trick — orderBy + a >= / <= range bounded by ""; prefix-only, case-sensitive, single-field
- The real answer at this scale — fetch every document, filter client-side with plain string .includes(), matching the MongoDB sibling's own "full collection scan is fine here" logic one layer up
- Structural immunity — no Express router to mis-order, no regex ever constructed from user input, so neither of the MongoDB sibling's own two real crashes can occur here at all
- The real hidden cost — Firestore bills per document read; every search reads the entire collection regardless of match count, mitigated here with a 300ms debounce
- Ceiling — a genuine, larger-scale catalogue would need a real third-party search service (an Algolia-style Firestore extension), the same honest ceiling the MongoDB sibling names for its own full-collection regex scan
- Next chapter: The Catalogue List & Detail Views in React