entry : resultSequential) {
+ sequentialIds.add(entry.entityId());
+ sequentialScores.add(entry.score());
+ }
+
+ assertAll(
+ () -> assertTrue(Files.exists(parallelIndexDir.resolve("embeddings.graph"))),
+ () -> assertTrue(Files.exists(parallelIndexDir.resolve("embeddings.meta"))),
+ () -> assertTrue(Files.exists(sequentialIndexDir.resolve("embeddingsSequential.graph"))),
+ () -> assertTrue(Files.exists(sequentialIndexDir.resolve("embeddingsSequential.meta")))
+ );
+
+ // Both indices were built from the same data with the same HNSW parameters,
+ // so search results must be identical.
+ assertEquals(parallelIds, sequentialIds,
+ "Parallel and sequential on-disk writes should produce identical search results");
+ assertEquals(parallelScores, sequentialScores,
+ "Parallel and sequential on-disk writes should produce identical search scores");
+ }
+
+
}
diff --git a/gigamap/jvector/src/test/java/org/eclipse/store/gigamap/jvector/VectorIndicesTest.java b/gigamap/jvector/src/test/java/org/eclipse/store/gigamap/jvector/VectorIndicesTest.java
new file mode 100644
index 00000000..5a6351f6
--- /dev/null
+++ b/gigamap/jvector/src/test/java/org/eclipse/store/gigamap/jvector/VectorIndicesTest.java
@@ -0,0 +1,379 @@
+package org.eclipse.store.gigamap.jvector;
+
+/*-
+ * #%L
+ * EclipseStore GigaMap JVector
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import org.eclipse.store.gigamap.types.GigaMap;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for {@link VectorIndices}.
+ *
+ * Tests the core functionality of vector index management:
+ * - Index registration and retrieval
+ * - Index name validation
+ * - Lifecycle management
+ */
+class VectorIndicesTest
+{
+ record Document(String content, float[] embedding) {}
+
+ static class DocumentVectorizer extends Vectorizer
+ {
+ @Override
+ public float[] vectorize(final Document entity)
+ {
+ return entity.embedding();
+ }
+ }
+
+ @Test
+ void testAddIndex()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ final VectorIndex index = vectorIndices.add("test-index", config, new DocumentVectorizer());
+
+ assertNotNull(index);
+ assertEquals("test-index", index.name());
+ }
+
+ @Test
+ void testAddDuplicateIndexThrows()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ vectorIndices.add("duplicate", config, new DocumentVectorizer());
+
+ assertThrows(RuntimeException.class, () ->
+ vectorIndices.add("duplicate", config, new DocumentVectorizer())
+ );
+ }
+
+ @Test
+ void testGetExistingIndex()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ final VectorIndex created = vectorIndices.add("my-index", config, new DocumentVectorizer());
+ final VectorIndex retrieved = vectorIndices.get("my-index");
+
+ assertSame(created, retrieved);
+ }
+
+ @Test
+ void testGetNonExistentIndexReturnsNull()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ assertNull(vectorIndices.get("non-existent"));
+ }
+
+ @Test
+ void testEnsureCreatesNewIndex()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ final VectorIndex index = vectorIndices.ensure("new-index", config, new DocumentVectorizer());
+
+ assertNotNull(index);
+ assertEquals("new-index", index.name());
+ }
+
+ @Test
+ void testEnsureReturnsExistingIndex()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ final VectorIndex first = vectorIndices.ensure("existing", config, new DocumentVectorizer());
+ final VectorIndex second = vectorIndices.ensure("existing", config, new DocumentVectorizer());
+
+ assertSame(first, second);
+ }
+
+ @Test
+ void testValidateIndexNameNull()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () ->
+ vectorIndices.add(null, config, new DocumentVectorizer())
+ );
+ }
+
+ @Test
+ void testValidateIndexNameEmpty()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () ->
+ vectorIndices.add("", config, new DocumentVectorizer())
+ );
+ }
+
+ @Test
+ void testValidateIndexNameWithSlash()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () ->
+ vectorIndices.add("invalid/name", config, new DocumentVectorizer())
+ );
+ }
+
+ @Test
+ void testValidateIndexNameWithBackslash()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () ->
+ vectorIndices.add("invalid\\name", config, new DocumentVectorizer())
+ );
+ }
+
+ @Test
+ void testValidateIndexNameTooLong()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ final String tooLong = "a".repeat(201);
+
+ assertThrows(IllegalArgumentException.class, () ->
+ vectorIndices.add(tooLong, config, new DocumentVectorizer())
+ );
+ }
+
+ @Test
+ void testValidateIndexNameWithValidCharacters()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ assertDoesNotThrow(() ->
+ vectorIndices.add("valid-index_name.123", config, new DocumentVectorizer())
+ );
+ }
+
+ @Test
+ void testInternalAddPropagates()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ vectorIndices.add("index1", config, new DocumentVectorizer());
+ vectorIndices.add("index2", config, new DocumentVectorizer());
+
+ final Document doc = new Document("test", new float[]{1.0f, 0.0f, 0.0f});
+ gigaMap.add(doc);
+
+ final VectorIndex index1 = vectorIndices.get("index1");
+ final VectorIndex index2 = vectorIndices.get("index2");
+
+ final VectorSearchResult result1 = index1.search(new float[]{1.0f, 0.0f, 0.0f}, 1);
+ final VectorSearchResult result2 = index2.search(new float[]{1.0f, 0.0f, 0.0f}, 1);
+
+ assertEquals(1, result1.size());
+ assertEquals(1, result2.size());
+ }
+
+ @Test
+ void testInternalRemovePropagates()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ vectorIndices.add("index1", config, new DocumentVectorizer());
+ vectorIndices.add("index2", config, new DocumentVectorizer());
+
+ final Document doc = new Document("test", new float[]{1.0f, 0.0f, 0.0f});
+ gigaMap.add(doc);
+ gigaMap.removeById(0);
+
+ final VectorIndex index1 = vectorIndices.get("index1");
+ final VectorIndex index2 = vectorIndices.get("index2");
+
+ final VectorSearchResult result1 = index1.search(new float[]{1.0f, 0.0f, 0.0f}, 1);
+ final VectorSearchResult result2 = index2.search(new float[]{1.0f, 0.0f, 0.0f}, 1);
+
+ assertEquals(0, result1.size());
+ assertEquals(0, result2.size());
+ }
+
+ @Test
+ void testInternalRemoveAllPropagates()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ vectorIndices.add("index1", config, new DocumentVectorizer());
+
+ gigaMap.add(new Document("test1", new float[]{1.0f, 0.0f, 0.0f}));
+ gigaMap.add(new Document("test2", new float[]{0.0f, 1.0f, 0.0f}));
+
+ gigaMap.removeAll();
+
+ final VectorIndex index1 = vectorIndices.get("index1");
+ final VectorSearchResult result = index1.search(new float[]{1.0f, 0.0f, 0.0f}, 10);
+
+ assertEquals(0, result.size());
+ }
+
+ @Test
+ void testIterateIndices()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ vectorIndices.add("index1", config, new DocumentVectorizer());
+ vectorIndices.add("index2", config, new DocumentVectorizer());
+ vectorIndices.add("index3", config, new DocumentVectorizer());
+
+ final int[] count = {0};
+ vectorIndices.iterate(index -> count[0]++);
+
+ assertEquals(3, count[0]);
+ }
+
+ @Test
+ void testAccessIndices()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ vectorIndices.add("index1", config, new DocumentVectorizer());
+ vectorIndices.add("index2", config, new DocumentVectorizer());
+
+ vectorIndices.accessIndices(table -> {
+ assertNotNull(table.get("index1"));
+ assertNotNull(table.get("index2"));
+ assertNull(table.get("non-existent"));
+ });
+ }
+
+ @Test
+ void testIndexAutoPopulatesExistingEntities()
+ {
+ final GigaMap gigaMap = GigaMap.New();
+
+ gigaMap.add(new Document("doc1", new float[]{1.0f, 0.0f, 0.0f}));
+ gigaMap.add(new Document("doc2", new float[]{0.0f, 1.0f, 0.0f}));
+
+ final VectorIndices vectorIndices = gigaMap.index().register(VectorIndices.Category());
+
+ final VectorIndexConfiguration config = VectorIndexConfiguration.builder()
+ .dimension(3)
+ .similarityFunction(VectorSimilarityFunction.COSINE)
+ .build();
+
+ final VectorIndex index = vectorIndices.add("new-index", config, new DocumentVectorizer());
+
+ final VectorSearchResult result = index.search(new float[]{1.0f, 0.0f, 0.0f}, 10);
+
+ assertEquals(2, result.size(), "Index should auto-populate with existing entities");
+ }
+}
+