Billion-row RAG: tenants, parents, and why postfilter dies

Strong RAG at 1B rows routes tenants before ANN, expands parents to SKUs inside the snapshot, and refuses global neighbors. Storage math, Java ports, isolation tests.

The recruiting desk I built ITJobOpportunities as a hiring platform for Costa Rica and LATAM tech talent. Candidates use a public job portal. Recruiters use a company-scoped ATS. Same Spring Boot API. Two doors. Keycloak names the company on the ATS side. I never take that company from the chat. On the public side a recruiter publishes a featured job : skills, short URL, scheduled promotion. A candidate can Easy Apply with a resume. That creates or updates a candidate and a job application, scores relevance, and notifies both sides. Job Fit is the “check your fit” card on the posting: a 0 to 100 score, matched and missing skills, a short summary. It is not a recruiter document. It saves a candidate only if an email is on the resume. Resume improve is a separate public rewrite, with an ATS-friendly PDF. On the ATS a recruiter at Company A sees that company's jobs, pipeline, clients, and candidates. They run hiring stages, clarifications, candidate Messages, notifications, Job Flyer Studio, and merge. Merge is company-scoped on purpose. Talent Assistant is the chat that turns a sentence like senior Java engineer, five years into a shortlist inside that talent pool. Skills, inferred positions, optional years. Merged-away duplicates stay out. Two AI documents sit on that desk, and people mix them up. AI Candidate Summary is the person. One persisted brief across profile evidence and application history. It goes stale when the evidence fingerprint changes. You regenerate only then. AI Brief is the application. A recruiter hits Generate on one Easy Apply or featured-job file. Structured English for a client presentation: copy, PDF, Strong Match. Messages do not silently rewrite it. Code Training Lab is the screening room next door. It is not the retrieve. The person is not the application. Job Fit is not the AI Brief. Talent Assistant stays inside the company that logged in. That is the product I actually run. This essay is not a tour. It is what happens if Talent Assistant, or any retrieve that feeds those briefs, searches every company's resumes on the same ATS. I am not claiming the product already ships a billion-row graph. I am saying the nest is already there: company, person, application. This is the piece after vector search always returns a neighbor . That essay is why the first retrieve lies. This one is the machine you need when the corpus is a billion records spread across companies, clients, and candidates. I am writing for two readers at once. If you have never shipped retrieval-augmented generation (RAG), stay with the desk, Maya, and the word list. If you already have pgvector in production, skim the primer, then sit on company shapes, the inbox lottery, and the expand rules. One company works. A multi-company ATS does not. A recruiter at Company A opens Talent Assistant and asks for a senior Java engineer, five years. If that retrieve stays inside their company, we can shortlist Maya or say no match. If it searches every company's resumes on the platform, the closest neighbor is often someone else's candidate. I already keep company-scoped candidates, clients, and applications. One parent document per person. One SKU document per Easy Apply or featured-job application. People then say: we have many companies on the same ATS now. A billion rows. Just grow the index. That sentence collapses two keys that are not interchangeable. A tenant is whose cabinet: Company A is not Company B. A parent is the person: Maya Chen. Serving Company B's resume in Company A's Talent Assistant is a leak. Finding Company A's Maya and then her featured-job application is the retrieve. If you index them as one flat neighbor list, the system will do the wrong one under load and look fine in the demo. A billion neighbors is not a retrieval system. It is a routing failure with extra RAM. Words I will keep using RAG means: look up trusted text, then let a language model write. The lookup is the product. Generation without a lookup is just a fluent guess. I care about the lookup. The next three pictures are the words I will keep using. If you can point at each one, the Java later is those pictures with types. An embedding is a long list of numbers that stands in for a sentence. Close numbers mean close meaning. “Java engineer” sits near “Spring backend.” Not near “pastry chef.” Approximate nearest neighbor (ANN) is a fast, slightly fuzzy “who is closest?” It always returns someone. That someone is a neighbor , not an answer. I wrote the catalog essay about that lie. Here the neighbor can also belong to Company B. Close meaning is a circle. Company B's Maya can sit in it. The fingerprint does not know the company. A tenant in this ATS is the company console after login. Look at the header: Company A. The sidebar is Jobs, Candidates, Talent Assistant, Pipeline. That whole workspace is one tenant. Company B is the same product behind another Keycloak session. Merge is already company-scoped for the same reason. Retrieval has to be too. The id comes from the login, not from the chat. If Talent Assistant can see two company names, the retrieve is already wrong. A parent in this ATS is the candidate record. Open Candidates, open Maya Chen: Profile, Files, Skills, Job Applications, AI Summary. That workspace is the person. AI Candidate Summary is the parent document. A SKU is one row on Job Applications: Easy Apply or the featured Java file. AI Brief is generated on that application, not on the person. A snapshot is this week's evidence on the profile. Last month's PDF stays in Files. Finding Maya's profile is not the shortlist. Finding the application on her Job Applications tab, this week, is. Tenant is not a parent Picture two companies on ITJobOpportunities. A recruiter at Company A opens Talent Assistant. Company B has a Java engineer whose resume sounds almost the same. They type senior Java engineer, five years . The closest sentence on the whole ATS might live in Company B's folder. If I hand that sentence to the model, I have shown another company's candidate. That is the tenant mistake. Merge in this product is already company-scoped for the same reason. Retrieval has to be too. Now stay inside Company A. The recruiter did not type an application id. They named a person and a role. The candidate row is the parent. The featured-job or Easy Apply file is the SKU. Finding Maya and then asking this week's sync which client req she applied to is the retrieve. That is the parent move. Same cosine. Opposite permissions. I keep meeting designs that store (id, embedding, metadata jsonb) and treat tenant_id and parent_code as two tags the reranker can read later. That is how Company A's chat cites Company B's Maya: the words were close and nobody owned the door. A tenant answers: may this signed-in company see this row. The id comes from the login. I never take it from the chat JSON. I never let the model pass tenantId as a tool argument and trust it. A parent answers: which candidate is this application from. The id comes from ingest. I use it to expand a meaning hit into files I actually have. Same cosine. Opposite permissions. Write that down before you pick an ANN library. Key Question If I get it wrong If I get it right tenant_id Whose cabinet Company B's Maya in Company A's chat, or empty after a late filter Search never opens another company import_id Which week's resume sync Last month's PDF in this week's shortlist Old syncs stay on the shelf for rollback parent_code Which person is this Mushy resume text, or no application to send A meaning hit expands to real applications item_code Which file goes on the shortlist The model invents an application hit@k has a ground-truth id On ITJobOpportunities I already split candidate from application. In the retrieval ports that is EmbeddingEntityType.PARENT and SKU . The snapshot key is import_id . The ATS already has a company key on top. Three scopes. One ANN call that ignores them is not “simpler....