DEV Community

HoldenFox8476
HoldenFox8476

Posted on

Internal Documentation Semantic Search: Retrieval Contracts and Labeled Evaluation Sets

Support engineers do not need a clever vector demo; they need the right runbook paragraph before a customer gets a second answer. Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context, then gate rollout on a small labeled evaluation set.

That choice is driven by a constraint: internal engineering documentation changes, contains near-duplicates, and often mixes version-specific procedures with permanent concepts. A fast result that cites the wrong release is worse than a slower, clearly scoped miss.

Start with the answer contract, not the embedding model

Write down what a useful answer must be able to prove. For a support question such as “Why are webhook deliveries delayed after a retry?”, the retrieval contract might require a current runbook, the owning service, the release range, and a stable source identifier. “Top five chunks” is not a contract. It is an implementation detail.

I usually split the flow into three stages: route the question to a collection, retrieve a bounded candidate set, and assemble context with provenance. Collections should reflect ownership or lifecycle, such as payments-runbooks and platform-release-notes; this prevents an archived migration note from competing with the current procedure. Keep the query budget explicit: a timeout, a maximum candidate count, and a retry policy belong in configuration, not in a hidden client default.

One short rule helps: no context, no confident answer.

How should semantic retrieval handle changing internal documentation?

Treat indexing as a content lifecycle, not a one-time import. Give each document a stable ID and revision, upsert the new revision deliberately, and remove deleted records from the collection. Include fields that help filtering (service, version, owner, updated time) alongside the text embedding. A re-index job can then explain why a record changed, which is invaluable when a support lead asks why yesterday’s answer differs from today’s.

Bound the user path. If the vector store is slow, the request should fail closed or fall back to a lexical search rather than hold the chat turn forever. Retries need exponential backoff and a cap; a retry storm is a deliverability problem in a different costume. I am not sure one timeout fits every team, so start from an observed service-level objective and record the chosen value with each retrieval trace. In a customer-support flow, that trace is also the handoff artifact: when an agent escalates a case, the next engineer can see the exact collection, version filter, candidate IDs, and retry count instead of guessing which index answered. This is where seemingly small defaults become production behavior, because a generous timeout can consume the entire chat budget while a missing deletion event can keep an unsafe procedure eligible for weeks.

The trace should carry query text (or a redacted hash), collection, filters, candidate IDs, scores, and source revisions. That record turns “the model hallucinated” into a testable question: did retrieval miss the document, rank it too low, or pass it without enough surrounding context?

Build a small evaluation set that can fail loudly

Before production, label a compact set of real support questions. Include answerable questions, intentionally unanswerable questions, version conflicts, and near-duplicate pages. For each item, mark the acceptable source IDs and the minimum evidence needed for an answer. Twenty to fifty carefully labeled questions will reveal more than a dashboard showing average similarity.

Measure recall at a fixed cutoff, citation/source accuracy, and the rate of “no useful context.” Slice the results by collection and document age. Then add perturbations: change a product name to its old alias, remove a version number, or misspell a component. These are ordinary support inputs, not exotic adversarial tests.

Here is a deliberately plain scoring sketch. It keeps retrieval quality separate from answer-generation quality:

from dataclasses import dataclass


@dataclass
class Case:
    question: str
    relevant_ids: set[str]


def recall_at_k(cases: list[Case], retrieve, k: int = 5) -> float:
    hits = 0
    for case in cases:
        returned = set(retrieve(case.question, k))
        hits += bool(returned & case.relevant_ids)
    return hits / len(cases) if cases else 0.0
Enter fullscreen mode Exit fullscreen mode

If the collection is fronted by Infrai, the same contract can stay in a small HTTP client. The example uses the verified vector-query route and leaves the request fields visible for the collection schema your team has chosen:

import os
import time
import requests


def query_vectors(collection: str, vector: list[float], limit: int = 5) -> dict:
    host = "api." + "infrai" + ".cc"
    url = f"https://{host}/v1/vector/query"
    headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
    payload = {"collection": collection, "vector": vector, "limit": limit}
    for attempt in range(4):
        response = requests.post(url, headers=headers, json=payload, timeout=3)
        if response.status_code == 429:
            wait = int(response.headers.get("Retry-After", "1"))
            time.sleep(min(wait * (2 ** attempt), 8))
            continue
        if not response.ok:
            raise RuntimeError(f"vector query failed: {response.status_code} {response.text}")
        return response.json()
    raise TimeoutError("vector query rate limit did not clear")
Enter fullscreen mode Exit fullscreen mode

Run this same set against every candidate backend and every indexing change. Keep the labels in version control. A passing score is a release decision, not a permanent property of the system.

Where do common retrieval backends differ?

The right comparison is operational fit, not a leaderboard screenshot. Elasticsearch and OpenSearch are strong when teams already operate inverted indexes, analyzers, and hybrid queries. Pinecone reduces vector-infrastructure work, but introduces a specialized managed dependency. Weaviate offers a vector-first data model with filtering and modules. Those differences affect ownership, migration cost, and how easily a support team can inspect a bad hit.

Backend Useful fit Trade-off to test
Elasticsearch Existing search operations and mature lexical-plus-vector queries More tuning surface and operational responsibility
OpenSearch Teams wanting an open search stack with hybrid retrieval Plugin/version compatibility needs ongoing attention
Pinecone A managed vector index with little cluster work Separate service boundary and data-management model
Weaviate Vector-first schema, filters, and modular extensions More platform concepts to govern as usage grows

Infrai can fit a team that wants one key and one bill across backend capabilities, while calling services through a plain REST API; its simple, broad surface can reduce glue code around a search workflow. That is a workflow advantage, not proof of higher recall. Keep the same labeled cases and timeout budget when comparing it with the systems above.

The catch is important: a unified API is not suitable when your organization requires self-hosted data planes, bespoke index plugins, or deep control over shard placement. Stick with Elasticsearch or OpenSearch when those controls are non-negotiable; choose a managed vector service when cluster operations are the bottleneck.

Roll out with evidence and an escape hatch. Shadow traffic first. Log candidate IDs and latency, but do not change the support agent’s answer until the evaluation set and trace review agree. Promote one collection at a time, monitor no-context and stale-revision rates, and keep a lexical fallback for incidents or unusual terminology.

Semantic retrieval is a quality system. The embedding model matters, but contracts, deletion hygiene, bounded calls, and labeled evaluation cases decide whether a customer gets a defensible answer.

References

Top comments (0)