TotalApp Docs

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 1 — Knowledge Engine (this file, always local Ollama) Stage 2 — a domain's own AI Synthesis engine (writerEngine-routed)
StageWhere it runsWhat it does
Stage 1Entirely client-side, in src/services/KnowledgeEngine.tsEmbeds 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 2Client-side (callClaude()) or server-side (a domain's own server/engines/*Engine.ts), depending on the user's Settings → Agentic choiceTakes 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:

ParameterTypePurpose
queryTextstringThe user's search/input text to embed and rank candidates against.
candidatesT extends { content: string }Any array of objects with a content string — generic over the domain's own record shape.
signalAbortSignal (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 wrapperUses options?Consumed by
patentEmbeddingService.tsRe-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.tsYes — { minScore: 0.6, topK: 5 } by defaultTrade Compliance & Sanctions screen's "Search Similar Rules" step
fintechEmbeddingService.tsNo — 12 functions, all built as infrastructure, none wired into their target screens' UI yetNot 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 options parameter; 3 didn't need to (they apply no threshold/limit at all, or compare a single candidate). The remaining migration surface is fintechEmbeddingService.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

Why is this called an "Engine" if it lives in src/services/, not server/engines/?
"Engine" names its architectural role — the Stage 1 half of the hybrid engine pair, symmetric with a domain's Stage 2 "AI Engine" — not a file-location rule. It's the browser-side counterpart to the server-side *Engine.ts modules, and it can never move server-side since it talks directly to the user's own local Ollama instance.
Why was this extracted instead of leaving each domain with its own copy?
Before extraction, three modules had independently written near-identical embed-loop-and-rank logic with subtly different bugs — the original 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.
Does adding options break any existing call site?
No. It's a purely additive 4th parameter with a default of 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.
Is the embedding cache shared across different add-ons?
Yes, by design and safely — the cache key is derived from the embedded text's own content, not from a caller-supplied id, so there's no cross-domain collision risk even though Patent, Customs, and (once wired) Fintech all share the same Map instance.