Wall-clock time measured with
perf_counter().
ILIKE substring search.
The deliberately naive baseline. Search raw text, measure what happens, inspect PostgreSQL, then improve it.
What happened during this search?
Application measurements and PostgreSQL execution diagnostics are intentionally shown separately.
Application measurements
Measured from Python while the application performs the search.
Tracked Python memory still allocated when measurement finished.
Highest tracked Python allocation
observed by tracemalloc.
These memory numbers describe Python allocations. They do not represent PostgreSQL server RAM usage.
PostgreSQL execution
Parsed from
EXPLAIN
(ANALYZE, BUFFERS, FORMAT JSON)
.
Run a search to inspect the execution plan.
PostgreSQL will explain whether it scanned table rows sequentially or used another access strategy.
Waiting for PostgreSQL.
Total execution time reported by PostgreSQL itself.
Time PostgreSQL spent deciding how to execute the query.
How long the scan node took before producing its first result.
Time reported when that scan operation completed.
Rows emitted by the inspected database scan node.
Rows PostgreSQL inspected but discarded because they did not match the filter.
Shared buffer hits: pages PostgreSQL already had available in memory.
Shared buffer reads: pages that were not already present in PostgreSQL's shared buffer cache.
What does this report mean?
Run a search and SearchLab will translate PostgreSQL's execution plan into a short explanation here.
RAW QUERY
See exactly what PostgreSQL executed
Open
EXPLAIN (
ANALYZE,
BUFFERS,
FORMAT JSON
)
SELECT
id,
name,
description,
brand,
category,
price,
rating,
stock
FROM products
WHERE
name ILIKE %s
OR description ILIKE %s
OR brand ILIKE %s
OR category ILIKE %s
LIMIT 10;
RAW RESPONSE
See PostgreSQL's original JSON plan
Open
Run a search to inspect the raw EXPLAIN response.
Version benchmark
The same experiment will be repeated as the search architecture evolves.
Compare versions using the same dataset, query, machine, database state and measurement method. One random run is not a scientific benchmark.
Matching products
Search results will appear here. The first 10 are shown initially.
No search yet
Run a query above to inspect both its database behavior and returned products.
Why V1 behaves this way
Open any topic to review the reasoning behind the baseline implementation.
01
What V1 actually does
The complete search architecture
V1 treats search as simple string pattern matching.
query.strip()
↓
%query%
↓
ILIKE across four columns
↓
LIMIT 10
↓
Results
There is no ranking engine, stemming, fuzzy matching, full-text index or search-specific infrastructure yet.
02
LIKE, ILIKE, `%` and `_`
PostgreSQL pattern matching
LIKE performs
case-sensitive matching while
PostgreSQL ILIKE
performs case-insensitive matching.
apple%
Starts with apple
%apple
Ends with apple
%apple%
Contains apple
M_cBook
`_` represents exactly
one character
03
Why multi-word search breaks
Substrings are not language
Suppose the product is:
Logitech Wireless Mechanical Keyboard
and the user searches:
wireless keyboard
V1 creates:
%wireless keyboard%
That exact contiguous substring does
not exist because
Mechanical appears between
the two words.
04
Why there is no stemming
Characters are not linguistic meaning
To ILIKE, these are simply
different strings:
run
runner
running
runs
V1 does not know that they share a linguistic root. PostgreSQL Full-Text Search will introduce that concept in V2.
05
Why there is no relevance ranking
Match versus no match
A product containing
MacBook Air in its name
should probably rank above a sleeve that
merely mentions MacBook Air in its
description.
V1 does not naturally calculate:
MacBook Air 0.93
Laptop Sleeve 0.61
USB-C Charger 0.42
Without explicit ranking logic, matching rows do not have a useful relevance score.
06
Why `%keyboard%` can be expensive
Leading wildcard problem
The leading `%` means
keyboard can appear
anywhere inside the stored value.
keyboard...
gaming keyboard...
wireless keyboard...
case for keyboard...
PostgreSQL therefore cannot simply assume where the match begins. Depending on the query and available indexes, it may inspect many rows.
07
What a Sequential Scan means
Reading rows and applying the filter
If PostgreSQL reports:
Seq Scan on products
it means PostgreSQL chose to scan table data and test rows against the search condition instead of locating candidates through an appropriate index access path.
The report above translates that into Sequential Scan so you do not have to memorize planner abbreviations.
08
What buffer hits and reads mean
PostgreSQL page activity
PostgreSQL works with database pages.
This is why the same query can behave differently between a cold and warm run.
09
Why LIMIT 10 can hide scan cost
PostgreSQL may stop early
If matches are common, PostgreSQL can stop after it has produced the ten rows required by:
LIMIT 10
A sequential scan therefore does not automatically mean all 100,000 rows were inspected.
This is why V1 includes a deliberately nonexistent test query:
qzxneverexists92847
PostgreSQL cannot stop early because no tenth match ever appears.
10
Why Python time and PostgreSQL time differ
Two different measurement boundaries
perf_counter() measures
application-observed elapsed time around
the database operation.
That can include:
- Opening the database connection
- Sending the query
- PostgreSQL executing it
- Transferring result data
- Psycopg converting values
- Fetching rows
PostgreSQL's
Execution Time
measures database-side execution from
EXPLAIN ANALYZE.
They are different metrics and should remain separate.
11
Why tracemalloc is not database RAM
Python process versus PostgreSQL
tracemalloc tracks Python
memory allocations.
It can observe allocations associated with Python lists, dictionaries, strings and Psycopg result objects.
PostgreSQL runs separately, so the reported peak memory must not be described as PostgreSQL query memory.
12
Why V1 exists
Establish the baseline before V2
V1 deliberately does not solve:
- stemming
- tokenization
- relevance ranking
- typo tolerance
- fuzzy search
- full-text indexes
- autocomplete
- caching
Build the simplest correct search, measure it, understand where it breaks, then evolve the architecture deliberately.