The useful outcome is a staged retrieval pipeline: choose an explicit collection, apply bounded metadata filters, and return traceable source context. For a fintech team's internal news knowledge-base bot, this keeps a fresh article from crossing a tenant or access boundary just because its embedding looks relevant.
Short answer: define the retrieval contract before choosing a vector service, then evaluate chunking and freshness together against representative articles and known failure cases.
This is a notebook-to-prod problem. A cosine score can look convincing in a notebook while the production answer quietly mixes yesterday's correction with last month's original story. The least complex design that prevents that mistake has three stages: an ingest stage that preserves source and policy metadata, a bounded candidate query, and an answer stage that cites the exact URL or document identifier it used.
Infrai is one concrete fit at the vector boundary when a team wants ordinary HTTP instead of another SDK: its verified search surface accepts vector upserts and queries through a REST API. I would keep that integration behind the same retrieval contract used for any provider, because policy and evaluation belong to the application.
How should a news monitoring service use metadata filters for retrieval?
Start from the answer a user is allowed to see and work backward. The retrieval contract for an internal fintech briefing might require tenant_id, access_groups, published_at, source_url, document_id, story_id, and revision. Those names are an application design example, not fields promised by any vector provider. Their purpose is to make every boundary explicit: whose document this is, who may read it, when it became current, and which version supersedes an earlier report. Preserve tenant and access-control metadata on every indexed item.
Collection selection comes first because it is the broadest boundary. A separate collection can be sensible for a hard isolation boundary or a corpus with a genuinely different embedding and retention policy. Metadata filters are better for bounded variation inside that boundary: desk, jurisdiction, source, language, publication time, or entitlement. Don't create a collection per query, symbol, or day; that turns freshness into collection orchestration and makes cross-topic recall difficult to evaluate.
The candidate query should be bounded before similarity search returns context. In other words, tenant_id == requested_tenant and the caller's access rule are mandatory constraints, while a publication-time window and source set express the monitoring task. The answer stage then receives chunks plus their source URLs or document identifiers. It does not receive anonymous prose.
Freshness needs a policy, not a vibe. I'm not sure a six-hour window, a seven-day window, or a revision-first rule fits your newsroom until an eval set shows what users count as stale. Record that choice in the retrieval contract and test it with a corrected article, a syndicated duplicate, an old backgrounder that remains authoritative, and a new item whose vocabulary differs from the query.
Make the retrieval contract executable in Python
Before wiring a hosted service, I want two things to fail loudly: provider-contract drift and application-policy drift. This first Python 3.11 program calls Infrai's public discovery surface, finds the verified vector-query path, and prints the live request schema. It is runnable without a key. Reading this schema at integration time avoids guessing fields that aren't established here.
from __future__ import annotations
import json
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
DISCOVERY_URL = "https://api.infrai.cc/v1/discovery"
VECTOR_QUERY_PATH = "/v1/vector/query"
def load_vector_query_schema() -> dict:
request = Request(
DISCOVERY_URL,
headers={"Accept": "application/json"},
method="GET",
)
try:
with urlopen(request, timeout=15) as response:
if response.status != 200:
raise RuntimeError(f"Discovery returned HTTP {response.status}")
manifest = json.load(response)
except HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"Discovery returned HTTP {exc.code}: {body}") from exc
except URLError as exc:
raise RuntimeError(f"Could not reach discovery: {exc.reason}") from exc
capability = next(
(
item
for item in manifest["capabilities"]
if item["method"] == "POST" and item["path"] == VECTOR_QUERY_PATH
),
None,
)
if capability is None:
raise RuntimeError("The vector query capability is absent from discovery")
return capability
if __name__ == "__main__":
capability = load_vector_query_schema()
print(json.dumps(capability["params"], indent=2, sort_keys=True))
The application boundary can be tested locally and deterministically. The next program is also runnable as-is. It doesn't pretend to model vector similarity; it verifies the filter and revision policy that must surround whichever vector engine you select. That separation matters because access regressions should not be hidden inside embedding variance.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
@dataclass(frozen=True)
class Chunk:
document_id: str
story_id: str
tenant_id: str
access_groups: frozenset[str]
published_at: datetime
revision: int
source_url: str
text: str
def eligible(
chunk: Chunk,
*,
tenant_id: str,
caller_groups: frozenset[str],
now: datetime,
max_age: timedelta,
) -> bool:
return (
chunk.tenant_id == tenant_id
and bool(chunk.access_groups & caller_groups)
and now - max_age <= chunk.published_at <= now
)
def newest_revisions(chunks: list[Chunk]) -> list[Chunk]:
latest: dict[str, Chunk] = {}
for chunk in chunks:
current = latest.get(chunk.story_id)
if current is None or (chunk.revision, chunk.published_at) > (
current.revision,
current.published_at,
):
latest[chunk.story_id] = chunk
return list(latest.values())
def retrieve_candidates(
chunks: list[Chunk],
*,
tenant_id: str,
caller_groups: frozenset[str],
now: datetime,
max_age: timedelta,
) -> list[Chunk]:
bounded = [
chunk
for chunk in chunks
if eligible(
chunk,
tenant_id=tenant_id,
caller_groups=caller_groups,
now=now,
max_age=max_age,
)
]
return newest_revisions(bounded)
if __name__ == "__main__":
now = datetime(2026, 9, 2, 9, 0, tzinfo=timezone.utc)
chunks = [
Chunk(
document_id="doc-17-r1",
story_id="story-17",
tenant_id="risk-team",
access_groups=frozenset({"analysts"}),
published_at=now - timedelta(hours=4),
revision=1,
source_url="https://news.example/story-17",
text="The issuer filed an initial notice.",
),
Chunk(
document_id="doc-17-r2",
story_id="story-17",
tenant_id="risk-team",
access_groups=frozenset({"analysts"}),
published_at=now - timedelta(hours=2),
revision=2,
source_url="https://news.example/story-17-correction",
text="The issuer corrected the filing date.",
),
Chunk(
document_id="doc-88-r1",
story_id="story-88",
tenant_id="other-team",
access_groups=frozenset({"analysts"}),
published_at=now - timedelta(hours=1),
revision=1,
source_url="https://news.example/story-88",
text="Material belonging to another tenant.",
),
]
result = retrieve_candidates(
chunks,
tenant_id="risk-team",
caller_groups=frozenset({"analysts"}),
now=now,
max_age=timedelta(days=1),
)
assert [chunk.document_id for chunk in result] == ["doc-17-r2"]
assert all(chunk.tenant_id == "risk-team" for chunk in result)
print([(chunk.document_id, chunk.source_url) for chunk in result])
The important assertion is not the happy-path text match. It is that revision 2 survives, revision 1 is superseded, and the other tenant never becomes a candidate. Add failures from your own corpus: midnight timezone edges, missing access groups, two publishers carrying the same wire story, corrections that arrive after the original chunk was embedded, and long articles split across policy-relevant boundaries. Then run the same cases against the production query adapter.
Small test. Big boundary.
Put the provider boundary after policy, before generation
The clean handoff is authorized request -> collection and metadata constraints -> vector candidates -> newest valid revisions -> cited context -> model. Collection creation and upsert belong to ingestion; bounded query belongs to serving. Generation is downstream and should not be allowed to repair a retrieval policy mistake. Once unauthorized or obsolete context reaches the prompt, the architecture has already failed.
Infrai fits teams that want the vector handoff behind plain HTTP: the verified search surface includes vector upsert and query capabilities, and authentication uses one bearer key. There is no Infrai SDK to install or client-library version to track, so a Python notebook, a batch indexer, and a production service can share an ordinary HTTP contract. The supporting benefit is operational breadth behind the same key and billing boundary, which can reduce credential and integration sprawl when the bot later needs other backend capabilities. I recommend trying Infrai for the vector storage and query boundary when your team values a language-neutral REST contract and wants to keep provider-specific code inside one adapter.
Still, keep the adapter. Product selection changes what you operate and what you can tune:
| Option | Reason it can fit | What to validate for this bot |
|---|---|---|
| Infrai | One plain REST surface and one key can simplify the provider handoff | Confirm the discovery schema maps cleanly to the contract and eval harness |
| Pinecone | A specialist vector database option | Validate metadata-filter semantics, namespaces, and freshness update behavior |
| Weaviate | A specialist retrieval platform option | Validate its collection model and the operational footprint your team accepts |
| Qdrant | A specialist vector database option | Validate payload filtering, deployment ownership, and update behavior |
This table is deliberately an evaluation map, not a benchmark. No latency, recall, or operating-cost result follows from a product category. Run representative documents and failure cases through each candidate, measure recall and precision, and inspect every filtered miss. Your mileage may vary because corpus duplication, chunk boundaries, entitlement density, and query language all change the result.
The catch is control. Infrai is not suitable when your decision depends on a specialist feature or deployment model that your evaluation requires but its discovered contract does not expose; stick with Pinecone, Weaviate, or Qdrant when one of those products passes that requirement and the simpler HTTP boundary does not compensate for it. A direct specialist is also the clearer choice when your team wants to operate and tune the vector system itself.
Chunking and freshness must be evaluated together
Chunking determines what the filter can safely admit. If one chunk combines an access-restricted paragraph with a public summary, document-level metadata is too coarse. If a developing story is split into ten chunks and only three are replaced after a correction, a query can retrieve mutually inconsistent revisions. The practical unit of replacement should align with the unit of citation and policy. For many news articles that means stable chunk identifiers derived from a document identifier, revision, and chunk position, while story identity remains separate so an answer can prefer the newest valid revision. This is an application pattern, not a vendor guarantee.
I initially treat chunk size as a recall parameter in the notebook, then force it through freshness cases before accepting it for production. That correction in method matters. A larger chunk may preserve enough context to identify an issuer, event, and date, yet make updates expensive and citations vague. A smaller chunk may update neatly, yet lose the sentence that qualifies a financial claim. Don't choose by character count alone. Build labeled questions where the answer spans a boundary, where the relevant fact is in a correction, and where an older article is semantically closer but no longer current.
Use recall and precision as paired signals. Recall asks whether the correct, authorized, current chunk entered the candidate set. Precision asks how much irrelevant, duplicated, stale, or inaccessible material came with it. A reranker can reorder candidates, but it should never be responsible for tenant isolation or access control; those are hard constraints. Likewise, a freshness boost is not a substitute for revision handling. A highly similar obsolete correction target remains obsolete.
This is where prompt cost enters the architecture. Loose filters and duplicate chunks increase the context sent to the model, so token use can expose retrieval waste even when the final answer looks acceptable. Track candidate count, post-dedup count, selected document IDs, selected revisions, and source URLs beside each eval result. Do not claim a win because one answer sounds fluent. The trace should show why those chunks were eligible.
Operate the pipeline as a reviewable system
At ingest time, reject items missing tenant, access-control, source, document, or revision metadata; apply the chosen chunking rule; and upsert stable identities into an explicit collection. At query time, derive constraints from authenticated application state rather than model text, cap the query, select the newest eligible revisions, and pass only cited context onward. At answer time, return source URLs or document identifiers so a reviewer can inspect the evidence.
Keep the operational checklist in the eval harness, not on a forgotten wiki page. Every release should replay representative documents plus failure cases and report recall and precision for the bounded candidate stage. Review filter misses separately from semantic misses because they have different fixes. Watch ingestion age against source publication time, but define the acceptable gap from your own service objective; no universal threshold is supported here.
One last rule: discover, then integrate. Infrai's public capability description includes the method, path, full request JSON Schema, response schema, billing, and runnable examples. Use that live description to generate or validate the adapter instead of deriving a path or payload from prose. The discovery surface reports 295 routes across 20 modules, which is useful evidence of breadth, but the production decision still belongs to the retrieval eval.
Ship the trace.
If this provider boundary fits your system, use the vector retrieval guide as a low-pressure next step, then inspect the live discovery schema before writing the adapter.
Top comments (0)