native-vector-store 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,394 @@
1
+ #include "vector_store.h"
2
+ #include "vector_store_loader.h"
3
+ #include <thread>
4
+ #include <random>
5
+ #include <chrono>
6
+ #include <iostream>
7
+ #include <atomic>
8
+ #include <cassert>
9
+ #include <sstream>
10
+ #include <iomanip>
11
+ #include <filesystem>
12
+
13
+ using namespace std::chrono;
14
+
15
+ // Test configuration
16
+ constexpr size_t DIM = 1536; // OpenAI embedding dimension
17
+
18
+ // Helper to generate random embedding
19
+ std::vector<float> generate_random_embedding(size_t dim, std::mt19937& rng) {
20
+ std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
21
+ std::vector<float> embedding(dim);
22
+ float sum = 0.0f;
23
+
24
+ for (size_t i = 0; i < dim; ++i) {
25
+ embedding[i] = dist(rng);
26
+ sum += embedding[i] * embedding[i];
27
+ }
28
+
29
+ // Normalize
30
+ float inv_norm = 1.0f / std::sqrt(sum);
31
+ for (size_t i = 0; i < dim; ++i) {
32
+ embedding[i] *= inv_norm;
33
+ }
34
+
35
+ return embedding;
36
+ }
37
+
38
+ // Helper to create JSON document
39
+ std::string create_json_document(const std::string& id, const std::string& text,
40
+ const std::vector<float>& embedding) {
41
+ std::stringstream json;
42
+ json << "{\"id\":\"" << id << "\",\"text\":\"" << text << "\",\"metadata\":{\"embedding\":[";
43
+
44
+ for (size_t i = 0; i < embedding.size(); ++i) {
45
+ if (i > 0) json << ",";
46
+ json << std::fixed << std::setprecision(6) << embedding[i];
47
+ }
48
+
49
+ json << "]}}";
50
+ return json.str();
51
+ }
52
+
53
+
54
+ // Test 1: Producer-consumer loading performance
55
+ void test_loading_performance() {
56
+ std::cout << "\nšŸ“ Test 1: Producer-consumer loadDir performance (1K documents)\n";
57
+
58
+ // Check if test data exists
59
+ const std::string test_data_dir = "../test_data";
60
+ if (!std::filesystem::exists(test_data_dir)) {
61
+ std::cout << "āŒ Test data directory not found: " << test_data_dir << "\n";
62
+ std::cout << " Run: node test/generate_test_data.js\n";
63
+ std::exit(1);
64
+ }
65
+
66
+ VectorStore store(DIM);
67
+ auto start = high_resolution_clock::now();
68
+
69
+ // Load using the clean VectorStoreLoader interface
70
+ VectorStoreLoader::loadDirectory(&store, test_data_dir);
71
+
72
+ auto load_end = high_resolution_clock::now();
73
+ auto load_elapsed = duration_cast<milliseconds>(load_end - start).count();
74
+
75
+ std::cout << "āœ… Loaded " << store.size() << " documents in " << load_elapsed << "ms\n";
76
+ std::cout << " Rate: " << (store.size() * 1000 / load_elapsed) << " docs/sec\n";
77
+
78
+ // Store should already be finalized by loadDir
79
+ assert(store.is_finalized());
80
+ std::cout << " Store finalized by loadDir\n";
81
+ }
82
+
83
+ // Test 2: Phase enforcement validation
84
+ void test_phase_enforcement() {
85
+ std::cout << "\n🚦 Test 2: Phase enforcement validation\n";
86
+
87
+ VectorStore store(DIM);
88
+ std::mt19937 rng(42);
89
+ simdjson::ondemand::parser parser;
90
+
91
+ // Verify search fails before finalization
92
+ auto query = generate_random_embedding(DIM, rng);
93
+ auto results = store.search(query.data(), 10);
94
+ assert(results.empty());
95
+ std::cout << " āœ… Search correctly blocked before finalization\n";
96
+
97
+ // Add some documents
98
+ for (size_t i = 0; i < 100; ++i) {
99
+ auto embedding = generate_random_embedding(DIM, rng);
100
+ std::string json_str = create_json_document(
101
+ "phase-" + std::to_string(i),
102
+ "Phase test document " + std::to_string(i),
103
+ embedding
104
+ );
105
+
106
+ simdjson::padded_string padded(json_str);
107
+ simdjson::ondemand::document doc;
108
+ if (!parser.iterate(padded).get(doc)) {
109
+ auto error = store.add_document(doc);
110
+ assert(error == simdjson::SUCCESS);
111
+ }
112
+ }
113
+
114
+ // Finalize the store
115
+ store.finalize();
116
+ assert(store.is_finalized());
117
+
118
+ // Verify we can search now
119
+ results = store.search(query.data(), 10);
120
+ assert(!results.empty());
121
+ std::cout << " āœ… Search works after finalization\n";
122
+
123
+ // Verify document addition fails after finalization
124
+ auto embedding = generate_random_embedding(DIM, rng);
125
+ std::string json_str = create_json_document("blocked", "Should fail", embedding);
126
+ simdjson::padded_string padded(json_str);
127
+ simdjson::ondemand::document doc;
128
+ parser.iterate(padded).get(doc);
129
+ auto error = store.add_document(doc);
130
+ assert(error == simdjson::INCORRECT_TYPE);
131
+ std::cout << " āœ… Document addition correctly blocked after finalization\n";
132
+ }
133
+
134
+ // Test 3: 64MB+1 allocation (expect fail)
135
+ void test_oversize_allocation() {
136
+ std::cout << "\nšŸ“ Test 3: 64MB+1 allocation (expect fail)\n";
137
+
138
+ VectorStore store(10);
139
+
140
+ // Create a document with metadata that exceeds chunk size
141
+ std::stringstream huge_json;
142
+ huge_json << "{\"id\":\"huge\",\"text\":\"test\",\"metadata\":{\"embedding\":[";
143
+ for (int i = 0; i < 10; ++i) {
144
+ if (i > 0) huge_json << ",";
145
+ huge_json << "0.1";
146
+ }
147
+ huge_json << "],\"huge\":\"";
148
+ // Add 64MB + 1 byte of data
149
+ for (size_t i = 0; i < 67108865; ++i) {
150
+ huge_json << "x";
151
+ }
152
+ huge_json << "\"}}";
153
+
154
+ std::string json_str = huge_json.str();
155
+ simdjson::padded_string padded(json_str);
156
+ simdjson::ondemand::parser parser;
157
+ simdjson::ondemand::document doc;
158
+
159
+ auto error = parser.iterate(padded).get(doc);
160
+ if (!error) {
161
+ // This should fail in the allocator
162
+ error = store.add_document(doc);
163
+ if (error == simdjson::MEMALLOC) {
164
+ std::cout << "āœ… Correctly rejected oversize allocation\n";
165
+ } else {
166
+ std::cout << "āŒ Should have failed with MEMALLOC error, got: " << simdjson::error_message(error) << "\n";
167
+ std::exit(1);
168
+ }
169
+ } else {
170
+ std::cout << "āŒ Failed to parse test JSON: " << simdjson::error_message(error) << "\n";
171
+ std::exit(1);
172
+ }
173
+ }
174
+
175
+ // Test 4: Alignment requests
176
+ void test_alignment_requests() {
177
+ std::cout << "\nšŸŽÆ Test 4: Various alignment requests\n";
178
+
179
+ class TestArenaAllocator : public ArenaAllocator {
180
+ public:
181
+ void test_alignments() {
182
+ // Test valid alignments
183
+ size_t valid_aligns[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096};
184
+
185
+ for (size_t align : valid_aligns) {
186
+ void* ptr = allocate(128, align);
187
+ if (!ptr) {
188
+ std::cout << "āŒ Failed to allocate with alignment " << align << "\n";
189
+ std::exit(1);
190
+ }
191
+ assert(((uintptr_t)ptr % align) == 0);
192
+ }
193
+ std::cout << "āœ… All valid alignments handled correctly\n";
194
+
195
+ // Test invalid alignment (>4096)
196
+ void* ptr = allocate(128, 8192);
197
+ if (ptr) {
198
+ std::cout << "āŒ Should have rejected alignment > 4096\n";
199
+ std::exit(1);
200
+ } else {
201
+ std::cout << "āœ… Correctly rejected large alignment\n";
202
+ }
203
+ }
204
+ };
205
+
206
+ TestArenaAllocator allocator;
207
+ allocator.test_alignments();
208
+ }
209
+
210
+ // Test 5: Phase separation - load, finalize, then search
211
+ void test_phase_separation() {
212
+ std::cout << "\nšŸ”„ Test 5: Phase separation - load, finalize, then search\n";
213
+
214
+ VectorStore store(DIM);
215
+ auto start = high_resolution_clock::now();
216
+
217
+ // Phase 1: Load documents (single-threaded for simplicity)
218
+ std::mt19937 rng(42);
219
+ simdjson::ondemand::parser parser;
220
+ size_t docs_loaded = 0;
221
+
222
+ for (size_t i = 0; i < 1000; ++i) {
223
+ auto embedding = generate_random_embedding(DIM, rng);
224
+ std::string json_str = create_json_document(
225
+ "doc-" + std::to_string(i),
226
+ "Document " + std::to_string(i),
227
+ embedding
228
+ );
229
+
230
+ simdjson::padded_string padded(json_str);
231
+ simdjson::ondemand::document doc;
232
+ if (!parser.iterate(padded).get(doc)) {
233
+ auto error = store.add_document(doc);
234
+ if (!error) {
235
+ docs_loaded++;
236
+ }
237
+ }
238
+ }
239
+
240
+ auto load_time = duration_cast<milliseconds>(high_resolution_clock::now() - start).count();
241
+ std::cout << " Loaded " << docs_loaded << " documents in " << load_time << "ms\n";
242
+
243
+ // Verify searches fail before finalization
244
+ auto query = generate_random_embedding(DIM, rng);
245
+ auto results = store.search(query.data(), 10);
246
+ assert(results.empty());
247
+ std::cout << " āœ… Searches correctly blocked before finalization\n";
248
+
249
+ // Phase 2: Finalize the store
250
+ auto finalize_start = high_resolution_clock::now();
251
+ store.finalize();
252
+ auto finalize_time = duration_cast<milliseconds>(high_resolution_clock::now() - finalize_start).count();
253
+ std::cout << " Finalized (normalized) in " << finalize_time << "ms\n";
254
+
255
+ // Verify no more documents can be added
256
+ {
257
+ auto embedding = generate_random_embedding(DIM, rng);
258
+ std::string json_str = create_json_document("blocked", "Should fail", embedding);
259
+ simdjson::padded_string padded(json_str);
260
+ simdjson::ondemand::document doc;
261
+ parser.iterate(padded).get(doc);
262
+ auto error = store.add_document(doc);
263
+ assert(error == simdjson::INCORRECT_TYPE);
264
+ std::cout << " āœ… Document additions correctly blocked after finalization\n";
265
+ }
266
+
267
+ // Phase 3: Concurrent searches (multiple threads)
268
+ std::atomic<size_t> total_searches{0};
269
+ auto search_start = high_resolution_clock::now();
270
+
271
+ std::vector<std::thread> searchers;
272
+ for (size_t t = 0; t < 4; ++t) {
273
+ searchers.emplace_back([&store, &total_searches, t]() {
274
+ std::mt19937 rng(t);
275
+ for (size_t i = 0; i < 25; ++i) {
276
+ auto query = generate_random_embedding(DIM, rng);
277
+ auto results = store.search(query.data(), 10);
278
+ assert(!results.empty() && results.size() <= 10);
279
+ total_searches++;
280
+ }
281
+ });
282
+ }
283
+
284
+ for (auto& t : searchers) {
285
+ t.join();
286
+ }
287
+
288
+ auto search_time = duration_cast<milliseconds>(high_resolution_clock::now() - search_start).count();
289
+ std::cout << " Performed " << total_searches.load() << " concurrent searches in " << search_time << "ms\n";
290
+
291
+ auto total_time = duration_cast<milliseconds>(high_resolution_clock::now() - start).count();
292
+ std::cout << "āœ… Phase separation test completed in " << total_time << "ms\n";
293
+ }
294
+
295
+ // Test 6: Concurrent search performance after finalization
296
+ void test_concurrent_search_performance() {
297
+ std::cout << "\nšŸ” Test 6: Concurrent search performance\n";
298
+
299
+ VectorStore store(DIM);
300
+
301
+ // Load test data
302
+ std::mt19937 rng(42);
303
+ simdjson::ondemand::parser parser;
304
+
305
+ for (size_t i = 0; i < 10000; ++i) {
306
+ auto embedding = generate_random_embedding(DIM, rng);
307
+ std::string json_str = create_json_document(
308
+ "search-" + std::to_string(i),
309
+ "Document for search testing " + std::to_string(i),
310
+ embedding
311
+ );
312
+
313
+ simdjson::padded_string padded(json_str);
314
+ simdjson::ondemand::document doc;
315
+ if (!parser.iterate(padded).get(doc)) {
316
+ store.add_document(doc);
317
+ }
318
+ }
319
+
320
+ std::cout << " Loaded " << store.size() << " documents\n";
321
+
322
+ // Finalize the store
323
+ auto finalize_start = high_resolution_clock::now();
324
+ store.finalize();
325
+ auto finalize_time = duration_cast<milliseconds>(high_resolution_clock::now() - finalize_start).count();
326
+ std::cout << " Finalized in " << finalize_time << "ms\n";
327
+
328
+ // Test concurrent searches
329
+ const size_t num_threads = 8;
330
+ const size_t searches_per_thread = 100;
331
+ std::atomic<size_t> total_searches{0};
332
+ std::atomic<size_t> total_results{0};
333
+
334
+ auto search_start = high_resolution_clock::now();
335
+
336
+ std::vector<std::thread> searchers;
337
+ for (size_t t = 0; t < num_threads; ++t) {
338
+ searchers.emplace_back([&store, &total_searches, &total_results, t]() {
339
+ std::mt19937 rng(t);
340
+ size_t local_results = 0;
341
+
342
+ for (size_t i = 0; i < searches_per_thread; ++i) {
343
+ auto query = generate_random_embedding(DIM, rng);
344
+ auto results = store.search(query.data(), 10);
345
+ assert(!results.empty() && results.size() <= 10);
346
+ local_results += results.size();
347
+ total_searches++;
348
+ }
349
+
350
+ total_results += local_results;
351
+ });
352
+ }
353
+
354
+ for (auto& t : searchers) {
355
+ t.join();
356
+ }
357
+
358
+ auto search_time = duration_cast<milliseconds>(high_resolution_clock::now() - search_start).count();
359
+
360
+ std::cout << "āœ… Performed " << total_searches.load() << " concurrent searches in " << search_time << "ms\n";
361
+ std::cout << " Average results per search: " << (total_results.load() / total_searches.load()) << "\n";
362
+ std::cout << " Throughput: " << (total_searches.load() * 1000 / search_time) << " searches/sec\n";
363
+ }
364
+
365
+ int main() {
366
+ std::cout << "šŸ”„ Starting concurrent stress tests...\n";
367
+
368
+ // Detect which sanitizer is enabled
369
+ #if defined(__has_feature)
370
+ #if __has_feature(address_sanitizer)
371
+ std::cout << " Running with AddressSanitizer (ASAN)\n";
372
+ #elif __has_feature(thread_sanitizer)
373
+ std::cout << " Running with ThreadSanitizer (TSAN)\n";
374
+ #endif
375
+ #elif defined(__SANITIZE_ADDRESS__)
376
+ std::cout << " Running with AddressSanitizer (ASAN)\n";
377
+ #elif defined(__SANITIZE_THREAD__)
378
+ std::cout << " Running with ThreadSanitizer (TSAN)\n";
379
+ #else
380
+ std::cout << " āš ļø Running without sanitizers - use 'make stress' for ASAN by default\n";
381
+ std::cout << " Or use: make stress SANITIZER=thread for TSAN\n";
382
+ std::cout << " make stress SANITIZER=none to disable\n";
383
+ #endif
384
+
385
+ test_loading_performance();
386
+ test_phase_enforcement();
387
+ // test_oversize_allocation();
388
+ test_alignment_requests();
389
+ test_phase_separation();
390
+ test_concurrent_search_performance();
391
+
392
+ std::cout << "\nāœ… All stress tests passed!\n";
393
+ return 0;
394
+ }