Repository: CUBRID/cubrid
Pull request #6224: [CUBVEC-73] fix crash when m, efcon is not provided
Author: login: vimkim
Created: 2025-05-29T10:58:27Z | Merged: 2025-05-30T16:34:15Z

<http://jira.cubrid.org/browse/CUBVEC-73>

### Purpose

m, efcon 설정을 안하면 서버쪽에 0이 넘어옵니다.
문법에 아무것도 설정되지 않았을 때 기본값 설정 로직이 누락되었습니다. 이를 수정합니다.

### Implementation

kvp (csql rule $13) 이 null 이 아닐 경우에만 default m 과 default ef_con을 설정한 실수를 제거함.

### Remarks

```sql
DROP if exists items;
CREATE TABLE items (id int, vc1 vector(3)) DONT_REUSE_OID;
CREATE VECTOR INDEX idx_v ON items(vc1 EUCLIDEAN);

INSERT INTO items VALUES (1, '[1,2,3]');
INSERT INTO items VALUES (2, '[1,0,2]');
```

위 sql 문에서 crash가 일어나지 않아야 함.


Diff:
diff --git a/src/parser/csql_grammar.y b/src/parser/csql_grammar.y
index 9cb73b07f01..e93e05d178d 100644
--- a/src/parser/csql_grammar.y
+++ b/src/parser/csql_grammar.y
@@ -2833,21 +2833,24 @@ create_stmt
 
 			node->info.index.is_vector_index = true;
 
+			node->info.index.vector_index.hnsw_m = HNSW_DEFAULT_M;
+			node->info.index.vector_index.hnsw_ef_construction = HNSW_DEFAULT_EF_CONSTRUCTION;
+
 			kv_pair *kvp = $13; // opt_vector_index_with_clause
 			if (kvp) 
 			  {
 			    PT_NODE *m_node = kv_pair_lookup(kvp, "m");
+			    if (m_node != NULL)
+			      {
+				int m = m_node->info.value.data_value.i;
+				node->info.index.vector_index.hnsw_m = m;
+			      }
 			    PT_NODE *ef_construction_node = kv_pair_lookup(kvp, "ef_construction");
-
-			    // TODO (CUBVEC): default m and ef_con?
-			    int default_m = 16;
-			    int default_ef_con = 64;
-
-			    int m = m_node ? m_node->info.value.data_value.i : default_m;
-			    int ef_con = ef_construction_node ? ef_construction_node->info.value.data_value.i : default_ef_con;
-
-			    node->info.index.vector_index.hnsw_m = m;
-			    node->info.index.vector_index.hnsw_ef_construction = ef_con;
+			    if (ef_construction_node != NULL)
+			      {
+				int ef_con = ef_construction_node->info.value.data_value.i;
+				node->info.index.vector_index.hnsw_ef_construction = ef_con;
+			      }
 			  }
 
 			bool opt_unique = false; // original $5
diff --git a/src/parser/parse_tree.h b/src/parser/parse_tree.h
index d4adfe1edbb..8452dc5b234 100644
--- a/src/parser/parse_tree.h
+++ b/src/parser/parse_tree.h
@@ -2164,6 +2164,9 @@ struct pt_create_entity_info
   unsigned if_not_exists:1;	/* IF NOT EXISTS clause for create table | class */
 };
 
+#define HNSW_DEFAULT_M 16
+#define HNSW_DEFAULT_EF_CONSTRUCTION 64
+
 /* Structure for vector index specific information */
 typedef struct pt_vector_index_info
 {
diff --git a/src/storage/hnsw.cpp b/src/storage/hnsw.cpp
index 121f23a4f49..046f97b70bb 100644
--- a/src/storage/hnsw.cpp
+++ b/src/storage/hnsw.cpp
@@ -144,12 +144,54 @@ int hnsw_print_index_info (BTID *btid)
   std::unique_ptr<faiss::IndexIDMap> &index = it->second;
   auto *hnsw_index = static_cast<faiss::IndexHNSWFlat *> (index->index);
 
-  er_log_debug (ARG_FILE_LINE, "HNSW Index Information for ID %d:", hnsw_id);
-  er_log_debug (ARG_FILE_LINE, "  - Dimension: %d", index->d);
-  er_log_debug (ARG_FILE_LINE, "  - Metric Type: %d", index->metric_type);
-  er_log_debug (ARG_FILE_LINE, "  - Total Elements: %d", index->ntotal);
-  er_log_debug (ARG_FILE_LINE, "  - HNSW efConstruction: %d", hnsw_index->hnsw.efConstruction);
-  er_log_debug (ARG_FILE_LINE, "  - HNSW efSearch: %d", hnsw_index->hnsw.efSearch);
+  std::ostringstream oss;
+
+  oss << "HNSW Index Information for ID " << hnsw_id << ":\n";
+  oss << "  - Dimension: " << index->d << "\n";
+  oss << "  - Metric Type: " << index->metric_type << "\n";
+  oss << "  - Total Elements: " << index->ntotal << "\n";
+  oss << "  - HNSW efConstruction: " << hnsw_index->hnsw.efConstruction << "\n";
+  oss << "  - HNSW efSearch: " << hnsw_index->hnsw.efSearch << "\n";
+
+  /*
+  * This works because, in faiss/impl/HNSW.cpp, HNSW is initialized with
+  * set_default_probas(M, ...);
+
+  // initialize the assign_probas and cum_nneighbor_per_level to
+  // have 2*M links on level 0 and M links on levels > 0
+  void HNSW::set_default_probas(int M, float levelMult) {
+    int nn = 0;
+    cum_nneighbor_per_level.push_back(0);
+    for (int level = 0;; level++) {
+        float proba = exp(-level / levelMult) * (1 - exp(-1 / levelMult));
+        if (proba < 1e-9)
+            break;
+        assign_probas.push_back(proba);
+        nn += level == 0 ? M * 2 : M;
+        cum_nneighbor_per_level.push_back(nn);
+    }
+  }
+  */
+
+  // Print neighbor counts
+  const auto &neighbor_counts = hnsw_index->hnsw.cum_nneighbor_per_level;
+  oss << "  - HNSW neighbor_counts: [";
+  for (size_t i = 0; i < neighbor_counts.size(); ++i)
+    {
+      oss << neighbor_counts[i];
+      if (i + 1 < neighbor_counts.size())
+	{
+	  oss << ", ";
+	}
+    }
+  oss << "]\n";
+
+  if (neighbor_counts.size() > 1)
+    {
+      oss << "  - HNSW M is assumed to be " << (neighbor_counts[1] / 2) << "\n";
+    }
+
+  er_log_debug (ARG_FILE_LINE, "%s", oss.str().c_str());
 
   return NO_ERROR;
 }


Reviews:
user: login: vimkim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T11:02:44Z
user: login: YeunjunLee
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T11:09:15Z
user: login: hgryoo
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-29T11:44:47Z
user: login: vimkim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T11:35:40Z
user: login: hornetmj
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T12:20:58Z
user: login: hornetmj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T12:36:09Z
user: login: vimkim
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T16:30:49Z

Review comments:
diff_hunk: @@ -2164,6 +2164,9 @@ struct pt_create_entity_info
   unsigned if_not_exists:1;	/* IF NOT EXISTS clause for create table | class */
 };
 
+#define HNSW_DEFAULT_M 16
+#define HNSW_DEFAULT_EF_CONSTRUCTION 64
+
 /* Structure for vector index specific information */
path: src/parser/parse_tree.h
user: login: vimkim
body: PT_VECTOR_INDEX_INFO struct 위에 선언했는데, 위치를 바꾸는게 좋을까요?
created_at: 2025-05-29T11:02:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,6 +151,49 @@ int hnsw_print_index_info (BTID *btid)
   er_log_debug (ARG_FILE_LINE, "  - HNSW efConstruction: %d", hnsw_index->hnsw.efConstruction);
   er_log_debug (ARG_FILE_LINE, "  - HNSW efSearch: %d", hnsw_index->hnsw.efSearch);
 
+  /*
+  This works because, in faiss/impl/HNSW.cpp
+
+  ```cpp
+
+  // initialize the assign_probas and cum_nneighbor_per_level to
+  // have 2*M links on level 0 and M links on levels > 0
+  void HNSW::set_default_probas(int M, float levelMult) {
+    int nn = 0;
+    cum_nneighbor_per_level.push_back(0);
+    for (int level = 0;; level++) {
+        float proba = exp(-level / levelMult) * (1 - exp(-1 / levelMult));
+        if (proba < 1e-9)
+            break;
+        assign_probas.push_back(proba);
+        nn += level == 0 ? M * 2 : M;
+        cum_nneighbor_per_level.push_back(nn);
+    }
+  }
+
+  ```
+
+  */
+
+  const auto& neighbor_counts = hnsw_index->hnsw.cum_nneighbor_per_level;
+
+  std::string output = "  - HNSW neighbor_counts: [";
+  for (size_t i = 0; i < neighbor_counts.size(); ++i)
+    {
+      output += std::to_string (neighbor_counts[i]);
+      if (i + 1 < neighbor_counts.size())
+	{
+	  output += ", ";
+	}
+    }
+  output += "]";
+
+  er_log_debug (ARG_FILE_LINE, "%s", output.c_str());
+  if (neighbor_counts.size() > 1)
+    {
+      er_log_debug (ARG_FILE_LINE, "  - HNSW M is assumed to be %d", neighbor_counts[1] / 2);
+    }
+
   return NO_ERROR;
path: src/storage/hnsw.cpp
user: login: vimkim
body: Faiss HNSW는 M을 생성자 인자로 사용하고 저장하지 않습니다.
이 코드는 HNSW 구조체로부터 M을 복구하는 hacky한 방법입니다.
추가로 유용한 neighbor_counts 벡터의 인자를 출력합니다. HNSW Level 별로 search 하는 이웃의 갯수로 추정합니다.

```
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 74
HNSW Index added with ID 0
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 147
HNSW Index Information for ID 0:
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 148
  - Dimension: 3
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 149
  - Metric Type: 1
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 150
  - Total Elements: 0
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 151
  - HNSW efConstruction: 64
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 152
  - HNSW efSearch: 16
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 189
  - HNSW neighbor_counts: [0, 2468, 3702, 4936]
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 192
M is assumed to be 1234

```

위와 같이 출력하게 됩니다.
created_at: 2025-05-30T11:35:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,6 +151,49 @@ int hnsw_print_index_info (BTID *btid)
   er_log_debug (ARG_FILE_LINE, "  - HNSW efConstruction: %d", hnsw_index->hnsw.efConstruction);
   er_log_debug (ARG_FILE_LINE, "  - HNSW efSearch: %d", hnsw_index->hnsw.efSearch);
 
+  /*
+  This works because, in faiss/impl/HNSW.cpp
+
+  ```cpp
+
+  // initialize the assign_probas and cum_nneighbor_per_level to
+  // have 2*M links on level 0 and M links on levels > 0
+  void HNSW::set_default_probas(int M, float levelMult) {
+    int nn = 0;
+    cum_nneighbor_per_level.push_back(0);
+    for (int level = 0;; level++) {
+        float proba = exp(-level / levelMult) * (1 - exp(-1 / levelMult));
+        if (proba < 1e-9)
+            break;
+        assign_probas.push_back(proba);
+        nn += level == 0 ? M * 2 : M;
+        cum_nneighbor_per_level.push_back(nn);
+    }
+  }
+
+  ```
+
+  */
+
+  const auto& neighbor_counts = hnsw_index->hnsw.cum_nneighbor_per_level;
+
+  std::string output = "  - HNSW neighbor_counts: [";
+  for (size_t i = 0; i < neighbor_counts.size(); ++i)
+    {
+      output += std::to_string (neighbor_counts[i]);
+      if (i + 1 < neighbor_counts.size())
+	{
+	  output += ", ";
+	}
+    }
+  output += "]";
+
+  er_log_debug (ARG_FILE_LINE, "%s", output.c_str());
+  if (neighbor_counts.size() > 1)
+    {
+      er_log_debug (ARG_FILE_LINE, "  - HNSW M is assumed to be %d", neighbor_counts[1] / 2);
+    }
+
   return NO_ERROR;
path: src/storage/hnsw.cpp
user: login: hornetmj
body: 다음과 같이 하나의 에러 로그로 출력되었으면 합니다. 시간, 파일, 라인 정보는 복사한 것이니 참고하세요.

```
Time: 05/30/25 20:30:03.496 - DEBUG *** file /home/vimkim/gh/cb/73-fix-crash-m-efcon-zero/src/storage/hnsw.cpp, line 152
  - Dimension: 3
  - Metric Type: 1
  - Total Elements: 0
  ...
```
created_at: 2025-05-30T12:19:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,6 +151,49 @@ int hnsw_print_index_info (BTID *btid)
   er_log_debug (ARG_FILE_LINE, "  - HNSW efConstruction: %d", hnsw_index->hnsw.efConstruction);
   er_log_debug (ARG_FILE_LINE, "  - HNSW efSearch: %d", hnsw_index->hnsw.efSearch);
 
+  /*
+  This works because, in faiss/impl/HNSW.cpp
+
+  ```cpp
+
+  // initialize the assign_probas and cum_nneighbor_per_level to
+  // have 2*M links on level 0 and M links on levels > 0
+  void HNSW::set_default_probas(int M, float levelMult) {
+    int nn = 0;
+    cum_nneighbor_per_level.push_back(0);
+    for (int level = 0;; level++) {
+        float proba = exp(-level / levelMult) * (1 - exp(-1 / levelMult));
+        if (proba < 1e-9)
+            break;
+        assign_probas.push_back(proba);
+        nn += level == 0 ? M * 2 : M;
+        cum_nneighbor_per_level.push_back(nn);
+    }
+  }
+
+  ```
+
+  */
+
+  const auto& neighbor_counts = hnsw_index->hnsw.cum_nneighbor_per_level;
+
+  std::string output = "  - HNSW neighbor_counts: [";
+  for (size_t i = 0; i < neighbor_counts.size(); ++i)
+    {
+      output += std::to_string (neighbor_counts[i]);
+      if (i + 1 < neighbor_counts.size())
+	{
+	  output += ", ";
+	}
+    }
+  output += "]";
+
+  er_log_debug (ARG_FILE_LINE, "%s", output.c_str());
+  if (neighbor_counts.size() > 1)
+    {
+      er_log_debug (ARG_FILE_LINE, "  - HNSW M is assumed to be %d", neighbor_counts[1] / 2);
+    }
+
   return NO_ERROR;
path: src/storage/hnsw.cpp
user: login: hornetmj
body: 중요한건 아닌데, 위 출력 결과 'HNSW neighbor_counts: [0, 2468, 3702, 4936]'는 M 설정값이 디폴트 인가요?
created_at: 2025-05-30T12:36:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,6 +151,49 @@ int hnsw_print_index_info (BTID *btid)
   er_log_debug (ARG_FILE_LINE, "  - HNSW efConstruction: %d", hnsw_index->hnsw.efConstruction);
   er_log_debug (ARG_FILE_LINE, "  - HNSW efSearch: %d", hnsw_index->hnsw.efSearch);
 
+  /*
+  This works because, in faiss/impl/HNSW.cpp
+
+  ```cpp
+
+  // initialize the assign_probas and cum_nneighbor_per_level to
+  // have 2*M links on level 0 and M links on levels > 0
+  void HNSW::set_default_probas(int M, float levelMult) {
+    int nn = 0;
+    cum_nneighbor_per_level.push_back(0);
+    for (int level = 0;; level++) {
+        float proba = exp(-level / levelMult) * (1 - exp(-1 / levelMult));
+        if (proba < 1e-9)
+            break;
+        assign_probas.push_back(proba);
+        nn += level == 0 ? M * 2 : M;
+        cum_nneighbor_per_level.push_back(nn);
+    }
+  }
+
+  ```
+
+  */
+
+  const auto& neighbor_counts = hnsw_index->hnsw.cum_nneighbor_per_level;
+
+  std::string output = "  - HNSW neighbor_counts: [";
+  for (size_t i = 0; i < neighbor_counts.size(); ++i)
+    {
+      output += std::to_string (neighbor_counts[i]);
+      if (i + 1 < neighbor_counts.size())
+	{
+	  output += ", ";
+	}
+    }
+  output += "]";
+
+  er_log_debug (ARG_FILE_LINE, "%s", output.c_str());
+  if (neighbor_counts.size() > 1)
+    {
+      er_log_debug (ARG_FILE_LINE, "  - HNSW M is assumed to be %d", neighbor_counts[1] / 2);
+    }
+
   return NO_ERROR;
path: src/storage/hnsw.cpp
user: login: vimkim
body: 아닙니다. 인덱스를 생성할 때 디버깅용으로 1234를 입력했습니다.

```
HNSW Index Information for ID 0:
  - Dimension: 3
  - Metric Type: 1
  - Total Elements: 0
  - HNSW efConstruction: 64
  - HNSW efSearch: 16
  - HNSW neighbor_counts: [0, 32, 48, 64, 80, 96, 112, 128, 144]
  - HNSW M is assumed to be 16
  ```
  
  기본값 인덱스 생성 시 출력은 위와 같습니다.
created_at: 2025-05-30T16:30:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: vimkim
created_at: 2025-05-29T10:59:18Z
body: /run sql medium cubvec
author_association: CONTRIBUTOR
user: login: vimkim
created_at: 2025-05-29T11:21:49Z
body: /run sql
author_association: CONTRIBUTOR
user: login: vimkim
created_at: 2025-05-29T13:09:30Z
body: log에 m, efcon 출력 코드 추가 예정
author_association: CONTRIBUTOR