Node.js Gaming Enrichment — 3 Keyword and Semantic Search Signals for Docs A developer outlined a Node.js contract for combining lexical and semantic search signals when enriching game catalog documentation, keeping embedding-provider response shapes out of the domain model. The approach stores source text, embedding model identifier and content version as a single revision, normalizes each adapter's scores into a documented 0..1 range, and fuses lexical and semantic candidates while recording component scores and freshness. The stated goal is making re-indexing deliberate and provider changes survivable. A game catalog changes under your feet. Publishers rename editions, players use abbreviations, and yesterday's description can disappear after a rights update. That operational constraint changes the answer: combine keyword and semantic retrieval, but keep both behind a small Node.js contract. Do not let an embedding provider's response shape become your catalog model. TL;DR: exact terms should go to lexical search, fuzzy intent should get an embedding signal, and the final rank should combine both with observable rules. Store the source text, embedding model identifier, and content version as one revision. That makes re-indexing deliberate and provider changes survivable. The before/after mental model is short. Before: description - one search service - result . After: versioned description - lexical index plus vector index - fused candidates - enrichment job , with trace context and outcome counters crossing every arrow. Three signals matter: exact-match rank, semantic similarity, and source freshness. Keyword search is excellent when a string carries identity. A query for NG+ , 4K , a platform code, or an exact expansion title should not be softened into a vague neighborhood. Lexical indexes also make token matches inspectable. An operator can explain why a document appeared without reverse-engineering a vector. Exact strings matter. Semantic search handles a different failure: two descriptions can express the same play style with few shared words. A player may ask for a cooperative puzzle game while a publisher writes about solving rooms with a friend. Embeddings can bring those descriptions closer, but similarity is not truth. It is one ranking input. The trade-off is concrete. Lexical-only retrieval misses paraphrases. Vector-only retrieval can bury decisive strings and makes model changes an index migration. A fused candidate set preserves both forms of evidence. Freshness is the third signal because a semantically close, retired edition is still the wrong input for enrichment. This is where portability starts. The application owns the meaning of a CatalogHit ; adapters own the mechanics of producing lexical and vector candidates. No provider-specific score crosses that boundary until it has been normalized and labeled. Keep the contract boring. It should accept stable domain data, return enough evidence to debug a rank, and expose no SDK classes. The example below is intentionally small, but it contains the fields that are painful to reconstruct later. export type CatalogDocument = { id: string; contentVersion: number; title: string; description: string; active: boolean; }; export type Candidate = { documentId: string; source: "lexical" | "semantic"; normalizedScore: number; contentVersion: number; }; export interface LexicalIndex { search query: string, limit: number : Promise