Orchestrate

SAP HANA Cloud vector engine: embeddings next to business data, searched with SQL

How SAP HANA Cloud stores and searches embeddings in SQL, with business filters, an HNSW index and in-database embeddings, and when to choose it.

Updated Oct 4, 2026Foundational 8 minDeep 40 min
Foundational layer · 8 min read

The 60-second version

A RAG system needs a place to keep embeddings: lists of numbers that stand for the meaning of each passage. Most teams add a separate vector database for that.

SAP HANA Cloud takes another route. Its vector engine stores embeddings in an ordinary database column, next to the business data, and searches them with ordinary SQL. One query can say: "find the passages closest in meaning to this question, but only for company code 1000, only for order-to-cash, and only rules valid today."

SAP made the vector engine generally available in April 2024. As of October 2026 it covers storage, similarity search, an index for speed on large collections, and, on instances with the right option enabled, creating embeddings inside the database.

The decision for a leader is simple to state: should our embeddings live next to our SAP data and its controls, or somewhere else?

Why it matters to the business

Picture the running example. A credit clerk asks why order 4711 is blocked. The answer depends on the company code: one subsidiary blocks on the credit limit alone, another also blocks on overdue items. A rule that starts next month must not be quoted as if it applies today.

A vector search alone can't tell these rules apart. They read almost the same. What separates them is business context: company code, process, valid-from date. In SAP HANA Cloud those are columns in the same table as the embedding, so the filter and the search run in one statement.

Three effects for the business:

  • Fewer wrong answers. Filters keep another subsidiary's rule, or next month's rule, out of the answer. Precision comes from data the company already trusts.
  • One less system. If the company already runs SAP HANA Cloud, there is no extra database to buy, secure, back up and staff. Embeddings follow the database's existing backup, monitoring and access rules.
  • Shorter path to SAP data. Many SAP-side tools, such as SAP's Python clients and CAP, already talk to SAP HANA Cloud. Teams reuse skills they have.

The costs are real too. Embeddings and their index use database memory, so they show up in sizing and in the bill. And the database team now runs an AI workload, with its own tuning and testing.

How SAP does it

As of October 2026, from SAP's own material:

  • Vector type. A column of type REAL_VECTOR holds an embedding of 1 to 65,000 numbers, as SAP Learning describes it. SAP's open-source LangChain package also supports HALF_VECTOR, which stores each number in half the space, on SAP HANA Cloud releases from mid-2025.
  • Search in SQL. Functions such as COSINE_SIMILARITY and L2DISTANCE compare vectors inside normal SELECT statements, so business filters are ordinary WHERE conditions.
  • Index for scale. An HNSW vector index makes searches fast on large collections, at the cost of memory and a small chance of missing a close match. Without an index, the database compares the question with every row, which is exact.
  • Embeddings in the database. A VECTOR_EMBEDDING function turns text into embeddings with SAP-provided models. SAP's integration documentation says this needs the NLP (natural language processing) option enabled on the instance.
  • Developer tools. SAP's Python driver hdbcli, the hana-ml Python client, the langchain-hana package and CAP (SAP's application framework) all work with the vector engine.
  • Generative AI hub. When the vector engine became generally available, SAP said generative AI hub in SAP AI Core would use SAP HANA Cloud to store embeddings. SAP AI Core's grounding service is the managed option, where SAP runs the vector store for you.

Where should our embeddings live?

Situation Good fit Why
Pilot on made-up or public data, this month A local store (Chroma) or exact search in code No procurement; rebuilt in minutes
Company already runs SAP HANA Cloud; answers must respect company code, plant or dates SAP HANA Cloud vector engine Filters are SQL on columns next to the vectors; existing operations and controls
Team wants no index or database to run SAP AI Core grounding (managed) SAP runs storage and search; less control over tuning
Hundreds of millions of vectors, specialised search needs, a team to run it A dedicated vector database Built for that scale; another system to secure and staff
Data lives outside SAP and never meets SAP data Whatever the platform team already runs No reason to move it

A useful rule of thumb: put embeddings where their filters live. If the filter is company code and validity, and those sit in SAP HANA Cloud, the vectors usually belong there too.

Questions to ask

  • Do we already run SAP HANA Cloud, and does our release support the vector types and index we plan to use?
  • Which business columns must filter every search: company code, sales organization, plant, language, validity? Are they stored next to the embeddings?
  • How many passages do we expect in a year, and what do the embeddings and their index add to our memory sizing and cost?
  • Will we compute embeddings in the database (VECTOR_EMBEDDING) or outside it? Is the NLP option enabled, and what does it change in sizing or cost?
  • Which embedding model, and which version, made the stored vectors? Who decides when to change it, and who pays for re-embedding everything?
  • Which database user does the application connect as, and what can that user read?
  • How do we test that search quality holds after we add an index or change its settings?

Common misconceptions

  • "We need a separate vector database for RAG." Not if SAP HANA Cloud already holds the data. The vector engine is a feature of the database, not a separate product.
  • "An index always makes search better." It makes search faster on large collections. It can return slightly different results than exact search. Small collections often don't need one.
  • "Vectors in SAP HANA Cloud inherit SAP authorizations." They don't. A copied passage carries no S/4HANA roles. Filters on company code and similar columns must be designed and enforced by the application.
  • "Any embeddings can be mixed in one table." Vectors from different models, or different versions of one model, can't be compared. Record the model with every row.
  • "In-database embeddings are always available." They need the NLP option on the instance. Check before planning on them.

Key terms

  • Vector engine: the SAP HANA Cloud features that store and compare embeddings.
  • REAL_VECTOR / HALF_VECTOR: column types for embeddings; half vectors use half the space per number.
  • Similarity search: finding the rows whose vectors are closest to a question's vector.
  • Exact search: comparing the question with every row; always finds the true closest rows.
  • HNSW index: a graph-based index that makes similarity search fast on large collections, with a small chance of missing a match.
  • VECTOR_EMBEDDING: an SAP HANA Cloud function that turns text into an embedding inside the database.
  • NLP option: an instance setting that SAP's documentation names as needed for in-database embeddings and re-ranking.
  • Business filter: a WHERE condition on a business column, such as company code, that narrows a search.

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1What is the main reason to keep embeddings in SAP HANA Cloud rather than a separate store?

    Answer: A. The vectors sit in a table next to business columns, so one statement can filter and rank. Authorizations are not copied into embeddings; filters still have to be designed.
  2. 2Two subsidiaries have nearly identical credit-block rules. How does the vector engine help keep them apart?

    Answer: C. Rules that read alike get similar embeddings, so similarity alone can't separate them. The company code column next to the vector can, in the same query.
  3. 3Your team plans an HNSW index for 2,000 help notes. What do you ask first?

    Answer: B. An index speeds up large collections at the cost of memory and a small chance of missing matches. Small collections are often served well by exact search.
  4. 4A vendor proposes VECTOR_EMBEDDING to create embeddings inside SAP HANA Cloud. What must you check?

    Answer: D. SAP's integration documentation says in-database embeddings need NLP enabled on the instance. That is a configuration and cost question to settle before planning on it.
  5. 5Six months in, the team wants a newer embedding model. What is the main consequence?

    Answer: B. Vectors from different models can't be compared. Changing the model means re-embedding everything, which is why the model should be recorded with every row.
  6. 6Which situation points away from the SAP HANA Cloud vector engine?

    Answer: C. With the vector engine, your team runs the database side: sizing, indexes, tests. A team that wants none of that would look at a managed option such as SAP AI Core grounding.
Deep layer · 40 min read

Mental model: a vector is just another column

Everything in this topic follows from one idea: in SAP HANA Cloud, an embedding is a column in a table. It sits beside COMPANY_CODE, PROCESS and VALID_FROM. A similarity search is a SELECT that computes a score per row and sorts by it.

So the questions you already know from SQL apply: which rows are visible, which filter runs, which index helps, what the query costs. The new parts are few: a vector type, two similarity functions, a vector index with three dials, and an optional function that creates embeddings in the database.

flowchart LR
  Q[Question] --> E[Embedding<br/>384 numbers]
  E --> S["SELECT TOP 3 ...<br/>COSINE_SIMILARITY"]
  F["WHERE company code,<br/>process, valid from"] --> S
  T[(COURSE_HELP_CHUNKS<br/>text, labels, REAL_VECTOR)] --> S
  I[HNSW index<br/>optional] -.-> S
  S --> R[Top passages<br/>to the model]

You met the pieces before. Vector databases explained showed exact search versus HNSW. Hybrid search and metadata filtering showed filters as a precision and access tool. This topic runs both inside SAP HANA Cloud.

How it works

The vector types

SAP Learning describes REAL_VECTOR as a vector of IEEE 754 single-precision numbers, so each number takes 4 bytes. A column can fix its length, REAL_VECTOR(384), or leave it open. Fix it: a fixed length rejects vectors from a model with the wrong size at insert time, which catches mix-ups early.

SAP's langchain-hana package also accepts HALF_VECTOR, with 2 bytes per number. Its code says HALF_VECTOR needs SAP HANA Cloud version 2025.15 (QRC 2/2025) or later, and REAL_VECTOR needs 2024.2 (QRC 1/2024). Half precision halves the memory of the vectors; test that recall stays acceptable for your data before you switch.

SAP Learning lists restrictions on REAL_VECTOR. You can't sort or group by the vector itself, use it in arithmetic, or use it in row tables or as a partitioning key. Its list also names Parquet files and the native storage extension (NSE), HANA's disk-based store. Restrictions change between releases, so check the SAP HANA Cloud Vector Engine Guide for yours.

Writing vectors

SAP Learning shows the text form: TO_REAL_VECTOR('[0.1, 0.8]') turns a bracketed list into a vector, and TO_NVARCHAR(vector) turns it back into text. That is readable and good for learning.

For loading many rows, SAP's own langchain-hana package sends vectors in a binary format instead: a 4-byte little-endian number of dimensions, then each number as a 4-byte float (2 bytes for HALF_VECTOR). It passes those bytes straight to the vector column as a query parameter. The lab below does the same for loading and uses the text form for questions.

Searching: similarity, filters and TOP

A search computes a score per row and keeps the best:

SELECT TOP 3 ID, TEXT,
       COSINE_SIMILARITY(EMBEDDING, TO_REAL_VECTOR(?)) AS SCORE
  FROM COURSE_HELP_CHUNKS
 WHERE (COMPANY_CODE = ? OR COMPANY_CODE = '*')
   AND VALID_FROM <= ?
 ORDER BY SCORE DESC
Function Range (SAP Learning) Sort for "most alike"
COSINE_SIMILARITY(a, b) -1 to 1; higher is more alike DESC
L2DISTANCE(a, b) 0 or more; lower is more alike ASC

If your embeddings are normalised to length 1, as the course's Sentence Transformers vectors are, both functions rank rows in the same order. Pick one and use it everywhere, because a vector index is built for one function.

SAP Learning also shows similarity inside a WHERE condition, such as COSINE_SIMILARITY(...) > 0.8. A threshold like that is useful for "no good match" answers. The right value depends on your model and data; measure it, don't copy it.

The ? marks are parameters. The driver sends the values separately from the SQL text, so a question or a company code typed by a user can never change the statement. Never build the WHERE clause by pasting user input into the SQL string.

Exact search and the HNSW index

Without a vector index, SAP HANA Cloud compares the question with every row that passes the filters. That is exact search: the true top k, every time. Its cost grows with the number of rows.

An HNSW vector index builds a graph of near neighbours so a search visits only a small part of the data. Both SAP Python packages create it with SQL of this shape:

CREATE HNSW VECTOR INDEX COURSE_HELP_CHUNKS_IDX
  ON COURSE_HELP_CHUNKS (EMBEDDING)
  SIMILARITY FUNCTION COSINE_SIMILARITY
  BUILD CONFIGURATION '{"M": 16, "efConstruction": 100}'
  SEARCH CONFIGURATION '{"efSearch": 100}'
  ONLINE
Part What it does
SIMILARITY FUNCTION COSINE_SIMILARITY or L2DISTANCE; queries must rank by the same function to use the index
M Links per node in the graph; more links, more memory, better recall. langchain-hana accepts 4 to 1000
efConstruction Candidates considered while building; slower build, better graph. 1 to 100,000
efSearch Candidates considered per query; slower search, better recall. 1 to 100,000
ONLINE SAP's hana-ml documentation: takes a shared table lock, so inserts and updates continue; without it, an exclusive lock blocks the table while the index builds

Leave out the configurations and the database uses its own defaults. DROP INDEX name ONLINE removes an index, again with a shared lock.

How do you compare index and exact search on the same data? SAP's hana-ml client shows the switch: when its use_vector_index option is off, it adds WITH HINT (NO_VECTOR_INDEX) to the query. That hint asks for exact search even when an index exists. The lab uses it to measure recall: the share of the true top 10 that the index also returns.

Embeddings inside the database

SAP HANA Cloud can create embeddings itself with VECTOR_EMBEDDING(text, type, model). SAP's hana-ml client calls it with the type 'DOCUMENT' for stored passages and 'QUERY' for questions, and lists two model IDs: SAP_NEB.20240715 (its default) and SAP_GXY.20250407. SAP's LangChain documentation says the instance needs NLP enabled.

The same documentation describes in-database re-ranking with a cross-encoder model (SAP_CER.20250701 in its example), also with NLP enabled. The package code calls it through a window function, CROSS_ENCODE(...) OVER(). This gives you a database-side option for the stage-2 re-ranking from Re-ranking and query rewriting.

Why it matters: in-database embeddings mean the text doesn't leave the database to be embedded, and a calculated column can keep embeddings in step with the text. The trade-off is that you are tied to the models SAP offers there. This course computes embeddings in Python, so every lab also runs without the NLP option.

Reading the version

Features depend on the release. SAP's langchain-hana package reads it with SELECT CLOUD_VERSION FROM SYS.M_DATABASE before it allows HALF_VECTOR. The lab's info command runs the same query, so you know which release you are on before you try a feature.

Build it yourself: a vector table in SAP HANA Cloud

Before you start: complete Set up your computer for this course and Set up for Unit 7. That setup installed hdbcli, created your trial SAP HANA Cloud instance, saved the four HANA_DB_ lines in .env, and proved a tiny vector search with hana_hello.py. For real embeddings you need Sentence Transformers from Set up for Unit 3; without it, add --offline. If you made unit07/chunks.jsonl in Chunking and document preparation, the lab loads it too.

You will build hana_vector_lab.py, one script with five commands. load embeds twelve made-up SAP help notes and loads them into a table with business columns. search runs a filtered similarity search in SQL. index creates or drops an HNSW index. bench loads 20,000 synthetic vectors and measures the index against exact search. info shows the version and size. Every command also runs with --sample, where a local file stands in for the database, so you can follow along without an instance.

flowchart LR
  N[12 help notes<br/>+ chunks.jsonl] --> L[load<br/>embed + insert]
  L --> T[(SAP HANA Cloud<br/>or --sample file)]
  T --> S[search<br/>SQL + filters]
  T --> I[index create / drop]
  B[bench<br/>20,000 vectors] --> R[recall and ms:<br/>index vs exact]

What you need

  • Your course folder with the Unit 7 setup. No new library: the lab uses hdbcli, python-dotenv and numpy, which arrived with the Unit 3 and Unit 7 libraries.
  • For the real runs: your SAP HANA Cloud trial instance, started today in SAP HANA Cloud Central. Cost: free on the BTP trial.
  • About 45 minutes, plus a few minutes for bench to load 20,000 rows.
  • No account? Add --sample to every command. Searches still run; the index steps show the SQL they would send.

Step 1: Open your course folder and start the database

  1. Open a terminal and turn on your environment.

    Windows (PowerShell):

    cd $HOME\orchestrate-course
    .\.venv\Scripts\Activate.ps1

    macOS/Linux:

    cd ~/orchestrate-course
    source .venv/bin/activate
  2. Open SAP HANA Cloud Central (BTP cockpit, Instances and Subscriptions, click SAP HANA Cloud). Find orchestrate-hana. If it is stopped, open its actions menu (the three dots) and click Start. Wait until it shows as running.

  3. Prove the connection still works with the setup script:

    python unit07/hana_hello.py

    You should see Connected as DBADMIN. and two ranked notes. If it times out, the instance isn't running yet. Skip this step if you are using --sample.

Step 2: Create the script

  1. In VS Code, right-click the unit07 folder, choose New File, name it hana_vector_lab.py, paste the code below and save.
"""Unit 7: the SAP HANA Cloud vector engine, end to end.

Loads made-up SAP help notes (and unit07/chunks.jsonl if you made it) into a table with a
REAL_VECTOR column, searches them with COSINE_SIMILARITY plus business filters in SQL, builds
and drops an HNSW vector index, and measures index recall and speed against exact search.

How to run (from your course folder, with .venv turned on):
    python unit07/hana_vector_lab.py load                      # embed the notes and load them
    python unit07/hana_vector_lab.py search "why is this order blocked" --company-code 1000
    python unit07/hana_vector_lab.py index create --m 16 --ef-construction 100 --ef-search 100
    python unit07/hana_vector_lab.py index drop
    python unit07/hana_vector_lab.py bench --n 20000           # index vs exact search
    python unit07/hana_vector_lab.py info
Options for every command:
    --sample    no account: a local file stands in for SAP HANA Cloud (searches are always exact)
    --offline   toy embeddings, no model download (matches words, not meaning)
    --show-sql  print each SQL statement before it runs
"""
import argparse
import datetime as dt
import hashlib
import json
import math
import os
import re
import struct
import sys
import time
from pathlib import Path

HERE = Path(__file__).resolve().parent
CHUNKS = HERE / "chunks.jsonl"                 # from "Chunking and document preparation" (optional)
SAMPLE_DB = HERE / "hana_lab_sample.json"      # the --sample stand-in for the database
TABLE, INDEX = "COURSE_HELP_CHUNKS", "COURSE_HELP_CHUNKS_IDX"
BENCH_TABLE, BENCH_INDEX = "COURSE_BENCH_VECTORS", "COURSE_BENCH_VECTORS_IDX"
DIM = 384                                      # all-MiniLM-L6-v2 gives 384 numbers per text
MODEL = "sentence-transformers/all-MiniLM-L6-v2"

# Made-up help notes: (id, process, company code or * for all, valid from, text)
NOTES = [
    ("kb-001", "order-to-cash", "1000", "2026-01-01", "Company code 1000: a sales order is blocked for delivery when the customer's open items plus the order value exceed the credit limit. Credit management releases it."),
    ("kb-002", "order-to-cash", "2000", "2026-01-01", "Company code 2000: a sales order is blocked when it exceeds the credit limit, and also when the customer has items overdue by more than 30 days."),
    ("kb-003", "order-to-cash", "1000", "2026-11-01", "Company code 1000, from November 2026: credit blocks on orders below 500 EUR are released automatically each night."),
    ("kb-004", "order-to-cash", "*", "2025-07-01", "Orders for export customers stop at delivery when customs or export documents are missing."),
    ("kb-005", "order-to-cash", "*", "2025-07-01", "A delivery block set by the sales team holds an order until pricing or payment terms are confirmed with the customer."),
    ("kb-006", "procure-to-pay", "*", "2025-07-01", "An invoice is blocked for payment when the invoiced quantity is higher than the quantity received in goods receipt."),
    ("kb-007", "procure-to-pay", "1000", "2026-07-01", "Company code 1000: a price difference of more than 5 percent between purchase order and supplier invoice blocks the invoice."),
    ("kb-008", "procure-to-pay", "2000", "2026-07-01", "Company code 2000: a price difference of more than 2 percent between purchase order and supplier invoice blocks the invoice."),
    ("kb-009", "procure-to-pay", "*", "2025-07-01", "Three-way match compares purchase order, goods receipt and invoice before the invoice can be paid."),
    ("kb-010", "plan-to-produce", "*", "2025-07-01", "MRP raises an exception message when a planned receipt arrives after the date the material is needed."),
    ("kb-011", "plan-to-produce", "*", "2025-07-01", "A reschedule-in exception means MRP suggests moving a planned order earlier to cover demand."),
    ("kb-012", "plan-to-produce", "3000", "2026-01-01", "Company code 3000: planners review MRP exceptions every morning before 9:00, starting with materials on the critical parts list."),
]

SQL_CREATE = (f"CREATE COLUMN TABLE {TABLE} (ID NVARCHAR(100) PRIMARY KEY, TEXT NVARCHAR(5000), "
              "SOURCE NVARCHAR(200), PROCESS NVARCHAR(20), COMPANY_CODE NVARCHAR(4), VALID_FROM DATE, "
              f"EMBED_MODEL NVARCHAR(60), EMBEDDING REAL_VECTOR({DIM}))")
SQL_INSERT = f"INSERT INTO {TABLE} VALUES (?, ?, ?, ?, ?, ?, ?, ?)"


# ---------- embeddings ----------

def toy_embed(text: str) -> list:
    """Stand-in for a real model: hash each word into one of 384 slots. Matches words, not meaning."""
    vector = [0.0] * DIM
    for word in re.findall(r"[a-z0-9]+", text.lower()):
        vector[int(hashlib.md5(word.encode()).hexdigest(), 16) % DIM] += 1.0
    length = math.sqrt(sum(v * v for v in vector)) or 1.0
    return [v / length for v in vector]


def get_embedder(offline: bool):
    """Return (model name stored with each row, function from a list of texts to a list of vectors)."""
    if offline:
        return "toy-hash-384", lambda texts: [toy_embed(t) for t in texts]
    try:
        from sentence_transformers import SentenceTransformer
    except ImportError:
        sys.exit("sentence-transformers is not installed. See Set up for Unit 3, or add --offline.")
    try:
        model = SentenceTransformer(MODEL)
    except Exception as error:
        sys.exit(f"Could not load the model ({type(error).__name__}). Check your network, or add --offline.")
    return "minilm-384", lambda texts: model.encode(texts, normalize_embeddings=True).tolist()


def vector_text(vector) -> str:
    """'[0.1,0.2,...]': the text form that TO_REAL_VECTOR turns into a vector."""
    return "[" + ",".join(f"{v:.6f}" for v in vector) + "]"


def vector_bytes(vector) -> bytes:
    """Binary REAL_VECTOR: 4-byte little-endian length, then 4-byte floats. Faster than text for loading."""
    return struct.pack(f"<I{len(vector)}f", len(vector), *vector)


def cosine(a, b) -> float:
    dot = sum(x * y for x, y in zip(a, b))
    return dot / ((math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b))) or 1.0)


# ---------- the SQL for a filtered vector search ----------

def search_sql(top: int, company_code, process, valid_on, exact: bool, table: str = TABLE):
    """Build the SELECT and the list of filter values. The question vector is the first parameter."""
    where, params = [], []
    if company_code:
        where.append("(COMPANY_CODE = ? OR COMPANY_CODE = '*')")
        params.append(company_code)
    if process:
        where.append("PROCESS = ?")
        params.append(process)
    if valid_on:
        where.append("VALID_FROM <= ?")
        params.append(valid_on)
    sql = (f"SELECT TOP {int(top)} ID, PROCESS, COMPANY_CODE, TEXT, "
           f"COSINE_SIMILARITY(EMBEDDING, TO_REAL_VECTOR(?)) AS SCORE FROM {table}")
    if where:
        sql += " WHERE " + " AND ".join(where)
    sql += " ORDER BY SCORE DESC"
    if exact:
        sql += " WITH HINT (NO_VECTOR_INDEX)"   # compare every row, even if an index exists
    return sql, params


def index_sql(table: str, index: str, m, ef_construction, ef_search) -> str:
    build = {k: v for k, v in (("M", m), ("efConstruction", ef_construction)) if v}
    sql = f"CREATE HNSW VECTOR INDEX {index} ON {table} (EMBEDDING) SIMILARITY FUNCTION COSINE_SIMILARITY"
    if build:
        sql += f" BUILD CONFIGURATION '{json.dumps(build)}'"
    if ef_search:
        sql += f" SEARCH CONFIGURATION '{json.dumps({'efSearch': ef_search})}'"
    return sql + " ONLINE"


# ---------- two backends with the same methods ----------

class HanaBackend:
    """Talks to your SAP HANA Cloud instance with SAP's hdbcli driver."""

    def __init__(self, show_sql: bool):
        try:
            from dotenv import load_dotenv
            from hdbcli import dbapi
        except ImportError as error:
            sys.exit(f"{error.name} is not installed. Run: pip install -r requirements.txt")
        load_dotenv()
        names = ["HANA_DB_ADDRESS", "HANA_DB_PORT", "HANA_DB_USER", "HANA_DB_PASSWORD"]
        missing = [n for n in names if not os.getenv(n)]
        if missing:
            sys.exit(f"Missing in .env: {', '.join(missing)}. See Set up for Unit 7, Step 5, or add --sample.")
        self.dbapi, self.show_sql = dbapi, show_sql
        try:
            self.conn = dbapi.connect(address=os.getenv("HANA_DB_ADDRESS"), port=int(os.getenv("HANA_DB_PORT")),
                                      user=os.getenv("HANA_DB_USER"), password=os.getenv("HANA_DB_PASSWORD"))
        except dbapi.Error as error:
            sys.exit(f"Could not connect: {error}\nIs the instance running (it stops every night)? "
                     "Check the address, port, user, password and the allowed IP addresses.")

    def run(self, sql, params=None, many=False, quiet_errors=False):
        if self.show_sql:
            print(f"  SQL> {sql}")
        cursor = self.conn.cursor()
        try:
            if many:
                cursor.executemany(sql, params)
            else:
                cursor.execute(sql, params or [])
            return cursor.fetchall() if cursor.description else []
        except self.dbapi.Error as error:
            if quiet_errors:
                return None
            sys.exit(f"SAP HANA Cloud refused the statement: {error}")
        finally:
            cursor.close()

    def reset_table(self, create_sql, table):
        self.run(f"DROP TABLE {table}", quiet_errors=True)   # normal to fail the first time
        self.run(create_sql)

    def insert(self, sql, rows):
        for start in range(0, len(rows), 1000):            # send 1,000 rows per round trip
            batch = [r[:-1] + (vector_bytes(r[-1]),) for r in rows[start:start + 1000]]
            self.run(sql, batch, many=True)

    def search(self, vector, top, company_code=None, process=None, valid_on=None, exact=False, table=TABLE):
        sql, params = search_sql(top, company_code, process, valid_on, exact, table)
        return self.run(sql, [vector_text(vector)] + params)

    def models(self):
        rows = self.run(f"SELECT DISTINCT EMBED_MODEL FROM {TABLE}", quiet_errors=True)
        return None if rows is None else {r[0] for r in rows}

    def create_index(self, table, index, m, efc, efs):
        self.run(index_sql(table, index, m, efc, efs))

    def drop_index(self, index):
        return self.run(f"DROP INDEX {index} ONLINE", quiet_errors=True) is not None

    def info(self):
        version = self.run("SELECT CLOUD_VERSION FROM SYS.M_DATABASE", quiet_errors=True)
        count = self.run(f"SELECT COUNT(*) FROM {TABLE}", quiet_errors=True)
        return (version[0][0] if version else "unknown"), (count[0][0] if count else None)


class SampleBackend:
    """No account: keeps rows in unit07/hana_lab_sample.json and searches them exactly in Python.
    It prints the same SQL the real backend would send, so you can read along."""

    def __init__(self, show_sql: bool):
        self.show_sql = show_sql
        self.data = json.loads(SAMPLE_DB.read_text(encoding="utf-8")) if SAMPLE_DB.exists() else {}

    def _sql(self, sql):
        if self.show_sql:
            print(f"  SQL> {sql}")

    def _save(self):
        SAMPLE_DB.write_text(json.dumps(self.data), encoding="utf-8")

    def reset_table(self, create_sql, table):
        self._sql(f"DROP TABLE {table}")
        self._sql(create_sql)
        self.data = {"rows": [], "index": None}

    def insert(self, sql, rows):
        self._sql(sql)
        self.data["rows"] += [list(r[:5]) + [str(r[5]), r[6], r[7]] for r in rows]
        self._save()

    def search(self, vector, top, company_code=None, process=None, valid_on=None, exact=False, table=TABLE):
        self._sql(search_sql(top, company_code, process, valid_on, exact, table)[0])
        if not self.data.get("rows"):
            sys.exit("The sample store is empty. Run first: python unit07/hana_vector_lab.py load --sample")
        hits = []
        for rid, text, _src, proc, code, valid, _model, emb in self.data["rows"]:
            if company_code and code not in (company_code, "*"):
                continue
            if process and proc != process:
                continue
            if valid_on and valid > str(valid_on):
                continue
            hits.append((rid, proc, code, text, cosine(emb, vector)))
        return sorted(hits, key=lambda h: -h[4])[:top]

    def models(self):
        return {r[6] for r in self.data["rows"]} if self.data.get("rows") else None

    def create_index(self, table, index, m, efc, efs):
        self._sql(index_sql(table, index, m, efc, efs))
        self.data["index"] = {"M": m, "efConstruction": efc, "efSearch": efs}
        self._save()

    def drop_index(self, index):
        self._sql(f"DROP INDEX {index} ONLINE")
        had = bool(self.data.get("index"))
        self.data["index"] = None
        self._save()
        return had

    def info(self):
        return "sample (no database)", len(self.data.get("rows", [])) if self.data else None


# ---------- commands ----------

def read_rows(model_name, embed):
    """Built-in notes plus chunks.jsonl if it exists; returns rows ready to insert."""
    records = [(i, t, "course-help-notes", p, c, v) for i, p, c, v, t in NOTES]
    if CHUNKS.exists():
        for line in CHUNKS.read_text(encoding="utf-8").splitlines():
            if line.strip():
                rec = json.loads(line)
                meta = rec.get("metadata", {})
                records.append((rec["id"][:100], rec["text"][:5000], meta.get("source", "chunks.jsonl"),
                                meta.get("process", "procure-to-pay"), meta.get("company_code", "*"),
                                meta.get("valid_from") or "2025-01-01"))
    vectors = embed([r[1] for r in records])
    return [r[:5] + (dt.date.fromisoformat(r[5]), model_name, vec) for r, vec in zip(records, vectors)]


def cmd_load(db, args):
    model_name, embed = get_embedder(args.offline)
    rows = read_rows(model_name, embed)
    db.reset_table(SQL_CREATE, TABLE)
    db.insert(SQL_INSERT, rows)
    extra = len(rows) - len(NOTES)
    print(f"Loaded {len(rows)} rows into {TABLE} ({len(NOTES)} help notes"
          + (f" + {extra} chunks from chunks.jsonl" if extra else "") + f"), embedded with {model_name}.")


def cmd_search(db, args):
    model_name, embed = get_embedder(args.offline)
    stored = db.models()
    if stored is None:
        sys.exit(f"No table {TABLE} yet. Run first: python unit07/hana_vector_lab.py load"
                 + (" --sample" if args.sample else ""))
    if stored != {model_name}:
        sys.exit(f"The table was embedded with {', '.join(sorted(stored))}, but this run uses {model_name}. "
                 "Vectors from different models can't be compared: run load again with the same options.")
    start = time.perf_counter()
    hits = db.search(embed([args.question])[0], args.top, args.company_code, args.process,
                     args.valid_on, args.exact)
    ms = 1000 * (time.perf_counter() - start)
    filters = [f"{k} {v}" for k, v in (("company code", args.company_code), ("process", args.process),
                                         ("valid on", args.valid_on)) if v]
    print(f'Question: "{args.question}"' + (f"  [{', '.join(filters)}]" if filters else "")
          + ("  (exact search)" if args.exact else ""))
    if not hits:
        print("No rows matched the filters. That is a valid empty result, not an error.")
    for rank, (rid, proc, code, text, score) in enumerate(hits, 1):
        print(f"{rank}. {rid}  {proc}  company {code}  similarity {float(score):.3f}\n   {text[:150]}")
    print(f"({ms:.0f} ms)")


def cmd_index(db, args):
    if args.action == "drop":
        print(f"Dropped {INDEX}." if db.drop_index(INDEX) else f"No index {INDEX} to drop.")
        return
    db.drop_index(INDEX)                     # replace an older index with the new settings
    start = time.perf_counter()
    db.create_index(TABLE, INDEX, args.m, args.ef_construction, args.ef_search)
    print(f"Created HNSW index {INDEX} in {time.perf_counter() - start:.1f} s "
          f"(M {args.m or 'default'}, efConstruction {args.ef_construction or 'default'}, "
          f"efSearch {args.ef_search or 'default'}).")
    if args.sample:
        print("Sample mode only records the settings; its searches stay exact. Run without --sample to use a real index.")


def cmd_bench(db, args):
    try:
        import numpy as np
    except ImportError:
        sys.exit("numpy is not installed. Run: pip install -r requirements.txt")
    rng = np.random.default_rng(7)
    centres = rng.normal(size=(200, DIM)).astype(np.float32)
    centres /= np.linalg.norm(centres, axis=1, keepdims=True)

    def make(n):
        noise = rng.normal(size=(n, DIM)).astype(np.float32)
        noise /= np.linalg.norm(noise, axis=1, keepdims=True)
        v = centres[rng.integers(0, 200, size=n)] + 0.9 * noise
        return v / np.linalg.norm(v, axis=1, keepdims=True)

    vectors, queries = make(args.n), make(args.queries)
    codes = rng.choice(["1000", "2000", "3000"], size=args.n, p=[0.60, 0.395, 0.005])
    k = 10

    def truth(code=None):
        ids = np.arange(args.n) if code is None else np.where(codes == code)[0]
        scores = queries @ vectors[ids].T
        return [set(ids[np.argsort(-row)[:k]].tolist()) for row in scores]

    def measure(exact, code=None, expected=None):
        found, start = [], time.perf_counter()
        for q in queries:
            rows = db.search(q.tolist(), k, company_code=code, exact=exact, table=BENCH_TABLE)
            found.append({int(r[0]) for r in rows})
        ms = 1000 * (time.perf_counter() - start) / len(queries)
        return ms, sum(len(f & t) / k for f, t in zip(found, expected)) / len(expected)

    print(f"Made {args.n:,} synthetic vectors of {DIM} numbers; company code 3000 holds "
          f"{int((codes == '3000').sum())} of them (0.5%). {args.queries} test questions, top {k}.")
    if args.sample:
        start = time.perf_counter()
        truth()
        print(f"Sample mode: exact top {k} for all questions in Python took {time.perf_counter() - start:.2f} s.")
        print("The index comparison needs SAP HANA Cloud. Run without --sample. It would send:")
        print("  " + index_sql(BENCH_TABLE, BENCH_INDEX, args.m, args.ef_construction, args.ef_search))
        print("  " + search_sql(k, "3000", None, None, True, BENCH_TABLE)[0])
        return

    db.reset_table(f"CREATE COLUMN TABLE {BENCH_TABLE} (ID INTEGER PRIMARY KEY, PROCESS NVARCHAR(20), "
                   f"COMPANY_CODE NVARCHAR(4), TEXT NVARCHAR(10), EMBEDDING REAL_VECTOR({DIM}))", BENCH_TABLE)
    start = time.perf_counter()
    db.insert(f"INSERT INTO {BENCH_TABLE} VALUES (?, ?, ?, ?, ?)",
              [(i, "bench", str(codes[i]), "", vectors[i].tolist()) for i in range(args.n)])
    print(f"Loaded in {time.perf_counter() - start:.1f} s.")
    all_truth, rare_truth = truth(), truth("3000")
    results = [("exact (NO_VECTOR_INDEX)", "all") + measure(True, None, all_truth),
               ("exact (NO_VECTOR_INDEX)", "3000") + measure(True, "3000", rare_truth)]
    start = time.perf_counter()
    db.create_index(BENCH_TABLE, BENCH_INDEX, args.m, args.ef_construction, args.ef_search)
    print(f"Built the HNSW index in {time.perf_counter() - start:.1f} s.")
    results += [("HNSW index", "all") + measure(False, None, all_truth),
                ("HNSW index", "3000") + measure(False, "3000", rare_truth)]
    print(f"\n{'search':26}{'company code':>14}{'ms per query':>14}{'recall@10':>11}")
    for name, code, ms, rec in results:
        print(f"{name:26}{code:>14}{ms:>14.1f}{rec:>11.2f}")
    if not args.keep:
        db.run(f"DROP TABLE {BENCH_TABLE}")
        print(f"\nDropped {BENCH_TABLE} (its index goes with it). Use --keep to look at it first.")


def cmd_info(db, args):
    version, count = db.info()
    print(f"Database version: {version}")
    if count is None:
        print(f"No table {TABLE} yet. Run: python unit07/hana_vector_lab.py load")
        return
    models = db.models() or set()
    print(f"{TABLE}: {count} rows, embedding model(s): {', '.join(sorted(models)) or '-'}")
    print(f"Vector data: about {count * (DIM * 4 + 4) / 1024:.0f} KB ({count} rows x {DIM} numbers x 4 bytes, "
          "before index, text and other columns)")


def main():
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--sample", action="store_true", help="no account: use a local file instead of HANA")
    common.add_argument("--offline", action="store_true", help="toy embeddings; no model download")
    common.add_argument("--show-sql", action="store_true", help="print each SQL statement")
    hnsw = argparse.ArgumentParser(add_help=False)
    hnsw.add_argument("--m", type=int, help="links per node (database default if left out)")
    hnsw.add_argument("--ef-construction", type=int, help="candidates while building")
    hnsw.add_argument("--ef-search", type=int, help="candidates while searching")

    parser = argparse.ArgumentParser(description="SAP HANA Cloud vector engine lab.")
    sub = parser.add_subparsers(dest="command", required=True)
    sub.add_parser("load", parents=[common], help="embed the notes and load the table")
    s = sub.add_parser("search", parents=[common], help="filtered vector search")
    s.add_argument("question")
    s.add_argument("--company-code", help="e.g. 1000; rows for * (all company codes) always match")
    s.add_argument("--process", choices=["order-to-cash", "procure-to-pay", "plan-to-produce"])
    s.add_argument("--valid-on", type=dt.date.fromisoformat, help="only rows valid on this date, YYYY-MM-DD")
    s.add_argument("--top", type=int, default=3)
    s.add_argument("--exact", action="store_true", help="add WITH HINT (NO_VECTOR_INDEX)")
    i = sub.add_parser("index", parents=[common, hnsw], help="create or drop the HNSW index")
    i.add_argument("action", choices=["create", "drop"])
    b = sub.add_parser("bench", parents=[common, hnsw], help="recall and speed: index vs exact")
    b.add_argument("--n", type=int, default=20000, help="how many synthetic vectors (default 20000)")
    b.add_argument("--queries", type=int, default=50, help="how many test questions (default 50)")
    b.add_argument("--keep", action="store_true", help="don't drop the bench table at the end")
    sub.add_parser("info", parents=[common], help="version, row count, models, size estimate")
    args = parser.parse_args()

    db = SampleBackend(args.show_sql) if args.sample else HanaBackend(args.show_sql)
    {"load": cmd_load, "search": cmd_search, "index": cmd_index, "bench": cmd_bench, "info": cmd_info}[args.command](db, args)


if __name__ == "__main__":
    main()

Step 3: Load the help notes

  1. Load the notes with the real embedding model:

    python unit07/hana_vector_lab.py load --show-sql

    No instance? Run python unit07/hana_vector_lab.py load --sample --show-sql. No model? Add --offline.

What success looks like (from our test with --sample --offline; the real run prints the same lines):

  SQL> DROP TABLE COURSE_HELP_CHUNKS
  SQL> CREATE COLUMN TABLE COURSE_HELP_CHUNKS (ID NVARCHAR(100) PRIMARY KEY, TEXT NVARCHAR(5000), SOURCE NVARCHAR(200), PROCESS NVARCHAR(20), COMPANY_CODE NVARCHAR(4), VALID_FROM DATE, EMBED_MODEL NVARCHAR(60), EMBEDDING REAL_VECTOR(384))
  SQL> INSERT INTO COURSE_HELP_CHUNKS VALUES (?, ?, ?, ?, ?, ?, ?, ?)
Loaded 12 rows into COURSE_HELP_CHUNKS (12 help notes), embedded with toy-hash-384.

With the real model it says embedded with minilm-384. If you have chunks.jsonl, it adds + 11 chunks from chunks.jsonl (your count may differ). The first DROP TABLE fails quietly on the first run, because there is nothing to drop yet; that is expected.

Look at the table: every row has text, three business columns, the name of the model that made its vector, and the vector itself. Company code * means "applies to all company codes".

Step 4: Search with and without business filters

  1. Ask the running example's question with no filter:

    python unit07/hana_vector_lab.py search "why is this customer's order blocked for delivery"
  2. Ask again as someone working in company code 2000:

    python unit07/hana_vector_lab.py search "why is this customer's order blocked for delivery" --company-code 2000 --show-sql

What success looks like (from our test with --sample --offline; with the real model your scores and some ranks will differ):

Question: "why is this customer's order blocked for delivery"
1. kb-001  order-to-cash  company 1000  similarity 0.433
   Company code 1000: a sales order is blocked for delivery when the customer's open items plus the order value exceed the credit limit. Credit managemen
2. kb-006  procure-to-pay  company *  similarity 0.267
   An invoice is blocked for payment when the invoiced quantity is higher than the quantity received in goods receipt.
3. kb-002  order-to-cash  company 2000  similarity 0.239
   Company code 2000: a sales order is blocked when it exceeds the credit limit, and also when the customer has items overdue by more than 30 days.
(1 ms)
  SQL> SELECT TOP 3 ID, PROCESS, COMPANY_CODE, TEXT, COSINE_SIMILARITY(EMBEDDING, TO_REAL_VECTOR(?)) AS SCORE FROM COURSE_HELP_CHUNKS WHERE (COMPANY_CODE = ? OR COMPANY_CODE = '*') ORDER BY SCORE DESC
Question: "why is this customer's order blocked for delivery"  [company code 2000]
1. kb-006  procure-to-pay  company *  similarity 0.267
   An invoice is blocked for payment when the invoiced quantity is higher than the quantity received in goods receipt.
2. kb-002  order-to-cash  company 2000  similarity 0.239
   Company code 2000: a sales order is blocked when it exceeds the credit limit, and also when the customer has items overdue by more than 30 days.
3. kb-005  order-to-cash  company *  similarity 0.209
   A delivery block set by the sales team holds an order until pricing or payment terms are confirmed with the customer.
(1 ms)

Without a filter, company 1000's rule ranks first, which is wrong for a clerk in company code 2000. With the filter, it is gone, and company 2000's rule is in the list. The toy embeddings still rank an invoice note high, because they share words like "blocked"; add --process order-to-cash to remove it, or use the real model. Against SAP HANA Cloud the time includes the network round trip, so it will be higher than in sample mode.

  1. Try the validity filter. A rule for company code 1000 starts in November 2026:

    python unit07/hana_vector_lab.py search "orders below 500 EUR released automatically" --company-code 1000 --valid-on 2026-10-04
    python unit07/hana_vector_lab.py search "orders below 500 EUR released automatically" --company-code 1000 --valid-on 2026-12-01 --top 2

What success looks like (from our test with --sample --offline, second command):

Question: "orders below 500 EUR released automatically"  [company code 1000, valid on 2026-12-01]
1. kb-003  order-to-cash  company 1000  similarity 0.639
   Company code 1000, from November 2026: credit blocks on orders below 500 EUR are released automatically each night.
2. kb-004  order-to-cash  company *  similarity 0.192
   Orders for export customers stop at delivery when customs or export documents are missing.
(0 ms)

On 4 October the new rule doesn't appear at all; on 1 December it ranks first. The vector search didn't change. The VALID_FROM <= ? condition did the work.

  1. Try a filter that matches nothing: --company-code 9999 --process plan-to-produce. You get the two MRP notes that apply to all company codes. A filter that leaves no rows prints No rows matched the filters, which is a valid empty result, not an error.

Step 5: Create and drop an HNSW index

  1. Create an index on the help notes table:

    python unit07/hana_vector_lab.py index create --m 16 --ef-construction 100 --ef-search 100 --show-sql

What success looks like (from our test with --sample; against SAP HANA Cloud the build time is real):

  SQL> DROP INDEX COURSE_HELP_CHUNKS_IDX ONLINE
  SQL> CREATE HNSW VECTOR INDEX COURSE_HELP_CHUNKS_IDX ON COURSE_HELP_CHUNKS (EMBEDDING) SIMILARITY FUNCTION COSINE_SIMILARITY BUILD CONFIGURATION '{"M": 16, "efConstruction": 100}' SEARCH CONFIGURATION '{"efSearch": 100}' ONLINE
Created HNSW index COURSE_HELP_CHUNKS_IDX in 0.0 s (M 16, efConstruction 100, efSearch 100).
Sample mode only records the settings; its searches stay exact. Run without --sample to use a real index.

The script drops any older index first, so you can rerun it with new settings.

  1. Run a search from Step 4 again, then once more with --exact. On twelve rows the results are identical: an index only pays off on large tables, which is what Step 6 measures.

  2. Drop the index again:

    python unit07/hana_vector_lab.py index drop

    You should see Dropped COURSE_HELP_CHUNKS_IDX.

bench makes 20,000 synthetic vectors of 384 numbers, gives each a company code (60% for 1000, 39.5% for 2000, 0.5% for 3000, as in Vector databases explained), and loads them into COURSE_BENCH_VECTORS. It computes the true top 10 for 50 test questions in Python, then asks SAP HANA Cloud four ways: exact and indexed, across all rows and inside company code 3000.

  1. Run it with the settings you found best in the vector databases lab (or the defaults below):

    python unit07/hana_vector_lab.py bench --n 20000 --m 16 --ef-construction 100 --ef-search 100

What success looks like (shape only; we couldn't reach SAP HANA Cloud from our test environment, so the numbers are placeholders):

Made 20,000 synthetic vectors of 384 numbers; company code 3000 holds 106 of them (0.5%). 50 test questions, top 10.
Loaded in xx.x s.
Built the HNSW index in x.x s.

search                      company code  ms per query  recall@10
exact (NO_VECTOR_INDEX)              all          xx.x       1.00
exact (NO_VECTOR_INDEX)             3000          xx.x       1.00
HNSW index                           all          xx.x       0.9x
HNSW index                          3000          xx.x       x.xx

Dropped COURSE_BENCH_VECTORS (its index goes with it). Use --keep to look at it first.

How to read it:

  • Exact rows show 1.00 recall. That is the sanity check: SAP HANA Cloud and Python agree on the true top 10. Anything clearly below 1.00 means the data or the question vectors didn't arrive intact.
  • The HNSW "all" row shows what the index costs in recall and saves in time. On 20,000 rows the saving may be small, because each query also pays the network round trip.
  • The HNSW "3000" row is the one to watch. It tells you how this release handles a rare filter together with the index. Whatever it shows, that is your evidence; don't assume the behaviour of another vector store.

With --sample, bench makes the same data, times the exact search in Python and prints the SQL it would send, then stops. From our test:

Made 20,000 synthetic vectors of 384 numbers; company code 3000 holds 106 of them (0.5%). 50 test questions, top 10.
Sample mode: exact top 10 for all questions in Python took 0.04 s.
The index comparison needs SAP HANA Cloud. Run without --sample. It would send:
  CREATE HNSW VECTOR INDEX COURSE_BENCH_VECTORS_IDX ON COURSE_BENCH_VECTORS (EMBEDDING) SIMILARITY FUNCTION COSINE_SIMILARITY BUILD CONFIGURATION '{"M": 16, "efConstruction": 100}' SEARCH CONFIGURATION '{"efSearch": 100}' ONLINE
  SELECT TOP 10 ID, PROCESS, COMPANY_CODE, TEXT, COSINE_SIMILARITY(EMBEDDING, TO_REAL_VECTOR(?)) AS SCORE FROM COURSE_BENCH_VECTORS WHERE (COMPANY_CODE = ? OR COMPANY_CODE = '*') ORDER BY SCORE DESC WITH HINT (NO_VECTOR_INDEX)

Step 7: Check the version and size

  1. Run:

    python unit07/hana_vector_lab.py info

What success looks like (from our test with --sample --offline; the real run shows your instance's version, such as a number starting with the year):

Database version: sample (no database)
COURSE_HELP_CHUNKS: 12 rows, embedding model(s): toy-hash-384
Vector data: about 18 KB (12 rows x 384 numbers x 4 bytes, before index, text and other columns)

The size line is the vector data alone. An index, the text and the other columns come on top. Use the same sum, rows times dimensions times 4 bytes, for a first sizing estimate of a real knowledge base.

If the model line ever shows two names, you loaded rows with two different models. search refuses to run in that case, because their vectors can't be compared. Run load again with one set of options.

Step 8: Save your work

  1. --sample writes unit07/hana_lab_sample.json. It is rebuildable, so keep it out of Git: open .gitignore, add this line at the end and save:

    unit07/hana_lab_sample.json
  2. Commit:

    git add .gitignore unit07/hana_vector_lab.py
    git commit -m "Unit 7: SAP HANA Cloud vector engine lab"

    Check with git status first: .env must never appear.

What the code does

Part What it does
NOTES Twelve made-up help notes with process, company code (* for all) and valid-from date
get_embedder Sentence Transformers all-MiniLM-L6-v2 (384 numbers), or toy hashed embeddings with --offline
vector_bytes Packs a vector in the binary format SAP's langchain-hana package uses: length, then 4-byte floats
vector_text Writes a vector as [0.1,...] for TO_REAL_VECTOR(?)
search_sql Builds the SELECT TOP with COSINE_SIMILARITY, parameterised filters and, for --exact, WITH HINT (NO_VECTOR_INDEX)
index_sql Builds CREATE HNSW VECTOR INDEX ... ONLINE with optional build and search settings
HanaBackend Connects with hdbcli, runs SQL, inserts 1,000 rows per round trip, reads CLOUD_VERSION
SampleBackend The --sample stand-in: same methods, rows in a JSON file, exact search in Python, prints the same SQL
cmd_search Refuses to search if the table was embedded with another model, then prints the ranked rows
cmd_bench Synthetic data, true top 10 in numpy, then exact versus indexed queries in SAP HANA Cloud, with recall@10

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't found in this terminal Turn on .venv (Step 1); on macOS/Linux use python3 until it is on
hdbcli is not installed or dotenv is not installed Library missing from this .venv Check for (.venv) in the prompt, then pip install -r requirements.txt
numpy is not installed Only bench needs it pip install numpy, then add numpy to requirements.txt
sentence-transformers is not installed or Could not load the model No Unit 3 model, or the download is blocked Add --offline, or follow Set up for Unit 3
Missing in .env: HANA_DB_ADDRESS, ... No connection settings, or you ran from another folder Run from the course folder; see Step 5 of Set up for Unit 7, or add --sample
Could not connect: ... Cannot resolve host name Wrong address, or :443 pasted into it Put the host name only in HANA_DB_ADDRESS and 443 in HANA_DB_PORT
Could not connect with a timeout Instance stopped overnight, IP not allowed, or the network blocks port 443 Start it in SAP HANA Cloud Central; check Allow all IP addresses; try another network or ask IT
No table COURSE_HELP_CHUNKS yet search ran before load Run load with the same --sample choice
The table was embedded with ..., but this run uses ... You loaded with --offline and searched without it, or the other way round Use the same options for load and search
SAP HANA Cloud refused the statement: ... REAL_VECTOR or HNSW The instance release lacks the feature, or the syntax changed Run info for the version; compare with the SAP HANA Cloud Vector Engine Guide; ask in the course questions
bench takes several minutes on Loaded in 20,000 rows over a slow network Try --n 5000 first

The SAP way

As of October 2026, there are several SAP-provided ways to work with the vector engine. All of them end up in the same SQL.

SQL directly, with hdbcli

What the lab does. Full control, no extra library, and the clearest view of what runs. You write the table, the filters and the index yourself. Use it when you need exact control over columns and filters, or when other tools would hide too much.

hana-ml, SAP's Python machine learning client

SAP documents vector methods in hana_ml.dataframe:

  • ConnectionContext.create_vector_index(...) with index_type='HNSW', a similarity function, build_config, search_config and online; drop_vector_index(...) to remove it.
  • DataFrame.add_vector(text_col, text_type='DOCUMENT', model_version=...) adds an embedding column with VECTOR_EMBEDDING.
  • DataFrame.sort_by_similarity(...) ranks rows against a question text or vector, with use_vector_index to choose index or exact search.
  • ConnectionContext.embed_query(...) embeds a question in the database.

It suits data teams already working with HANA DataFrames, and pairs with the TextSplitter from Chunking and document preparation.

langchain-hana, SAP's LangChain package

# SKETCH: needs pip install langchain-hana, a running SAP HANA Cloud instance
# (Set up for Unit 7) and a LangChain embeddings object named embeddings.
import os
from dotenv import load_dotenv
from hdbcli import dbapi
from langchain_hana import HanaDB

load_dotenv()
connection = dbapi.connect(address=os.getenv("HANA_DB_ADDRESS"), port=int(os.getenv("HANA_DB_PORT")),
                           user=os.getenv("HANA_DB_USER"), password=os.getenv("HANA_DB_PASSWORD"))
store = HanaDB(connection=connection, embedding=embeddings, table_name="HELP_NOTES",
               specific_metadata_columns=["company_code"])     # a real column, faster to filter
store.add_texts(["Company code 1000: orders above the credit limit are blocked."],
                metadatas=[{"company_code": "1000", "process": "order-to-cash"}])
store.create_hnsw_index(m=16, ef_construction=100, ef_search=100)
hits = store.similarity_search("why is my order blocked", k=3,
                               filter={"company_code": {"$in": ["1000", "*"]}})

HanaDB creates a table with the columns VEC_TEXT, VEC_META (JSON metadata) and VEC_VECTOR, using cosine similarity by default. Keys listed in specific_metadata_columns get their own columns, which its documentation says filter faster than reading them from the JSON column. Put access labels such as company code there. With HanaInternalEmbeddings and NLP enabled, the package uses VECTOR_EMBEDDING instead of a Python model.

CAP, SAP's application framework

CAP has a vector type for SAP HANA. Its April 2026 release notes add vector support on H2, SQLite and PostgreSQL (beta) for local development, CQL.cosineSimilarity and CQL.l2Distance for similarity search, and, in CAP Java, CQL.vectorEmbedding for VECTOR_EMBEDDING (beta). SAP's older CAP vector sample, cap-ai-vector-engine-sample, was archived in January 2026 and points to the codejam-cap-llm repository. Use CAP when the retrieval is part of a CAP side-by-side extension, as in CAP and side-by-side extensions.

In-database embeddings and re-ranking

VECTOR_EMBEDDING and the CROSS_ENCODE re-ranker run inside SAP HANA Cloud with SAP-provided models, once NLP is enabled on the instance. Check in SAP HANA Cloud Central whether your instance, trial or paid, offers the option, and what it adds to sizing.

-- SKETCH: needs NLP enabled on the instance; model ID as listed in SAP's hana-ml documentation.
SELECT TOP 3 ID, TEXT,
       COSINE_SIMILARITY(EMBEDDING,
                         VECTOR_EMBEDDING('why is my order blocked', 'QUERY', 'SAP_NEB.20240715')) AS SCORE
  FROM HELP_NOTES_NEB
 ORDER BY SCORE DESC

The stored vectors in HELP_NOTES_NEB must come from the same model, created with 'DOCUMENT'. You can't search the lab's MiniLM vectors with an SAP model's question vector.

The managed alternative: SAP AI Core grounding

If you want SAP to run the vector store, SAP AI Core's grounding service keeps collections of chunks and searches them for you, as described in Vector databases explained. You give up index tuning and SQL filters on your own columns; you gain no database work.

Licensing notes

  • The vector engine is a feature of SAP HANA Cloud. What you pay for is the instance: vectors and indexes use its memory and storage. Check your contract for metrics.
  • The BTP trial is free for learning and stops every night. Team sandboxes use the free tier or paid plans in a productive account, as Set up for Unit 7 explains.
  • hdbcli is free to install under SAP's developer licence (see Set up for Unit 7); langchain-hana is open source.

Build vs. SAP

Need Local store or own code SAP HANA Cloud vector engine SAP AI Core grounding
Learning, offline tests Best Trial, stops nightly Needs SAP AI Core access
Filters on SAP business columns Metadata you copy SQL WHERE on real columns Key/value metadata
Data already in SAP HANA Cloud Copy out Already there Upload chunks
Index tuning Full control M, efConstruction, efSearch None
Embeddings Any model in your code Any model in your code, or SAP models in the database Configured per collection
Operations Yours Your HANA Cloud team SAP
Best when Prototyping, small sets Answers depend on SAP context and controls You want no store to run

Production concerns

  • Authorizations. A passage copied into SAP HANA Cloud carries no S/4HANA roles. Store access labels (company code, sales organization, confidentiality) as columns, and build the filter on the server from the user's identity, never from a value the client sends. The next topic in Unit 7, on grounding with SAP authorizations, goes deeper.
  • Database users. The application connects as a technical user that can read only the vector tables it needs, never as DBADMIN. Loading jobs use a separate user with write access. Unit 11 covers least privilege for agents.
  • SQL injection. Pass questions, vectors and filter values as ? parameters. Never paste user input into the SQL text.
  • Evaluation. Keep the bench approach: a fixed set of questions with their exact top k, rerun after every index change, data load or release upgrade. Track filtered and unfiltered recall separately. Unit 8 builds full retrieval evaluation.
  • Sizing and cost. Start from rows × dimensions × 4 bytes (2 for HALF_VECTOR), then add the index, text, label columns and growth. Size before you build; memory is what you pay for.
  • One model per table. Record the model in every row, as the lab does. A model change means re-embedding everything, preferably into a new table that you switch to once it is tested.
  • Index changes are table operations. Build and drop indexes ONLINE on tables in use, so loads and queries continue. Plan rebuilds outside peak hours anyway.
  • Freshness. Decide how soon new or changed documents must be searchable, and delete superseded passages, or filter them by validity, so old rules don't come back.
  • Clean core. Keep vector tables in SAP HANA Cloud on SAP BTP, side by side. Read S/4HANA data through released APIs; never write embeddings into S/4HANA tables.

Pitfalls

  • Ranking the wrong way round. COSINE_SIMILARITY sorts DESC; L2DISTANCE sorts ASC. Swapping them returns the worst matches first.
  • An index for one function, queries with the other. The index is built for one similarity function. Query with the same one.
  • Mixed models in one table. The vectors still have the right length, so nothing fails; the results are just wrong. Record and check the model.
  • Building SQL from strings. It works in a demo and opens an injection hole. Use parameters.
  • Assuming filters and index work together. Measure filtered recall with a rare value, as bench does, on your release.
  • Copying a similarity threshold. A cut-off like 0.8 depends on the model and the data. Measure it on your own questions.
  • Forgetting the daily start. On the trial, most morning errors are a stopped instance.

Exercise

Repeat your index study in SAP HANA Cloud and compare it with the local result. The report feeds Unit 8, where you evaluate retrieval end to end.

  1. Open unit07/index_lab_report.txt from the Vector databases explained exercise and note the M and ef_search you chose. No report? Use M 8 and efSearch 50.

  2. Start your SAP HANA Cloud instance (Step 1).

  3. Run bench with those settings and save the output.

    Windows (PowerShell):

    python unit07/hana_vector_lab.py bench --n 20000 --m 8 --ef-search 50 | Out-File -Encoding utf8 unit07/hana_index_report.txt

    macOS/Linux:

    python unit07/hana_vector_lab.py bench --n 20000 --m 8 --ef-search 50 > unit07/hana_index_report.txt
  4. Run it once more with a larger --ef-search, for example 200, and append the result (PowerShell: Out-File -Append; macOS/Linux: >>).

  5. At the end of unit07/hana_index_report.txt, add two lines in your own words: the settings you would choose, and whether the company code 3000 row behaved like the unfiltered one.

  6. Commit: git add unit07/hana_index_report.txt and git commit -m "Unit 7: HANA Cloud index report".

No instance available? Run step 3 with --sample, save that output instead, and write which settings you would test first and why. Then run the real comparison when you have an instance.

Done when unit07/hana_index_report.txt holds two bench runs from SAP HANA Cloud with four result rows each, exact rows at 1.00 recall, and your two lines naming the settings you chose and how the filtered row compared.

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1In SAP HANA Cloud, what is an embedding, as far as SQL is concerned?

    Answer: B. The vector engine stores embeddings in a REAL_VECTOR (or HALF_VECTOR) column next to the other columns. That is why filters, joins and parameters work as in any other SQL.
  2. 2Your query ranks by L2DISTANCE(...) with ORDER BY SCORE DESC. What happens?

    Answer: C. L2DISTANCE is 0 or more, and smaller means more alike, so it sorts ascending. COSINE_SIMILARITY goes from -1 to 1 and sorts descending.
  3. 3Why does the lab load vectors in the binary format but send questions as TO_REAL_VECTOR(?) text?

    Answer: A. SAP's langchain-hana package sends a length plus 4-byte floats when it loads rows, which avoids converting long strings. For a single question vector the bracketed text form from SAP Learning is clear and cheap enough.
  4. 4What does WITH HINT (NO_VECTOR_INDEX) do in the lab?

    Answer: D. SAP's hana-ml client adds this hint when its vector index option is off. The lab uses it to get the true top 10 from SAP HANA Cloud and compare the index against it.
  5. 5Why does the lab build the index with ONLINE?

    Answer: B. SAP's hana-ml documentation says the online mode takes a shared table lock, while without it an exclusive lock blocks the table during the build. On a table in use, that matters.
  6. 6bench shows recall 0.97 for all rows but 0.60 inside company code 3000. What do you do?

    Answer: C. A rare filter with an approximate index can miss true matches, and this is the case to measure. Raising efSearch or using exact search for small filtered sets are the levers; dropping the filter would break access rules.
  7. 7A user types a company code into the app. How should it reach the SQL?

    Answer: B. Parameters keep input from changing the statement. For access, the server sets the company code from who the user is, never from a value the client chooses.
  8. 8The team wants to use VECTOR_EMBEDDING with SAP_NEB.20240715 for questions against the lab's existing table. What is the problem?

    Answer: D. A question and the stored passages must be embedded by the same model. Moving to an SAP model in the database means re-embedding the passages with it, as 'DOCUMENT', and checking that NLP is enabled.

Sources

  • AI with Context: SAP HANA Cloud Vector Engine (SAP News, April 2024) — vector engine generally available with the April 2024 quarterly release; relational, graph, spatial, JSON and vector data in one database; use cases (semantic search over contracts and service notes, recommendations, RAG); generative AI hub to use SAP HANA Cloud for vector storage
  • SAP HANA Cloud Vector Engine (SAP Learning, Introduction to SAP HANA Cloud) — REAL_VECTOR of IEEE 754 single-precision numbers, 1 to 65,000 dimensions; use cases; CRUD in SQL; combining vectors with spatial, graph and JSON; Python clients, hana-ml and CAP
  • SAP HANA Cloud Vector Engine: vector SQL (SAP Learning) — CREATE TABLE with a REAL_VECTOR column; TO_REAL_VECTOR, TO_NVARCHAR, COSINE_SIMILARITY (-1 to 1), L2DISTANCE (0 or more); SELECT TOP ... ORDER BY; similarity in a WHERE condition; UPDATE and DELETE; restrictions (no ordering or arithmetic on vectors, no row tables, partitioning keys, Parquet or NSE)
  • hana_ml.dataframe (SAP HANA Python machine learning client, help.sap.com) — create_vector_index (HNSW only; COSINE_SIMILARITY or L2DISTANCE; build_config, search_config; online uses a shared lock instead of an exclusive one); drop_vector_index; sort_by_similarity with use_vector_index; add_vector with DOCUMENT or QUERY; embed_query; models SAP_NEB.20240715 and SAP_GXY.20250407. Package source (hana-ml 2.30.26091800) adds WITH HINT (NO_VECTOR_INDEX) when use_vector_index is False and calls VECTOR_EMBEDDING(text, type, model)
  • langchain-hana (PyPI, maintained by SAP) — version 1.2.0; package source builds CREATE HNSW VECTOR INDEX ... SIMILARITY FUNCTION ... BUILD CONFIGURATION ... SEARCH CONFIGURATION ... ONLINE, checks M 4 to 1000, efConstruction and efSearch 1 to 100000; REAL_VECTOR needs version 2024.2 (QRC 1/2024), HALF_VECTOR 2025.15 (QRC 2/2025); binary vector format (4-byte length, then 4-byte or 2-byte numbers); CLOUD_VERSION from SYS.M_DATABASE; CROSS_ENCODE window function for re-ranking
  • SAP HANA Cloud vector engine integration (LangChain documentation) — HanaDB with default columns VEC_TEXT, VEC_META, VEC_VECTOR; COSINE_SIMILARITY default, EUCLIDEAN_DISTANCE option; specific_metadata_columns for faster filters; filter operators; HanaInternalEmbeddings and re-ranking (model SAP_CER.20250701) need NLP enabled on the instance
  • CAP release notes, April 2026 (capire) — vector type on H2, SQLite and PostgreSQL (beta) for local development; CQL.cosineSimilarity and CQL.l2Distance; CQL.vectorEmbedding for VECTOR_EMBEDDING in CAP Java (beta)
  • cap-ai-vector-engine-sample (SAP-samples on GitHub) — CAP sample with SAP AI Core embeddings stored in SAP HANA Cloud; archived 14 January 2026 and points to SAP-samples/codejam-cap-llm

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in