WHAT YOU WILL BUILD
A counterexample showing how filtering only after Top K can leave an empty result.
Ranking and eligibility answer different questions. A passage can score highly and still belong to another tenant or an obsolete version. The exercise compares selecting from eligible records with filtering a globally truncated result.
Before you start
Use a text editor and Node.js 22 or newer ↗. Check your version with node --version. Save the downloaded file in an empty folder, then open a terminal in that folder.
All data is included. No packages, account, or API key are required. This is exact dot-product sorting of three handwritten vectors. It does not implement a vector database, HNSW, or a production authorization system.
Download the lab (.mjs)FOLLOW ALONG
Work through the example.
- 01
Inspect the candidate pool
The highest-scoring record belongs to tenant B. Tenant A's old policy is second; its current policy is third. The request is for tenant A's current content only.
- 02
Run the post-filter baseline
Take the global top result and then check eligibility. The only candidate is rejected, leaving zero results even though a valid current policy exists.
- 03
Run the eligible-corpus search
Filter by tenant and current version first, then rank. The valid current-policy record is returned. The printed results show why these two orders are not equivalent.
- 04
Carry the boundary into an integration
When using a database, apply trusted access constraints in its query and check its filtered-search behavior. Test strict filters, no eligible results, deleted records, and current-version changes with representative data.
THE COMPLETE LAB
Run it locally.
Run this command from the folder containing your downloaded file:
node vector-filtering.mjsView or copy the complete JavaScript
// SaveMyToken local lab. Run with Node.js 22 or newer.
import assert from "node:assert/strict";
// Exact dot-product ranking over toy vectors, not an ANN index.
const query = [1, 0];
const records = [
{ id: "other-tenant", tenant: "B", current: true, vector: [1, 0] },
{ id: "old-policy", tenant: "A", current: false, vector: [0.99, 0.01] },
{ id: "current-policy", tenant: "A", current: true, vector: [0.8, 0.2] },
];
const score = row => row.vector.reduce((sum, x, i) => sum + x * query[i], 0);
const rank = rows => [...rows].sort((a, b) => score(b) - score(a));
const eligible = row => row.tenant === "A" && row.current;
const postFilter = rank(records).slice(0, 1).filter(eligible);
const preFilter = rank(records.filter(eligible)).slice(0, 1);
assert.equal(postFilter.length, 0);
assert.equal(preFilter[0].id, "current-policy");
console.log("Filter after top 1:", postFilter.length, "results");
console.log("Filter before top 1:", preFilter[0].id);
Expected output for the unchanged example
Filter after top 1: 0 results
Filter before top 1: current-policyThe assertions also check the baseline behavior. After changing an input, predict the result and update the relevant assertion.
NOW CHANGE ONE THING
Make the example your own.
Add a second current tenant-A document and test K=2. Then mark both current records false and verify that no result is returned instead of relaxing the access filter.
You are done when…
Every returned record is eligible, and you can explain the empty post-filter result without blaming the similarity metric.
If something goes wrong
Increasing global K may find an eligible record but does not replace access control. Approximate indexes may also have different recall behavior under restrictive filters; measure it.
EXPLAIN WHAT YOU LEARNED
Interview practice
Try answering aloud before opening the reference answer. These are original learning questions, not a record of any employer's interviews.
01Why keep metadata alongside vectors?
Metadata identifies the source, version, tenant or access scope, and other eligibility conditions. The vector alone cannot reliably encode those rules.
Watch for: Semantic similarity is not a permission check.
02Why can filtering after Top K miss valid evidence?
The global shortlist may contain only ineligible records. Filtering that truncated list cannot recover eligible records excluded before filtering.
Watch for: An empty shortlist does not prove the eligible corpus contains no answer.
03How does exact search differ from approximate search?
Exact search considers the full eligible set under the defined metric. Approximate search trades some retrieval accuracy for efficiency through an index or candidate-selection method.
Watch for: This three-record sorting example is not evidence of approximate-index performance.
04How should updated documents be handled?
Associate chunks with a document version, publish a consistent updated set, and remove or exclude superseded chunks. Test retrieval immediately after the change.
Watch for: Adding new vectors while keeping old conflicting passages active can mix policy versions.
05What should you measure when selecting an index?
Measure recall against an exact baseline together with latency, memory, build time, updates, and filtering behavior on representative data.
Watch for: A latency number without corpus size and recall conditions is hard to interpret.
Sources and further reading
The explanation and local exercises were written for SaveMyToken. These references support the underlying concepts; the sample outputs describe only the supplied examples.
Qdrant: filtering ↗Sentence Transformers: semantic textual similarity ↗