SAP HANA Cloud Vector Engine 是一个完全集成到 SAP HANA Cloud 数据库中的向量存储。
安装
安装@sap/hana-langchain 外部集成包,以及本笔记本中使用的其他包。
npm install @sap/hana-langchain @langchain/core@latest @langchain/classic@latest langchain@latest
凭证
确保您的 SAP HANA 实例正在运行。从环境变量加载凭证,并使用您首选的 HANA 客户端创建连接。import * as dotenv from 'dotenv';
dotenv.config();
import hanaClient from "@sap/hana-client";
const connectionParams = {
host: process.env.HANA_DB_ADDRESS,
port: process.env.HANA_DB_PORT,
user: process.env.HANA_DB_USER,
password: process.env.HANA_DB_PASSWORD,
};
const client = hanaClient.createConnection(connectionParams);
// connect to hanaDB
await new Promise<void>((resolve, reject) => {
client.connect((err: Error) => {
// Use arrow function here
if (err) {
reject(err);
} else {
console.log("Connected to SAP HANA successfully.");
resolve();
}
});
});
初始化
要初始化一个HanaDB 向量存储,您需要一个数据库连接和一个嵌入实例。SAP HANA Cloud Vector Engine 支持外部和内部嵌入。
使用外部嵌入
import { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-large" });
使用内部嵌入
或者,您可以直接在 SAP HANA 中使用其原生VECTOR_EMBEDDING() 函数计算嵌入。如果您在 TypeScript 环境中提供内部嵌入支持,可以像传递 HanaDB 一样初始化并传递它。有关内部嵌入的更多信息,请参阅 SAP HANA VECTOR_EMBEDDING 函数。
注意:确保您的 SAP HANA Cloud 实例中启用了 NLP。
import { HanaInternalEmbeddings } from "@sap/hana-langchain";
const internalEmbeddings = new HanaInternalEmbeddings({
internalEmbeddingModelId:
process.env.HANA_DB_EMBEDDING_MODEL_ID || "SAP_NEB.20240715",
});
// optionally, you can specify a remote source to use models from your deployed SAP AI CORE instance:
/*
const internalEmbeddings = new HanaInternalEmbeddings({
internalEmbeddingModelId:
process.env.HANA_DB_EMBEDDING_REMOTE_MODEL_ID || "YOUR_EMBEDDING_MODEL_ID",
remoteSourceSchema:
process.env.HANA_DB_EMBEDDING_REMOTE_SOURCE_SCHEMA || "YOUR_REMOTE_SOURCE_SCHEMA_NAME",
remoteSource:
process.env.HANA_DB_EMBEDDING_REMOTE_SOURCE || "YOUR_REMOTE_SOURCE_NAME",
});
*/
HanaDB 并提供用于存储向量的表名来创建向量存储:
// define instance args
// check the interface to see all possible options
const args: HanaDBArgs = {
connection: client,
tableName: "MY_TABLE",
};
// Create a LangChain VectorStore interface for the HANA database and specify the table (collection) to use in args.
const db = new HanaDB(embeddings, args);
// need to initialize once an instance is created.
await db.initialize()
管理向量存储
创建向量存储后,我们可以通过添加和删除不同的项目来与之交互。向向量存储添加项目
我们可以通过使用addDocuments 函数向我们的向量存储添加项目。
import { Document } from "@langchain/core/documents";
const docs = [new Document({ pageContent: "Some text" }), new Document({ pageContent: "Other docs" })];
await db.addDocuments(docs);
await db.addDocuments([
{ pageContent: "foo", metadata: { start: 100, end: 150, docName: "foo.txt", quality: "bad" } },
{ pageContent: "bar", metadata: { start: 200, end: 250, docName: "bar.txt", quality: "good" } },
]);
注意useMapMerge仅允许用于HanaInternalEmbeddings实例。
// A useMapMerge flag can be supplied in the options for faster insertion
// map merge only works with internal Embeddings
await db.addDocuments(docs, { useMapMerge: true });
从向量存储删除项目
await db.delete({ filter: { quality: "bad" } });
查询向量存储
直接查询
相似度搜索
执行简单的相似度搜索并在元数据上过滤,可以按如下方式完成:// With filtering on {"quality": "bad"}, only one document should be returned
const docs = await db.similaritySearch("foobar", 2, { quality: "bad" });
console.log(docs);
[
{
pageContent: "foo",
metadata: { start: 100, end: 150, docName: "foo.txt", quality: "bad" }
}
]
MMR 搜索
执行最大边际相关性(MMR)并在元数据上过滤的搜索,可以按如下方式完成:const docsMMR = await db.maxMarginalRelevanceSearch("foobar", {
k: 2,
fetchK: 5,
filter: { quality: "bad" },
});
console.log(docsMMR);
[
{
pageContent: "foo",
metadata: { start: 100, end: 150, docName: "foo.txt", quality: "bad" }
}
]
将向量存储转换为检索器
您还可以将向量存储转换为检索器,以便在您的链中更轻松地使用。const retriever = db.asRetriever();
const docsRetriever = await retriever.invoke("foobar", {filter: { quality: "good" }});
console.log(docsRetriever);
[
{
pageContent: "bar",
metadata: { start: 200, end: 250, docName: "bar.txt", quality: "good" }
}
]
距离相似度算法
HanaDB 支持以下距离相似度算法:
- 余弦相似度(默认)
- 欧几里得距离(L2)
distanceStrategy 参数在初始化 HanaDB 实例时指定距离策略。
const argsDist: HanaDBArgs = {
connection: client,
tableName: "MY_TABLE",
distanceStrategy: "EUCLIDEAN",
// distanceStrategy: "COSINE", // (default)
};
const dbDist = new HanaDB(embeddings, argsDist);
await dbDist.initialize();
创建 HNSW 索引
向量索引可以显著加快向量的 top-k 最近邻查询。用户可以使用createHnswIndex 函数创建分层可导航小世界(HNSW)向量索引。
有关在数据库级别创建索引的更多信息,请参阅 官方文档。
const argsHnsw: HanaDBArgs = {
connection: client,
tableName: "MY_TABLE",
};
const dbHnsw = new HanaDB(embeddings, argsHnsw);
await dbHnsw.initialize();
dbHnsw.createHnswIndex({
indexName: "MY_HNSW_INDEX",
efSearch: 100, // Max number of neighbors per graph node (valid range: 4 to 1000)
m: 200, // Max number of candidates during graph construction (valid range: 1 to 100000)
efConstruction: 500, // Min number of candidates during the search (valid range: 1 to 100000)
});
高级过滤
除了基本的基于值的过滤功能外,还可以使用更高级的过滤。下表显示了可用的过滤运算符。| 运算符 | 语义 | 支持的操作数数据类型 |
|---|---|---|
$eq | 相等 (==) | number, string, boolean, DateValue, null |
$ne | 不相等 (!=) | number, string, boolean, DateValue, null |
$lt | 小于 (<) | number, string, DateValue |
$lte | 小于或等于 (<=) | number, string, DateValue |
$gt | 大于 (>) | number, string, DateValue |
$gte | 大于或等于 (>=) | number, string, DateValue |
$in | 包含在给定值的集合中 (in) | 类型为 (number, string, boolean, DateValue) 的相同值数组 |
$nin | 不包含在给定值的集合中 (not in) | 类型为 (number, string, boolean, DateValue) 的相同值数组 |
$between | 在两个边界值之间的范围内 | 类型为 (number, string, DateValue) 的 2 值数组 |
$like | 基于 SQL 中 “LIKE” 语义的文本相等(使用 ”%” 作为通配符) | string |
$contains | 过滤包含特定关键字的文档 | 非空字符串 |
$and | 逻辑 “与”,支持两个或多个操作数 | list(Filter) |
$or | 逻辑 “或”,支持两个或多个操作数 | list(Filter) |
const docs: Document[] = [
{
pageContent: "First",
metadata: { name: "Adam Smith", is_active: true, id: 1, height: 10.0 },
},
{
pageContent: "Second",
metadata: { name: "Bob Johnson", is_active: false, id: 2, height: 5.7 },
},
{
pageContent: "Third",
metadata: { name: "Jane Doe", is_active: true, id: 3, height: 2.4 },
},
];
const args: HanaDBArgs = {
connection: client,
tableName: "LANGCHAIN_DEMO_ADVANCED_FILTER",
};
const vectorStore = new HanaDB(embeddings, args);
// need to initialize once an instance is created.
await vectorStore.initialize();
// Delete already existing documents from the table
await vectorStore.delete({ filter: {} });
await vectorStore.addDocuments(docs);
// Helper function to print filter results
function printFilterResult(result: Document[]) {
if (result.length === 0) {
console.log("<empty result>");
} else {
result.forEach((doc) => console.log(JSON.stringify(doc.metadata)) );
}
}
$ne, $gt, $gte, $lt, $lte 进行过滤
let advancedFilter;
advancedFilter = { id: { $ne: 1 } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { id: { $gt: 1 } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { id: { $gte: 1 } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { id: { $lt: 1 } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { id: { $lte: 1 } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
Filter: {"id":{"$ne":1}}
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
{ name: 'Jane Doe', is_active: true, id: 3, height: 2.4 }
Filter: {"id":{"$gt":1}}
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
{ name: 'Jane Doe', is_active: true, id: 3, height: 2.4 }
Filter: {"id":{"$gte":1}}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
{ name: 'Jane Doe', is_active: true, id: 3, height: 2.4 }
Filter: {"id":{"$lt":1}}
<empty result>
Filter: {"id":{"$lte":1}}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
$between, $in, $nin 进行过滤
advancedFilter = { id: { $between: [1, 2] } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { name: { $in: ["Adam Smith", "Bob Johnson"] } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { name: { $nin: ["Adam Smith", "Bob Johnson"] } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
Filter: {"id":{"$between":[1,2]}}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
Filter: {"name":{"$in":["Adam Smith","Bob Johnson"]}}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
Filter: {"name":{"$nin":["Adam Smith","Bob Johnson"]}}
{ name: 'Jane Doe', is_active: true, id: 3, height: 2.4 }
$like 进行文本过滤
advancedFilter = { name: { $like: "a%" } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { name: { $like: "%a%" } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
Filter: {"name":{"$like":"a%"}}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
Filter: {"name":{"$like":"%a%"}}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
{ name: 'Jane Doe', is_active: true, id: 3, height: 2.4 }
$contains 进行文本过滤
advancedFilter = { name: { $contains: "bob" } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { name: { $contains: "bo" } };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = {"name": {"$contains": "Adam Johnson"}}
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = {"name": {"$contains": "Adam Smith"}}
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
Filter: {"name":{"$contains":"bob"}}
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
Filter: {"name":{"$contains":"bo"}}
<empty result>
Filter: {'name': {'$contains': 'Adam Johnson'}}
<empty result>
Filter: {'name': {'$contains': 'Adam Smith'}}
{'name': 'Adam Smith', 'is_active': True, 'id': 1, 'height': 10.0}
$and, $or 进行组合过滤
advancedFilter = { $or: [{ id: 1 }, { name: "bob" }] };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { $and: [{ id: 1 }, { id: 2 }] };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { $and: [{ name: { $contains: "bob" } }, { id: 2 }] };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = { $or: [{ id: 1 }, { id: 2 }, { id: 3 }] };
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
advancedFilter = {
$and: [{ $or: [{ id: 1 }, { id: 2 }] }, { height: { $gte: 5.0 } }],
};
console.log(`Filter: ${JSON.stringify(advancedFilter)}`);
printFilterResult(
await vectorStore.similaritySearch("just testing", 5, advancedFilter)
);
Filter: {'$or': [{'id': 1}, {'name': 'bob'}]}
{'name': 'Adam Smith', 'is_active': True, 'id': 1, 'height': 10.0}
Filter: {"$and":[{"id":1},{"id":2}]}
<empty result>
Filter: {"$and":[{"name":{"$contains":"bob"}},{"id":2}]}
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
Filter: {"$or":[{"id":1},{"id":2},{"id":3}]}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
{ name: 'Jane Doe', is_active: true, id: 3, height: 2.4 }
Filter: {"$and":[{"$or":[{"id":1},{"id":2}]},{"height":{"$gte":5.0}}]}
{ name: 'Adam Smith', is_active: true, id: 1, height: 10 }
{ name: 'Bob Johnson', is_active: false, id: 2, height: 5.7 }
重新排序结果
相似度搜索的结果可以通过将rerank_config 字典传递给 similaritySearch 方法来进行重新排序。重新排序使用 SAP HANA 的 CROSS_ENCODE 函数和交叉编码模型执行。
rerank_config 字典支持以下字段:
-
query(string, 必填) — 用于重新排序的查询文本。如果未提供,将使用原始相似度搜索查询。 -
modelId(string, 必填) — 用于重新排序的交叉编码模型 ID。 -
topN(number, 可选) — 重新排序后返回的顶部结果数量。默认为 3。 -
rankFields(string[], 可选) — 要包含在重新排序中的元数据字段列表。指定时,这些字段将与内容连接起来进行评分。
注意: 确保您的 SAP HANA Cloud 实例中启用了 NLP,以使用重新排序功能。更多详情,请参阅 SAP HANA 数据库附加功能。
// Prepare sample documents with metadata
const docs = [
new Document({
pageContent: "Python is a programming language",
metadata: { category: "programming", difficulty: "beginner" },
}),
new Document({
pageContent: "Machine learning uses algorithms to learn patterns",
metadata: { category: "AI", difficulty: "intermediate" },
}),
new Document({
pageContent: "Neural networks are inspired by the human brain",
metadata: { category: "AI", difficulty: "advanced" },
}),
];
// Initialize embeddings
const embeddings = new OpenAIEmbeddings();
const args: HanaDBArgs = {
connection: client,
tableName: "testReranking",
};
// Create a LangChain VectorStore interface for the HANA database and specify the table (collection) to use in args.
const vectorStore = new HanaDB(embeddings, args);
// need to initialize once an instance is created.
await vectorStore.initialize();
// Delete already existing documents from the table
await vectorStore.delete({ filter: {} });
await vectorStore.addDocuments(docs);
基本重新排序
使用rerankConfig 参数执行带重新排序的相似度搜索:
const rerankConfig: RerankConfigOptions = {
modelId: process.env.HANA_DB_RERANKING_MODEL_ID || "SAP_CER.20250701",
topN: 2,
}
const docsReranked = await vectorStore.similaritySearch("AI Technology", 3, undefined, undefined, rerankConfig);
console.log("Reranked Results:");
docsReranked.forEach((doc) => {
console.log("-".repeat(80));
console.log(`Content: ${doc.pageContent}`);
console.log("Metadata:", doc.metadata);
});
Reranked Results:
--------------------------------------------------------------------------------
Content: Machine learning uses algorithms to learn patterns
Metadata: { category: 'AI', difficulty: 'intermediate' }
--------------------------------------------------------------------------------
Content: Python is a programming language
Metadata: { category: 'programming', difficulty: 'beginner' }
带元数据字段的重新排序
使用rankFields 在重新排序过程中包含元数据。当元数据包含应影响排名的相关信息时,这很有用:
const rerankConfigWithFields: RerankConfigOptions = {
query: "beginner AI Concepts",
modelId: process.env.HANA_DB_RERANKING_MODEL_ID || "SAP_CER.20250701",
topN: 2,
rankFields: ["category", "difficulty"],
};
const docsRerankedwithFields = await vectorStore.similaritySearch("learning algorithm", 3, undefined, undefined, rerankConfigWithFields);
console.log("Reranked results with metadata fields:");
docsRerankedwithFields.forEach((doc) => {
console.log("-".repeat(80));
console.log(`Content: ${doc.pageContent}`);
console.log("Metadata:", doc.metadata);
});
Reranked results with metadata fields:
--------------------------------------------------------------------------------
Content: Neural networks are inspired by the human brain
Metadata: { category: 'AI', difficulty: 'advanced' }
--------------------------------------------------------------------------------
Content: Machine learning uses algorithms to learn patterns
Metadata: { category: 'AI', difficulty: 'intermediate' }
带分数的重新排序
使用similaritySearchWithScore 获取与文档一起的重新排序分数:
const rerankConfigWithScores: RerankConfigOptions = {
modelId: process.env.HANA_DB_RERANKING_MODEL_ID || "SAP_CER.20250701",
topN: 3,
};
const docsRerankedwithScores = await vectorStore.similaritySearchWithScore("neural network architecture", 3, undefined, undefined, rerankConfigWithScores);
console.log("Reranked results with scores:");
docsRerankedwithScores.forEach(([doc, score]) => {
console.log("-".repeat(80));
console.log("Score:", score.toFixed(4));
console.log(`Content: ${doc.pageContent}`);
});
Reranked results with scores:
--------------------------------------------------------------------------------
Score: 0.0435
Content: Neural networks are inspired by the human brain
--------------------------------------------------------------------------------
Score: 0.0146
Content: Python is a programming language
--------------------------------------------------------------------------------
Score: 0.0145
Content: Machine learning uses algorithms to learn patterns
独立的 HanaReranker 重新排序
为了对重新排序过程有更多控制,您可以直接使用HanaReranker 类作为文档压缩器。当您需要从任何来源重新排序文档,而不仅仅是相似性搜索结果时,这很有用:
import { HanaReranker, RerankConfigOptions } from "@sap/hana-langchain";
// Documents to rerank (can come from any source)
const docsToCompress = [
new Document({
pageContent: "Python programming basics",
}),
new Document({
pageContent: "Advanced machine learning techniques",
}),
new Document({
pageContent: "Introduction to neural networks",
}),
new Document({
pageContent: "Deep learning applications",
}),
new Document({
pageContent: "Reinforcement learning strategies",
}),
new Document({
pageContent: "Natural language processing techniques",
}),
];
const reranker = new HanaReranker(client, process.env.HANA_DB_RERANKING_MODEL_ID || "SAP_CER.20250701");
await reranker.initialize();
// Rerank documents based on a query using compress_documents (returns top Math.min(5, documents.length))
// Reranking scores will be added to the metadata of each document under the key "relevance_score"
const compressedDocs = await reranker.compressDocuments(docsToCompress, "AI and deep learning");
console.log("Reranked documents:");
compressedDocs.forEach((doc) => {
console.log("-".repeat(80));
console.log(`Content: ${doc.pageContent}`);
console.log(`Relevance score: ${doc.metadata?.relevance_score?.toFixed(4)}`);
});
// Or use the rerank method for more control over topN
const rerankedDocs = await reranker.rerank(docsToCompress, "machine learning", 2);
console.log("Top 2 reranked results:");
rerankedDocs.forEach(([idx, score, doc]) => {
console.log(` [${idx}] Score: ${score.toFixed(4)} - ${doc?.pageContent}`);
});
Compressed documents:
--------------------------------------------------------------------------------
Content: Deep learning applications
Relevance score: 0.2188
--------------------------------------------------------------------------------
Content: Advanced machine learning techniques
Relevance score: 0.0748
--------------------------------------------------------------------------------
Content: Introduction to neural networks
Relevance score: 0.0079
--------------------------------------------------------------------------------
Content: Natural language processing techniques
Relevance score: 0.0040
--------------------------------------------------------------------------------
Content: Reinforcement learning strategies
Relevance score: 0.0036
Top 2 reranked results:
[1] Score: 0.4351 - Advanced machine learning techniques
[3] Score: 0.0342 - Deep learning applications
用于检索增强生成
有关如何使用此向量存储进行检索增强生成(RAG)的指南,请参阅以下章节:标准表与包含向量数据的自定义表
默认情况下,嵌入表包含三列:VEC_TEXT:文档文本VEC_META:文档元数据VEC_VECTOR:嵌入向量
- 用于文本/上下文的
NCLOB/NVARCHAR列 - 用于元数据的
NCLOB/NVARCHAR列 - 用于嵌入向量的
REAL_VECTOR列
// Access the vector DB with a new table
const dbDefaultArgs: HanaDBArgs = {
connection: client,
tableName: "LANGCHAIN_DEMO_NEW_TABLE"
};
const dbDefault = new HanaDB(embeddings, dbDefaultArgs);
await dbDefault.initialize();
// Delete already existing entries from the table
await dbDefault.delete({ filter: {} });
// Add a simple document with some metadata
docs = [
new Document({
pageContent: "A simple document",
metadata: { start: 100, end: 150, docName: "simple.txt" }
})
]
await dbDefault.addDocuments(docs);
const result = await new Promise<any[]>((resolve, reject) => {
client.exec(
`SELECT COLUMN_NAME, DATA_TYPE_NAME FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME = CURRENT_SCHEMA AND TABLE_NAME = 'LANGCHAIN_DEMO_NEW_TABLE'`,
(err: Error, rows: any) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
row.forEach((r) => {
console.log(`${r.COLUMN_NAME}: ${r.DATA_TYPE_NAME}`);
});
VEC_TEXT: NCLOB
VEC_META: NCLOB
VEC_VECTOR: REAL_VECTOR
// Helper function to parse fvecs format for REAL_VECTOR
function parseFvecs(b: ArrayBuffer): number[] {
const v = new DataView(b)
const d = v.getUint32(0, true)
return Array.from({ length: d }, (_, i) => v.getFloat32(4 + i * 4, true))
}
const resultVal = await new Promise<any[]>((resolve, reject) => {
client.exec(
`SELECT * FROM LANGCHAIN_DEMO_NEW_TABLE LIMIT 1`,
(err: Error, rows: any) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
rowVal.forEach((r) => {
console.log(`VEC_TEXT: ${r.VEC_TEXT}`); // The text
console.log(`VEC_META: ${r.VEC_META}`); // The metadata
const embedding = parseFvecs(r.VEC_VECTOR);
console.log(`VEC_VECTOR: ${embedding.length, embedding.slice(0, 3).concat(['...']).concat(embedding.slice(-3))}`); // The vector
});
VEC_TEXT: A simple document
VEC_META: {"start": 100, "end": 150, "docName": "simple.txt"}
VEC_VECTOR: 768 [-0.01989901065826416, 0.02785174734890461, 0.0020877711940556765, '...', 0.0183248370885849, 0.009469633921980858, 0.04312701150774956]
- 用于嵌入的文本/上下文的
NCLOB或NVARCHAR类型列 - 用于元数据的
NCLOB或NVARCHAR类型列 - 用于嵌入向量的
REAL_VECTOR或HALF_VECTOR类型列
// Create a new table "MY_OWN_TABLE_ADD" with three "standard" columns and one additional column
const myOwnTableName = "MY_OWN_TABLE_ADD";
await new Promise<void>((resolve, reject) => {
client.exec(
`CREATE TABLE MY_OWN_TABLE_ADD (
SOME_OTHER_COLUMN NVARCHAR(42),
MY_TEXT NVARCHAR(2048),
MY_METADATA NVARCHAR(1024),
MY_VECTOR REAL_VECTOR,
)`,
(err: Error) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
// Create a HanaDB instance with the own table
const dbOwnTableArgs: HanaDBArgs = {
connection: client,
tableName: myOwnTableName,
textColumnName: "MY_TEXT",
metadataColumnName: "MY_METADATA",
vectorColumnName: "MY_VECTOR",
};
const dbOwnTable = new HanaDB(embeddings, dbOwnTableArgs);
await dbOwnTable.initialize();
// Add a simple document with some metadata
docs = [
new Document({
pageContent: "Some other text",
metadata: {start: 400, end: 450, docName: "other.txt"}
})
]
await dbOwnTable.addDocuments(docs);
// Check if data has been inserted into our own table
const resultOwnTable = await new Promise<any[]>((resolve, reject) => {
client.exec(
`SELECT * FROM ${myOwnTableName} LIMIT 1`,
(err: Error, rows: any) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
rowOwnTable.forEach((r) => {
console.log(`SOME_OTHER_COLUMN: ${r.SOME_OTHER_COLUMN}`); // should be NULL
console.log(`MY_TEXT: ${r.MY_TEXT}`); // The text
console.log(`MY_METADATA: ${r.MY_METADATA}`); // The metadata
const embedding = parseFvecs(r.MY_VECTOR);
console.log(`MY_VECTOR: ${embedding.length, embedding.slice(0, 3).concat(['...']).concat(embedding.slice(-3))}`); // The vector
});
SOME_OTHER_COLUMN: null
MY_TEXT: Some other text
MY_METADATA: {"start":400,"end":450,"docName":"other.txt"}
MY_VECTOR: 768 [0.016170687973499298, -0.01129427831619978, -0.0005921399570070207, '...', 0.017849743366241455, 0.0003932560794055462, -0.00045805066474713385]
const moreDocs = [
new Document({
pageContent: "ome more text",
metadata: {start: 800, end: 950, docName: "more.txt"}
})
]
awwait dbOwnTable.addDocuments(moreDocs);
const foundDocs = await dbOwnTable.similaritySearch("What's up?", 2);
foundDocs.forEach((doc) => {
console.log("-".repeat(80));
console.log(doc.pageContent);
});
--------------------------------------------------------------------------------
Some more text
--------------------------------------------------------------------------------
Some other text
使用自定义列进行过滤性能优化
为了允许灵活的元数据值,所有元数据默认都存储为 JSON 格式在元数据列中。如果已知一些使用的元数据键和值类型,可以通过创建目标表时将键名作为列名,并通过specificMetadataColumns 列表将它们传递给 HanaDB 构造函数来将它们存储在额外的列中。在插入期间,与这些值匹配的元数据键会被复制到特殊列中。过滤器会使用特殊列而不是元数据 JSON 列来处理 specificMetadataColumns 列表中的键。
// Create a new table "PERFORMANT_CUSTOMTEXT_FILTER" with three "standard" columns and one additional column
const performantTableName = "PERFORMANT_CUSTOMTEXT_FILTER";
await new Promise<void>((resolve, reject) => {
client.exec(
`CREATE TABLE ${performantTableName} (
CUSTOMTEXT NVARCHAR(500),
MY_TEXT NVARCHAR(2048),
MY_METADATA NVARCHAR(1024),
MY_VECTOR REAL_VECTOR,
)`,
(err: Error) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
// Create a HanaDB instance with the table
const dbPerformantArgs: HanaDBArgs = {
connection: client,
tableName: performantTableName,
textColumnName: "MY_TEXT",
metadataColumnName: "MY_METADATA",
vectorColumnName: "MY_VECTOR",
specificMetadataColumns: ["CUSTOMTEXT"],
};
const dbPerformant = new HanaDB(embeddings, dbPerformantArgs);
await dbPerformant.initialize();
// Add a simple document with some metadata
const performantDocs = [
new Document({
pageContent: "Some other text",
metadata: {
start: 400,
end: 450,
docName: "other.txt",
CUSTOMTEXT: "Filters on this value are very performant"
}
})
]
await dbPerformant.addDocuments(performantDocs);
// Check if data has been inserted into our own table
const resultPerformant = await new Promise<any[]>((resolve, reject) => {
client.exec(
`SELECT * FROM ${performantTableName} LIMIT 1`,
(err: Error, rows: any) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
rowPerformant.forEach((r) => {
console.log(`CUSTOMTEXT: ${r.CUSTOMTEXT}`); // The custom text metadata
console.log(`MY_TEXT: ${r.MY_TEXT}`); // The text
console.log(`MY_METADATA: ${r.MY_METADATA}`); // The metadata
const embedding = parseFvecs(r.MY_VECTOR);
console.log(`MY_VECTOR: ${embedding.length, embedding.slice(0, 3).concat(['...']).concat(embedding.slice(-3))}`); // The vector
});
CUSTOMTEXT: Filters on this value are very performant
MY_TEXT: Some other text
MY_METADATA: {"start":400,"end":450,"docName":"other.txt","CUSTOMTEXT":"Filters on this value are very performant"}
768 [0.016170687973499298, -0.01129427831619978, -0.0005921399570070207, '...', 0.017849743366241455, 0.0003932560794055462, -0.00045805066474713385]
const advancedFilter = { CUSTOMTEXT: { $like: "%value%" } };
const foundPerformantDocs = await dbPerformant.similaritySearch("What's up?", 2, advancedFilter);
foundPerformantDocs.forEach((doc) => {
console.log("-".repeat(80));
console.log(doc.pageContent);
});
--------------------------------------------------------------------------------
Some more text
--------------------------------------------------------------------------------
Some other text
简单示例
加载示例文档 “state_of_the_union.txt” 并从中创建片段。 首先,安装@langchain/textsplitters 包:
npm install @langchain/textsplitters
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
import { CharacterTextSplitter } from "@langchain/textsplitters";
// Load documents from file
const loader = new TextLoader("./state_of_the_union.txt");
const textDocuments = await loader.load();
const textSplitter = new CharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 0,
});
const textChunks = await textSplitter.splitDocuments(textDocuments);
console.log("Number of document chunks:", textChunks.length);
Number of document chunks: 88
// Delete already existing documents from the table
await db.delete({ filter: {} });
// add the loaded document chunks
await db.addDocuments(textChunks);
const query = "What did the president say about Ketanji Brown Jackson";
const docs = await db.similaritySearch(query, 2);
docs.forEach((doc) => {
console.log("-".repeat(80));
console.log(doc.pageContent);
});
--------------------------------------------------------------------------------
One of the most serious constitutional responsibilities a President has is nominating
someone to serve on the United States Supreme Court.
And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson.
One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.
--------------------------------------------------------------------------------
As I said last year, especially to our younger transgender Americans, I will always have your back as your President,
so you can be yourself and reach your God-given potential.
While it often appears that we never agree, that isn’t true. I signed 80 bipartisan bills into law last year.
From preventing government shutdowns to protecting Asian-Americans from still-too-common hate crimes to reforming military justice
const argsL2d: HanaDBArgs = {
connection: client,
tableName: "STATE_OF_THE_UNION",
distanceStrategy: "EUCLIDEAN",
};
const dbL2d = new HanaDB(embeddings, argsL2d);
await dbL2d.initialize();
const docsL2d = await dbL2d.similaritySearch(query, 2);
docsL2d.forEach((doc) => {
console.log("-".repeat(80));
console.log(doc.pageContent);
});
--------------------------------------------------------------------------------
One of the most serious constitutional responsibilities a President has is nominating
someone to serve on the United States Supreme Court.
And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson.
One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.
--------------------------------------------------------------------------------
As I said last year, especially to our younger transgender Americans, I will always have your back as your President,
so you can be yourself and reach your God-given potential.
While it often appears that we never agree, that isn’t true. I signed 80 bipartisan bills into law last year.
From preventing government shutdowns to protecting Asian-Americans from still-too-common hate crimes to reforming military justice
最大边际相关性搜索 (MMR)
Maximal marginal relevance 优化查询的相似性以及选定文档之间的多样性。前 20 个(fetch_k)项目将从数据库中检索。然后 MMR 算法将找到最佳的 2 个(k)匹配项。
const docsMMR = await db.maxMarginalRelevanceSearch(query, {
k: 2,
fetchK: 20,
});
docsMMR.forEach((doc) => {
console.log("-".repeat(80));
console.log(doc.pageContent);
});
--------------------------------------------------------------------------------
One of the most serious constitutional responsibilities a President has is nominating someone
to serve on the United States Supreme Court.
And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson.
One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.
--------------------------------------------------------------------------------
Groups of citizens blocking tanks with their bodies. Everyone from students to retirees teachers turned
soldiers defending their homeland.
In this struggle as President Zelenskyy said in his speech to the European Parliament “Light will win over darkness.”
The Ukrainian Ambassador to the United States is here tonight.
Let each of us here tonight in this Chamber send an unmistakable signal to Ukraine and to the world.
创建 HNSW 向量索引
向量索引可以显著加快向量的 top-k 最近邻查询。用户可以使用createHnswIndex 函数创建分层可导航小世界(HNSW)向量索引。
有关在数据库级别创建索引的更多信息,请参阅 官方文档。
// HanaDB instance uses cosine similarity as default
const argsCosine: HanaDBArgs = {
connection: client,
tableName: "STATE_OF_THE_UNION",
};
// Initialize both HanaDB instances
const dbCosine = new HanaDB(embeddings, argsCosine);
await dbCosine.initialize();
// Attempting to create the HNSW index with default parameters
await dbCosine.createHnswIndex(); // If no other parameters are specified, the default values will be used
// Default values: m=64, efConstruction=128, efSearch=200
// The default index name will be: STATE_OF_THE_UNION_COSINE_idx
// Second instance using the existing table "STATE_OF_THE_UNION" but with L2 Euclidean distance
const argsL2: HanaDBArgs = {
connection: client,
tableName: "STATE_OF_THE_UNION",
distanceStrategy: "EUCLIDEAN", // Use Euclidean distance for this instance
};
const dbL2 = new HanaDB(embeddings, argsL2);
await dbL2.initialize();
// This will create an index based on L2 distance strategy.
await dbL2.createHnswIndex({
indexName: "STATE_OF_THE_UNION_L2_index",
efSearch: 400, // Max number of neighbors per graph node (valid range: 4 to 1000)
m: 50, // Max number of candidates during graph construction (valid range: 1 to 100000)
efConstruction: 150, // Min number of candidates during the search (valid range: 1 to 100000)
});
// Use L2 index to perform MMR
const docsL2HNSW = await dbL2.maxMarginalRelevanceSearch(query, {
k: 2,
fetchK: 20,
});
docsL2HNSW.forEach((doc) => {
console.log("-".repeat(80));
console.log(doc.pageContent);
});
--------------------------------------------------------------------------------
One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court.
And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.
--------------------------------------------------------------------------------
Groups of citizens blocking tanks with their bodies. Everyone from students to retirees teachers turned soldiers defending their homeland.
In this struggle as President Zelenskyy said in his speech to the European Parliament “Light will win over darkness.” The Ukrainian Ambassador to the United States is here tonight.
Let each of us here tonight in this Chamber send an unmistakable signal to Ukraine and to the world.
- 相似性函数:索引的相似性函数默认为 余弦相似性。如果您想使用不同的相似性函数(例如,
L2距离),您需要在初始化HanaDB实例时指定它。 - 默认参数:在
createHnswIndex函数中,如果用户没有为m、efConstruction或efSearch等参数提供自定义值,将自动使用默认值(例如,m=64、efConstruction=128、efSearch=200)。这些值确保索引以合理的性能创建,而无需用户干预。
官方原文:src/oss/javascript/integrations/vectorstores/sap_hanavector.mdx · 非官方翻译,译文由 AI 生成,以英文原文为准。