Lesson 4: Filters and structure

"3-bedroom flats under 400k in Ghent" has one correct answer set. Similarity search alone will get it approximately - which is wrong.

~15 minutes. Builds on Lesson 3.

Exact constraints are not similarity

An embedding of "under 400,000" is near listings around that price - including 425,000. A buyer with a hard budget does not want "near". Exact constraints must become metadata filters: hard predicates the index applies, not hints the ranker weighs.

parsed = extract(query)            # {city: "Ghent", type: "flat", bedrooms: 3,
                                   #  price_max: 400000, semantic: null}
results = vector_search(
  embed(query_text or parsed.semantic),
  filter = parsed.filters,         # applied by the index, not after
  top = 100)

This is why Lesson 2 insisted on typed metadata fields: a filter can only bind to a field that exists. Every serious vector store supports filtered search - pgvector inside SQL WHERE clauses, Pinecone and Weaviate with filter expressions.[1],[2],[3]

Route by query type

QueryRoute
"cheapest 3-bed flat in Ghent"Pure structured: a database query. No vectors involved - and none needed.
"bright family home near good schools"Pure semantic: hybrid retrieval from Lesson 3.
"3-bed under 400k in Ghent with a garden feel"Mixed: filters for city, bedrooms, price; semantic ranking for "garden feel" within the filtered set.
The extractor is itself a generation step (a small model call with a schema, or rules). Log its output: a wrong filter is invisible in the answer - results look plausible, they are just from the wrong set. This surfaces again in the Lesson 7 checklist.

The filtered-ANN trap

Approximate vector indexes traverse a graph of neighbours and then drop rows failing the filter. With a selective filter (one city, one tenant), the top-k neighbourhood may contain almost no surviving rows - you asked for 100, you get 7, recall collapses silently. Know your store's answer: pgvector 0.8+ offers iterative scans that keep searching until enough filtered results are found;[1] dedicated stores document pre-filtering modes.[2] The test is easy: count results on your most selective real filter.

Retrieval practice

From memory, before scrolling up.

Your win: you can split any query into hard predicates and a semantic remainder, route it, and name the one silent failure (post-filtered ANN) to test for.

Primary source

Read the filtering sections of the pgvector README - it is the most honest documentation of the filtered-ANN problem and its costs.

Ask your teaching agent to design the filter schema for your actual listing fields - bring the column list.