diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AbstractNode.java b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AbstractNode.java
new file mode 100644
index 0000000000..1a40dfedbb
--- /dev/null
+++ b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AbstractNode.java
@@ -0,0 +1,101 @@
+/*
+ * AbstractNode.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.apple.foundationdb.async.hnsw;
+
+import com.apple.foundationdb.tuple.Tuple;
+import com.google.common.collect.ImmutableList;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+/**
+ * An abstract base class implementing the {@link Node} interface.
+ *
+ * This class provides the fundamental structure for a node within the HNSW graph,
+ * managing a unique {@link Tuple} primary key and an immutable list of its neighbors.
+ * Subclasses are expected to provide concrete implementations, potentially adding
+ * more state or behavior.
+ *
+ * @param the type of the node reference used for neighbors, which must extend {@link NodeReference}
+ */
+abstract class AbstractNode implements Node {
+ @Nonnull
+ private final Tuple primaryKey;
+
+ @Nonnull
+ private final List neighbors;
+
+ /**
+ * Constructs a new {@code AbstractNode} with a specified primary key and a list of neighbors.
+ *
+ * @param primaryKey the unique identifier for this node; must not be {@code null}
+ * @param neighbors the list of nodes connected to this node; must not be {@code null}
+ */
+ protected AbstractNode(@Nonnull final Tuple primaryKey,
+ @Nonnull final List neighbors) {
+ this.primaryKey = primaryKey;
+ this.neighbors = ImmutableList.copyOf(neighbors);
+ }
+
+ /**
+ * Gets the primary key that uniquely identifies this object.
+ * @return the primary key {@link Tuple}, which will never be {@code null}.
+ */
+ @Nonnull
+ @Override
+ public Tuple getPrimaryKey() {
+ return primaryKey;
+ }
+
+ /**
+ * Gets the list of neighbors connected to this node.
+ *
+ * This method returns a direct reference to the internal list which is
+ * immutable.
+ * @return a non-null, possibly empty, list of neighbors.
+ */
+ @Nonnull
+ @Override
+ public List getNeighbors() {
+ return neighbors;
+ }
+
+
+ /**
+ * Converts this node into its {@link CompactNode} representation.
+ *
+ * A {@code CompactNode} is a space-efficient implementation {@code Node}. This method provides the
+ * conversion logic to transform the current object into that compact form.
+ *
+ * @return a non-null {@link CompactNode} representing the current node.
+ */
+ @Nonnull
+ public abstract CompactNode asCompactNode();
+
+ /**
+ * Converts this node into its {@link InliningNode} representation.
+ * @return this object cast to an {@link InliningNode}; never {@code null}.
+ * @throws ClassCastException if this object is not actually an instance of
+ * {@link InliningNode}.
+ */
+ @Nonnull
+ public abstract InliningNode asInliningNode();
+}
diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AbstractStorageAdapter.java b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AbstractStorageAdapter.java
new file mode 100644
index 0000000000..84e7db99ab
--- /dev/null
+++ b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AbstractStorageAdapter.java
@@ -0,0 +1,236 @@
+/*
+ * AbstractStorageAdapter.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.apple.foundationdb.async.hnsw;
+
+import com.apple.foundationdb.ReadTransaction;
+import com.apple.foundationdb.Transaction;
+import com.apple.foundationdb.linear.AffineOperator;
+import com.apple.foundationdb.linear.Quantizer;
+import com.apple.foundationdb.subspace.Subspace;
+import com.apple.foundationdb.tuple.Tuple;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * An abstract base class for {@link StorageAdapter} implementations.
+ *
+ * This class provides the common infrastructure for managing HNSW graph data within a {@link Subspace}.
+ * It handles the configuration, node creation, and listener management, while delegating the actual
+ * storage-specific read and write operations to concrete subclasses through the {@code fetchNodeInternal}
+ * and {@code writeNodeInternal} abstract methods.
+ *
+ * @param the type of {@link NodeReference} used to reference nodes in the graph
+ */
+abstract class AbstractStorageAdapter implements StorageAdapter {
+ @Nonnull
+ private static final Logger logger = LoggerFactory.getLogger(AbstractStorageAdapter.class);
+
+ @Nonnull
+ private final Config config;
+ @Nonnull
+ private final NodeFactory nodeFactory;
+ @Nonnull
+ private final Subspace subspace;
+ @Nonnull
+ private final OnWriteListener onWriteListener;
+ @Nonnull
+ private final OnReadListener onReadListener;
+
+ @Nonnull
+ private final Subspace dataSubspace;
+
+ /**
+ * Constructs a new {@code AbstractStorageAdapter}.
+ *
+ * This constructor initializes the adapter with the necessary configuration,
+ * factories, and listeners for managing an HNSW graph. It also sets up a
+ * dedicated data subspace within the provided main subspace for storing node data.
+ *
+ * @param config the HNSW graph configuration
+ * @param nodeFactory the factory to create new nodes of type {@code }
+ * @param subspace the primary subspace for storing all graph-related data
+ * @param onWriteListener the listener to be called on write operations
+ * @param onReadListener the listener to be called on read operations
+ */
+ protected AbstractStorageAdapter(@Nonnull final Config config, @Nonnull final NodeFactory nodeFactory,
+ @Nonnull final Subspace subspace,
+ @Nonnull final OnWriteListener onWriteListener,
+ @Nonnull final OnReadListener onReadListener) {
+ this.config = config;
+ this.nodeFactory = nodeFactory;
+ this.subspace = subspace;
+ this.onWriteListener = onWriteListener;
+ this.onReadListener = onReadListener;
+ this.dataSubspace = subspace.subspace(Tuple.from(SUBSPACE_PREFIX_DATA));
+ }
+
+ @Override
+ @Nonnull
+ public Config getConfig() {
+ return config;
+ }
+
+ @Nonnull
+ @Override
+ public NodeFactory getNodeFactory() {
+ return nodeFactory;
+ }
+
+ @Override
+ @Nonnull
+ public Subspace getSubspace() {
+ return subspace;
+ }
+
+ /**
+ * Gets the cached subspace for the data associated with this component.
+ *
+ * The data subspace defines the portion of the directory space where the data
+ * for this component is stored.
+ *
+ * @return the non-null {@link Subspace} for the data
+ */
+ @Override
+ @Nonnull
+ public Subspace getDataSubspace() {
+ return dataSubspace;
+ }
+
+ @Override
+ @Nonnull
+ public OnWriteListener getOnWriteListener() {
+ return onWriteListener;
+ }
+
+ @Override
+ @Nonnull
+ public OnReadListener getOnReadListener() {
+ return onReadListener;
+ }
+
+ /**
+ * Asynchronously fetches a node from a specific layer of the HNSW.
+ *
+ * The node is identified by its {@code layer} and {@code primaryKey}. The entire fetch operation is
+ * performed within the given {@link ReadTransaction}. After the underlying
+ * fetch operation completes, the retrieved node is validated by the
+ * {@link #checkNode(Node)} method before the returned future is completed.
+ *
+ * @param readTransaction the non-null transaction to use for the read operation
+ * @param storageTransform an affine vector transformation operator that is used to transform the fetched vector
+ * into the storage space that is currently being used
+ * @param layer the layer of the tree from which to fetch the node
+ * @param primaryKey the non-null primary key that identifies the node to fetch
+ *
+ * @return a {@link CompletableFuture} that will complete with the fetched {@link AbstractNode}
+ * once it has been read from storage and validated
+ */
+ @Nonnull
+ @Override
+ public CompletableFuture> fetchNode(@Nonnull final ReadTransaction readTransaction,
+ @Nonnull final AffineOperator storageTransform,
+ int layer, @Nonnull Tuple primaryKey) {
+ return fetchNodeInternal(readTransaction, storageTransform, layer, primaryKey).thenApply(this::checkNode);
+ }
+
+ /**
+ * Asynchronously fetches a specific node from the data store for a given layer and primary key.
+ *
+ * This is an internal, abstract method that concrete subclasses must implement to define
+ * the storage-specific logic for retrieving a node. The operation is performed within the
+ * context of the provided {@link ReadTransaction}.
+ *
+ * @param readTransaction the transaction to use for the read operation; must not be {@code null}
+ * @param storageTransform an affine vector transformation operator that is used to transform the fetched vector
+ * into the storage space that is currently being used
+ * @param layer the layer index from which to fetch the node
+ * @param primaryKey the primary key that uniquely identifies the node to be fetched; must not be {@code null}
+ *
+ * @return a {@link CompletableFuture} that will be completed with the fetched {@link AbstractNode}.
+ * The future will complete with {@code null} if no node is found for the given key and layer.
+ */
+ @Nonnull
+ protected abstract CompletableFuture> fetchNodeInternal(@Nonnull ReadTransaction readTransaction,
+ @Nonnull AffineOperator storageTransform,
+ int layer, @Nonnull Tuple primaryKey);
+
+ /**
+ * Method to perform basic invariant check(s) on a newly-fetched node.
+ *
+ * @param node the node to check
+ * was passed in
+ *
+ * @return the node that was passed in
+ */
+ @Nullable
+ private > T checkNode(@Nullable final T node) {
+ return node;
+ }
+
+ /**
+ * Writes a given node and its neighbor modifications to the underlying storage.
+ *
+ * This operation is executed within the context of the provided {@link Transaction}.
+ * It handles persisting the node's data at a specific {@code layer} and applies
+ * the changes to its neighbors as defined in the {@link NeighborsChangeSet}.
+ * This method delegates the core writing logic to an internal method and provides
+ * debug logging upon completion.
+ *
+ * @param transaction the non-null {@link Transaction} context for this write operation
+ * @param quantizer the quantizer to use
+ * @param node the non-null {@link Node} to be written to storage
+ * @param layer the layer index where the node is being written
+ * @param changeSet the non-null {@link NeighborsChangeSet} detailing the modifications
+ * to the node's neighbors
+ */
+ @Override
+ public void writeNode(@Nonnull final Transaction transaction, @Nonnull final Quantizer quantizer,
+ @Nonnull final AbstractNode node, final int layer,
+ @Nonnull final NeighborsChangeSet changeSet) {
+ writeNodeInternal(transaction, quantizer, node, layer, changeSet);
+ if (logger.isTraceEnabled()) {
+ logger.trace("written node with key={} at layer={}", node.getPrimaryKey(), layer);
+ }
+ }
+
+ /**
+ * Writes a single node to the data store as part of a larger transaction.
+ *
+ * This is an abstract method that concrete implementations must provide.
+ * It is responsible for the low-level persistence of the given {@code node} at a
+ * specific {@code layer}. The implementation should also handle the modifications
+ * to the node's neighbors, as detailed in the {@code changeSet}.
+ *
+ * @param transaction the non-null transaction context for the write operation
+ * @param quantizer the quantizer to use
+ * @param node the non-null {@link Node} to write
+ * @param layer the layer or level of the node in the structure
+ * @param changeSet the non-null {@link NeighborsChangeSet} detailing additions or
+ * removals of neighbor links
+ */
+ protected abstract void writeNodeInternal(@Nonnull Transaction transaction, @Nonnull Quantizer quantizer,
+ @Nonnull AbstractNode node, int layer,
+ @Nonnull NeighborsChangeSet changeSet);
+}
diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AccessInfo.java b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AccessInfo.java
new file mode 100644
index 0000000000..792012796b
--- /dev/null
+++ b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AccessInfo.java
@@ -0,0 +1,111 @@
+/*
+ * AccessInfo.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.apple.foundationdb.async.hnsw;
+
+import com.apple.foundationdb.linear.RealVector;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Objects;
+
+/**
+ * Class to capture the current state of this HNSW that cannot be expressed as metadata but that also is not the actual
+ * data that is inserted, organized and retrieved. For instance, an HNSW needs to keep track of the entry point that
+ * resides in the highest layer(currently). Another example is any information that pertains to coordinate system
+ * transformations that have to be carried out prior/posterior to inserting/retrieving an item into/from the HNSW.
+ */
+class AccessInfo {
+ /**
+ * The current entry point. All searches start here.
+ */
+ @Nonnull
+ private final EntryNodeReference entryNodeReference;
+
+ /**
+ * A seed that can be used to reconstruct a random rotator {@link com.apple.foundationdb.linear.FhtKacRotator} used
+ * in ({@link StorageTransform}.
+ */
+ private final long rotatorSeed;
+
+ /**
+ * The negated centroid that is usually derived as an average over some vectors seen so far. It is used to create
+ * the {@link StorageTransform}. The centroid is stored in its negated form (i.e. {@code centroid * (-1)}) as the
+ * {@link com.apple.foundationdb.linear.AffineOperator} adds its translation vector but the centroid needs to be
+ * subtracted.
+ */
+ @Nullable
+ private final RealVector negatedCentroid;
+
+ public AccessInfo(@Nonnull final EntryNodeReference entryNodeReference, final long rotatorSeed,
+ @Nullable final RealVector negatedCentroid) {
+ this.entryNodeReference = entryNodeReference;
+ this.rotatorSeed = rotatorSeed;
+ this.negatedCentroid = negatedCentroid;
+ }
+
+ @Nonnull
+ public EntryNodeReference getEntryNodeReference() {
+ return entryNodeReference;
+ }
+
+ public boolean canUseRaBitQ() {
+ return getNegatedCentroid() != null;
+ }
+
+ public long getRotatorSeed() {
+ return rotatorSeed;
+ }
+
+ @Nullable
+ public RealVector getNegatedCentroid() {
+ return negatedCentroid;
+ }
+
+ @Nonnull
+ public AccessInfo withNewEntryNodeReference(@Nonnull final EntryNodeReference entryNodeReference) {
+ return new AccessInfo(entryNodeReference, getRotatorSeed(), getNegatedCentroid());
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!(o instanceof AccessInfo)) {
+ return false;
+ }
+ final AccessInfo that = (AccessInfo)o;
+ return rotatorSeed == that.rotatorSeed &&
+ Objects.equals(entryNodeReference, that.entryNodeReference) &&
+ Objects.equals(negatedCentroid, that.negatedCentroid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(entryNodeReference, rotatorSeed, negatedCentroid);
+ }
+
+ @Nonnull
+ @Override
+ public String toString() {
+ return "AccessInfo[" +
+ "entryNodeReference=" + entryNodeReference +
+ ", rotatorSeed=" + rotatorSeed +
+ ", centroid=" + negatedCentroid + "]";
+ }
+}
diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AggregatedVector.java b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AggregatedVector.java
new file mode 100644
index 0000000000..b47dcd1131
--- /dev/null
+++ b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/AggregatedVector.java
@@ -0,0 +1,70 @@
+/*
+ * AggregatedVector.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.apple.foundationdb.async.hnsw;
+
+import com.apple.foundationdb.linear.RealVector;
+import com.apple.foundationdb.linear.Transformed;
+
+import javax.annotation.Nonnull;
+import java.util.Objects;
+
+/**
+ * A record-like class wrapping a {@link RealVector} and a count. This data structure is used to keep a running sum
+ * of many vectors in order to compute their centroid at a later time.
+ */
+class AggregatedVector {
+ private final int partialCount;
+ @Nonnull
+ private final Transformed partialVector;
+
+ public AggregatedVector(final int partialCount, @Nonnull final Transformed partialVector) {
+ this.partialCount = partialCount;
+ this.partialVector = partialVector;
+ }
+
+ public int getPartialCount() {
+ return partialCount;
+ }
+
+ @Nonnull
+ public Transformed getPartialVector() {
+ return partialVector;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!(o instanceof AggregatedVector)) {
+ return false;
+ }
+ final AggregatedVector that = (AggregatedVector)o;
+ return partialCount == that.partialCount && Objects.equals(partialVector, that.partialVector);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(partialCount, partialVector);
+ }
+
+ @Override
+ public String toString() {
+ return "AggregatedVector[" + partialCount + ", " + partialVector + "]";
+ }
+}
diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/BaseNeighborsChangeSet.java b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/BaseNeighborsChangeSet.java
new file mode 100644
index 0000000000..490b4bc844
--- /dev/null
+++ b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/BaseNeighborsChangeSet.java
@@ -0,0 +1,96 @@
+/*
+ * BaseNeighborsChangeSet.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.apple.foundationdb.async.hnsw;
+
+import com.apple.foundationdb.Transaction;
+import com.apple.foundationdb.linear.Quantizer;
+import com.apple.foundationdb.tuple.Tuple;
+import com.google.common.collect.ImmutableList;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * A base implementation of the {@link NeighborsChangeSet} interface.
+ *
+ * This class represents a complete, non-delta state of a node's neighbors. It holds a fixed, immutable
+ * list of neighbors provided at construction time. As such, it does not support parent change sets or writing deltas.
+ *
+ * @param the type of the node reference, which must extend {@link NodeReference}
+ */
+class BaseNeighborsChangeSet implements NeighborsChangeSet {
+ @Nonnull
+ private final List neighbors;
+
+ /**
+ * Creates a new change set with the specified neighbors.
+ *
+ * This constructor creates an immutable copy of the provided list.
+ *
+ * @param neighbors the list of neighbors for this change set; must not be null.
+ */
+ public BaseNeighborsChangeSet(@Nonnull final List neighbors) {
+ this.neighbors = ImmutableList.copyOf(neighbors);
+ }
+
+ /**
+ * Gets the parent change set.
+ *
+ * This implementation always returns {@code null}, as this type of change set
+ * does not have a parent.
+ *
+ * @return always {@code null}.
+ */
+ @Nullable
+ @Override
+ public BaseNeighborsChangeSet getParent() {
+ return null;
+ }
+
+ /**
+ * Retrieves the list of neighbors associated with this object.
+ *
+ * This implementation fulfills the {@code merge} contract by simply returning the
+ * existing list of neighbors without performing any additional merging logic.
+ * @return a non-null list of neighbors. The generic type {@code N} represents
+ * the type of the neighboring elements.
+ */
+ @Nonnull
+ @Override
+ public List merge() {
+ return neighbors;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
This implementation is a no-op and does not write any delta information,
+ * as indicated by the empty method body.
+ */
+ @Override
+ public void writeDelta(@Nonnull final InliningStorageAdapter storageAdapter, @Nonnull final Transaction transaction,
+ @Nonnull final Quantizer quantizer, final int layer, @Nonnull final AbstractNode node,
+ @Nonnull final Predicate primaryKeyPredicate) {
+ // nothing to be written
+ }
+}
diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/CompactNode.java b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/CompactNode.java
new file mode 100644
index 0000000000..cb742506ca
--- /dev/null
+++ b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/CompactNode.java
@@ -0,0 +1,166 @@
+/*
+ * CompactNode.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.apple.foundationdb.async.hnsw;
+
+import com.apple.foundationdb.annotation.SpotBugsSuppressWarnings;
+import com.apple.foundationdb.half.Half;
+import com.apple.foundationdb.linear.RealVector;
+import com.apple.foundationdb.linear.Transformed;
+import com.apple.foundationdb.tuple.Tuple;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a compact node within a graph structure, extending {@link AbstractNode}.
+ *
+ * This node type is considered "compact" because it directly stores its associated
+ * data vector of type {@link RealVector}. It is used to represent a vector in a
+ * vector space and maintains references to its neighbors via {@link NodeReference} objects.
+ *
+ * @see AbstractNode
+ * @see NodeReference
+ */
+class CompactNode extends AbstractNode {
+ @Nonnull
+ private static final NodeFactory FACTORY = new NodeFactory<>() {
+ @SuppressWarnings("unchecked")
+ @Nonnull
+ @Override
+ @SpotBugsSuppressWarnings("NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE")
+ public AbstractNode create(@Nonnull final Tuple primaryKey,
+ @Nullable final Transformed vector,
+ @Nonnull final List extends NodeReference> neighbors) {
+ return new CompactNode(primaryKey, Objects.requireNonNull(vector), (List)neighbors);
+ }
+
+ @Nonnull
+ @Override
+ public NodeKind getNodeKind() {
+ return NodeKind.COMPACT;
+ }
+ };
+
+ @Nonnull
+ private final Transformed vector;
+
+ /**
+ * Constructs a new {@code CompactNode} instance.
+ *
+ * This constructor initializes the node with its primary key, a data vector,
+ * and a list of its neighbors. It delegates the initialization of the
+ * {@code primaryKey} and {@code neighbors} to the superclass constructor.
+ *
+ * @param primaryKey the primary key that uniquely identifies this node; must not be {@code null}.
+ * @param vector the data vector of type {@code RealVector} associated with this node; must not be {@code null}.
+ * @param neighbors a list of {@link NodeReference} objects representing the neighbors of this node; must not be
+ * {@code null}.
+ */
+ public CompactNode(@Nonnull final Tuple primaryKey, @Nonnull final Transformed vector,
+ @Nonnull final List neighbors) {
+ super(primaryKey, neighbors);
+ this.vector = vector;
+ }
+
+ /**
+ * Returns a {@link NodeReference} that uniquely identifies this node.
+ *
+ * This implementation creates the reference using the node's primary key, obtained via {@code getPrimaryKey()}. It
+ * ignores the provided {@code vector} parameter, which exists to fulfill the contract of the overridden method.
+ *
+ * @param vector the vector context, which is ignored in this implementation.
+ * Per the {@code @Nullable} annotation, this can be {@code null}.
+ *
+ * @return a non-null {@link NodeReference} to this node.
+ */
+ @Nonnull
+ @Override
+ public NodeReference getSelfReference(@Nullable final Transformed vector) {
+ return new NodeReference(getPrimaryKey());
+ }
+
+ /**
+ * Gets the kind of this node.
+ * This implementation always returns {@link NodeKind#COMPACT}.
+ * @return the node kind, which is guaranteed to be {@link NodeKind#COMPACT}.
+ */
+ @Nonnull
+ @Override
+ public NodeKind getKind() {
+ return NodeKind.COMPACT;
+ }
+
+ /**
+ * Gets the vector of {@code Half} objects.
+ * @return the non-null vector of {@link Half} objects.
+ */
+ @Nonnull
+ public Transformed getVector() {
+ return vector;
+ }
+
+ /**
+ * Returns this node as a {@code CompactNode}. As this class is already a {@code CompactNode}, this method provides
+ * {@code this}.
+ * @return this object cast as a {@code CompactNode}, which is guaranteed to be non-null.
+ */
+ @Nonnull
+ @Override
+ public CompactNode asCompactNode() {
+ return this;
+ }
+
+ /**
+ * Returns this node as an {@link InliningNode}.
+ *
+ * This override is for node types that are not inlining nodes. As such, it
+ * will always fail.
+ * @return this node as a non-null {@link InliningNode}
+ * @throws IllegalStateException always, as this is not an inlining node
+ */
+ @Nonnull
+ @Override
+ public InliningNode asInliningNode() {
+ throw new IllegalStateException("this is not an inlining node");
+ }
+
+ /**
+ * Gets the shared factory instance for creating {@link NodeReference} objects.
+ *
+ * This static factory method is the preferred way to obtain a {@code NodeFactory}
+ * for {@link NodeReference} instances, as it returns a shared, pre-configured object.
+ *
+ * @return a shared, non-null instance of {@code NodeFactory}
+ */
+ @Nonnull
+ public static NodeFactory factory() {
+ return FACTORY;
+ }
+
+ @Override
+ public String toString() {
+ return "C[primaryKey=" + getPrimaryKey() +
+ ";vector=" + vector +
+ ";neighbors=" + getNeighbors() + "]";
+ }
+}
diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/CompactStorageAdapter.java b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/CompactStorageAdapter.java
new file mode 100644
index 0000000000..d14bee5368
--- /dev/null
+++ b/fdb-extensions/src/main/java/com/apple/foundationdb/async/hnsw/CompactStorageAdapter.java
@@ -0,0 +1,292 @@
+/*
+ * CompactStorageAdapter.java
+ *
+ * This source file is part of the FoundationDB open source project
+ *
+ * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.apple.foundationdb.async.hnsw;
+
+import com.apple.foundationdb.KeyValue;
+import com.apple.foundationdb.Range;
+import com.apple.foundationdb.ReadTransaction;
+import com.apple.foundationdb.StreamingMode;
+import com.apple.foundationdb.Transaction;
+import com.apple.foundationdb.async.AsyncIterable;
+import com.apple.foundationdb.async.AsyncUtil;
+import com.apple.foundationdb.linear.AffineOperator;
+import com.apple.foundationdb.linear.Quantizer;
+import com.apple.foundationdb.linear.RealVector;
+import com.apple.foundationdb.linear.Transformed;
+import com.apple.foundationdb.subspace.Subspace;
+import com.apple.foundationdb.tuple.ByteArrayUtil;
+import com.apple.foundationdb.tuple.Tuple;
+import com.google.common.base.Verify;
+import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * The {@code CompactStorageAdapter} class is a concrete implementation of {@link StorageAdapter} for managing HNSW
+ * graph data in a compact format.
+ *
+ * It handles the serialization and deserialization of graph nodes to and from a persistent data store. This
+ * implementation is optimized for space efficiency by storing nodes with their accompanying vector data and by storing
+ * just neighbor primary keys. It extends {@link AbstractStorageAdapter} to inherit common storage logic.
+ */
+class CompactStorageAdapter extends AbstractStorageAdapter implements StorageAdapter {
+ @Nonnull
+ private static final Logger logger = LoggerFactory.getLogger(CompactStorageAdapter.class);
+
+ /**
+ * Constructs a new {@code CompactStorageAdapter}.
+ *
+ * @param config the HNSW graph configuration, must not be null. See {@link Config}.
+ * @param nodeFactory the factory used to create new nodes of type {@link NodeReference}, must not be null.
+ * @param subspace the {@link Subspace} where the graph data is stored, must not be null.
+ * @param onWriteListener the listener to be notified of write events, must not be null.
+ * @param onReadListener the listener to be notified of read events, must not be null.
+ */
+ public CompactStorageAdapter(@Nonnull final Config config,
+ @Nonnull final NodeFactory nodeFactory,
+ @Nonnull final Subspace subspace,
+ @Nonnull final OnWriteListener onWriteListener,
+ @Nonnull final OnReadListener onReadListener) {
+ super(config, nodeFactory, subspace, onWriteListener, onReadListener);
+ }
+
+ /**
+ * Asynchronously fetches a node from the database for a given layer and primary key.
+ *
+ * This internal method constructs a raw byte key from the {@code layer} and {@code primaryKey} within the store's
+ * data subspace. It then uses the provided {@link ReadTransaction} to retrieve the raw value. If a value is found,
+ * it is deserialized into a {@link AbstractNode} object using the {@code nodeFromRaw} method.
+ *
+ * @param readTransaction the transaction to use for the read operation
+ * @param storageTransform an affine vector transformation operator that is used to transform the fetched vector
+ * into the current storage space
+ * @param layer the layer of the node to fetch
+ * @param primaryKey the primary key of the node to fetch
+ *
+ * @return a future that will complete with the fetched {@link AbstractNode}
+ *
+ * @throws IllegalStateException if the node cannot be found in the database for the given key
+ */
+ @Nonnull
+ @Override
+ protected CompletableFuture> fetchNodeInternal(@Nonnull final ReadTransaction readTransaction,
+ @Nonnull final AffineOperator storageTransform,
+ final int layer,
+ @Nonnull final Tuple primaryKey) {
+ final byte[] keyBytes = getDataSubspace().pack(Tuple.from(layer, primaryKey));
+
+ return readTransaction.get(keyBytes)
+ .thenApply(valueBytes -> {
+ if (valueBytes == null) {
+ throw new IllegalStateException("cannot fetch node");
+ }
+ return nodeFromRaw(storageTransform, layer, primaryKey, keyBytes, valueBytes);
+ });
+ }
+
+ /**
+ * Deserializes a raw key-value byte array pair into a {@code Node}.
+ *
+ * This method first converts the {@code valueBytes} into a {@link Tuple} and then,
+ * along with the {@code primaryKey}, constructs the final {@code Node} object.
+ * It also notifies any registered {@link OnReadListener} about the raw key-value
+ * read and the resulting node creation.
+ *
+ * @param storageTransform an affine vector transformation operator that is used to transform the fetched vector
+ * into the storage space that is currently being used
+ * @param layer the layer of the HNSW where this node resides
+ * @param primaryKey the primary key for the node
+ * @param keyBytes the raw byte representation of the node's key
+ * @param valueBytes the raw byte representation of the node's value, which will be deserialized
+ *
+ * @return a non-null, deserialized {@link AbstractNode} object
+ */
+ @Nonnull
+ private AbstractNode nodeFromRaw(@Nonnull final AffineOperator storageTransform, final int layer,
+ final @Nonnull Tuple primaryKey,
+ @Nonnull final byte[] keyBytes, @Nonnull final byte[] valueBytes) {
+ final Tuple nodeTuple = Tuple.fromBytes(valueBytes);
+ final AbstractNode node = nodeFromKeyValuesTuples(storageTransform, primaryKey, nodeTuple);
+ final OnReadListener onReadListener = getOnReadListener();
+ onReadListener.onNodeRead(layer, node);
+ onReadListener.onKeyValueRead(layer, keyBytes, valueBytes);
+ return node;
+ }
+
+ /**
+ * Constructs a compact {@link AbstractNode} from its representation as stored key and value tuples.
+ *
+ * This method deserializes a node by extracting its components from the provided tuples. It verifies that the
+ * node is of type {@link NodeKind#COMPACT} before delegating the final construction to
+ * {@link #compactNodeFromTuples(AffineOperator, Tuple, Tuple, Tuple)}. The {@code valueTuple} is expected to have
+ * a specific structure: the serialized node kind at index 0, a nested tuple for the vector at index 1, and a nested
+ * tuple for the neighbors at index 2.
+ *
+ * @param storageTransform an affine vector transformation operator that is used to transform the fetched vector
+ * into the storage space that is currently being used
+ * @param primaryKey the tuple representing the primary key of the node
+ * @param valueTuple the tuple containing the serialized node data, including kind, vector, and neighbors
+ *
+ * @return the reconstructed compact {@link AbstractNode}
+ *
+ * @throws com.google.common.base.VerifyException if the node kind encoded in {@code valueTuple} is not
+ * {@link NodeKind#COMPACT}
+ */
+ @Nonnull
+ private AbstractNode nodeFromKeyValuesTuples(@Nonnull final AffineOperator storageTransform,
+ @Nonnull final Tuple primaryKey,
+ @Nonnull final Tuple valueTuple) {
+ final NodeKind nodeKind = NodeKind.fromSerializedNodeKind((byte)valueTuple.getLong(0));
+ Verify.verify(nodeKind == NodeKind.COMPACT);
+
+ final Tuple vectorTuple;
+ final Tuple neighborsTuple;
+
+ vectorTuple = valueTuple.getNestedTuple(1);
+ neighborsTuple = valueTuple.getNestedTuple(2);
+ return compactNodeFromTuples(storageTransform, primaryKey, vectorTuple, neighborsTuple);
+ }
+
+ /**
+ * Creates a compact in-memory representation of a graph node from its constituent storage tuples.
+ *
+ * This method deserializes the raw data stored in {@code Tuple} objects into their
+ * corresponding in-memory types. It extracts the vector, constructs a list of
+ * {@link NodeReference} objects for the neighbors, and then uses a factory to
+ * assemble the final {@code Node} object.
+ *
+ *
+ * @param storageTransform an affine vector transformation operator that is used to transform the fetched vector
+ * into the storage space that is currently being used
+ * @param primaryKey the tuple representing the node's primary key
+ * @param vectorTuple the tuple containing the node's vector data
+ * @param neighborsTuple the tuple containing a list of nested tuples, where each nested tuple represents a neighbor
+ *
+ * @return a new {@code Node} instance containing the deserialized data from the input tuples
+ */
+ @Nonnull
+ private AbstractNode compactNodeFromTuples(@Nonnull final AffineOperator storageTransform,
+ @Nonnull final Tuple primaryKey,
+ @Nonnull final Tuple vectorTuple,
+ @Nonnull final Tuple neighborsTuple) {
+ final Transformed vector =
+ storageTransform.transform(StorageAdapter.vectorFromTuple(getConfig(), vectorTuple));
+ final List nodeReferences = Lists.newArrayListWithExpectedSize(neighborsTuple.size());
+
+ for (int i = 0; i < neighborsTuple.size(); i ++) {
+ final Tuple neighborTuple = neighborsTuple.getNestedTuple(i);
+ nodeReferences.add(new NodeReference(neighborTuple));
+ }
+
+ return getNodeFactory().create(primaryKey, vector, nodeReferences);
+ }
+
+ /**
+ * Writes the internal representation of a compact node to the data store within a given transaction.
+ * This method handles the serialization of the node's vector and its final set of neighbors based on the
+ * provided {@code neighborsChangeSet}.
+ *
+ *
The node is stored as a {@link Tuple} with the structure {@code (NodeKind, RealVector, NeighborPrimaryKeys)}.
+ * The key for the storage is derived from the node's layer and its primary key. After writing, it notifies any
+ * registered write listeners via {@code onNodeWritten} and {@code onKeyValueWritten}.
+ *
+ * @param transaction the {@link Transaction} to use for the write operation.
+ * @param quantizer the quantizer to use
+ * @param node the {@link AbstractNode} to be serialized and written; it is processed as a {@link CompactNode}.
+ * @param layer the graph layer index for the node, used to construct the storage key.
+ * @param neighborsChangeSet a {@link NeighborsChangeSet} containing the additions and removals, which are
+ * merged to determine the final set of neighbors to be written.
+ */
+ @Override
+ public void writeNodeInternal(@Nonnull final Transaction transaction, @Nonnull final Quantizer quantizer,
+ @Nonnull final AbstractNode node, final int layer,
+ @Nonnull final NeighborsChangeSet neighborsChangeSet) {
+ final byte[] key = getDataSubspace().pack(Tuple.from(layer, node.getPrimaryKey()));
+
+ final List