Short answer: use staged retrieval, then rank evidence with freshness and source traceability as first-class signals. For a logistics catalog, that usually means small, semantic chunks for durable product facts, a separate path for newly changed release notes, and a bounded final query that can be explained after the fact.
The mistake is treating “relevant” as one number. A result about a discontinued pallet size can be semantically close to a current product page and still be the wrong answer. Retrieval has to answer two questions at once: does this text match the request, and is it valid now?
A field guide to ranking choices
Start with a retrieval contract before choosing an index. Write down the answer unit, freshness window, source identifier, and the maximum number of passages the generator may see. Here is a compact decision table for a developer changelog tracker that feeds product-content search:
| Situation | First signal | Guardrail | Pick this when |
|---|---|---|---|
| Stable product specification | Semantic similarity | Version and product filters | The fact changes rarely and wording varies |
| New release note | Recency plus similarity | Require a canonical URL and timestamp | A change must appear quickly |
| Conflicting entries | Source authority plus version | Keep both candidates for review | A rollback or correction is possible |
| High-volume query | Coarse similarity | Tight timeout and top-k cap | Latency matters more than exhaustive recall |
This table is a starting point, not a universal scorecard. A catalog team should label a few dozen real queries, including “what changed this week?” and “which carrier supports this package?”, then tune weights against recall and precision. I’m not sure one fixed freshness window works across every warehouse; your mileage may vary when a carrier publishes corrections hours after a launch.
How should ranking signals shape a developer changelog tracker retrieval architecture?
Think in stages. The first stage discovers candidate text. The second stage applies deterministic constraints. The third stage records why each passage won. In words: query enters, candidates fan out, filters narrow, evidence is logged, and only then does answer generation begin.
For web-backed discovery, /v1/web/search is the broad net. Store the returned URL, title, retrieval time, and any published timestamp as metadata. For content already ingested, /v1/vector/query is the focused lookup. Keep these paths separate in telemetry so a drop in web recall is not hidden by a healthy vector index.
Chunking decides what a score can mean. Split a changelog by release entry or product component, not by an arbitrary character count that cuts a migration warning in half. Preserve the heading, version, effective date, and deprecation status beside the text. A 180-word chunk that contains one complete change is more useful than three 60-word fragments whose metadata got separated.
Freshness should be a bounded boost, not a license to ignore relevance. One simple policy is to decay the boost after a documented window and cap its maximum contribution. That prevents a noisy “updated” timestamp from outranking a precise compatibility note. Deleted records must leave the collection as well; otherwise an old answer can win forever because it has a perfect embedding.
An observable implementation path
The following TypeScript sketch keeps the contract explicit. It uses placeholders for credentials and collection names, and it calls only the two retrieval routes defined for this workflow.
type Candidate = { text: string; url: string; title: string; publishedAt?: string; retrievedAt: string; source: "web" | "vector"; similarity: number; }; const timeoutMs = 1200; const topK = 8; async function withTimeout<T>(work: Promise<T>): Promise<T> { const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error("retrieval_timeout")), timeoutMs), ); return Promise.race([work, timeout]); } function freshnessBoost(candidate: Candidate, now = Date.now()): number { if (!candidate.publishedAt) return 0; const ageDays = Math.max(0, (now - Date.parse(candidate.publishedAt)) / 86_400_000); return Math.min(0.18, 0.18 * Math.exp(-ageDays / 14)); } function rank(candidates: Candidate[]): Candidate[] { return candidates .map((candidate) => ({ candidate, score: candidate.similarity + freshnessBoost(candidate), })) .sort((a, b) => b.score - a.score) .slice(0, topK) .map(({ candidate }) => candidate); } async function retrieve(query: string): Promise<Candidate[]> { const web = withTimeout(fetch("/v1/web/search", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ query, limit: topK }), }).then((response) => response.json() as Promise<Candidate[]>)); const vector = withTimeout(fetch("/v1/vector/query", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ collection: "product-changelog", query, top_k: topK }), }).then((response) => response.json() as Promise<Candidate[]>)); const settled = await Promise.allSettled([web, vector]); const candidates = settled.flatMap((item) => item.status === "fulfilled" ? item.value : []); return rank(candidates.filter((item) => item.url && item.text)); } In production, emit one event per stage: query id, source, candidate count, timeout, filter count, and final rank. Do not log the full customer query if it can contain private shipment data. A trace should still let an operator reconstruct the answer from a stable document identifier and URL.
Ship the smallest trace first.
Here is a concrete walk-through. A warehouse manager searches for “new lithium-battery labeling rules.” The web stage may return a regulator notice, a carrier bulletin, and an old internal migration note. Their embeddings can be close because all three mention labels and batteries. The deterministic stage then checks the effective date and product region. The regulator notice gets a high authority value; the carrier bulletin gets a recent publication boost; the internal note is retained only if its version is still active. If the notice has no timestamp, the freshness term stays at zero instead of guessing. The final trace records those decisions, along with the eight-candidate cap and the 1,200 ms timeout. When a support engineer later sees an answer that cites the old migration note, they can tell whether the issue came from discovery, metadata, deletion lag, or the scoring weights. That is a much faster conversation than arguing about whether the language model “understood” the question. It also gives the ingestion owner a precise repair: fetch the corrected notice, replace the stale record, and rerun the same labeled query.
Keep the score components visible in dashboards. A histogram of similarity, freshness contribution, and authority contribution often reveals a bad weight before users file a ticket. Alert on sudden drops in candidate counts, spikes in timeout rate, and increases in results whose effective date is older than the configured window. These are retrieval alerts, not model alerts, so the on-call engineer can act without waiting for a prompt change.
Run a labeled evaluation set before rollout and after every re-index. Track recall at the chosen top-k, citation coverage, stale-result rate, and p95 retrieval latency. A green average can hide a bad tail: “latest carrier cutoff” deserves its own test slice, because a stale result there has an operational cost that a misspelled product adjective does not.
The limits you should accept up front
This design is not suitable when the source has no trustworthy timestamps or canonical identifiers; use a curated feed or a human approval step then. It is also a poor fit for exact transactional state, where a database lookup should outrank semantic similarity. Stick with a direct relational query when the user asks for a live inventory count.
The catch is operational work. Re-indexing changed content, removing deletions, labeling examples, and maintaining trace metadata take ownership across ingestion and search teams. A simpler single index can be reasonable for a small, slow-moving catalog, but it gives up the clean diagnosis that staged retrieval provides. Choose based on the failure you can afford to investigate at 02:00, not on a benchmark screenshot.

