Databases / Working draft

Finding the
blue mug

A catalogue knows what it sells. A search index helps turn what someone types into useful candidates.

Inverted indexes and vector retrieval2 interactive modelsDatabase field guide ↗

A visitor to our ceramics shop types “blue mug” into the search box. The catalogue includes a Blue ceramic mug, an Indigo ceramic cup, a Blue cereal bowl, and a White ceramic mug. The visitor hasn't given us a product ID. They've described something, and now we need to decide which products match and which to show first.

One approach reads every title and checks it for those words. That works, but repeats the same inspection for every query. We can prepare another arrangement: for each word, keep the IDs of products whose titles contain it. Instead of asking what words product 1 contains, we can ask which products contain “blue”.

This is an inverted index. The word points to a list of matching document IDs, called a postings list. Here a document is a product's searchable representation. It might include its description and category too, but we'll start with titles.

01 / WORDS

What counts as the same word?

Before building the lists, we split each title into tokens. “Blue ceramic mug” becomes “blue”, “ceramic”, and “mug” after lowercasing. The query follows the same process, so “BLUE mug” can find a title written with an initial capital. The combination of splitting and transforming text is called analysis.

Illustration · Turning titles into term lists

Catalogue titles

1 Blue ceramic mug

2 Indigo ceramic cup

3 Blue cereal bowl

4 White ceramic mug

Some postings lists

blue → 1, 3

mug → 1, 4

ceramic → 1, 2, 4

cup → 2

Intersecting the blue and mug lists leaves product 1. Taking their union gives products 1, 3, and 4. Other terms and the fifth product, a steel flask, are omitted from this drawing.

Analysis is a choice about meaning. A stemmer might reduce “mugs” to “mug”. A synonym rule might relate “cup” and “mug”. Those choices can rescue a useful match, but can also erase a distinction the visitor intended. Product identifiers need different treatment: splitting an exact SKU at its hyphens may be actively unhelpful. Search engines commonly allow different analysis for different fields.

Our simple analyser does neither stemming nor synonym expansion. “Blue mugs” therefore doesn't find the mug by requiring both tokens. That failure isn't a broken postings lookup; the requested term “mugs” simply has no list. Understanding the tokens is often the first useful step when debugging a puzzling result.

02 / MATCHES

A candidate isn't necessarily the best result

If both query terms are required, we intersect their lists. “Blue” points to products 1 and 3; “mug” points to 1 and 4. Only product 1 appears in both. If either term is enough, we take the union, giving us the mug, bowl, and white mug. The query's matching rule changes the candidate set before we decide its order.

Returning every candidate alphabetically may be a poor answer. A title matching both “blue” and “mug” seems more promising than one matching only “blue”. A word appearing in almost every title, like “ceramic”, tells us less than a rare one. Ranking combines evidence like this into a score, then orders the candidates.

Lucene-based engines such as Elasticsearch use BM25 as a standard scoring method. It considers how often a term appears, how common it is across documents, and document length; repetition has diminishing value. Field weights can give a title match more influence than a passing mention in a long description. These are relevance decisions, rather than facts about whether the product is purchasable.

A stock constraint answers that separate question. We can require stock greater than zero without rewarding a product for having more units. Filtering and ranking need both a sensible combination and current data. The best-scoring blue mug is no use to this visitor if it can't be bought.

Interactive experiment01

Which products do the term lists find?

Five catalogue titles are indexed. The analyser groups Unicode letters and digits, lowercases them, and treats punctuation as a separator. It does not stem words, remove accents, or add synonyms. Search uses visible segments. Every catalogue update here is also delivered to and accepted by the search engine into its indexing buffer. Refresh opens that accepted version for search; it does not fetch the catalogue. Each matched query term adds 1 divided by the number of searchable titles containing that term to the score.

Search for blue mug with all terms, then any term. Rename and restock the mug, and compare the catalogue with search before and after Refresh. Merge segments should preserve the answer.

Controls

Result Terms, searchable versions, and ranked matches

What this model leaves out

Only titles are tokenised. A matched term contributes 1 divided by the number of visible titles containing it; scores are summed. This is an invented ranking rule, not BM25. Delivery from the catalogue to the search engine succeeds immediately; there is no delivery queue or failure. Refresh is manual and each nonempty refresh creates one segment from the already accepted indexing buffer. Merge rewrites every live document. Real engines add postings compression, term frequencies, positions, field lengths, shard coordination, and more complex merges. No durability or timing is simulated.

03 / VISIBILITY

Accepted doesn't always mean searchable

Our catalogue editor renames the Blue ceramic mug to Indigo ceramic cup and records four newly delivered units. The catalogue can accept those changes while the searchable representation still contains the old title and zero stock. There may be an asynchronous delivery step from the catalogue to the search engine. Even after the search engine accepts an update, its search readers may need another step to see it.

Lucene stores the index in segments. Each segment contains its own term dictionary and postings. Incoming documents can be collected in a buffer, then written into a new segment. Search readers need that segment opened before they can query it. In Elasticsearch this visibility step is called a refresh. Its timing is configurable; a successful indexing response by itself needn't promise immediate search visibility.

Updating an indexed document replaces its searchable representation. The engine can record that the old version is deleted and add a new version rather than editing an existing segment's postings in place. Once the change is visible, queries must ignore the old version, so our mug stops matching “blue” and begins matching “indigo”. A later merge can rewrite live documents into fewer segments and discard deleted versions.

Merging costs work now to reduce accumulated work later. Many small segments give a search more places to consult; deleted versions occupy space until reclaimed. This resembles the immutable parts in a column store, although the data structures and query work differ. Refresh, merge, and durable recovery are also different jobs: opening a segment for search isn't proof that every necessary byte has been synchronised to durable storage.

Elasticsearch supports waiting for a refresh through the write API's refresh=wait_for option. That can make a particular workflow easier to reason about, but can't make an upstream catalogue update reach the search engine any sooner. An editor's “saved” message must describe which step completed. Checkout should still confirm price and stock with the authoritative catalogue transaction.

04 / SIMILARITY

“Indigo cup” might be what they meant

Term matching has a clear limit: the visitor's words and the product's words can differ. Synonyms help when we know the relationship. Another approach turns text into an embedding, a vector of numbers, and compares query and product vectors with a distance or similarity function. Nearby vectors may represent related meanings even without shared words. How useful that relationship is depends on the embedding model and our catalogue.

An exact nearest-neighbour query evaluates the required distances and returns the closest eligible products under that metric. “Exact” describes finding neighbours of these vectors, not finding the products a person truly wanted. A perfectly executed search over unsuitable embeddings can still produce poor recommendations.

As the collection grows, calculating every distance can become expensive. Approximate nearest-neighbour indexes, including HNSW and IVFFlat in pgvector, reduce the search work by considering selected candidates. They can miss neighbours that an exact search would find. Search effort, index size, update cost, and recall need evaluating together. Recomputing exact distances for retrieved candidates can improve their ordering, but it can't recover a product that never entered the candidate set.

Filters add another wrinkle. If we take the two nearest products and then remove out-of-stock ones, we may return fewer than two. The nearest two eligible products could be farther away and absent from that small list. An engine may combine filtering with traversal, search more candidates, or use a selective filter to make exact distance evaluation practical. The query plan matters.

Interactive experiment02

When does the stock filter run?

These are the same five titles in their original state. We've assigned two coordinates by hand: blue colour and mug-like shape. Every distance is calculated exactly. The question is which two in-stock products a nearest-neighbour query returns.

For Blue mug, choose Take two, then filter. Both nearest products are out of stock. Choose Filter, then take two to find two eligible products instead.

Controls

Result Exact distances and eligible results

What this model leaves out

Coordinates are hand-assigned, not produced by an embedding model. Distances are Euclidean in two dimensions, and all five are computed before either ordering is applied. The diagram tests the effect of truncating a candidate list before filtering. It does not implement or benchmark HNSW, IVFFlat, approximate recall, or a production embedding's relevance.

pgvector's documentation describes filtering after approximate index scans and offers iterative scans to look farther when needed. That is a product-specific mechanism; the experiment above only demonstrates candidate truncation using exact distances. A real evaluation should compare approximate results with an exact baseline under the same filters, then separately judge whether either answer helps visitors.

05 / THE CHOICE

What belongs in the search index?

A separate search engine earns its place when relevance, language analysis, multiple searchable fields, or retrieval workload require it. It also adds another representation to populate, monitor, and rebuild. PostgreSQL's built-in full-text search may already cover a modest catalogue's lexical needs, and pgvector can keep vector retrieval beside relational data. The presence of a search box doesn't alone require a separate service.

Start with actual queries and desired results. A visitor typing an exact SKU needs precise matching. Someone typing “something for morning coffee” asks a different question; lexical and vector candidates might both help, followed by a shared ranking stage. Combining them requires an explicit rule: lexical scores and vector distances aren't automatically comparable numbers.

Now suppose the shop adds a “ready to ship today” filter. Good title matching won't make a delayed stock update disappear. We need to trace delivery and refresh, decide how stale the filter can be, and confirm availability before purchase. Search makes promising candidates easy to find; the transaction still decides what we can sell.

Sources and model notes

Working draft. Primary sources accessed 2 October 2026. Titles, rankings, and coordinates in the experiments are invented. No counts or distances are product benchmarks.

The lexical experiment uses a sum of inverse document counts to make rare terms visible without reproducing BM25. The vector experiment uses all exact distances and changes only the order of filtering and taking two results.