Skip to content

Commit 71cc760

Browse files
committed
initial version from poc
1 parent d5e59a8 commit 71cc760

File tree

20 files changed

+2202
-29
lines changed

20 files changed

+2202
-29
lines changed

fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/Config.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package com.apple.foundationdb.async.hnsw;
2222

2323
import com.apple.foundationdb.linear.Metric;
24+
import com.google.common.base.Preconditions;
2425
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2526

2627
import javax.annotation.Nonnull;
@@ -88,19 +89,20 @@ public final class Config {
8889
* This attribute (named {@code M_max} by the HNSW paper) is the maximum connectivity value for nodes stored on a
8990
* layer greater than {@code 0}. We will never create more that {@code mMax} neighbors for a node. That means that
9091
* we even prune the neighbors of a node if the actual number of neighbors would otherwise exceed {@code mMax}.
92+
* Note that this attribute must be greater than or equal to {@link #m}.
9193
*/
9294
private final int mMax;
9395

9496
/**
9597
* This attribute (named {@code M_max0} by the HNSW paper) is the maximum connectivity value for nodes stored on
9698
* layer {@code 0}. We will never create more that {@code mMax0} neighbors for a node that is stored on that layer.
9799
* That means that we even prune the neighbors of a node if the actual number of neighbors would otherwise exceed
98-
* {@code mMax0}.
100+
* {@code mMax0}. Note that this attribute must be greater than or equal to {@link #mMax}.
99101
*/
100102
private final int mMax0;
101103

102104
/**
103-
* Maximum size of the search queues (on independent queue per layer) that are used during the insertion of a new
105+
* Maximum size of the search queues (one independent queue per layer) that are used during the insertion of a new
104106
* node. If {@code efConstruction} is set to {@code 1}, the search naturally follows a greedy approach
105107
* (monotonous descent), whereas a high number for {@code efConstruction} allows for a more nuanced search that can
106108
* tolerate (false) local minima.
@@ -172,6 +174,8 @@ private Config(final long randomSeed, @Nonnull final Metric metric, final int nu
172174
final double sampleVectorStatsProbability, final double maintainStatsProbability,
173175
final int statsThreshold, final boolean useRaBitQ, final int raBitQNumExBits,
174176
final int maxNumConcurrentNodeFetches, final int maxNumConcurrentNeighborhoodFetches) {
177+
Preconditions.checkArgument(m <= mMax);
178+
Preconditions.checkArgument(mMax <= mMax0);
175179
this.randomSeed = randomSeed;
176180
this.metric = metric;
177181
this.numDimensions = numDimensions;

fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/StorageAdapter.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
import com.apple.foundationdb.async.AsyncUtil;
3030
import com.apple.foundationdb.linear.AffineOperator;
3131
import com.apple.foundationdb.linear.DoubleRealVector;
32-
import com.apple.foundationdb.linear.FloatRealVector;
33-
import com.apple.foundationdb.linear.HalfRealVector;
3432
import com.apple.foundationdb.linear.Quantizer;
3533
import com.apple.foundationdb.linear.RealVector;
3634
import com.apple.foundationdb.linear.Transformed;
@@ -59,7 +57,6 @@
5957
* @param <N> the type of {@link NodeReference} this storage adapter manages
6058
*/
6159
interface StorageAdapter<N extends NodeReference> {
62-
ImmutableList<VectorType> VECTOR_TYPES = ImmutableList.copyOf(VectorType.values());
6360

6461
/**
6562
* Subspace for data.
@@ -199,29 +196,24 @@ static RealVector vectorFromTuple(@Nonnull final Config config, @Nonnull final T
199196
/**
200197
* Creates a {@link RealVector} from a byte array.
201198
* <p>
202-
* This method interprets the input byte array by interpreting the first byte of the array as the precision shift.
203-
* The byte array must have the proper size, i.e. the invariant {@code (bytesLength - 1) % precision == 0} must
204-
* hold.
199+
* This method interprets the input byte array by interpreting the first byte of the array.
200+
* It the delegates to {@link RealVector#fromBytes(VectorType, byte[])}.
205201
* @param config an HNSW config
206202
* @param vectorBytes the non-null byte array to convert.
207203
* @return a new {@link RealVector} instance created from the byte array.
208-
* @throws com.google.common.base.VerifyException if the length of {@code vectorBytes} does not meet the invariant
209-
* {@code (bytesLength - 1) % precision == 0}
210204
*/
211205
@Nonnull
212206
static RealVector vectorFromBytes(@Nonnull final Config config, @Nonnull final byte[] vectorBytes) {
213207
final byte vectorTypeOrdinal = vectorBytes[0];
214-
switch (fromVectorTypeOrdinal(vectorTypeOrdinal)) {
215-
case HALF:
216-
return HalfRealVector.fromBytes(vectorBytes);
217-
case SINGLE:
218-
return FloatRealVector.fromBytes(vectorBytes);
219-
case DOUBLE:
220-
return DoubleRealVector.fromBytes(vectorBytes);
208+
switch (RealVector.fromVectorTypeOrdinal(vectorTypeOrdinal)) {
221209
case RABITQ:
222210
Verify.verify(config.isUseRaBitQ());
223211
return EncodedRealVector.fromBytes(vectorBytes, config.getNumDimensions(),
224212
config.getRaBitQNumExBits());
213+
case HALF:
214+
case SINGLE:
215+
case DOUBLE:
216+
return RealVector.fromBytes(vectorBytes);
225217
default:
226218
throw new RuntimeException("unable to serialize vector");
227219
}
@@ -251,11 +243,6 @@ static Tuple tupleFromVector(@Nonnull final RealVector vector) {
251243
return Tuple.from(vector.getRawData());
252244
}
253245

254-
@Nonnull
255-
static VectorType fromVectorTypeOrdinal(final int ordinal) {
256-
return VECTOR_TYPES.get(ordinal);
257-
}
258-
259246
@Nonnull
260247
static CompletableFuture<AccessInfo> fetchAccessInfo(@Nonnull final Config config,
261248
@Nonnull final ReadTransaction readTransaction,

fdb-extensions/src/main/java/com/apple/foundationdb/linear/RealVector.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020

2121
package com.apple.foundationdb.linear;
2222

23-
import com.google.common.base.Preconditions;
2423
import com.apple.foundationdb.half.Half;
24+
import com.google.common.base.Preconditions;
25+
import com.google.common.collect.ImmutableList;
2526

2627
import javax.annotation.Nonnull;
2728

@@ -34,6 +35,8 @@
3435
* data type conversions and raw data representation.
3536
*/
3637
public interface RealVector {
38+
ImmutableList<VectorType> VECTOR_TYPES = ImmutableList.copyOf(VectorType.values());
39+
3740
/**
3841
* Returns the number of elements in the vector, i.e. the number of dimensions.
3942
* @return the number of dimensions
@@ -189,4 +192,47 @@ default RealVector multiply(final double scalarFactor) {
189192
}
190193
return withData(result);
191194
}
195+
196+
@Nonnull
197+
static VectorType fromVectorTypeOrdinal(final int ordinal) {
198+
return VECTOR_TYPES.get(ordinal);
199+
}
200+
201+
/**
202+
* Creates a {@link RealVector} from a byte array.
203+
* <p>
204+
* This method interprets the input byte array by interpreting the first byte of the array as the type of vector.
205+
* It then delegates to {@link #fromBytes(VectorType, byte[])} to do the actual deserialization.
206+
*
207+
* @param vectorBytes the non-null byte array to convert.
208+
* @return a new {@link RealVector} instance created from the byte array.
209+
*/
210+
@Nonnull
211+
static RealVector fromBytes(@Nonnull final byte[] vectorBytes) {
212+
final byte vectorTypeOrdinal = vectorBytes[0];
213+
return fromBytes(fromVectorTypeOrdinal(vectorTypeOrdinal), vectorBytes);
214+
}
215+
216+
/**
217+
* Creates a {@link RealVector} from a byte array.
218+
* <p>
219+
* This implementation dispatches to the actual logic that deserialize a byte array to a vector which is located in
220+
* the respective implementations of {@link RealVector}.
221+
* @param vectorType the vector type of the serialized vector
222+
* @param vectorBytes the non-null byte array to convert.
223+
* @return a new {@link RealVector} instance created from the byte array.
224+
*/
225+
@Nonnull
226+
static RealVector fromBytes(@Nonnull final VectorType vectorType, @Nonnull final byte[] vectorBytes) {
227+
switch (vectorType) {
228+
case HALF:
229+
return HalfRealVector.fromBytes(vectorBytes);
230+
case SINGLE:
231+
return FloatRealVector.fromBytes(vectorBytes);
232+
case DOUBLE:
233+
return DoubleRealVector.fromBytes(vectorBytes);
234+
default:
235+
throw new RuntimeException("unable to serialize vector");
236+
}
237+
}
192238
}

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/Index.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@ public Index(@Nonnull Index orig, @Nullable final IndexPredicate predicate) {
190190
this.lastModifiedVersion = orig.lastModifiedVersion;
191191
}
192192

193+
/**
194+
* Copy constructor. This will create an index that is identical to the current <code>Index</code> with a given
195+
* set of index options.
196+
* @param orig original index to copy
197+
* @param indexOptions the index options.
198+
*/
199+
public Index(@Nonnull Index orig, @Nonnull final Map<String, String> indexOptions) {
200+
this(orig.name, orig.rootExpression, orig.type, ImmutableMap.copyOf(indexOptions), orig.predicate);
201+
if (orig.primaryKeyComponentPositions != null) {
202+
this.primaryKeyComponentPositions = Arrays.copyOf(orig.primaryKeyComponentPositions, orig.primaryKeyComponentPositions.length);
203+
} else {
204+
this.primaryKeyComponentPositions = null;
205+
}
206+
this.subspaceKey = normalizeSubspaceKey(name, orig.subspaceKey);
207+
this.useExplicitSubspaceKey = orig.useExplicitSubspaceKey;
208+
this.addedVersion = orig.addedVersion;
209+
this.lastModifiedVersion = orig.lastModifiedVersion;
210+
}
211+
193212
@SuppressWarnings({"deprecation", "squid:CallToDeprecatedMethod", "java:S3776"}) // Old (deprecated) index type needs grouping compatibility
194213
@SpotBugsSuppressWarnings("NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
195214
public Index(@Nonnull RecordMetaDataProto.Index proto) throws KeyExpression.DeserializationException {

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/IndexOptions.java

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package com.apple.foundationdb.record.metadata;
2222

2323
import com.apple.foundationdb.annotation.API;
24+
import com.apple.foundationdb.async.hnsw.Config;
2425
import com.apple.foundationdb.async.rtree.RTree;
2526
import com.apple.foundationdb.record.provider.foundationdb.IndexMaintainer;
2627

@@ -223,6 +224,143 @@ public class IndexOptions {
223224
*/
224225
public static final String RTREE_USE_NODE_SLOT_INDEX = "rtreeUseNodeSlotIndex";
225226

227+
/**
228+
* HNSW-only: The random seed that is used to probabilistically determine the highest layer of an insert into an
229+
* HNSW structure. See {@link Config#getRandomSeed()}. The default random seed is
230+
* {@link Config#DEFAULT_RANDOM_SEED}.
231+
*/
232+
public static final String HNSW_RANDOM_SEED = "hnswRandomSeed";
233+
234+
/**
235+
* HNSW-only: The metric that is used to determine distances between vectors. The default metric is
236+
* {@link Config#DEFAULT_METRIC}. See {@link Config#getMetric()}.
237+
*/
238+
public static final String HNSW_METRIC = "hnswMetric";
239+
240+
/**
241+
* HNSW-only: The number of dimensions used. All vectors must have exactly this number of dimensions. This option
242+
* must be set when interacting with a vector index as it there is no default.
243+
* See {@link Config#getNumDimensions()}.
244+
*/
245+
public static final String HNSW_NUM_DIMENSIONS = "hnswNumDimensions";
246+
247+
/**
248+
* HNSW-only: Indicator if all layers except layer {@code 0} use inlining. If inlining is used, each node is
249+
* persisted as a key/value pair per neighbor which includes the vectors of the neighbors but not for itself. If
250+
* inlining is not used, each node is persisted as exactly one key/value pair per node which stores its own vector
251+
* but specifically excludes the vectors of the neighbors. The default value is set to
252+
* {@link Config#DEFAULT_USE_INLINING}. See {@link Config#isUseInlining()}.
253+
*/
254+
public static final String HNSW_USE_INLINING = "hnswUseInlining";
255+
256+
/**
257+
* HNSW-only: This option (named {@code M} by the HNSW paper) is the connectivity value for all nodes stored on
258+
* any layer. While by no means enforced or even enforceable, we strive to create and maintain exactly {@code m}
259+
* neighbors for a node. Due to insert/delete operations it is possible that the actual number of neighbors a node
260+
* references is not exactly {@code m} at any given time. The default value is set to {@link Config#DEFAULT_M}.
261+
* See {@link Config#getM()}.
262+
*/
263+
public static final String HNSW_M = "hnswM";
264+
265+
/**
266+
* HNSW-only: This attribute (named {@code M_max} by the HNSW paper) is the maximum connectivity value for nodes
267+
* stored on a layer greater than {@code 0}. A node can never have more that {@code mMax} neighbors. That means that
268+
* neighbors of a node are pruned if the actual number of neighbors would otherwise exceed {@code mMax}. Note that
269+
* this option must be greater than or equal to {@link #HNSW_M}. The default value is set to
270+
* {@link Config#DEFAULT_M_MAX}. See {@link Config#getMMax()}.
271+
*/
272+
public static final String HNSW_M_MAX = "hnswMax";
273+
274+
/**
275+
* HNSW-only: This option (named {@code M_max0} by the HNSW paper) is the maximum connectivity value for nodes
276+
* stored on layer {@code 0}. We will never create more that {@code mMax0} neighbors for a node that is stored on
277+
* that layer. That means that we even prune the neighbors of a node if the actual number of neighbors would
278+
* otherwise exceed {@code mMax0}. Note that this option must be greater than or equal to {@link #HNSW_M_MAX}.
279+
* The default value is set to {@link Config#DEFAULT_M_MAX_0}. See {@link Config#getMMax0()}.
280+
*/
281+
public static final String HNSW_M_MAX_0 = "hnswMax0";
282+
283+
/**
284+
* HNSW-only: Maximum size of the search queues (one independent queue per layer) that are used during the insertion
285+
* of a new node. If {@code HNSW_EF_CONSTRUCTION} is set to {@code 1}, the search naturally follows a greedy
286+
* approach (monotonous descent), whereas a high number for {@code HNSW_EF_CONSTRUCTION} allows for a more nuanced
287+
* search that can tolerate (false) local minima. The default value is set to {@link Config#DEFAULT_EF_CONSTRUCTION}.
288+
* See {@link Config#getEfConstruction()}.
289+
*/
290+
public static final String HNSW_EF_CONSTRUCTION = "hnswEfConstruction";
291+
292+
/**
293+
* HNSW-only: Indicator to signal if, during the insertion of a node, the set of nearest neighbors of that node is
294+
* to be extended by the actual neighbors of those neighbors to form a set of candidates that the new node may be
295+
* connected to during the insert operation. The default value is set to {@link Config#DEFAULT_EXTEND_CANDIDATES}.
296+
* See {@link Config#isExtendCandidates()}.
297+
*/
298+
public static final String HNSW_EXTEND_CANDIDATES = "hnswExtendCandidates";
299+
300+
/**
301+
* HNSW-only: Indicator to signal if, during the insertion of a node, candidates that have been discarded due to not
302+
* satisfying the select-neighbor heuristic may get added back in to pad the set of neighbors if the new node would
303+
* otherwise have too few neighbors (see {@link Config#getM()}). The default value is set to
304+
* {@link Config#DEFAULT_KEEP_PRUNED_CONNECTIONS}. See {@link Config#isKeepPrunedConnections()}.
305+
*/
306+
public static final String HNSW_KEEP_PRUNED_CONNECTIONS = "hnswKeepPrunedConnections";
307+
308+
/**
309+
* HNSW-only: If sampling is necessary (currently iff {@link #HNSW_USE_RABITQ} is {@code "true"}), this option
310+
* represents the probability of a vector being inserted to also be written into the samples subspace of the hnsw
311+
* structure. The vectors in that subspace are continuously aggregated until a total {@link #HNSW_STATS_THRESHOLD}
312+
* has been reached. The default value is set to {@link Config#DEFAULT_SAMPLE_VECTOR_STATS_PROBABILITY}. See
313+
* {@link Config#getSampleVectorStatsProbability()}.
314+
*/
315+
public static final String HNSW_SAMPLE_VECTOR_STATS_PROBABILITY = "hnswSampleVectorStatsProbability";
316+
317+
/**
318+
* HNSW-only: If sampling is necessary (currently iff {@link #HNSW_USE_RABITQ} is {@code "true"}), this option
319+
* represents the probability of the samples subspace to be further aggregated (rolled-up) when a new vector is
320+
* inserted. The vectors in that subspace are continuously aggregated until a total
321+
* {@link #HNSW_STATS_THRESHOLD} has been reached. The default value is set to
322+
* {@link Config#DEFAULT_MAINTAIN_STATS_PROBABILITY}. See {@link Config#getMaintainStatsProbability()}.
323+
*/
324+
public static final String HNSW_MAINTAIN_STATS_PROBABILITY = "hnswMaintainStatsProbability";
325+
326+
/**
327+
* HNSW-only: If sampling is necessary (currently iff {@link #HNSW_USE_RABITQ} is {@code "true"}), this option
328+
* represents the threshold (being a number of vectors) that when reached causes the stats maintenance logic to
329+
* compute the actual statistics (currently the centroid of the vectors that have been inserted to far). The result
330+
* is then inserted into the access info subspace of the index. The default value is set to
331+
* {@link Config#DEFAULT_STATS_THRESHOLD}. See {@link Config#getStatsThreshold()}.
332+
*/
333+
public static final String HNSW_STATS_THRESHOLD = "hnswStatsThreshold";
334+
335+
/**
336+
* HNSW-only: Indicator if we should RaBitQ quantization. See {@link com.apple.foundationdb.rabitq.RaBitQuantizer}
337+
* for more details. The default value is set to {@link Config#DEFAULT_USE_RABITQ}.
338+
* See {@link Config#isUseRaBitQ()}.
339+
*/
340+
public static final String HNSW_USE_RABITQ = "hnswUseRaBitQ";
341+
342+
/**
343+
* HNSW-only: Number of bits per dimensions iff {@link #HNSW_USE_RABITQ} is set to {@code "true"}, ignored
344+
* otherwise. If RaBitQ encoding is used, a vector is stored using roughly
345+
* {@code 25 + numDimensions * (numExBits + 1) / 8} bytes. The default value is set to
346+
* {@link Config#DEFAULT_RABITQ_NUM_EX_BITS}. See {@link Config#getRaBitQNumExBits()}.
347+
*/
348+
public static final String HNSW_RABITQ_NUM_EX_BITS = "hnswRaBitQNumExBits";
349+
350+
/**
351+
* HNSW-only: Maximum number of concurrent node fetches during search and modification operations. The default value
352+
* is set to {@link Config#DEFAULT_MAX_NUM_CONCURRENT_NODE_FETCHES}.
353+
* See {@link Config#getMaxNumConcurrentNodeFetches()}.
354+
*/
355+
public static final String HNSW_MAX_NUM_CONCURRENT_NODE_FETCHES = "hnswMaxNumConcurrentNodeFetches";
356+
357+
/**
358+
* HNSW-only: Maximum number of concurrent neighborhood fetches during modification operations when the neighbors
359+
* are pruned. The default value is set to {@link Config#DEFAULT_MAX_NUM_CONCURRENT_NEIGHBOR_FETCHES}.
360+
* See {@link Config#getMaxNumConcurrentNeighborhoodFetches()}.
361+
*/
362+
public static final String HNSW_MAX_NUM_CONCURRENT_NEIGHBORHOOD_FETCHES = "hnswMaxNumConcurrentNeighborhoodFetches";
363+
226364
private IndexOptions() {
227365
}
228366
}

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/metadata/IndexTypes.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ public class IndexTypes {
164164
*/
165165
public static final String MULTIDIMENSIONAL = "multidimensional";
166166

167+
/**
168+
* An index using an HNSW structure.
169+
*/
170+
public static final String VECTOR = "vector";
171+
167172
private IndexTypes() {
168173
}
169174
}

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/FDBStoreTimer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,14 @@ public enum Counts implements Count {
761761
LOCKS_ATTEMPTED("number of attempts to register a lock", false),
762762
/** Count of the locks released. */
763763
LOCKS_RELEASED("number of locks released", false),
764+
VECTOR_NODE_READS("intermediate nodes read", false),
765+
VECTOR_NODE_READ_BYTES("intermediate node bytes read", true),
766+
VECTOR_NODE0_READS("intermediate nodes read", false),
767+
VECTOR_NODE0_READ_BYTES("intermediate node bytes read", true),
768+
VECTOR_NODE_WRITES("intermediate nodes written", false),
769+
VECTOR_NODE_WRITE_BYTES("intermediate node bytes written", true),
770+
VECTOR_NODE0_WRITES("intermediate nodes written", false),
771+
VECTOR_NODE0_WRITE_BYTES("intermediate node bytes written", true),
764772
;
765773

766774
private final String title;

0 commit comments

Comments
 (0)