CDS pluginCDS plugintypeInstalls into a CAP project and extends the runtime. CAP loads it through cds.requires or the plugin hook.See every one →What this type is →independently maintainedindependently maintainedoriginPublished by one person or a small company outside SAP. Says nothing about quality; the readings say that.See every one →

@saptarishi/cds-plugin-vector-hana

Vector storage and similarity search on SAP HANA from CAP, packaged as a plugin so the embedding column and the query are modelled rather than hand-written.

Keeps an embedding beside each row and searches it by cosine similarity, through two backends with one API: a column table using REAL_VECTOR and COSINE_SIMILARITY in SAP HANA Cloud, or a JSON column in a local SQLite file with the cosine computed in JavaScript, so the same code runs on a laptop and against a subscribed database. The modelled route is a @rag annotation on a CDS entity naming the fields to embed and the vector size: the plugin builds the vector table, embeds and upserts the row's projected text after every CREATE and UPDATE, removes the vector before DELETE, and declares two collection-bound OData actions on the entity — searchByMeaning for ranked rows, askAbout for an answer together with the rows it was drawn from. What gets embedded is one row's projected fields as one vector, and the embedding and chat calls are not its own: it takes any object with an embed() method, and the README composes it with the same author's @saptarishi/cds-plugin-llm, a required peer dependency and where the model provider is chosen. @sap/cds is an optional peer dependency, so the two stores and the RAG helper can also be constructed in plain Node.

Facts

Latestv0.13.0ReleasedLast pushLicenceApache-2.0

Where it sits

Lives in
Provides
Retrieval-augmented generationCAPRetrieval-augmented generationcapabilityGrounding a model's answer in data retrieved at question time: text split into chunks, each chunk embedded as a vector, and the chunks nearest to the question placed into the prompt. Semantic search is the same retrieval with a person reading the matches, and fine-tuning changes the model's weights, not its input.
Flavors
Node.jsfrom its runtime
Requires
@sap/cds≥ 7@sap/cdsartifactThe peer range, as published.
Optional
SAP HANA CloudSAP HANA CloudartifactThe HanaVectorStore backend is used; the SQLite backend runs without it.vector store

Who is behind it

kalyanjanumpallykalyanjanumpallyPersonMaintains this
In the atlas: maintains 2
@kalyanjanumpally
maintainerJanumpally KalyanPersonContributes to this
In the atlas: contributes to 2
@janumpallykalyan
1 contributor

Before choosing this

Conditions

LicenceSAP Developer License Agreement 3.2 CAP§ 1, 2, 3A cds-plugin is used with SAP CAP materials, or an artifact is explicitly identified as a CAP plugin or application.
  • § 1 License. Productive customer applications are limited to SAP BTP or another SAP-licensed platform, and mass extraction to a non-SAP product is restricted except where solely required for SAP interoperability.
  • § 2 Intellectual Property. The agreement retains SAP's rights in the covered materials and limits making APIs, tools or software available to third parties, while preserving ownership of customer applications subject to SAP's underlying rights.
  • § 3 Artificial Intelligence Training. The agreement prohibits use of the covered materials and customer applications for training artificial-intelligence models or systems.
SAP Developer License Agreement 3.2 CAP →

Also, from Retrieval-augmented generation§ 3: Retrieval and inference are not automatically AI training. The section becomes relevant if the indexed or retrieved covered material is instead used for training, developing or refining the model or system.

What it declares

Every artifact

Type
CDS plugin
Origin
Independently maintained
Runtime
Node.js

As a CDS plugin

Declared for
>=7
Type declarations
declared
Distributed through
does not say

Tasks this contributes to1Beta

Answer a question from records in a CAP serviceA question asked in plain words is answered from rows the CAP model already holds: the records nearest to the question in meaning are found and put in front of a language model, so the answer rests on those records rather than on what the model remembers.Coversvector table and search
What it does

Moves the embed-on-write and the search into the model: a @rag annotation on an entity names the fields to embed, the plugin builds the vector table and keeps it in sync on every CRUD operation, and what is embedded is one row's projected fields as one vector. A SQLite fallback ships with it, so the same code runs on a laptop without SAP HANA Cloud.

Remaining work

Choose the fields to embed and install a provider for the embedding and chat calls, which are not in this package. A record whose text does not fit one vector is not addressed: the README describes no chunking. The README says its HANA backend is verified against a mock hdb client and not yet against a HANA Cloud instance, which it names as its top open item; the SQLite backend is what its integration tests exercise.

All options on the Retrieval-augmented generation page →

Connections2

@saptarishi/cds-plugin-vector-hana connections@saptarishi/cds-plugin-vector-hanaPROVIDES→ provides1provides stated Answers the storage and retrieval half inside a CAP project: a vector table per @rag-annotated entity kept in sync by the plugin's own CRUD handlers, top-K cosine search with an optional metadata filter, hybrid vector-and-keyword retrieval fused by reciprocal rank, and a RAG helper that assembles the hits into a citation-tagged prompt and calls a chat provider, with a streaming variant that returns the hits before the first token. The embedding and chat models come from a separate provider plugin, and the embedded unit is one row's projected fields. kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana README: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/README.md kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana README: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/README.md kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana README: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/README.md kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana README: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/README.md kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana README: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/README.md kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana CHANGELOG: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/CHANGELOG.md kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana CHANGELOG: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/CHANGELOG.mdRetrieval-augmented generationNEEDS→ optionally needs1optionally needs stated If: The HanaVectorStore backend is used; the SQLite backend runs without it. kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana README: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/README.md kalyanjanumpally/sap-joule-ai-plugin — cds-plugin-vector-hana README: https://raw.githubusercontent.com/kalyanjanumpally/sap-joule-ai-plugin/main/cds-plugin-vector-hana/README.mdSAP HANA Cloudif

Install

npm add @saptarishi/cds-plugin-vector-hana

Derived from the package name. No recipe recorded; the README has the configuration.

Where it sits in your app

Download
Your CAP applicationYour SAP BTP subaccountdocumentedmay needif The HanaVectorStore backendis used; the SQLite backendruns without it@saptarishi/cds-plugin-vector-hana, module@saptarishi/cds-plug…SAP HANA Cloud, service instanceSAP HANA Cloud
SAPnot SAP's, or the project's ownunder a condition

README

quoted from kalyanjanumpally/sap-joule-ai-plugin under Apache-2.0, read
kalyanjanumpally/sap-joule-ai-plugin/cds-plugin-vector-hana/README.mdOn GitHub

@saptarishi/cds-plugin-vector-hana

npm license

Vector store for SAP CAP that speaks HANA Cloud's native REAL_VECTOR + COSINE_SIMILARITY. Ships with a SQLite fallback so you can build and test semantic-search features on your laptop without paying for HANA.

Composes with @saptarishi/cds-plugin-llm for embeddings — bring any provider (Ollama, OpenAI, Groq, GenAI Hub).

Install

npm install @saptarishi/cds-plugin-vector-hana @saptarishi/cds-plugin-llm

# then choose a backend:
npm install better-sqlite3    # for local dev
# OR
npm install hdb               # for HANA Cloud

Both drivers are optional peer deps — install only what you need.

Use — SQLite (dev)

const { GroqLLMService } = require('@saptarishi/cds-plugin-llm');
const { SqliteVectorStore } = require('@saptarishi/cds-plugin-vector-hana');

// Any embed()-capable LLMService — Ollama is a good local pick, but Groq /
// OpenAI-compat work too.
const embed = new GroqLLMService('emb', null, { modelId: 'text-embedding-3-small' });
await embed.init();

const store = new SqliteVectorStore({
  embed,
  dimension: 1536,                  // must match your embedding model's output
  table: 'supplier_contracts',
  dbPath: './contracts.sqlite',     // or ':memory:' for tests
});
await store.init();

// Index some documents
await store.upsert({
  id: 'C-4711',
  text: 'Master supply agreement with Acme Steel GmbH for cold-rolled coils, EUR pricing, Incoterms 2020 DDP, valid through 2027-12-31.',
  metadata: { supplier: 'Acme Steel', category: 'raw-materials', region: 'EMEA' },
});

// Query
const hits = await store.search({
  text: 'which contracts cover steel supply in Europe?',
  topK: 5,
  filter: { region: 'EMEA' },       // optional metadata filter
});

hits.forEach(h => {
  console.log(`${h.score.toFixed(3)}  ${h.id}  ${h.text.slice(0, 60)}...`);
});

Use — HANA Cloud (production)

Identical API — only the class name changes:

const { HanaVectorStore } = require('@saptarishi/cds-plugin-vector-hana');

const store = new HanaVectorStore({
  embed,
  dimension: 1536,
  table: 'SUPPLIER_CONTRACTS',
  connection: {                     // standard hdb options
    host: '<subaccount>.hanacloud.ondemand.com',
    port: 443,
    user: process.env.HANA_USER,
    password: process.env.HANA_PASSWORD,
  },
});
await store.init();

// Same upsert / search / delete API as SQLite backend.

On BTP with a HANA service binding: extract credentials from the binding and pass as connection directly — same shape.

HNSW vector index (new in v0.3.0)

For production-scale corpora (tens of thousands of rows and up), exhaustive COSINE_SIMILARITY scans over every row will eventually become the bottleneck. Pass an index option and init() creates an HNSW graph index for approximate-nearest-neighbor search:

const store = new HanaVectorStore({
  embed,
  dimension: 1536,
  table: 'SUPPLIER_CONTRACTS',
  connection: { /* ... */ },
  index: {
    type: 'hnsw',
    similarity: 'cosine',                       // or 'l2'
    // name: 'MY_IDX',                          // defaults to '<table>_<column>_HNSW_IDX'
    buildParameters: { ef_construction: 200, M: 64 },
  },
});
await store.init();

Emits:

CREATE HNSW VECTOR INDEX "SUPPLIER_CONTRACTS_embedding_HNSW_IDX"
  ON "SUPPLIER_CONTRACTS" ("embedding")
  SIMILARITY FUNCTION COSINE_SIMILARITY
  BUILD PARAMETERS ('ef_construction=200,M=64')

Requires HANA Cloud QRC 2/2024 or later (when HNSW GA'd). Search queries are unchanged — HANA transparently uses the index when COSINE_SIMILARITY is in the ORDER BY.

@rag actions auto-declared on OData (new in v0.6.0 / v0.7.0)

Every @rag-annotated entity gets two collection-bound OData actions out of the box — zero handler code:

POST /odata/v4/app/Suppliers/AppService.searchByMeaning
Content-Type: application/json

{ "query": "steel coils shipped from Europe", "topK": 5 }

→ value: [ <Suppliers row>, ... ] in relevance order. The plugin runs the vector search, projects hit IDs back to the entity via SELECT ... WHERE ID IN (...), and re-sorts to match the hit ranking (SQL doesn't preserve it).

POST /odata/v4/app/Suppliers/AppService.askAbout
Content-Type: application/json

{ "query": "Which suppliers can ship steel coils to Germany within two weeks?", "topK": 5 }

→ { answer: "...", sources: [ <Suppliers row>, ... ] }. Handler runs the full RAG pipeline (retrieve → augment → chat) and returns both the LLM answer AND the source rows (in hit-rank order) so the caller can render citations. Optional systemInstructions field on the request body overrides the default "answer from context only, cite by [id]" instruction per-call.

Opt-outs and overrides via @rag.actions:

} @rag: {
  fields:    ['name', 'description'],
  dimension: 768,
  actions: false                                    // disable ALL auto-declared actions
  // or:  actions: { ask: false }                   // disable just askAbout
  // or:  actions: { search: false, ask: false }    // disable both
  // or:  actions: { search: 'findSuppliers',
                     ask:    'answerAbout' }        // custom action names
};

If the entity already declares its own actions.searchByMeaning or actions.askAbout (user-written, or another plugin's), the auto-declaration is skipped with a warning — the plugin never overwrites developer code.

The synthesized return type for askAbout — <ServiceName>.<EntityShort>AskAboutResult — is added to cds.model.definitions at load time. Bring your own type by that name if you want a richer shape (e.g., adding a citations field); the plugin will re-use it.

@rag annotation — auto-indexed entities (new in v0.5.0)

Skip the boilerplate. Annotate a CDS entity with @rag and the plugin builds a vector table, keeps it in sync on every CRUD, and hands you two ergonomic operations on cds.vectorHana:

entity Suppliers {
  key ID     : UUID;
  name       : String;
  description: LargeString;
  country    : String;
} @rag: {
  fields:    ['name', 'description'],  // projected + embedded per row
  dimension: 768,                      // your embedding model's vector size
  store:     'sqlite',                 // or 'hana'
  topK:      5,                        // default topK for searchByMeaning
};
// Anywhere in a CAP handler
const hits = await cds.vectorHana.searchByMeaning({
  entity: 'AppService.Suppliers',
  query:  'steel coils shipped from Europe',
});

const { answer, hits: sources } = await cds.vectorHana.askAbout({
  entity: 'AppService.Suppliers',
  query:  'Which suppliers can ship steel coils to Germany within two weeks?',
});

The plugin registers after CREATE|UPDATE handlers so every save embeds + upserts the row's projected text into the vector table, and a before DELETE handler so the vector goes when the row does. Rows where every projected field is empty are silently skipped — no dead vectors clogging your search.

Optional bits:

  • @rag.provider — cds.services alias for the embedder (default 'llm').
  • @rag.chatter — separate alias for askAbout()'s chat provider (default: same as provider). Useful when you want to embed with a cheap local model and chat with a smart one.
  • @rag.table — override the derived table name (default: <entity>_vec).
  • cds.vectorHana.backfill('AppService.Suppliers') — re-index every existing row. Use when enabling @rag on an already-populated table.
RAG in one call (new in v0.4.0)

Compose the store with any @saptarishi/cds-plugin-llm provider to get retrieval + citation-tagged prompt + chat generation in a single call:

const { RAG, SqliteVectorStore } = require('@saptarishi/cds-plugin-vector-hana');
const { OllamaLLMService } = require('@saptarishi/cds-plugin-llm');

const embedder = new OllamaLLMService({ credentials: { embeddingModel: 'nomic-embed-text' } });
const chatter  = new OllamaLLMService({ credentials: { model: 'qwen2.5:14b' } });

const store = new SqliteVectorStore({ embed: embedder, dimension: 768, table: 'policies' });
await store.init();
await store.upsertMany([
  { id: 'refund',   text: 'Refunds are accepted within 30 days.', metadata: { category: 'policy' } },
  { id: 'shipping', text: 'Shipping is free over 50 EUR.',         metadata: { category: 'policy' } },
]);

const rag = new RAG({ llm: chatter, store });

const { answer, hits } = await rag.answer({ query: 'How long do I have to return an item?' });
//  → answer: "You have 30 days to return an item [refund]."
//    hits:   [ { id: 'refund', score: 0.82, ... }, ... ]

// Streaming variant — hits available before the first token
const { hits: streamHits, stream } = await rag.stream({ query: '...', topK: 3 });
// render `streamHits` immediately as sources; consume `stream` as chunks arrive
for await (const chunk of stream) process.stdout.write(chunk.text ?? '');

The default system instruction tells the model to answer from context only and cite by [id]. Both the instruction and the context template are overridable per-instance (via the constructor) or per-call (via systemInstructions).

Batch upsert (new in v0.2.0)

For loading many rows at once — one embed call for the whole batch, one round-trip to persist:

await store.upsertMany([
  { id: 'contract-1', text: '...', metadata: { region: 'EMEA' } },
  { id: 'contract-2', text: '...', metadata: { region: 'APAC' } },
  // ...
]);

SQLite backend runs a prepared statement inside a transaction. HANA backend uses a multi-row MERGE INTO (UNION ALL of DUMMY selects), chunked at upsertChunkSize (default 100).

What the SQL actually does (HANA backend)

-- Table creation (init)
CREATE COLUMN TABLE "SUPPLIER_CONTRACTS" (
  "id"        NVARCHAR(256) PRIMARY KEY,
  "text"      NCLOB NOT NULL,
  "embedding" REAL_VECTOR(1536) NOT NULL,
  "metadata"  NCLOB
);

-- Upsert (via MERGE INTO)
MERGE INTO "SUPPLIER_CONTRACTS" AS T
USING (SELECT ? AS "id", ? AS "text", TO_REAL_VECTOR(?) AS "embedding", ? AS "metadata" FROM DUMMY) AS S
ON T."id" = S."id"
WHEN MATCHED THEN UPDATE SET "text" = S."text", "embedding" = S."embedding", "metadata" = S."metadata"
WHEN NOT MATCHED THEN INSERT VALUES (S."id", S."text", S."embedding", S."metadata");

-- Search (with metadata filter)
SELECT TOP 5
  "id", "text", "metadata",
  COSINE_SIMILARITY("embedding", TO_REAL_VECTOR(?)) AS "score"
FROM "SUPPLIER_CONTRACTS"
WHERE JSON_VALUE("metadata", '$.region') = ?
ORDER BY "score" DESC;

Backend comparison

SqliteVectorStoreHanaVectorStore
StorageJSON array in TEXT columnNative REAL_VECTOR(N)
SimilarityJS cosine at query timeNative COSINE_SIMILARITY()
Scan patternFull table scan (JS-side)SQL-level TOP N push-down
Scale ceiling~10K rows before latency hurtsMillions (HNSW index in HANA Cloud)
Metadata filterjson_extract(...)JSON_VALUE(...)
Additional installbetter-sqlite3 (native compile)hdb (pure JS)
Live-verifiedYes — integration tests exercise itNo — mock-verified (see below)

Stability

0.1.0. API expected to remain stable through 0.x for the SQLite backend (integration-tested end-to-end). The HANA backend is wire-protocol-verified against a mock hdb client and matches SAP HANA Cloud's documented vector engine SQL. Live-verification against a real HANA Cloud instance is the top open item — welcome as a community PR.

FAQ

Do I need HANA Cloud to develop with this? No. The SQLite backend has the identical API and works out of the box. Build + test + demo locally, then swap SqliteVectorStore for HanaVectorStore at deploy time.

Which embedding model should I use? Match the vector size to your dataset scale:

  • nomic-embed-text (Ollama, 768-dim) — free, local, fine for demos
  • text-embedding-3-small (OpenAI, 1536-dim) — good balance
  • text-embedding-3-large (OpenAI, 3072-dim) — best retrieval quality, higher cost
  • mxbai-embed-large (Ollama, 1024-dim) — strong local option

How does this differ from CAP's @sap/cds-dbs HANA driver? @sap/cds-dbs gives you full CAP entity mapping over HANA. This plugin is scoped narrowly to vector storage + semantic search — it's a small library, not a CAP database adapter. Use both together in a CAP app.

Can I use this without the @saptarishi/cds-plugin-llm plugin? Technically yes — pass any object with an embed() method matching the plugin's shape. But the plugin's uniform interface across providers is the whole reason this composes cleanly.

Where's the HNSW index configuration? Not exposed in 0.1.0. HANA Cloud's vector engine can build HNSW indexes for large tables — for now, add the index via a manual DDL step after init(). HNSW auto-provisioning is a follow-up item.

License

Apache-2.0

Apache-2.0, read

History

Latest

v0.13.0

Changed 1, Other 1

Notes
Changed
  • Peer dependency widened: @saptarishi/cds-plugin-llm peer range is now >=1.0.0 <3 (was ^1.0.0). Consumers pinning the 2.x line — which added 30+ new middleware primitives with a formal API-stability contract — no longer trip npm ci's peer-dep ERESOLVE check. The plugin uses zero 2.x-only APIs internally; the 1.x-only pin was conservative and blocked mixed installs unnecessarily. No code changes on either side; this is a manifest-only bump so downstream projects can freely combine the latest of both plugins.
Notes
  • Breaking for consumers only if they had pinned ^0.12.0 exactly — ^0.13.0 semver requires an explicit dependency update. This is a widening (strictly permissive) change; no behavioural difference at runtime.
  1. v0.12.0

    Features 3, Other 4

    Notes
    Added
    • @rag.metadataFields — array of row fields to copy into the vector-store metadata dict on every upsert, so filter: { region: 'EMEA' } on searchByMeaning / askAbout actually filters. Closes the gap noted in the demo app: 0.11.0 shipped the filter param plumbing, but only the implicit { entity } key was persisted in metadata, so filter: '{"region":"EMEA"}' matched nothing.

      @rag: {
        fields:         ['supplierName', 'contractType', 'terms'],    // → embedded text
        metadataFields: ['region', 'category', 'supplierName'],       // → filterable metadata
        dimension:      768,
        store:          'sqlite',
        search:         'hybrid',
      }
      
      // Later:
      await cds.vectorHana.searchByMeaning({
        entity: 'ProcurementService.SupplierContracts',
        query:  'aluminum short lead time',
        filter: { region: 'EMEA', category: 'raw-materials' },
        topK:   5,
      });
    • Default: metadataFields: [] — pure backward-compat. Existing consumers see zero change; only rows re-upserted after adding metadataFields carry the new keys.

    • 7 new tests (204 total): buildItem copies metadataFields into metadata dict, skips null values, keeps text unchanged (metadata is separate from the embedding source), defaults to empty array; normalizeConfig defaults, rejects non-array, passes through configured values.

    Notes
    • Metadata is separate from embedding source. fields controls what gets embedded (the vector). metadataFields controls what's stored as filterable metadata. Fields listed in both get double-duty — that's fine. Fields in metadataFields only don't affect embedding quality.
    • Re-upsert required for existing rows. New metadataFields don't back-fill retroactively. Two paths: (a) delete the vector rows for the entity and let CAP's CREATE handler re-index them; (b) call cds.vectorHana.backfill(entityName) explicitly.
    • Filter semantics on SQLite are exact-match via json_extract(metadata, '$.key') = ? (added in 0.6.0). HANA uses the same shape (added in 0.8.0). Both backends already support the filter param — this release just makes the metadata dict richer so filters have something to match against.
    • Type limits. SQLite json_extract compares as strings when the stored value was a string. Numeric filters ({ tier: 3 }) work but string comparison rules apply. If you need range queries, roll a custom keywordSearch or drop to raw SQL.
  2. v0.11.0

    Features 4, Changed 1, Other 3

    Notes
    Added
    • alpha param on hybridSearch, RAG.retrieve, RAG.answer, and the auto-declared OData searchByMeaning + askAbout actions. Single knob in [0, 1]: alpha=1 → pure vector, alpha=0 → pure keyword, alpha=0.5 → balanced (default RRF). When supplied, overrides the separate vectorWeight + keywordWeight params. Wins over two-knob weighting: monotonic, dimensionless, easy to sweep in a Grafana slider.

      // Programmatic
      await cds.vectorHana.searchByMeaning({
        entity: 'ProcurementService.SupplierContracts',
        query:  'aluminum EMEA short lead time',
        filter: { region: 'EMEA' },
        alpha:  0.7,   // lean vector; keyword still contributes 30%
        topK:   5,
      });
      
      // Via OData bound action
      curl -X POST http://localhost:4004/procurement/SupplierContracts/ProcurementService.searchByMeaning \
        -H 'Content-Type: application/json' \
        -d '{"query":"aluminum EMEA","filter":"{\"region\":\"EMEA\"}","alpha":0.7,"topK":5}'
    • filter param exposed on OData actions. The plugin already accepted filter on the programmatic searchByMeaning / askAbout calls; now the auto-declared bound actions carry a filter: cds.String param that accepts a JSON-encoded metadata filter (e.g. '{"region":"EMEA"}'). Applied to both vector and keyword retrieval. Invalid JSON returns 400 with a clear diagnostic. Objects (from programmatic calls) still pass through unchanged.

    • parseFilterJson(raw) helper exported from cdsPlugin for consumers writing custom action handlers.

    • 15 new tests (197 total): alpha maps to weights correctly (=1 pure vector, =0 pure keyword, =0.5 balanced), alpha overrides explicit vectorWeight/keywordWeight when supplied, out-of-range alpha (>1, <0, NaN) throws, filter propagates to BOTH vector + keyword sub-queries, RAG.retrieve + RAG.answer forward both params, parseFilterJson (undefined/empty, object passthrough, JSON string parse, array→throws, invalid JSON→throws, non-string non-object→throws).

    Changed
    • OData action param snapshots updated (searchByMeaning, askAbout) — snapshot tests now include filter + alpha. If you were asserting exact param shapes in downstream tests, add these two.
    Notes
    • Weights vs alpha precedence. alpha wins when supplied. If you were setting vectorWeight + keywordWeight explicitly, keep doing so — nothing changes. If you switch to alpha, drop the explicit weights.
    • filter semantics unchanged. Same object shape the programmatic call already accepted. The OData surface just adds a JSON-string wrapper so the wire representation stays a single scalar param.
    • HANA + SQLite backends both support filter already (added in 0.6.0 for SQLite, 0.8.0 for HANA). alpha is purely fusion-side and works with any backend that implements _keywordSearch — SQLite FTS5 works; HANA's CONTAINS-backed keyword search works. Backends without keyword search fall back to vector-only regardless of alpha.
  3. v0.10.0

    Features 7, Other 4

    Notes
    Added
    • createQueryExpander({ llm, strategy, n?, ... }) — query-side amplification. The 0.9.0 CHANGELOG explicitly noted "query reformulation is not included" — this fills the gap. Two strategies:

      • 'hyde' (default) — Hypothetical Document Embeddings. The LLM writes a plausible short answer to the question; both the question AND the hypothetical answer get embedded and retrieved on. Works because a plausible answer clusters near real source documents in embedding space even when the question doesn't. Boosts recall on abstract / under-specified queries where the vocabulary gap between question and source is large.
      • 'multi-query' — Generate N distinct paraphrases of the question, retrieve for each in parallel, fuse via RRF. Boosts recall on ambiguous queries by covering multiple phrasings the user might not have used.
      const { createQueryExpander, llmRerank, RAG } = require('@saptarishi/cds-plugin-vector-hana');
      
      const rag = new RAG({
        llm, store,
        mode:   'hybrid',
        expand: createQueryExpander({ llm, strategy: 'hyde' }),
        rerank: llmRerank({ llm }),
      });
      const { answer, hits } = await rag.answer({ query: 'refund policy?', topK: 5 });
    • RAG.expand hook. New expand?: QueryExpander option on the RAG constructor. retrieve() now:

      1. Calls expand(query) → gets string[] of related queries (typically 2-4).
      2. Runs each through store.search() or store.hybridSearch() in parallel with candidateK = max(topK * 4, 10).
      3. Fuses all lists via RRF.
      4. Optionally reranks with rerank.
      5. Returns top-K.

      Per-query candidateK is only expanded when there's more than one query — the single-query fast path (no expander) is unchanged from 0.9.0.

    • Robust to LLM failures. Any error / empty reply from the expander collapses back to [originalQuery]. Malformed multi-query responses (< N valid lines) still produce the original + whatever parsed. Nothing worse than "no expansion" ever ships.

    • Customization:

      • model, maxTokens — passed through to the expansion LLM call.
      • systemInstructions — override the strategy's default rubric.
      • buildUserPrompt: ({ query, n }) => string — write your own template.
    • Line-cleaning for multi-query: leading 1., 2), -, *, •, and similar markers are stripped from each paraphrase. Users don't need to add "no numbering" to their custom system prompts.

    • Exports: createQueryExpander. TS defs added: QueryExpander, QueryExpanderOptions, plus expand?: QueryExpander on RAGOptions and readonly expand: QueryExpander | null on the RAG class.

    • 21 new tests (182 total): construction validation (llm/strategy/n bounds), HyDE (returns [query, hypothetical], LLM error fallback, empty-reply fallback, system-prompt shape), multi-query (parse N lines, strip bullets/numbering, cap at N even when LLM over-produces, LLM error fallback), customization (model + maxTokens forwarding, systemInstructions override, buildUserPrompt receives { query, n }), RAG integration (expand called once per retrieve, results fused across queries, expand=null skips fusion, expand + rerank compose, expander throw → falls back cleanly, empty-array reply → falls back), constructor rejection of non-function expand, and one end-to-end integration test wiring HyDE → hybrid → llmRerank → answer with a real SqliteVectorStore + mock LLM.

    Notes
    • Additive — ^0.9 consumers bump to ^0.10 with zero code changes. expand is opt-in; nothing changes for callers not using it.
    • The full recommended pipeline is now: expand (HyDE or multi-query) → hybrid retrieve per query → RRF fuse → llmRerank → augment prompt → LLM answer. Every stage is a separately shipped primitive that you can enable or disable independently.
    • Latency-wise: HyDE adds one LLM call (the hypothetical answer). Multi-query adds one LLM call PLUS N-fold retrieval fan-out. Point the expander at a fast small model (Haiku, Gemini Flash, Groq's llama-3.1-8b-instant) and the added latency stays under a second even in the worst case.
    • HyDE and multi-query are complementary; a future release may add strategy: 'both' that runs both and fuses everything. For now, pick one — HyDE typically wins on abstract/conceptual queries, multi-query on entity/keyword queries.
  4. v0.9.0

    Features 8, Other 3

    Notes
    Added
    • llmRerank({ llm, model?, batchSize?, systemInstructions?, buildUserPrompt? }) — built-in LLM reranker factory. 0.8.0 shipped the RAG.rerank hook as pluggable infrastructure with no built-in impl; this fills the gap. Given a query and a list of retrieved hits, the returned Reranker asks the LLM to score each hit on a 0-10 scale via structured output, then re-sorts descending and attaches a rerankScore field.

      const { llmRerank, RAG } = require('@saptarishi/cds-plugin-vector-hana');
      
      const rerank = llmRerank({
        llm,                         // required — any LLMService with chat()
        model: 'claude-haiku-4-5',   // optional — cheap fast model recommended for reranking
        batchSize: 20,               // optional — hits per LLM call (default 20)
      });
      
      const rag = new RAG({ llm, store, mode: 'hybrid', rerank });
      const { answer, hits } = await rag.answer({ query: 'refund policy?', topK: 5 });
      // hits sorted by LLM relevance score; each hit gains a rerankScore field
    • Robust to LLM failures. Malformed JSON, throwing calls, or partial responses never regress the answer path below "vanilla hybrid":

      • LLM throws → all hits get the neutral score 5 (original order preserved).
      • Malformed JSON → parse falls back to the original order.
      • Indices missing from the response → those hits get the neutral score 5.
      • Scores outside [0, 10] are clamped.
    • Batching for long candidate lists — hits are split into batchSize-sized slices and scored in parallel Promise.all calls. Default 20 fits comfortably in most models' structured-output limits and keeps per-batch latency low.

    • Structured output. Every rerank call sets format to a { scores: [{ index, score }] } schema, so providers that support strict JSON mode (OpenAI, Anthropic, Gemini) never need lenient parsing. The plugin's data-shape output from Anthropic is honored on the fast path.

    • Custom prompt hook. buildUserPrompt: ({ query, hits, startIndex }) => string for domain-specific scoring criteria — e.g. "prefer contracts still valid this quarter" or "weight the terms column more than the supplier name". systemInstructions overrides the default scoring rubric.

    • Passage truncation to 500 chars per hit inside the default prompt so a large hit.text doesn't blow the context window. Consumers with longer passages can override buildUserPrompt to control this precisely.

    • Exports: llmRerank + DEFAULT_BATCH_SIZE. TS defs added — LlmRerankOptions — with @since 0.9.0.

    • 19 new tests (161 total): construction validation (llm requirement, batchSize bounds), happy path (score-based sorting + rerankScore field + single LLM call for small batches + model/maxTokens forwarding + structured-output schema shape), batching (multiple LLM calls with correct global-index translation), robustness (missing indices default to 5 with original order preserved, LLM throw → fallback, malformed JSON → fallback, JSON embedded in a plain-string reply parsed, scores clamped to [0,10], empty hits + empty query short-circuit before the LLM call), prompt shape (default lists passages by index, systemInstructions override, custom buildUserPrompt gets { query, hits, startIndex }), RAG integration (rerank sorts hits + the answer prompt then cites the re-sorted top).

    Notes
    • Additive — ^0.8 consumers bump to ^0.9 with zero code changes. llmRerank is opt-in via new RAG({ ..., rerank: llmRerank({ llm }) }); nothing changes for callers not using it.
    • Recommended pairing: hybrid retrieval → llmRerank(cheap model). Hybrid pulls a wider candidate set with candidateK (default 4×topK); reranker scores them and picks the winners. Latency stays manageable if you point the reranker at a fast small model (Haiku, Gemini Flash, llama-3.1-8b-instant) — the rerank prompt is short and structured.
    • The llmRerank hook shipped here is for post-retrieval reranking. Query reformulation ("HyDE" / multi-query expansion) is a separate concern, not included in this release — write your own preprocessing step if you need it.
  5. v0.8.1

    Fixes 3, Other 1

    Notes
    Fixed
    • Auto-declared OData actions (searchByMeaning, askAbout) were emitted as instance-bound — the EDMX had Parameter Name="in" Type="ProcurementService.SupplierContracts" (single entity) instead of Collection(...). Callers who tried the collection URL (POST /Suppliers/Service.searchByMeaning) got Error: Action ... must be called on a single instance; the actions only worked via POST /Suppliers(<dummy-id>)/Service.searchByMeaning — a UX regression nobody caught because the plugin's own tests only checked the CSN mutation, not what CAP compiled to EDMX.
    • Fix: add '@cds.odata.bindingparameter.collection': true to both auto-declared actions. Verified via cds.compile.to.edmx() — the parameter now becomes Type="Collection(...)" and both URL patterns work. This was already the intent in 0.6.0 (see the CHANGELOG entry there — "collection-bound searchByMeaning"); the CSN just missed the specific annotation CAP requires to emit Collection.
    • 1 new test (142 total) pinning the annotation on both searchByMeaning and askAbout.
    Notes
    • Behavior on the CAP handler side is unchanged — the handler always looked up the store by entity name (ignoring the instance id). This fix only changes what CAP compiles into the OData metadata + which URL pattern it accepts. Existing consumers who worked around the bug by supplying a dummy id will still work after this fix; the collection URL now also works.
  6. v0.8.0

    Features 7, Other 4

    Notes
    Added
    • Hybrid retrieval via Reciprocal Rank Fusion (RRF) — new store.hybridSearch() runs vector + keyword in parallel and fuses per-list ranks. Directly improves recall on messy S/4 text where cosine-only misses: SKUs, PO numbers, exact identifiers, jargon queries.

      const hits = await store.hybridSearch({
        text: 'refund window PO-4500000123',
        topK: 5,
        candidateK: 20,        // fetch top-20 from each retrieval before fusion
        vectorWeight: 1,
        keywordWeight: 1,
        k: 60,                 // canonical RRF constant from Cormack et al. 2009
        filter: { region: 'EMEA' },
      });
      // → hits[i].fusionScore alongside the original .score
    • store.keywordSearch() — new public method + _keywordSearch backend hook. Naive token-match count on the text column (case-insensitive substring LIKE, one hit per query token). Implemented on both SqliteVectorStore and HanaVectorStore. Backends without a keyword impl (custom stores extending the base class) fall back to [] — hybrid degrades to vector-only, no error. For production scale, override _keywordSearch with a real FTS index (SQLite FTS5, HANA CONTAINS(), etc.).

    • reciprocalRankFusion({ lists, weights?, k? }) — pure utility exported from lib/rrf.js. Handles doc canonicalization by id, merges metadata across lists (later lists enrich fields the earlier didn't have), preserves the first-seen list's text and score. Weights let callers bias one retrieval over another; k controls how much the multi-list bonus dominates over single-list top-ranks.

    • RAG hybrid mode + rerank hook:

      const rag = new RAG({
        llm, store,
        mode: 'hybrid',               // 'vector' (default) or 'hybrid'
        rerank: async (hits, query) => // optional post-retrieval hook
          llmRerankOrCrossEncoder(hits, query),
      });
      const { answer, hits } = await rag.answer({ query: '...', mode: 'hybrid' });

      The reranker runs after fusion. Return hits in the desired final order; RAG trims to topK if the reranker returns more than that.

    • @rag.search annotation — 'vector' (default) or 'hybrid'. Opts an annotated entity's cds.vectorHana.searchByMeaning() and .askAbout() into hybrid retrieval at every call site without any handler code:

      entity Suppliers {
        key ID     : UUID;
        name       : String;
        description: LargeString;
      } @rag: {
        fields:    ['name', 'description'],
        dimension: 768,
        search:    'hybrid',           // ← new
      };

      Per-call override via plugin.searchByMeaning({ entity, query, mode: 'vector' }) still works if a specific query needs one strategy over the other.

    • New exports: reciprocalRankFusion, tokenize (from lib/VectorStore.js). TS defs added: HybridSearchParams, HybridSearchHit, RetrievalMode, Reranker.

    • 30 new tests (141 total): RRF utility (validation, basic fusion, weight bias, multi-list bonus, metadata merging, null-id handling, fusionScore surfacing), SqliteVectorStore keyword search (token ranking, topK cap, metadata filter, SKU-style exact match, invalid inputs, all-short-tokens edge case), hybrid search (vector+keyword combination, candidateK, weight bias, no-keyword-impl fallback, input validation), RAG hybrid mode (routing to hybridSearch, per-call override, missing-hybridSearch error), RAG rerank (hook invocation, over-topK trimming), constructor validation (bad mode / rerank).

    Notes
    • Additive — store.search() shape is unchanged; existing consumers can stay on it. hybridSearch() is opt-in. ^0.7 consumers can bump to ^0.8 with zero code changes.
    • The SQLite _keywordSearch uses LIKE for substring matching — deliberately dumb so exact SKUs / order numbers (like PO-4500000123, sup-42) match literally without needing stemming or a stopword list. For high-volume production, override with an FTS5 virtual table.
    • The HANA _keywordSearch uses the same LIKE fallback for portability. On a production HANA with a text index, override with CONTAINS() for fuzzy + linguistic-aware matching.
    • The rerank hook is opt-in and pluggable — this release ships the infrastructure but no built-in reranker. Consumers can plug their own LLM-scoring, cross-encoder inference, or business-logic filter. A future release may ship an llmRerank({ llm }) factory.
  7. v0.7.4

    Fixes 1, Other 1

    Notes
    Fixed
    • Auto-declared OData actions never actually appeared in $metadata. The plugin's CSN mutation ran at cds.on('loaded') — but CAP fires loaded BEFORE assigning cds.model = m. The plugin's guard if (!cds.model?.definitions) return; was falsy, so mutateCsn silently no-op'd and the actions were never declared. The mutation code, the action shape, the type synthesis — all correct. Just never ran. Fix: read the model from the loaded event argument (which CAP always passes as the raw model) instead of relying on cds.model being assigned yet. Verified end-to-end: Action Name="searchByMeaning" and Action Name="askAbout" now appear in the OData $metadata document.
    Notes
    • Together with 0.7.1 (flat-annotation form), 0.7.2 (quiet reboots), and 0.7.3 (lazy cds.connect.to), this is the fourth patch closing gaps discovered while wiring @rag into a real CAP app (joule-project-api). If you were on any of 0.7.0–0.7.3 and wondered why nothing appeared on the OData surface — bump to 0.7.4 and it will.
  8. v0.7.3

    Fixes 1

    Notes
    Fixed
    • @rag entities were skipped at boot with "cds.services[''] not found". The plugin looked up the embedder via cds.services[alias], but CAP populates that dict lazily on first cds.connect.to(alias). When the plugin's served handler ran before any app code had touched the service, the lookup returned undefined and the plugin skipped the entity — meaning @rag-annotated entities never got their vector store or OData actions in production apps that didn't happen to touch the LLM elsewhere. Now resolveService() falls back to cds.connect.to(alias) (awaited) to force-instantiate the service, matching the standard CAP pattern. Test doubles that pre-populate cds.services still work — they hit the fast path.
  9. v0.7.2

    Fixes 1

    Notes
    Fixed
    • Silenced spurious "action already declared" warnings on normal boots. activate() calls the CSN mutation twice — once immediately (in case the model is already loaded) and once via cds.on('loaded'). When both fired against the same model, the second run hit its own already-declared actions and logged idempotency warnings, even though nothing was wrong. Now the plugin tracks which entity defs it has already mutated via a WeakSet and skips them silently on re-entry. Genuine collisions (a user-declared actions.searchByMeaning or a competing plugin) still warn — that's a real signal worth surfacing.
  10. v0.7.1

    Fixes 3, Other 1

    Notes
    Fixed
    • @rag annotation was silently ignored in real CAP apps. The CDS compiler flattens @rag: { fields: [...], dimension, ... } into top-level keys @rag.fields, @rag.dimension, ..., which the plugin's def['@rag'] check couldn't see — so the plugin walked past every annotated entity without doing anything. Test doubles that passed a nested object worked fine, but every actual cds compile-produced CSN hit the flat form. Both forms are now supported:
      • Nested (from cds.linked or hand-authored CSN): def['@rag'] = { ... }
      • Flat (from cdsc or the CAP runtime pre-linked): def['@rag.fields'] = ..., def['@rag.dimension'] = ...
      • Flat with nested keys: def['@rag.actions.search'] = 'findX' reconstructs into actions: { search: 'findX' }
    • New exported helper readRagAnnotation(def) — reconstructs the config regardless of shape; useful for third-party plugins that want to introspect the annotation.
    • 7 new tests (111 total) covering both annotation forms including the nested-wins-if-both-present rule and the shorthand @rag: true / false.
    Notes
    • Behaviorally identical to 0.7.0 for anyone whose tests worked. If you noticed @rag-annotated entities booting quietly without OData actions appearing on /odata/v4/..., this is the fix — bump and restart.
  11. v0.7.0

    Features 5, Other 3

    Notes
    Added
    • askAbout as an OData action — RAG conversation over your entity, no glue code. Completes the @rag OData surface started in 0.6.0. Every @rag-annotated entity now gets a second auto-declared action alongside searchByMeaning: askAbout(query, topK, systemInstructions) returning a synthesized { answer: String; sources: array of <Entity> } type. Handler runs the full RAG pipeline (retrieve → augment → chat) and projects hit IDs back to the entity via SELECT ... WHERE ID IN (...) in hit-rank order.

      POST /odata/v4/app/Suppliers/AppService.askAbout
      Content-Type: application/json
      
      { "query": "Which suppliers can ship steel coils to Germany within two weeks?", "topK": 5 }

      Returns:

      {
        "answer":  "Based on the sources, Acme Steel and Nord Metallwerke can meet that lead time [sup-42], [sup-101].",
        "sources": [
          { "ID": "sup-42",  "name": "Acme Steel",       "description": "...", "country": "DE" },
          { "ID": "sup-101", "name": "Nord Metallwerke", "description": "...", "country": "DE" }
        ]
      }
    • Synthesized <Entity>AskAboutResult types added to cds.model.definitions at load time. Structured returns need a top-level named type in CSN (inline anonymous structs aren't legal in action returns). The plugin adds <ServiceName>.<EntityShort>AskAboutResult per annotated entity with { answer: cds.String, sources: array of <Entity> }. Idempotent — pre-existing types are never overwritten.

    • Config knobs (extended @rag.actions):

      • @rag.actions: false — disable ALL auto-declared actions (still applies).
      • @rag.actions.ask: false — disable just askAbout (keep searchByMeaning).
      • @rag.actions.ask: 'answerAbout' — custom action name. Validated as a JS identifier at boot.
      • @rag.actions.search (from 0.6.0) — unchanged.
    • Idempotent everywhere. If the entity already declares its own actions.askAbout (user-written, or another plugin), the auto-declaration is skipped with a warning — same policy as searchByMeaning. Re-running declareActions on an entity where the result type already exists does NOT replace it (protects downstream code that may hold references).

    • 15 new tests (104 total): expanded normalizeActionsConfig (default shape, per-action false, custom names, invalid name for both), declareActions (result type synthesis, action shape, idempotency for search + ask independently, no double-synthesis of the type, custom names, all opt-out combinations), OData handler behavior (registration alongside search, { answer, sources } shape with sources in hit-rank order, empty-query 400, empty hits { answer, sources: [] }, systemInstructions pass-through to RAG, per-action opt-out honored, custom name).

    Notes
    • Additive — existing @rag-annotated entities gain the new action automatically without config changes. If your app already declared its own askAbout action on an entity, the plugin sees it and steps aside (logs a warning) — nothing breaks.
    • The synthesized result type name is <ServiceName>.<EntityShort>AskAboutResult. Because the full entity name is service-qualified, there's no collision between entities sharing a short name across services. If you already have a type by that name, the plugin will re-use it — bring your own if you want a richer shape (e.g., adding a citations field).
    • Handler uses cds.vectorHana.askAbout() under the hood, so per-request options that flow through (systemInstructions, topK) work the same in both APIs. The chatter LLM alias defaults to @rag.provider (or 'llm'); override with @rag.chatter: '<alias>' when you want cheap-embed + smart-chat.
  12. v0.6.0

    Features 5, Other 3

    Notes
    Added
    • @rag now auto-declares a bound OData action on every annotated entity — zero handler code, callable from Joule / any OData client. The plugin now mutates the CSN at cds.on('loaded') (or immediately if the model is already loaded), adding a collection-bound searchByMeaning(query: String, topK: Integer) action that returns array of <Entity>. The action handler is registered on served: it runs the vector search, projects the hit IDs back to entity rows via SELECT.from(<Entity>).where({ ID: { in: [...] } }), and returns them in hit-rank order (SQL WHERE ID IN doesn't preserve order, so the handler re-sorts).

      entity Suppliers {
        key ID     : UUID;
        name       : String;
        description: LargeString;
      } @rag: {
        fields:    ['name', 'description'],
        dimension: 768,
      };
      POST /odata/v4/app/Suppliers/AppService.searchByMeaning
      Content-Type: application/json
      
      { "query": "steel coils from Europe", "topK": 5 }

      Returns value: [ <Suppliers row>, <Suppliers row>, ... ] in relevance order — exactly as if the user had written the action + handler by hand.

    • Configurable via @rag.actions:

      • @rag.actions: false — disable ALL auto-declared actions on this entity.
      • @rag.actions.search: false — disable just searchByMeaning (keep the JS API + auto-index alive).
      • @rag.actions.search: 'findSuppliers' — custom action name. Validated as a JS identifier at boot.
    • Idempotent CSN mutation. If the entity already declares an actions[<name>] (user-written, or another plugin), the auto-declaration is skipped with a warning — the plugin never silently overwrites developer code. The mutation runs BOTH immediately (in case the model is already loaded) AND on cds.on('loaded') (in case it wasn't); the idempotency guard keeps this safe.

    • New helper exported from lib/cdsPlugin.js: declareActions(entityName, def, config, log) — the low-level CSN mutation used internally. Exposed for tests and for embedded scenarios where a plugin author wants to declare the action without running the full activate() lifecycle.

    • 18 new tests (89 total): normalizeActionsConfig (default / false / per-action false / custom name / invalid name), declareActions (mutation shape, idempotency, custom name, both opt-out shapes), activate (mutation immediate vs deferred), handler registration on served, handler behavior (rank order, empty-query 400, empty hits [], opt-out, custom action name).

    Notes
    • Additive — existing @rag-annotated entities gain the new action automatically without config changes. If your app already declared its own searchByMeaning action on an entity, the plugin sees it and steps aside (logs a warning) — nothing breaks.
    • The action returns array of <Entity>, not SearchHit. Score / metadata are not exposed via OData in this release — callers who need them use the direct cds.vectorHana.searchByMeaning() JS API. A future release may synthesize <Entity>SearchResult types that include score.
    • askAbout as an OData action is NOT in 0.6.0 (structured return types across N entities add CSN complexity worth scoping separately). Keep using the JS API for now: cds.vectorHana.askAbout({ entity, query }).
  13. v0.5.0

    Features 7, Other 3

    Notes
    Added
    • @rag CDS annotation — one line on your entity, auto-indexed vector search on save/update/delete. The package now ships a real CAP plugin (cds-plugin.js) that inspects the compiled model on cds.on('served'), builds a VectorStore per @rag-annotated entity, and wires CRUD handlers so the vector table stays in lockstep with the entity's rows — the developer writes zero glue code.

      entity Suppliers {
        key ID     : UUID;
        name       : String;
        description: LargeString;
        country    : String;
      } @rag: {
        fields:    ['name', 'description'],  // concatenated + embedded per row
        dimension: 768,                      // your embedding model's vector size
        store:     'sqlite',                 // or 'hana'
        topK:      5,                        // default topK for searchByMeaning
      };
      // Anywhere in a CAP handler:
      const hits    = await cds.vectorHana.searchByMeaning({ entity: 'AppService.Suppliers', query: 'steel coils' });
      const { answer, hits: sources } = await cds.vectorHana.askAbout({
        entity: 'AppService.Suppliers',
        query:  'Which suppliers ship steel from Europe?',
      });
    • Plugin handle at cds.vectorHana — same ergonomic pattern as the SAP-shipped cds.mtx / cds.auth handles. Four operations:

      • getStore(entityName) — the underlying VectorStore (bypass this API and drop to raw search() / upsert() when you need to).
      • searchByMeaning({ entity, query, topK?, filter? }) → SearchHit[].
      • askAbout({ entity, query, topK?, filter?, systemInstructions?, ...chatOpts }) → { answer, hits, raw }. Uses RAG under the hood; the extra fields (model, maxTokens, thinking, ...) are forwarded to the chat provider.
      • backfill(entityName) — re-index every row of an entity, useful after enabling @rag on an existing table or bulk-imports that bypassed the service layer.
    • Automatic CRUD sync. Handlers registered per annotated entity:

      • after CREATE|UPDATE → build text from @rag.fields, embed, upsert. Rows where every projected field is empty are silently skipped (no dead vectors).
      • before DELETE → remove the id from the store before CAP deletes the row (so a failing delete leaves both sides in sync).
    • Provider aliases via cds.services[...]. @rag.provider names an alias into cds.requires.<alias> — the same lookup CAP uses for every other service. Defaults to 'llm'. @rag.chatter overrides just the LLM used for askAbout() when you want cheap-embed + smart-chat (e.g., embed with ollama, chat with anthropic).

    • Fail-safe activation. Every phase (bad @rag config, missing provider alias, unknown store kind, store init() failure) logs a specific error and skips that ONE entity — the rest of the app still boots. Silent-fail on plugin activation is the SAP-plugin convention (see @sap/cds-mtx).

    • 26 new tests (71 total): normalizeConfig (7), buildItem (4), activate wiring including error paths (5), CRUD sync (5), searchByMeaning (3), askAbout including chatter option forwarding (2). Runs with a hand-rolled fake cds — no @sap/cds install required, matching the existing test isolation model.

    • TS defs for CdsRagPlugin, ActivateCdsPluginOptions, and the activateCdsPlugin(cds, options?) factory. Power users can activate the plugin manually (embedded runtimes, tests) without going through cds-plugin.js.

    Notes
    • Additive — the existing VectorStore / HanaVectorStore / SqliteVectorStore / RAG API is unchanged. ^0.4 consumers can bump to ^0.5 with zero code changes.
    • @sap/cds is now declared as an optional peer dep. The RAG and VectorStore classes still work standalone (no cds required); the CDS plugin obviously needs cds to be present at runtime.
    • Zero new production dependencies. The CDS plugin is pure composition over the existing VectorStore / RAG classes.
  14. v0.4.0

    Features 4, Other 2

    Notes
    Added
    • RAG class — turnkey retrieval-augmented-generation on top of any VectorStore + @saptarishi/cds-plugin-llm provider. Composes store.search() and llm.chat()/stream() into a single call that retrieves, augments the prompt with citation-tagged context, and generates an answer. No new dependencies.

      const { RAG, SqliteVectorStore } = require('@saptarishi/cds-plugin-vector-hana');
      const { OllamaLLMService } = require('@saptarishi/cds-plugin-llm');
      
      const embedder = new OllamaLLMService({ credentials: { embeddingModel: 'nomic-embed-text' } });
      const chatter  = new OllamaLLMService({ credentials: { model: 'qwen2.5:14b' } });
      
      const store = new SqliteVectorStore({ embed: embedder, dimension: 768, table: 'policies' });
      await store.init();
      await store.upsertMany([
        { id: 'refund',   text: 'Refunds are accepted within 30 days.', metadata: { category: 'policy' } },
        { id: 'shipping', text: 'Shipping is free over 50 EUR.',         metadata: { category: 'policy' } },
      ]);
      
      const rag = new RAG({ llm: chatter, store });
      const { answer, hits } = await rag.answer({ query: 'How long do I have to return an item?' });
      //  → answer: "You have 30 days to return an item [refund]."
      //    hits:   [ { id: 'refund', score: 0.82, ... }, { id: 'shipping', score: 0.41, ... } ]

      API surface (lib/rag.js):

      • new RAG({ llm, store, systemInstructions?, promptTemplate? }) — llm needs chat(); store needs search(); both are validated at construction.
      • retrieve({ query, topK = 5, filter }) — thin, semantically-named pass-through to store.search().
      • augment({ query, hits, systemInstructions }) — builds the { system, messages } payload without calling the LLM. Useful when the caller wants to prepend a chat history or run their own middleware first.
      • answer({ query, topK, filter, systemInstructions, ...chatOpts }) — full RAG in one call. Returns { answer: string, hits: SearchHit[], raw: unknown }. Extra fields (model, maxTokens, thinking, ...) are forwarded verbatim to llm.chat(); RAG-specific fields (query, topK, filter, systemInstructions) are stripped so provider SDKs don't see them.
      • stream({ query, ... }) — same as answer() but returns { hits, stream }. Hits resolve before the first token, so a UI can render the source list while the answer streams in.
    • Provider-agnostic reply extraction. answer() normalizes the LLM reply to a plain string regardless of provider shape (plain string, { text }, Anthropic content blocks, Ollama { message: { content } }, OpenAI choices[0].message.content). The full envelope is still exposed on raw for callers that need usage/metadata.

    • Default prompt tells the model to cite by [id]. The default system instruction is "answer from context only, cite sources by their bracketed id"; the default context template renders each hit as [hit_id] (metadata: {...}): hit_text. Both are overridable per-instance or per-call. defaultPromptTemplate and DEFAULT_SYSTEM_INSTRUCTIONS are exported so callers can compose with the built-ins.

    • 21 new tests (45 total): constructor validation (llm/store/methods), retrieve defaults + validation, augment with default + custom template + empty hits + per-call override, answer end-to-end + reply-shape normalization (5 provider shapes) + option forwarding + filter pass-through, stream streams and returns hits eagerly + rejects when llm has no stream().

    Notes
    • Additive — the existing VectorStore / HanaVectorStore / SqliteVectorStore API is unchanged. ^0.3 consumers can bump to ^0.4 with zero code changes.
    • Zero new dependencies. RAG is 100% composition over the surfaces the two plugins already expose.
  15. v0.3.0

    Features 2, Other 1

    Notes
    Added
    • index option on HanaVectorStore — creates an HNSW vector index at table-creation time. Required for scaling beyond a few thousand rows: HANA's HNSW graph index enables approximate-nearest-neighbor (ANN) search that stays fast even on millions of vectors, versus the exhaustive scan of vanilla COSINE_SIMILARITY. Requires HANA Cloud QRC 2/2024 or later.
      • index.type — must be 'hnsw'
      • index.similarity — 'cosine' (default, COSINE_SIMILARITY) or 'l2' (L2DISTANCE)
      • index.name — explicit index name (default: '<table>_<embeddingColumn>_HNSW_IDX')
      • index.buildParameters — dict serialized into HANA's BUILD PARAMETERS ('k=v,...') clause (common keys: ef_construction, M)
    • 5 new tests (24 total) verifying: default naming + COSINE_SIMILARITY, custom L2DISTANCE + name + build params, no-op when index is unset, constructor validation of type/similarity.
    Notes
    • Additive — existing tables and existing HanaVectorStore constructions without index behave exactly as in 0.2.0.
  16. v0.2.0

    Features 3, Other 1

    Notes
    Added
    • upsertMany(items) batch API. Embeds all text values in a single embed() round-trip (providers that support input: string[] return N vectors at once) and persists via a backend-specific batched path.
      • SQLite backend: prepared-statement inside a db.transaction() — atomic and fast.
      • HANA backend: multi-row MERGE INTO via UNION ALL of DUMMY SELECTs. Chunked at upsertChunkSize (default 100) to cap query text size / parameter count.
    • New tests (19 total): SQLite batch insert / update / validation / provider-count mismatch; HANA multi-row MERGE SQL shape + chunking behavior.
    • TS defs: upsertMany on VectorStore, upsertChunkSize on HanaVectorStoreOptions.
    Notes
    • Additive — existing upsert() unchanged. Consumers on ^0.1 can bump to ^0.2 with zero code changes.
  17. v0.1.0

    Features 5

    Notes

    Initial release.

    Added
    • VectorStore abstract base class — defines the init / upsert / search / delete interface. Delegates embedding computation to any @saptarishi/cds-plugin-llm provider instance.
    • SqliteVectorStore backend — stores vectors as JSON in a TEXT column; cosine similarity computed in JavaScript at query time. Uses better-sqlite3 (optional peer dep). Integration-tested end-to-end (7 tests).
    • HanaVectorStore backend — SAP HANA Cloud's native REAL_VECTOR(N) column type + COSINE_SIMILARITY() function. MERGE INTO for upsert semantics; TOP N ... ORDER BY score DESC for ranked retrieval; JSON_VALUE() for metadata filters. Uses hdb client (optional peer dep, pure JS — no native compile). Wire-protocol-verified against a mocked hdb (6 tests) but not yet live-verified against a real HANA Cloud instance.
    • TypeScript definitions in lib/index.d.ts (typed SearchHit<M> generic on metadata shape).
    • README with SQLite + HANA usage, backend comparison table, and the raw SQL that the HANA backend generates (for reviewers who want to see the wire protocol).
5downloads per week, npm
Weekly downloads for @saptarishi/cds-plugin-vector-hana26 complete calendar weeks from 2026-04-27 to 2026-10-25; the latest total is 5.27 Apr – 3 May: 04 May – 10 May: 011 May – 17 May: 018 May – 24 May: 025 May – 31 May: 01 Jun – 7 Jun: 08 Jun – 14 Jun: 015 Jun – 21 Jun: 022 Jun – 28 Jun: 029 Jun – 5 Jul: 06 Jul – 12 Jul: 013 Jul – 19 Jul: 020 Jul – 26 Jul: 027 Jul – 2 Aug: 3883 Aug – 9 Aug: 1,92110 Aug – 16 Aug: 13017 Aug – 23 Aug: 6624 Aug – 30 Aug: 4331 Aug – 6 Sept: 217 Sept – 13 Sept: 3814 Sept – 20 Sept: 3521 Sept – 27 Sept: 5928 Sept – 4 Oct: 225 Oct – 11 Oct: 3112 Oct – 18 Oct: 1119 Oct – 25 Oct: 5
Retrieval-augmented generation
  1. SAP AI CoreAI runtime on BTP with the Generative AI Hubfrom BTP/BAIP
  2. SAP HANA CloudManaged SAP HANA database service on BTPfrom BTP/BAIP
  3. @cap-js/aiValue-help recommendations and SAP AI Core as a CAP servicefrom CAP
  4. cap-llm-pluginGenerative AI in CAP with fields anonymised through SAP HANAfrom CAP
  5. @mi8y/cap-agents-aicore-vectorstoreLangChain vector store on SAP AI Core Document Groundingfrom CAP
  6. @mi8y/cap-agents-cds-vectorstoreLangChain vector store with CDS entities for embeddingsfrom CAP
  7. zvdbVector database implemented in ABAP for document chunk retrievalfrom ABAP
  8. @sap/cdsCAP Node.js runtimefrom CAP
  9. @saptarishi/cds-plugin-llmCAP service kind putting eleven model providers behind one APIfrom CAP
  10. sap-knowledge-pipelinePython library turning OData or HANA records into chunked documentsfrom BTP/BAIP

Details

Sources5
npm registrydiscovered herechecked
The discovery passdiscovered herechecked
npm registry, @saptarishi/cds-plugin-vector-hanaversion, releases, installschecked
cds-plugin-vector-hana/README.mdquoted, and what the entry declareschecked

Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.

Compatibility and provenance
runtimesnode
declared for>=7
latestv0.13.0
first release
releases and cadence18, ~0d median
discovered vianpm registry, The discovery pass
first seen in the atlas
ecosystem and domaincap, ai-and-agents
Contributors1

From the repository’s contributor list, .

History in the atlas6
  1. The repository root is a Joule copilot monorepo whose README describes five Joule skills, an agent and a pitch site; this package is one subdirectory of it. npm added beside it.
  2. Two backends behind one API, the @rag annotation and the OData actions it declares, and the separate provider plugin the embeddings come from.
  3. runtimes: node; cds_range: >=7; typed: true
  4. capability:retrieval-augmented-generation, with what_it_does and seven quoted sources
  5. Vector storage and similarity search on SAP HANA from CAP, packaged as a plugin so the embedding column and the query are modelled rather than hand-written.
  6. First seen.
For your agent
Ref
artifact:saptarishi-cds-plugin-vector-hana
JSON
/artifacts/saptarishi-cds-plugin-vector-hana.json
MCP
Connecting an agent