Skip to content

Commit 1eec173

Browse files
committed
enable the user to create scalar indexes on specific table columns (after table creation), list all indexes with detailed info, and drop indexes by name — all via the existing CLI interface.
New struct: LanceDBIndexInfo — struct with name, index_type, columns, num_columns New functions: - lancedb_table_list_indices_detailed() — calls the LanceDB SDK's list_indices() and returns full IndexConfig data (name, type, columns) instead of just names - lancedb_free_index_list_detailed() — frees the struct array and all nested allocations - sdk_index_type_to_c() — maps the SDK's IndexType enum to the C API's LanceDBIndexType examples/s3vector_concurrent_service.cpp : adding CreateScalarIndex each column can have a different type (BTREE, BITMAP, LABELLIST;default BTREE) ListScalarIndexes : Lists all indexes on the table with name, type, and columns DropScalarIndex : Drops an index by its auto-generated name (e.g., category_idx) Unit tests (tests/test_scalar_index.cpp) : several unit tests covering the new lancedb_table_list_indices_detailed C binding Signed-off-by: gal salomon <gal.salomon@gmail.com>
1 parent beba1e2 commit 1eec173

4 files changed

Lines changed: 541 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,30 @@ if(BUILD_TESTS)
378378
)
379379
list(APPEND TEST_TARGETS lancedb_index_tests)
380380

381+
# Add test executable for scalar index detailed tests (runs with valgrind)
382+
add_executable(lancedb_scalar_index_tests
383+
tests/test_main.cpp
384+
tests/test_common.cpp
385+
tests/test_scalar_index.cpp
386+
)
387+
target_link_libraries(lancedb_scalar_index_tests
388+
PRIVATE
389+
lancedb
390+
Catch2::Catch2
391+
Threads::Threads
392+
${ARROW_LIBRARIES}
393+
)
394+
target_include_directories(lancedb_scalar_index_tests
395+
PRIVATE ${ARROW_INCLUDE_DIRS}
396+
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include
397+
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests
398+
)
399+
target_compile_options(lancedb_scalar_index_tests PRIVATE ${ARROW_CFLAGS_OTHER})
400+
set_target_properties(lancedb_scalar_index_tests PROPERTIES
401+
BUILD_RPATH ${RUST_TARGET_DIR}
402+
)
403+
list(APPEND TEST_TARGETS lancedb_scalar_index_tests)
404+
381405
# Add test executable for vector tests (runs without valgrind)
382406
add_executable(lancedb_vector_index_tests
383407
tests/test_main.cpp
@@ -517,6 +541,19 @@ if(BUILD_TESTS)
517541
--suppressions=${CMAKE_CURRENT_SOURCE_DIR}/valgrind.supp
518542
$<TARGET_FILE:lancedb_index_tests>
519543
)
544+
# Run scalar index detailed tests with valgrind
545+
add_test(NAME lancedb_scalar_index_tests
546+
COMMAND ${VALGRIND_EXECUTABLE}
547+
--tool=memcheck
548+
--leak-check=full
549+
--show-leak-kinds=definite
550+
--errors-for-leak-kinds=definite
551+
--track-origins=yes
552+
--error-exitcode=1
553+
--log-file=${CMAKE_BINARY_DIR}/valgrind_scalar_index.txt
554+
--suppressions=${CMAKE_CURRENT_SOURCE_DIR}/valgrind.supp
555+
$<TARGET_FILE:lancedb_scalar_index_tests>
556+
)
520557
# Run query tests with valgrind
521558
add_test(NAME lancedb_query_tests
522559
COMMAND ${TEST_ENV_PREFIX} ${VALGRIND_EXECUTABLE}
@@ -537,6 +574,7 @@ if(BUILD_TESTS)
537574
add_test(NAME lancedb_table_meta_tests COMMAND ${TEST_ENV_PREFIX} $<TARGET_FILE:lancedb_table_meta_tests>)
538575
add_test(NAME lancedb_index_tests COMMAND ${TEST_ENV_PREFIX} $<TARGET_FILE:lancedb_index_tests>)
539576
add_test(NAME lancedb_query_tests COMMAND ${TEST_ENV_PREFIX} $<TARGET_FILE:lancedb_query_tests>)
577+
add_test(NAME lancedb_scalar_index_tests COMMAND ${TEST_ENV_PREFIX} $<TARGET_FILE:lancedb_scalar_index_tests>)
540578
endif()
541579

542580
# Run vector index tests WITHOUT valgrind (too slow under valgrind)

examples/s3vector_concurrent_service.cpp

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,6 +2381,264 @@ ApiResponse UpdateIndexConfig(const json& request) {
23812381
});
23822382
}
23832383

2384+
// ============================================================================
2385+
// Scalar Index Management
2386+
// ============================================================================
2387+
2388+
static LanceDBIndexType parse_scalar_index_type(const std::string& type_str) {
2389+
if (type_str == "BTREE") return LANCEDB_INDEX_BTREE;
2390+
if (type_str == "BITMAP") return LANCEDB_INDEX_BITMAP;
2391+
if (type_str == "LABELLIST") return LANCEDB_INDEX_LABELLIST;
2392+
return LANCEDB_INDEX_AUTO; // invalid sentinel
2393+
}
2394+
2395+
static std::string index_type_to_string(LanceDBIndexType type) {
2396+
switch (type) {
2397+
case LANCEDB_INDEX_BTREE: return "BTREE";
2398+
case LANCEDB_INDEX_BITMAP: return "BITMAP";
2399+
case LANCEDB_INDEX_LABELLIST: return "LABELLIST";
2400+
case LANCEDB_INDEX_FTS: return "FTS";
2401+
case LANCEDB_INDEX_IVF_FLAT: return "IVF_FLAT";
2402+
case LANCEDB_INDEX_IVF_PQ: return "IVF_PQ";
2403+
case LANCEDB_INDEX_IVF_HNSW_PQ: return "IVF_HNSW_PQ";
2404+
case LANCEDB_INDEX_IVF_HNSW_SQ: return "IVF_HNSW_SQ";
2405+
default: return "UNKNOWN";
2406+
}
2407+
}
2408+
2409+
// CreateScalarIndex - Create scalar indexes on specific columns
2410+
ApiResponse CreateScalarIndex(const json& request) {
2411+
if (!request.contains("indexName")) {
2412+
return make_error(400, "ValidationException", "indexName is required");
2413+
}
2414+
if (!request.contains("vectorBucketName")) {
2415+
return make_error(400, "ValidationException", "vectorBucketName is required");
2416+
}
2417+
if (!request.contains("columns") || !request["columns"].is_array() || request["columns"].empty()) {
2418+
return make_error(400, "ValidationException",
2419+
"columns array is required (each element: {name, indexType})");
2420+
}
2421+
2422+
std::string bucket_name = request["vectorBucketName"].get<std::string>();
2423+
std::string index_name = request["indexName"].get<std::string>();
2424+
2425+
if (!LanceDBHelper::index_exists(bucket_name, index_name)) {
2426+
return make_error(404, "NotFoundException",
2427+
"Index '" + index_name + "' not found");
2428+
}
2429+
2430+
// Validate columns before connecting
2431+
struct ColSpec {
2432+
std::string name;
2433+
LanceDBIndexType type;
2434+
};
2435+
std::vector<ColSpec> col_specs;
2436+
2437+
for (const auto& col : request["columns"]) {
2438+
if (!col.contains("name") || !col["name"].is_string() || col["name"].get<std::string>().empty()) {
2439+
return make_error(400, "ValidationException",
2440+
"Each column must have a 'name' field");
2441+
}
2442+
std::string col_name = col["name"].get<std::string>();
2443+
std::string type_str = col.value("indexType", "BTREE");
2444+
2445+
LanceDBIndexType idx_type = parse_scalar_index_type(type_str);
2446+
if (idx_type == LANCEDB_INDEX_AUTO) {
2447+
return make_error(400, "ValidationException",
2448+
"Invalid indexType '" + type_str + "' for column '" + col_name +
2449+
"'. Must be BTREE, BITMAP, or LABELLIST");
2450+
}
2451+
col_specs.push_back({col_name, idx_type});
2452+
}
2453+
2454+
// Connect and open table
2455+
std::string db_path = utils::get_index_db_path(bucket_name, index_name);
2456+
LanceDBConnection* conn = LanceDBHelper::connect(db_path);
2457+
if (!conn) {
2458+
return make_error(500, "InternalServerException",
2459+
"Failed to connect to index database");
2460+
}
2461+
2462+
LanceDBTable* table = LanceDBHelper::open_table(conn, "vectors");
2463+
if (!table) {
2464+
lancedb_connection_free(conn);
2465+
return make_error(500, "InternalServerException",
2466+
"Failed to open vectors table");
2467+
}
2468+
2469+
// Create a scalar index for each column
2470+
json created = json::array();
2471+
for (const auto& spec : col_specs) {
2472+
const char* col_name = spec.name.c_str();
2473+
const char* columns[] = {col_name};
2474+
LanceDBScalarIndexConfig cfg;
2475+
cfg.replace = 1;
2476+
cfg.force_update_statistics = 0;
2477+
2478+
char* err_msg = nullptr;
2479+
LanceDBError result = lancedb_table_create_scalar_index(
2480+
table, columns, 1, spec.type, &cfg, &err_msg);
2481+
2482+
if (result != LANCEDB_SUCCESS) {
2483+
std::string error = err_msg ? err_msg : lancedb_error_to_message(result);
2484+
if (err_msg) lancedb_free_string(err_msg);
2485+
lancedb_table_free(table);
2486+
lancedb_connection_free(conn);
2487+
return make_error(500, "InternalServerException",
2488+
"Failed to create scalar index on column '" + spec.name + "': " + error);
2489+
}
2490+
2491+
created.push_back({
2492+
{"column", spec.name},
2493+
{"indexType", index_type_to_string(spec.type)}
2494+
});
2495+
2496+
LOG_INFO("CREATE_SCALAR_INDEX", index_name,
2497+
"Scalar index created on column '" + spec.name +
2498+
"' (type=" + index_type_to_string(spec.type) + ")");
2499+
}
2500+
2501+
lancedb_table_free(table);
2502+
lancedb_connection_free(conn);
2503+
2504+
return make_success({
2505+
{"indexName", index_name},
2506+
{"vectorBucketName", bucket_name},
2507+
{"createdIndexes", created}
2508+
});
2509+
}
2510+
2511+
// ListScalarIndexes - List all indexes on the table
2512+
ApiResponse ListScalarIndexes(const json& request) {
2513+
if (!request.contains("indexName")) {
2514+
return make_error(400, "ValidationException", "indexName is required");
2515+
}
2516+
if (!request.contains("vectorBucketName")) {
2517+
return make_error(400, "ValidationException", "vectorBucketName is required");
2518+
}
2519+
2520+
std::string bucket_name = request["vectorBucketName"].get<std::string>();
2521+
std::string index_name = request["indexName"].get<std::string>();
2522+
2523+
if (!LanceDBHelper::index_exists(bucket_name, index_name)) {
2524+
return make_error(404, "NotFoundException",
2525+
"Index '" + index_name + "' not found");
2526+
}
2527+
2528+
std::string db_path = utils::get_index_db_path(bucket_name, index_name);
2529+
LanceDBConnection* conn = LanceDBHelper::connect(db_path);
2530+
if (!conn) {
2531+
return make_error(500, "InternalServerException",
2532+
"Failed to connect to index database");
2533+
}
2534+
2535+
LanceDBTable* table = LanceDBHelper::open_table(conn, "vectors");
2536+
if (!table) {
2537+
lancedb_connection_free(conn);
2538+
return make_error(500, "InternalServerException",
2539+
"Failed to open vectors table");
2540+
}
2541+
2542+
LanceDBIndexInfo* indices = nullptr;
2543+
size_t count = 0;
2544+
char* err_msg = nullptr;
2545+
LanceDBError result = lancedb_table_list_indices_detailed(
2546+
table, &indices, &count, &err_msg);
2547+
2548+
if (result != LANCEDB_SUCCESS) {
2549+
std::string error = err_msg ? err_msg : lancedb_error_to_message(result);
2550+
if (err_msg) lancedb_free_string(err_msg);
2551+
lancedb_table_free(table);
2552+
lancedb_connection_free(conn);
2553+
return make_error(500, "InternalServerException",
2554+
"Failed to list indices: " + error);
2555+
}
2556+
2557+
json indexes_json = json::array();
2558+
for (size_t i = 0; i < count; i++) {
2559+
json idx;
2560+
idx["name"] = indices[i].name ? std::string(indices[i].name) : "";
2561+
idx["indexType"] = index_type_to_string(indices[i].index_type);
2562+
2563+
json cols = json::array();
2564+
for (size_t j = 0; j < indices[i].num_columns; j++) {
2565+
if (indices[i].columns[j]) {
2566+
cols.push_back(std::string(indices[i].columns[j]));
2567+
}
2568+
}
2569+
idx["columns"] = cols;
2570+
indexes_json.push_back(idx);
2571+
}
2572+
2573+
if (indices) lancedb_free_index_list_detailed(indices, count);
2574+
lancedb_table_free(table);
2575+
lancedb_connection_free(conn);
2576+
2577+
return make_success({
2578+
{"indexName", index_name},
2579+
{"vectorBucketName", bucket_name},
2580+
{"indexes", indexes_json}
2581+
});
2582+
}
2583+
2584+
// DropScalarIndex - Drop a scalar index by name
2585+
ApiResponse DropScalarIndex(const json& request) {
2586+
if (!request.contains("indexName")) {
2587+
return make_error(400, "ValidationException", "indexName is required");
2588+
}
2589+
if (!request.contains("vectorBucketName")) {
2590+
return make_error(400, "ValidationException", "vectorBucketName is required");
2591+
}
2592+
if (!request.contains("scalarIndexName")) {
2593+
return make_error(400, "ValidationException", "scalarIndexName is required");
2594+
}
2595+
2596+
std::string bucket_name = request["vectorBucketName"].get<std::string>();
2597+
std::string index_name = request["indexName"].get<std::string>();
2598+
std::string scalar_index_name = request["scalarIndexName"].get<std::string>();
2599+
2600+
if (!LanceDBHelper::index_exists(bucket_name, index_name)) {
2601+
return make_error(404, "NotFoundException",
2602+
"Index '" + index_name + "' not found");
2603+
}
2604+
2605+
std::string db_path = utils::get_index_db_path(bucket_name, index_name);
2606+
LanceDBConnection* conn = LanceDBHelper::connect(db_path);
2607+
if (!conn) {
2608+
return make_error(500, "InternalServerException",
2609+
"Failed to connect to index database");
2610+
}
2611+
2612+
LanceDBTable* table = LanceDBHelper::open_table(conn, "vectors");
2613+
if (!table) {
2614+
lancedb_connection_free(conn);
2615+
return make_error(500, "InternalServerException",
2616+
"Failed to open vectors table");
2617+
}
2618+
2619+
char* err_msg = nullptr;
2620+
LanceDBError result = lancedb_table_drop_index(table, scalar_index_name.c_str(), &err_msg);
2621+
2622+
lancedb_table_free(table);
2623+
lancedb_connection_free(conn);
2624+
2625+
if (result != LANCEDB_SUCCESS) {
2626+
std::string error = err_msg ? err_msg : lancedb_error_to_message(result);
2627+
if (err_msg) lancedb_free_string(err_msg);
2628+
return make_error(500, "InternalServerException",
2629+
"Failed to drop index '" + scalar_index_name + "': " + error);
2630+
}
2631+
2632+
LOG_INFO("DROP_SCALAR_INDEX", index_name,
2633+
"Dropped scalar index '" + scalar_index_name + "'");
2634+
2635+
return make_success({
2636+
{"indexName", index_name},
2637+
{"vectorBucketName", bucket_name},
2638+
{"droppedIndex", scalar_index_name}
2639+
});
2640+
}
2641+
23842642
// ============================================================================
23852643
// Command Line Interface
23862644
// ============================================================================
@@ -2401,6 +2659,9 @@ S3 Vector Concurrent Service with Background Indexing
24012659
TriggerRebuild Manually trigger index rebuild
24022660
GetIndexState Get current index state and stats
24032661
UpdateIndexConfig Update index configuration and thresholds
2662+
CreateScalarIndex Create scalar indexes on table columns
2663+
ListScalarIndexes List all indexes on the table
2664+
DropScalarIndex Drop an index by name
24042665
--help, -h Show this help message
24052666
24062667
DESIGN:
@@ -2480,12 +2741,40 @@ S3 Vector Concurrent Service with Background Indexing
24802741
"deletionRatioThreshold": 0.25
24812742
}'
24822743
2744+
# Create scalar indexes on columns
2745+
./s3vector_concurrent_service CreateScalarIndex '{
2746+
"vectorBucketName": "my-bucket",
2747+
"indexName": "my-index",
2748+
"columns": [
2749+
{"name": "category", "indexType": "BITMAP"},
2750+
{"name": "price", "indexType": "BTREE"}
2751+
]
2752+
}'
2753+
2754+
# List all indexes on the table
2755+
./s3vector_concurrent_service ListScalarIndexes '{
2756+
"vectorBucketName": "my-bucket",
2757+
"indexName": "my-index"
2758+
}'
2759+
2760+
# Drop a scalar index by name
2761+
./s3vector_concurrent_service DropScalarIndex '{
2762+
"vectorBucketName": "my-bucket",
2763+
"indexName": "my-index",
2764+
"scalarIndexName": "category_idx"
2765+
}'
2766+
24832767
INDEX TYPES:
24842768
IVF_FLAT - Inverted file index (no compression)
24852769
IVF_PQ - IVF with product quantization (default)
24862770
IVF_HNSW_PQ - IVF with HNSW graph and PQ
24872771
IVF_HNSW_SQ - IVF with HNSW graph and scalar quantization
24882772
2773+
SCALAR INDEX TYPES:
2774+
BTREE - B-tree index (ordered data, range queries)
2775+
BITMAP - Bitmap index (low-cardinality categorical data)
2776+
LABELLIST - Label list index (list/tag columns)
2777+
24892778
QUERY PARAMETERS:
24902779
nprobes - Number of IVF partitions to search (higher = more accurate)
24912780
refineFactor - Number of candidates to refine (higher = more accurate)
@@ -2601,6 +2890,12 @@ int main(int argc, char* argv[]) {
26012890
response = GetIndexState(request);
26022891
} else if (command == "UpdateIndexConfig") {
26032892
response = UpdateIndexConfig(request);
2893+
} else if (command == "CreateScalarIndex") {
2894+
response = CreateScalarIndex(request);
2895+
} else if (command == "ListScalarIndexes") {
2896+
response = ListScalarIndexes(request);
2897+
} else if (command == "DropScalarIndex") {
2898+
response = DropScalarIndex(request);
26042899
} else {
26052900
std::cerr << "Unknown command: " << command << std::endl;
26062901
print_help();

0 commit comments

Comments
 (0)