This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch cassandra-6.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cassandra-6.0 by this push:
     new b45f90cec4 Sharding support for non-vector SAI in-memory indexes
b45f90cec4 is described below

commit b45f90cec4b74f0dadec1ced92d7d5921645b279
Author: Yash Nasery <[email protected]>
AuthorDate: Sat Mar 14 11:45:12 2026 +0530

    Sharding support for non-vector SAI in-memory indexes
    
    patch by Yash Nasery, reviewed by Caleb Rackliffe and Francisco Guerrero 
for CASSANDRA-18216
---
 CHANGES.txt                                        |   1 +
 NEWS.txt                                           |   1 +
 .../developing/cql/indexing/sai/_sai-create.adoc   |   1 +
 .../pages/reference/cql-commands/create-index.adoc |   8 +-
 .../cassandra/db/memtable/ShardBoundaries.java     |  58 +++-
 src/java/org/apache/cassandra/dht/Range.java       |  16 +-
 .../index/accord/RouteMemtableIndexManager.java    |   9 +-
 .../cassandra/index/sai/StorageAttachedIndex.java  |  50 +++-
 .../cassandra/index/sai/disk/RowMapping.java       |  12 +-
 .../index/sai/disk/v1/MemtableIndexWriter.java     |   2 +-
 .../cassandra/index/sai/memory/MemtableIndex.java  | 103 +++----
 .../index/sai/memory/MemtableIndexManager.java     |  16 +-
 .../index/sai/memory/ShardedMemtableIndex.java     | 272 +++++++++++++++++++
 ...tableIndex.java => UnshardedMemtableIndex.java} |  38 ++-
 .../test/cql3/SingleNodeTableWalkTest.java         |  17 +-
 .../microbench/sai/AbstractMemtableIndexBench.java | 118 ++++++++
 .../microbench/sai/MemtableIndexFlushBench.java    | 150 +++++++++++
 .../sai/MemtableIndexPartitionReadBench.java       | 174 ++++++++++++
 .../microbench/sai/MemtableIndexReadBench.java     | 135 ++++++++++
 .../microbench/sai/MemtableIndexWriteBench.java    | 105 ++++++++
 .../cassandra/db/memtable/ShardBoundariesTest.java | 117 ++++++++
 .../cassandra/dht/RangeIntersectsBoundsTest.java   | 296 +++++++++++++++++++++
 .../index/sai/cql/StorageAttachedIndexDDLTest.java |  75 ++++++
 .../sai/memory/ShardedMemtableIndexFlushTest.java  |  55 ++++
 .../index/sai/memory/ShardedMemtableIndexTest.java | 255 ++++++++++++++++++
 .../index/sai/memory/TrieMemoryIndexTest.java      |  65 +----
 .../index/sai/utils/SAIRandomizedTester.java       |  69 +++++
 27 files changed, 2056 insertions(+), 162 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 80603ef454..015680a337 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 6.0-alpha3
+ * Sharding support for non-vector SAI in-memory indexes (CASSANDRA-18216)
  * Index hints serialization rejects a count that the validation accepts 
(CASSANDRA-21700)
  * Do not expose system_metrics virtual keyspace by default (CASSANDRA-21720)
  * Tighten up NoSpamLogger changes after migration to Caffeine cache 
(CASSANDRA-21721)
diff --git a/NEWS.txt b/NEWS.txt
index 7cb208f520..af526ab092 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -149,6 +149,7 @@ New features
       cassandra.data_response_in_memory_max_rows_cl_one 
andcassandra.data_response_in_memory_max_size_cl_one.
       A limit is disabled by setting it to 0, and setting both limits of a 
tier to 0 disables the in-memory path for that tier.
       Note that the limits are not enforced in the middle of a range tombstone 
span, so a response may exceed them in that case.
+    - SAI Memtable-attached indexes now support sharding via WITH OPTIONS. See 
CQL documentation for details.
 Upgrading
 ---------
     - memtable_offheap_space: 0 is now rejected at startup when 
memtable_allocation_type is
diff --git 
a/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc 
b/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc
index 45e9362837..b5627c9065 100644
--- a/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc
+++ b/doc/modules/cassandra/pages/developing/cql/indexing/sai/_sai-create.adoc
@@ -17,6 +17,7 @@ The SAI index options are defined in the `WITH OPTIONS` 
clause.
 The `case_sensitive` option is set to `false` to allow case-insensitive 
searches.
 The `normalize` option is set to `true` to allow searches to be normalized for 
Unicode characters.
 The `ascii_only` option is set to `true` to allow searches to be limited to 
ASCII characters.
+The `shards` option sets the number of in-memory index shards per Memtable. 
(Set to `'auto'` for `cassandra.memtable.shard.count` shards. Not supported on 
vector column indexes.)
 
 The `map` collection data type is the one exception, as shown in the 
xref:#map-collection-in-sai-index[example below].
 
diff --git 
a/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc 
b/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc
index 5b191709e5..5a2802f45d 100644
--- a/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc
+++ b/doc/modules/cassandra/pages/reference/cql-commands/create-index.adoc
@@ -90,7 +90,7 @@ The regular column syntax applies for collection types `list` 
and `set`.
 [cols=",,"]
 |============================
 |option_map 2+<|Define options in JSON simple format.
-.4+||`case_sensitive` |Ignore case in matching string values. Default: `true`
+.5+||`case_sensitive` |Ignore case in matching string values. Default: `true`
 |`normalize`|When set to `true`, perform 
https://unicode.org/faq/normalization.html[Unicode normalization] on indexed 
strings.
 SAI supports Normalization Form C (NFC) Unicode.
 When set to `true`, SAI normalizes the different versions of a given Unicode 
character to a single version, retaining all the marks and symbols in the index.
@@ -107,6 +107,12 @@ Default: `false`.
 The similarity function is used to compute the similarity between two vectors.
 Valid options are: `EUCLIDEAN`, `DOT_PRODUCT`, `COSINE`
 Default: `COSINE`
+|`shards` |Number of in-memory index shards per memtable. Set to `'auto'` to 
use `cassandra.memtable.shard.count` shards.
+
+Not supported on vector column indexes.
+
+Default: `1`
+Maximum: `256`
 |============================
 
 == Usage notes
diff --git a/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java 
b/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java
index 102d087bb2..b09687fa32 100644
--- a/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java
+++ b/src/java/org/apache/cassandra/db/memtable/ShardBoundaries.java
@@ -22,8 +22,13 @@ import java.util.List;
 
 import com.google.common.annotations.VisibleForTesting;
 
+import org.agrona.collections.IntArrayList;
+
 import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.tcm.Epoch;
 
@@ -39,14 +44,18 @@ public class ShardBoundaries
 {
     private static final Token[] EMPTY_TOKEN_ARRAY = new Token[0];
 
-    // Special boundaries that map all tokens to one shard.
-    // These boundaries will be used in either of these cases:
+    // Special boundaries that map all tokens to one shard. These boundaries 
will be used in either of these cases:
     // - there is only 1 shard configured
     // - the default partitioner doesn't support splitting
     // - the keyspace is local system keyspace
+    private static final Range<PartitionPosition>[] EMPTY_RANGE_ARRAY = new 
Range[0];
+    private static final IntArrayList EMPTY_BOUNDARIES_SHARDS = new 
IntArrayList(new int[] { 0 }, 1, IntArrayList.DEFAULT_NULL_VALUE);
+
     public static final ShardBoundaries NONE = new 
ShardBoundaries(EMPTY_TOKEN_ARRAY, Epoch.EMPTY);
 
     private final Token[] boundaries;
+    private final Range<PartitionPosition>[] ranges;
+    private final IntArrayList allShards;
     public final Epoch epoch;
 
     @VisibleForTesting
@@ -54,6 +63,34 @@ public class ShardBoundaries
     {
         this.boundaries = boundaries;
         this.epoch = epoch;
+        this.ranges = precomputeRanges();
+
+        IntArrayList shards = new IntArrayList(boundaries.length + 1, 
IntArrayList.DEFAULT_NULL_VALUE);
+        for (int i = 0; i <= boundaries.length; i++)
+            shards.addInt(i);
+        this.allShards = shards;
+    }
+
+    private Range<PartitionPosition>[] precomputeRanges()
+    {
+        if (boundaries.length == 0)
+            return EMPTY_RANGE_ARRAY;
+
+        IPartitioner partitioner = boundaries[0].getPartitioner();
+        Range<PartitionPosition>[] ranges = new Range[boundaries.length + 1];
+        int rangeIndex = 0;
+        PartitionPosition minimum = 
partitioner.getMinimumToken().minKeyBound();
+
+        for (Token boundary : boundaries)
+        {
+            PartitionPosition boundaryPosition = boundary.maxKeyBound();
+            ranges[rangeIndex++] = new Range<>(minimum, boundaryPosition);
+            minimum = boundaryPosition;
+        }
+
+        ranges[rangeIndex] = new Range<>(minimum, 
partitioner.getMaximumTokenForSplitting().maxKeyBound());
+
+        return ranges;
     }
 
     public ShardBoundaries(List<Token> boundaries, Epoch epoch)
@@ -87,6 +124,23 @@ public class ShardBoundaries
         return getShardForToken(key.getToken());
     }
 
+    public IntArrayList getShardsForRange(AbstractBounds<PartitionPosition> 
keyRange)
+    {
+        if (boundaries.length == 0)
+            return EMPTY_BOUNDARIES_SHARDS;
+
+        // If the keyRange tokens match and are minimum then it represents the 
entire token ring
+        // then we need to return all the shards.
+        if (keyRange.right.isMinimum() && 
keyRange.left.compareTo(keyRange.right) == 0)
+            return allShards;
+        
+        IntArrayList inRange = new IntArrayList(ranges.length, 
IntArrayList.DEFAULT_NULL_VALUE);
+        for (int shard = 0; shard < ranges.length; shard++)
+            if (ranges[shard].intersects(keyRange))
+                inRange.addInt(shard);
+        return inRange;
+    }
+
     public Token getShardStartBoundary(int shardId)
     {
         if (shardId <= 0 || shardId >= boundaries.length)
diff --git a/src/java/org/apache/cassandra/dht/Range.java 
b/src/java/org/apache/cassandra/dht/Range.java
index a95249d426..910a3a1539 100644
--- a/src/java/org/apache/cassandra/dht/Range.java
+++ b/src/java/org/apache/cassandra/dht/Range.java
@@ -178,7 +178,21 @@ public class Range<T extends RingPosition<T>> extends 
AbstractBounds<T> implemen
             return intersects((Range<T>) that);
         if (that instanceof Bounds)
             return intersects((Bounds<T>) that);
-        throw new UnsupportedOperationException("Intersection is only 
supported for Bounds and Range objects; found " + that.getClass());
+        if (that instanceof ExcludingBounds)
+            return intersects((ExcludingBounds<T>) that);
+        if (that instanceof IncludingExcludingBounds)
+            return intersects((IncludingExcludingBounds<T>) that);
+        throw new UnsupportedOperationException("Intersection is not supported 
for " + that.getClass());
+    }
+    
+    public boolean intersects(IncludingExcludingBounds<T> that)
+    {
+        return contains(that.left) || intersects(new Range<>(that.left, 
that.right));
+    }
+
+    public boolean intersects(ExcludingBounds<T> that)
+    {
+        return intersects(new Range<>(that.left, that.right));
     }
 
     /**
diff --git 
a/src/java/org/apache/cassandra/index/accord/RouteMemtableIndexManager.java 
b/src/java/org/apache/cassandra/index/accord/RouteMemtableIndexManager.java
index 7c9adb6c3d..ce39300d22 100644
--- a/src/java/org/apache/cassandra/index/accord/RouteMemtableIndexManager.java
+++ b/src/java/org/apache/cassandra/index/accord/RouteMemtableIndexManager.java
@@ -58,17 +58,10 @@ public class RouteMemtableIndexManager implements 
MemtableIndexManager
         JournalKey journalKey = 
AccordKeyspace.JournalColumns.getJournalKey(key);
         if (!RouteJournalIndex.allowed(journalKey))
             return 0;
-        //TODO (performance): we dropped jdk8 and this was fixed in jdk8... so 
do we need to do this still?
-        MemtableIndex current = liveMemtableIndexMap.get(mt);
 
-        // We expect the relevant IndexMemtable to be present most of the 
time, so only make the
-        // call to computeIfAbsent() if it's not. (see 
https://bugs.openjdk.java.net/browse/JDK-8161372)
-        MemtableIndex target = (current != null)
-                               ? current
-                               : liveMemtableIndexMap.computeIfAbsent(mt, 
memtable -> new MemtableIndex());
+        MemtableIndex target = liveMemtableIndexMap.computeIfAbsent(mt, 
memtable -> new MemtableIndex());
 
         long start = nanoTime();
-
         long bytes = 0;
 
         ByteBuffer value = RouteIndexFormat.extractParticipants(index, 
journalKey.id, row);
diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java 
b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
index 881ca81911..9f93e0bf95 100644
--- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
+++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
@@ -88,6 +88,7 @@ import 
org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
 import org.apache.cassandra.index.sai.disk.format.Version;
 import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig;
 import org.apache.cassandra.index.sai.memory.MemtableIndexManager;
+import org.apache.cassandra.index.sai.memory.ShardedMemtableIndex;
 import org.apache.cassandra.index.sai.metrics.ColumnQueryMetrics;
 import org.apache.cassandra.index.sai.metrics.IndexMetrics;
 import org.apache.cassandra.index.sai.utils.IndexIdentifier;
@@ -117,6 +118,7 @@ import org.apache.cassandra.utils.concurrent.OpOrder;
 
 import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
 
+import static 
org.apache.cassandra.config.CassandraRelevantProperties.MEMTABLE_SHARD_COUNT;
 import static 
org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig.MAX_TOP_K;
 
 public class StorageAttachedIndex implements Index
@@ -140,6 +142,9 @@ public class StorageAttachedIndex implements Index
     @VisibleForTesting
     public static final String ANALYSIS_ON_KEY_COLUMNS_MESSAGE = "Analysis 
options are not supported on primary key columns, but found ";
 
+    @VisibleForTesting
+    public static final String VECTOR_SHARDING_SUPPORT_MESSAGE = "A 
storage-attached index on a vector column does not support sharding";
+
     public static final String ANN_LIMIT_ERROR = "Use of ANN OF in an ORDER BY 
clause requires a LIMIT that is not greater than %s. LIMIT was %s";
 
     private static final Logger logger = 
LoggerFactory.getLogger(StorageAttachedIndex.class);
@@ -159,7 +164,8 @@ public class StorageAttachedIndex implements Index
                                                                      
IndexWriterConfig.OPTIMIZE_FOR,
                                                                      
NonTokenizingOptions.CASE_SENSITIVE,
                                                                      
NonTokenizingOptions.NORMALIZE,
-                                                                     
NonTokenizingOptions.ASCII);
+                                                                     
NonTokenizingOptions.ASCII,
+                                                                     
ShardedMemtableIndex.SHARDS_OPTION);
 
     public static final Set<CQL3Type> SUPPORTED_TYPES = 
ImmutableSet.of(CQL3Type.Native.ASCII, CQL3Type.Native.BIGINT, 
CQL3Type.Native.DATE,
                                                                         
CQL3Type.Native.DOUBLE, CQL3Type.Native.FLOAT, CQL3Type.Native.INT,
@@ -169,8 +175,13 @@ public class StorageAttachedIndex implements Index
                                                                         
CQL3Type.Native.VARINT, CQL3Type.Native.DECIMAL, CQL3Type.Native.BOOLEAN,
                                                                         
CQL3Type.Native.BLOB);
 
+    // Upper bound on the shards option to prevent runaway per-shard 
allocations at first-write.
+    public static final int MAX_SHARD_COUNT = 256;
+
     private static final Set<Class<? extends IPartitioner>> 
ILLEGAL_PARTITIONERS =
             ImmutableSet.of(OrderPreservingPartitioner.class, 
LocalPartitioner.class, ByteOrderedPartitioner.class, RandomPartitioner.class);
+    private static final int DEFAULT_SHARD_COUNT = 
MEMTABLE_SHARD_COUNT.getInt(FBUtilities.getAvailableProcessors());
+    private static final String AUTO_SHARDS_OPTION = "auto";
 
     private final ColumnFamilyStore baseCfs;
     private final IndexMetadata indexMetadata;
@@ -184,6 +195,7 @@ public class StorageAttachedIndex implements Index
     private final MemtableIndexManager memtableIndexManager;
     private final IndexMetrics indexMetrics;
     private final MaxThreshold maxTermSizeGuardrail;
+    private final int shardCount;
 
     // Tracks whether we've started the index build on initialization.
     private volatile boolean initBuildStarted = false;
@@ -207,6 +219,7 @@ public class StorageAttachedIndex implements Index
         analyzerFactory = AbstractAnalyzer.fromOptions(indexTermType, 
indexMetadata.options);
         memtableIndexManager = new MemtableIndexManager(this);
         indexMetrics = new IndexMetrics(this, memtableIndexManager);
+
         if (indexTermType.isVector())
             maxTermSizeGuardrail = Guardrails.saiVectorTermSize;
         else if (indexTermType.isFrozen())
@@ -215,6 +228,14 @@ public class StorageAttachedIndex implements Index
             maxTermSizeGuardrail = Guardrails.saiBlobTermSize;
         else
             maxTermSizeGuardrail = Guardrails.saiStringTermSize;
+
+        String shardsOption = 
indexMetadata.options.get(ShardedMemtableIndex.SHARDS_OPTION);
+        if (shardsOption == null)
+            shardCount = 1;
+        else if (shardsOption.equalsIgnoreCase(AUTO_SHARDS_OPTION))
+            shardCount = DEFAULT_SHARD_COUNT;
+        else
+            shardCount = Integer.parseInt(shardsOption);
     }
 
     /**
@@ -281,6 +302,28 @@ public class StorageAttachedIndex implements Index
         }
 
         IndexTermType indexTermType = IndexTermType.create(target.left, 
metadata.partitionKeyColumns(), target.right);
+        String shardsOption = options.get(ShardedMemtableIndex.SHARDS_OPTION);
+        if (shardsOption != null)
+        {
+            if (indexTermType.isVector())
+                throw new 
InvalidRequestException(VECTOR_SHARDING_SUPPORT_MESSAGE);
+
+            if (!shardsOption.equalsIgnoreCase(AUTO_SHARDS_OPTION))
+            {
+                try
+                {
+                    int shardCount = Integer.parseInt(shardsOption);
+                    if (shardCount <= 0)
+                        throw new InvalidRequestException("Shard count for a 
storage-attached index must be a positive integer, was " + shardCount);
+                    if (shardCount > MAX_SHARD_COUNT)
+                        throw new InvalidRequestException("Shard count for a 
storage-attached index must not exceed " + MAX_SHARD_COUNT + ", was " + 
shardCount);
+                }
+                catch (NumberFormatException e)
+                {
+                    throw new InvalidRequestException("Shard count for a 
storage-attached index must be a valid integer, got '" + shardsOption + "'");
+                }
+            }
+        }
         AbstractAnalyzer.fromOptions(indexTermType, analysisOptions);
         IndexWriterConfig config = IndexWriterConfig.fromOptions(null, 
indexTermType, options);
 
@@ -334,6 +377,11 @@ public class StorageAttachedIndex implements Index
         return indexMetadata;
     }
 
+    public int shardCount()
+    {
+        return shardCount;
+    }
+
     @Override
     public Callable<?> getInitializationTask()
     {
diff --git a/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java 
b/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java
index 5bf64360af..19cedd0b93 100644
--- a/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/RowMapping.java
@@ -52,7 +52,7 @@ public class RowMapping
     public static final RowMapping DUMMY = new RowMapping()
     {
         @Override
-        public Iterator<Pair<ByteComparable, LongArrayList>> 
merge(MemtableIndex index) { return Collections.emptyIterator(); }
+        public Iterator<Pair<ByteComparable, LongArrayList>> 
merge(MemtableIndex index, PrimaryKey minKey, PrimaryKey maxKey) { return 
Collections.emptyIterator(); }
 
         @Override
         public void complete() {}
@@ -99,11 +99,13 @@ public class RowMapping
      *
      * @return an iterator of term -> postings list {@link Pair}s
      */
-    public Iterator<Pair<ByteComparable, LongArrayList>> merge(MemtableIndex 
index)
+    public Iterator<Pair<ByteComparable, LongArrayList>> merge(MemtableIndex 
index,
+                                                               PrimaryKey 
minKey,
+                                                               PrimaryKey 
maxKey)
     {
         assert complete : "RowMapping is not built.";
 
-        Iterator<Pair<ByteComparable, PrimaryKeys>> iterator = 
index.iterator();
+        Iterator<Pair<ByteComparable, Iterator<PrimaryKey>>> iterator = 
index.iterator(minKey.partitionKey(), maxKey.partitionKey());
         return new AbstractGuavaIterator<>()
         {
             @Override
@@ -111,10 +113,10 @@ public class RowMapping
             {
                 while (iterator.hasNext())
                 {
-                    Pair<ByteComparable, PrimaryKeys> pair = iterator.next();
+                    Pair<ByteComparable, Iterator<PrimaryKey>> pair = 
iterator.next();
 
                     LongArrayList postings = null;
-                    Iterator<PrimaryKey> primaryKeys = pair.right.iterator();
+                    Iterator<PrimaryKey> primaryKeys = pair.right;
 
                     while (primaryKeys.hasNext())
                     {
diff --git 
a/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java 
b/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java
index 795d5c1a0d..e8201c800f 100644
--- a/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java
+++ b/src/java/org/apache/cassandra/index/sai/disk/v1/MemtableIndexWriter.java
@@ -145,7 +145,7 @@ public class MemtableIndexWriter implements 
PerColumnIndexWriter
             }
             else
             {
-                final Iterator<Pair<ByteComparable, LongArrayList>> iterator = 
rowMapping.merge(memtable);
+                final Iterator<Pair<ByteComparable, LongArrayList>> iterator = 
rowMapping.merge(memtable, minKey, maxKey);
 
                 long cellCount = 0;
                 if (iterator.hasNext())
diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java 
b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java
index ab24fff087..cd4cb76da1 100644
--- a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java
+++ b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java
@@ -22,7 +22,6 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.Iterator;
 import java.util.List;
-import java.util.concurrent.atomic.LongAdder;
 import java.util.function.Function;
 
 import org.apache.cassandra.db.Clustering;
@@ -31,7 +30,6 @@ import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.db.memtable.Memtable;
 import org.apache.cassandra.dht.AbstractBounds;
 import org.apache.cassandra.index.sai.QueryContext;
-import org.apache.cassandra.index.sai.StorageAttachedIndex;
 import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
 import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
 import org.apache.cassandra.index.sai.disk.v1.vector.PrimaryKeyWithScore;
@@ -39,96 +37,63 @@ import 
org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
 import org.apache.cassandra.index.sai.plan.Expression;
 import org.apache.cassandra.index.sai.utils.IndexIdentifier;
 import org.apache.cassandra.index.sai.utils.PrimaryKey;
-import org.apache.cassandra.index.sai.utils.PrimaryKeys;
 import org.apache.cassandra.utils.CloseableIterator;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.bytecomparable.ByteComparable;
 
-public class MemtableIndex implements MemtableOrdering
+public interface MemtableIndex extends MemtableOrdering
 {
-    private final MemoryIndex memoryIndex;
-    private final LongAdder writeCount = new LongAdder();
-    private final LongAdder estimatedMemoryUsed = new LongAdder();
-    private final Memtable memtable;
+    long writeCount();
 
-    public MemtableIndex(StorageAttachedIndex index, Memtable memtable)
-    {
-        this.memoryIndex = index.termType().isVector() ? new 
VectorMemoryIndex(index, memtable) : new TrieMemoryIndex(index);
-        this.memtable = memtable;
-    }
+    long estimatedMemoryUsed();
 
-    public long writeCount()
-    {
-        return writeCount.sum();
-    }
+    boolean isEmpty();
 
-    public long estimatedMemoryUsed()
-    {
-        return estimatedMemoryUsed.sum();
-    }
+    Memtable getMemtable();
 
-    public boolean isEmpty()
-    {
-        return memoryIndex.isEmpty();
-    }
-
-    public Memtable getMemtable()
-    {
-        return memtable;
-    }
+    ByteBuffer getMinTerm();
 
-    public ByteBuffer getMinTerm()
-    {
-        return memoryIndex.getMinTerm();
-    }
+    ByteBuffer getMaxTerm();
 
-    public ByteBuffer getMaxTerm()
-    {
-        return memoryIndex.getMaxTerm();
-    }
-
-    public long index(DecoratedKey key, Clustering<?> clustering, ByteBuffer 
value)
-    {
-        if (value == null || (value.remaining() == 0 && 
memoryIndex.index.termType().skipsEmptyValue()))
-            return 0;
-
-        long ram = memoryIndex.add(key, clustering, value);
-        writeCount.increment();
-        estimatedMemoryUsed.add(ram);
-        return ram;
-    }
-
-    public long update(DecoratedKey key, Clustering<?> clustering, ByteBuffer 
oldValue, ByteBuffer newValue)
-    {
-        return memoryIndex.update(key, clustering, oldValue, newValue);
-    }
-
-    public KeyRangeIterator search(QueryContext queryContext, Expression 
expression, AbstractBounds<PartitionPosition> keyRange)
-    {
-        return memoryIndex.search(queryContext, expression, keyRange);
-    }
+    long index(DecoratedKey key, Clustering<?> clustering, ByteBuffer value);
 
-    public Iterator<Pair<ByteComparable, PrimaryKeys>> iterator()
+    /** Implementation only for Vector Indexes */
+    default long update(DecoratedKey key, Clustering<?> clustering, ByteBuffer 
oldValue, ByteBuffer newValue)
     {
-        return memoryIndex.iterator();
+        throw new UnsupportedOperationException();
     }
 
-    public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor 
indexDescriptor,
-                                                            IndexIdentifier 
indexIdentifier,
-                                                            
Function<PrimaryKey, Integer> postingTransformer) throws IOException
+    KeyRangeIterator search(QueryContext queryContext, Expression expression, 
AbstractBounds<PartitionPosition> keyRange);
+
+    /**
+     * Returns an iterator over the in-memory index in ascending term order.
+     * <p>
+     * Returned data may contain keys outside [minKey, maxKey]. The bounds
+     * are used for shard selection only; implementations without sharding may
+     * ignore them.
+     * <p>
+     * Callers should invoke this method only when the source Memtable is no 
longer accepting
+     * writes. Otherwise, concurrent inserts may be observed partway through 
iteration.
+     */
+    Iterator<Pair<ByteComparable, Iterator<PrimaryKey>>> iterator(DecoratedKey 
min, DecoratedKey max);
+
+    /** Implementation only for Vector Indexes */
+    default SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor 
indexDescriptor,
+                                                             IndexIdentifier 
indexIdentifier,
+                                                             
Function<PrimaryKey, Integer> postingTransformer) throws IOException
     {
-        return memoryIndex.writeDirect(indexDescriptor, indexIdentifier, 
postingTransformer);
+        throw new UnsupportedOperationException();
     }
 
     @Override
-    public CloseableIterator<PrimaryKeyWithScore> orderBy(QueryContext 
queryContext, Expression orderer, AbstractBounds<PartitionPosition> keyRange)
+    default CloseableIterator<PrimaryKeyWithScore> orderBy(QueryContext 
queryContext, Expression orderer, AbstractBounds<PartitionPosition> keyRange)
     {
-        return memoryIndex.orderBy(queryContext, orderer, keyRange);
+        throw new UnsupportedOperationException();
     }
 
     @Override
-    public CloseableIterator<PrimaryKeyWithScore> orderResultsBy(QueryContext 
queryContext, List<PrimaryKey> results, Expression orderer)
+    default CloseableIterator<PrimaryKeyWithScore> orderResultsBy(QueryContext 
queryContext, List<PrimaryKey> results, Expression orderer)
     {
-        return memoryIndex.orderResultsBy(queryContext, results, orderer);
+        throw new UnsupportedOperationException();
     }
 }
diff --git 
a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java 
b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java
index c2f7087fc9..e6e9342ac2 100644
--- a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java
+++ b/src/java/org/apache/cassandra/index/sai/memory/MemtableIndexManager.java
@@ -35,6 +35,7 @@ import 
org.apache.cassandra.cql3.statements.schema.IndexTarget;
 import org.apache.cassandra.db.DecoratedKey;
 import org.apache.cassandra.db.lifecycle.ILifecycleTransaction;
 import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.memtable.ShardBoundaries;
 import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.index.sai.StorageAttachedIndex;
 import org.apache.cassandra.utils.Clock;
@@ -59,12 +60,15 @@ public class MemtableIndexManager
 
     private MemtableIndex initializeMemtableIndex(Memtable mt)
     {
-        MemtableIndex current = liveMemtableIndexMap.get(mt);
-
-        // We expect the relevant IndexMemtable to be present most of the 
time, so only make the
-        // call to computeIfAbsent() if it's not. (see 
https://bugs.openjdk.java.net/browse/JDK-8161372)
-        return current != null ? current
-                               : liveMemtableIndexMap.computeIfAbsent(mt, 
memtable -> new MemtableIndex(index, memtable));
+        return liveMemtableIndexMap.computeIfAbsent(mt, memtable -> {
+            int shardCount = index.shardCount();
+            if (shardCount > 1)
+            {
+                ShardBoundaries boundaries = 
index.baseCfs().localRangeSplits(shardCount);
+                return new ShardedMemtableIndex(index, boundaries, memtable);
+            }
+            return new UnshardedMemtableIndex(index, memtable);
+        });
     }
 
     public long index(DecoratedKey key, Row row, Memtable mt)
diff --git 
a/src/java/org/apache/cassandra/index/sai/memory/ShardedMemtableIndex.java 
b/src/java/org/apache/cassandra/index/sai/memory/ShardedMemtableIndex.java
new file mode 100644
index 0000000000..48bd107b23
--- /dev/null
+++ b/src/java/org/apache/cassandra/index/sai/memory/ShardedMemtableIndex.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.index.sai.memory;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.LongAdder;
+
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterators;
+
+import org.agrona.collections.IntArrayList;
+
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.memtable.ShardBoundaries;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.index.sai.iterators.KeyRangeConcatIterator;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.index.sai.utils.PrimaryKeys;
+import org.apache.cassandra.utils.MergeIterator;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+
+public class ShardedMemtableIndex implements MemtableIndex
+{
+    private final ShardBoundaries boundaries;
+    private final MemoryIndex[] shards;
+    private final StorageAttachedIndex index;
+    private final LongAdder writeCount = new LongAdder();
+    private final LongAdder estimatedMemoryUsed = new LongAdder();
+    private final Memtable memtable;
+
+    public static final String SHARDS_OPTION = "shards";
+
+    public ShardedMemtableIndex(StorageAttachedIndex index, ShardBoundaries 
boundaries, Memtable memtable)
+    {
+        this.index = index;
+        this.boundaries = boundaries;
+        this.shards = generateShards(boundaries.shardCount(), index);
+        this.memtable = memtable;
+    }
+
+    private MemoryIndex[] generateShards(int splits, StorageAttachedIndex 
index)
+    {
+        MemoryIndex[] generatedShards = new MemoryIndex[splits];
+
+        for (int shard = 0; shard < boundaries.shardCount(); shard++)
+        {
+            generatedShards[shard] = new TrieMemoryIndex(index);
+        }
+
+        return generatedShards;
+    }
+
+    @VisibleForTesting
+    public int shardCount()
+    {
+        return shards.length;
+    }
+
+    @Override
+    public long writeCount()
+    {
+        return writeCount.sum();
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return getMinTerm() == null;
+    }
+
+    @Override
+    public Memtable getMemtable()
+    {
+        return memtable;
+    }
+
+    @Override
+    public long estimatedMemoryUsed()
+    {
+        return estimatedMemoryUsed.sum();
+    }
+
+    /**
+     * Returns the minimum indexed term in the combined memory indexes.
+     * This can be {@code null} if the indexed memtable was empty. Users of the
+     * {@code MemtableIndex} requiring a non-null minimum term should
+     * use the {@link MemtableIndex#isEmpty} method.
+     *
+     * <p>
+     * <b>Note:</b> Individual index shards can return {@code null} here if 
the index
+     *      didn't receive any terms within the token range of the shard
+     * </p>
+     *
+     * @return the minimum indexed term across all shards, or {@code null} if 
the index is empty.
+     */
+    @Override
+    @Nullable
+    public ByteBuffer getMinTerm()
+    {
+        ByteBuffer result = null;
+        for (MemoryIndex shard : shards)
+            result = index.termType().min(shard.getMinTerm(), result);
+        return result;
+    }
+
+    /**
+     *  Returns the maximum indexed term in the combined memory indexes.
+     *  This can be {@code null} if the indexed memtable was empty. Users of 
the
+     *  {@code MemtableIndex} requiring a non-null maximum term should
+     *  use the {@link MemtableIndex#isEmpty} method.
+     *
+     *  <p>
+     *  <b>Note:</b> Individual index shards can return {@code null} here if 
the index
+     *      didn't receive any terms within the token range of the shard
+     *  </p>
+     *
+     * @return the maximum indexed term across all shards, or {@code null} if 
the index is empty
+     */
+    @Override
+    @Nullable
+    public ByteBuffer getMaxTerm()
+    {
+        ByteBuffer result = null;
+        for (MemoryIndex shard : shards)
+            result = index.termType().max(shard.getMaxTerm(), result);
+        return result;
+    }
+
+    @Override
+    public long index(DecoratedKey key, Clustering<?> clustering, ByteBuffer 
value)
+    {
+        if (value == null || (value.remaining() == 0 && 
index.termType().skipsEmptyValue()))
+            return 0;
+
+        long ram = shards[boundaries.getShardForKey(key)].add(key, clustering, 
value);
+        writeCount.increment();
+        estimatedMemoryUsed.add(ram);
+        return ram;
+    }
+
+    @Override
+    public KeyRangeIterator search(QueryContext queryContext, Expression 
expression, AbstractBounds<PartitionPosition> keyRange)
+    {
+        IntArrayList shardsForRange = boundaries.getShardsForRange(keyRange);
+        KeyRangeConcatIterator.Builder builder = 
KeyRangeConcatIterator.builder(shardsForRange.size());
+
+        // InMemoryKeyRangeIterator holds no closeable resources; so a builder 
cleanup is not needed on exceptions.
+        for (int i = 0; i < shardsForRange.size(); i++)
+        {
+            int shard = shardsForRange.getInt(i);
+            builder.add(shards[shard].search(queryContext, expression, 
keyRange));
+        }
+
+        return builder.build();
+    }
+
+    @Override
+    public Iterator<Pair<ByteComparable, Iterator<PrimaryKey>>> 
iterator(DecoratedKey min, DecoratedKey max)
+    {
+        int minSubrange = min == null ? 0 : boundaries.getShardForKey(min);
+        int maxSubrange = max == null ? shards.length - 1 : 
boundaries.getShardForKey(max);
+
+        Preconditions.checkArgument(minSubrange <= maxSubrange,
+                                    "iterator(min, max) requires min <= max 
but got min shard %s > max shard %s",
+                                    minSubrange, maxSubrange);
+
+        List<Iterator<Pair<ByteComparable, PrimaryKeys>>> shardIterators = new 
ArrayList<>(maxSubrange - minSubrange + 1);
+
+        for (int i = minSubrange; i <= maxSubrange; i++)
+            shardIterators.add(shards[i].iterator());
+
+        return MergeIterator.get(shardIterators,
+                                 (o1, o2) -> ByteComparable.compare(o1.left, 
o2.left, ByteComparable.Version.OSS50),
+                                 new 
PrimaryKeysMergeReducer(shardIterators.size()));
+    }
+
+    /**
+     *  The PrimaryKeysMergeReducer receives the range iterators from each of 
the shards selected based on the
+     *  min and max keys passed to the iterator method. It doesn't strictly do 
any reduction because the terms in each
+     *  shard are unique. It will receive at most one shard entry per selected 
shard before {@link #getReduced}
+     *  is called.
+     */
+    private static class PrimaryKeysMergeReducer extends 
MergeIterator.Reducer<Pair<ByteComparable, PrimaryKeys>, Pair<ByteComparable, 
Iterator<PrimaryKey>>>
+    {
+        private final Pair<ByteComparable, PrimaryKeys>[] perShardTermToKeys;
+        private final List<Iterator<PrimaryKey>> currentTermKeys;
+
+        private ByteComparable currentTerm;
+        private Iterator<PrimaryKey> lastReturnedIterator;
+
+        @SuppressWarnings("unchecked")
+        PrimaryKeysMergeReducer(int shards)
+        {
+            this.perShardTermToKeys = new Pair[shards];
+            this.currentTermKeys = new ArrayList<>(shards);
+        }
+
+        /**
+         * Receive the term entry for a shard. This should only be called once 
for each
+         * shard before reduction.
+         *
+         * @param idx the index of the shard contributing this entry
+         * @param current the term and its associated primary keys from the 
shard
+         */
+        @Override
+        public void reduce(int idx, Pair<ByteComparable, PrimaryKeys> current)
+        {
+            Preconditions.checkArgument(perShardTermToKeys[idx] == null, 
"Terms should be unique in the memory index");
+
+            perShardTermToKeys[idx] = current;
+
+            if (current != null && currentTerm == null)
+                currentTerm = current.left;
+        }
+
+        @Override
+        protected Pair<ByteComparable, Iterator<PrimaryKey>> getReduced()
+        {
+            Preconditions.checkArgument(currentTerm != null, "The term must 
exist in memory index");
+
+            for (Pair<ByteComparable, PrimaryKeys> p : perShardTermToKeys)
+                if (p != null && p.right != null && !p.right.isEmpty())
+                    currentTermKeys.add(p.right.iterator());
+
+            lastReturnedIterator = 
Iterators.concat(currentTermKeys.iterator());
+            return Pair.create(currentTerm, lastReturnedIterator);
+        }
+
+        @Override
+        protected void onKeyChange()
+        {
+            Preconditions.checkState(lastReturnedIterator == null || 
!lastReturnedIterator.hasNext(),
+                                     "Previous per-term Iterator<PrimaryKey> 
was not drained before the outer iterator advanced");
+
+            Arrays.fill(perShardTermToKeys, null);
+            currentTermKeys.clear();
+            currentTerm = null;
+            lastReturnedIterator = null;
+        }
+    }
+}
diff --git a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java 
b/src/java/org/apache/cassandra/index/sai/memory/UnshardedMemtableIndex.java
similarity index 81%
copy from src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java
copy to 
src/java/org/apache/cassandra/index/sai/memory/UnshardedMemtableIndex.java
index ab24fff087..c40ab93125 100644
--- a/src/java/org/apache/cassandra/index/sai/memory/MemtableIndex.java
+++ b/src/java/org/apache/cassandra/index/sai/memory/UnshardedMemtableIndex.java
@@ -44,49 +44,56 @@ import org.apache.cassandra.utils.CloseableIterator;
 import org.apache.cassandra.utils.Pair;
 import org.apache.cassandra.utils.bytecomparable.ByteComparable;
 
-public class MemtableIndex implements MemtableOrdering
+public class UnshardedMemtableIndex implements MemtableIndex
 {
     private final MemoryIndex memoryIndex;
     private final LongAdder writeCount = new LongAdder();
     private final LongAdder estimatedMemoryUsed = new LongAdder();
     private final Memtable memtable;
 
-    public MemtableIndex(StorageAttachedIndex index, Memtable memtable)
+    public UnshardedMemtableIndex(StorageAttachedIndex index, Memtable 
memtable)
     {
         this.memoryIndex = index.termType().isVector() ? new 
VectorMemoryIndex(index, memtable) : new TrieMemoryIndex(index);
         this.memtable = memtable;
     }
 
+    @Override
     public long writeCount()
     {
         return writeCount.sum();
     }
 
+    @Override
     public long estimatedMemoryUsed()
     {
         return estimatedMemoryUsed.sum();
     }
 
+    @Override
     public boolean isEmpty()
     {
         return memoryIndex.isEmpty();
     }
 
+    @Override
     public Memtable getMemtable()
     {
         return memtable;
     }
 
+    @Override
     public ByteBuffer getMinTerm()
     {
         return memoryIndex.getMinTerm();
     }
 
+    @Override
     public ByteBuffer getMaxTerm()
     {
         return memoryIndex.getMaxTerm();
     }
 
+    @Override
     public long index(DecoratedKey key, Clustering<?> clustering, ByteBuffer 
value)
     {
         if (value == null || (value.remaining() == 0 && 
memoryIndex.index.termType().skipsEmptyValue()))
@@ -98,21 +105,43 @@ public class MemtableIndex implements MemtableOrdering
         return ram;
     }
 
+    /** Used only for Vector Indexes */
+    @Override
     public long update(DecoratedKey key, Clustering<?> clustering, ByteBuffer 
oldValue, ByteBuffer newValue)
     {
         return memoryIndex.update(key, clustering, oldValue, newValue);
     }
 
+    @Override
     public KeyRangeIterator search(QueryContext queryContext, Expression 
expression, AbstractBounds<PartitionPosition> keyRange)
     {
         return memoryIndex.search(queryContext, expression, keyRange);
     }
 
-    public Iterator<Pair<ByteComparable, PrimaryKeys>> iterator()
+    @Override
+    public Iterator<Pair<ByteComparable, Iterator<PrimaryKey>>> 
iterator(DecoratedKey min, DecoratedKey max)
     {
-        return memoryIndex.iterator();
+        Iterator<Pair<ByteComparable, PrimaryKeys>> memoryIndexIterator = 
memoryIndex.iterator();
+
+        return new Iterator<>()
+        {
+            @Override
+            public boolean hasNext()
+            {
+                return memoryIndexIterator.hasNext();
+            }
+
+            @Override
+            public Pair<ByteComparable, Iterator<PrimaryKey>> next()
+            {
+                Pair<ByteComparable, PrimaryKeys> p = 
memoryIndexIterator.next();
+                return Pair.create(p.left, p.right.iterator());
+            }
+        };
     }
 
+    /** Used only for Vector Indexes */
+    @Override
     public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor 
indexDescriptor,
                                                             IndexIdentifier 
indexIdentifier,
                                                             
Function<PrimaryKey, Integer> postingTransformer) throws IOException
@@ -120,6 +149,7 @@ public class MemtableIndex implements MemtableOrdering
         return memoryIndex.writeDirect(indexDescriptor, indexIdentifier, 
postingTransformer);
     }
 
+    /** Used only for Vector Indexes */
     @Override
     public CloseableIterator<PrimaryKeyWithScore> orderBy(QueryContext 
queryContext, Expression orderer, AbstractBounds<PartitionPosition> keyRange)
     {
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
index adba049f90..7eb72ebce9 100644
--- 
a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTableWalkTest.java
@@ -25,6 +25,7 @@ import java.util.Collections;
 import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.NavigableSet;
 import java.util.Optional;
 import java.util.TreeMap;
@@ -95,6 +96,14 @@ public class SingleNodeTableWalkTest extends StatefulASTBase
                                                                                
                                          .stream()
                                                                                
                                          .filter(t -> 
!AbstractTypeGenerators.isUnsafeEquality(t))
                                                                                
                                          .collect(Collectors.toList()));
+    private static final String SHARDS = "shards";
+    @SuppressWarnings("unchecked")
+    private static final Map<String, String>[] SHARD_OPTIONS = new Map[] {
+        Collections.emptyMap(),
+        Collections.singletonMap(SHARDS, "1"),
+        Collections.singletonMap(SHARDS, "4"),
+        Collections.singletonMap(SHARDS, "auto")
+    };
     private static final Logger logger = 
LoggerFactory.getLogger(SingleNodeTableWalkTest.class);
 
     protected static boolean READ_AFTER_WRITE = false;
@@ -695,12 +704,18 @@ public class SingleNodeTableWalkTest extends 
StatefulASTBase
                     colExpression = new 
CreateIndexDDL.CollectionReference(CreateIndexDDL.CollectionReference.Kind.FULL,
 colExpression);
 
                 String name = "tbl_" + col.name;
+
+                Map<String, String> indexOptions = Collections.emptyMap();
+                // If indexer is of Kind SAI, we want to randomize shards for 
testing the ShardedMemtableIndex implementation.
+                if (indexer.kind() == CreateIndexDDL.Indexer.Kind.sai )
+                    indexOptions = rs.pick(SHARD_OPTIONS);
+
                 CreateIndexDDL ddl = new 
CreateIndexDDL(rs.pick(CreateIndexDDL.Version.values()),
                                                         indexer,
                                                         Optional.of(new 
Symbol(name, UTF8Type.instance)),
                                                         
TableReference.from(metadata),
                                                         
Collections.singletonList(colExpression),
-                                                        
Collections.emptyMap());
+                                                        indexOptions);
                 String stmt = ddl.toCQL();
                 logger.info(stmt);
                 cluster.schemaChange(stmt);
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/sai/AbstractMemtableIndexBench.java
 
b/test/microbench/org/apache/cassandra/test/microbench/sai/AbstractMemtableIndexBench.java
new file mode 100644
index 0000000000..4632ea6eab
--- /dev/null
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/sai/AbstractMemtableIndexBench.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.test.microbench.sai;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.statements.schema.IndexTarget;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.index.sai.memory.MemtableIndex;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+
+public abstract class AbstractMemtableIndexBench extends CQLTester
+{
+    private static final int RANDOM_STRING_SIZE = 64 * 1024 * 1024;
+    private static String keyspace;
+
+    protected MemtableIndex memtableIndex;
+    protected DecoratedKey[] partitionKeys;
+    protected ByteBuffer[] terms;
+    protected StorageAttachedIndex index;
+
+    protected ColumnFamilyStore cfs;
+    protected String table;
+    private char[] randomChars = new char[RANDOM_STRING_SIZE];
+
+    public void setup(int numberOfTerms, int rowsPerPartition)
+    {
+        setupServer();
+        setupTableAndKeyspace();
+        setupCfsAndIndex();
+        setupPartitionKeys(numberOfTerms, rowsPerPartition);
+        setupTerms(numberOfTerms);
+    }
+
+    public void setupServer()
+    {
+        CQLTester.setUpClass();
+        DatabaseDescriptor.setAutoSnapshot(false);
+    }
+
+    public void setupTableAndKeyspace()
+    {
+        keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 
'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = 
false");
+        table = createTable(keyspace,
+                            "CREATE TABLE %s ( partition_id text, value_text 
text, PRIMARY KEY(partition_id)) with compression = {'enabled': false}",
+                            "memtable_index");
+        execute("use " + keyspace + ";");
+    }
+
+    public void setupCfsAndIndex()
+    {
+        cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
+        cfs.disableAutoCompaction();
+
+        Map<String, String> options = new HashMap<>();
+        options.put(IndexTarget.CUSTOM_INDEX_OPTION_NAME,
+                    StorageAttachedIndex.class.getCanonicalName());
+        options.put("target", "value_text");
+
+        IndexMetadata indexMetadata = 
IndexMetadata.fromSchemaMetadata("value_text_idx", IndexMetadata.Kind.CUSTOM, 
options);
+
+        index = new StorageAttachedIndex(cfs, indexMetadata);
+    }
+
+    public void setupPartitionKeys(int numberOfTerms, int rowsPerPartition)
+    {
+        TableMetadata tableMetadata = cfs.metadata();
+
+        int numberOfKeys = numberOfTerms / rowsPerPartition;
+
+        partitionKeys = new DecoratedKey[numberOfKeys];
+        for (int i = 0; i < numberOfKeys; i++)
+            partitionKeys[i] = 
tableMetadata.partitioner.decorateKey(tableMetadata.partitionKeyType.fromString("partition_"
 + i));
+    }
+
+    public void setupTerms(int numberOfTerms)
+    {
+        Random random = new Random();
+        for (int i = 0; i < RANDOM_STRING_SIZE; i++)
+            randomChars[i] = (char)('a' + random.nextInt(26));
+
+        int length = 64;
+        terms = new ByteBuffer[numberOfTerms];
+        for (int i = 0; i < numberOfTerms; i++)
+            terms[i] = 
UTF8Type.instance.decompose(generateRandomString(random, length));
+    }
+
+    private String generateRandomString(Random random, int length)
+    {
+        return new String(randomChars, random.nextInt(RANDOM_STRING_SIZE - 
length), length);
+    }
+}
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexFlushBench.java
 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexFlushBench.java
new file mode 100644
index 0000000000..aa57522ae1
--- /dev/null
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexFlushBench.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.test.microbench.sai;
+
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.memtable.ShardBoundaries;
+import org.apache.cassandra.index.sai.memory.ShardedMemtableIndex;
+import org.apache.cassandra.index.sai.memory.UnshardedMemtableIndex;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 5)
+@Fork(value = 1, jvmArgsAppend = { "-Xmx4G", "-Xms4G", 
"-Djmh.executor=CUSTOM", 
"-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor"})
+@State(Scope.Benchmark)
+public class MemtableIndexFlushBench extends AbstractMemtableIndexBench
+{
+    // 1 Million partitionKeys
+    private static final int NUM_PARTITION_KEYS = 1000000;
+    private static final int SMALL_POOL_SIZE = 8 * 1024;
+
+    @Param({ "1", "4", "8"})
+    int shardCount;
+
+    @Param({"50", "100"})
+    int numberOfTerms;
+
+    private char[] smallPool = new char[SMALL_POOL_SIZE];
+
+    @Setup(Level.Trial)
+    public void setup()
+    {
+        setupServer();
+        setupTableAndKeyspace();
+        setupCfsAndIndex();
+        setupPartitionKeys();
+        setupIndexesExpressionsAndTerms();
+    }
+
+    public void setupPartitionKeys()
+    {
+        TableMetadata tableMetadata = cfs.metadata();
+
+        partitionKeys = new DecoratedKey[NUM_PARTITION_KEYS];
+        for (int i = 0; i < NUM_PARTITION_KEYS; i++)
+            partitionKeys[i] = 
tableMetadata.partitioner.decorateKey(tableMetadata.partitionKeyType.fromString("partition_"
 + i));
+    }
+
+    public void setupIndexesExpressionsAndTerms() {
+        Memtable memtable = cfs.getCurrentMemtable();
+        if (shardCount > 1)
+        {
+            ShardBoundaries boundaries = cfs.localRangeSplits(shardCount);
+            memtableIndex = new ShardedMemtableIndex(index, boundaries, 
memtable);
+        }
+        else
+            memtableIndex = new UnshardedMemtableIndex(index, memtable);
+
+        setupTerms(numberOfTerms);
+        populateIndexData();
+    }
+
+    @Override
+    public void setupTerms(int numberOfTerms)
+    {
+        Random random = new Random();
+        for (int i = 0; i < SMALL_POOL_SIZE; i++)
+            smallPool[i] = (char)('a' + random.nextInt(26));
+
+        int length = 64;
+        terms = new ByteBuffer[numberOfTerms];
+        for (int i = 0; i < numberOfTerms; i++)
+            terms[i] = UTF8Type.instance.decompose(
+            new String(smallPool, random.nextInt(SMALL_POOL_SIZE - length), 
length));
+    }
+
+    private void populateIndexData()
+    {
+        int termCount = 0;
+
+        for (int i = 0; i < NUM_PARTITION_KEYS; i++)
+        {
+            DecoratedKey partitionKey = partitionKeys[i];
+            memtableIndex.index(partitionKey, Clustering.EMPTY, 
terms[termCount]);
+
+            if (++termCount == numberOfTerms)
+            {
+                termCount = 0;
+            }
+        }
+    }
+
+    @Benchmark
+    public long flushBench()
+    {
+        Iterator<Pair<ByteComparable, Iterator<PrimaryKey>>> it = 
memtableIndex.iterator(null, null);
+        long count = 0;
+        while (it.hasNext())
+        {
+            Iterator<PrimaryKey> primaryKeyIterator = it.next().right;
+            while (primaryKeyIterator.hasNext())
+            {
+                primaryKeyIterator.next();
+                count++;
+            }
+        }
+        return count;
+    }
+}
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexPartitionReadBench.java
 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexPartitionReadBench.java
new file mode 100644
index 0000000000..b77e1f4c66
--- /dev/null
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexPartitionReadBench.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.test.microbench.sai;
+
+import java.nio.ByteBuffer;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.memtable.ShardBoundaries;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Bounds;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.memory.ShardedMemtableIndex;
+import org.apache.cassandra.index.sai.memory.UnshardedMemtableIndex;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.schema.TableMetadata;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 6, time = 3)
+@Measurement(iterations = 5, time = 5)
+@Fork(value = 1, jvmArgsAppend = { "-Xmx4G", "-Xms4G", 
"-Djmh.executor=CUSTOM", 
"-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor"})
+@State(Scope.Benchmark)
+public class MemtableIndexPartitionReadBench extends AbstractMemtableIndexBench
+{
+    // 1 Million partitionKeys
+    private static final int NUM_PARTITION_KEYS = 1000000;
+    private static final int NUMBER_OF_SEARCHES = 1000;
+    private static final int SMALL_POOL_SIZE = 8 * 1024;
+
+    @Param({ "1", "4", "8"})
+    int shardCount;
+
+    @Param({"50", "100"})
+    int numberOfTerms;
+
+    private char[] smallPool = new char[SMALL_POOL_SIZE];
+    private Expression[] stringEqualityExpressions;
+    private QueryContext queryContext;
+    private AbstractBounds<PartitionPosition>[] keyRanges;
+
+    @State(Scope.Thread)
+    public static class ThreadState
+    {
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+    }
+
+    @Setup(Level.Trial)
+    public void setup()
+    {
+        setupServer();
+        setupTableAndKeyspace();
+        setupCfsAndIndex();
+        setupPartitionKeys();
+        setupQueryContext();
+        setupIndexesExpressionsAndTerms();
+    }
+
+    private void setupQueryContext()
+    {
+        // setup a dummy query context, interface signature needs it.
+        queryContext = new QueryContext(null, Long.MAX_VALUE);
+    }
+
+    public void setupPartitionKeys()
+    {
+        TableMetadata tableMetadata = cfs.metadata();
+
+        partitionKeys = new DecoratedKey[NUM_PARTITION_KEYS];
+        for (int i = 0; i < NUM_PARTITION_KEYS; i++)
+            partitionKeys[i] = 
tableMetadata.partitioner.decorateKey(tableMetadata.partitionKeyType.fromString("partition_"
 + i));
+    }
+
+    public void setupIndexesExpressionsAndTerms() {
+        Memtable memtable = cfs.getCurrentMemtable();
+        if (shardCount > 1)
+        {
+            ShardBoundaries boundaries = cfs.localRangeSplits(shardCount);
+            memtableIndex = new ShardedMemtableIndex(index, boundaries, 
memtable);
+        }
+        else
+            memtableIndex = new UnshardedMemtableIndex(index, memtable);
+
+        setupTerms(numberOfTerms);
+        populateIndexDataAndKeyRanges();
+        populateExpressions();
+    }
+
+    @Override
+    public void setupTerms(int numberOfTerms)
+    {
+        Random random = new Random();
+        for (int i = 0; i < SMALL_POOL_SIZE; i++)
+            smallPool[i] = (char)('a' + random.nextInt(26));
+
+        int length = 64;
+        terms = new ByteBuffer[numberOfTerms];
+        for (int i = 0; i < numberOfTerms; i++)
+            terms[i] = UTF8Type.instance.decompose(
+                new String(smallPool, random.nextInt(SMALL_POOL_SIZE - 
length), length));
+    }
+
+    private void populateIndexDataAndKeyRanges()
+    {
+        int termCount = 0;
+        keyRanges = new AbstractBounds[NUM_PARTITION_KEYS];
+
+        for (int i = 0; i < NUM_PARTITION_KEYS; i++)
+        {
+            DecoratedKey partitionKey = partitionKeys[i];
+            memtableIndex.index(partitionKey, Clustering.EMPTY, 
terms[termCount]);
+            keyRanges[i] = new Bounds<>(partitionKey, partitionKey);
+
+            if (++termCount == numberOfTerms)
+            {
+                termCount = 0;
+            }
+        }
+    }
+
+    private void populateExpressions()
+    {
+        Random random = new Random();
+        stringEqualityExpressions = new Expression[NUMBER_OF_SEARCHES];
+        for (int i = 0; i < NUMBER_OF_SEARCHES; i++)
+            stringEqualityExpressions[i] = 
Expression.create(index).add(Operator.EQ, terms[random.nextInt(terms.length)]);
+    }
+
+    @Benchmark
+    public long stringEqualityPartitionRestrictedRangeSearch(ThreadState state)
+    {
+        long size = 0;
+        memtableIndex.search(queryContext,
+                             
stringEqualityExpressions[state.random.nextInt(stringEqualityExpressions.length)],
+                             
keyRanges[state.random.nextInt(keyRanges.length)]);
+        return size;
+    }
+}
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexReadBench.java
 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexReadBench.java
new file mode 100644
index 0000000000..560b348f58
--- /dev/null
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexReadBench.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.test.microbench.sai;
+
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.DataRange;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.memtable.ShardBoundaries;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.index.sai.QueryContext;
+import org.apache.cassandra.index.sai.memory.ShardedMemtableIndex;
+import org.apache.cassandra.index.sai.memory.UnshardedMemtableIndex;
+import org.apache.cassandra.index.sai.plan.Expression;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 5)
+@Fork(value = 1, jvmArgsAppend = { "-Xmx4G", "-Xms4G", 
"-Djmh.executor=CUSTOM", 
"-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor"})
+@State(Scope.Benchmark)
+public class MemtableIndexReadBench extends AbstractMemtableIndexBench
+{
+    private static final int NUMBER_OF_SEARCHES = 1000;
+    private static final AbstractBounds<PartitionPosition> ALL_DATA_RANGE = 
DataRange.allData(Murmur3Partitioner.instance).keyRange();
+
+    @Param({ "1", "4", "8"})
+    int shardCount;
+
+    @Param({"1000000" })
+    protected int numberOfTerms;
+
+    @Param({ "1", "10", "100"})
+    protected int rowsPerPartition;
+
+    private Expression[] stringEqualityExpressions;
+    private QueryContext queryContext;
+
+    @State(Scope.Thread)
+    public static class ThreadState
+    {
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+    }
+
+    @Setup(Level.Trial)
+    public void setup()
+    {
+        super.setup(numberOfTerms, rowsPerPartition);
+    }
+
+    @Setup(Level.Iteration)
+    public void setupIndexesAndExpressions() {
+        Memtable memtable = cfs.getCurrentMemtable();
+        if (shardCount > 1)
+        {
+            ShardBoundaries boundaries = cfs.localRangeSplits(shardCount);
+            memtableIndex = new ShardedMemtableIndex(index, boundaries, 
memtable);
+        }
+        else
+            memtableIndex = new UnshardedMemtableIndex(index, memtable);
+
+        populateIndexData();
+        populateExpressions();
+        // setup a dummy query context, interface signature needs it.
+        queryContext = new QueryContext(null, Long.MAX_VALUE);
+    }
+
+    private void populateIndexData()
+    {
+        int rowCount = 0;
+        int keyCount = 0;
+        for (int i = 0; i < numberOfTerms; i++)
+        {
+            memtableIndex.index(partitionKeys[keyCount], Clustering.EMPTY, 
terms[i]);
+            if (++rowCount == rowsPerPartition)
+            {
+                rowCount = 0;
+                keyCount++;
+            }
+        }
+    }
+
+    private void populateExpressions()
+    {
+        Random random = new Random();
+        stringEqualityExpressions = new Expression[NUMBER_OF_SEARCHES];
+        for (int i = 0; i < NUMBER_OF_SEARCHES; i++)
+            stringEqualityExpressions[i] = 
Expression.create(index).add(Operator.EQ, terms[random.nextInt(terms.length)]);
+    }
+
+    @Benchmark
+    public long stringEqualitySearch(ThreadState state)
+    {
+        long size = 0;
+        memtableIndex.search(queryContext,
+            
stringEqualityExpressions[state.random.nextInt(stringEqualityExpressions.length)],
+            ALL_DATA_RANGE);
+        return size;
+    }
+}
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexWriteBench.java
 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexWriteBench.java
new file mode 100644
index 0000000000..3cdb9ce9c4
--- /dev/null
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/sai/MemtableIndexWriteBench.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.test.microbench.sai;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.commitlog.CommitLog;
+import org.apache.cassandra.db.memtable.Memtable;
+import org.apache.cassandra.db.memtable.ShardBoundaries;
+import org.apache.cassandra.index.sai.memory.ShardedMemtableIndex;
+import org.apache.cassandra.index.sai.memory.UnshardedMemtableIndex;
+
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 5)
+@Fork(value = 1, jvmArgsAppend = { "-Xmx4G", "-Xms4G", 
"-Djmh.executor=CUSTOM", 
"-Djmh.executor.class=org.apache.cassandra.test.microbench.FastThreadExecutor"})
+@Threads(8)
+@State(Scope.Benchmark)
+public class MemtableIndexWriteBench extends AbstractMemtableIndexBench
+{
+    @Param({ "1", "4", "8"})
+    int shardCount;
+
+    @Param({"1000000" })
+    protected int numberOfTerms;
+
+    @Param({ "1", "10", "100"})
+    protected int rowsPerPartition;
+
+    @State(Scope.Thread)
+    public static class ThreadState
+    {
+        ThreadLocalRandom random = ThreadLocalRandom.current();
+    }
+
+    @Setup(Level.Trial)
+    public void setup()
+    {
+        super.setup(numberOfTerms, rowsPerPartition);
+    }
+
+    @Setup(Level.Iteration)
+    public void setupIndexes()
+    {
+        Memtable memtable = cfs.getCurrentMemtable();
+        if (shardCount > 1)
+        {
+            ShardBoundaries boundaries = cfs.localRangeSplits(shardCount);
+            memtableIndex = new ShardedMemtableIndex(index, boundaries, 
memtable);
+        }
+        else
+            memtableIndex = new UnshardedMemtableIndex(index, memtable);
+    }
+
+    @Benchmark
+    public long write(ThreadState state)
+    {
+        return 
memtableIndex.index(partitionKeys[state.random.nextInt(partitionKeys.length)],
+                                   Clustering.EMPTY,
+                                   terms[state.random.nextInt(terms.length)]);
+    }
+    
+    @TearDown(Level.Trial)
+    public void teardown() throws InterruptedException
+    {
+        CommitLog.instance.shutdownBlocking();
+        CQLTester.tearDownClass();
+        CQLTester.cleanup();
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/db/memtable/ShardBoundariesTest.java 
b/test/unit/org/apache/cassandra/db/memtable/ShardBoundariesTest.java
new file mode 100644
index 0000000000..813dbb793e
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/memtable/ShardBoundariesTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.db.memtable;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.ServerTestUtils;
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.BufferDecoratedKey;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.dht.Bounds;
+import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.dht.Range;
+import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.tcm.Epoch;
+import org.apache.cassandra.utils.ByteBufferUtil;
+
+import static org.junit.Assert.assertEquals;
+
+public class ShardBoundariesTest
+{
+    private ShardBoundaries boundaries;
+
+    @Before
+    public void setUp()
+    {
+        DatabaseDescriptor.daemonInitialization();
+        DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
+        ServerTestUtils.prepareServerNoRegister();
+        boundaries = new ShardBoundaries(new Token[]{ token(10), token(20), 
token(30) }, Epoch.EMPTY);
+    }
+
+    private static Murmur3Partitioner.LongToken token(long value)
+    {
+        return new Murmur3Partitioner.LongToken(value);
+    }
+
+    private static DecoratedKey key(long tokenValue)
+    {
+        return new BufferDecoratedKey(token(tokenValue), 
ByteBufferUtil.bytes(tokenValue));
+    }
+
+    @Test
+    public void testGetShardForToken()
+    {
+        assertEquals(0, boundaries.getShardForToken(token(5)));
+        assertEquals(0, boundaries.getShardForToken(token(10)));
+        assertEquals(1, boundaries.getShardForToken(token(15)));
+        assertEquals(1, boundaries.getShardForToken(token(20)));
+        assertEquals(2, boundaries.getShardForToken(token(30)));
+        assertEquals(3, boundaries.getShardForToken(token(35)));
+    }
+
+    @Test
+    public void testGetShardsForRangeNoBoundaries()
+    {
+        List<Integer> shards = ShardBoundaries.NONE.getShardsForRange(new 
Bounds<>(key(1), key(100)));
+        assertEquals(List.of(0), shards);
+    }
+
+    @Test
+    public void testGetShardsForRangeWholeRing()
+    {
+        PartitionPosition minimum = 
Murmur3Partitioner.instance.getMinimumToken().minKeyBound();
+        List<Integer> shards = boundaries.getShardsForRange(new 
Range<>(minimum, minimum));
+        assertEquals(List.of(0, 1, 2, 3), shards);
+    }
+
+    @Test
+    public void testGetShardsForRangeSingleShard()
+    {
+        List<Integer> shards = boundaries.getShardsForRange(new 
Bounds<>(key(12), key(15)));
+        assertEquals(List.of(1), shards);
+    }
+
+    @Test
+    public void testGetShardsForRangeMultipleContiguousShards()
+    {
+        List<Integer> shards = boundaries.getShardsForRange(new 
Bounds<>(key(5), key(25)));
+        assertEquals(List.of(0, 1, 2), shards);
+    }
+
+    @Test
+    public void testGetShardsForRangeOpenEndedRight()
+    {
+        PartitionPosition minimum = 
Murmur3Partitioner.instance.getMinimumToken().minKeyBound();
+        List<Integer> shards = boundaries.getShardsForRange(new 
Bounds<>(key(15), minimum));
+        assertEquals(List.of(1, 2, 3), shards);
+    }
+
+    @Test
+    public void testGetShardsForRangeWrappingRange()
+    {
+        List<Integer> shards = boundaries.getShardsForRange(new 
Range<>(key(25), key(5)));
+        assertEquals(List.of(0, 2, 3), shards);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/dht/RangeIntersectsBoundsTest.java 
b/test/unit/org/apache/cassandra/dht/RangeIntersectsBoundsTest.java
new file mode 100644
index 0000000000..daca3787f7
--- /dev/null
+++ b/test/unit/org/apache/cassandra/dht/RangeIntersectsBoundsTest.java
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.dht;
+
+import org.junit.Test;
+
+import org.apache.cassandra.CassandraTestBase;
+import org.apache.cassandra.CassandraTestBase.DDDaemonInitialization;
+import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
+import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@DDDaemonInitialization
+public class RangeIntersectsBoundsTest extends CassandraTestBase
+{
+    @Test
+    public void rangeIntersectsBounds() throws Exception
+    {
+        Range<Token> all = new Range<>(new BigIntegerToken("0"), new 
BigIntegerToken("0"));
+        Range<Token> some = new Range<>(new BigIntegerToken("4"), new 
BigIntegerToken("8"));
+        Range<Token> someWrapped = new Range<>(some.right, some.left);
+
+        // Coda:
+        // l - matches left token of some range
+        // r - matches right token of some range
+        // b - below left token of some range
+        // a - above right token of some range
+        // i - inside some range (above left and below right
+        Bounds<Token> lr = new Bounds<>(new BigIntegerToken("4"), new 
BigIntegerToken("8"));
+        Bounds<Token> br = new Bounds<>(new BigIntegerToken("3"), new 
BigIntegerToken("8"));
+        Bounds<Token> bi = new Bounds<>(new BigIntegerToken("3"), new 
BigIntegerToken("7"));
+        Bounds<Token> ba = new Bounds<>(new BigIntegerToken("3"), new 
BigIntegerToken("9"));
+        Bounds<Token> la = new Bounds<>(new BigIntegerToken("4"), new 
BigIntegerToken("9"));
+        Bounds<Token> li = new Bounds<>(new BigIntegerToken("4"), new 
BigIntegerToken("7"));
+        Bounds<Token> ii = new Bounds<>(new BigIntegerToken("5"), new 
BigIntegerToken("7"));
+        Bounds<Token> ir = new Bounds<>(new BigIntegerToken("5"), new 
BigIntegerToken("8"));
+        Bounds<Token> bb = new Bounds<>(new BigIntegerToken("2"), new 
BigIntegerToken("3"));
+        Bounds<Token> aa = new Bounds<>(new BigIntegerToken("9"), new 
BigIntegerToken("10"));
+        Bounds<Token> bl = new Bounds<>(new BigIntegerToken("3"), new 
BigIntegerToken("4"));
+        Bounds<Token> ra = new Bounds<>(new BigIntegerToken("8"), new 
BigIntegerToken("9"));
+
+        assertTrue(all.intersects(lr));
+        assertTrue(all.intersects(br));
+        assertTrue(all.intersects(bi));
+        assertTrue(all.intersects(ba));
+        assertTrue(all.intersects(la));
+        assertTrue(all.intersects(li));
+        assertTrue(all.intersects(ii));
+        assertTrue(all.intersects(ir));
+        assertTrue(all.intersects(bb));
+        assertTrue(all.intersects(aa));
+        assertTrue(all.intersects(bl));
+        assertTrue(all.intersects(ra));
+
+        assertTrue(some.intersects(lr));
+        assertTrue(some.intersects(br));
+        assertTrue(some.intersects(bi));
+        assertTrue(some.intersects(ba));
+        assertTrue(some.intersects(la));
+        assertTrue(some.intersects(li));
+        assertTrue(some.intersects(ii));
+        assertTrue(some.intersects(ir));
+        assertFalse(some.intersects(bb));
+        assertFalse(some.intersects(aa));
+        assertFalse(some.intersects(bl));
+        assertTrue(some.intersects(ra));
+
+        assertTrue(someWrapped.intersects(lr));
+        assertTrue(someWrapped.intersects(br));
+        assertTrue(someWrapped.intersects(bi));
+        assertTrue(someWrapped.intersects(ba));
+        assertTrue(someWrapped.intersects(la));
+        assertTrue(someWrapped.intersects(li));
+        assertFalse(someWrapped.intersects(ii));
+        assertFalse(someWrapped.intersects(ir));
+        assertTrue(someWrapped.intersects(bb));
+        assertTrue(someWrapped.intersects(aa));
+        assertTrue(someWrapped.intersects(bl));
+        assertTrue(someWrapped.intersects(ra));
+    }
+
+    @Test
+    public void rangeIntersectsExcludingBounds() throws Exception
+    {
+        Range<Token> all = new Range<>(new BigIntegerToken("0"), new 
BigIntegerToken("0"));
+        Range<Token> some = new Range<>(new BigIntegerToken("4"), new 
BigIntegerToken("8"));
+        Range<Token> someWrapped = new Range<>(some.right, some.left);
+
+        // Coda:
+        // l - matches left token of some range
+        // r - matches right token of some range
+        // b - below left token of some range
+        // a - above right token of some range
+        // i - inside some range (above left and below right
+        ExcludingBounds<Token> lr = new ExcludingBounds<>(new 
BigIntegerToken("4"), new BigIntegerToken("8"));
+        ExcludingBounds<Token> br = new ExcludingBounds<>(new 
BigIntegerToken("3"), new BigIntegerToken("8"));
+        ExcludingBounds<Token> bi = new ExcludingBounds<>(new 
BigIntegerToken("3"), new BigIntegerToken("7"));
+        ExcludingBounds<Token> ba = new ExcludingBounds<>(new 
BigIntegerToken("3"), new BigIntegerToken("9"));
+        ExcludingBounds<Token> la = new ExcludingBounds<>(new 
BigIntegerToken("4"), new BigIntegerToken("9"));
+        ExcludingBounds<Token> li = new ExcludingBounds<>(new 
BigIntegerToken("4"), new BigIntegerToken("7"));
+        ExcludingBounds<Token> ii = new ExcludingBounds<>(new 
BigIntegerToken("5"), new BigIntegerToken("7"));
+        ExcludingBounds<Token> ir = new ExcludingBounds<>(new 
BigIntegerToken("5"), new BigIntegerToken("8"));
+        ExcludingBounds<Token> bb = new ExcludingBounds<>(new 
BigIntegerToken("2"), new BigIntegerToken("3"));
+        ExcludingBounds<Token> aa = new ExcludingBounds<>(new 
BigIntegerToken("9"), new BigIntegerToken("10"));
+        ExcludingBounds<Token> bl = new ExcludingBounds<>(new 
BigIntegerToken("3"), new BigIntegerToken("4"));
+        ExcludingBounds<Token> ra = new ExcludingBounds<>(new 
BigIntegerToken("8"), new BigIntegerToken("9"));
+
+        assertTrue(all.intersects(lr));
+        assertTrue(all.intersects(br));
+        assertTrue(all.intersects(bi));
+        assertTrue(all.intersects(ba));
+        assertTrue(all.intersects(la));
+        assertTrue(all.intersects(li));
+        assertTrue(all.intersects(ii));
+        assertTrue(all.intersects(ir));
+        assertTrue(all.intersects(bb));
+        assertTrue(all.intersects(aa));
+        assertTrue(all.intersects(bl));
+        assertTrue(all.intersects(ra));
+
+        assertTrue(some.intersects(lr));
+        assertTrue(some.intersects(br));
+        assertTrue(some.intersects(bi));
+        assertTrue(some.intersects(ba));
+        assertTrue(some.intersects(la));
+        assertTrue(some.intersects(li));
+        assertTrue(some.intersects(ii));
+        assertTrue(some.intersects(ir));
+        assertFalse(some.intersects(bb));
+        assertFalse(some.intersects(aa));
+        assertFalse(some.intersects(bl));
+        assertFalse(some.intersects(ra));
+
+        assertFalse(someWrapped.intersects(lr));
+        assertTrue(someWrapped.intersects(br));
+        assertTrue(someWrapped.intersects(bi));
+        assertTrue(someWrapped.intersects(ba));
+        assertTrue(someWrapped.intersects(la));
+        assertFalse(someWrapped.intersects(li));
+        assertFalse(someWrapped.intersects(ii));
+        assertFalse(someWrapped.intersects(ir));
+        assertTrue(someWrapped.intersects(bb));
+        assertTrue(someWrapped.intersects(aa));
+        assertTrue(someWrapped.intersects(bl));
+        assertTrue(someWrapped.intersects(ra));
+
+        Range<Token> range = new Range<>(Murmur3Partitioner.MINIMUM, new 
Murmur3Partitioner.LongToken(-1));
+        ExcludingBounds<Token> bounds = new ExcludingBounds<>(new 
Murmur3Partitioner.LongToken(-3248873570005575792L), 
Murmur3Partitioner.MINIMUM);
+
+        assertTrue(range.intersects(bounds));
+
+        range = new Range<>(new Murmur3Partitioner.LongToken(-1), 
Murmur3Partitioner.MINIMUM);
+
+        assertTrue(range.intersects(bounds));
+
+    }
+
+    @Test
+    public void rangeIntersectsIncludingExcludingBounds()
+    {
+        Range<Token> all = new Range<>(new BigIntegerToken("0"), new 
BigIntegerToken("0"));
+        Range<Token> some = new Range<>(new BigIntegerToken("4"), new 
BigIntegerToken("8"));
+        Range<Token> someWrapped = new Range<>(some.right, some.left);
+
+        // Coda:
+        // l - matches left token of some range
+        // r - matches right token of some range
+        // b - below left token of some range
+        // a - above right token of some range
+        // i - inside some range (above left and below right
+        IncludingExcludingBounds<Token> lr = new 
IncludingExcludingBounds<>(new BigIntegerToken("4"), new BigIntegerToken("8"));
+        IncludingExcludingBounds<Token> br = new 
IncludingExcludingBounds<>(new BigIntegerToken("3"), new BigIntegerToken("8"));
+        IncludingExcludingBounds<Token> bi = new 
IncludingExcludingBounds<>(new BigIntegerToken("3"), new BigIntegerToken("7"));
+        IncludingExcludingBounds<Token> ba = new 
IncludingExcludingBounds<>(new BigIntegerToken("3"), new BigIntegerToken("9"));
+        IncludingExcludingBounds<Token> la = new 
IncludingExcludingBounds<>(new BigIntegerToken("4"), new BigIntegerToken("9"));
+        IncludingExcludingBounds<Token> li = new 
IncludingExcludingBounds<>(new BigIntegerToken("4"), new BigIntegerToken("7"));
+        IncludingExcludingBounds<Token> ii = new 
IncludingExcludingBounds<>(new BigIntegerToken("5"), new BigIntegerToken("7"));
+        IncludingExcludingBounds<Token> ir = new 
IncludingExcludingBounds<>(new BigIntegerToken("5"), new BigIntegerToken("8"));
+        IncludingExcludingBounds<Token> bb = new 
IncludingExcludingBounds<>(new BigIntegerToken("2"), new BigIntegerToken("3"));
+        IncludingExcludingBounds<Token> aa = new 
IncludingExcludingBounds<>(new BigIntegerToken("9"), new BigIntegerToken("10"));
+        IncludingExcludingBounds<Token> bl = new 
IncludingExcludingBounds<>(new BigIntegerToken("3"), new BigIntegerToken("4"));
+        IncludingExcludingBounds<Token> ra = new 
IncludingExcludingBounds<>(new BigIntegerToken("8"), new BigIntegerToken("9"));
+
+        assertTrue(all.intersects(lr));
+        assertTrue(all.intersects(br));
+        assertTrue(all.intersects(bi));
+        assertTrue(all.intersects(ba));
+        assertTrue(all.intersects(la));
+        assertTrue(all.intersects(li));
+        assertTrue(all.intersects(ii));
+        assertTrue(all.intersects(ir));
+        assertTrue(all.intersects(bb));
+        assertTrue(all.intersects(aa));
+        assertTrue(all.intersects(bl));
+        assertTrue(all.intersects(ra));
+
+        assertTrue(some.intersects(lr));
+        assertTrue(some.intersects(br));
+        assertTrue(some.intersects(bi));
+        assertTrue(some.intersects(ba));
+        assertTrue(some.intersects(la));
+        assertTrue(some.intersects(li));
+        assertTrue(some.intersects(ii));
+        assertTrue(some.intersects(ir));
+        assertFalse(some.intersects(bb));
+        assertFalse(some.intersects(aa));
+        assertFalse(some.intersects(bl));
+        assertTrue(some.intersects(ra));
+
+        assertTrue(someWrapped.intersects(lr));
+        assertTrue(someWrapped.intersects(br));
+        assertTrue(someWrapped.intersects(bi));
+        assertTrue(someWrapped.intersects(ba));
+        assertTrue(someWrapped.intersects(la));
+        assertTrue(someWrapped.intersects(li));
+        assertFalse(someWrapped.intersects(ii));
+        assertFalse(someWrapped.intersects(ir));
+        assertTrue(someWrapped.intersects(bb));
+        assertTrue(someWrapped.intersects(aa));
+        assertTrue(someWrapped.intersects(bl));
+        assertTrue(someWrapped.intersects(ra));
+    }
+
+    /**
+     * Test that we handle partial bounds of the type x > n or x >= n which 
specifically have
+     * their right value as minimum.
+     */
+    @Test
+    public void rangeIntersectsPartialBounds()
+    {
+        Range<Token> range = new Range<>(Murmur3Partitioner.MINIMUM, new 
Murmur3Partitioner.LongToken(-1L));
+
+        Bounds<Token> boundsMatch = new Bounds<>(new 
Murmur3Partitioner.LongToken(-2L), Murmur3Partitioner.MINIMUM);
+        Bounds<Token> boundsNoMatch = new Bounds<>(new 
Murmur3Partitioner.LongToken(0L), Murmur3Partitioner.MINIMUM);
+
+        assertTrue(range.intersects(boundsMatch));
+        assertFalse(range.intersects(boundsNoMatch));
+
+        ExcludingBounds<Token> excBoundsMatch = new ExcludingBounds<>(new 
Murmur3Partitioner.LongToken(-2L), Murmur3Partitioner.MINIMUM);
+        ExcludingBounds<Token> excBoundsNoMatch = new ExcludingBounds<>(new 
Murmur3Partitioner.LongToken(-1L), Murmur3Partitioner.MINIMUM);
+
+        assertTrue(range.intersects(excBoundsMatch));
+        assertFalse(range.intersects(excBoundsNoMatch));
+
+        IncludingExcludingBounds<Token> incExcBoundsMatch = new 
IncludingExcludingBounds<>(new Murmur3Partitioner.LongToken(-2L), 
Murmur3Partitioner.MINIMUM);
+        IncludingExcludingBounds<Token> incExcBoundsNoMatch = new 
IncludingExcludingBounds<>(new Murmur3Partitioner.LongToken(0L), 
Murmur3Partitioner.MINIMUM);
+
+        assertTrue(range.intersects(incExcBoundsMatch));
+        assertFalse(range.intersects(incExcBoundsNoMatch));
+    }
+
+    @Test
+    public void wrappingRangeIntersectsTest()
+    {
+        // (100, max] + [min, -100]
+        Range<Token> wrapping = new Range<>(new LongToken(100), new 
LongToken(-100));
+
+        // inside the upper segment, sharing its left endpoint with the range
+        assertFalse(wrapping.intersects(new ExcludingBounds<>(new 
LongToken(50), new LongToken(100))));
+        assertTrue(wrapping.intersects(new ExcludingBounds<>(new 
LongToken(50), new LongToken(101))));
+        assertTrue(wrapping.intersects(new ExcludingBounds<>(new 
LongToken(100), new LongToken(200))));
+        assertTrue(wrapping.intersects(new IncludingExcludingBounds<>(new 
LongToken(100), new LongToken(200))));
+
+        // inside the lower segment, sharing its right endpoint with the range
+        assertTrue(wrapping.intersects(new ExcludingBounds<>(new 
LongToken(-200), new LongToken(-100))));
+        assertTrue(wrapping.intersects(new IncludingExcludingBounds<>(new 
LongToken(-200), new LongToken(-100))));
+
+        // in the gap between the two segments; only the included left 
endpoint of [-100, 100) is in the range
+        assertFalse(wrapping.intersects(new ExcludingBounds<>(new 
LongToken(-100), new LongToken(100))));
+        assertTrue(wrapping.intersects(new IncludingExcludingBounds<>(new 
LongToken(-100), new LongToken(100))));
+        assertFalse(wrapping.intersects(new ExcludingBounds<>(new 
LongToken(-50), new LongToken(50))));
+        assertFalse(wrapping.intersects(new IncludingExcludingBounds<>(new 
LongToken(-50), new LongToken(50))));
+
+        // right endpoint at the minimum, i.e. unbounded on the right
+        assertTrue(wrapping.intersects(new ExcludingBounds<>(new 
LongToken(200), Murmur3Partitioner.MINIMUM)));
+        assertTrue(wrapping.intersects(new IncludingExcludingBounds<>(new 
LongToken(200), Murmur3Partitioner.MINIMUM)));
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java 
b/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java
index 8e82d15a27..d179a2512c 100644
--- 
a/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java
+++ 
b/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java
@@ -82,9 +82,12 @@ import org.apache.cassandra.inject.InvokePointBuilder;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.service.StorageService;
+import org.apache.cassandra.utils.FBUtilities;
 import org.apache.cassandra.utils.Throwables;
 
 import static java.util.Collections.singletonList;
+import static 
org.apache.cassandra.config.CassandraRelevantProperties.MEMTABLE_SHARD_COUNT;
+import static 
org.apache.cassandra.index.sai.StorageAttachedIndex.VECTOR_SHARDING_SUPPORT_MESSAGE;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -269,6 +272,78 @@ public class StorageAttachedIndexDDLTest extends SAITester
         
.hasMessage(String.format(CreateIndexStatement.INVALID_CHARS_CUSTOM_INDEX_TARGET,
 invalidColumn));
     }
 
+    @Test
+    public void shouldRejectShardsOptionOnVectorIndex()
+    {
+        createTable("CREATE TABLE %s (id text PRIMARY KEY, val vector<float, 
3>)");
+
+        assertThatThrownBy(() -> executeNet("CREATE INDEX ON %s(val) USING 
'sai' WITH OPTIONS = { 'shards' : '4' }"))
+        .isInstanceOf(InvalidQueryException.class)
+        .hasMessageContaining(VECTOR_SHARDING_SUPPORT_MESSAGE);
+
+        assertThatThrownBy(() -> executeNet("CREATE INDEX ON %s(val) USING 
'sai' WITH OPTIONS = { 'shards' : 'auto' }"))
+        .isInstanceOf(InvalidQueryException.class)
+        .hasMessageContaining(VECTOR_SHARDING_SUPPORT_MESSAGE);
+    }
+
+    @Test
+    public void shouldCreateIndexWithValidShardsOption()
+    {
+        createTable("CREATE TABLE %s (id text PRIMARY KEY, val1 text, val2 
text)");
+        createIndex("CREATE INDEX ON %s(val1) USING 'sai' WITH OPTIONS = { 
'shards' : '1' }");
+        createIndex("CREATE INDEX ON %s(val2) USING 'sai' WITH OPTIONS = { 
'shards' : '4' }");
+        assertEquals(2, saiCreationCounter.get());
+    }
+
+    @Test
+    public void shouldRejectNonIntegerShardsOption()
+    {
+        createTable("CREATE TABLE %s (id text PRIMARY KEY, val text)");
+
+        assertThatThrownBy(() -> executeNet("CREATE INDEX ON %s(val) USING 
'sai' WITH OPTIONS = { 'shards' : 'abc' }"))
+        .isInstanceOf(InvalidQueryException.class)
+        .hasMessageContaining("Shard count for a storage-attached index must 
be a valid integer");
+    }
+
+    @Test
+    public void shouldCreateIndexWithAutoShardsOption()
+    {
+        createTable("CREATE TABLE %s (id text PRIMARY KEY, val1 text, val2 
text, val3 text)");
+        createIndex("CREATE INDEX auto_idx ON %s(val1) USING 'sai' WITH 
OPTIONS = { 'shards' : 'auto' }");
+        createIndex("CREATE INDEX ON %s(val2) USING 'sai' WITH OPTIONS = { 
'shards' : 'Auto' }");
+        createIndex("CREATE INDEX ON %s(val3) USING 'sai' WITH OPTIONS = { 
'shards' : 'AUTO' }");
+        assertEquals(3, saiCreationCounter.get());
+
+        SecondaryIndexManager sim = getCurrentColumnFamilyStore().indexManager;
+        StorageAttachedIndex index = (StorageAttachedIndex) 
sim.getIndexByName("auto_idx");
+        int expectedShardCount = 
MEMTABLE_SHARD_COUNT.getInt(FBUtilities.getAvailableProcessors());
+        assertEquals(expectedShardCount, index.shardCount());
+    }
+
+    @Test
+    public void shouldRejectNonPositiveShardsOption()
+    {
+        createTable("CREATE TABLE %s (id text PRIMARY KEY, val text)");
+
+        assertThatThrownBy(() -> executeNet("CREATE INDEX ON %s(val) USING 
'sai' WITH OPTIONS = { 'shards' : '0' }"))
+        .isInstanceOf(InvalidQueryException.class)
+        .hasMessageContaining("Shard count for a storage-attached index must 
be a positive integer");
+
+        assertThatThrownBy(() -> executeNet("CREATE INDEX ON %s(val) USING 
'sai' WITH OPTIONS = { 'shards' : '-5' }"))
+        .isInstanceOf(InvalidQueryException.class)
+        .hasMessageContaining("Shard count for a storage-attached index must 
be a positive integer");
+    }
+
+    @Test
+    public void shouldRejectShardsOptionGreaterThanMaxShardCount()
+    {
+        createTable("CREATE TABLE %s (id text PRIMARY KEY, val text)");
+
+        assertThatThrownBy(() -> executeNet("CREATE INDEX ON %s(val) USING 
'sai' WITH OPTIONS = { 'shards' : '257' }"))
+        .isInstanceOf(InvalidQueryException.class)
+        .hasMessageContaining("Shard count for a storage-attached index must 
not exceed");
+    }
+
     @Test
     public void shouldCreateIndexIfExists()
     {
diff --git 
a/test/unit/org/apache/cassandra/index/sai/memory/ShardedMemtableIndexFlushTest.java
 
b/test/unit/org/apache/cassandra/index/sai/memory/ShardedMemtableIndexFlushTest.java
new file mode 100644
index 0000000000..d2d41a301c
--- /dev/null
+++ 
b/test/unit/org/apache/cassandra/index/sai/memory/ShardedMemtableIndexFlushTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.index.sai.memory;
+
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.index.sai.utils.SAIRandomizedTester;
+
+public class ShardedMemtableIndexFlushTest extends SAIRandomizedTester
+{
+    @Test
+    public void flushShardedIndexWithReversedClustering()
+    {
+        createTable("CREATE TABLE %s (pk0 varint, ck0 bigint, ck1 text, v0 
uuid, PRIMARY KEY (pk0, ck0, ck1)) WITH CLUSTERING ORDER BY (ck0 DESC, ck1 
DESC)");
+
+        createIndex("CREATE CUSTOM INDEX ON %s(ck0) USING 
'StorageAttachedIndex' WITH OPTIONS = {'shards': '4'}");
+        createIndex("CREATE CUSTOM INDEX ON %s(ck1) USING 
'StorageAttachedIndex'");
+
+        execute("INSERT INTO %s (pk0, ck0, ck1, v0) VALUES (0, 
3796795943589367651, 'apple', uuid()) USING TIMESTAMP 2");
+        execute("INSERT INTO %s (pk0, ck0, ck1, v0) VALUES (0, 
5390896686766209114, 'banana', uuid()) USING TIMESTAMP 4");
+
+        execute("UPDATE %s USING TIMESTAMP 5 SET v0 = uuid() WHERE pk0 = 
13271891 AND ck0 = 7466188966332780245 AND ck1 = 'cherry'");
+        execute("UPDATE %s USING TIMESTAMP 5 SET v0 = uuid() WHERE pk0 = 
2081887771 AND ck0 = 7466188966332780245 AND ck1 = 'cherry'");
+
+        execute("UPDATE %s USING TIMESTAMP 6 SET v0 = uuid() WHERE pk0 = 0 AND 
ck0 = 8977613850235134686 AND ck1 = 'date'");
+        execute("UPDATE %s USING TIMESTAMP 6 SET v0 = uuid() WHERE pk0 = 4982 
AND ck0 = 8977613850235134686 AND ck1 = 'date'");
+
+        // Verify data is visible before flush
+        UntypedResultSet beforeFlush = execute("SELECT * FROM %s WHERE ck0 = 
8977613850235134686 AND ck1 = 'date' ALLOW FILTERING");
+        assertRowCount(beforeFlush, 2);
+
+        flush();
+
+        // After flush, same query should return same results
+        UntypedResultSet afterFlush = execute("SELECT * FROM %s WHERE ck0 = 
8977613850235134686 AND ck1 = 'date' ALLOW FILTERING");
+        assertRowCount(afterFlush, 2);
+    }
+}
\ No newline at end of file
diff --git 
a/test/unit/org/apache/cassandra/index/sai/memory/ShardedMemtableIndexTest.java 
b/test/unit/org/apache/cassandra/index/sai/memory/ShardedMemtableIndexTest.java
new file mode 100644
index 0000000000..9241e1da8d
--- /dev/null
+++ 
b/test/unit/org/apache/cassandra/index/sai/memory/ShardedMemtableIndexTest.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.cassandra.index.sai.memory;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.statements.schema.IndexTarget;
+import org.apache.cassandra.db.Clustering;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.PartitionPosition;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.memtable.ShardBoundaries;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
+import org.apache.cassandra.index.sai.plan.Expression;
+import org.apache.cassandra.index.sai.utils.PrimaryKey;
+import org.apache.cassandra.index.sai.utils.SAIRandomizedTester;
+import org.apache.cassandra.schema.IndexMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.Pair;
+import org.apache.cassandra.utils.bytecomparable.ByteComparable;
+import org.apache.cassandra.utils.bytecomparable.ByteSource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ShardedMemtableIndexTest extends SAIRandomizedTester
+{
+    private ColumnFamilyStore cfs;
+    private IPartitioner partitioner;
+    private StorageAttachedIndex index;
+    private ShardBoundaries boundaries;
+    private ShardedMemtableIndex memtableIndex;
+    private Map<DecoratedKey, Integer> keyMap;
+    private Map<Integer, Integer> rowMap;
+
+    @Before
+    public void setup() throws Throwable
+    {
+        // CQLTester @BeforeClass already sets up server.
+        // Set up the keyspace and the table.
+        String keyspace = createKeyspace("CREATE KEYSPACE %s with replication 
= { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = 
false");
+        String table = createTable(keyspace, "CREATE TABLE %s (pk int PRIMARY 
KEY, val int)", "memtable_index");
+
+        setupCfsAndIndex(keyspace, table);
+
+        partitioner = cfs.getPartitioner();
+        keyMap = new TreeMap<>();
+        rowMap = new HashMap<>();
+    }
+
+    public void setupCfsAndIndex(String keyspace, String table)
+    {
+        cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
+        cfs.disableAutoCompaction();
+
+        Map<String, String> options = new HashMap<>();
+        options.put(IndexTarget.CUSTOM_INDEX_OPTION_NAME, 
StorageAttachedIndex.class.getCanonicalName());
+        options.put("target", "val");
+
+        IndexMetadata indexMetadata = 
IndexMetadata.fromSchemaMetadata("val_idx", IndexMetadata.Kind.CUSTOM, options);
+
+        index = new StorageAttachedIndex(cfs, indexMetadata);
+        boundaries = cfs.localRangeSplits(8);
+    }
+
+    @Test
+    public void onHeapAllocationTest()
+    {
+        memtableIndex = new ShardedMemtableIndex(index, boundaries, 
cfs.getCurrentMemtable());
+        assertEquals(8, memtableIndex.shardCount());
+
+        assertEquals(0L, memtableIndex.writeCount());
+
+        for (int row = 0; row < 100; row++)
+        {
+            addRow(row, row);
+        }
+
+        assertTrue(memtableIndex.writeCount() > 0);
+    }
+
+    @Test
+    public void randomQueryTest()
+    {
+        memtableIndex = new ShardedMemtableIndex(index, boundaries, 
cfs.getCurrentMemtable());
+        assertEquals(8, memtableIndex.shardCount());
+
+        for (int row = 0; row < getRandom().nextIntBetween(1000, 5000); row++)
+        {
+            int pk = getRandom().nextIntBetween(0, 10000);
+            while (rowMap.containsKey(pk))
+                pk = getRandom().nextIntBetween(0, 10000);
+            int value = getRandom().nextIntBetween(0, 100);
+            rowMap.put(pk, value);
+            addRow(pk, value);
+        }
+
+        List<DecoratedKey> keys = new ArrayList<>(keyMap.keySet());
+
+        for (int executionCount = 0; executionCount < 1000; executionCount++)
+        {
+            Expression expression = generateRandomExpression(index);
+
+            AbstractBounds<PartitionPosition> keyRange = 
generateRandomBounds(keys, partitioner);
+
+            Set<Integer> expectedKeys = keyMap.keySet()
+                                              .stream()
+                                              .filter(keyRange::contains)
+                                              .map(keyMap::get)
+                                              .filter(pk -> 
expression.isSatisfiedBy(Int32Type.instance.decompose(rowMap.get(pk))))
+                                              .collect(Collectors.toSet());
+
+            Set<Integer> foundKeys = new HashSet<>();
+
+            try (KeyRangeIterator iterator = memtableIndex.search(null, 
expression, keyRange))
+            {
+                while (iterator.hasNext())
+                {
+                    int key = 
Int32Type.instance.compose(iterator.next().partitionKey().getKey());
+                    assertFalse(foundKeys.contains(key));
+                    foundKeys.add(key);
+                }
+            }
+
+            assertEquals(expectedKeys, foundKeys);
+        }
+    }
+
+    @Test
+    public void indexIteratorTest()
+    {
+        memtableIndex = new ShardedMemtableIndex(index, boundaries, 
cfs.getCurrentMemtable());
+        assertEquals(8, memtableIndex.shardCount());
+
+        Map<Integer, Set<DecoratedKey>> terms = buildTermMap();
+
+        terms.entrySet()
+             .stream()
+             .forEach(entry -> entry.getValue()
+                                    .forEach(pk -> 
addRow(Int32Type.instance.compose(pk.getKey()), entry.getKey())));
+
+        for (int executionCount = 0; executionCount < 1000; executionCount++)
+        {
+            // These keys have midrange tokens that select 3 of the 8 shards
+            DecoratedKey first = makeKey(cfs.metadata(), 
getRandom().nextIntBetween(0, 20000));
+            DecoratedKey second = makeKey(cfs.metadata(), 
getRandom().nextIntBetween(0, 20000));
+            final DecoratedKey minimum = first.compareTo(second) <= 0 ? first 
: second;
+            final DecoratedKey maximum = first.compareTo(second) <= 0 ? second 
: first;
+
+            Iterator<Pair<ByteComparable, Iterator<PrimaryKey>>> iterator = 
memtableIndex.iterator(minimum, maximum);
+
+            while (iterator.hasNext())
+            {
+                Pair<ByteComparable, Iterator<PrimaryKey>> termPair = 
iterator.next();
+                int term = termFromComparable(termPair.left);
+
+                // The iterator will return keys outside the range of min/max 
so we need to filter here to
+                // get the correct keys
+                List<DecoratedKey> expectedPks = terms.get(term)
+                                                      .stream()
+                                                      .filter(pk -> 
pk.compareTo(minimum) >= 0 && pk.compareTo(maximum) <= 0)
+                                                      .sorted()
+                                                      
.collect(Collectors.toList());
+
+                List<DecoratedKey> termPks = new ArrayList<>();
+
+                while (termPair.right.hasNext())
+                {
+                    DecoratedKey pk = termPair.right.next().partitionKey();
+                    if (pk.compareTo(minimum) >= 0 && pk.compareTo(maximum) <= 
0)
+                        termPks.add(pk);
+                }
+
+                assertEquals(expectedPks, termPks);
+            }
+        }
+    }
+
+    private int termFromComparable(ByteComparable comparable)
+    {
+        ByteSource.Peekable peekable = 
ByteSource.peekable(comparable.asComparableBytes(ByteComparable.Version.OSS50));
+        return 
Int32Type.instance.compose(Int32Type.instance.fromComparableBytes(peekable, 
ByteComparable.Version.OSS50));
+    }
+
+    private Map<Integer, Set<DecoratedKey>> buildTermMap()
+    {
+        Map<Integer, Set<DecoratedKey>> terms = new HashMap<>();
+
+        for (int count = 0; count < 10000; count++)
+        {
+            int term = getRandom().nextIntBetween(0, 100);
+            Set<DecoratedKey> pks;
+            if (terms.containsKey(term))
+                pks = terms.get(term);
+            else
+            {
+                pks = new HashSet<>();
+                terms.put(term, pks);
+            }
+            DecoratedKey key = makeKey(cfs.metadata(), 
getRandom().nextIntBetween(0, 20000));
+            while (pks.contains(key))
+                key = makeKey(cfs.metadata(), getRandom().nextIntBetween(0, 
20000));
+            pks.add(key);
+        }
+
+        return terms;
+    }
+
+    private void addRow(int pk, int value)
+    {
+        DecoratedKey key = makeKey(cfs.metadata(), pk);
+        memtableIndex.index(key, Clustering.EMPTY, 
Int32Type.instance.decompose(value));
+        keyMap.put(key, pk);
+    }
+
+    private DecoratedKey makeKey(TableMetadata table, Integer partitionKey)
+    {
+        ByteBuffer key = 
table.partitionKeyType.fromString(partitionKey.toString());
+        return table.partitioner.decorateKey(key);
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/index/sai/memory/TrieMemoryIndexTest.java 
b/test/unit/org/apache/cassandra/index/sai/memory/TrieMemoryIndexTest.java
index 3e512e9e23..0c396f8747 100644
--- a/test/unit/org/apache/cassandra/index/sai/memory/TrieMemoryIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/memory/TrieMemoryIndexTest.java
@@ -33,7 +33,6 @@ import org.HdrHistogram.Histogram;
 import org.junit.Ignore;
 import org.junit.Test;
 
-import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.cql3.statements.schema.IndexTarget;
 import org.apache.cassandra.db.Clustering;
 import org.apache.cassandra.db.ColumnFamilyStore;
@@ -43,11 +42,7 @@ import org.apache.cassandra.db.marshal.AbstractType;
 import org.apache.cassandra.db.marshal.Int32Type;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.dht.AbstractBounds;
-import org.apache.cassandra.dht.Bounds;
-import org.apache.cassandra.dht.ExcludingBounds;
-import org.apache.cassandra.dht.IncludingExcludingBounds;
 import org.apache.cassandra.dht.Murmur3Partitioner;
-import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.index.sai.StorageAttachedIndex;
 import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
 import org.apache.cassandra.index.sai.plan.Expression;
@@ -110,9 +105,9 @@ public class TrieMemoryIndexTest extends SAIRandomizedTester
 
         for (int executionCount = 0; executionCount < 1000; executionCount++)
         {
-            Expression expression = generateRandomExpression();
+            Expression expression = generateRandomExpression(this.index);
 
-            AbstractBounds<PartitionPosition> keyRange = 
generateRandomBounds(keys);
+            AbstractBounds<PartitionPosition> keyRange = 
generateRandomBounds(keys, Murmur3Partitioner.instance);
 
             Set<Integer> expectedKeys = keyMap.keySet()
                                               .stream()
@@ -137,62 +132,6 @@ public class TrieMemoryIndexTest extends 
SAIRandomizedTester
         }
     }
 
-    private AbstractBounds<PartitionPosition> 
generateRandomBounds(List<DecoratedKey> keys)
-    {
-        PartitionPosition leftBound = getRandom().nextBoolean() ? 
Murmur3Partitioner.instance.getMinimumToken().minKeyBound()
-                                                                : 
keys.get(getRandom().nextIntBetween(0, keys.size() - 
1)).getToken().minKeyBound();
-
-        PartitionPosition rightBound = getRandom().nextBoolean() ? 
Murmur3Partitioner.instance.getMinimumToken().minKeyBound()
-                                                                 : 
keys.get(getRandom().nextIntBetween(0, keys.size() - 
1)).getToken().maxKeyBound();
-
-        AbstractBounds<PartitionPosition> keyRange;
-
-        if (leftBound.isMinimum() && rightBound.isMinimum())
-            keyRange = new Range<>(leftBound, rightBound);
-        else
-        {
-            if (AbstractBounds.strictlyWrapsAround(leftBound, rightBound))
-            {
-                PartitionPosition temp = leftBound;
-                leftBound = rightBound;
-                rightBound = temp;
-            }
-            if (getRandom().nextBoolean())
-                keyRange = new Bounds<>(leftBound, rightBound);
-            else if (getRandom().nextBoolean())
-                keyRange = new ExcludingBounds<>(leftBound, rightBound);
-            else
-                keyRange = new IncludingExcludingBounds<>(leftBound, 
rightBound);
-        }
-        return keyRange;
-    }
-
-    private Expression generateRandomExpression()
-    {
-        Expression expression = Expression.create(index);
-
-        int equality = getRandom().nextIntBetween(0, 100);
-        int lower = getRandom().nextIntBetween(0, 75);
-        int upper = getRandom().nextIntBetween(25, 100);
-        while (upper <= lower)
-            upper = getRandom().nextIntBetween(0, 100);
-
-        if (getRandom().nextBoolean())
-            expression.add(Operator.EQ, 
Int32Type.instance.decompose(equality));
-        else
-        {
-            boolean useLower = getRandom().nextBoolean();
-            boolean useUpper = getRandom().nextBoolean();
-            if (!useLower && !useUpper)
-                useLower = useUpper = true;
-            if (useLower)
-                expression.add(getRandom().nextBoolean() ? Operator.GT : 
Operator.GTE, Int32Type.instance.decompose(lower));
-            if (useUpper)
-                expression.add(getRandom().nextBoolean() ? Operator.LT : 
Operator.LTE, Int32Type.instance.decompose(upper));
-        }
-        return expression;
-    }
-
     @Test
     public void shouldAcceptPrefixValuesTest()
     {
diff --git 
a/test/unit/org/apache/cassandra/index/sai/utils/SAIRandomizedTester.java 
b/test/unit/org/apache/cassandra/index/sai/utils/SAIRandomizedTester.java
index 7cff65acd8..b321280af2 100644
--- a/test/unit/org/apache/cassandra/index/sai/utils/SAIRandomizedTester.java
+++ b/test/unit/org/apache/cassandra/index/sai/utils/SAIRandomizedTester.java
@@ -18,6 +18,7 @@
 package org.apache.cassandra.index.sai.utils;
 
 import java.io.IOException;
+import java.util.List;
 
 import com.google.common.base.Preconditions;
 
@@ -28,11 +29,22 @@ import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.PartitionPosition;
 import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.Bounds;
+import org.apache.cassandra.dht.ExcludingBounds;
+import org.apache.cassandra.dht.IPartitioner;
+import org.apache.cassandra.dht.IncludingExcludingBounds;
 import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.index.sai.SAITester;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
 import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
 import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
+import org.apache.cassandra.index.sai.plan.Expression;
 import org.apache.cassandra.index.sai.postings.PostingList;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.SequenceBasedSSTableId;
@@ -199,4 +211,61 @@ public class SAIRandomizedTester extends SAITester
             array[randomPosition] = temp;
         }
     }
+
+    public static Expression generateRandomExpression(StorageAttachedIndex 
index)
+    {
+        Expression expression = Expression.create(index);
+
+        int equality = getRandom().nextIntBetween(0, 100);
+        int lower = getRandom().nextIntBetween(0, 75);
+        int upper = getRandom().nextIntBetween(25, 100);
+        while (upper <= lower)
+            upper = getRandom().nextIntBetween(0, 100);
+
+        if (getRandom().nextBoolean())
+            expression.add(Operator.EQ, 
Int32Type.instance.decompose(equality));
+        else
+        {
+            boolean useLower = getRandom().nextBoolean();
+            boolean useUpper = getRandom().nextBoolean();
+            if (!useLower && !useUpper)
+                useLower = useUpper = true;
+            if (useLower)
+                expression.add(getRandom().nextBoolean() ? Operator.GT : 
Operator.GTE, Int32Type.instance.decompose(lower));
+            if (useUpper)
+                expression.add(getRandom().nextBoolean() ? Operator.LT : 
Operator.LTE, Int32Type.instance.decompose(upper));
+        }
+        return expression;
+    }
+
+    public static AbstractBounds<PartitionPosition> 
generateRandomBounds(List<DecoratedKey> keys,
+                                                                         
IPartitioner partitioner)
+    {
+        PartitionPosition leftBound = getRandom().nextBoolean() ? 
partitioner.getMinimumToken().minKeyBound()
+                                                                : 
keys.get(getRandom().nextIntBetween(0, keys.size() - 
1)).getToken().minKeyBound();
+
+        PartitionPosition rightBound = getRandom().nextBoolean() ? 
partitioner.getMinimumToken().minKeyBound()
+                                                                 : 
keys.get(getRandom().nextIntBetween(0, keys.size() - 
1)).getToken().maxKeyBound();
+
+        AbstractBounds<PartitionPosition> keyRange;
+
+        if (leftBound.isMinimum() && rightBound.isMinimum())
+            keyRange = new Range<>(leftBound, rightBound);
+        else
+        {
+            if (AbstractBounds.strictlyWrapsAround(leftBound, rightBound))
+            {
+                PartitionPosition temp = leftBound;
+                leftBound = rightBound;
+                rightBound = temp;
+            }
+            if (getRandom().nextBoolean())
+                keyRange = new Bounds<>(leftBound, rightBound);
+            else if (getRandom().nextBoolean())
+                keyRange = new ExcludingBounds<>(leftBound, rightBound);
+            else
+                keyRange = new IncludingExcludingBounds<>(leftBound, 
rightBound);
+        }
+        return keyRange;
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to