Search: Real Django ORM Querying Across the Catalogue
Personal Catalogue: Django & PostgreSQL
Chapter 7 · Search: Real Django ORM Querying Across the Catalogue
Chapter 6 answered "what Python books do I have?" through tags. This chapter answers the app's own broader real question — "do I already own this?" — with a genuine free-text search box that works across every item's title and creator at once, then goes one step further using a real feature this project's own PostgreSQL choice specifically unlocks.
A Real Search Box
A plain GET-based form, matching the same query-string approach the tag filter already uses:
A GET form is a deliberate choice here, not an oversight — the resulting URL (/?q=fastapi) is genuinely shareable and bookmarkable, exactly like the ?tag= URLs from Chapter 6.
Basic Search with icontains + Q Objects
icontains is Django's own case-insensitive "contains" lookup — title__icontains=query matches regardless of capitalization, which is the correct default for a personal catalogue search where a user shouldn't have to remember exact casing. Q(...) | Q(...) combines the two conditions with a real SQL OR, matching an item whose title or whose creator contains the search term.
queryset variable in sequence, visiting /?tag=python&q=fastapi genuinely combines both — items must match the tag and the search term. This falls out naturally from how the ORM's own .filter() chaining works, with no extra code needed to make the two features cooperate.
A Real PostgreSQL-Specific Upgrade: Full-Text Search
icontains is a real, working solution — and it's exactly what the PHP + MySQL variant of this project (catalog-php1) uses, via a plain SQL LIKE clause. Because this variant specifically chose PostgreSQL, a genuinely more capable option exists: Django's own django.contrib.postgres.search module, wrapping PostgreSQL's real, built-in full-text search engine.
icontains/LIKE ever can: it applies real language-aware stemming, so searching "programming" also matches items whose title or creator contains "programmer" or "program" — a real linguistic match, not just a substring match. SearchRank then orders results by real relevance rather than by whatever arbitrary order the database happens to return rows in, so a title that's a strong match appears above one where the search term only appears incidentally. This is a genuine, direct payoff of choosing PostgreSQL specifically for this variant — the PHP + MySQL sibling has no equivalent built-in feature to reach for.
SearchVector works correctly with no extra setup, but it recomputes the searchable text from title and creator on every single query — genuinely fine for a personal catalogue of a few hundred or even a few thousand items, but a real, documented performance cost that grows with the table. A properly indexed setup adds a real PostgreSQL GIN index over a precomputed SearchVectorField, letting full-text search stay fast even at much larger scale. That's real, additional setup this course deliberately doesn't build — a personal catalogue's own realistic size doesn't need it — but it's worth knowing the option exists for a much larger dataset.
Trying It Out
Search for a partial, differently-cased word from an item's own title or creator and confirm it's found. Then try a word related to, but not literally contained in, an existing title — like "programmer" when the actual stored title contains "programming" — and confirm the full-text search version still finds it, where the earlier icontains version would not have.
Hands-On Exercises
Build the icontains/Q-object version of search from this chapter, then confirm /?tag=python&q=fastapi genuinely returns only items matching both conditions at once.
Switch to the PostgreSQL full-text search version, add an item titled "Learning Python Programming", and confirm a search for "programmer" (not "programming") still finds it — then explain why icontains alone would not have.
Explain why this project deliberately doesn't build a GIN index for the full-text search field, and under what real condition that decision should be revisited.
📄 View solutionChapter 7 Quick Reference
- Basic search —
Q(title__icontains=query) | Q(creator__icontains=query), a case-insensitive OR across two fields - Filters chain — search and the Chapter 6 tag filter combine automatically via sequential
.filter()calls on the same queryset - PostgreSQL full-text search —
SearchVector/SearchQuery/SearchRankfromdjango.contrib.postgres.search, giving real stemming and relevance rankingicontainscan't provide - Real tradeoff — full-text search recomputes the search vector per query without a GIN index; fine at personal-catalogue scale, worth revisiting at real scale
- Next chapter: Styling & Templates