以编程方式使用文档

>SAP HANA Cloud 向量引擎 是一个完全集成到 SAP HANA Cloud database.

设置

安装 @sap/hana-langchain 外部集成包,以及整个笔记本中使用的其他包。

npm install @sap/hana-langchain @langchain/core@latest @langchain/classic@latest langchain@latest

凭证

确保您的 SAP HANA 实例正在运行。从环境变量加载凭证,并使用您首选的 HANA 客户端创建连接。

dotenv.config();


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();
    }
  });
});

了解更多关于 中的 SAP HANA 的信息.

初始化

要初始化一个 HanaDB 向量存储,您需要一个数据库连接和一个嵌入实例。SAP HANA Cloud 向量引擎支持外部嵌入和内部嵌入。

使用外部嵌入

const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-large" });

使用内部嵌入

或者,您可以使用 SAP HANA 的原生 VECTOR_EMBEDDING() 功能直接在 SAP HANA 中计算嵌入。如果您有可用的内部嵌入支持,请在 TypeScript 环境中初始化并将其传递给 HanaDB 。有关内部嵌入的更多信息,请参阅 SAP HANA VECTOR_EMBEDDING 函数.

> 注意:请确保您的 SAP HANA Cloud 实例已启用 NLP。

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",
  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 function.

await db.addDocuments([
  new Document({ pageContent: "Hello world" }),
  new Document({ pageContent: "Other 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" } },
]);

从向量存储中删除项目

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)

您可以在初始化 HanaDB 实例时使用 distanceStrategy parameter.

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 function.

创建分层可导航小世界(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)
});

如果未指定其他参数,将使用默认值

默认值:m=64,ef_construction=128,ef_search=200

The default index name will be: "\_idx"

高级过滤

除了基本的基于值的过滤功能外,还可以使用更高级的过滤功能。下表显示了可用的过滤运算符。

运算符语义
$eq相等 (==)
$ne不相等 (!=)
$lt小于 (&lt;)
$lte小于等于 (&lt;=)
$gt大于 (&gt;)
$gte大于等于 (&gt;=)
$in包含在给定值集中 (in)
$nin不包含在给定值集中 (not in)
$between在两个边界值之间
$like基于 SQL 中 "LIKE" 语义的文本相等性(使用 "%" 作为通配符)
$contains过滤包含特定关键字的文档
$and逻辑"与",支持两个或多个操作数
$or逻辑"或",支持两个或多个操作数
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 }

用于检索增强生成

有关如何使用此向量存储进行检索增强生成 (RAG) 的指南,请参阅以下部分:

标准表与包含向量数据的自定义表

默认情况下,嵌入表包含三列:

  • - VEC_TEXT:文档文本
  • - VEC_META:文档元数据
  • - VEC_VECTOR:嵌入向量

自定义表必须至少有三列与标准表的语义相匹配:

  • - An NCLOB/NVARCHAR column for the text/context
  • - An NCLOB/NVARCHAR 用于元数据的列
  • - A REAL_VECTOR 用于嵌入向量的列

允许额外的列;确保它们接受新文档插入时的 NULL 值。

//  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);

显示表"LANGCHAIN"中的列_DEMO_NEW_TABLE"

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

显示插入文档在三列中的值

由于 HANA 的 dbapi 驱动程序默认以 Buffer 对象输出向量列,因此我们将创建一个辅助函数将函数转换为数字列表。

// 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 or NVARCHAR for the text/context of the embeddings
  • - 具有类型的列 NCLOB or NVARCHAR 用于元数据
  • - 具有类型的列 REAL_VECTOR or HALF_VECTOR 用于嵌入向量

该表可以包含额外的列。当新的文档被插入到表中时,这些额外的列必须允许NULL值。

// 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 list.

// 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]

这些特殊列对langchain接口的其他部分是完全透明的。一切都像以前一样工作,只是性能更高。

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 package:

npm install @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 function.

创建分层可导航小世界(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 instance.
  • 距离),你需要在初始化默认参数 createHnswIndex 时指定它。在 m, efConstruction, or efSearch函数中,如果用户没有为 m=64, efConstruction=128, efSearch=200等参数提供自定义值,将自动使用默认值(例如,