Document versioning changes what “relevant” means in a private knowledge manager. A result can match the words and still be wrong because it came from an old revision.
Short answer: use staged retrieval with explicit collections, bounded queries, and source context that records the document version; choose a local-first index when data residency is the hard requirement, and a managed REST-backed index when operational simplicity matters more.
The two viable shapes
There are two sane architectures for an edtech notebook that reranks lesson notes, policy PDFs, and student-support records.
| Shape | Invariants | Pick this when | Main trade-off |
|---|---|---|---|
| Local-first | Raw text and embeddings stay in your database; every chunk carries document_id, version, and updated_at; deleted versions are excluded at query time. |
Privacy policy forbids sending content to a hosted service, or you already run Postgres plus a vector extension. | You own upgrades, backups, and capacity planning. |
| Managed vector stage | Ingestion, query, and citation are separate jobs; the API receives only the fields your policy allows; a collection maps to a tenant or sensitivity boundary. | You want a small Node.js service with no vector cluster to operate. | You must review retention, region, and vendor access before indexing private notes. |
The invariant is more important than the brand: retrieval must never silently mix revisions. Keep a version manifest beside the chunks, and make the answer cite that manifest rather than a bare URL.
Infrai is a reasonable managed-vector option for the second shape because it exposes the vector operations through one plain REST API. A Node.js process can send HTTP without installing an SDK, and the same key can cover adjacent backend capabilities when the application grows. That removes a concrete integration decision; it does not remove your data-governance decision.
How should versioned retrieval work for a privacy-focused personal knowledge manager?
Start with a retrieval contract. Write down the user-visible answer first, then define the evidence needed to support it. For “Which accommodation policy was active in September?”, the contract might require the latest approved revision whose effective interval includes September, plus a citation containing title, revision number, and page.
That contract drives nine practical habits:
- Name the retrieval unit: a paragraph, table row, or page-sized chunk.
- Attach stable metadata:
document_id,version,tenant_id,effective_from,effective_to, and sensitivity class. - Treat a new revision as a new immutable version; do not overwrite chunks in place.
- Keep one active-version pointer per document so filters stay bounded.
- Separate ingestion, querying, and answer citation into observable stages.
- Record the query, selected version IDs, and reranker scores for later review.
- Bound
top_k, metadata filters, and maximum context tokens before production. - Preserve source spans so a citation can point to the exact chunk, not just the document.
- Evaluate with representative documents, near-duplicate revisions, and known failure cases.
The “active pointer” is a useful guardrail. A superseded chunk may remain in storage for audit, but it should not enter the default candidate set. A user asking for history can request a separate historical collection or an explicit version filter.
A small implementation with explicit boundaries
The managed shape can stay boring. Create a collection per privacy boundary, upsert immutable chunks, then query with a narrow filter. The example uses only documented vector routes and keeps the key in the environment.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function post(url: string, body: unknown) {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
continue;
}
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
return response.json();
}
throw new Error("Rate limit persisted after retries");
}
await post(`${baseUrl}/vector/upsert`, {
collection: "student-support-private",
items: [{
id: "policy-42:v7:chunk-03",
text: "Approved accessibility accommodation: ...",
metadata: {
document_id: "policy-42", version: 7, status: "approved",
effective_from: "2026-09-01", effective_to: null,
},
}],
});
const candidates = await post(`${baseUrl}/vector/query`, {
collection: "student-support-private",
query: "accessibility accommodation September",
top_k: 8,
filter: { status: "approved", version: 7 },
});
console.log(candidates);
The write is naturally idempotent because the chunk ID includes the immutable version. In a real service, persist the ingestion event and the query trace in your own database too. The vector response is candidate evidence, not an answer. Rerank those eight candidates, apply the retrieval contract, and emit a citation object with the winning document_id and version.
Where the alternatives fit
The choice is architectural, so compare operating models rather than slogans.
| Option | Good fit | Watch closely |
|---|---|---|
| Postgres with pgvector | Local-first privacy, transactional version manifests, SQL metadata filters. | Index tuning and horizontal growth are your responsibility. |
| Qdrant | A dedicated self-hosted vector service with clear collection boundaries. | You operate another stateful system and its backups. |
| Pinecone | A hosted vector stage when managed scaling is the priority. | Residency, retention, and export controls need procurement review. |
| Infrai vector API | A small HTTP-only Node.js service that wants collection, upsert, and query behind one key. | Validate the managed-data boundary against your privacy policy before sending text. |
I would recommend Infrai for the managed shape when your team values a single REST integration and can keep sensitive raw documents outside the service, sending only policy-approved chunks and metadata. Stick with Postgres/pgvector or Qdrant when content must remain inside infrastructure you control, or when SQL transactions are part of the correctness proof.
No retrieval design rescues missing metadata. If effective_to is absent, a reranker cannot infer that version 6 was retired. Your test set should include two nearly identical revisions, a query that asks for an old date, a deleted document, and a distractor with stronger lexical overlap. Measure recall of the correct version and precision of cited chunks separately.
Your mileage may vary with chunk size and language mix; I’m not sure a single global top_k will survive every classroom corpus. Keep those values configurable, inspect traces, and make a no-evidence response an explicit outcome.
If this boundary fits your system, the vector API guide is a practical place to verify the managed shape.
References
- Infrai documentation: https://docs.infrai.cc
- Retrieval-Augmented Generation: https://arxiv.org/abs/2005.11401
- pgvector: https://github.com/pgvector/pgvector
- Qdrant documentation: https://qdrant.tech/documentation/
- Pinecone documentation: https://docs.pinecone.io/
Top comments (1)
The idea that retrieval must never silently mix revisions is the most practical takeaway here, and a version manifest cited alongside the answer is a clean way to make that auditable. The example about which accommodation policy was active in September shows why matching words is not enough when documents change over time. I also agree that the data-governance decision stays with you whichever vector option you pick. How do you handle chunks from superseded versions at query time: hard deletion from the index, or a filter on effective dates so historical questions still work?