Item History & Live Search-as-You-Type

Food Tracker (Django)

Chapter 8 · Item History & Live Search-as-You-Type

Every item ever added lives in one list — active items with a real expiry date, used items without one, exactly as Chapter 2 modeled them. This chapter makes that combined history searchable in real time, and lands on a genuinely different architecture than this app's Firebase-based sibling course did.

The History View

def history(request): items = Item.objects.all().order_by("-added_at") return render(request, "pantry/history.html", {"items": items})

A Thin JSON Endpoint — Deliberately Not DRF Yet

Live search genuinely needs the frontend to ask the server "what matches this text?" repeatedly, without a full page reload per keystroke — exactly the kind of interactive feature Chapter 6 flagged as DRF's own eventual territory. But standing up Django REST Framework's full machinery for one simple, read-only endpoint would be premature. This chapter takes the honest middle path: a single minimal view returning JsonResponse directly, no serializer class involved. DRF's real payoff (Chapter 11) arrives once several endpoints and genuinely richer validation actually justify it — not before.

def search_items(request): query = request.GET.get("q", "") items = Item.objects.filter(name__icontains=query)[:20] if query else Item.objects.none() results = [{"id": i.id, "name": i.name, "status": i.status} for i in items] return JsonResponse(results, safe=False)

name__icontains is a real, native, case-insensitive substring match, built directly into Django's ORM — one line, no workaround, no shadow field. safe=False is required because JsonResponse defaults to expecting a dict (a historical safeguard against a JSON-hijacking risk that applied to older browsers when a bare array was the top-level response); returning a plain list means opting out of that default explicitly.

A Genuinely Fair, Direct Contrast

Food Tracker (React + Firebase)'s own Chapter 8 needed real, honest work to get case-insensitive, substring-anywhere matching at all — Firestore has no equivalent of SQL's LIKE, so that course built a nameLower shadow field and ultimately chose client-side filtering specifically to get genuine mid-word matching. Here, on a real relational database, icontains already does exactly that, natively, in the query itself. This is a genuine SQL-side advantage worth crediting plainly, the same way Chapter 7 already credited SQL honestly for not needing a composite index.

The Frontend Genuinely Needs Debouncing This Time

This is the one place this course's own approach is more expensive per keystroke than the Firebase course's own choice, and worth being precise about why: this view hits the real database on every single request, rather than filtering an already-loaded, small array in memory. Debouncing — waiting for typing to pause before actually firing the request — genuinely matters here, unlike the Firebase course's own chosen approach, which loaded the full history once and needed no debounce at all because filtering afterward never touched the network again.

// pantry/static/pantry/search.js let debounceTimer; document.getElementById("search-box").addEventListener("input", (e) => { clearTimeout(debounceTimer); const query = e.target.value; debounceTimer = setTimeout(async () => { const response = await fetch(`/search/?q=${encodeURIComponent(query)}`); const results = await response.json(); renderResults(results); }, 300); });
Two legitimate architectures, shaped by two different constraints
Food Tracker (React + Firebase) chose client-side filtering: zero network cost per keystroke, no debounce needed, but explicitly scoped to a realistically small personal dataset. This course chooses a server round-trip per search: it needs debouncing, but it scales cleanly to a history far larger than "load it all into memory once" could ever comfortably handle, and gets substring/case-insensitive matching for free via icontains rather than needing a workaround field at all. Neither is the universally "right" search architecture — each is the correct answer to a different actual constraint.
Test the endpoint directly first
Visiting /search/?q=chedd in a browser returns the raw JSON — confirming the query itself works before writing a single line of the debounced frontend JS.
[:20] limits the response size, not the query cost
Slicing to 20 results controls how much comes back, but a leading-wildcard LIKE '%...%' query (which is what icontains compiles to) generally can't use a standard B-tree index efficiently regardless of how few rows are ultimately returned — the database may still have to scan a large portion of the table to find those 20 matches. At real scale, a genuine full-text or trigram search feature (PostgreSQL's own trigram extension, for instance) would be the actual fix, not a smaller result slice.

Where This Course Is Headed

Marking items used, recipe lookup, Django REST Framework — now genuinely justified by more than one interactive endpoint — deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain what name__icontains gives this course for free, and what workaround the Firebase sibling course's own Chapter 8 needed to build to get roughly the same capability.

📄 View solution
Exercise 2

Explain why this course's search needs debouncing while the Firebase sibling course's own chosen search approach didn't. What's the actual underlying difference in where the filtering happens?

📄 View solution
Exercise 3

Explain why slicing the queryset to [:20] doesn't actually solve the performance concern with a LIKE '%...%' query, and what this chapter names as the real fix at genuine scale.

📄 View solution

Chapter 8 Quick Reference

  • name__icontains — free, native, case-insensitive substring search; no shadow field needed, unlike the Firebase sibling course
  • A thin JsonResponse view — deliberately not DRF yet; one endpoint doesn't justify the full framework
  • safe=False — required to return a plain JSON list rather than a dict
  • Debouncing genuinely needed here — every search hits the real database, unlike the Firebase course's own load-once-filter-in-memory approach
  • [:20] limits the response, not the query cost — a leading-wildcard LIKE can't use a standard index regardless
  • Next chapter: Marking Items Used