Knowledge Engine
The shared Stage 1 semantic-search core (src/services/KnowledgeEngine.ts) that every domain's "find similar records" feature is built on.
What This Engine Does
Knowledge Engine is the single, shared implementation of Stage 1 ("Embedding Engine") in TotalApp's two-stage hybrid AI pattern. Any screen that needs to rank a list of candidate records by semantic similarity to a query text — before handing a shortlist to a deeper AI analysis — goes through this one module. It embeds text via the user's local Ollama instance, ranks candidates by cosine similarity, applies an optional score threshold and top-K limit, caches computed embeddings for the session, and falls back to a keyword match when the embedding engine is unavailable.
In one sentence
Knowledge Engine is the only place in the codebase that calls the embedding model, the only place with the embedding cache, and the only place with the keyword fallback — every domain (Patent & R&D, Customs & Global Trade, and future add-ons) wraps it instead of reimplementing it.
The Two-Stage Hybrid Pattern This Engine Completes
Knowledge Engine is always Stage 1. Stage 2 is a separate, domain-specific engine (e.g. Patent Agent, or the Customs Trade Compliance audit) that reasons over the shortlist Stage 1 produced:
| Stage | Where it runs | What it does |
|---|---|---|
| Stage 1 | Entirely client-side, in src/services/KnowledgeEngine.ts | Embeds the query text and every candidate's content string via the user's own local Ollama instance (ollamaEngine.embed + cosineSimilarity), never touching the server. Domain wrappers (patentEmbeddingService.ts, customsEmbeddingService.ts, fintechEmbeddingService.ts) just supply the candidate shape and a content-formatting helper. |
| Stage 2 | Client-side (callClaude()) or server-side (a domain's own server/engines/*Engine.ts), depending on the user's Settings → Agentic choice | Takes the top Stage-1 matches and reasons over them — risk scoring, novelty classification, diagnostic summaries, etc. Only reached on the server when writerEngine is 'api' or 'local-cli'. |
Why does Stage 1 never reach the server?
ollama/local-llm/web-llm all run in the user's own browser or on their own machine — the Render server has no network path to a user's 127.0.0.1:11434 Ollama instance. This is why Knowledge Engine deliberately lives under src/services/, not server/engines/ — despite the name, it can never move server-side. "Engine" here names its role in the Stage 1 / Stage 2 pair, not a deployment location.
The Core Function — rankDocsByEmbedding()
Every domain wrapper calls one generic function:
| Parameter | Type | Purpose |
|---|---|---|
queryText | string | The user's search/input text to embed and rank candidates against. |
candidates | T extends { content: string } | Any array of objects with a content string — generic over the domain's own record shape. |
signal | AbortSignal (optional) | Standard cancellation, unchanged since the function's original (pre-consolidation) signature. |
options | { minScore?: number; topK?: number } (optional) | Purely additive 4th parameter. Omit it entirely for the original behaviour (no threshold, no limit) — existing callers that predate this parameter keep compiling and running unchanged. |
It returns { matches: EmbeddingRankedMatch<T>[]; degraded: boolean }. degraded: true means the embedding path failed and matches came from the keyword fallback instead — the function never throws.
Internal Mechanics
Embedding calls
Reads ollamaEmbeddingModel and ollamaBaseUrl from useUIStore.getState() (the user's Settings → Agentic selection). Both the query text and every candidate's content are embedded via ollamaEngine.embed(). Text longer than MAX_EMBED_CHARS (3000 characters) is truncated — local embedding models have a limited token context window, and Ollama rejects an oversized batch with a 500 rather than truncating server-side.
Cosine similarity
Uses the shared cosineSimilarity(a, b) primitive from knowledgeSearchService.ts — the same function My Knowledge / Attach Knowledge uses for its own ranking. Returns 0 for degenerate input (empty vector, length mismatch) rather than throwing or returning NaN.
In-memory embedding cache
A session-lifetime Map<string, number[]>, keyed by the embedded text itself (`${text.length}:${text}`) — not by a caller-supplied id. Two practical consequences: a changed record automatically invalidates its own cache entry (no separate updatedAt/version field needed), and the cache is safely shared across every domain (patent, customs, fintech) without any risk of cross-domain id collisions producing a wrong result — the key is derived from content, not from an id namespace. The cache is never persisted (page reload clears it) since embeddings are cheap to recompute and tied to whichever Ollama model happens to be configured at the time.
Threshold + top-K ordering
Matches are sorted by score, then the minScore threshold is applied, then the result is truncated to topK. This order matters: applying the threshold before truncation guarantees a weak match can never occupy a slot that should have gone to a stronger one that happened to be ranked just outside the naive top-K window.
Keyword fallback
Engaged whenever the embedding path can't produce a result: no embedding model configured, the embed call fails or returns an empty vector, or (when a threshold is set) every candidate falls below it. The query text is tokenized on whitespace/punctuation (tokens under 3 characters are dropped), and candidates are scored by the fraction of query tokens their content contains. This guarantees Stage 2 always receives at least a rough context, even with Ollama completely unavailable, rather than an empty list.
Domain Wrappers — Who Uses It
No screen calls Knowledge Engine directly. Each domain has a thin wrapper service that supplies its own candidate type and a content-formatting helper:
| Domain wrapper | Uses options? | Consumed by |
|---|---|---|
patentEmbeddingService.ts | Re-exports rankDocsByEmbedding unchanged — the 9 Patent & R&D Operations screens keep their original 3-argument calls; 6 of them now pass options to apply their own minScore/topK centrally instead of filtering client-side. | Patent Collision Scanner, Prior Art Search, Claims Matrix Comparer, FTO Risk Analysis, and 5 more Patent & R&D screens |
customsEmbeddingService.ts | Yes — { minScore: 0.6, topK: 5 } by default | Trade Compliance & Sanctions screen's "Search Similar Rules" step |
fintechEmbeddingService.ts | No — 12 functions, all built as infrastructure, none wired into their target screens' UI yet | Not yet consumed by any Financial Audit & Fintech Ops screen |
A new domain follows the same pattern: define a candidate type, write one format<Domain>ForEmbedding() helper that turns a record into a content string (reused for both the embedding candidate and the Stage 2 prompt's context block, so the two never drift apart), and call rankDocsByEmbedding() with sensible minScore/topK defaults.
End-User Documentation
This page is the technical/developer reference. For the plain-language explanation of this feature aimed at end users (what it does, where it appears, why the AI ranking runs locally) see Finding Similar Records under the Semantic Search section.
Known Limitations
- No real external corpus. Candidate pools are either static in-app data (Patent & R&D's
COMPETITOR_CANDIDATES) or the tenant's own operational records (Customs's sanction rule ledger) — never a live external database (a real patent office API, a real sanctions-list API). Wiring a real external source is a separate, larger integration. - 6 of 9 Patent & R&D screens migrated to the centralized
optionsparameter; 3 didn't need to (they apply no threshold/limit at all, or compare a single candidate). The remaining migration surface isfintechEmbeddingService.ts's 12 functions, none of which are wired into any screen's UI yet — connecting them is a separate, larger UI task per screen, not a Knowledge Engine change. - Always dependent on the user having Ollama running locally with an embedding model selected in Settings → Agentic; when it isn't, every caller degrades to the keyword fallback rather than failing outright.
Frequently Asked Questions
src/services/, not server/engines/?*Engine.ts modules, and it can never move server-side since it talks directly to the user's own local Ollama instance.patentEmbeddingService.ts had no cache or threshold at all, and an early version of customsEmbeddingService.ts applied its threshold after truncating to top-K, letting a weak match occupy a slot a stronger one deserved. Consolidating into one engine fixed the ordering bug once, in one place, and every domain wrapper now gets caching and fallback behaviour for free.options break any existing call site?undefined — every pre-existing 3-argument call keeps compiling and behaves exactly as before, including the 3 patent-rd-ops screens and the fintech wrapper functions that haven't migrated to it.Map instance.