SAP HANA Cloud Vector Engine

SAP HANA Cloud Vector Engine

SAP HANA Cloud Vector EngineSAP HANA Cloud 데이터베이스에 완전히 통합된 벡터 스토어예요.

설정 (Setup)

@sap/hana-langchain 외부 통합 패키지와 이 노트북 전체에서 사용하는 다른 패키지를 설치하세요.

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

자격 증명 (Credentials)

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

SAP HANA에 대해 자세히 알아보려면 SAP HANA란 무엇인가?를 참고하세요.

초기화 (Initialization)

HanaDB 벡터 스토어를 초기화하려면 데이터베이스 연결과 임베딩 인스턴스가 필요해요. SAP HANA Cloud Vector Engine은 외부·내부 임베딩을 모두 지원해요.

외부 임베딩 사용 (Using external embeddings)

import { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-large" });

내부 임베딩 사용 (Using internal embeddings)

또는 SAP HANA의 네이티브 VECTOR_EMBEDDING() 함수를 사용해 임베딩을 SAP HANA에서 직접 계산할 수 있어요. TypeScript 환경에서 내부 임베딩 지원을 사용할 수 있다면 초기화해 HanaDB에 비슷하게 전달하세요. 내부 임베딩에 대한 자세한 내용은 SAP HANA VECTOR_EMBEDDING 함수를 참고하세요.

Caution: 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()

벡터 스토어 관리 (Manage vector store)

벡터 스토어를 만든 뒤 다양한 항목을 추가하고 삭제하며 상호작용할 수 있어요.

벡터 스토어에 항목 추가 (Add items to vector store)

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" } },
]);

map merge 삽입으로 문서 추가

Note useMapMergeHanaInternalEmbeddings 인스턴스에서만 허용돼요.

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

벡터 스토어에서 항목 삭제 (Delete items from vector store)

await db.delete({ filter: { quality: "bad" } });

벡터 스토어 쿼리 (Query vector store)

직접 쿼리 (Query directly)

유사도 검색 (Similarity search)

메타데이터 필터링이 있는 간단한 유사도 검색은 다음과 같이 수행할 수 있어요:

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

리트리버로 변환해 쿼리 (Query by turning into retriever)

벡터 스토어를 리트리버로 변환해 체인에서 더 쉽게 사용할 수도 있어요.

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" }
    }
]

거리 유사도 알고리즘 (Distance similarity algorithm)

HanaDB는 다음 거리 유사도 알고리즘을 지원해요:

  • 코사인 유사도 (기본)
  • 유클리드 거리 (L2)

HanaDB 인스턴스를 초기화할 때 distanceStrategy 파라미터로 거리 전략을 지정할 수 있어요.

const argsDist: HanaDBArgs = {
  connection: client,
  tableName: "MY_TABLE",
  distanceStrategy: "EUCLIDEAN",
    // distanceStrategy: "COSINE", // (default)
};
const dbDist = new HanaDB(embeddings, argsDist);
await dbDist.initialize();

HNSW 인덱스 생성 (Creating a HNSW index)

벡터 인덱스는 벡터에 대한 top-k 최근접 이웃 쿼리를 크게 빠르게 할 수 있어요. createHnswIndex 함수로 계층적 탐색 가능한 소세계(Hierarchical Navigable Small World, 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

기본 인덱스 이름은 "<TABLE_NAME>_idx"가 돼요.

고급 필터링 (Advanced filtering)

기본 값 기반 필터링 기능 외에도 더 고급 필터링을 사용할 수 있어요. 아래 표는 사용 가능한 필터 연산자를 보여줘요.

연산자 의미 지원되는 피연산자 데이터 타입
$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 특정 키워드를 포함하는 문서 필터링 비어있지 않은 string
$and 논리 "and", 두 개 이상 피연산자 지원 list(Filter)
$or 논리 "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 }

결과 재랭킹 (Reranking results)

similaritySearch 메서드에 rerank_config 딕셔너리를 전달해 유사도 검색 결과를 재랭킹할 수 있어요. 재랭킹은 Cross Encoding 모델을 사용한 SAP HANA의 CROSS_ENCODE 함수로 수행돼요.

rerank_config 딕셔너리는 다음 필드를 지원해요:

  • query (string, required) — 재랭킹에 사용할 쿼리 텍스트. 제공하지 않으면 원래 유사도 검색 쿼리가 사용돼요.
  • modelId (string, required) — 재랭킹에 사용할 cross-encoding 모델의 모델 ID.
  • topN (number, optional) — 재랭킹 후 반환할 상위 결과 수. 기본값 3.
  • rankFields (string[], optional) — 재랭킹에 포함할 메타데이터 필드 목록. 지정하면 이 필드들이 콘텐츠와 연결되어 점수에 사용돼요.

Note: 재랭킹 기능을 사용하려면 SAP HANA Cloud 인스턴스에서 NLP가 활성화되어 있는지 확인하세요. 자세한 내용은 SAP HANA Database Additional Features를 참고하세요.

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

기본 재랭킹 (Basic reranking)

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' }

메타데이터 필드로 재랭킹 (Reranking with metadata fields)

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' }

점수로 재랭킹 (Reranking with scores)

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로 독립 실행 재랭킹 (Standalone reranking with HanaReranker)

재랭킹 과정을 더 제어하려면 HanaReranker 클래스를 문서 압축기(document compressor)로 직접 사용할 수 있어요. 유사도 검색뿐 아니라 어떤 출처의 문서든 재랭킹하려 할 때 유용해요:

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) 사용법 (Usage for retrieval-augmented generation)

이 벡터 스토어를 검색 증강 생성(RAG)에 사용하는 방법에 대한 가이드는 다음 섹션을 참고하세요:

벡터 데이터가 있는 표준 테이블 vs 커스텀 테이블 (Standard tables vs. custom tables with vector data)

기본적으로 임베딩 테이블에는 세 개의 열이 있어요:

  • VEC_TEXT: 문서 텍스트
  • VEC_META: 문서 메타데이터
  • VEC_VECTOR: 임베딩 벡터

커스텀 테이블은 표준 테이블의 의미와 일치하는 열이 최소 세 개 있어야 해요:

  • 텍스트/컨텍스트용 NCLOB/NVARCHAR
  • 메타데이터용 NCLOB/NVARCHAR
  • 임베딩 벡터용 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 또는 NVARCHAR 타입 열
  • 메타데이터용 NCLOB 또는 NVARCHAR 타입 열
  • 임베딩 벡터용 REAL_VECTOR 또는 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

커스텀 열로 필터 성능 최적화 (Filter performance optimization with custom columns)

유연한 메타데이터 값을 허용하기 위해 기본적으로 모든 메타데이터는 메타데이터 열에 JSON으로 저장돼요. 사용 중인 메타데이터 키와 값 타입 중 일부를 알고 있다면, 키 이름을 열 이름으로 한 대상 테이블을 만들고 specificMetadataColumns 목록을 통해 HanaDB 생성자에 전달해 이를 추가 열에 저장할 수 있어요. 해당 값과 일치하는 메타데이터 키는 삽입 시 특수 열로 복사돼요. 필터는 specificMetadataColumns 목록의 키에 대해 메타데이터 JSON 열 대신 특수 열을 사용해요.

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

간단한 예제 (A simple example)

샘플 문서 "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

Maximal marginal relevance는 쿼리에 대한 유사성과 선택된 문서 간의 다양성을 모두 최적화해요. 처음 20(fetch_k)개 항목이 DB에서 검색돼요. 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 벡터 인덱스 생성 (Creating a HNSW vector index)

벡터 인덱스는 벡터에 대한 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);
});

핵심 포인트 (Key Points):

  • 유사도 함수: 인덱스의 유사도 함수는 기본적으로 코사인 유사도예요. 다른 유사도 함수(예: L2 거리)를 사용하려면 HanaDB 인스턴스를 초기화할 때 지정해야 해요.
  • 기본 파라미터: createHnswIndex 함수에서 사용자가 m, efConstruction, efSearch 같은 파라미터에 커스텀 값을 제공하지 않으면 기본값(예: m=64, efConstruction=128, efSearch=200)이 자동으로 사용돼요. 이 값들은 사용자 개입 없이도 합리적인 성능으로 인덱스가 생성되도록 보장해요.

출처: 문서

본문

HanaDB는 SAP HANA Cloud Vector Engine에 완전히 통합된 벡터 스토어예요. @sap/hana-langchain 패키지에서 가져와 HANA 연결과 임베딩(외부 OpenAIEmbeddings 또는 내부 HanaInternalEmbeddingsVECTOR_EMBEDDING() 사용)으로 구성하고 initialize()로 초기화해요. 문서 추가(useMapMerge 지원)·삭제, 유사도 검색·MMR·asRetriever(), 코사인·유클리드 거리 전략, createHnswIndex(HNSW 인덱스), $eq·$like·$contains·$and·$or 등 고급 필터, CROSS_ENCODE 기반 재랭킹(rerankConfig, HanaReranker), 커스텀 테이블·특정 메타데이터 열 최적화를 지원해요.

더 알아보기 (Learn more)