"3-bedroom flats under 400k in Ghent" has one correct answer set. Similarity search alone will get it approximately - which is wrong.
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]
| Query | Route |
|---|---|
| "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. |
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.
From memory, before scrolling up.
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.