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?
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.
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.
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.
"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.
Pick one answer for each question. The explanation appears after you choose.
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.
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.
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.
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.
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.
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.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
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]
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.
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.
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.
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.
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.
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]
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
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
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.
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.
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()
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".
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"
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.
Try the validity filter. A rule for company code 1000 starts in November 2026:
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.
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.
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.
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.
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.
Run it with the settings you found best in the vector databases lab (or the defaults below):
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)
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.
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.
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.
# 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 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.
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.
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.
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.
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.
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.
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.
Start your SAP HANA Cloud instance (Step 1).
Run bench with those settings and save the output.
Run it once more with a larger --ef-search, for example 200, and append the result (PowerShell: Out-File -Append; macOS/Linux: >>).
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.
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 whenunit07/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.
Pick one answer for each question. The explanation appears after you choose.
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.
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.
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.
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.
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.
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.
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.
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.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
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: 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)