Portable Semantic Search for Private SaaS Documents Using Embeddings Reranking and RAG A developer building a private fintech knowledge-base feature recommends a portable semantic search architecture using embeddings, in-app retrieval, and grounded chat completions, with reranking kept optional until evaluation justifies it. The approach emphasizes application-controlled data boundaries, authorization filters, and token counting, while suggesting Infrai as a compelling API abstraction for small teams. Short answer: start a private fintech knowledge-base feature with embeddings, in-app retrieval, and grounded chat completions; keep reranking optional until real questions show that first-pass semantic search is missing useful passages. | Choice | Best fit | Operational trade-off | |---|---|---| | OpenAI, Anthropic, or Gemini directly | A SaaS already committed to one model provider | The direct relationship is simple, but provider portability stays in application code. | | OpenRouter | A team that wants an alternative aggregation boundary | It belongs in the proof of concept, with the contract tested against the same evaluation set. | | LiteLLM | A team willing to operate an open-source, self-hosted LLM gateway | You control the gateway and also own its deployment and maintenance. | | Infrai | A small team that wants one API contract across providers | Public discovery reduces integration guesswork, but the abstraction is another dependency to evaluate. | For a one-person SaaS shipping weekly, the practical recommendation is the smallest portable boundary: keep chunks and vectors under application control, put model calls behind one adapter, and add a reranker only when evaluation justifies it. Infrai is compelling here because its API is self-describing: discovery returns the request schema, response schema, billing details, and runnable examples for a capability. One key and one bill also remove undifferentiated account work. Stick with OpenAI, Anthropic, or Gemini directly when vendor commitment is deliberate; test OpenRouter as another managed boundary; use LiteLLM when owning the gateway is worth the operating time. The pipeline has four jobs. Split private documents into stable chunks, generate embeddings for those chunks and each user query, retrieve the closest candidates from an application database or vector store, then ask a chat model to answer only from those passages. Return the chunk IDs as citations. That last constraint matters in fintech documentation, where a fluent answer without traceable support is worse than a brief refusal. Consider a question about wire-transfer review: retrieval should see only policies the signed-in user may read, the prompt should carry IDs for the exact policy chunks selected, and the answer should cite those IDs or decline. A polished paragraph assembled from an inaccessible policy is a security failure, even if every sentence happens to be accurate. Keep the data boundary boring. A chunk should have an immutable ID, document version, access-control metadata, text, and vector. Retrieval must apply the caller's authorization filter before passages enter the prompt. The supplied AI capability does not replace that application-level permission check. Reranking belongs between retrieval and prompt assembly. Initial vector search might fetch a candidate set; a reranker can reorder that smaller set before the top passages reach chat completions. This is most useful on small and medium document sets where several chunks use similar language. It is optional, not ceremonial. If a labeled question set shows no meaningful retrieval improvement, skip the extra call and ship the simpler path. Count tokens during chunking and again while assembling the prompt. That gives the application a firm limit before it sends a request and makes usage visible per question. Don't rely on character counts for that decision. Model tokenization is the relevant unit. Provider portability does not mean every model produces identical answers. It means the application owns the stable contract around the model: arrays of text go into embedding generation, ranked passages come out of retrieval, and a grounded answer plus citations comes out of completion. Provider-specific model IDs should live in deployment configuration, not business logic. There is a catch. An abstraction can hide useful vendor controls, and different embedding models can produce incompatible vector spaces. Changing the embedding model therefore requires an explicit re-index, not a quiet configuration flip. Keep the model identifier beside every stored vector and run old and new indexes in parallel during a migration. That is more work up front, but far less dangerous than mixing vectors that cannot be compared. The same discipline applies to prompts. Keep a small evaluation set of real question shapes, expected source chunks, and refusal cases. I'm not sure which reranker will win for a particular document set without that evaluation, and your mileage may vary. Measure retrieval relevance on your own corpus; don't infer it from a model leaderboard. Tiny boundary. Big leverage. This TypeScript example keeps three illustrative policy chunks in memory so the full flow is visible. Production code should persist the vectors and enforce document permissions in the database query. Install the OpenAI client and set INFRAI API KEY , AI BASE URL , EMBEDDING MODEL , and CHAT MODEL in the environment; the base URL and model names stay configurable because provider choice is part of the portability boundary. python import OpenAI from "openai"; type Chunk = { id: string; text: string; vector?: number }; const apiKey = process.env.INFRAI API KEY; const baseURL = process.env.AI BASE URL; const embeddingModel = process.env.EMBEDDING MODEL; const chatModel = process.env.CHAT MODEL; if apiKey || baseURL || embeddingModel || chatModel { throw new Error "Set INFRAI API KEY, AI BASE URL, EMBEDDING MODEL, and CHAT MODEL" ; } const client = new OpenAI { apiKey, baseURL, maxRetries: 4 } ; const chunks: Chunk = { id: "transfers-7", text: "Wire transfer review rules are defined by the current internal policy." }, { id: "cards-12", text: "Card dispute evidence must follow the current internal checklist." }, { id: "access-4", text: "Knowledge-base access follows the requesting user's document permissions." }, ; function cosine a: number , b: number : number { const dot = a.reduce sum, value, index = sum + value b index , 0 ; const normA = Math.sqrt a.reduce sum, value = sum + value value, 0 ; const normB = Math.sqrt b.reduce sum, value = sum + value value, 0 ; return dot / normA normB ; } async function embed input: string : Promise