mirror of https://github.com/apache/cassandra
Add support for a vector search index in SAI
- Adds jbellis/jvector (1.0.2) library for DiskANN based indexes on floating point vectors - Adds ORDER BY ANN OF capability to do ANN search and order the results by score patch by Mike Adamson; reviewed by Andrés de la Peña, Jonathon Ellis for CASSANDRA-18715 Co-authored-by Jonathon Ellis jbellis@gmail.com Co-authored-by Zhao Yang zhaoyangsingapore@gmail.com
This commit is contained in:
parent
b59b832eba
commit
949b760f55
|
|
@ -372,5 +372,9 @@
|
|||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-analysis-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.jbellis</groupId>
|
||||
<artifactId>jvector</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -1201,6 +1201,11 @@
|
|||
<artifactId>lucene-analysis-common</artifactId>
|
||||
<version>9.7.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.jbellis</groupId>
|
||||
<artifactId>jvector</artifactId>
|
||||
<version>1.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.carrotsearch.randomizedtesting</groupId>
|
||||
<artifactId>randomizedtesting-runner</artifactId>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
5.0-alpha2
|
||||
* Add support for vector search in SAI (CASSANDRA-18715)
|
||||
* Remove crc_check_chance from CompressionParams (CASSANDRA-18872)
|
||||
* Fix schema loading of UDTs inside vectors inside UDTs (CASSANDRA-18964)
|
||||
* Add cqlsh autocompletion for the vector data type (CASSANDRA-18946)
|
||||
|
|
|
|||
1
NEWS.txt
1
NEWS.txt
|
|
@ -86,6 +86,7 @@ New features
|
|||
src/java/org/apache/cassandra/db/compaction/UnifiedCompactionStrategy.md
|
||||
- New `VectorType` (cql `vector<element_type, dimension>`) which adds new fixed-length element arrays. See CASSANDRA-18504
|
||||
- Added new vector similarity functions `similarity_cosine`, `similarity_euclidean` and `similarity_dot_product`.
|
||||
- Added ANN vector similarity search via ORDER BY ANN OF syntax on SAI indexes (using jvector library).
|
||||
- Removed UDT type migration logic for 3.6+ clusters upgrading to 4.0. If migration has been disabled, it must be
|
||||
enabled before upgrading to 5.0 if the cluster used UDTs. See CASSANDRA-18504
|
||||
- Entended max expiration time from 2038-01-19T03:14:06+00:00 to 2106-02-07T06:28:13+00:00
|
||||
|
|
|
|||
|
|
@ -224,7 +224,8 @@ K_MASKED: M A S K E D;
|
|||
K_UNMASK: U N M A S K;
|
||||
K_SELECT_MASKED: S E L E C T '_' M A S K E D;
|
||||
|
||||
K_VECTOR: V E C T O R;
|
||||
K_VECTOR: V E C T O R;
|
||||
K_ANN: A N N;
|
||||
|
||||
// Case-insensitive alpha characters
|
||||
fragment A: ('a'|'A');
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ selectStatement returns [SelectStatement.RawStatement expr]
|
|||
@init {
|
||||
Term.Raw limit = null;
|
||||
Term.Raw perPartitionLimit = null;
|
||||
Map<ColumnIdentifier, Boolean> orderings = new LinkedHashMap<>();
|
||||
List<Ordering.Raw> orderings = new ArrayList<>();
|
||||
List<Selectable.Raw> groups = new ArrayList<>();
|
||||
boolean allowFiltering = false;
|
||||
boolean isJson = false;
|
||||
|
|
@ -459,11 +459,17 @@ customIndexExpression [WhereClause.Builder clause]
|
|||
: 'expr(' idxName[name] ',' t=term ')' { clause.add(new CustomIndexExpression(name, t));}
|
||||
;
|
||||
|
||||
orderByClause[Map<ColumnIdentifier, Boolean> orderings]
|
||||
orderByClause[List<Ordering.Raw> orderings]
|
||||
@init{
|
||||
boolean reversed = false;
|
||||
Ordering.Direction direction = Ordering.Direction.ASC;
|
||||
}
|
||||
: c=cident (K_ANN K_OF t=term)? (K_ASC | K_DESC { direction = Ordering.Direction.DESC; })?
|
||||
{
|
||||
Ordering.Raw.Expression expr = (t == null)
|
||||
? new Ordering.Raw.SingleColumn(c)
|
||||
: new Ordering.Raw.Ann(c, t);
|
||||
orderings.add(new Ordering.Raw(expr, direction));
|
||||
}
|
||||
: c=cident (K_ASC | K_DESC { reversed = true; })? { orderings.put(c, reversed); }
|
||||
;
|
||||
|
||||
groupByClause[List<Selectable.Raw> groups]
|
||||
|
|
@ -2014,5 +2020,6 @@ basic_unreserved_keyword returns [String str]
|
|||
| K_UNMASK
|
||||
| K_SELECT_MASKED
|
||||
| K_VECTOR
|
||||
| K_ANN
|
||||
) { $str = $k.text; }
|
||||
;
|
||||
|
|
|
|||
|
|
@ -429,8 +429,9 @@ public enum CassandraRelevantProperties
|
|||
SAI_INTERSECTION_CLAUSE_LIMIT("cassandra.sai.intersection_clause_limit", "2"),
|
||||
/** Latest version to be used for SAI index writing */
|
||||
SAI_LATEST_VERSION("cassandra.sai.latest_version", "aa"),
|
||||
SAI_MAX_FROZEN_TERM_SIZE("cassandra.sai.max_frozen_term_size_kb", "5"),
|
||||
SAI_MAX_STRING_TERM_SIZE("cassandra.sai.max_string_term_size_kb", "1"),
|
||||
SAI_MAX_FROZEN_TERM_SIZE("cassandra.sai.max_frozen_term_size", "5KiB"),
|
||||
SAI_MAX_STRING_TERM_SIZE("cassandra.sai.max_string_term_size", "1KiB"),
|
||||
SAI_MAX_VECTOR_TERM_SIZE("cassandra.sai.max_vector_term_size", "32KiB"),
|
||||
|
||||
/** Minimum number of reachable leaves for a given node to be eligible for an auxiliary posting list */
|
||||
SAI_MINIMUM_POSTINGS_LEAVES("cassandra.sai.minimum_postings_leaves", "64"),
|
||||
|
|
@ -455,6 +456,18 @@ public enum CassandraRelevantProperties
|
|||
SAI_TEST_BALANCED_TREE_DEBUG_ENABLED("cassandra.sai.test.balanced_tree_debug_enabled", "false"),
|
||||
SAI_TEST_DISABLE_TIMEOUT("cassandra.sai.test.disable.timeout", "false"),
|
||||
|
||||
/** Whether to allow the user to specify custom options to the hnsw index */
|
||||
SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS("cassandra.sai.vector.allow_custom_parameters", "false"),
|
||||
|
||||
/** Controls the maximum top-k limit for vector search */
|
||||
SAI_VECTOR_SEARCH_MAX_TOP_K("cassandra.sai.vector_search.max_top_k", "1000"),
|
||||
|
||||
/**
|
||||
* Controls the maximum number of PrimaryKeys that will be read into memory at one time when ordering/limiting
|
||||
* the results of an ANN query constrained by non-ANN predicates.
|
||||
*/
|
||||
SAI_VECTOR_SEARCH_ORDER_CHUNK_SIZE("cassandra.sai.vector_search.order_chunk_size", "100000"),
|
||||
|
||||
SCHEMA_PULL_INTERVAL_MS("cassandra.schema_pull_interval_ms", "60000"),
|
||||
SCHEMA_UPDATE_HANDLER_FACTORY_CLASS("cassandra.schema.update_handler_factory.class"),
|
||||
SEARCH_CONCURRENCY_FACTOR("cassandra.search_concurrency_factor", "1"),
|
||||
|
|
|
|||
|
|
@ -258,6 +258,23 @@ public enum Operator
|
|||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
},
|
||||
ANN(15)
|
||||
{
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "ANN";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSatisfiedBy(AbstractType<?> type, ByteBuffer leftOperand, ByteBuffer rightOperand)
|
||||
{
|
||||
// The ANN operator is only supported by the vector index so, normally, should never be called directly.
|
||||
// In networked queries (non-local) the coordinator will end up calling the row filter directly. So, this
|
||||
// needs to return true so that the returned values are allowed through to the VectorTopKProcessor
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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.cql3;
|
||||
|
||||
import org.apache.cassandra.cql3.restrictions.SingleColumnRestriction;
|
||||
import org.apache.cassandra.cql3.restrictions.SingleRestriction;
|
||||
import org.apache.cassandra.schema.ColumnMetadata;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
|
||||
/**
|
||||
* A single element of an ORDER BY clause.
|
||||
* <code>ORDER BY ordering1 [, ordering2 [, ...]] </code>
|
||||
* <p>
|
||||
* An ordering comprises an expression that produces the values to compare against each other
|
||||
* and a sorting direction (ASC, DESC).
|
||||
*/
|
||||
public class Ordering
|
||||
{
|
||||
public final Expression expression;
|
||||
public final Direction direction;
|
||||
|
||||
public Ordering(Expression expression, Direction direction)
|
||||
{
|
||||
this.expression = expression;
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
public static abstract class Expression
|
||||
{
|
||||
protected final ColumnMetadata columnMetadata;
|
||||
|
||||
public Expression(ColumnMetadata columnMetadata)
|
||||
{
|
||||
this.columnMetadata = columnMetadata;
|
||||
}
|
||||
|
||||
public boolean hasNonClusteredOrdering()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public SingleRestriction toRestriction()
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public ColumnMetadata getColumn()
|
||||
{
|
||||
return columnMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single column in <code>ORDER BY column</code>
|
||||
*/
|
||||
public static class SingleColumn extends Expression
|
||||
{
|
||||
public SingleColumn(ColumnMetadata columnMetadata)
|
||||
{
|
||||
super(columnMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An expression used in Approximate Nearest Neighbor ordering. <code>ORDER BY column ANN OF value</code>
|
||||
*/
|
||||
public static class Ann extends Expression
|
||||
{
|
||||
final Term vectorValue;
|
||||
|
||||
public Ann(ColumnMetadata columnMetadata, Term vectorValue)
|
||||
{
|
||||
super(columnMetadata);
|
||||
this.vectorValue = vectorValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNonClusteredOrdering()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SingleRestriction toRestriction()
|
||||
{
|
||||
return new SingleColumnRestriction.AnnRestriction(columnMetadata, vectorValue);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Direction
|
||||
{ASC, DESC}
|
||||
|
||||
|
||||
/**
|
||||
* Represents ANTLR's abstract syntax tree of a single element in the {@code ORDER BY} clause.
|
||||
* This comes directly out of CQL parser.
|
||||
*/
|
||||
public static class Raw
|
||||
{
|
||||
|
||||
final Expression expression;
|
||||
final Direction direction;
|
||||
|
||||
public Raw(Expression expression, Direction direction)
|
||||
{
|
||||
this.expression = expression;
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves column identifiers against the table schema.
|
||||
* Binds markers (?) to columns.
|
||||
*/
|
||||
public Ordering bind(TableMetadata table, VariableSpecifications boundNames)
|
||||
{
|
||||
return new Ordering(expression.bind(table, boundNames), direction);
|
||||
}
|
||||
|
||||
public interface Expression
|
||||
{
|
||||
Ordering.Expression bind(TableMetadata table, VariableSpecifications boundNames);
|
||||
}
|
||||
|
||||
public static class SingleColumn implements Expression
|
||||
{
|
||||
final ColumnIdentifier column;
|
||||
|
||||
SingleColumn(ColumnIdentifier column)
|
||||
{
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Ordering.Expression bind(TableMetadata table, VariableSpecifications boundNames)
|
||||
{
|
||||
return new Ordering.SingleColumn(table.getExistingColumn(column));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Ann implements Expression
|
||||
{
|
||||
final ColumnIdentifier columnId;
|
||||
final Term.Raw vectorValue;
|
||||
|
||||
Ann(ColumnIdentifier column, Term.Raw vectorValue)
|
||||
{
|
||||
this.columnId = column;
|
||||
this.vectorValue = vectorValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Ordering.Expression bind(TableMetadata table, VariableSpecifications boundNames)
|
||||
{
|
||||
ColumnMetadata column = table.getExistingColumn(columnId);
|
||||
Term value = vectorValue.prepare(table.keyspace, column);
|
||||
value.collectMarkerSpecification(boundNames);
|
||||
return new Ordering.Ann(column, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -114,6 +114,16 @@ public abstract class QueryOptions
|
|||
return new OptionsWithColumnSpecifications(options, columnSpecs);
|
||||
}
|
||||
|
||||
public static QueryOptions withConsistencyLevel(QueryOptions options, ConsistencyLevel consistencyLevel)
|
||||
{
|
||||
return new OptionsWithConsistencyLevel(options, consistencyLevel);
|
||||
}
|
||||
|
||||
public static QueryOptions withPageSize(QueryOptions options, int pageSize)
|
||||
{
|
||||
return new OptionsWithPageSize(options, pageSize);
|
||||
}
|
||||
|
||||
public abstract ConsistencyLevel getConsistency();
|
||||
public abstract List<ByteBuffer> getValues();
|
||||
public abstract boolean skipMetadata();
|
||||
|
|
@ -424,6 +434,40 @@ public abstract class QueryOptions
|
|||
}
|
||||
}
|
||||
|
||||
static class OptionsWithConsistencyLevel extends QueryOptionsWrapper
|
||||
{
|
||||
private final ConsistencyLevel consistencyLevel;
|
||||
|
||||
OptionsWithConsistencyLevel(QueryOptions wrapped, ConsistencyLevel consistencyLevel)
|
||||
{
|
||||
super(wrapped);
|
||||
this.consistencyLevel = consistencyLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConsistencyLevel getConsistency()
|
||||
{
|
||||
return consistencyLevel;
|
||||
}
|
||||
}
|
||||
|
||||
static class OptionsWithPageSize extends QueryOptionsWrapper
|
||||
{
|
||||
private final int pageSize;
|
||||
|
||||
OptionsWithPageSize(QueryOptions wrapped, int pageSize)
|
||||
{
|
||||
super(wrapped);
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPageSize()
|
||||
{
|
||||
return pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>QueryOptions</code> decorator that provides access to the column specifications.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -155,6 +155,8 @@ public abstract class Relation
|
|||
case LIKE_MATCHES:
|
||||
case LIKE:
|
||||
return newLikeRestriction(table, boundNames, relationType);
|
||||
case ANN:
|
||||
throw invalidRequest("ANN is only supported in ORDER BY");
|
||||
default: throw invalidRequest("Unsupported \"!=\" relation: %s", this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
|
|
@ -89,6 +91,11 @@ public abstract class UntypedResultSet implements Iterable<UntypedResultSet.Row>
|
|||
return size() == 0;
|
||||
}
|
||||
|
||||
public Stream<Row> stream()
|
||||
{
|
||||
return StreamSupport.stream(spliterator(), false);
|
||||
}
|
||||
|
||||
public abstract int size();
|
||||
public abstract Row one();
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import org.apache.cassandra.db.marshal.FloatType;
|
|||
import org.apache.cassandra.db.marshal.VectorType;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
import org.apache.lucene.index.VectorSimilarityFunction;
|
||||
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
|
||||
|
||||
public class VectorFcts
|
||||
{
|
||||
|
|
@ -39,7 +39,7 @@ public class VectorFcts
|
|||
}
|
||||
|
||||
private static FunctionFactory createSimilarityFunctionFactory(String name,
|
||||
VectorSimilarityFunction luceneFunction,
|
||||
VectorSimilarityFunction vectorSimilarityFunction,
|
||||
boolean supportsZeroVectors)
|
||||
{
|
||||
return new FunctionFactory(name,
|
||||
|
|
@ -55,7 +55,7 @@ public class VectorFcts
|
|||
int dimensions = firstArgType.dimension;
|
||||
if (!argTypes.stream().allMatch(t -> ((VectorType<?>) t).dimension == dimensions))
|
||||
throw new InvalidRequestException("All arguments must have the same vector dimensions");
|
||||
return createSimilarityFunction(name.name, firstArgType, luceneFunction, supportsZeroVectors);
|
||||
return createSimilarityFunction(name.name, firstArgType, vectorSimilarityFunction, supportsZeroVectors);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,24 @@ public abstract class MultiColumnRestriction implements SingleRestriction
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Index findSupportingIndex(IndexRegistry indexRegistry)
|
||||
{
|
||||
for (Index index : indexRegistry.listIndexes())
|
||||
if (isSupportedBy(index))
|
||||
return index;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan)
|
||||
{
|
||||
for (Index index : indexQueryPlan.getIndexes())
|
||||
if (isSupportedBy(index))
|
||||
return index;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsFiltering(Index.Group indexGroup)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -69,6 +69,21 @@ public interface Restriction
|
|||
*/
|
||||
boolean hasSupportingIndex(IndexRegistry indexRegistry);
|
||||
|
||||
/**
|
||||
* Find first supporting index for current restriction
|
||||
*
|
||||
* @param indexRegistry the index registry
|
||||
* @return <code>index</code> if the restriction is on indexed columns, <code>null</code>
|
||||
*/
|
||||
Index findSupportingIndex(IndexRegistry indexRegistry);
|
||||
|
||||
/**
|
||||
* Find the first supporting index for the current restriction from an {@link Index.QueryPlan}.
|
||||
* @param indexQueryPlan the index query plan
|
||||
* @return <code>index</code> if the restriction is on indexed columns, <code>null</code>
|
||||
*/
|
||||
Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan);
|
||||
|
||||
/**
|
||||
* Returns whether this restriction would need filtering if the specified index group were used.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
|
|||
private final boolean hasIn;
|
||||
private final boolean hasContains;
|
||||
private final boolean hasSlice;
|
||||
private final boolean hasAnn;
|
||||
private final boolean hasOnlyEqualityRestrictions;
|
||||
|
||||
public RestrictionSet()
|
||||
|
|
@ -74,6 +75,7 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true);
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +84,7 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
|
|||
boolean hasIn,
|
||||
boolean hasContains,
|
||||
boolean hasSlice,
|
||||
boolean hasAnn,
|
||||
boolean hasOnlyEqualityRestrictions)
|
||||
{
|
||||
this.restrictions = restrictions;
|
||||
|
|
@ -89,6 +92,7 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
|
|||
this.hasIn = hasIn;
|
||||
this.hasContains = hasContains;
|
||||
this.hasSlice = hasSlice;
|
||||
this.hasAnn = hasAnn;
|
||||
this.hasOnlyEqualityRestrictions = hasOnlyEqualityRestrictions;
|
||||
}
|
||||
|
||||
|
|
@ -168,18 +172,20 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
|
|||
public RestrictionSet addRestriction(SingleRestriction restriction)
|
||||
{
|
||||
// RestrictionSet is immutable so we need to clone the restrictions map.
|
||||
TreeMap<ColumnMetadata, SingleRestriction> newRestricitons = new TreeMap<>(this.restrictions);
|
||||
TreeMap<ColumnMetadata, SingleRestriction> newRestrictions = new TreeMap<>(this.restrictions);
|
||||
|
||||
boolean newHasIn = hasIn || restriction.isIN();
|
||||
boolean newHasContains = hasContains || restriction.isContains();
|
||||
boolean newHasSlice = hasSlice || restriction.isSlice();
|
||||
boolean newHasAnn = hasAnn || restriction.isANN();
|
||||
boolean newHasOnlyEqualityRestrictions = hasOnlyEqualityRestrictions && (restriction.isEQ() || restriction.isIN());
|
||||
|
||||
return new RestrictionSet(mergeRestrictions(newRestricitons, restriction),
|
||||
return new RestrictionSet(mergeRestrictions(newRestrictions, restriction),
|
||||
hasMultiColumnRestrictions || restriction.isMultiColumn(),
|
||||
newHasIn,
|
||||
newHasContains,
|
||||
newHasSlice,
|
||||
newHasAnn,
|
||||
newHasOnlyEqualityRestrictions);
|
||||
}
|
||||
|
||||
|
|
@ -244,6 +250,30 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndex(IndexRegistry indexRegistry)
|
||||
{
|
||||
for (SingleRestriction restriction : restrictions.values())
|
||||
{
|
||||
Index index = restriction.findSupportingIndex(indexRegistry);
|
||||
if (index != null)
|
||||
return index;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan)
|
||||
{
|
||||
for (SingleRestriction restriction : restrictions.values())
|
||||
{
|
||||
Index index = restriction.findSupportingIndexFromQueryPlan(indexQueryPlan);
|
||||
if (index != null)
|
||||
return index;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the column after the specified one.
|
||||
*
|
||||
|
|
@ -318,6 +348,11 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
|
|||
return hasSlice;
|
||||
}
|
||||
|
||||
public boolean hasAnn()
|
||||
{
|
||||
return hasAnn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all of the underlying restrictions are EQ or IN restrictions.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -90,6 +90,18 @@ class RestrictionSetWrapper implements Restrictions
|
|||
return restrictions.hasSupportingIndex(indexRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndex(IndexRegistry indexRegistry)
|
||||
{
|
||||
return restrictions.findSupportingIndex(indexRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan)
|
||||
{
|
||||
return restrictions.findSupportingIndexFromQueryPlan(indexQueryPlan);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsFiltering(Index.Group indexGroup)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -81,6 +81,26 @@ public abstract class SingleColumnRestriction implements SingleRestriction
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndex(IndexRegistry indexRegistry)
|
||||
{
|
||||
for (Index index : indexRegistry.listIndexes())
|
||||
if (isSupportedBy(index))
|
||||
return index;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan)
|
||||
{
|
||||
for (Index index : indexQueryPlan.getIndexes())
|
||||
if (isSupportedBy(index))
|
||||
return index;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsFiltering(Index.Group indexGroup)
|
||||
{
|
||||
|
|
@ -804,4 +824,69 @@ public abstract class SingleColumnRestriction implements SingleRestriction
|
|||
return Pair.create(operator, newValue);
|
||||
}
|
||||
}
|
||||
public static final class AnnRestriction extends SingleColumnRestriction
|
||||
{
|
||||
private final Term value;
|
||||
|
||||
public AnnRestriction(ColumnMetadata columnDef, Term value)
|
||||
{
|
||||
super(columnDef);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public ByteBuffer value(QueryOptions options)
|
||||
{
|
||||
return value.bindAndGet(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isANN()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFunctionsTo(List<Function> functions)
|
||||
{
|
||||
value.addFunctionsTo(functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
MultiColumnRestriction toMultiColumnRestriction()
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToRowFilter(RowFilter filter,
|
||||
IndexRegistry indexRegistry,
|
||||
QueryOptions options)
|
||||
{
|
||||
filter.add(columnDef, Operator.ANN, value.bindAndGet(options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiCBuilder appendTo(MultiCBuilder builder, QueryOptions options)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("ANN(%s)", value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SingleRestriction doMergeWith(SingleRestriction otherRestriction)
|
||||
{
|
||||
throw invalidRequest("%s cannot be restricted by more than one relation in an ANN ordering", columnDef.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSupportedBy(Index index)
|
||||
{
|
||||
return index.supportsExpression(columnDef, Operator.ANN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ public interface SingleRestriction extends Restriction
|
|||
return false;
|
||||
}
|
||||
|
||||
public default boolean isANN()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if this restriction is based on equality comparison rather than a range or negation
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -19,8 +19,12 @@ package org.apache.cassandra.cql3.restrictions;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Streams;
|
||||
|
||||
import org.apache.cassandra.cql3.*;
|
||||
import org.apache.cassandra.cql3.functions.Function;
|
||||
|
|
@ -30,6 +34,8 @@ import org.apache.cassandra.db.*;
|
|||
import org.apache.cassandra.db.filter.RowFilter;
|
||||
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.db.marshal.FloatType;
|
||||
import org.apache.cassandra.db.marshal.VectorType;
|
||||
import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
|
||||
import org.apache.cassandra.db.virtual.VirtualTable;
|
||||
import org.apache.cassandra.dht.*;
|
||||
|
|
@ -63,6 +69,14 @@ public final class StatementRestrictions
|
|||
"Executing this query despite the performance unpredictability with ALLOW FILTERING has been disabled " +
|
||||
"by the allow_filtering_enabled property in cassandra.yaml";
|
||||
|
||||
public static final String ANN_REQUIRES_INDEX_MESSAGE = "ANN ordering by vector requires the column to be indexed";
|
||||
|
||||
public static final String VECTOR_INDEXES_ANN_ONLY_MESSAGE = "Vector indexes only support ANN queries";
|
||||
|
||||
public static final String ANN_ONLY_SUPPORTED_ON_VECTOR_MESSAGE = "ANN ordering is only supported on float vector indexes";
|
||||
|
||||
public static final String ANN_REQUIRES_INDEXED_FILTERING_MESSAGE = "ANN ordering by vector requires all restricted column(s) to be indexed";
|
||||
|
||||
/**
|
||||
* The type of statement
|
||||
*/
|
||||
|
|
@ -138,11 +152,12 @@ public final class StatementRestrictions
|
|||
TableMetadata table,
|
||||
WhereClause whereClause,
|
||||
VariableSpecifications boundNames,
|
||||
List<Ordering> orderings,
|
||||
boolean selectsOnlyStaticColumns,
|
||||
boolean allowFiltering,
|
||||
boolean forView)
|
||||
{
|
||||
this(state, type, table, whereClause, boundNames, selectsOnlyStaticColumns, type.allowUseOfSecondaryIndices(), allowFiltering, forView);
|
||||
this(state, type, table, whereClause, boundNames, orderings, selectsOnlyStaticColumns, type.allowUseOfSecondaryIndices(), allowFiltering, forView);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -154,6 +169,7 @@ public final class StatementRestrictions
|
|||
TableMetadata table,
|
||||
WhereClause whereClause,
|
||||
VariableSpecifications boundNames,
|
||||
List<Ordering> orderings,
|
||||
boolean selectsOnlyStaticColumns,
|
||||
boolean allowUseOfSecondaryIndices,
|
||||
boolean allowFiltering,
|
||||
|
|
@ -161,9 +177,7 @@ public final class StatementRestrictions
|
|||
{
|
||||
this(type, table, allowFiltering);
|
||||
|
||||
IndexRegistry indexRegistry = null;
|
||||
if (type.allowUseOfSecondaryIndices())
|
||||
indexRegistry = IndexRegistry.obtain(table);
|
||||
final IndexRegistry indexRegistry = type.allowUseOfSecondaryIndices() ? IndexRegistry.obtain(table) : null;
|
||||
|
||||
/*
|
||||
* WHERE clause. For a given entity, rules are:
|
||||
|
|
@ -206,6 +220,10 @@ public final class StatementRestrictions
|
|||
}
|
||||
}
|
||||
|
||||
// ORDER BY clause.
|
||||
// Some indexes can be used for ordering.
|
||||
nonPrimaryKeyRestrictions = addOrderingRestrictions(orderings, nonPrimaryKeyRestrictions);
|
||||
|
||||
hasRegularColumnsRestrictions = nonPrimaryKeyRestrictions.hasRestrictionFor(ColumnMetadata.Kind.REGULAR);
|
||||
|
||||
boolean hasQueriableClusteringColumnIndex = false;
|
||||
|
|
@ -274,10 +292,55 @@ public final class StatementRestrictions
|
|||
throw invalidRequest("Non PRIMARY KEY columns found in where clause: %s ",
|
||||
Joiner.on(", ").join(nonPrimaryKeyColumns));
|
||||
}
|
||||
|
||||
var annRestriction = Streams.stream(nonPrimaryKeyRestrictions).filter(SingleRestriction::isANN).findFirst();
|
||||
if (annRestriction.isPresent())
|
||||
{
|
||||
// If there is an ANN restriction then it must be for a vector<float, n> column, and it must have an index
|
||||
var annColumn = annRestriction.get().getFirstColumn();
|
||||
|
||||
if (!annColumn.type.isVector() || !(((VectorType<?>)annColumn.type).elementType instanceof FloatType))
|
||||
throw invalidRequest(StatementRestrictions.ANN_ONLY_SUPPORTED_ON_VECTOR_MESSAGE);
|
||||
if (indexRegistry == null || indexRegistry.listIndexes().stream().noneMatch(i -> i.dependsOn(annColumn)))
|
||||
throw invalidRequest(StatementRestrictions.ANN_REQUIRES_INDEX_MESSAGE);
|
||||
// We do not allow ANN queries using partition key restrictions that need filtering
|
||||
if (partitionKeyRestrictions.needFiltering(table))
|
||||
throw invalidRequest(StatementRestrictions.ANN_REQUIRES_INDEXED_FILTERING_MESSAGE);
|
||||
// We do not allow ANN query filtering using non-indexed columns
|
||||
var nonAnnColumns = Streams.stream(nonPrimaryKeyRestrictions)
|
||||
.filter(r -> !r.isANN())
|
||||
.map(Restriction::getFirstColumn)
|
||||
.collect(Collectors.toList());
|
||||
var clusteringColumns = clusteringColumnsRestrictions.getColumnDefinitions();
|
||||
if (!nonAnnColumns.isEmpty() || !clusteringColumns.isEmpty())
|
||||
{
|
||||
var nonIndexedColumns = Stream.concat(nonAnnColumns.stream(), clusteringColumns.stream())
|
||||
.filter(c -> indexRegistry.listIndexes().stream().noneMatch(i -> i.dependsOn(c)))
|
||||
.collect(Collectors.toList());
|
||||
if (!nonIndexedColumns.isEmpty())
|
||||
throw invalidRequest(StatementRestrictions.ANN_REQUIRES_INDEXED_FILTERING_MESSAGE);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We do not support indexed vector restrictions that are not part of an ANN ordering
|
||||
var vectorColumn = nonPrimaryKeyRestrictions.getColumnDefs()
|
||||
.stream()
|
||||
.filter(c -> c.type.isVector())
|
||||
.findFirst();
|
||||
if (vectorColumn.isPresent() && indexRegistry.listIndexes().stream().anyMatch(i -> i.dependsOn(vectorColumn.get())))
|
||||
throw invalidRequest(StatementRestrictions.VECTOR_INDEXES_ANN_ONLY_MESSAGE);
|
||||
}
|
||||
|
||||
if (hasQueriableIndex)
|
||||
{
|
||||
usesSecondaryIndexing = true;
|
||||
else if (!allowFiltering && requiresAllowFilteringIfNotSpecified())
|
||||
throw invalidRequest(allowFilteringMessage(state));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!allowFiltering && requiresAllowFilteringIfNotSpecified())
|
||||
throw invalidRequest(allowFilteringMessage(state));
|
||||
}
|
||||
|
||||
filterRestrictions.add(nonPrimaryKeyRestrictions);
|
||||
}
|
||||
|
|
@ -450,6 +513,10 @@ public final class StatementRestrictions
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean isTopK()
|
||||
{
|
||||
return nonPrimaryKeyRestrictions.hasAnn();
|
||||
}
|
||||
/**
|
||||
* Returns the <code>Restrictions</code> for the specified type of columns.
|
||||
*
|
||||
|
|
@ -476,6 +543,38 @@ public final class StatementRestrictions
|
|||
return this.usesSecondaryIndexing;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a hack to push ordering down to indexes.
|
||||
* Indexes are selected based on RowFilter only, so we need to turn orderings into restrictions
|
||||
* so they end up in the row filter.
|
||||
*
|
||||
* @param orderings orderings from the select statement
|
||||
* @return the {@link RestrictionSet} with the added orderings
|
||||
*/
|
||||
private RestrictionSet addOrderingRestrictions(List<Ordering> orderings, RestrictionSet restrictionSet)
|
||||
{
|
||||
List<Ordering> annOrderings = orderings.stream().filter(o -> o.expression.hasNonClusteredOrdering()).collect(Collectors.toList());
|
||||
|
||||
if (annOrderings.size() > 1)
|
||||
throw new InvalidRequestException("Cannot specify more than one ANN ordering");
|
||||
else if (annOrderings.size() == 1)
|
||||
{
|
||||
if (orderings.size() > 1)
|
||||
throw new InvalidRequestException("ANN ordering does not support any other ordering");
|
||||
Ordering annOrdering = annOrderings.get(0);
|
||||
if (annOrdering.direction != Ordering.Direction.ASC)
|
||||
throw new InvalidRequestException("Descending ANN ordering is not supported");
|
||||
SingleRestriction restriction = annOrdering.expression.toRestriction();
|
||||
return restrictionSet.addRestriction(restriction);
|
||||
}
|
||||
return restrictionSet;
|
||||
}
|
||||
|
||||
private static Iterable<Restriction> allColumnRestrictions(ClusteringColumnRestrictions clusteringColumnsRestrictions, RestrictionSet nonPrimaryKeyRestrictions)
|
||||
{
|
||||
return Iterables.concat(clusteringColumnsRestrictions.getRestrictionSet(), nonPrimaryKeyRestrictions);
|
||||
}
|
||||
|
||||
private void processPartitionKeyRestrictions(ClientState state, boolean hasQueriableIndex, boolean allowFiltering, boolean forView)
|
||||
{
|
||||
if (!type.allowPartitionKeyRanges())
|
||||
|
|
|
|||
|
|
@ -286,6 +286,18 @@ final class TokenFilter implements PartitionKeyRestrictions
|
|||
restrictions.addToRowFilter(filter, indexRegistry, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndex(IndexRegistry indexRegistry)
|
||||
{
|
||||
return restrictions.findSupportingIndex(indexRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan)
|
||||
{
|
||||
return restrictions.findSupportingIndexFromQueryPlan(indexQueryPlan);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsFiltering(Index.Group indexGroup)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -129,6 +129,18 @@ public abstract class TokenRestriction implements PartitionKeyRestrictions
|
|||
throw new UnsupportedOperationException("Index expression cannot be created for token restriction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndex(IndexRegistry indexRegistry)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Index findSupportingIndexFromQueryPlan(Index.QueryPlan indexQueryPlan)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsFiltering(Index.Group indexGroup)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
package org.apache.cassandra.cql3.statements;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.cassandra.audit.AuditLogContext;
|
||||
|
|
@ -167,7 +168,8 @@ public class DeleteStatement extends ModificationStatement
|
|||
bindVariables,
|
||||
operations,
|
||||
whereClause,
|
||||
conditions);
|
||||
conditions,
|
||||
Collections.emptyList());
|
||||
|
||||
DeleteStatement stmt = new DeleteStatement(bindVariables,
|
||||
metadata,
|
||||
|
|
|
|||
|
|
@ -1037,13 +1037,14 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
|
|||
VariableSpecifications boundNames,
|
||||
Operations operations,
|
||||
WhereClause where,
|
||||
Conditions conditions)
|
||||
Conditions conditions,
|
||||
List<Ordering> orderings)
|
||||
{
|
||||
if (where.containsCustomExpressions())
|
||||
throw new InvalidRequestException(CUSTOM_EXPRESSIONS_NOT_ALLOWED);
|
||||
|
||||
boolean applyOnlyToStaticColumns = appliesOnlyToStaticColumns(operations, conditions);
|
||||
return new StatementRestrictions(state, type, metadata, where, boundNames, applyOnlyToStaticColumns, false, false);
|
||||
return new StatementRestrictions(state, type, metadata, where, boundNames, orderings, applyOnlyToStaticColumns, false, false);
|
||||
}
|
||||
|
||||
public List<Pair<ColumnIdentifier, ColumnCondition.Raw>> getConditions()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import javax.annotation.concurrent.ThreadSafe;
|
|||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
|
|
@ -35,7 +36,9 @@ import org.slf4j.LoggerFactory;
|
|||
import org.apache.cassandra.audit.AuditLogContext;
|
||||
import org.apache.cassandra.audit.AuditLogEntryType;
|
||||
import org.apache.cassandra.auth.Permission;
|
||||
import org.apache.cassandra.cql3.restrictions.SingleRestriction;
|
||||
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||
import org.apache.cassandra.index.Index;
|
||||
import org.apache.cassandra.schema.ColumnMetadata;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.schema.SchemaConstants;
|
||||
|
|
@ -109,6 +112,15 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(SelectStatement.logger, 1, TimeUnit.MINUTES);
|
||||
|
||||
public static final int DEFAULT_PAGE_SIZE = 10000;
|
||||
public static final String TOPK_CONSISTENCY_LEVEL_ERROR = "Top-K queries can only be run with consistency level ONE/LOCAL_ONE. Consistency level %s was used.";
|
||||
public static final String TOPK_LIMIT_ERROR = "Top-K queries must have a limit specified and the limit must be less than the query page size";
|
||||
public static final String TOPK_PARTITION_LIMIT_ERROR = "Top-K queries do not support per-partition limits";
|
||||
public static final String TOPK_AGGREGATION_ERROR = "Top-K queries can not be run with aggregation";
|
||||
public static final String TOPK_CONSISTENCY_LEVEL_WARNING = "Top-K queries can only be run with consistency level ONE " +
|
||||
"/ LOCAL_ONE / NODE_LOCAL. Consistency level %s was requested. " +
|
||||
"Downgrading the consistency level to %s.";
|
||||
public static final String TOPK_PAGE_SIZE_WARNING = "Top-K queries do not support paging and the page size is set to %d, " +
|
||||
"which is less than LIMIT %d. The page size has been set to %<d to match the LIMIT.";
|
||||
|
||||
public final VariableSpecifications bindVariables;
|
||||
public final TableMetadata table;
|
||||
|
|
@ -129,10 +141,10 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
/**
|
||||
* The comparator used to orders results when multiple keys are selected (using IN).
|
||||
*/
|
||||
private final Comparator<List<ByteBuffer>> orderingComparator;
|
||||
private final ColumnComparator<List<ByteBuffer>> orderingComparator;
|
||||
|
||||
// Used by forSelection below
|
||||
private static final Parameters defaultParameters = new Parameters(Collections.emptyMap(),
|
||||
private static final Parameters defaultParameters = new Parameters(Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
false,
|
||||
false,
|
||||
|
|
@ -145,7 +157,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
StatementRestrictions restrictions,
|
||||
boolean isReversed,
|
||||
AggregationSpecification.Factory aggregationSpecFactory,
|
||||
Comparator<List<ByteBuffer>> orderingComparator,
|
||||
ColumnComparator<List<ByteBuffer>> orderingComparator,
|
||||
Term limit,
|
||||
Term perPartitionLimit)
|
||||
{
|
||||
|
|
@ -281,14 +293,51 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
|
||||
Selectors selectors = selection.newSelectors(options);
|
||||
AggregationSpecification aggregationSpec = getAggregationSpec(options);
|
||||
ReadQuery query = getQuery(options, state.getClientState(), selectors.getColumnFilter(),
|
||||
nowInSec, userLimit, userPerPartitionLimit, pageSize, aggregationSpec);
|
||||
DataLimits limit = getDataLimits(userLimit, userPerPartitionLimit, pageSize, aggregationSpec);
|
||||
|
||||
// Handle additional validation for topK queries
|
||||
if (restrictions.isTopK())
|
||||
{
|
||||
checkFalse(aggregationSpec != null, TOPK_AGGREGATION_ERROR);
|
||||
|
||||
// We aren't going to allow SERIAL at all, so we can error out on those.
|
||||
checkFalse(options.getConsistency() == ConsistencyLevel.LOCAL_SERIAL ||
|
||||
options.getConsistency() == ConsistencyLevel.SERIAL,
|
||||
String.format(TOPK_CONSISTENCY_LEVEL_ERROR, options.getConsistency()));
|
||||
|
||||
if (options.getConsistency() != ConsistencyLevel.ONE &&
|
||||
options.getConsistency() != ConsistencyLevel.LOCAL_ONE &&
|
||||
options.getConsistency() != ConsistencyLevel.NODE_LOCAL)
|
||||
{
|
||||
ConsistencyLevel supplied = options.getConsistency();
|
||||
ConsistencyLevel downgrade = supplied.isDatacenterLocal() ? ConsistencyLevel.LOCAL_ONE : ConsistencyLevel.ONE;
|
||||
|
||||
options = QueryOptions.withConsistencyLevel(options, downgrade);
|
||||
|
||||
ClientWarn.instance.warn(String.format(TOPK_CONSISTENCY_LEVEL_WARNING, supplied, downgrade));
|
||||
}
|
||||
|
||||
checkFalse(limit.isUnlimited(), TOPK_LIMIT_ERROR);
|
||||
|
||||
checkFalse(limit.perPartitionCount() != DataLimits.NO_LIMIT, TOPK_PARTITION_LIMIT_ERROR);
|
||||
|
||||
if (pageSize > 0 && pageSize < limit.count())
|
||||
{
|
||||
int oldPageSize = pageSize;
|
||||
pageSize = limit.count();
|
||||
limit = getDataLimits(userLimit, userPerPartitionLimit, pageSize, aggregationSpec);
|
||||
options = QueryOptions.withPageSize(options, pageSize);
|
||||
ClientWarn.instance.warn(String.format(TOPK_PAGE_SIZE_WARNING, oldPageSize, limit.count()));
|
||||
}
|
||||
}
|
||||
|
||||
ReadQuery query = getQuery(options, state.getClientState(), selectors.getColumnFilter(), nowInSec, limit);
|
||||
|
||||
if (options.isReadThresholdsEnabled())
|
||||
query.trackWarnings();
|
||||
ResultMessage.Rows rows;
|
||||
|
||||
if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize)))
|
||||
if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize) || query.isTopK()))
|
||||
{
|
||||
rows = execute(query, options, state.getClientState(), selectors, nowInSec, userLimit, null, queryStartNanoTime, unmask);
|
||||
}
|
||||
|
|
@ -341,10 +390,19 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
int pageSize,
|
||||
AggregationSpecification aggregationSpec)
|
||||
{
|
||||
boolean isPartitionRangeQuery = restrictions.isKeyRange() || restrictions.usesSecondaryIndexing();
|
||||
|
||||
DataLimits limit = getDataLimits(userLimit, perPartitionLimit, pageSize, aggregationSpec);
|
||||
|
||||
return getQuery(options, state, columnFilter, nowInSec, limit);
|
||||
}
|
||||
|
||||
public ReadQuery getQuery(QueryOptions options,
|
||||
ClientState state,
|
||||
ColumnFilter columnFilter,
|
||||
long nowInSec,
|
||||
DataLimits limit)
|
||||
{
|
||||
boolean isPartitionRangeQuery = restrictions.isKeyRange() || restrictions.usesSecondaryIndexing();
|
||||
|
||||
if (isPartitionRangeQuery)
|
||||
return getRangeCommand(options, state, columnFilter, limit, nowInSec);
|
||||
|
||||
|
|
@ -483,7 +541,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
|
||||
// Please note that the isExhausted state of the pager only gets updated when we've closed the page, so this
|
||||
// shouldn't be moved inside the 'try' above.
|
||||
if (!pager.isExhausted())
|
||||
if (!pager.isExhausted() && !pager.pager.isTopK())
|
||||
msg.result.metadata.setHasMorePages(pager.state());
|
||||
|
||||
return msg;
|
||||
|
|
@ -535,7 +593,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
|
||||
try (ReadExecutionController executionController = query.executionController())
|
||||
{
|
||||
if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize)))
|
||||
if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize) || query.isTopK()))
|
||||
{
|
||||
try (PartitionIterator data = query.executeInternal(executionController))
|
||||
{
|
||||
|
|
@ -822,7 +880,9 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
// If we do post ordering we need to get all the results sorted before we can trim them.
|
||||
if (aggregationSpec != AggregationSpecification.AGGREGATE_EVERYTHING)
|
||||
{
|
||||
if (!needsPostQueryOrdering())
|
||||
// If we aren't need post-query ordering but we are doing index ordering (currently ANN only) then
|
||||
// we do need to use the user limit.
|
||||
if (!needsPostQueryOrdering() || needIndexOrdering())
|
||||
cqlRowLimit = userLimit;
|
||||
cqlPerPartitionLimit = perPartitionLimit;
|
||||
}
|
||||
|
|
@ -939,7 +999,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
ResultSet cqlRows = result.build();
|
||||
maybeWarn(result, options);
|
||||
|
||||
orderResults(cqlRows);
|
||||
orderResults(cqlRows, options);
|
||||
|
||||
cqlRows.trim(userLimit);
|
||||
|
||||
|
|
@ -1080,19 +1140,28 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
|
||||
private boolean needsPostQueryOrdering()
|
||||
{
|
||||
// We need post-query ordering only for queries with IN on the partition key and an ORDER BY.
|
||||
return restrictions.keyIsInRelation() && !parameters.orderings.isEmpty();
|
||||
// We need post-query ordering only for queries with IN on the partition key and an ORDER BY or index restriction reordering
|
||||
return restrictions.keyIsInRelation() && !parameters.orderings.isEmpty() || needIndexOrdering();
|
||||
}
|
||||
|
||||
private boolean needIndexOrdering()
|
||||
{
|
||||
return orderingComparator != null && orderingComparator.indexOrdering();
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders results when multiple keys are selected (using IN)
|
||||
* Orders results when multiple keys are selected (using IN).
|
||||
* <p>
|
||||
* In the case of ANN ordering the rows are first ordered in index column order and then by primary key.
|
||||
*/
|
||||
private void orderResults(ResultSet cqlRows)
|
||||
private void orderResults(ResultSet cqlRows, QueryOptions options)
|
||||
{
|
||||
if (cqlRows.size() == 0 || !needsPostQueryOrdering())
|
||||
return;
|
||||
|
||||
Collections.sort(cqlRows.rows, orderingComparator);
|
||||
Comparator<List<ByteBuffer>> comparator = orderingComparator.prepareFor(table, getRowFilter(options), options);
|
||||
if (comparator != null)
|
||||
cqlRows.rows.sort(comparator);
|
||||
}
|
||||
|
||||
public static class RawStatement extends QualifiedStatement
|
||||
|
|
@ -1133,13 +1202,13 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
List<Selectable> selectables = RawSelector.toSelectables(selectClause, table);
|
||||
boolean containsOnlyStaticColumns = selectOnlyStaticColumns(table, selectables);
|
||||
|
||||
StatementRestrictions restrictions = prepareRestrictions(state, table, bindVariables, containsOnlyStaticColumns, forView);
|
||||
List<Ordering> orderings = getOrderings(table);
|
||||
StatementRestrictions restrictions = prepareRestrictions(state, table, bindVariables, orderings, containsOnlyStaticColumns, forView);
|
||||
|
||||
// If we order post-query, the sorted column needs to be in the ResultSet for sorting,
|
||||
// even if we don't ultimately ship them to the client (CASSANDRA-4911).
|
||||
Map<ColumnMetadata, Boolean> orderingColumns = getOrderingColumns(table);
|
||||
Set<ColumnMetadata> resultSetOrderingColumns = restrictions.keyIsInRelation() ? orderingColumns.keySet()
|
||||
: Collections.emptySet();
|
||||
Map<ColumnMetadata, Ordering> orderingColumns = getOrderingColumns(orderings);
|
||||
Set<ColumnMetadata> resultSetOrderingColumns = getResultSetOrdering(restrictions, orderingColumns);
|
||||
|
||||
Selection selection = prepareSelection(table,
|
||||
selectables,
|
||||
|
|
@ -1163,18 +1232,18 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
&& perPartitionLimit != null,
|
||||
"PER PARTITION LIMIT is not allowed with aggregate queries.");
|
||||
|
||||
Comparator<List<ByteBuffer>> orderingComparator = null;
|
||||
ColumnComparator<List<ByteBuffer>> orderingComparator = null;
|
||||
boolean isReversed = false;
|
||||
|
||||
if (!orderingColumns.isEmpty())
|
||||
{
|
||||
assert !forView;
|
||||
verifyOrderingIsAllowed(restrictions);
|
||||
verifyOrderingIsAllowed(restrictions, orderingColumns);
|
||||
orderingComparator = getOrderingComparator(selection, restrictions, orderingColumns);
|
||||
isReversed = isReversed(table, orderingColumns, restrictions);
|
||||
if (isReversed)
|
||||
orderingComparator = Collections.reverseOrder(orderingComparator);
|
||||
}
|
||||
if (isReversed && orderingComparator != null)
|
||||
orderingComparator = orderingComparator.reverse();
|
||||
}
|
||||
|
||||
checkNeedsFiltering(table, restrictions);
|
||||
|
||||
|
|
@ -1190,6 +1259,13 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
prepareLimit(bindVariables, perPartitionLimit, keyspace(), perPartitionLimitReceiver()));
|
||||
}
|
||||
|
||||
private Set<ColumnMetadata> getResultSetOrdering(StatementRestrictions restrictions, Map<ColumnMetadata, Ordering> orderingColumns)
|
||||
{
|
||||
if (restrictions.keyIsInRelation() || orderingColumns.values().stream().anyMatch(o -> o.expression.hasNonClusteredOrdering()))
|
||||
return orderingColumns.keySet();
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
private Selection prepareSelection(TableMetadata table,
|
||||
List<Selectable> selectables,
|
||||
VariableSpecifications boundNames,
|
||||
|
|
@ -1242,22 +1318,30 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the columns used to order the data.
|
||||
* @return the columns used to order the data.
|
||||
* Returns the columns in the same order as given list used to order the data.
|
||||
* @return the columns in the same order as given list used to order the data.
|
||||
*/
|
||||
private Map<ColumnMetadata, Boolean> getOrderingColumns(TableMetadata table)
|
||||
private Map<ColumnMetadata, Ordering> getOrderingColumns(List<Ordering> orderings)
|
||||
{
|
||||
if (parameters.orderings.isEmpty())
|
||||
if (orderings.isEmpty())
|
||||
return Collections.emptyMap();
|
||||
|
||||
Map<ColumnMetadata, Boolean> orderingColumns = new LinkedHashMap<>();
|
||||
for (Map.Entry<ColumnIdentifier, Boolean> entry : parameters.orderings.entrySet())
|
||||
Map<ColumnMetadata, Ordering> orderingColumns = new LinkedHashMap<>();
|
||||
for (Ordering ordering : orderings)
|
||||
{
|
||||
orderingColumns.put(table.getExistingColumn(entry.getKey()), entry.getValue());
|
||||
ColumnMetadata column = ordering.expression.getColumn();
|
||||
orderingColumns.put(column, ordering);
|
||||
}
|
||||
return orderingColumns;
|
||||
}
|
||||
|
||||
private List<Ordering> getOrderings(TableMetadata table)
|
||||
{
|
||||
return parameters.orderings.stream()
|
||||
.map(o -> o.bind(table, bindVariables))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the restrictions.
|
||||
*
|
||||
|
|
@ -1270,6 +1354,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
private StatementRestrictions prepareRestrictions(ClientState state,
|
||||
TableMetadata metadata,
|
||||
VariableSpecifications boundNames,
|
||||
List<Ordering> orderings,
|
||||
boolean selectsOnlyStaticColumns,
|
||||
boolean forView) throws InvalidRequestException
|
||||
{
|
||||
|
|
@ -1278,6 +1363,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
metadata,
|
||||
whereClause,
|
||||
boundNames,
|
||||
orderings,
|
||||
selectsOnlyStaticColumns,
|
||||
parameters.allowFiltering,
|
||||
forView);
|
||||
|
|
@ -1295,9 +1381,11 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
return prepLimit;
|
||||
}
|
||||
|
||||
private static void verifyOrderingIsAllowed(StatementRestrictions restrictions) throws InvalidRequestException
|
||||
private static void verifyOrderingIsAllowed(StatementRestrictions restrictions, Map<ColumnMetadata, Ordering> orderingColumns) throws InvalidRequestException
|
||||
{
|
||||
checkFalse(restrictions.usesSecondaryIndexing(), "ORDER BY with 2ndary indexes is not supported.");
|
||||
if (orderingColumns.values().stream().anyMatch(o -> o.expression.hasNonClusteredOrdering()))
|
||||
return;
|
||||
checkFalse(restrictions.usesSecondaryIndexing(), "ORDER BY with 2ndary indexes is not supported, except for ANN queries.");
|
||||
checkFalse(restrictions.isKeyRange(), "ORDER BY is only supported when the partition key is restricted by an EQ or an IN.");
|
||||
}
|
||||
|
||||
|
|
@ -1424,11 +1512,19 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
checkFalse(f.isAggregate(), "Aggregate functions are not supported within the GROUP BY clause, got: %s", f.name());
|
||||
}
|
||||
|
||||
private Comparator<List<ByteBuffer>> getOrderingComparator(Selection selection,
|
||||
StatementRestrictions restrictions,
|
||||
Map<ColumnMetadata, Boolean> orderingColumns)
|
||||
throws InvalidRequestException
|
||||
private ColumnComparator<List<ByteBuffer>> getOrderingComparator(Selection selection,
|
||||
StatementRestrictions restrictions,
|
||||
Map<ColumnMetadata, Ordering> orderingColumns) throws InvalidRequestException
|
||||
{
|
||||
for (Map.Entry<ColumnMetadata, Ordering> e : orderingColumns.entrySet())
|
||||
{
|
||||
if (e.getValue().expression.hasNonClusteredOrdering())
|
||||
{
|
||||
Preconditions.checkState(orderingColumns.size() == 1);
|
||||
return new IndexColumnComparator(e.getValue().expression.toRestriction(), selection.getOrderingIndex(e.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!restrictions.keyIsInRelation())
|
||||
return null;
|
||||
|
||||
|
|
@ -1444,14 +1540,17 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
: new CompositeComparator(sorters, idToSort);
|
||||
}
|
||||
|
||||
private boolean isReversed(TableMetadata table, Map<ColumnMetadata, Boolean> orderingColumns, StatementRestrictions restrictions) throws InvalidRequestException
|
||||
private boolean isReversed(TableMetadata table, Map<ColumnMetadata, Ordering> orderingColumns, StatementRestrictions restrictions) throws InvalidRequestException
|
||||
{
|
||||
if (orderingColumns.values().stream().anyMatch(o -> o.expression.hasNonClusteredOrdering()))
|
||||
return false;
|
||||
Boolean[] reversedMap = new Boolean[table.clusteringColumns().size()];
|
||||
int i = 0;
|
||||
for (Map.Entry<ColumnMetadata, Boolean> entry : orderingColumns.entrySet())
|
||||
for (var entry : orderingColumns.entrySet())
|
||||
{
|
||||
ColumnMetadata def = entry.getKey();
|
||||
boolean reversed = entry.getValue();
|
||||
Ordering ordering = entry.getValue();
|
||||
boolean reversed = ordering.direction == Ordering.Direction.DESC;
|
||||
|
||||
checkTrue(def.isClusteringColumn(),
|
||||
"Order by is currently only supported on the clustered columns of the PRIMARY KEY, got %s", def.name);
|
||||
|
|
@ -1522,13 +1621,13 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
public static class Parameters
|
||||
{
|
||||
// Public because CASSANDRA-9858
|
||||
public final Map<ColumnIdentifier, Boolean> orderings;
|
||||
public final List<Ordering.Raw> orderings;
|
||||
public final List<Selectable.Raw> groups;
|
||||
public final boolean isDistinct;
|
||||
public final boolean allowFiltering;
|
||||
public final boolean isJson;
|
||||
|
||||
public Parameters(Map<ColumnIdentifier, Boolean> orderings,
|
||||
public Parameters(List<Ordering.Raw> orderings,
|
||||
List<Selectable.Raw> groups,
|
||||
boolean isDistinct,
|
||||
boolean allowFiltering,
|
||||
|
|
@ -1551,6 +1650,43 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
|
||||
return bValue == null ? 1 : comparator.compare(aValue, bValue);
|
||||
}
|
||||
|
||||
public ColumnComparator<T> reverse()
|
||||
{
|
||||
return new ReversedColumnComparator<>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if ordering is performed by index
|
||||
*/
|
||||
public boolean indexOrdering()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a prepared {@link ColumnComparator} for current table and query-options
|
||||
*/
|
||||
public Comparator<T> prepareFor(TableMetadata table, RowFilter rowFilter, QueryOptions options)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReversedColumnComparator<T> extends ColumnComparator<T>
|
||||
{
|
||||
private final ColumnComparator<T> wrapped;
|
||||
|
||||
public ReversedColumnComparator(ColumnComparator<T> wrapped)
|
||||
{
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(T o1, T o2)
|
||||
{
|
||||
return wrapped.compare(o2, o1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1573,6 +1709,44 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
|
|||
}
|
||||
}
|
||||
|
||||
private static class IndexColumnComparator extends ColumnComparator<List<ByteBuffer>>
|
||||
{
|
||||
private final SingleRestriction restriction;
|
||||
private final int columnIndex;
|
||||
|
||||
public IndexColumnComparator(SingleRestriction restriction, int columnIndex)
|
||||
{
|
||||
this.restriction = restriction;
|
||||
this.columnIndex = columnIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean indexOrdering()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<List<ByteBuffer>> prepareFor(TableMetadata table, RowFilter rowFilter, QueryOptions options)
|
||||
{
|
||||
if (table.indexes.isEmpty() || rowFilter.isEmpty())
|
||||
return this;
|
||||
|
||||
Index.QueryPlan indexQueryPlan = Keyspace.openAndGetStore(table).indexManager.getBestIndexQueryPlanFor(rowFilter);
|
||||
|
||||
Index index = restriction.findSupportingIndexFromQueryPlan(indexQueryPlan);
|
||||
assert index != null;
|
||||
Comparator<ByteBuffer> comparator = index.getPostQueryOrdering(restriction, options);
|
||||
return (a, b) -> compare(comparator, a.get(columnIndex), b.get(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(List<ByteBuffer> o1, List<ByteBuffer> o2)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in orderResults(...) method when multiple 'ORDER BY' conditions where given
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ public class UpdateStatement extends ModificationStatement
|
|||
metadata,
|
||||
whereClause.build(),
|
||||
bindVariables,
|
||||
Collections.emptyList(),
|
||||
applyOnlyToStaticColumns,
|
||||
false,
|
||||
false);
|
||||
|
|
@ -253,6 +254,7 @@ public class UpdateStatement extends ModificationStatement
|
|||
metadata,
|
||||
whereClause.build(),
|
||||
bindVariables,
|
||||
Collections.emptyList(),
|
||||
applyOnlyToStaticColumns,
|
||||
false,
|
||||
false);
|
||||
|
|
@ -320,7 +322,8 @@ public class UpdateStatement extends ModificationStatement
|
|||
bindVariables,
|
||||
operations,
|
||||
whereClause,
|
||||
conditions);
|
||||
conditions,
|
||||
Collections.emptyList());
|
||||
|
||||
return new UpdateStatement(type,
|
||||
bindVariables,
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ public final class CreateViewStatement extends AlterSchemaStatement
|
|||
table,
|
||||
whereClause,
|
||||
VariableSpecifications.empty(),
|
||||
Collections.emptyList(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ import org.apache.cassandra.dht.Range;
|
|||
import org.apache.cassandra.dht.Splitter;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.StartupException;
|
||||
import org.apache.cassandra.index.SecondaryIndexManager;
|
||||
import org.apache.cassandra.index.internal.CassandraIndex;
|
||||
|
|
@ -1459,9 +1460,12 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner
|
|||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new RuntimeException(e.getMessage()
|
||||
+ " for ks: "
|
||||
+ getKeyspaceName() + ", table: " + name, e);
|
||||
String message = e.getMessage() + " for ks: " + keyspace.getName() + ", table: " + name;
|
||||
|
||||
if (e instanceof InvalidRequestException)
|
||||
throw new InvalidRequestException(message, e);
|
||||
|
||||
throw new RuntimeException(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR
|
|||
public PartitionIterator postReconciliationProcessing(PartitionIterator result)
|
||||
{
|
||||
Index.QueryPlan queryPlan = indexQueryPlan();
|
||||
return queryPlan == null ? result : queryPlan.postProcessor().apply(result);
|
||||
return queryPlan == null ? result : queryPlan.postProcessor(this).apply(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -254,6 +254,12 @@ public abstract class ReadCommand extends AbstractReadQuery
|
|||
return indexQueryPlan;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTopK()
|
||||
{
|
||||
return indexQueryPlan != null && indexQueryPlan.isTopK();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Index.Searcher indexSearcher()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -121,6 +121,9 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
|
|||
|
||||
if (replica == null)
|
||||
{
|
||||
if (command.isTopK())
|
||||
return;
|
||||
|
||||
logger.warn("Received a read request from {} for a range that is not owned by the current replica {}.",
|
||||
message.from(),
|
||||
command);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ public class ReadExecutionController implements AutoCloseable
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isRangeCommand()
|
||||
{
|
||||
return command != null && command.isRangeRequest();
|
||||
}
|
||||
|
||||
public ReadExecutionController indexReadController()
|
||||
{
|
||||
return indexController;
|
||||
|
|
|
|||
|
|
@ -256,4 +256,15 @@ public interface ReadQuery
|
|||
default void trackWarnings()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* The query is a top-k query if the query has an {@link org.apache.cassandra.index.Index.QueryPlan} that
|
||||
* supports top-k ordering.
|
||||
*
|
||||
* @return {@code true} if this is a top-k query
|
||||
*/
|
||||
default boolean isTopK()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -674,7 +674,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
|
|||
case GTE:
|
||||
case GT:
|
||||
{
|
||||
assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for 'complex' types";
|
||||
assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for collection types";
|
||||
|
||||
// In order to support operators on Counter types, their value has to be extracted from internal
|
||||
// representation. See CASSANDRA-11629
|
||||
|
|
@ -699,8 +699,9 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
|
|||
case LIKE_SUFFIX:
|
||||
case LIKE_CONTAINS:
|
||||
case LIKE_MATCHES:
|
||||
case ANN:
|
||||
{
|
||||
assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for 'complex' types";
|
||||
assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for collection types";
|
||||
ByteBuffer foundValue = getValue(metadata, partitionKey, row);
|
||||
// Note that CQL expression are always of the form 'x < 4', i.e. the tested value is on the left.
|
||||
return foundValue != null && operator.isSatisfiedBy(column.type, foundValue, value);
|
||||
|
|
|
|||
|
|
@ -236,6 +236,19 @@ public class ByteArrayAccessor implements ValueAccessor<byte[]>
|
|||
return ByteArrayUtil.getFloat(value, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] toFloatArray(byte[] value, int dimension)
|
||||
{
|
||||
float[] array = new float[dimension];
|
||||
int offset = 0;
|
||||
for (int i = 0; i < dimension; i++)
|
||||
{
|
||||
array[i] = getFloat(value, offset);
|
||||
offset += Float.BYTES;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(byte[] value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package org.apache.cassandra.db.marshal;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.UUID;
|
||||
|
|
@ -243,6 +244,18 @@ public class ByteBufferAccessor implements ValueAccessor<ByteBuffer>
|
|||
return ByteBufferUtil.toFloat(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] toFloatArray(ByteBuffer value, int dimension)
|
||||
{
|
||||
FloatBuffer floatBuffer = value.asFloatBuffer();
|
||||
if (floatBuffer.remaining() != dimension)
|
||||
throw new IllegalArgumentException(String.format("Could not convert to a float[] with different dimension. " +
|
||||
"Was expecting %d but got %d", dimension, floatBuffer.remaining()));
|
||||
float[] floatArray = new float[floatBuffer.remaining()];
|
||||
floatBuffer.get(floatArray);
|
||||
return floatArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double toDouble(ByteBuffer value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -361,6 +361,9 @@ public interface ValueAccessor<V>
|
|||
/** returns a TimeUUID from offset 0 */
|
||||
Ballot toBallot(V value);
|
||||
|
||||
/** returns a float[] from offset 0 */
|
||||
float[] toFloatArray(V value, int dimension);
|
||||
|
||||
/**
|
||||
* writes the byte value {@param value} to {@param dst} at offset {@param offset}
|
||||
* @return the number of bytes written to {@param value}
|
||||
|
|
|
|||
|
|
@ -158,14 +158,8 @@ public final class VectorType<T> extends AbstractType<List<T>>
|
|||
|
||||
if (isNull(input, accessor))
|
||||
return null;
|
||||
float[] array = new float[dimension];
|
||||
int offset = 0;
|
||||
for (int i = 0; i < dimension; i++)
|
||||
{
|
||||
array[i] = accessor.getFloat(input, offset);
|
||||
offset += Float.BYTES;
|
||||
}
|
||||
return array;
|
||||
|
||||
return accessor.toFloatArray(input, dimension);
|
||||
}
|
||||
|
||||
public ByteBuffer decompose(T... values)
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ public class View
|
|||
if (null == select)
|
||||
{
|
||||
SelectStatement.Parameters parameters =
|
||||
new SelectStatement.Parameters(Collections.emptyMap(),
|
||||
new SelectStatement.Parameters(Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
false,
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@
|
|||
package org.apache.cassandra.index;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
|
@ -34,6 +36,8 @@ import javax.annotation.Nonnull;
|
|||
import javax.annotation.Nullable;
|
||||
|
||||
import org.apache.cassandra.cql3.Operator;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.cql3.restrictions.Restriction;
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.db.DeletionTime;
|
||||
|
|
@ -452,6 +456,18 @@ public interface Index
|
|||
*/
|
||||
public RowFilter getPostIndexQueryFilter(RowFilter filter);
|
||||
|
||||
/**
|
||||
* Return a comparator that reorders query result before sending to client
|
||||
*
|
||||
* @param restriction restriction that requires current index
|
||||
* @param options query options
|
||||
* @return a comparator for post-query ordering; or null if not supported
|
||||
*/
|
||||
default Comparator<ByteBuffer> getPostQueryOrdering(Restriction restriction, QueryOptions options)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an estimate of the number of results this index is expected to return for any given
|
||||
* query that it can be used to answer. Used in conjunction with indexes() and supportsExpression()
|
||||
|
|
@ -960,8 +976,10 @@ public interface Index
|
|||
* The function takes a PartitionIterator of the results from the replicas which has already been collated
|
||||
* and reconciled, along with the command being executed. It returns another PartitionIterator containing the results
|
||||
* of the transformation (which may be the same as the input if the transformation is a no-op).
|
||||
*
|
||||
* @param command the read command being executed
|
||||
*/
|
||||
default Function<PartitionIterator, PartitionIterator> postProcessor()
|
||||
default Function<PartitionIterator, PartitionIterator> postProcessor(ReadCommand command)
|
||||
{
|
||||
return partitions -> partitions;
|
||||
}
|
||||
|
|
@ -995,6 +1013,14 @@ public interface Index
|
|||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if given index query plan is a top-k request
|
||||
*/
|
||||
default boolean isTopK()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -40,14 +40,17 @@ import org.apache.cassandra.db.marshal.AbstractType;
|
|||
import org.apache.cassandra.db.marshal.AsciiType;
|
||||
import org.apache.cassandra.db.marshal.BooleanType;
|
||||
import org.apache.cassandra.db.marshal.CompositeType;
|
||||
import org.apache.cassandra.db.marshal.FloatType;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.db.marshal.UUIDType;
|
||||
import org.apache.cassandra.db.marshal.VectorType;
|
||||
import org.apache.cassandra.db.rows.Cell;
|
||||
import org.apache.cassandra.db.rows.Row;
|
||||
import org.apache.cassandra.dht.IPartitioner;
|
||||
import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer;
|
||||
import org.apache.cassandra.index.sai.disk.SSTableIndex;
|
||||
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.metrics.ColumnQueryMetrics;
|
||||
import org.apache.cassandra.index.sai.metrics.IndexMetrics;
|
||||
|
|
@ -91,6 +94,7 @@ public class IndexContext
|
|||
private final IndexViewManager viewManager;
|
||||
private final IndexMetrics indexMetrics;
|
||||
private final ColumnQueryMetrics columnQueryMetrics;
|
||||
private final IndexWriterConfig indexWriterConfig;
|
||||
private final AbstractAnalyzer.AnalyzerFactory analyzerFactory;
|
||||
private final PrimaryKey.Factory primaryKeyFactory;
|
||||
|
||||
|
|
@ -115,6 +119,8 @@ public class IndexContext
|
|||
this.indexMetadata = indexMetadata;
|
||||
this.memtableIndexManager = indexMetadata == null ? null : new MemtableIndexManager(this);
|
||||
|
||||
this.indexWriterConfig = indexMetadata == null ? IndexWriterConfig.emptyConfig()
|
||||
: IndexWriterConfig.fromOptions(indexMetadata.name, validator, indexMetadata.options);
|
||||
this.indexMetrics = indexMetadata == null ? null : new IndexMetrics(this);
|
||||
this.viewManager = new IndexViewManager(this);
|
||||
this.columnQueryMetrics = isLiteral() ? new ColumnQueryMetrics.TrieIndexMetrics(this)
|
||||
|
|
@ -144,6 +150,11 @@ public class IndexContext
|
|||
return keyspace;
|
||||
}
|
||||
|
||||
public IndexWriterConfig getIndexWriterConfig()
|
||||
{
|
||||
return indexWriterConfig;
|
||||
}
|
||||
|
||||
public IndexMetrics getIndexMetrics()
|
||||
{
|
||||
return indexMetrics;
|
||||
|
|
@ -267,6 +278,12 @@ public class IndexContext
|
|||
op == Operator.LIKE_MATCHES ||
|
||||
op == Operator.LIKE_SUFFIX) return false;
|
||||
|
||||
// ANN is only supported against vectors, and vector indexes only support ANN
|
||||
if (isVector())
|
||||
return op == Operator.ANN;
|
||||
if (op == Operator.ANN)
|
||||
return false;
|
||||
|
||||
Expression.IndexOperator operator = Expression.IndexOperator.valueOf(op);
|
||||
|
||||
if (isNonFrozenCollection())
|
||||
|
|
@ -354,6 +371,11 @@ public class IndexContext
|
|||
return TypeUtil.isLiteral(getValidator());
|
||||
}
|
||||
|
||||
public boolean isVector()
|
||||
{
|
||||
return getValidator().isVector() && ((VectorType<?>)getValidator()).elementType instanceof FloatType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ public class QueryContext
|
|||
|
||||
public boolean queryTimedOut = false;
|
||||
|
||||
private VectorQueryContext vectorContext;
|
||||
|
||||
public QueryContext(ReadCommand readCommand, long executionQuotaMs)
|
||||
{
|
||||
this.readCommand = readCommand;
|
||||
|
|
@ -78,4 +80,11 @@ public class QueryContext
|
|||
throw new QueryCancelledException(readCommand);
|
||||
}
|
||||
}
|
||||
|
||||
public VectorQueryContext vectorContext()
|
||||
{
|
||||
if (vectorContext == null)
|
||||
vectorContext = new VectorQueryContext(readCommand);
|
||||
return vectorContext;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
package org.apache.cassandra.index.sai;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
|
@ -40,16 +41,21 @@ import java.util.function.BooleanSupplier;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.Futures; // checkstyle: permit this import
|
||||
import com.google.common.util.concurrent.ListenableFuture; // checkstyle: permit this import
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQL3Type;
|
||||
import org.apache.cassandra.cql3.CqlBuilder;
|
||||
import org.apache.cassandra.cql3.Operator;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.cql3.restrictions.Restriction;
|
||||
import org.apache.cassandra.cql3.restrictions.SingleColumnRestriction;
|
||||
import org.apache.cassandra.cql3.statements.schema.IndexTarget;
|
||||
import org.apache.cassandra.db.CassandraWriteContext;
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
|
|
@ -61,6 +67,8 @@ import org.apache.cassandra.db.compaction.CompactionManager;
|
|||
import org.apache.cassandra.db.filter.RowFilter;
|
||||
import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.db.marshal.FloatType;
|
||||
import org.apache.cassandra.db.marshal.VectorType;
|
||||
import org.apache.cassandra.db.memtable.Memtable;
|
||||
import org.apache.cassandra.db.partitions.PartitionUpdate;
|
||||
import org.apache.cassandra.db.rows.Row;
|
||||
|
|
@ -79,6 +87,7 @@ import org.apache.cassandra.index.sai.analyzer.NonTokenizingOptions;
|
|||
import org.apache.cassandra.index.sai.disk.SSTableIndex;
|
||||
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.utils.TypeUtil;
|
||||
import org.apache.cassandra.index.sai.view.View;
|
||||
import org.apache.cassandra.index.transactions.IndexTransaction;
|
||||
|
|
@ -90,17 +99,36 @@ import org.apache.cassandra.io.sstable.format.SSTableReader;
|
|||
import org.apache.cassandra.schema.ColumnMetadata;
|
||||
import org.apache.cassandra.schema.IndexMetadata;
|
||||
import org.apache.cassandra.schema.TableMetadata;
|
||||
import org.apache.cassandra.service.ClientWarn;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
import org.apache.cassandra.utils.concurrent.OpOrder;
|
||||
|
||||
import static org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig.MAX_TOP_K;
|
||||
|
||||
public class StorageAttachedIndex implements Index
|
||||
{
|
||||
public static final String NAME = "sai";
|
||||
|
||||
public static final String VECTOR_USAGE_WARNING = "SAI ANN indexes on vector columns are experimental and are not recommended for production use.\n" +
|
||||
"They don't yet support SELECT queries with:\n" +
|
||||
" * Consistency level higher than ONE/LOCAL_ONE.\n" +
|
||||
" * Paging.\n" +
|
||||
" * No LIMIT clauses.\n" +
|
||||
" * PER PARTITION LIMIT clauses.\n" +
|
||||
" * GROUP BY clauses.\n" +
|
||||
" * Aggregation functions.\n" +
|
||||
" * Filters on columns without a SAI index.";
|
||||
|
||||
public static final String VECTOR_NON_FLOAT_ERROR = "SAI ANN indexes are only allowed on vector columns with float elements";
|
||||
public static final String VECTOR_1_DIMENSION_COSINE_ERROR = "Cosine similarity is not supported for single-dimension vectors";
|
||||
public static final String VECTOR_MULTIPLE_DATA_DIRECTORY_ERROR = "SAI ANN indexes are not allowed on multiple data directories";
|
||||
|
||||
@VisibleForTesting
|
||||
public static final String ANALYSIS_ON_KEY_COLUMNS_MESSAGE = "Analysis options are not supported on primary key columns, but found ";
|
||||
|
||||
|
||||
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);
|
||||
|
||||
private static class StorageAttachedIndexBuildingSupport implements IndexBuildingSupport
|
||||
|
|
@ -147,6 +175,10 @@ public class StorageAttachedIndex implements Index
|
|||
|
||||
private static final Set<String> VALID_OPTIONS = ImmutableSet.of(IndexTarget.TARGET_OPTION_NAME,
|
||||
IndexTarget.CUSTOM_INDEX_OPTION_NAME,
|
||||
IndexWriterConfig.MAXIMUM_NODE_CONNECTIONS,
|
||||
IndexWriterConfig.CONSTRUCTION_BEAM_WIDTH,
|
||||
IndexWriterConfig.SIMILARITY_FUNCTION,
|
||||
IndexWriterConfig.OPTIMIZE_FOR,
|
||||
NonTokenizingOptions.CASE_SENSITIVE,
|
||||
NonTokenizingOptions.NORMALIZE,
|
||||
NonTokenizingOptions.ASCII);
|
||||
|
|
@ -242,7 +274,15 @@ public class StorageAttachedIndex implements Index
|
|||
throw new InvalidRequestException("Cannot create more than one storage-attached index on the same column: " + target.left);
|
||||
}
|
||||
|
||||
Map<String, String> analysisOptions = AbstractAnalyzer.getAnalyzerOptions(options);
|
||||
if (target.left.isPrimaryKeyColumn() && !analysisOptions.isEmpty())
|
||||
{
|
||||
throw new InvalidRequestException(ANALYSIS_ON_KEY_COLUMNS_MESSAGE + new CqlBuilder().append(analysisOptions));
|
||||
}
|
||||
|
||||
AbstractType<?> type = TypeUtil.cellValueType(target.left, target.right);
|
||||
AbstractAnalyzer.fromOptions(type, analysisOptions);
|
||||
IndexWriterConfig config = IndexWriterConfig.fromOptions(null, type, options);
|
||||
|
||||
// If we are indexing map entries we need to validate the subtypes
|
||||
if (TypeUtil.isComposite(type))
|
||||
|
|
@ -257,13 +297,21 @@ public class StorageAttachedIndex implements Index
|
|||
{
|
||||
throw new InvalidRequestException("Unsupported type: " + type.asCQL3Type());
|
||||
}
|
||||
|
||||
Map<String, String> analysisOptions = AbstractAnalyzer.getAnalyzerOptions(options);
|
||||
if (target.left.isPrimaryKeyColumn() && !analysisOptions.isEmpty())
|
||||
else if (type.isVector())
|
||||
{
|
||||
throw new InvalidRequestException(ANALYSIS_ON_KEY_COLUMNS_MESSAGE + new CqlBuilder().append(analysisOptions));
|
||||
VectorType<?> vectorType = (VectorType<?>) type;
|
||||
|
||||
if (!(vectorType.elementType instanceof FloatType))
|
||||
throw new InvalidRequestException(VECTOR_NON_FLOAT_ERROR);
|
||||
|
||||
if (vectorType.dimension == 1 && config.getSimilarityFunction() == VectorSimilarityFunction.COSINE)
|
||||
throw new InvalidRequestException(VECTOR_1_DIMENSION_COSINE_ERROR);
|
||||
|
||||
if (DatabaseDescriptor.getRawConfig().data_file_directories.length > 1)
|
||||
throw new InvalidRequestException(VECTOR_MULTIPLE_DATA_DIRECTORY_ERROR);
|
||||
|
||||
ClientWarn.instance.warn(VECTOR_USAGE_WARNING);
|
||||
}
|
||||
AbstractAnalyzer.fromOptions(type, analysisOptions);
|
||||
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
|
@ -528,6 +576,40 @@ public class StorageAttachedIndex implements Index
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<ByteBuffer> getPostQueryOrdering(Restriction restriction, QueryOptions options)
|
||||
{
|
||||
// For now, only support ANN
|
||||
assert restriction instanceof SingleColumnRestriction.AnnRestriction;
|
||||
|
||||
Preconditions.checkState(indexContext.isVector());
|
||||
|
||||
SingleColumnRestriction.AnnRestriction annRestriction = (SingleColumnRestriction.AnnRestriction) restriction;
|
||||
VectorSimilarityFunction function = indexContext.getIndexWriterConfig().getSimilarityFunction();
|
||||
|
||||
float[] target = TypeUtil.decomposeVector(indexContext, annRestriction.value(options).duplicate());
|
||||
|
||||
return (leftBuf, rightBuf) -> {
|
||||
float[] left = TypeUtil.decomposeVector(indexContext, leftBuf.duplicate());
|
||||
double scoreLeft = function.compare(left, target);
|
||||
|
||||
float[] right = TypeUtil.decomposeVector(indexContext, rightBuf.duplicate());
|
||||
double scoreRight = function.compare(right, target);
|
||||
return Double.compare(scoreRight, scoreLeft); // descending order
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(ReadCommand command) throws InvalidRequestException
|
||||
{
|
||||
if (!getIndexContext().isVector())
|
||||
return;
|
||||
|
||||
// to avoid overflow of the vector graph internal data structure and avoid OOM when filtering top-k
|
||||
if (command.limits().count() > MAX_TOP_K)
|
||||
throw new InvalidRequestException(String.format(ANN_LIMIT_ERROR, MAX_TOP_K, command.limits().count()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEstimatedResultRows()
|
||||
{
|
||||
|
|
@ -670,12 +752,15 @@ public class StorageAttachedIndex implements Index
|
|||
@Override
|
||||
public void updateRow(Row oldRow, Row newRow)
|
||||
{
|
||||
insertRow(newRow);
|
||||
adjustMemtableSize(indexContext.getMemtableIndexManager().update(key, oldRow, newRow, memtable),
|
||||
CassandraWriteContext.fromContext(writeContext).getGroup());
|
||||
}
|
||||
|
||||
void adjustMemtableSize(long additionalSpace, OpOrder.Group opGroup)
|
||||
{
|
||||
memtable.markExtraOnHeapUsed(additionalSpace, opGroup);
|
||||
// The memtable will assert if we try and reduce its memory usage so, for now, just don't tell it.
|
||||
if (additionalSpace >= 0)
|
||||
memtable.markExtraOnHeapUsed(additionalSpace, opGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import io.github.jbellis.jvector.util.Bits;
|
||||
import org.apache.cassandra.db.ReadCommand;
|
||||
import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
||||
import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
|
||||
import org.apache.cassandra.index.sai.disk.v1.vector.DiskAnn;
|
||||
import org.apache.cassandra.index.sai.disk.v1.vector.OnHeapGraph;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
|
||||
|
||||
/**
|
||||
* This represents the state of a vector query. It is repsonsible for maintaining a list of any {@link PrimaryKey}s
|
||||
* that have been updated or deleted during a search of the indexes.
|
||||
* <p>
|
||||
* The number of {@link #shadowedPrimaryKeys} is compared before and after a search is performed. If it changes, it
|
||||
* means that a {@link PrimaryKey} was found to have been changed. In this case the whole search is repeated until the
|
||||
* counts match.
|
||||
* <p>
|
||||
* When this process has completed, a {@link Bits} array is generated. This is used by the vector graph search to
|
||||
* identify which nodes in the graph to include in the results.
|
||||
*/
|
||||
public class VectorQueryContext
|
||||
{
|
||||
private final int limit;
|
||||
// Holds primary keys that are shadowed by expired TTL or row tombstone or range tombstone.
|
||||
// They are populated by the StorageAttachedIndexSearcher during filtering. They are used to generate
|
||||
// a bitset for the graph search to indicate graph nodes to ignore.
|
||||
private TreeSet<PrimaryKey> shadowedPrimaryKeys;
|
||||
|
||||
public VectorQueryContext(ReadCommand readCommand)
|
||||
{
|
||||
this.limit = readCommand.limits().count();
|
||||
}
|
||||
|
||||
public int limit()
|
||||
{
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void recordShadowedPrimaryKey(PrimaryKey primaryKey)
|
||||
{
|
||||
if (shadowedPrimaryKeys == null)
|
||||
shadowedPrimaryKeys = new TreeSet<>();
|
||||
shadowedPrimaryKeys.add(primaryKey);
|
||||
}
|
||||
|
||||
// Returns true if the row ID will be included or false if the row ID will be shadowed
|
||||
public boolean shouldInclude(long sstableRowId, PrimaryKeyMap primaryKeyMap)
|
||||
{
|
||||
return shadowedPrimaryKeys == null || !shadowedPrimaryKeys.contains(primaryKeyMap.primaryKeyFromRowId(sstableRowId));
|
||||
}
|
||||
|
||||
public boolean shouldInclude(PrimaryKey pk)
|
||||
{
|
||||
return shadowedPrimaryKeys == null || !shadowedPrimaryKeys.contains(pk);
|
||||
}
|
||||
|
||||
public boolean containsShadowedPrimaryKey(PrimaryKey primaryKey)
|
||||
{
|
||||
return shadowedPrimaryKeys != null && shadowedPrimaryKeys.contains(primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return shadowed primary keys, in ascending order
|
||||
*/
|
||||
public NavigableSet<PrimaryKey> getShadowedPrimaryKeys()
|
||||
{
|
||||
if (shadowedPrimaryKeys == null)
|
||||
return Collections.emptyNavigableSet();
|
||||
return shadowedPrimaryKeys;
|
||||
}
|
||||
|
||||
public Bits bitsetForShadowedPrimaryKeys(OnHeapGraph<PrimaryKey> graph)
|
||||
{
|
||||
if (shadowedPrimaryKeys == null)
|
||||
return null;
|
||||
|
||||
return new IgnoredKeysBits(graph, shadowedPrimaryKeys);
|
||||
}
|
||||
|
||||
public Bits bitsetForShadowedPrimaryKeys(SegmentMetadata metadata, PrimaryKeyMap primaryKeyMap, DiskAnn graph) throws IOException
|
||||
{
|
||||
Set<Integer> ignoredOrdinals = null;
|
||||
try (var ordinalsView = graph.getOrdinalsView())
|
||||
{
|
||||
for (PrimaryKey primaryKey : getShadowedPrimaryKeys())
|
||||
{
|
||||
// not in current segment
|
||||
if (primaryKey.compareTo(metadata.minKey) < 0 || primaryKey.compareTo(metadata.maxKey) > 0)
|
||||
continue;
|
||||
|
||||
long sstableRowId = primaryKeyMap.rowIdFromPrimaryKey(primaryKey);
|
||||
if (sstableRowId == Long.MAX_VALUE) // not found
|
||||
continue;
|
||||
|
||||
int segmentRowId = Math.toIntExact(sstableRowId - metadata.rowIdOffset);
|
||||
// not in segment yet
|
||||
if (segmentRowId < 0)
|
||||
continue;
|
||||
// end of segment
|
||||
if (segmentRowId > metadata.maxSSTableRowId)
|
||||
break;
|
||||
|
||||
int ordinal = ordinalsView.getOrdinalForRowId(segmentRowId);
|
||||
if (ordinal >= 0)
|
||||
{
|
||||
if (ignoredOrdinals == null)
|
||||
ignoredOrdinals = new HashSet<>();
|
||||
ignoredOrdinals.add(ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoredOrdinals == null)
|
||||
return null;
|
||||
|
||||
return new IgnoringBits(ignoredOrdinals, metadata);
|
||||
}
|
||||
|
||||
private static class IgnoringBits implements Bits
|
||||
{
|
||||
private final Set<Integer> ignoredOrdinals;
|
||||
private final int length;
|
||||
|
||||
public IgnoringBits(Set<Integer> ignoredOrdinals, SegmentMetadata metadata)
|
||||
{
|
||||
this.ignoredOrdinals = ignoredOrdinals;
|
||||
this.length = 1 + Math.toIntExact(metadata.maxSSTableRowId - metadata.rowIdOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int index)
|
||||
{
|
||||
return !ignoredOrdinals.contains(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
private static class IgnoredKeysBits implements Bits
|
||||
{
|
||||
private final OnHeapGraph<PrimaryKey> graph;
|
||||
private final NavigableSet<PrimaryKey> ignored;
|
||||
|
||||
public IgnoredKeysBits(OnHeapGraph<PrimaryKey> graph, NavigableSet<PrimaryKey> ignored)
|
||||
{
|
||||
this.graph = graph;
|
||||
this.ignored = ignored;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int ordinal)
|
||||
{
|
||||
var keys = graph.keysFromOrdinal(ordinal);
|
||||
return keys.stream().anyMatch(k -> !ignored.contains(k));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return graph.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ package org.apache.cassandra.index.sai.disk;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -64,7 +66,7 @@ public class IndexSearchResultIterator extends KeyRangeIterator
|
|||
{
|
||||
List<KeyRangeIterator> subIterators = new ArrayList<>(1 + sstableIndexes.size());
|
||||
|
||||
KeyRangeIterator memtableIterator = expression.context.getMemtableIndexManager().searchMemtableIndexes(expression, keyRange);
|
||||
KeyRangeIterator memtableIterator = expression.context.getMemtableIndexManager().searchMemtableIndexes(queryContext, expression, keyRange);
|
||||
if (memtableIterator != null)
|
||||
subIterators.add(memtableIterator);
|
||||
|
||||
|
|
@ -78,10 +80,10 @@ public class IndexSearchResultIterator extends KeyRangeIterator
|
|||
if (sstableIndex.isReleased())
|
||||
throw new IllegalStateException(sstableIndex.getIndexContext().logMessage("Index was released from the view during the query"));
|
||||
|
||||
List<KeyRangeIterator> segmentIterators = sstableIndex.search(expression, keyRange, queryContext);
|
||||
List<KeyRangeIterator> indexIterators = sstableIndex.search(expression, keyRange, queryContext);
|
||||
|
||||
if (!segmentIterators.isEmpty())
|
||||
subIterators.addAll(segmentIterators);
|
||||
if (!indexIterators.isEmpty())
|
||||
subIterators.addAll(indexIterators);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
|
|
@ -96,6 +98,22 @@ public class IndexSearchResultIterator extends KeyRangeIterator
|
|||
return new IndexSearchResultIterator(union, sstableIndexes, queryContext);
|
||||
}
|
||||
|
||||
public static IndexSearchResultIterator build(List<KeyRangeIterator> sstableIntersections,
|
||||
KeyRangeIterator memtableResults,
|
||||
Set<SSTableIndex> referencedIndexes,
|
||||
QueryContext queryContext)
|
||||
{
|
||||
queryContext.sstablesHit += referencedIndexes
|
||||
.stream()
|
||||
.map(SSTableIndex::getSSTable).collect(Collectors.toSet()).size();
|
||||
queryContext.checkpoint();
|
||||
KeyRangeIterator union = KeyRangeUnionIterator.builder(sstableIntersections.size() + 1)
|
||||
.add(sstableIntersections)
|
||||
.add(memtableResults)
|
||||
.build();
|
||||
return new IndexSearchResultIterator(union, referencedIndexes, queryContext);
|
||||
}
|
||||
|
||||
protected PrimaryKey computeNext()
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import java.io.IOException;
|
|||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
|
||||
/**
|
||||
|
|
@ -70,6 +71,24 @@ public interface PrimaryKeyMap extends Closeable
|
|||
*/
|
||||
long rowIdFromPrimaryKey(PrimaryKey key);
|
||||
|
||||
/**
|
||||
* Returns the first row ID of the nearest {@link Token} greater than or equal to the given {@link Token},
|
||||
* or a negative value if not found
|
||||
*
|
||||
* @param token the {@link Token} to lookup
|
||||
* @return the ceiling row ID associated with the {@link Token} or a negative value
|
||||
*/
|
||||
long ceiling(Token token);
|
||||
|
||||
/**
|
||||
* Returns the last row ID of the nearest {@link Token} less than or equal to the given {@link Token},
|
||||
* or a negative value if the {@link Token} is at its minimum value
|
||||
*
|
||||
* @param token the {@link Token} to lookup
|
||||
* @return the floor row ID associated with the {@link Token}
|
||||
*/
|
||||
long floor(Token token);
|
||||
|
||||
@Override
|
||||
default void close()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -64,6 +64,18 @@ public class RowMapping
|
|||
|
||||
@Override
|
||||
public void add(PrimaryKey key, long sstableRowId) {}
|
||||
|
||||
@Override
|
||||
public int get(PrimaryKey key)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
private final InMemoryTrie<Long> rowMapping = new InMemoryTrie<>(BufferType.OFF_HEAP);
|
||||
|
|
@ -75,6 +87,8 @@ public class RowMapping
|
|||
|
||||
public long maxSSTableRowId = -1;
|
||||
|
||||
public int count;
|
||||
|
||||
private RowMapping()
|
||||
{}
|
||||
|
||||
|
|
@ -163,6 +177,25 @@ public class RowMapping
|
|||
if (minKey == null)
|
||||
minKey = key;
|
||||
maxKey = key;
|
||||
count++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SSTable row Id for a {@link PrimaryKey}
|
||||
*
|
||||
* @param key the {@link PrimaryKey}
|
||||
* @return a valid SSTable row Id for the {@link PrimaryKey} or -1 if the {@link PrimaryKey} doesn't exist
|
||||
* in the {@link RowMapping}
|
||||
*/
|
||||
public int get(PrimaryKey key)
|
||||
{
|
||||
Long sstableRowId = rowMapping.get(key);
|
||||
return sstableRowId == null ? -1 : Math.toIntExact(sstableRowId);
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
return count;
|
||||
}
|
||||
|
||||
public boolean hasRows()
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import org.apache.cassandra.index.sai.IndexContext;
|
|||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.SSTableContext;
|
||||
import org.apache.cassandra.index.sai.disk.format.Version;
|
||||
import org.apache.cassandra.index.sai.disk.v1.segment.SegmentOrdering;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.io.sstable.SSTableIdFactory;
|
||||
|
|
@ -50,7 +51,7 @@ import org.apache.cassandra.io.sstable.format.SSTableReader;
|
|||
* <li>Exposes the index metadata for the column index</li>
|
||||
* </ul>
|
||||
*/
|
||||
public abstract class SSTableIndex
|
||||
public abstract class SSTableIndex implements SegmentOrdering
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(SSTableIndex.class);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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.disk;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.exceptions.QueryCancelledException;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.postings.PostingList;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.apache.cassandra.utils.Throwables;
|
||||
|
||||
|
||||
/**
|
||||
* From sstable row id range iterator to primary key range iterator
|
||||
*/
|
||||
@NotThreadSafe
|
||||
public class SSTableRowIdKeyRangeIterator extends KeyRangeIterator
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||
|
||||
private final Stopwatch timeToExhaust = Stopwatch.createStarted();
|
||||
private final QueryContext queryContext;
|
||||
private final PrimaryKeyMap primaryKeyMap;
|
||||
private final PostingList postingList;
|
||||
|
||||
private boolean needsSkipping = false;
|
||||
private PrimaryKey skipToToken = null;
|
||||
|
||||
/**
|
||||
* Create a direct PostingListRangeIterator where the underlying PostingList is materialised
|
||||
* immediately so the posting list size can be used.
|
||||
*/
|
||||
private SSTableRowIdKeyRangeIterator(PrimaryKey min,
|
||||
PrimaryKey max,
|
||||
long count,
|
||||
PrimaryKeyMap primaryKeyMap,
|
||||
QueryContext queryContext,
|
||||
PostingList postingList)
|
||||
{
|
||||
super(min, max, count);
|
||||
|
||||
this.primaryKeyMap = primaryKeyMap;
|
||||
this.queryContext = queryContext;
|
||||
this.postingList = postingList;
|
||||
}
|
||||
|
||||
public static KeyRangeIterator create(PrimaryKeyMap primaryKeyMap, QueryContext queryContext, PostingList postingList)
|
||||
{
|
||||
if (postingList.size() <= 0)
|
||||
return KeyRangeIterator.empty();
|
||||
|
||||
PrimaryKey min = primaryKeyMap.primaryKeyFromRowId(postingList.minimum());
|
||||
PrimaryKey max = primaryKeyMap.primaryKeyFromRowId(postingList.maximum());
|
||||
return new SSTableRowIdKeyRangeIterator(min, max, postingList.size(), primaryKeyMap, queryContext, postingList);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performSkipTo(PrimaryKey nextKey)
|
||||
{
|
||||
if (skipToToken != null && skipToToken.compareTo(nextKey) >= 0)
|
||||
return;
|
||||
|
||||
skipToToken = nextKey;
|
||||
needsSkipping = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PrimaryKey computeNext()
|
||||
{
|
||||
try
|
||||
{
|
||||
queryContext.checkpoint();
|
||||
|
||||
// just end the iterator if we don't have a postingList or current segment is skipped
|
||||
if (exhausted())
|
||||
return endOfData();
|
||||
|
||||
long rowId = getNextRowId();
|
||||
if (rowId == PostingList.END_OF_STREAM)
|
||||
return endOfData();
|
||||
|
||||
return primaryKeyMap.primaryKeyFromRowId(rowId);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
//VSTODO We aren't tidying up resources here
|
||||
if (!(t instanceof QueryCancelledException))
|
||||
logger.error("Unable to provide next token!", t);
|
||||
|
||||
throw Throwables.cleaned(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
final long exhaustedInMills = timeToExhaust.stop().elapsed(TimeUnit.MILLISECONDS);
|
||||
logger.trace("PostinListRangeIterator exhausted after {} ms", exhaustedInMills);
|
||||
}
|
||||
|
||||
FileUtils.closeQuietly(Arrays.asList(postingList, primaryKeyMap));
|
||||
}
|
||||
|
||||
private boolean exhausted()
|
||||
{
|
||||
return needsSkipping && skipToToken.compareTo(getMaximum()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* reads the next sstable row ID from the underlying range iterator, potentially skipping to get there.
|
||||
*/
|
||||
private long getNextRowId() throws IOException
|
||||
{
|
||||
long sstableRowId;
|
||||
if (needsSkipping)
|
||||
{
|
||||
long targetRowID = primaryKeyMap.rowIdFromPrimaryKey(skipToToken);
|
||||
// skipToToken is larger than max token in token file
|
||||
if (targetRowID < 0)
|
||||
return PostingList.END_OF_STREAM;
|
||||
|
||||
sstableRowId = postingList.advance(targetRowID);
|
||||
needsSkipping = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sstableRowId = postingList.nextPosting();
|
||||
}
|
||||
|
||||
return sstableRowId;
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,11 @@ public enum IndexComponent
|
|||
*/
|
||||
TERMS_DATA("TermsData"),
|
||||
|
||||
/**
|
||||
* Product Quantization store used to store compressed vectors for the vector index
|
||||
*/
|
||||
COMPRESSED_VECTORS("CompressedVectors"),
|
||||
|
||||
/**
|
||||
* Stores postings written by {@link PostingsWriter}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public class Version implements Comparable<Version>
|
|||
|
||||
// These should be added in reverse order so that the latest version is used first. Version matching tests
|
||||
// are more likely to match the latest version, so we want to test that one first.
|
||||
public static final SortedSet<Version> ALL = new TreeSet<Version>(Comparator.reverseOrder()) {{
|
||||
public static final SortedSet<Version> ALL = new TreeSet<>(Comparator.reverseOrder()) {{
|
||||
add(AA);
|
||||
}};
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,17 @@ public class IndexFileUtils
|
|||
return new IndexOutputWriter(new ChecksummingWriter(file, writerOption));
|
||||
}
|
||||
|
||||
public IndexOutputWriter openOutput(File file, boolean append) throws IOException
|
||||
{
|
||||
assert writerOption.finishOnClose() : "IndexOutputWriter relies on close() to sync with disk.";
|
||||
|
||||
IndexOutputWriter indexOutputWriter = new IndexOutputWriter(new ChecksummingWriter(file, writerOption));
|
||||
if (append)
|
||||
indexOutputWriter.skipBytes(file.length());
|
||||
|
||||
return indexOutputWriter;
|
||||
}
|
||||
|
||||
public IndexInput openInput(FileHandle handle)
|
||||
{
|
||||
return IndexInputReader.create(handle);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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.disk.v1;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
|
||||
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.index.sai.disk.v1.vector.OptimizeFor;
|
||||
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_VECTOR_SEARCH_MAX_TOP_K;
|
||||
|
||||
/**
|
||||
* Per-index config for storage-attached index writers.
|
||||
*/
|
||||
public class IndexWriterConfig
|
||||
{
|
||||
public static final String MAXIMUM_NODE_CONNECTIONS = "maximum_node_connections";
|
||||
public static final int MAXIMUM_MAXIMUM_NODE_CONNECTIONS = 512;
|
||||
public static final int DEFAULT_MAXIMUM_NODE_CONNECTIONS = 16;
|
||||
|
||||
public static final String CONSTRUCTION_BEAM_WIDTH = "construction_beam_width";
|
||||
public static final int MAXIMUM_CONSTRUCTION_BEAM_WIDTH = 3200;
|
||||
public static final int DEFAULT_CONSTRUCTION_BEAM_WIDTH = 100;
|
||||
|
||||
public static final String SIMILARITY_FUNCTION = "similarity_function";
|
||||
public static final VectorSimilarityFunction DEFAULT_SIMILARITY_FUNCTION = VectorSimilarityFunction.COSINE;
|
||||
public static final String validSimilarityFunctions = Arrays.stream(VectorSimilarityFunction.values())
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
public static final String OPTIMIZE_FOR = "optimize_for";
|
||||
private static final OptimizeFor DEFAULT_OPTIMIZE_FOR = OptimizeFor.LATENCY;
|
||||
private static final String validOptimizeFor = Arrays.stream(OptimizeFor.values())
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
public static final int MAX_TOP_K = SAI_VECTOR_SEARCH_MAX_TOP_K.getInt();
|
||||
|
||||
private static final IndexWriterConfig EMPTY_CONFIG = new IndexWriterConfig(-1, -1, null, null);
|
||||
|
||||
// The maximum number of outgoing connections a node can have in a graph.
|
||||
private final int maximumNodeConnections;
|
||||
|
||||
// The size of the beam search used when finding nearest neighbours.
|
||||
private final int constructionBeamWidth;
|
||||
|
||||
// Used to determine the search to determine the topK results. The score returned is used to order the topK results.
|
||||
private final VectorSimilarityFunction similarityFunction;
|
||||
|
||||
private final OptimizeFor optimizeFor;
|
||||
|
||||
public IndexWriterConfig(int maximumNodeConnections,
|
||||
int constructionBeamWidth,
|
||||
VectorSimilarityFunction similarityFunction,
|
||||
OptimizeFor optimizerFor)
|
||||
{
|
||||
this.maximumNodeConnections = maximumNodeConnections;
|
||||
this.constructionBeamWidth = constructionBeamWidth;
|
||||
this.similarityFunction = similarityFunction;
|
||||
this.optimizeFor = optimizerFor;
|
||||
}
|
||||
|
||||
public int getMaximumNodeConnections()
|
||||
{
|
||||
return maximumNodeConnections;
|
||||
}
|
||||
|
||||
public int getConstructionBeamWidth()
|
||||
{
|
||||
return constructionBeamWidth;
|
||||
}
|
||||
|
||||
public VectorSimilarityFunction getSimilarityFunction()
|
||||
{
|
||||
return similarityFunction;
|
||||
}
|
||||
|
||||
public OptimizeFor getOptimizeFor()
|
||||
{
|
||||
return optimizeFor;
|
||||
}
|
||||
|
||||
public static IndexWriterConfig fromOptions(String indexName, AbstractType<?> type, Map<String, String> options)
|
||||
{
|
||||
int maximumNodeConnections = DEFAULT_MAXIMUM_NODE_CONNECTIONS;
|
||||
int queueSize = DEFAULT_CONSTRUCTION_BEAM_WIDTH;
|
||||
VectorSimilarityFunction similarityFunction = DEFAULT_SIMILARITY_FUNCTION;
|
||||
OptimizeFor optimizeFor = DEFAULT_OPTIMIZE_FOR;
|
||||
|
||||
if (options.get(MAXIMUM_NODE_CONNECTIONS) != null ||
|
||||
options.get(CONSTRUCTION_BEAM_WIDTH) != null ||
|
||||
options.get(SIMILARITY_FUNCTION) != null ||
|
||||
options.get(OPTIMIZE_FOR) != null)
|
||||
{
|
||||
if (!type.isVector())
|
||||
throw new InvalidRequestException(String.format("CQL type %s cannot have vector options", type.asCQL3Type()));
|
||||
|
||||
if (options.containsKey(MAXIMUM_NODE_CONNECTIONS))
|
||||
{
|
||||
if (!CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.getBoolean())
|
||||
throw new InvalidRequestException(String.format("Maximum node connections cannot be set without enabling %s", CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.name()));
|
||||
|
||||
try
|
||||
{
|
||||
maximumNodeConnections = Integer.parseInt(options.get(MAXIMUM_NODE_CONNECTIONS));
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
throw new InvalidRequestException(String.format("Maximum number of connections %s is not a valid integer for index %s",
|
||||
options.get(MAXIMUM_NODE_CONNECTIONS), indexName));
|
||||
}
|
||||
if (maximumNodeConnections <= 0 || maximumNodeConnections > MAXIMUM_MAXIMUM_NODE_CONNECTIONS)
|
||||
throw new InvalidRequestException(String.format("Maximum number of connections for index %s cannot be <= 0 or > %s, was %s", indexName, MAXIMUM_MAXIMUM_NODE_CONNECTIONS, maximumNodeConnections));
|
||||
}
|
||||
if (options.containsKey(CONSTRUCTION_BEAM_WIDTH))
|
||||
{
|
||||
if (!CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.getBoolean())
|
||||
throw new InvalidRequestException(String.format("Construction beam width cannot be set without enabling %s", CassandraRelevantProperties.SAI_VECTOR_ALLOW_CUSTOM_PARAMETERS.name()));
|
||||
|
||||
try
|
||||
{
|
||||
queueSize = Integer.parseInt(options.get(CONSTRUCTION_BEAM_WIDTH));
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
throw new InvalidRequestException(String.format("Construction beam width %s is not a valid integer for index %s",
|
||||
options.get(CONSTRUCTION_BEAM_WIDTH), indexName));
|
||||
}
|
||||
if (queueSize <= 0 || queueSize > MAXIMUM_CONSTRUCTION_BEAM_WIDTH)
|
||||
throw new InvalidRequestException(String.format("Construction beam width for index %s cannot be <= 0 or > %s, was %s", indexName, MAXIMUM_CONSTRUCTION_BEAM_WIDTH, queueSize));
|
||||
}
|
||||
if (options.containsKey(SIMILARITY_FUNCTION))
|
||||
{
|
||||
String option = options.get(SIMILARITY_FUNCTION).toUpperCase();
|
||||
try
|
||||
{
|
||||
similarityFunction = VectorSimilarityFunction.valueOf(option);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new InvalidRequestException(String.format("Similarity function %s was not recognized for index %s. Valid values are: %s",
|
||||
option, indexName, validSimilarityFunctions));
|
||||
}
|
||||
}
|
||||
if (options.containsKey(OPTIMIZE_FOR))
|
||||
{
|
||||
String option = options.get(OPTIMIZE_FOR).toUpperCase();
|
||||
try
|
||||
{
|
||||
optimizeFor = OptimizeFor.valueOf(option);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new InvalidRequestException(String.format("optimize_for '%s' was not recognized for index %s. Valid values are: %s",
|
||||
option, indexName, validOptimizeFor));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new IndexWriterConfig(maximumNodeConnections, queueSize, similarityFunction, optimizeFor);
|
||||
}
|
||||
|
||||
public static IndexWriterConfig emptyConfig()
|
||||
{
|
||||
return EMPTY_CONFIG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("IndexWriterConfig{%s=%d, %s=%d, %s=%s, %s=%s}",
|
||||
MAXIMUM_NODE_CONNECTIONS, maximumNodeConnections,
|
||||
CONSTRUCTION_BEAM_WIDTH, constructionBeamWidth,
|
||||
SIMILARITY_FUNCTION, similarityFunction,
|
||||
OPTIMIZE_FOR, optimizeFor);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ import org.apache.cassandra.index.sai.memory.MemtableIndex;
|
|||
import org.apache.cassandra.index.sai.memory.MemtableTermsIterator;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.index.sai.utils.TypeUtil;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||
|
||||
|
|
@ -104,22 +105,20 @@ public class MemtableIndexWriter implements PerColumnIndexWriter
|
|||
return;
|
||||
}
|
||||
|
||||
final Iterator<Pair<ByteComparable, LongArrayList>> iterator = rowMapping.merge(memtable);
|
||||
|
||||
try (MemtableTermsIterator terms = new MemtableTermsIterator(memtable.getMinTerm(), memtable.getMaxTerm(), iterator))
|
||||
if (indexContext.isVector())
|
||||
{
|
||||
long cellCount = flush(rowMapping.minKey, rowMapping.maxKey, indexContext.getValidator(), terms, rowMapping.maxSSTableRowId);
|
||||
flushVectorIndex(start, stopwatch);
|
||||
}
|
||||
else
|
||||
{
|
||||
final Iterator<Pair<ByteComparable, LongArrayList>> iterator = rowMapping.merge(memtable);
|
||||
|
||||
indexDescriptor.createComponentOnDisk(IndexComponent.COLUMN_COMPLETION_MARKER, indexContext);
|
||||
try (MemtableTermsIterator terms = new MemtableTermsIterator(memtable.getMinTerm(), memtable.getMaxTerm(), iterator))
|
||||
{
|
||||
long cellCount = flush(indexContext.getValidator(), terms, rowMapping.maxSSTableRowId);
|
||||
|
||||
indexContext.getIndexMetrics().memtableIndexFlushCount.inc();
|
||||
|
||||
long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
|
||||
|
||||
logger.debug(indexContext.logMessage("Completed flushing {} memtable index cells to SSTable {}. Duration: {} ms. Total elapsed: {} ms"),
|
||||
cellCount, indexDescriptor.sstableDescriptor, elapsed - start, elapsed);
|
||||
|
||||
indexContext.getIndexMetrics().memtableFlushCellsPerSecond.update((long) (cellCount * 1000.0 / Math.max(1, elapsed - start)));
|
||||
completeIndexFlush(cellCount, start, stopwatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable t)
|
||||
|
|
@ -131,11 +130,7 @@ public class MemtableIndexWriter implements PerColumnIndexWriter
|
|||
}
|
||||
}
|
||||
|
||||
private long flush(PrimaryKey minKey,
|
||||
PrimaryKey maxKey,
|
||||
AbstractType<?> termComparator,
|
||||
MemtableTermsIterator terms,
|
||||
long maxSSTableRowId) throws IOException
|
||||
private long flush(AbstractType<?> termComparator, MemtableTermsIterator terms, long maxSSTableRowId) throws IOException
|
||||
{
|
||||
long numRows;
|
||||
SegmentMetadata.ComponentMetadataMap indexMetas;
|
||||
|
|
@ -171,8 +166,8 @@ public class MemtableIndexWriter implements PerColumnIndexWriter
|
|||
numRows,
|
||||
terms.getMinSSTableRowId(),
|
||||
terms.getMaxSSTableRowId(),
|
||||
minKey,
|
||||
maxKey,
|
||||
rowMapping.minKey,
|
||||
rowMapping.maxKey,
|
||||
terms.getMinTerm(),
|
||||
terms.getMaxTerm(),
|
||||
indexMetas);
|
||||
|
|
@ -184,4 +179,43 @@ public class MemtableIndexWriter implements PerColumnIndexWriter
|
|||
|
||||
return numRows;
|
||||
}
|
||||
|
||||
private void flushVectorIndex(long startTime, Stopwatch stopwatch) throws IOException
|
||||
{
|
||||
SegmentMetadata.ComponentMetadataMap metadataMap = memtable.writeDirect(indexDescriptor, indexContext, rowMapping::get);
|
||||
|
||||
completeIndexFlush(rowMapping.size(), startTime, stopwatch);
|
||||
|
||||
SegmentMetadata metadata = new SegmentMetadata(0,
|
||||
rowMapping.size(),
|
||||
0,
|
||||
rowMapping.maxSSTableRowId,
|
||||
rowMapping.minKey,
|
||||
rowMapping.maxKey,
|
||||
ByteBufferUtil.bytes(0),
|
||||
ByteBufferUtil.bytes(0),
|
||||
metadataMap);
|
||||
|
||||
try (MetadataWriter writer = new MetadataWriter(indexDescriptor.openPerIndexOutput(IndexComponent.META, indexContext)))
|
||||
{
|
||||
SegmentMetadata.write(writer, Collections.singletonList(metadata));
|
||||
}
|
||||
}
|
||||
|
||||
private void completeIndexFlush(long cellCount, long startTime, Stopwatch stopwatch) throws IOException
|
||||
{
|
||||
indexDescriptor.createComponentOnDisk(IndexComponent.COLUMN_COMPLETION_MARKER, indexContext);
|
||||
|
||||
indexContext.getIndexMetrics().memtableIndexFlushCount.inc();
|
||||
|
||||
long elapsedTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
|
||||
|
||||
logger.debug(indexContext.logMessage("Completed flushing {} memtable index cells to SSTable {}. Duration: {} ms. Total elapsed: {} ms"),
|
||||
cellCount,
|
||||
indexDescriptor.sstableDescriptor,
|
||||
elapsedTime - startTime,
|
||||
elapsedTime);
|
||||
|
||||
indexContext.getIndexMetrics().memtableFlushCellsPerSecond.update((long) (cellCount * 1000.0 / Math.max(1, elapsedTime - startTime)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,15 +44,11 @@ public class PerColumnIndexFiles implements Closeable
|
|||
{
|
||||
this.indexDescriptor = indexDescriptor;
|
||||
this.indexContext = indexContext;
|
||||
if (indexContext.isLiteral())
|
||||
for (IndexComponent component : indexDescriptor.version.onDiskFormat().perColumnIndexComponents(indexContext))
|
||||
{
|
||||
files.put(IndexComponent.POSTING_LISTS, indexDescriptor.createPerIndexFileHandle(IndexComponent.POSTING_LISTS, indexContext, this::close));
|
||||
files.put(IndexComponent.TERMS_DATA, indexDescriptor.createPerIndexFileHandle(IndexComponent.TERMS_DATA, indexContext, this::close));
|
||||
}
|
||||
else
|
||||
{
|
||||
files.put(IndexComponent.BALANCED_TREE, indexDescriptor.createPerIndexFileHandle(IndexComponent.BALANCED_TREE, indexContext, this::close));
|
||||
files.put(IndexComponent.POSTING_LISTS, indexDescriptor.createPerIndexFileHandle(IndexComponent.POSTING_LISTS, indexContext, this::close));
|
||||
if (component == IndexComponent.META || component == IndexComponent.COLUMN_COMPLETION_MARKER)
|
||||
continue;
|
||||
files.put(component, indexDescriptor.createPerIndexFileHandle(component, indexContext, this::close));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +67,11 @@ public class PerColumnIndexFiles implements Closeable
|
|||
return getFile(IndexComponent.BALANCED_TREE);
|
||||
}
|
||||
|
||||
public FileHandle compressedVectors()
|
||||
{
|
||||
return getFile(IndexComponent.COMPRESSED_VECTORS);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"resource", "RedundantSuppression"})
|
||||
private FileHandle getFile(IndexComponent indexComponent)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import org.apache.cassandra.utils.NoSpamLogger;
|
|||
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_MAX_FROZEN_TERM_SIZE;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_MAX_STRING_TERM_SIZE;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.SAI_MAX_VECTOR_TERM_SIZE;
|
||||
|
||||
/**
|
||||
* Column index writer that accumulates (on-heap) indexed data from a compacted SSTable as it's being flushed to disk.
|
||||
|
|
@ -58,8 +59,9 @@ public class SSTableIndexWriter implements PerColumnIndexWriter
|
|||
private static final Logger logger = LoggerFactory.getLogger(SSTableIndexWriter.class);
|
||||
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
|
||||
|
||||
public static final int MAX_STRING_TERM_SIZE = SAI_MAX_STRING_TERM_SIZE.getInt() * 1024;
|
||||
public static final int MAX_FROZEN_TERM_SIZE = SAI_MAX_FROZEN_TERM_SIZE.getInt() * 1024;
|
||||
public static final long MAX_STRING_TERM_SIZE = SAI_MAX_STRING_TERM_SIZE.getSizeInBytes();
|
||||
public static final long MAX_FROZEN_TERM_SIZE = SAI_MAX_FROZEN_TERM_SIZE.getSizeInBytes();
|
||||
public static final long MAX_VECTOR_TERM_SIZE = SAI_MAX_VECTOR_TERM_SIZE.getSizeInBytes();
|
||||
public static final String TERM_OVERSIZE_MESSAGE = "Can't add term of column {} to index for key: {}, term size {} " +
|
||||
"max allowed size {}, use analyzed = true (if not yet set) for that column.";
|
||||
|
||||
|
|
@ -68,7 +70,7 @@ public class SSTableIndexWriter implements PerColumnIndexWriter
|
|||
private final long nowInSec = FBUtilities.nowInSeconds();
|
||||
private final AbstractAnalyzer analyzer;
|
||||
private final NamedMemoryLimiter limiter;
|
||||
private final int maxTermSize;
|
||||
private final long maxTermSize;
|
||||
private final BooleanSupplier isIndexValid;
|
||||
private final List<SegmentMetadata> segments = new ArrayList<>();
|
||||
|
||||
|
|
@ -82,7 +84,7 @@ public class SSTableIndexWriter implements PerColumnIndexWriter
|
|||
this.analyzer = indexContext.getAnalyzerFactory().create();
|
||||
this.limiter = limiter;
|
||||
this.isIndexValid = isIndexValid;
|
||||
this.maxTermSize = indexContext.isFrozen() ? MAX_FROZEN_TERM_SIZE : MAX_STRING_TERM_SIZE;
|
||||
this.maxTermSize = indexContext.isVector() ? MAX_VECTOR_TERM_SIZE : (indexContext.isFrozen() ? MAX_FROZEN_TERM_SIZE : MAX_STRING_TERM_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -327,9 +329,14 @@ public class SSTableIndexWriter implements PerColumnIndexWriter
|
|||
|
||||
private SegmentBuilder newSegmentBuilder()
|
||||
{
|
||||
SegmentBuilder builder = TypeUtil.isLiteral(indexContext.getValidator())
|
||||
? new SegmentBuilder.RAMStringSegmentBuilder(indexContext.getValidator(), limiter)
|
||||
: new SegmentBuilder.BlockBalancedTreeSegmentBuilder(indexContext.getValidator(), limiter);
|
||||
SegmentBuilder builder;
|
||||
|
||||
if (indexContext.isVector())
|
||||
builder = new SegmentBuilder.VectorSegmentBuilder(indexContext.getValidator(), limiter, indexContext.getIndexWriterConfig());
|
||||
else if (indexContext.isLiteral())
|
||||
builder = new SegmentBuilder.RAMStringSegmentBuilder(indexContext.getValidator(), limiter);
|
||||
else
|
||||
builder = new SegmentBuilder.BlockBalancedTreeSegmentBuilder(indexContext.getValidator(), limiter);
|
||||
|
||||
long globalBytesUsed = limiter.increment(builder.totalBytesAllocated());
|
||||
logger.debug(indexContext.logMessage("Created new segment builder while flushing SSTable {}. Global segment memory usage now at {}."),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import javax.annotation.concurrent.NotThreadSafe;
|
|||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.dht.IPartitioner;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexComponent;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
|
||||
|
|
@ -66,7 +66,6 @@ public class SkinnyPrimaryKeyMap implements PrimaryKeyMap
|
|||
protected final LongArray.Factory tokenReaderFactory;
|
||||
protected final LongArray.Factory partitionReaderFactory;
|
||||
protected final KeyLookup partitionKeyReader;
|
||||
protected final IPartitioner partitioner;
|
||||
protected final PrimaryKey.Factory primaryKeyFactory;
|
||||
|
||||
private final FileHandle tokensFile;
|
||||
|
|
@ -90,7 +89,6 @@ public class SkinnyPrimaryKeyMap implements PrimaryKeyMap
|
|||
NumericValuesMeta partitionKeyBlockOffsetsMeta = new NumericValuesMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.PARTITION_KEY_BLOCK_OFFSETS)));
|
||||
KeyLookupMeta partitionKeysMeta = new KeyLookupMeta(metadataSource.get(indexDescriptor.componentName(IndexComponent.PARTITION_KEY_BLOCKS)));
|
||||
this.partitionKeyReader = new KeyLookup(partitionKeyBlocksFile, partitionKeyBlockOffsetsFile, partitionKeysMeta, partitionKeyBlockOffsetsMeta);
|
||||
this.partitioner = sstable.metadata().partitioner;
|
||||
this.primaryKeyFactory = indexDescriptor.primaryKeyFactory;
|
||||
}
|
||||
catch (Throwable t)
|
||||
|
|
@ -108,7 +106,6 @@ public class SkinnyPrimaryKeyMap implements PrimaryKeyMap
|
|||
return new SkinnyPrimaryKeyMap(rowIdToToken,
|
||||
rowIdToPartitionId,
|
||||
partitionKeyReader.openCursor(),
|
||||
partitioner,
|
||||
primaryKeyFactory);
|
||||
}
|
||||
|
||||
|
|
@ -122,19 +119,16 @@ public class SkinnyPrimaryKeyMap implements PrimaryKeyMap
|
|||
protected final LongArray tokenArray;
|
||||
protected final LongArray partitionArray;
|
||||
protected final KeyLookup.Cursor partitionKeyCursor;
|
||||
protected final IPartitioner partitioner;
|
||||
protected final PrimaryKey.Factory primaryKeyFactory;
|
||||
|
||||
protected SkinnyPrimaryKeyMap(LongArray tokenArray,
|
||||
LongArray partitionArray,
|
||||
KeyLookup.Cursor partitionKeyCursor,
|
||||
IPartitioner partitioner,
|
||||
PrimaryKey.Factory primaryKeyFactory)
|
||||
{
|
||||
this.tokenArray = tokenArray;
|
||||
this.partitionArray = partitionArray;
|
||||
this.partitionKeyCursor = partitionKeyCursor;
|
||||
this.partitioner = partitioner;
|
||||
this.primaryKeyFactory = primaryKeyFactory;
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +152,22 @@ public class SkinnyPrimaryKeyMap implements PrimaryKeyMap
|
|||
return tokenCollisionDetection(primaryKey, rowId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long ceiling(Token token)
|
||||
{
|
||||
return tokenArray.indexOf(token.getLongValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long floor(Token token)
|
||||
{
|
||||
if (token.isMinimum())
|
||||
return Long.MIN_VALUE;
|
||||
|
||||
long rowId = tokenArray.indexOf(token.getLongValue());
|
||||
return rowId < 0 ? rowId : rowId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -86,6 +86,13 @@ public class V1OnDiskFormat implements OnDiskFormat
|
|||
IndexComponent.BALANCED_TREE,
|
||||
IndexComponent.POSTING_LISTS);
|
||||
|
||||
@VisibleForTesting
|
||||
public static final Set<IndexComponent> VECTOR_COMPONENTS = EnumSet.of(IndexComponent.COLUMN_COMPLETION_MARKER,
|
||||
IndexComponent.META,
|
||||
IndexComponent.COMPRESSED_VECTORS,
|
||||
IndexComponent.TERMS_DATA,
|
||||
IndexComponent.POSTING_LISTS);
|
||||
|
||||
/**
|
||||
* Global limit on heap consumed by all index segment building that occurs outside the context of Memtable flush.
|
||||
* <p>
|
||||
|
|
@ -244,7 +251,7 @@ public class V1OnDiskFormat implements OnDiskFormat
|
|||
@Override
|
||||
public Set<IndexComponent> perColumnIndexComponents(IndexContext indexContext)
|
||||
{
|
||||
return indexContext.isLiteral() ? LITERAL_COMPONENTS : NUMERIC_COMPONENTS;
|
||||
return indexContext.isVector() ? VECTOR_COMPONENTS : indexContext.isLiteral() ? LITERAL_COMPONENTS : NUMERIC_COMPONENTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ import org.apache.cassandra.index.sai.disk.SSTableIndex;
|
|||
import org.apache.cassandra.index.sai.disk.v1.segment.Segment;
|
||||
import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.index.sai.utils.TypeUtil;
|
||||
import org.apache.cassandra.io.sstable.format.SSTableReader;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
|
|
@ -166,13 +168,23 @@ public class V1SSTableIndex extends SSTableIndex
|
|||
{
|
||||
if (segment.intersects(keyRange))
|
||||
{
|
||||
segmentIterators.add(segment.search(expression, context));
|
||||
segmentIterators.add(segment.search(expression, keyRange, context));
|
||||
}
|
||||
}
|
||||
|
||||
return segmentIterators;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyRangeIterator limitToTopKResults(QueryContext context, List<PrimaryKey> primaryKeys, Expression expression) throws IOException
|
||||
{
|
||||
KeyRangeUnionIterator.Builder unionIteratorBuilder = KeyRangeUnionIterator.builder(segments.size());
|
||||
for (Segment segment : segments)
|
||||
unionIteratorBuilder.add(segment.limitToTopKResults(context, primaryKeys, expression));
|
||||
|
||||
return unionIteratorBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateSegmentView(SimpleDataSet dataset)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import javax.annotation.concurrent.ThreadSafe;
|
|||
|
||||
import org.apache.cassandra.db.Clustering;
|
||||
import org.apache.cassandra.db.ClusteringComparator;
|
||||
import org.apache.cassandra.dht.IPartitioner;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexComponent;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
|
||||
|
|
@ -92,7 +92,6 @@ public class WidePrimaryKeyMap extends SkinnyPrimaryKeyMap
|
|||
partitionIdToToken,
|
||||
partitionKeyReader.openCursor(),
|
||||
clusteringKeyReader.openCursor(),
|
||||
partitioner,
|
||||
primaryKeyFactory,
|
||||
clusteringComparator);
|
||||
}
|
||||
|
|
@ -112,11 +111,10 @@ public class WidePrimaryKeyMap extends SkinnyPrimaryKeyMap
|
|||
LongArray partitionArray,
|
||||
KeyLookup.Cursor partitionKeyCursor,
|
||||
KeyLookup.Cursor clusteringKeyCursor,
|
||||
IPartitioner partitioner,
|
||||
PrimaryKey.Factory primaryKeyFactory,
|
||||
ClusteringComparator clusteringComparator)
|
||||
{
|
||||
super(tokenArray, partitionArray, partitionKeyCursor, partitioner, primaryKeyFactory);
|
||||
super(tokenArray, partitionArray, partitionKeyCursor, primaryKeyFactory);
|
||||
|
||||
this.clusteringComparator = clusteringComparator;
|
||||
this.clusteringKeyCursor = clusteringKeyCursor;
|
||||
|
|
@ -144,6 +142,15 @@ public class WidePrimaryKeyMap extends SkinnyPrimaryKeyMap
|
|||
return clusteringKeyCursor.clusteredSeekToKey(clusteringComparator.asByteComparable(primaryKey.clustering()), rowId, startOfNextPartition(rowId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long floor(Token token)
|
||||
{
|
||||
if (token.isMinimum())
|
||||
return Long.MIN_VALUE;
|
||||
long rowId = tokenArray.indexOf(token.getLongValue());
|
||||
return rowId < 0 ? rowId : startOfNextPartition(rowId) - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -73,14 +73,16 @@ public abstract class AbstractBlockPackedReader implements LongArray
|
|||
@Override
|
||||
public long indexOf(long value)
|
||||
{
|
||||
// If we are searching backwards, we need to reset the lastIndex. This is not normal since we normally move
|
||||
// forwards when searching for tokens. We only (may) search backwards in vector searchs where we need the
|
||||
// primary key ranges presented as row IDs.
|
||||
if (value < previousValue)
|
||||
lastIndex = 0;
|
||||
|
||||
// already out of range
|
||||
if (lastIndex >= valueCount)
|
||||
return -1;
|
||||
|
||||
// We keep track previous returned value in lastIndex, so searching backward will not return correct result.
|
||||
// Also it's logically wrong to search backward during token iteration in PostingListRangeIterator.
|
||||
if (value < previousValue)
|
||||
throw new IllegalArgumentException(String.format("%d is smaller than prev token value %d", value, previousValue));
|
||||
previousValue = value;
|
||||
|
||||
int blockIndex = binarySearchBlockMinValues(value);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package org.apache.cassandra.index.sai.disk.v1.postings;
|
|||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
|
@ -41,6 +42,8 @@ public class MergePostingList implements PostingList
|
|||
private final PriorityQueue<PeekablePostingList> postingLists;
|
||||
private final List<PeekablePostingList> temp;
|
||||
private final Closeable onClose;
|
||||
private final long minimum;
|
||||
private final long maximum;
|
||||
private final long size;
|
||||
private long lastRowId = -1;
|
||||
|
||||
|
|
@ -49,11 +52,17 @@ public class MergePostingList implements PostingList
|
|||
this.temp = new ArrayList<>(postingLists.size());
|
||||
this.onClose = onClose;
|
||||
this.postingLists = postingLists;
|
||||
long minimum = 0;
|
||||
long maximum = 0;
|
||||
long totalPostings = 0;
|
||||
for (PostingList postingList : postingLists)
|
||||
{
|
||||
minimum = Math.min(minimum, postingList.minimum());
|
||||
maximum = Math.max(maximum, postingList.maximum());
|
||||
totalPostings += postingList.size();
|
||||
}
|
||||
this.minimum = minimum;
|
||||
this.maximum = maximum;
|
||||
this.size = totalPostings;
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +77,26 @@ public class MergePostingList implements PostingList
|
|||
return merge(postings, () -> FileUtils.close(postings));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"resource", "RedundantSuppression"})
|
||||
public static PostingList merge(List<PostingList> postings)
|
||||
{
|
||||
PriorityQueue<PeekablePostingList> postingsQueue = new PriorityQueue<>(postings.size(), Comparator.comparingLong(PeekablePostingList::peek));
|
||||
postings.stream().map(PeekablePostingList::makePeekable).forEach(postingsQueue::add);
|
||||
return merge(postingsQueue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long minimum()
|
||||
{
|
||||
return minimum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long maximum()
|
||||
{
|
||||
return maximum;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "resource", "RedundantSuppression"})
|
||||
@Override
|
||||
public long nextPosting() throws IOException
|
||||
{
|
||||
|
|
|
|||
|
|
@ -64,12 +64,11 @@ public class PostingListRangeIterator extends KeyRangeIterator
|
|||
private final PostingList postingList;
|
||||
private final IndexContext indexContext;
|
||||
private final PrimaryKeyMap primaryKeyMap;
|
||||
private final IndexSegmentSearcherContext searcherContext;
|
||||
private final long rowIdOffset;
|
||||
|
||||
private boolean needsSkipping = false;
|
||||
private PrimaryKey skipToToken = null;
|
||||
|
||||
|
||||
/**
|
||||
* Create a direct PostingListRangeIterator where the underlying PostingList is materialised
|
||||
* immediately so the posting list size can be used.
|
||||
|
|
@ -83,10 +82,25 @@ public class PostingListRangeIterator extends KeyRangeIterator
|
|||
this.indexContext = indexContext;
|
||||
this.primaryKeyMap = primaryKeyMap;
|
||||
this.postingList = searcherContext.postingList;
|
||||
this.searcherContext = searcherContext;
|
||||
this.rowIdOffset = searcherContext.segmentRowIdOffset;
|
||||
this.queryContext = searcherContext.context;
|
||||
}
|
||||
|
||||
public PostingListRangeIterator(IndexContext indexContext,
|
||||
PrimaryKeyMap primaryKeyMap,
|
||||
PostingList postingList,
|
||||
QueryContext queryContext)
|
||||
{
|
||||
super(primaryKeyMap.primaryKeyFromRowId(postingList.minimum()),
|
||||
primaryKeyMap.primaryKeyFromRowId(postingList.maximum()),
|
||||
postingList.size());
|
||||
this.indexContext = indexContext;
|
||||
this.primaryKeyMap = primaryKeyMap;
|
||||
this.postingList = postingList;
|
||||
this.rowIdOffset = 0;
|
||||
this.queryContext = queryContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performSkipTo(PrimaryKey nextKey)
|
||||
{
|
||||
|
|
@ -155,7 +169,7 @@ public class PostingListRangeIterator extends KeyRangeIterator
|
|||
return PostingList.END_OF_STREAM;
|
||||
}
|
||||
|
||||
segmentRowId = postingList.advance(targetRowID - searcherContext.segmentRowIdOffset);
|
||||
segmentRowId = postingList.advance(targetRowID - rowIdOffset);
|
||||
|
||||
needsSkipping = false;
|
||||
}
|
||||
|
|
@ -165,7 +179,7 @@ public class PostingListRangeIterator extends KeyRangeIterator
|
|||
}
|
||||
|
||||
return segmentRowId != PostingList.END_OF_STREAM
|
||||
? segmentRowId + searcherContext.segmentRowIdOffset
|
||||
? segmentRowId + rowIdOffset
|
||||
: PostingList.END_OF_STREAM;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,12 @@ import javax.annotation.concurrent.NotThreadSafe;
|
|||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
|
||||
import org.apache.cassandra.index.sai.disk.v1.DirectReaders;
|
||||
import org.apache.cassandra.index.sai.postings.OrdinalPostingList;
|
||||
import org.apache.cassandra.index.sai.postings.PostingList;
|
||||
import org.apache.cassandra.index.sai.disk.v1.LongArray;
|
||||
import org.apache.cassandra.index.sai.metrics.QueryEventListener;
|
||||
import org.apache.cassandra.index.sai.disk.io.SeekingRandomAccessInput;
|
||||
import org.apache.cassandra.index.sai.postings.OrdinalPostingList;
|
||||
import org.apache.cassandra.index.sai.postings.PostingList;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.apache.lucene.index.CorruptIndexException;
|
||||
import org.apache.lucene.store.IndexInput;
|
||||
|
|
@ -41,15 +38,13 @@ import org.apache.lucene.util.packed.DirectReader;
|
|||
|
||||
/**
|
||||
* Reads, decompresses and decodes postings lists written by {@link PostingsWriter}.
|
||||
*
|
||||
* <p>
|
||||
* Holds exactly one posting block in memory at a time. Does binary search over skip table to find a postings block to
|
||||
* load.
|
||||
*/
|
||||
@NotThreadSafe
|
||||
public class PostingsReader implements OrdinalPostingList
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(PostingsReader.class);
|
||||
|
||||
private final IndexInput input;
|
||||
private final SeekingRandomAccessInput seekingInput;
|
||||
private final QueryEventListener.PostingListEventListener listener;
|
||||
|
|
@ -62,7 +57,6 @@ public class PostingsReader implements OrdinalPostingList
|
|||
private long totalPostingsRead;
|
||||
private long actualPosting;
|
||||
|
||||
private long currentPosition;
|
||||
private LongValues currentFoRValues;
|
||||
private long postingsDecoded = 0;
|
||||
|
||||
|
|
@ -175,9 +169,9 @@ public class PostingsReader implements OrdinalPostingList
|
|||
* Advances to the first row ID beyond the current that is greater than or equal to the
|
||||
* target, and returns that row ID. Exhausts the iterator and returns {@link #END_OF_STREAM} if
|
||||
* the target is greater than the highest row ID.
|
||||
*
|
||||
* <p>
|
||||
* Does binary search over the skip table to find the next block to load into memory.
|
||||
*
|
||||
* <p>
|
||||
* Note: Callers must use the return value of this method before calling {@link #nextPosting()}, as calling
|
||||
* that method will return the next posting, not the one to which we have just advanced.
|
||||
*
|
||||
|
|
@ -349,7 +343,7 @@ public class PostingsReader implements OrdinalPostingList
|
|||
|
||||
byte bitsPerValue = in.readByte();
|
||||
|
||||
currentPosition = in.getFilePointer();
|
||||
long currentPosition = in.getFilePointer();
|
||||
|
||||
if (bitsPerValue == 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ import java.io.IOException;
|
|||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.agrona.collections.LongArrayList;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
|
|
@ -87,8 +85,6 @@ import static java.lang.Math.max;
|
|||
@NotThreadSafe
|
||||
public class PostingsWriter implements Closeable
|
||||
{
|
||||
protected static final Logger logger = LoggerFactory.getLogger(PostingsWriter.class);
|
||||
|
||||
// import static org.apache.lucene.codecs.lucene50.Lucene50PostingsFormat.BLOCK_SIZE;
|
||||
private final static int BLOCK_SIZE = 128;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.disk.v1.postings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.PrimitiveIterator;
|
||||
|
||||
import org.apache.cassandra.index.sai.postings.PostingList;
|
||||
import org.apache.lucene.util.LongHeap;
|
||||
|
||||
/**
|
||||
* A {@link PostingList} for ANN search results. Transforms result from similarity order to row ID order.
|
||||
*/
|
||||
public class VectorPostingList implements PostingList
|
||||
{
|
||||
private final LongHeap segmentRowIds;
|
||||
private final int size;
|
||||
private final int visitedCount;
|
||||
|
||||
public VectorPostingList(PrimitiveIterator.OfInt source, int limit, int visitedCount)
|
||||
{
|
||||
this.visitedCount = visitedCount;
|
||||
segmentRowIds = new LongHeap(Math.max(limit, 1));
|
||||
int n = 0;
|
||||
while (source.hasNext() && n++ < limit)
|
||||
segmentRowIds.push(source.nextInt());
|
||||
this.size = n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextPosting()
|
||||
{
|
||||
if (segmentRowIds.size() == 0)
|
||||
return PostingList.END_OF_STREAM;
|
||||
return segmentRowIds.pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size()
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long advance(long targetRowID) throws IOException
|
||||
{
|
||||
long rowId;
|
||||
do
|
||||
{
|
||||
rowId = nextPosting();
|
||||
} while (rowId < targetRowID);
|
||||
return rowId;
|
||||
}
|
||||
|
||||
public int getVisitedCount()
|
||||
{
|
||||
return visitedCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ package org.apache.cassandra.index.sai.disk.v1.segment;
|
|||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.cassandra.db.PartitionPosition;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
||||
|
|
@ -32,11 +34,11 @@ import org.apache.cassandra.index.sai.postings.PostingList;
|
|||
|
||||
/**
|
||||
* Abstract reader for individual segments of an on-disk index.
|
||||
*
|
||||
* <p>
|
||||
* Accepts shared resources (token/offset file readers), and uses them to perform lookups against on-disk data
|
||||
* structures.
|
||||
*/
|
||||
public abstract class IndexSegmentSearcher implements Closeable
|
||||
public abstract class IndexSegmentSearcher implements SegmentOrdering, Closeable
|
||||
{
|
||||
final PrimaryKeyMap.Factory primaryKeyMapFactory;
|
||||
final PerColumnIndexFiles indexFiles;
|
||||
|
|
@ -60,9 +62,12 @@ public abstract class IndexSegmentSearcher implements Closeable
|
|||
SegmentMetadata segmentMetadata,
|
||||
IndexContext indexContext) throws IOException
|
||||
{
|
||||
return indexContext.isLiteral()
|
||||
? new LiteralIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, indexContext)
|
||||
: new NumericIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, indexContext);
|
||||
if (indexContext.isVector())
|
||||
return new VectorIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, indexContext);
|
||||
else if (indexContext.isLiteral())
|
||||
return new LiteralIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, indexContext);
|
||||
else
|
||||
return new NumericIndexSegmentSearcher(primaryKeyMapFactory, indexFiles, segmentMetadata, indexContext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,11 +83,11 @@ public abstract class IndexSegmentSearcher implements Closeable
|
|||
*
|
||||
* @return {@link KeyRangeIterator} with matches for the given expression
|
||||
*/
|
||||
public abstract KeyRangeIterator search(Expression expression, QueryContext queryContext) throws IOException;
|
||||
public abstract KeyRangeIterator search(Expression expression, AbstractBounds<PartitionPosition> keyRange, QueryContext queryContext) throws IOException;
|
||||
|
||||
KeyRangeIterator toIterator(PostingList postingList, QueryContext queryContext) throws IOException
|
||||
KeyRangeIterator toPrimaryKeyIterator(PostingList postingList, QueryContext queryContext) throws IOException
|
||||
{
|
||||
if (postingList == null)
|
||||
if (postingList == null || postingList.size() == 0)
|
||||
return KeyRangeIterator.empty();
|
||||
|
||||
IndexSegmentSearcherContext searcherContext = new IndexSegmentSearcherContext(metadata.minKey,
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ public class IndexSegmentSearcherContext
|
|||
this.segmentRowIdOffset = segmentRowIdOffset;
|
||||
|
||||
this.minimumKey = minimumKey;
|
||||
|
||||
// use segment's metadata for the range iterator, may not be accurate, but should not matter to performance.
|
||||
this.maximumKey = maximumKey;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,17 +25,18 @@ import com.google.common.base.MoreObjects;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.db.PartitionPosition;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexComponent;
|
||||
import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
|
||||
import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners;
|
||||
import org.apache.cassandra.index.sai.metrics.QueryEventListener;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.postings.PostingList;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +80,7 @@ public class LiteralIndexSegmentSearcher extends IndexSegmentSearcher
|
|||
|
||||
@Override
|
||||
@SuppressWarnings({"resource", "RedundantSuppression"})
|
||||
public KeyRangeIterator search(Expression expression, QueryContext queryContext) throws IOException
|
||||
public KeyRangeIterator search(Expression expression, AbstractBounds<PartitionPosition> keyRange, QueryContext queryContext) throws IOException
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
logger.trace(indexContext.logMessage("Searching on expression '{}'..."), expression);
|
||||
|
|
@ -89,8 +90,7 @@ public class LiteralIndexSegmentSearcher extends IndexSegmentSearcher
|
|||
|
||||
final ByteComparable term = ByteComparable.fixedLength(expression.lower.value.encoded);
|
||||
QueryEventListener.TrieIndexEventListener listener = MulticastQueryEventListeners.of(queryContext, perColumnEventListener);
|
||||
PostingList postingList = reader.exactMatch(term, listener, queryContext);
|
||||
return toIterator(postingList, queryContext);
|
||||
return toPrimaryKeyIterator(reader.exactMatch(term, listener, queryContext), queryContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import com.google.common.base.MoreObjects;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.db.PartitionPosition;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
||||
|
|
@ -34,7 +36,6 @@ import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
|||
import org.apache.cassandra.index.sai.metrics.MulticastQueryEventListeners;
|
||||
import org.apache.cassandra.index.sai.metrics.QueryEventListener;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.postings.PostingList;
|
||||
import org.apache.lucene.index.CorruptIndexException;
|
||||
|
||||
import static org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeQueries.balancedTreeQueryFrom;
|
||||
|
|
@ -79,7 +80,7 @@ public class NumericIndexSegmentSearcher extends IndexSegmentSearcher
|
|||
|
||||
@Override
|
||||
@SuppressWarnings({"resource", "RedundantSuppression"})
|
||||
public KeyRangeIterator search(Expression exp, QueryContext context) throws IOException
|
||||
public KeyRangeIterator search(Expression exp, AbstractBounds<PartitionPosition> keyRange, QueryContext context) throws IOException
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
logger.trace(indexContext.logMessage("Searching on expression '{}'..."), exp);
|
||||
|
|
@ -88,8 +89,7 @@ public class NumericIndexSegmentSearcher extends IndexSegmentSearcher
|
|||
{
|
||||
final BlockBalancedTreeReader.IntersectVisitor query = balancedTreeQueryFrom(exp, treeReader.getBytesPerValue());
|
||||
QueryEventListener.BalancedTreeEventListener listener = MulticastQueryEventListeners.of(context, perColumnEventListener);
|
||||
PostingList postingList = treeReader.intersect(query, listener, context);
|
||||
return toIterator(postingList, context);
|
||||
return toPrimaryKeyIterator(treeReader.intersect(query, listener, context), context);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package org.apache.cassandra.index.sai.disk.v1.segment;
|
|||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
|||
import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
|
||||
/**
|
||||
|
|
@ -40,7 +42,7 @@ import org.apache.cassandra.io.util.FileUtils;
|
|||
* It also helps to reduce resource consumption for read requests as only segments that intersect with read request data
|
||||
* range need to be loaded.
|
||||
*/
|
||||
public class Segment implements Closeable
|
||||
public class Segment implements SegmentOrdering, Closeable
|
||||
{
|
||||
private final Token.KeyBound minKeyBound;
|
||||
private final Token.KeyBound maxKeyBound;
|
||||
|
|
@ -105,9 +107,15 @@ public class Segment implements Closeable
|
|||
|
||||
* @return range iterator that matches given expression
|
||||
*/
|
||||
public KeyRangeIterator search(Expression expression, QueryContext context) throws IOException
|
||||
public KeyRangeIterator search(Expression expression, AbstractBounds<PartitionPosition> keyRange, QueryContext context) throws IOException
|
||||
{
|
||||
return index.search(expression, context);
|
||||
return index.search(expression, keyRange, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyRangeIterator limitToTopKResults(QueryContext context, List<PrimaryKey> primaryKeys, Expression expression) throws IOException
|
||||
{
|
||||
return index.limitToTopKResults(context, primaryKeys, expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ import org.slf4j.LoggerFactory;
|
|||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
|
||||
import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig;
|
||||
import org.apache.cassandra.index.sai.disk.v1.bbtree.BlockBalancedTreeRamBuffer;
|
||||
import org.apache.cassandra.index.sai.disk.v1.bbtree.NumericIndexWriter;
|
||||
import org.apache.cassandra.index.sai.disk.v1.trie.LiteralIndexWriter;
|
||||
import org.apache.cassandra.index.sai.disk.v1.vector.OnHeapGraph;
|
||||
import org.apache.cassandra.index.sai.memory.RAMStringIndexer;
|
||||
import org.apache.cassandra.index.sai.utils.NamedMemoryLimiter;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
|
|
@ -161,6 +163,35 @@ public abstract class SegmentBuilder
|
|||
}
|
||||
}
|
||||
|
||||
public static class VectorSegmentBuilder extends SegmentBuilder
|
||||
{
|
||||
private final OnHeapGraph<Integer> graphIndex;
|
||||
|
||||
public VectorSegmentBuilder(AbstractType<?> termComparator, NamedMemoryLimiter limiter, IndexWriterConfig indexWriterConfig)
|
||||
{
|
||||
super(termComparator, limiter);
|
||||
graphIndex = new OnHeapGraph<>(termComparator, indexWriterConfig, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return graphIndex.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long addInternal(ByteBuffer term, int segmentRowId)
|
||||
{
|
||||
return graphIndex.add(term, segmentRowId, OnHeapGraph.InvalidVectorBehavior.IGNORE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SegmentMetadata.ComponentMetadataMap flushInternal(IndexDescriptor indexDescriptor, IndexContext indexContext) throws IOException
|
||||
{
|
||||
return graphIndex.writeData(indexDescriptor, indexContext, p -> p);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getActiveBuilderCount()
|
||||
{
|
||||
return ACTIVE_BUILDER_COUNT.get();
|
||||
|
|
|
|||
|
|
@ -128,6 +128,11 @@ public class SegmentMetadata
|
|||
this.componentMetadatas = new ComponentMetadataMap(input);
|
||||
}
|
||||
|
||||
public int toSegmentRowId(long sstableRowId)
|
||||
{
|
||||
return Math.toIntExact(sstableRowId - rowIdOffset);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"resource", "RedundantSuppression"})
|
||||
public static List<SegmentMetadata> load(MetadataSource source, PrimaryKey.Factory primaryKeyFactory) throws IOException
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.disk.v1.segment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
|
||||
/**
|
||||
* A {@link SegmentOrdering} orders and limits a list of {@link PrimaryKey}s.
|
||||
* <p>
|
||||
* When using {@link SegmentOrdering} there are several steps to
|
||||
* build the list of Primary Keys to be ordered and limited:
|
||||
* <p>
|
||||
* 1. Find all primary keys that match each non-ordering query predicate.
|
||||
* 2. Union and intersect the results of step 1 to build a single {@link KeyRangeIterator}
|
||||
* ordered by {@link PrimaryKey}.
|
||||
* 3. Filter out any shadowed primary keys.
|
||||
* 4. Fan the primary keys from step 3 out to each sstable segment to order and limit each
|
||||
* list of primary keys.
|
||||
* <p>
|
||||
* SegmentOrdering handles the fourth step.
|
||||
* <p>
|
||||
* Note: a segment ordering is only used when a query has both ordering and non-ordering predicates.
|
||||
* Where a query has only ordering predicates, the ordering is handled by
|
||||
* {@link org.apache.cassandra.index.sai.disk.SSTableIndex#search(Expression, AbstractBounds, QueryContext)}.
|
||||
*/
|
||||
public interface SegmentOrdering
|
||||
{
|
||||
/**
|
||||
* Reorder, limit, and put back into original order the results from a single sstable
|
||||
*/
|
||||
default KeyRangeIterator limitToTopKResults(QueryContext queryContext, List<PrimaryKey> primaryKeys, Expression expression) throws IOException
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
/*
|
||||
* 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.disk.v1.segment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.jbellis.jvector.util.Bits;
|
||||
import io.github.jbellis.jvector.util.SparseFixedBitSet;
|
||||
import org.agrona.collections.IntArrayList;
|
||||
import org.apache.cassandra.db.PartitionPosition;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.VectorQueryContext;
|
||||
import org.apache.cassandra.index.sai.disk.PrimaryKeyMap;
|
||||
import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
|
||||
import org.apache.cassandra.index.sai.disk.v1.vector.DiskAnn;
|
||||
import org.apache.cassandra.index.sai.disk.v1.vector.OptimizeFor;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeListIterator;
|
||||
import org.apache.cassandra.index.sai.memory.VectorMemoryIndex;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.postings.IntArrayPostingList;
|
||||
import org.apache.cassandra.index.sai.postings.PostingList;
|
||||
import org.apache.cassandra.index.sai.utils.AtomicRatio;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.index.sai.utils.RangeUtil;
|
||||
import org.apache.cassandra.index.sai.utils.TypeUtil;
|
||||
import org.apache.cassandra.tracing.Tracing;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
/**
|
||||
* Executes ANN search against a vector graph for an individual index segment.
|
||||
*/
|
||||
public class VectorIndexSegmentSearcher extends IndexSegmentSearcher
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||
|
||||
private final DiskAnn graph;
|
||||
private final int globalBruteForceRows;
|
||||
private final AtomicRatio actualExpectedRatio = new AtomicRatio();
|
||||
private final ThreadLocal<SparseFixedBitSet> cachedBitSets;
|
||||
private final OptimizeFor optimizeFor;
|
||||
|
||||
VectorIndexSegmentSearcher(PrimaryKeyMap.Factory primaryKeyMapFactory,
|
||||
PerColumnIndexFiles perIndexFiles,
|
||||
SegmentMetadata segmentMetadata,
|
||||
IndexContext indexContext) throws IOException
|
||||
{
|
||||
super(primaryKeyMapFactory, perIndexFiles, segmentMetadata, indexContext);
|
||||
graph = new DiskAnn(segmentMetadata.componentMetadatas, perIndexFiles, indexContext);
|
||||
cachedBitSets = ThreadLocal.withInitial(() -> new SparseFixedBitSet(graph.size()));
|
||||
globalBruteForceRows = Integer.MAX_VALUE;
|
||||
optimizeFor = indexContext.getIndexWriterConfig().getOptimizeFor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long indexFileCacheSize()
|
||||
{
|
||||
return graph.ramBytesUsed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyRangeIterator search(Expression exp, AbstractBounds<PartitionPosition> keyRange, QueryContext context) throws IOException
|
||||
{
|
||||
int limit = context.vectorContext().limit();
|
||||
|
||||
if (logger.isTraceEnabled())
|
||||
logger.trace(indexContext.logMessage("Searching on expression '{}'..."), exp);
|
||||
|
||||
if (exp.getOp() != Expression.IndexOperator.ANN)
|
||||
throw new IllegalArgumentException(indexContext.logMessage("Unsupported expression during ANN index query: " + exp));
|
||||
|
||||
int topK = optimizeFor.topKFor(limit);
|
||||
BitsOrPostingList bitsOrPostingList = bitsOrPostingListForKeyRange(context.vectorContext(), keyRange, topK);
|
||||
if (bitsOrPostingList.skipANN())
|
||||
return toPrimaryKeyIterator(bitsOrPostingList.postingList(), context);
|
||||
|
||||
float[] queryVector = TypeUtil.decomposeVector(indexContext, exp.lower.value.raw.duplicate());
|
||||
var vectorPostings = graph.search(queryVector, topK, limit, bitsOrPostingList.getBits());
|
||||
if (bitsOrPostingList.expectedNodesVisited >= 0)
|
||||
updateExpectedNodes(vectorPostings.getVisitedCount(), bitsOrPostingList.expectedNodesVisited);
|
||||
return toPrimaryKeyIterator(vectorPostings, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bit set we need to search the graph; otherwise return posting list to bypass the graph
|
||||
*/
|
||||
private BitsOrPostingList bitsOrPostingListForKeyRange(VectorQueryContext context, AbstractBounds<PartitionPosition> keyRange, int limit) throws IOException
|
||||
{
|
||||
try (PrimaryKeyMap primaryKeyMap = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap())
|
||||
{
|
||||
// not restricted
|
||||
if (RangeUtil.coversFullRing(keyRange))
|
||||
return new BitsOrPostingList(context.bitsetForShadowedPrimaryKeys(metadata, primaryKeyMap, graph));
|
||||
|
||||
// it will return the next row id if given key is not found.
|
||||
long minSSTableRowId = primaryKeyMap.ceiling(keyRange.left.getToken());
|
||||
// If we didn't find the first key, we won't find the last primary key either
|
||||
if (minSSTableRowId < 0)
|
||||
return new BitsOrPostingList(PostingList.EMPTY);
|
||||
long maxSSTableRowId = getMaxSSTableRowId(primaryKeyMap, keyRange.right);
|
||||
|
||||
if (minSSTableRowId > maxSSTableRowId)
|
||||
return new BitsOrPostingList(PostingList.EMPTY);
|
||||
|
||||
// if it covers entire segment, skip bit set
|
||||
if (minSSTableRowId <= metadata.minSSTableRowId && maxSSTableRowId >= metadata.maxSSTableRowId)
|
||||
return new BitsOrPostingList(context.bitsetForShadowedPrimaryKeys(metadata, primaryKeyMap, graph));
|
||||
|
||||
minSSTableRowId = Math.max(minSSTableRowId, metadata.minSSTableRowId);
|
||||
maxSSTableRowId = min(maxSSTableRowId, metadata.maxSSTableRowId);
|
||||
|
||||
// If num of matches are not bigger than limit, skip ANN.
|
||||
// (nRows should not include shadowed rows, but context doesn't break those out by segment,
|
||||
// so we will live with the inaccuracy.)
|
||||
var nRows = Math.toIntExact(maxSSTableRowId - minSSTableRowId + 1);
|
||||
int maxBruteForceRows = min(globalBruteForceRows, maxBruteForceRows(limit, nRows, graph.size()));
|
||||
logger.trace("Search range covers {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}",
|
||||
nRows, maxBruteForceRows, graph.size(), limit);
|
||||
Tracing.trace("Search range covers {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}",
|
||||
nRows, maxBruteForceRows, graph.size(), limit);
|
||||
if (nRows <= maxBruteForceRows)
|
||||
{
|
||||
IntArrayList postings = new IntArrayList(Math.toIntExact(nRows), -1);
|
||||
for (long sstableRowId = minSSTableRowId; sstableRowId <= maxSSTableRowId; sstableRowId++)
|
||||
{
|
||||
if (context.shouldInclude(sstableRowId, primaryKeyMap))
|
||||
postings.addInt(metadata.toSegmentRowId(sstableRowId));
|
||||
}
|
||||
return new BitsOrPostingList(new IntArrayPostingList(postings.toIntArray()));
|
||||
}
|
||||
|
||||
// create a bitset of ordinals corresponding to the rows in the given key range
|
||||
SparseFixedBitSet bits = bitSetForSearch();
|
||||
boolean hasMatches = false;
|
||||
try (var ordinalsView = graph.getOrdinalsView())
|
||||
{
|
||||
for (long sstableRowId = minSSTableRowId; sstableRowId <= maxSSTableRowId; sstableRowId++)
|
||||
{
|
||||
if (context.shouldInclude(sstableRowId, primaryKeyMap))
|
||||
{
|
||||
int segmentRowId = metadata.toSegmentRowId(sstableRowId);
|
||||
int ordinal = ordinalsView.getOrdinalForRowId(segmentRowId);
|
||||
if (ordinal >= 0)
|
||||
{
|
||||
bits.set(ordinal);
|
||||
hasMatches = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (!hasMatches)
|
||||
return new BitsOrPostingList(PostingList.EMPTY);
|
||||
|
||||
return new BitsOrPostingList(bits, VectorMemoryIndex.expectedNodesVisited(limit, nRows, graph.size()));
|
||||
}
|
||||
}
|
||||
|
||||
private long getMaxSSTableRowId(PrimaryKeyMap primaryKeyMap, PartitionPosition right)
|
||||
{
|
||||
// if the right token is the minimum token, there is no upper bound on the keyRange and
|
||||
// we can save a lookup by using the maxSSTableRowId
|
||||
if (right.isMinimum())
|
||||
return metadata.maxSSTableRowId;
|
||||
|
||||
long max = primaryKeyMap.floor(right.getToken());
|
||||
if (max < 0)
|
||||
return metadata.maxSSTableRowId;
|
||||
return max;
|
||||
}
|
||||
|
||||
private SparseFixedBitSet bitSetForSearch()
|
||||
{
|
||||
var bits = cachedBitSets.get();
|
||||
bits.clear();
|
||||
return bits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyRangeIterator limitToTopKResults(QueryContext context, List<PrimaryKey> primaryKeys, Expression expression) throws IOException
|
||||
{
|
||||
int limit = context.vectorContext().limit();
|
||||
// VSTODO would it be better to do a binary search to find the boundaries?
|
||||
List<PrimaryKey> keysInRange = primaryKeys.stream()
|
||||
.dropWhile(k -> k.compareTo(metadata.minKey) < 0)
|
||||
.takeWhile(k -> k.compareTo(metadata.maxKey) <= 0)
|
||||
.collect(Collectors.toList());
|
||||
if (keysInRange.isEmpty())
|
||||
return KeyRangeIterator.empty();
|
||||
int topK = optimizeFor.topKFor(limit);
|
||||
if (shouldUseBruteForce(topK, limit, keysInRange.size()))
|
||||
return new KeyRangeListIterator(metadata.minKey, metadata.maxKey, keysInRange);
|
||||
|
||||
try (PrimaryKeyMap primaryKeyMap = primaryKeyMapFactory.newPerSSTablePrimaryKeyMap())
|
||||
{
|
||||
// the iterator represents keys from the whole table -- we'll only pull of those that
|
||||
// are from our own token range, so we can use row ids to order the results by vector similarity.
|
||||
var maxSegmentRowId = metadata.toSegmentRowId(metadata.maxSSTableRowId);
|
||||
SparseFixedBitSet bits = bitSetForSearch();
|
||||
var rowIds = new IntArrayList();
|
||||
try (var ordinalsView = graph.getOrdinalsView())
|
||||
{
|
||||
for (PrimaryKey primaryKey : keysInRange)
|
||||
{
|
||||
long sstableRowId = primaryKeyMap.rowIdFromPrimaryKey(primaryKey);
|
||||
// skip rows that are not in our segment (or more preciesely, have no vectors that were indexed)
|
||||
// or are not in this segment (exactRowIdForPrimaryKey returns a negative value for not found)
|
||||
if (sstableRowId < metadata.minSSTableRowId)
|
||||
continue;
|
||||
|
||||
// if sstable row id has exceeded current ANN segment, stop
|
||||
if (sstableRowId > metadata.maxSSTableRowId)
|
||||
break;
|
||||
|
||||
int segmentRowId = metadata.toSegmentRowId(sstableRowId);
|
||||
rowIds.add(segmentRowId);
|
||||
// VSTODO now that we know the size of keys evaluated, is it worth doing the brute
|
||||
// force check eagerly to potentially skip the PK to sstable row id to ordinal lookup?
|
||||
int ordinal = ordinalsView.getOrdinalForRowId(segmentRowId);
|
||||
if (ordinal >= 0)
|
||||
bits.set(ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUseBruteForce(topK, limit, rowIds.size()))
|
||||
return toPrimaryKeyIterator(new IntArrayPostingList(rowIds.toIntArray()), context);
|
||||
|
||||
// else ask the index to perform a search limited to the bits we created
|
||||
float[] queryVector = TypeUtil.decomposeVector(indexContext, expression.lower.value.raw.duplicate());
|
||||
var results = graph.search(queryVector, topK, limit, bits);
|
||||
updateExpectedNodes(results.getVisitedCount(), expectedNodesVisited(topK, maxSegmentRowId, graph.size()));
|
||||
return toPrimaryKeyIterator(results, context);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldUseBruteForce(int topK, int limit, int numRows)
|
||||
{
|
||||
// if we have a small number of results then let TopK processor do exact NN computation
|
||||
var maxBruteForceRows = min(globalBruteForceRows, maxBruteForceRows(topK, numRows, graph.size()));
|
||||
logger.trace("SAI materialized {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}",
|
||||
numRows, maxBruteForceRows, graph.size(), limit);
|
||||
Tracing.trace("SAI materialized {} rows; max brute force rows is {} for sstable index with {} nodes, LIMIT {}",
|
||||
numRows, maxBruteForceRows, graph.size(), limit);
|
||||
return numRows <= maxBruteForceRows;
|
||||
}
|
||||
|
||||
private int maxBruteForceRows(int limit, int nPermittedOrdinals, int graphSize)
|
||||
{
|
||||
int expectedNodes = expectedNodesVisited(limit, nPermittedOrdinals, graphSize);
|
||||
// ANN index will do a bunch of extra work besides the full comparisons (performing PQ similarity for each edge);
|
||||
// brute force from sstable will also do a bunch of extra work (going through trie index to look up row).
|
||||
// VSTODO I'm not sure which one is more expensive (and it depends on things like sstable chunk cache hit ratio)
|
||||
// so I'm leaving it as a 1:1 ratio for now.
|
||||
return max(limit, expectedNodes);
|
||||
}
|
||||
|
||||
private int expectedNodesVisited(int limit, int nPermittedOrdinals, int graphSize)
|
||||
{
|
||||
var observedRatio = actualExpectedRatio.getUpdateCount() >= 10 ? actualExpectedRatio.get() : 1.0;
|
||||
return (int) (observedRatio * VectorMemoryIndex.expectedNodesVisited(limit, nPermittedOrdinals, graphSize));
|
||||
}
|
||||
|
||||
private void updateExpectedNodes(int actualNodesVisited, int expectedNodesVisited)
|
||||
{
|
||||
assert expectedNodesVisited >= 0 : expectedNodesVisited;
|
||||
assert actualNodesVisited >= 0 : actualNodesVisited;
|
||||
if (actualNodesVisited >= 1000 && actualNodesVisited > 2 * expectedNodesVisited || expectedNodesVisited > 2 * actualNodesVisited)
|
||||
logger.warn("Predicted visiting {} nodes, but actually visited {}", expectedNodesVisited, actualNodesVisited);
|
||||
actualExpectedRatio.update(actualNodesVisited, expectedNodesVisited);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("indexContext", indexContext)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
graph.close();
|
||||
}
|
||||
|
||||
private static class BitsOrPostingList
|
||||
{
|
||||
private final Bits bits;
|
||||
private final int expectedNodesVisited;
|
||||
private final PostingList postingList;
|
||||
|
||||
public BitsOrPostingList(@Nullable Bits bits, int expectedNodesVisited)
|
||||
{
|
||||
this.bits = bits;
|
||||
this.expectedNodesVisited = expectedNodesVisited;
|
||||
this.postingList = null;
|
||||
}
|
||||
|
||||
public BitsOrPostingList(@Nullable Bits bits)
|
||||
{
|
||||
this.bits = bits;
|
||||
this.postingList = null;
|
||||
this.expectedNodesVisited = -1;
|
||||
}
|
||||
|
||||
public BitsOrPostingList(PostingList postingList)
|
||||
{
|
||||
this.bits = null;
|
||||
this.postingList = Preconditions.checkNotNull(postingList);
|
||||
this.expectedNodesVisited = -1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Bits getBits()
|
||||
{
|
||||
Preconditions.checkState(!skipANN());
|
||||
return bits;
|
||||
}
|
||||
|
||||
public PostingList postingList()
|
||||
{
|
||||
Preconditions.checkState(skipANN());
|
||||
return postingList;
|
||||
}
|
||||
|
||||
public boolean skipANN()
|
||||
{
|
||||
return postingList != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ public class TrieTermsDictionaryReader extends Walker<TrieTermsDictionaryReader>
|
|||
super(rebufferer, root);
|
||||
}
|
||||
|
||||
public static final TrieSerializer<Long, DataOutputPlus> trieSerializer = new TrieSerializer<Long, DataOutputPlus>()
|
||||
public static final TrieSerializer<Long, DataOutputPlus> trieSerializer = new TrieSerializer<>()
|
||||
{
|
||||
@Override
|
||||
public int sizeofNode(SerializationNode<Long> node, long nodePosition)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.cliffc.high_scale_lib.NonBlockingHashMapLong;
|
||||
|
||||
import io.github.jbellis.jvector.util.Bits;
|
||||
|
||||
public class BitsUtil
|
||||
{
|
||||
public static Bits bitsIgnoringDeleted(Bits toAccept, Set<Integer> deletedOrdinals)
|
||||
{
|
||||
return deletedOrdinals.isEmpty()
|
||||
? toAccept
|
||||
: toAccept == null ? new NoDeletedBits(deletedOrdinals) : new NoDeletedIntersectingBits(toAccept, deletedOrdinals);
|
||||
}
|
||||
|
||||
public static <T> Bits bitsIgnoringDeleted(Bits toAccept, NonBlockingHashMapLong<VectorPostings<T>> postings)
|
||||
{
|
||||
return toAccept == null ? new NoDeletedPostings<>(postings) : new NoDeletedIntersectingPostings<>(toAccept, postings);
|
||||
}
|
||||
|
||||
private static abstract class BitsWithoutLength implements Bits, org.apache.lucene.util.Bits
|
||||
{
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
// length() is not called on search path
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoDeletedBits extends BitsWithoutLength
|
||||
{
|
||||
private final Set<Integer> deletedOrdinals;
|
||||
|
||||
private NoDeletedBits(Set<Integer> deletedOrdinals)
|
||||
{
|
||||
this.deletedOrdinals = deletedOrdinals;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int i)
|
||||
{
|
||||
return !deletedOrdinals.contains(i);
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoDeletedIntersectingBits extends BitsWithoutLength
|
||||
{
|
||||
private final Bits toAccept;
|
||||
private final Set<Integer> deletedOrdinals;
|
||||
|
||||
private NoDeletedIntersectingBits(Bits toAccept, Set<Integer> deletedOrdinals)
|
||||
{
|
||||
this.toAccept = toAccept;
|
||||
this.deletedOrdinals = deletedOrdinals;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int i)
|
||||
{
|
||||
return !deletedOrdinals.contains(i) && toAccept.get(i);
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoDeletedPostings<T> extends BitsWithoutLength
|
||||
{
|
||||
private final NonBlockingHashMapLong<VectorPostings<T>> postings;
|
||||
|
||||
public NoDeletedPostings(NonBlockingHashMapLong<VectorPostings<T>> postings)
|
||||
{
|
||||
this.postings = postings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int i)
|
||||
{
|
||||
var p = postings.get(i);
|
||||
assert p != null : "No postings for ordinal " + i;
|
||||
return !p.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoDeletedIntersectingPostings<T> extends BitsWithoutLength
|
||||
{
|
||||
private final Bits toAccept;
|
||||
private final NonBlockingHashMapLong<VectorPostings<T>> postings;
|
||||
|
||||
public NoDeletedIntersectingPostings(Bits toAccept, NonBlockingHashMapLong<VectorPostings<T>> postings)
|
||||
{
|
||||
this.toAccept = toAccept;
|
||||
this.postings = postings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int i)
|
||||
{
|
||||
var p = postings.get(i);
|
||||
assert p != null : "No postings for ordinal " + i;
|
||||
return !p.isEmpty() && toAccept.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.disk.SSTableIndex;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
|
||||
/**
|
||||
* A {@link KeyRangeIterator}, used when returning results from a vector search, that checks for query timeouts
|
||||
* during the iteration process.
|
||||
*/
|
||||
public class CheckpointingIterator extends KeyRangeIterator
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CheckpointingIterator.class);
|
||||
|
||||
private final QueryContext context;
|
||||
private final KeyRangeIterator union;
|
||||
private final Iterable<SSTableIndex> referencedIndexes;
|
||||
|
||||
public CheckpointingIterator(KeyRangeIterator wrapped,
|
||||
Iterable<SSTableIndex> referencedIndexes,
|
||||
Iterable<SSTableIndex> referencedAnnIndexesInHybridSearch,
|
||||
QueryContext queryContext)
|
||||
{
|
||||
super(wrapped.getMinimum(), wrapped.getMaximum(), wrapped.getCount());
|
||||
|
||||
this.union = wrapped;
|
||||
if (referencedAnnIndexesInHybridSearch != null)
|
||||
this.referencedIndexes = Iterables.concat(referencedIndexes, referencedAnnIndexesInHybridSearch);
|
||||
else
|
||||
this.referencedIndexes = referencedIndexes;
|
||||
this.context = queryContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PrimaryKey computeNext()
|
||||
{
|
||||
try
|
||||
{
|
||||
return union.hasNext() ? union.next() : endOfData();
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.checkpoint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performSkipTo(PrimaryKey nextKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
union.skipTo(nextKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.checkpoint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
FileUtils.closeQuietly(union);
|
||||
referencedIndexes.forEach(CheckpointingIterator::releaseQuietly);
|
||||
}
|
||||
|
||||
private static void releaseQuietly(SSTableIndex index)
|
||||
{
|
||||
try
|
||||
{
|
||||
index.release();
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
logger.error(index.getIndexContext().logMessage(String.format("Failed to release index on SSTable %s", index.getSSTable())), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
import io.github.jbellis.jvector.util.RamUsageEstimator;
|
||||
import org.apache.cassandra.db.marshal.VectorType;
|
||||
import org.apache.cassandra.io.util.SequentialWriter;
|
||||
|
||||
@NotThreadSafe
|
||||
public class CompactionVectorValues implements RamAwareVectorValues
|
||||
{
|
||||
private final int dimension;
|
||||
private final ArrayList<ByteBuffer> values = new ArrayList<>();
|
||||
private final VectorType<Float> type;
|
||||
|
||||
public CompactionVectorValues(VectorType<Float> type)
|
||||
{
|
||||
this.dimension = type.dimension;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
return values.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dimension()
|
||||
{
|
||||
return dimension;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] vectorValue(int i)
|
||||
{
|
||||
return type.composeAsFloat(values.get(i));
|
||||
}
|
||||
|
||||
/** return approximate bytes used by the new vector */
|
||||
public long add(int ordinal, ByteBuffer value)
|
||||
{
|
||||
if (ordinal != values.size())
|
||||
throw new IllegalArgumentException(String.format("CVV requires vectors to be added in ordinal order (%d given, expected %d)",
|
||||
ordinal, values.size()));
|
||||
values.add(value);
|
||||
return RamEstimation.concurrentHashMapRamUsed(1) + oneVectorBytesUsed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompactionVectorValues copy()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public long write(SequentialWriter writer) throws IOException
|
||||
{
|
||||
writer.writeInt(size());
|
||||
writer.writeInt(dimension());
|
||||
|
||||
for (var i = 0; i < size(); i++) {
|
||||
var bb = values.get(i);
|
||||
assert bb != null : "null vector at index " + i + " of " + size();
|
||||
writer.write(bb);
|
||||
}
|
||||
|
||||
return writer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueShared()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private long oneVectorBytesUsed()
|
||||
{
|
||||
return RamUsageEstimator.NUM_BYTES_OBJECT_REF;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import org.jctools.maps.NonBlockingHashMapLong;
|
||||
|
||||
public class ConcurrentVectorValues implements RamAwareVectorValues
|
||||
{
|
||||
private final int dimensions;
|
||||
private final NonBlockingHashMapLong<float[]> values = new NonBlockingHashMapLong<>();
|
||||
|
||||
public ConcurrentVectorValues(int dimensions)
|
||||
{
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size()
|
||||
{
|
||||
return values.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dimension()
|
||||
{
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] vectorValue(int i)
|
||||
{
|
||||
return values.get(i);
|
||||
}
|
||||
|
||||
/** return approximate bytes used by the new vector */
|
||||
public long add(int ordinal, float[] vector)
|
||||
{
|
||||
values.put(ordinal, vector);
|
||||
return RamEstimation.concurrentHashMapRamUsed(1) + oneVectorBytesUsed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValueShared()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConcurrentVectorValues copy()
|
||||
{
|
||||
// no actual copy required because we always return distinct float[] for distinct vector ordinals
|
||||
return this;
|
||||
}
|
||||
|
||||
private long oneVectorBytesUsed()
|
||||
{
|
||||
return Integer.BYTES + Integer.BYTES + (long) dimension() * Float.BYTES;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.PrimitiveIterator;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import io.github.jbellis.jvector.disk.CachingGraphIndex;
|
||||
import io.github.jbellis.jvector.disk.OnDiskGraphIndex;
|
||||
import io.github.jbellis.jvector.graph.GraphSearcher;
|
||||
import io.github.jbellis.jvector.graph.NeighborSimilarity;
|
||||
import io.github.jbellis.jvector.graph.SearchResult;
|
||||
import io.github.jbellis.jvector.graph.SearchResult.NodeScore;
|
||||
import io.github.jbellis.jvector.pq.CompressedVectors;
|
||||
import io.github.jbellis.jvector.util.Bits;
|
||||
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexComponent;
|
||||
import org.apache.cassandra.index.sai.disk.v1.PerColumnIndexFiles;
|
||||
import org.apache.cassandra.index.sai.disk.v1.postings.VectorPostingList;
|
||||
import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
|
||||
import org.apache.cassandra.io.util.FileHandle;
|
||||
import org.apache.cassandra.tracing.Tracing;
|
||||
|
||||
public class DiskAnn implements AutoCloseable
|
||||
{
|
||||
private final FileHandle graphHandle;
|
||||
private final OnDiskOrdinalsMap ordinalsMap;
|
||||
private final CachingGraphIndex graph;
|
||||
private final VectorSimilarityFunction similarityFunction;
|
||||
|
||||
// only one of these will be not null
|
||||
private final CompressedVectors compressedVectors;
|
||||
|
||||
public DiskAnn(SegmentMetadata.ComponentMetadataMap componentMetadatas, PerColumnIndexFiles indexFiles, IndexContext context) throws IOException
|
||||
{
|
||||
similarityFunction = context.getIndexWriterConfig().getSimilarityFunction();
|
||||
|
||||
SegmentMetadata.ComponentMetadata termsMetadata = componentMetadatas.get(IndexComponent.TERMS_DATA);
|
||||
graphHandle = indexFiles.termsData();
|
||||
graph = new CachingGraphIndex(new OnDiskGraphIndex<>(RandomAccessReaderAdapter.createSupplier(graphHandle), termsMetadata.offset));
|
||||
|
||||
long pqSegmentOffset = componentMetadatas.get(IndexComponent.COMPRESSED_VECTORS).offset;
|
||||
try (var pqFileHandle = indexFiles.compressedVectors(); var reader = new RandomAccessReaderAdapter(pqFileHandle))
|
||||
{
|
||||
reader.seek(pqSegmentOffset);
|
||||
var containsCompressedVectors = reader.readBoolean();
|
||||
if (containsCompressedVectors)
|
||||
compressedVectors = CompressedVectors.load(reader, reader.getFilePointer());
|
||||
else
|
||||
compressedVectors = null;
|
||||
}
|
||||
|
||||
SegmentMetadata.ComponentMetadata postingListsMetadata = componentMetadatas.get(IndexComponent.POSTING_LISTS);
|
||||
ordinalsMap = new OnDiskOrdinalsMap(indexFiles.postingLists(), postingListsMetadata.offset, postingListsMetadata.length);
|
||||
}
|
||||
|
||||
public long ramBytesUsed()
|
||||
{
|
||||
return graph.ramBytesUsed();
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
return graph.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Row IDs associated with the topK vectors near the query
|
||||
*/
|
||||
public VectorPostingList search(float[] queryVector, int topK, int limit, Bits acceptBits)
|
||||
{
|
||||
OnHeapGraph.validateIndexable(queryVector, similarityFunction);
|
||||
|
||||
var view = graph.getView();
|
||||
var searcher = new GraphSearcher.Builder<>(view).build();
|
||||
NeighborSimilarity.ScoreFunction scoreFunction;
|
||||
NeighborSimilarity.ReRanker<float[]> reRanker;
|
||||
if (compressedVectors == null)
|
||||
{
|
||||
scoreFunction = (NeighborSimilarity.ExactScoreFunction)
|
||||
i -> similarityFunction.compare(queryVector, view.getVector(i));
|
||||
reRanker = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
scoreFunction = compressedVectors.approximateScoreFunctionFor(queryVector, similarityFunction);
|
||||
reRanker = (i, map) -> similarityFunction.compare(queryVector, map.get(i));
|
||||
}
|
||||
var result = searcher.search(scoreFunction,
|
||||
reRanker,
|
||||
topK,
|
||||
ordinalsMap.ignoringDeleted(acceptBits));
|
||||
Tracing.trace("DiskANN search visited {} nodes to return {} results", result.getVisitedCount(), result.getNodes().length);
|
||||
return annRowIdsToPostings(result, limit);
|
||||
}
|
||||
|
||||
private class RowIdIterator implements PrimitiveIterator.OfInt, AutoCloseable
|
||||
{
|
||||
private final Iterator<NodeScore> it;
|
||||
private final OnDiskOrdinalsMap.RowIdsView rowIdsView = ordinalsMap.getRowIdsView();
|
||||
|
||||
private OfInt segmentRowIdIterator = IntStream.empty().iterator();
|
||||
|
||||
public RowIdIterator(NodeScore[] results)
|
||||
{
|
||||
this.it = Arrays.stream(results).iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
while (!segmentRowIdIterator.hasNext() && it.hasNext())
|
||||
{
|
||||
try
|
||||
{
|
||||
var ordinal = it.next().node;
|
||||
segmentRowIdIterator = Arrays.stream(rowIdsView.getSegmentRowIdsMatching(ordinal)).iterator();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return segmentRowIdIterator.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextInt() {
|
||||
if (!hasNext())
|
||||
throw new NoSuchElementException();
|
||||
return segmentRowIdIterator.nextInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
rowIdsView.close();
|
||||
}
|
||||
}
|
||||
|
||||
private VectorPostingList annRowIdsToPostings(SearchResult results, int limit)
|
||||
{
|
||||
try (var iterator = new RowIdIterator(results.getNodes()))
|
||||
{
|
||||
return new VectorPostingList(iterator, limit, results.getVisitedCount());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
ordinalsMap.close();
|
||||
graph.close();
|
||||
graphHandle.close();
|
||||
}
|
||||
|
||||
public OnDiskOrdinalsMap.OrdinalsView getOrdinalsView()
|
||||
{
|
||||
return ordinalsMap.getOrdinalsView();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.disk.v1.vector;
|
||||
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public class DiskBinarySearch
|
||||
{
|
||||
/**
|
||||
* Search for the target int between positions low and high, using the provided function
|
||||
* to retrieve the int value at the given ordinal.
|
||||
*
|
||||
* Returns the position at which target is found. Raises an exception if it is not found.
|
||||
*
|
||||
* This will not call f() after the target is found, so if f is performing disk seeks,
|
||||
* it will leave the underlying reader at the position right after reading the target.
|
||||
*
|
||||
* @return index if target is found; otherwise return -1 if targer is not found
|
||||
*/
|
||||
public static long searchInt(long low, long high, int target, Function<Long, Integer> f)
|
||||
{
|
||||
assert high < Long.MAX_VALUE >> 2 : "high is too large to avoid potential overflow: " + high;
|
||||
assert low < high : "low must be less than high: " + low + " >= " + high;
|
||||
|
||||
while (low < high)
|
||||
{
|
||||
long i = low + (high - low) / 2;
|
||||
int value = f.apply(i);
|
||||
if (target == value)
|
||||
return i;
|
||||
else if (target > value)
|
||||
low = i + 1;
|
||||
else
|
||||
high = i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import io.github.jbellis.jvector.util.Bits;
|
||||
import org.apache.cassandra.io.util.FileHandle;
|
||||
import org.apache.cassandra.io.util.RandomAccessReader;
|
||||
|
||||
public class OnDiskOrdinalsMap implements AutoCloseable
|
||||
{
|
||||
private final FileHandle fh;
|
||||
private final long ordToRowOffset;
|
||||
private final long segmentEnd;
|
||||
private final int size;
|
||||
// the offset where we switch from recording ordinal -> rows, to row -> ordinal
|
||||
private final long rowOrdinalOffset;
|
||||
private final Set<Integer> deletedOrdinals;
|
||||
|
||||
public OnDiskOrdinalsMap(FileHandle fh, long segmentOffset, long segmentLength)
|
||||
{
|
||||
deletedOrdinals = new HashSet<>();
|
||||
|
||||
this.segmentEnd = segmentOffset + segmentLength;
|
||||
this.fh = fh;
|
||||
try (var reader = fh.createReader())
|
||||
{
|
||||
reader.seek(segmentOffset);
|
||||
int deletedCount = reader.readInt();
|
||||
for (var i = 0; i < deletedCount; i++)
|
||||
{
|
||||
deletedOrdinals.add(reader.readInt());
|
||||
}
|
||||
|
||||
this.ordToRowOffset = reader.getFilePointer();
|
||||
this.size = reader.readInt();
|
||||
reader.seek(segmentEnd - 8);
|
||||
this.rowOrdinalOffset = reader.readLong();
|
||||
assert rowOrdinalOffset < segmentEnd : "rowOrdinalOffset " + rowOrdinalOffset + " is not less than segmentEnd " + segmentEnd;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Error initializing OnDiskOrdinalsMap at segment " + segmentOffset, e);
|
||||
}
|
||||
}
|
||||
|
||||
public RowIdsView getRowIdsView()
|
||||
{
|
||||
return new RowIdsView();
|
||||
}
|
||||
|
||||
public Bits ignoringDeleted(Bits acceptBits)
|
||||
{
|
||||
return BitsUtil.bitsIgnoringDeleted(acceptBits, deletedOrdinals);
|
||||
}
|
||||
|
||||
public class RowIdsView implements AutoCloseable
|
||||
{
|
||||
final RandomAccessReader reader = fh.createReader();
|
||||
|
||||
public int[] getSegmentRowIdsMatching(int vectorOrdinal) throws IOException
|
||||
{
|
||||
Preconditions.checkArgument(vectorOrdinal < size, "vectorOrdinal %s is out of bounds %s", vectorOrdinal, size);
|
||||
|
||||
// read index entry
|
||||
try
|
||||
{
|
||||
reader.seek(ordToRowOffset + 4L + vectorOrdinal * 8L);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(String.format("Error seeking to index offset for ordinal %d with ordToRowOffset %d",
|
||||
vectorOrdinal, ordToRowOffset), e);
|
||||
}
|
||||
var offset = reader.readLong();
|
||||
// seek to and read rowIds
|
||||
try
|
||||
{
|
||||
reader.seek(offset);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(String.format("Error seeking to rowIds offset for ordinal %d with ordToRowOffset %d",
|
||||
vectorOrdinal, ordToRowOffset), e);
|
||||
}
|
||||
var postingsSize = reader.readInt();
|
||||
var rowIds = new int[postingsSize];
|
||||
for (var i = 0; i < rowIds.length; i++)
|
||||
{
|
||||
rowIds[i] = reader.readInt();
|
||||
}
|
||||
return rowIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
public OrdinalsView getOrdinalsView()
|
||||
{
|
||||
return new OrdinalsView();
|
||||
}
|
||||
|
||||
public class OrdinalsView implements AutoCloseable
|
||||
{
|
||||
final RandomAccessReader reader = fh.createReader();
|
||||
private final long high = (segmentEnd - 8 - rowOrdinalOffset) / 8;
|
||||
|
||||
/**
|
||||
* @return order if given row id is found; otherwise return -1
|
||||
*/
|
||||
public int getOrdinalForRowId(int rowId) throws IOException
|
||||
{
|
||||
// Compute the offset of the start of the rowId to vectorOrdinal mapping
|
||||
long index = DiskBinarySearch.searchInt(0, Math.toIntExact(high), rowId, i -> {
|
||||
try
|
||||
{
|
||||
long offset = rowOrdinalOffset + i * 8;
|
||||
reader.seek(offset);
|
||||
return reader.readInt();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
// not found
|
||||
if (index < 0)
|
||||
return -1;
|
||||
|
||||
return reader.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
fh.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.cliffc.high_scale_lib.NonBlockingHashMapLong;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.jbellis.jvector.disk.OnDiskGraphIndex;
|
||||
import io.github.jbellis.jvector.graph.GraphIndex;
|
||||
import io.github.jbellis.jvector.graph.GraphIndexBuilder;
|
||||
import io.github.jbellis.jvector.graph.GraphSearcher;
|
||||
import io.github.jbellis.jvector.graph.NeighborSimilarity;
|
||||
import io.github.jbellis.jvector.graph.RandomAccessVectorValues;
|
||||
import io.github.jbellis.jvector.pq.CompressedVectors;
|
||||
import io.github.jbellis.jvector.pq.ProductQuantization;
|
||||
import io.github.jbellis.jvector.util.Bits;
|
||||
import io.github.jbellis.jvector.vector.VectorEncoding;
|
||||
import io.github.jbellis.jvector.vector.VectorSimilarityFunction;
|
||||
import org.apache.cassandra.db.marshal.AbstractType;
|
||||
import org.apache.cassandra.db.marshal.VectorType;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexComponent;
|
||||
import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
|
||||
import org.apache.cassandra.index.sai.disk.io.IndexFileUtils;
|
||||
import org.apache.cassandra.index.sai.disk.v1.IndexWriterConfig;
|
||||
import org.apache.cassandra.index.sai.disk.v1.SAICodecUtils;
|
||||
import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
|
||||
import org.apache.cassandra.io.util.SequentialWriter;
|
||||
import org.apache.cassandra.tracing.Tracing;
|
||||
import org.apache.cassandra.utils.ByteBufferUtil;
|
||||
import org.apache.lucene.util.StringHelper;
|
||||
|
||||
public class OnHeapGraph<T>
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(OnHeapGraph.class);
|
||||
|
||||
private final RamAwareVectorValues vectorValues;
|
||||
private final GraphIndexBuilder<float[]> builder;
|
||||
private final VectorType<?> vectorType;
|
||||
private final VectorSimilarityFunction similarityFunction;
|
||||
private final ConcurrentMap<float[], VectorPostings<T>> postingsMap;
|
||||
private final NonBlockingHashMapLong<VectorPostings<T>> postingsByOrdinal;
|
||||
private final AtomicInteger nextOrdinal = new AtomicInteger();
|
||||
private volatile boolean hasDeletions;
|
||||
|
||||
/**
|
||||
* @param termComparator the vector type
|
||||
* @param indexWriterConfig
|
||||
*
|
||||
* Will create a concurrent object.
|
||||
*/
|
||||
public OnHeapGraph(AbstractType<?> termComparator, IndexWriterConfig indexWriterConfig)
|
||||
{
|
||||
this(termComparator, indexWriterConfig, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param termComparator the vector type
|
||||
* @param indexWriterConfig the {@link IndexWriterConfig} for the graph
|
||||
* @param concurrent should be true for memtables, false for compaction. Concurrent allows us to search
|
||||
* while building the graph; non-concurrent allows us to avoid synchronization costs.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public OnHeapGraph(AbstractType<?> termComparator, IndexWriterConfig indexWriterConfig, boolean concurrent)
|
||||
{
|
||||
this.vectorType = (VectorType<?>) termComparator;
|
||||
vectorValues = concurrent
|
||||
? new ConcurrentVectorValues(((VectorType<?>) termComparator).dimension)
|
||||
: new CompactionVectorValues(((VectorType<Float>) termComparator));
|
||||
similarityFunction = indexWriterConfig.getSimilarityFunction();
|
||||
// We need to be able to inexpensively distinguish different vectors, with a slower path
|
||||
// that identifies vectors that are equal but not the same reference. A comparison
|
||||
// based Map (which only needs to look at vector elements until a difference is found)
|
||||
// is thus a better option than hash-based (which has to look at all elements to compute the hash).
|
||||
postingsMap = new ConcurrentSkipListMap<>(Arrays::compare);
|
||||
postingsByOrdinal = new NonBlockingHashMapLong<>();
|
||||
|
||||
builder = new GraphIndexBuilder<>(vectorValues,
|
||||
VectorEncoding.FLOAT32,
|
||||
similarityFunction,
|
||||
indexWriterConfig.getMaximumNodeConnections(),
|
||||
indexWriterConfig.getConstructionBeamWidth(),
|
||||
1.2f,
|
||||
1.4f);
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
return vectorValues.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return postingsMap.values().stream().allMatch(VectorPostings::isEmpty);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the incremental bytes ysed by adding the given vector to the index
|
||||
*/
|
||||
public long add(ByteBuffer term, T key, InvalidVectorBehavior behavior)
|
||||
{
|
||||
assert term != null && term.remaining() != 0;
|
||||
|
||||
var vector = vectorType.composeAsFloat(term);
|
||||
if (behavior == InvalidVectorBehavior.IGNORE)
|
||||
{
|
||||
try
|
||||
{
|
||||
validateIndexable(vector, similarityFunction);
|
||||
}
|
||||
catch (InvalidRequestException e)
|
||||
{
|
||||
logger.trace("Ignoring invalid vector during index build against existing data: {}", vector, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assert behavior == InvalidVectorBehavior.FAIL;
|
||||
validateIndexable(vector, similarityFunction);
|
||||
}
|
||||
|
||||
var bytesUsed = 0L;
|
||||
VectorPostings<T> postings = postingsMap.get(vector);
|
||||
// if the vector is already in the graph, all that happens is that the postings list is updated
|
||||
// otherwise, we add the vector in this order:
|
||||
// 1. to the postingsMap
|
||||
// 2. to the vectorValues
|
||||
// 3. to the graph
|
||||
// This way, concurrent searches of the graph won't see the vector until it's visible
|
||||
// in the other structures as well.
|
||||
if (postings == null)
|
||||
{
|
||||
postings = new VectorPostings<>(key);
|
||||
// since we are using ConcurrentSkipListMap, it is NOT correct to use computeIfAbsent here
|
||||
if (postingsMap.putIfAbsent(vector, postings) == null)
|
||||
{
|
||||
// we won the race to add the new entry; assign it an ordinal and add to the other structures
|
||||
var ordinal = nextOrdinal.getAndIncrement();
|
||||
postings.setOrdinal(ordinal);
|
||||
bytesUsed += RamEstimation.concurrentHashMapRamUsed(1); // the new posting Map entry
|
||||
bytesUsed += (vectorValues instanceof ConcurrentVectorValues)
|
||||
? ((ConcurrentVectorValues) vectorValues).add(ordinal, vector)
|
||||
: ((CompactionVectorValues) vectorValues).add(ordinal, term);
|
||||
bytesUsed += VectorPostings.emptyBytesUsed() + VectorPostings.bytesPerPosting();
|
||||
postingsByOrdinal.put(ordinal, postings);
|
||||
bytesUsed += builder.addGraphNode(ordinal, vectorValues);
|
||||
return bytesUsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
postings = postingsMap.get(vector);
|
||||
}
|
||||
}
|
||||
// postings list already exists, just add the new key (if it's not already in the list)
|
||||
if (postings.add(key))
|
||||
{
|
||||
bytesUsed += VectorPostings.bytesPerPosting();
|
||||
}
|
||||
|
||||
return bytesUsed;
|
||||
}
|
||||
|
||||
// copied out of a Lucene PR -- hopefully committed soon
|
||||
public static final float MAX_FLOAT32_COMPONENT = 1E17f;
|
||||
|
||||
public static void checkInBounds(float[] v)
|
||||
{
|
||||
for (int i = 0; i < v.length; i++)
|
||||
{
|
||||
if (!Float.isFinite(v[i]))
|
||||
{
|
||||
throw new IllegalArgumentException("non-finite value at vector[" + i + "]=" + v[i]);
|
||||
}
|
||||
|
||||
if (Math.abs(v[i]) > MAX_FLOAT32_COMPONENT)
|
||||
{
|
||||
throw new IllegalArgumentException("Out-of-bounds value at vector[" + i + "]=" + v[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateIndexable(float[] vector, VectorSimilarityFunction similarityFunction)
|
||||
{
|
||||
try
|
||||
{
|
||||
checkInBounds(vector);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new InvalidRequestException(e.getMessage());
|
||||
}
|
||||
|
||||
if (similarityFunction == VectorSimilarityFunction.COSINE)
|
||||
{
|
||||
for (int i = 0; i < vector.length; i++)
|
||||
{
|
||||
if (vector[i] != 0)
|
||||
return;
|
||||
}
|
||||
throw new InvalidRequestException("Zero vectors cannot be indexed or queried with cosine similarity");
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<T> keysFromOrdinal(int node)
|
||||
{
|
||||
return postingsByOrdinal.get(node).getPostings();
|
||||
}
|
||||
|
||||
public long remove(ByteBuffer term, T key)
|
||||
{
|
||||
assert term != null && term.remaining() != 0;
|
||||
|
||||
var vector = vectorType.composeAsFloat(term);
|
||||
var postings = postingsMap.get(vector);
|
||||
if (postings == null)
|
||||
{
|
||||
// it's possible for this to be called against a different memtable than the one
|
||||
// the value was originally added to, in which case we do not expect to find
|
||||
// the key among the postings for this vector
|
||||
return 0;
|
||||
}
|
||||
|
||||
hasDeletions = true;
|
||||
return postings.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return keys (PrimaryKey or segment row id) associated with the topK vectors near the query
|
||||
*/
|
||||
public PriorityQueue<T> search(float[] queryVector, int limit, Bits toAccept)
|
||||
{
|
||||
validateIndexable(queryVector, similarityFunction);
|
||||
|
||||
// search() errors out when an empty graph is passed to it
|
||||
if (vectorValues.size() == 0)
|
||||
return new PriorityQueue<>();
|
||||
|
||||
Bits bits = hasDeletions ? BitsUtil.bitsIgnoringDeleted(toAccept, postingsByOrdinal) : toAccept;
|
||||
GraphIndex<float[]> graph = builder.getGraph();
|
||||
var searcher = new GraphSearcher.Builder<>(graph.getView()).withConcurrentUpdates().build();
|
||||
NeighborSimilarity.ExactScoreFunction scoreFunction = node2 -> vectorCompareFunction(queryVector, node2);
|
||||
var result = searcher.search(scoreFunction, null, limit, bits);
|
||||
Tracing.trace("ANN search visited {} in-memory nodes to return {} results", result.getVisitedCount(), result.getNodes().length);
|
||||
var a = result.getNodes();
|
||||
PriorityQueue<T> keyQueue = new PriorityQueue<>();
|
||||
for (int i = 0; i < a.length; i++)
|
||||
keyQueue.addAll(keysFromOrdinal(a[i].node));
|
||||
return keyQueue;
|
||||
}
|
||||
|
||||
public SegmentMetadata.ComponentMetadataMap writeData(IndexDescriptor indexDescriptor, IndexContext indexContext, Function<T, Integer> postingTransformer) throws IOException
|
||||
{
|
||||
int nInProgress = builder.insertsInProgress();
|
||||
assert nInProgress == 0 : String.format("Attempting to write graph while %d inserts are in progress", nInProgress);
|
||||
assert nextOrdinal.get() == builder.getGraph().size() : String.format("nextOrdinal %d != graph size %d -- ordinals should be sequential",
|
||||
nextOrdinal.get(), builder.getGraph().size());
|
||||
assert vectorValues.size() == builder.getGraph().size() : String.format("vector count %d != graph size %d",
|
||||
vectorValues.size(), builder.getGraph().size());
|
||||
assert postingsMap.keySet().size() == vectorValues.size() : String.format("postings map entry count %d != vector count %d",
|
||||
postingsMap.keySet().size(), vectorValues.size());
|
||||
logger.debug("Writing graph with {} rows and {} distinct vectors", postingsMap.values().stream().mapToInt(VectorPostings::size).sum(), vectorValues.size());
|
||||
|
||||
try (var pqOutput = IndexFileUtils.instance.openOutput(indexDescriptor.fileFor(IndexComponent.COMPRESSED_VECTORS, indexContext), true);
|
||||
var postingsOutput = IndexFileUtils.instance.openOutput(indexDescriptor.fileFor(IndexComponent.POSTING_LISTS, indexContext), true);
|
||||
var indexOutput = IndexFileUtils.instance.openOutput(indexDescriptor.fileFor(IndexComponent.TERMS_DATA, indexContext), true))
|
||||
{
|
||||
SAICodecUtils.writeHeader(pqOutput);
|
||||
SAICodecUtils.writeHeader(postingsOutput);
|
||||
SAICodecUtils.writeHeader(indexOutput);
|
||||
|
||||
// compute and write PQ
|
||||
long pqOffset = pqOutput.getFilePointer();
|
||||
long pqPosition = writePQ(pqOutput.asSequentialWriter());
|
||||
long pqLength = pqPosition - pqOffset;
|
||||
|
||||
var deletedOrdinals = new HashSet<Integer>();
|
||||
postingsMap.values().stream().filter(VectorPostings::isEmpty).forEach(vectorPostings -> deletedOrdinals.add(vectorPostings.getOrdinal()));
|
||||
// remove ordinals that don't have corresponding row ids due to partition/range deletion
|
||||
for (VectorPostings<T> vectorPostings : postingsMap.values())
|
||||
{
|
||||
vectorPostings.computeRowIds(postingTransformer);
|
||||
if (vectorPostings.shouldAppendDeletedOrdinal())
|
||||
deletedOrdinals.add(vectorPostings.getOrdinal());
|
||||
}
|
||||
// write postings
|
||||
long postingsOffset = postingsOutput.getFilePointer();
|
||||
long postingsPosition = new VectorPostingsWriter<T>().writePostings(postingsOutput.asSequentialWriter(), vectorValues, postingsMap, deletedOrdinals);
|
||||
long postingsLength = postingsPosition - postingsOffset;
|
||||
|
||||
// complete (internal clean up) and write the graph
|
||||
builder.complete();
|
||||
long termsOffset = indexOutput.getFilePointer();
|
||||
OnDiskGraphIndex.write(builder.getGraph(), vectorValues, indexOutput.asSequentialWriter());
|
||||
long termsLength = indexOutput.getFilePointer() - termsOffset;
|
||||
|
||||
// write footers/checksums
|
||||
SAICodecUtils.writeFooter(pqOutput);
|
||||
SAICodecUtils.writeFooter(postingsOutput);
|
||||
SAICodecUtils.writeFooter(indexOutput);
|
||||
|
||||
// add components to the metadata map
|
||||
SegmentMetadata.ComponentMetadataMap metadataMap = new SegmentMetadata.ComponentMetadataMap();
|
||||
metadataMap.put(IndexComponent.TERMS_DATA, -1, termsOffset, termsLength, Map.of());
|
||||
metadataMap.put(IndexComponent.POSTING_LISTS, -1, postingsOffset, postingsLength, Map.of());
|
||||
Map<String, String> vectorConfigs = Map.of("SEGMENT_ID", ByteBufferUtil.bytesToHex(ByteBuffer.wrap(StringHelper.randomId())));
|
||||
metadataMap.put(IndexComponent.COMPRESSED_VECTORS, -1, pqOffset, pqLength, vectorConfigs);
|
||||
return metadataMap;
|
||||
}
|
||||
}
|
||||
|
||||
private float vectorCompareFunction(float[] queryVector, int node)
|
||||
{
|
||||
return similarityFunction.compare(queryVector, ((RandomAccessVectorValues<float[]>) vectorValues).vectorValue(node));
|
||||
}
|
||||
|
||||
private long writePQ(SequentialWriter writer) throws IOException
|
||||
{
|
||||
// don't bother with PQ if there are fewer than 1K vectors
|
||||
int M = vectorValues.dimension() / 2;
|
||||
writer.writeBoolean(vectorValues.size() >= 1024);
|
||||
if (vectorValues.size() < 1024)
|
||||
{
|
||||
logger.debug("Skipping PQ for only {} vectors", vectorValues.size());
|
||||
return writer.position();
|
||||
}
|
||||
|
||||
logger.debug("Computing PQ for {} vectors", vectorValues.size());
|
||||
// limit the PQ computation and encoding to one index at a time -- goal during flush is to
|
||||
// evict from memory ASAP so better to do the PQ build (in parallel) one at a time
|
||||
ProductQuantization pq;
|
||||
byte[][] encoded;
|
||||
synchronized (OnHeapGraph.class)
|
||||
{
|
||||
// train PQ and encode
|
||||
pq = ProductQuantization.compute(vectorValues, M, false);
|
||||
assert !vectorValues.isValueShared();
|
||||
encoded = IntStream.range(0, vectorValues.size())
|
||||
.parallel()
|
||||
.mapToObj(i -> pq.encode(vectorValues.vectorValue(i)))
|
||||
.toArray(byte[][]::new);
|
||||
}
|
||||
var cv = new CompressedVectors(pq, encoded);
|
||||
// save
|
||||
cv.write(writer);
|
||||
return writer.position();
|
||||
}
|
||||
|
||||
public enum InvalidVectorBehavior
|
||||
{
|
||||
IGNORE,
|
||||
FAIL
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.lang.Math.pow;
|
||||
|
||||
/**
|
||||
* Allows the vector index searches to be optimised for latency or recall. This is used by the
|
||||
* {@link org.apache.cassandra.index.sai.disk.v1.segment.VectorIndexSegmentSearcher} to determine how many results to ask the graph
|
||||
* to search for. If we are optimising for {@link #RECALL} we ask for more than the requested limit which
|
||||
* (since it will search deeper in the graph) will tend to surface slightly better results.
|
||||
*/
|
||||
public enum OptimizeFor
|
||||
{
|
||||
LATENCY(limit -> 0.979 + 4.021 * pow(limit, -0.761)), // f(1) = 5.0, f(100) = 1.1, f(1000) = 1.0
|
||||
RECALL(limit -> 0.509 + 9.491 * pow(limit, -0.402)); // f(1) = 10.0, f(100) = 2.0, f(1000) = 1.1
|
||||
|
||||
private final Function<Integer, Double> limitMultiplier;
|
||||
|
||||
OptimizeFor(Function<Integer, Double> limitMultiplier)
|
||||
{
|
||||
this.limitMultiplier = limitMultiplier;
|
||||
}
|
||||
|
||||
public int topKFor(int limit)
|
||||
{
|
||||
return (int)(limitMultiplier.apply(limit) * limit);
|
||||
}
|
||||
|
||||
public static OptimizeFor fromString(String value)
|
||||
{
|
||||
return valueOf(value.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import io.github.jbellis.jvector.graph.RandomAccessVectorValues;
|
||||
|
||||
public interface RamAwareVectorValues extends RandomAccessVectorValues<float[]>
|
||||
{
|
||||
float[] vectorValue(int i);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import org.apache.lucene.util.RamUsageEstimator;
|
||||
|
||||
public class RamEstimation
|
||||
{
|
||||
/**
|
||||
* @param externalNodeCount the size() of the ConcurrentHashMap
|
||||
* @return an estimate of the number of bytes used
|
||||
*/
|
||||
public static long concurrentHashMapRamUsed(int externalNodeCount) {
|
||||
long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF;
|
||||
long AH_BYTES = RamUsageEstimator.NUM_BYTES_ARRAY_HEADER;
|
||||
long CORES = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
long chmNodeBytes =
|
||||
REF_BYTES // node itself in Node[]
|
||||
+ 3L * REF_BYTES
|
||||
+ Integer.BYTES; // node internals
|
||||
float chmLoadFactor = 0.75f; // this is hardcoded inside ConcurrentHashMap
|
||||
// CHM has a striped counter Cell implementation, we expect at most one per core
|
||||
long chmCounters = AH_BYTES + CORES * (REF_BYTES + Long.BYTES);
|
||||
|
||||
double nodeCount = externalNodeCount / chmLoadFactor;
|
||||
|
||||
return
|
||||
(long) nodeCount * (chmNodeBytes + REF_BYTES)// nodes
|
||||
+ AH_BYTES // nodes array
|
||||
+ Long.BYTES
|
||||
+ 3 * Integer.BYTES
|
||||
+ 3 * REF_BYTES // extra internal fields
|
||||
+ chmCounters
|
||||
+ REF_BYTES; // the Map reference itself
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
import io.github.jbellis.jvector.disk.ReaderSupplier;
|
||||
import org.apache.cassandra.io.util.FileHandle;
|
||||
import org.apache.cassandra.io.util.RandomAccessReader;
|
||||
|
||||
public class RandomAccessReaderAdapter extends RandomAccessReader implements io.github.jbellis.jvector.disk.RandomAccessReader
|
||||
{
|
||||
static ReaderSupplier createSupplier(FileHandle fileHandle)
|
||||
{
|
||||
return () -> new RandomAccessReaderAdapter(fileHandle);
|
||||
}
|
||||
|
||||
RandomAccessReaderAdapter(FileHandle fileHandle)
|
||||
{
|
||||
super(fileHandle.instantiateRebufferer(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFully(float[] dest) throws IOException
|
||||
{
|
||||
var bh = bufferHolder;
|
||||
long position = getPosition();
|
||||
|
||||
FloatBuffer floatBuffer;
|
||||
if (bh.offset() == 0 && position % Float.BYTES == 0)
|
||||
{
|
||||
// this is a separate code path because buffer() and asFloatBuffer() both allocate
|
||||
// new and relatively expensive xBuffer objects, so we want to avoid doing that
|
||||
// twice, where possible
|
||||
floatBuffer = bh.floatBuffer();
|
||||
floatBuffer.position(Ints.checkedCast(position / Float.BYTES));
|
||||
}
|
||||
else
|
||||
{
|
||||
// offset is non-zero, and probably not aligned to Float.BYTES, so
|
||||
// set the position before converting to FloatBuffer.
|
||||
var bb = bh.buffer();
|
||||
bb.position(Ints.checkedCast(position - bh.offset()));
|
||||
floatBuffer = bb.asFloatBuffer();
|
||||
}
|
||||
|
||||
if (dest.length > floatBuffer.remaining())
|
||||
{
|
||||
// slow path -- desired slice is across region boundaries
|
||||
var bb = ByteBuffer.allocate(Float.BYTES * dest.length);
|
||||
readFully(bb);
|
||||
floatBuffer = bb.asFloatBuffer();
|
||||
}
|
||||
|
||||
floatBuffer.get(dest);
|
||||
seek(position + (long) Float.BYTES * dest.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read ints into an int[], starting at the current position.
|
||||
*
|
||||
* @param dest the array to read into
|
||||
* @param offset the offset in the array at which to start writing ints
|
||||
* @param count the number of ints to read
|
||||
*
|
||||
* Will change the buffer position.
|
||||
*/
|
||||
@Override
|
||||
public void read(int[] dest, int offset, int count) throws IOException
|
||||
{
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
var bh = bufferHolder;
|
||||
long position = getPosition();
|
||||
|
||||
IntBuffer intBuffer;
|
||||
if (bh.offset() == 0 && position % Integer.BYTES == 0)
|
||||
{
|
||||
// this is a separate code path because buffer() and asIntBuffer() both allocate
|
||||
// new and relatively expensive xBuffer objects, so we want to avoid doing that
|
||||
// twice, where possible
|
||||
intBuffer = bh.intBuffer();
|
||||
intBuffer.position(Ints.checkedCast(position / Integer.BYTES));
|
||||
}
|
||||
else
|
||||
{
|
||||
// offset is non-zero, and probably not aligned to Integer.BYTES, so
|
||||
// set the position before converting to IntBuffer.
|
||||
var bb = bh.buffer();
|
||||
bb.position(Ints.checkedCast(position - bh.offset()));
|
||||
intBuffer = bb.asIntBuffer();
|
||||
}
|
||||
|
||||
if (count > intBuffer.remaining())
|
||||
{
|
||||
// slow path -- desired slice is across region boundaries
|
||||
var bb = ByteBuffer.allocate(Integer.BYTES * count);
|
||||
readFully(bb);
|
||||
intBuffer = bb.asIntBuffer();
|
||||
}
|
||||
|
||||
intBuffer.get(dest, offset, count);
|
||||
seek(position + (long) Integer.BYTES * count);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.index.sai.disk.v1.vector;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import org.agrona.collections.IntArrayList;
|
||||
import org.apache.lucene.util.RamUsageEstimator;
|
||||
|
||||
public class VectorPostings<T>
|
||||
{
|
||||
private final List<T> postings;
|
||||
private volatile int ordinal = -1;
|
||||
|
||||
private volatile IntArrayList rowIds;
|
||||
|
||||
public VectorPostings(T firstKey)
|
||||
{
|
||||
// we expect that the overwhelmingly most common cardinality will be 1, so optimize for reads
|
||||
postings = new CopyOnWriteArrayList<>(List.of(firstKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split out from constructor only to make dealing with concurrent inserts easier for CassandraOnHeapGraph.
|
||||
* Should be called at most once per instance.
|
||||
*/
|
||||
void setOrdinal(int ordinal)
|
||||
{
|
||||
assert this.ordinal == -1 : String.format("ordinal already set to %d; attempted to set to %d", this.ordinal, ordinal);
|
||||
this.ordinal = ordinal;
|
||||
}
|
||||
|
||||
public boolean add(T key)
|
||||
{
|
||||
for (T existing : postings)
|
||||
if (existing.equals(key))
|
||||
return false;
|
||||
postings.add(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if current ordinal is removed by partition/range deletion.
|
||||
* Must be called after computeRowIds.
|
||||
*/
|
||||
public boolean shouldAppendDeletedOrdinal()
|
||||
{
|
||||
return !postings.isEmpty() && (rowIds != null && rowIds.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the rowIds corresponding to the {@code <T>} keys in this postings list.
|
||||
*/
|
||||
public void computeRowIds(Function<T, Integer> postingTransformer)
|
||||
{
|
||||
Preconditions.checkState(rowIds == null);
|
||||
|
||||
IntArrayList ids = new IntArrayList(postings.size(), -1);
|
||||
for (T key : postings)
|
||||
{
|
||||
int rowId = postingTransformer.apply(key);
|
||||
// partition deletion and range deletion won't trigger index update. There is no row id for given key during flush
|
||||
if (rowId >= 0)
|
||||
ids.add(rowId);
|
||||
}
|
||||
|
||||
rowIds = ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return rowIds corresponding to the {@code <T>} keys in this postings list.
|
||||
* Must be called after computeRowIds.
|
||||
*/
|
||||
public IntArrayList getRowIds()
|
||||
{
|
||||
Preconditions.checkNotNull(rowIds);
|
||||
return rowIds;
|
||||
}
|
||||
|
||||
public long remove(T key)
|
||||
{
|
||||
long bytesUsed = ramBytesUsed();
|
||||
postings.remove(key);
|
||||
return bytesUsed - ramBytesUsed();
|
||||
}
|
||||
|
||||
public long ramBytesUsed()
|
||||
{
|
||||
return emptyBytesUsed() + postings.size() * bytesPerPosting();
|
||||
}
|
||||
|
||||
public static long emptyBytesUsed()
|
||||
{
|
||||
long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF;
|
||||
long AH_BYTES = RamUsageEstimator.NUM_BYTES_ARRAY_HEADER;
|
||||
return Integer.BYTES + REF_BYTES + AH_BYTES;
|
||||
}
|
||||
|
||||
// we can't do this exactly without reflection, because keys could be Long or PrimaryKey.
|
||||
// PK is larger, so we'll take that and return an upper bound.
|
||||
// we already count the float[] vector in vectorValues, so leave it out here
|
||||
public static long bytesPerPosting()
|
||||
{
|
||||
long REF_BYTES = RamUsageEstimator.NUM_BYTES_OBJECT_REF;
|
||||
return REF_BYTES
|
||||
+ 2 * Long.BYTES // hashes in PreHashedDecoratedKey
|
||||
+ REF_BYTES; // key ByteBuffer, this is used elsewhere, so we don't take the deep size
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
return postings.size();
|
||||
}
|
||||
|
||||
public List<T> getPostings()
|
||||
{
|
||||
return postings;
|
||||
}
|
||||
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return postings.isEmpty();
|
||||
}
|
||||
|
||||
public int getOrdinal()
|
||||
{
|
||||
assert ordinal >= 0 : "ordinal not set";
|
||||
return ordinal;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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.disk.v1.vector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.cassandra.io.util.SequentialWriter;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
|
||||
public class VectorPostingsWriter<T>
|
||||
{
|
||||
public long writePostings(SequentialWriter writer,
|
||||
RamAwareVectorValues vectorValues,
|
||||
Map<float[], VectorPostings<T>> postingsMap,
|
||||
Set<Integer> deletedOrdinals) throws IOException
|
||||
{
|
||||
writeDeletedOrdinals(writer, deletedOrdinals);
|
||||
writeNodeOrdinalToRowIdMapping(writer, vectorValues, postingsMap);
|
||||
writeRowIdToNodeOrdinalMapping(writer, vectorValues, postingsMap);
|
||||
|
||||
return writer.position();
|
||||
}
|
||||
|
||||
private void writeDeletedOrdinals(SequentialWriter writer, Set<Integer> deletedOrdinals) throws IOException
|
||||
{
|
||||
writer.writeInt(deletedOrdinals.size());
|
||||
for (var ordinal : deletedOrdinals) {
|
||||
writer.writeInt(ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeNodeOrdinalToRowIdMapping(SequentialWriter writer,
|
||||
RamAwareVectorValues vectorValues,
|
||||
Map<float[], VectorPostings<T>> postingsMap) throws IOException
|
||||
{
|
||||
long ordToRowOffset = writer.getOnDiskFilePointer();
|
||||
|
||||
// total number of vectors
|
||||
writer.writeInt(vectorValues.size());
|
||||
|
||||
// Write the offsets of the postings for each ordinal
|
||||
var offsetsStartAt = ordToRowOffset + 4L + 8L * vectorValues.size();
|
||||
var nextOffset = offsetsStartAt;
|
||||
for (var i = 0; i < vectorValues.size(); i++) {
|
||||
// (ordinal is implied; don't need to write it)
|
||||
writer.writeLong(nextOffset);
|
||||
var rowIds = postingsMap.get(vectorValues.vectorValue(i)).getRowIds();
|
||||
nextOffset += 4 + (rowIds.size() * 4L); // 4 bytes for size and 4 bytes for each integer in the list
|
||||
}
|
||||
assert writer.position() == offsetsStartAt : "writer.position()=" + writer.position() + " offsetsStartAt=" + offsetsStartAt;
|
||||
|
||||
// Write postings lists
|
||||
for (var i = 0; i < vectorValues.size(); i++) {
|
||||
VectorPostings<T> postings = postingsMap.get(vectorValues.vectorValue(i));
|
||||
|
||||
var rowIds = postings.getRowIds();
|
||||
writer.writeInt(rowIds.size());
|
||||
for (int r = 0; r < rowIds.size(); r++)
|
||||
writer.writeInt(rowIds.getInt(r));
|
||||
}
|
||||
assert writer.position() == nextOffset;
|
||||
}
|
||||
|
||||
public void writeRowIdToNodeOrdinalMapping(SequentialWriter writer,
|
||||
RamAwareVectorValues vectorValues,
|
||||
Map<float[], VectorPostings<T>> postingsMap) throws IOException
|
||||
{
|
||||
List<Pair<Integer, Integer>> pairs = new ArrayList<>();
|
||||
|
||||
// Collect all (rowId, vectorOrdinal) pairs
|
||||
for (var i = 0; i < vectorValues.size(); i++) {
|
||||
var rowIds = postingsMap.get(vectorValues.vectorValue(i)).getRowIds();
|
||||
for (int r = 0; r < rowIds.size(); r++)
|
||||
pairs.add(Pair.create(rowIds.getInt(r), i));
|
||||
}
|
||||
|
||||
// Sort the pairs by rowId
|
||||
pairs.sort(Comparator.comparingInt(Pair::left));
|
||||
|
||||
// Write the pairs to the file
|
||||
long startOffset = writer.position();
|
||||
for (var pair : pairs) {
|
||||
writer.writeInt(pair.left);
|
||||
writer.writeInt(pair.right);
|
||||
}
|
||||
|
||||
// write the position of the beginning of rowid -> ordinals mappings to the end
|
||||
writer.writeLong(startOffset);
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,13 @@ public abstract class KeyRangeIterator extends AbstractGuavaIterator<PrimaryKey>
|
|||
this(statistics.min, statistics.max, statistics.count);
|
||||
}
|
||||
|
||||
public KeyRangeIterator(KeyRangeIterator range)
|
||||
{
|
||||
this(range == null ? null : range.min,
|
||||
range == null ? null : range.max,
|
||||
range == null ? -1 : range.count);
|
||||
}
|
||||
|
||||
public KeyRangeIterator(PrimaryKey min, PrimaryKey max, long count)
|
||||
{
|
||||
boolean isComplete = min != null && max != null && count != 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.iterators;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Iterators;
|
||||
import com.google.common.collect.PeekingIterator;
|
||||
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
|
||||
/**
|
||||
* A {@link KeyRangeIterator} that iterates over a list of {@link PrimaryKey}s without modifying the underlying list.
|
||||
*/
|
||||
public class KeyRangeListIterator extends KeyRangeIterator
|
||||
{
|
||||
private final PeekingIterator<PrimaryKey> keyQueue;
|
||||
|
||||
/**
|
||||
* Create a new {@link KeyRangeListIterator} that iterates over the provided list of keys.
|
||||
*
|
||||
* @param minimumKey the minimum key for the provided list of keys
|
||||
* @param maximumKey the maximum key for the provided list of keys
|
||||
* @param keys the list of keys to iterate over
|
||||
*/
|
||||
public KeyRangeListIterator(PrimaryKey minimumKey, PrimaryKey maximumKey, List<PrimaryKey> keys)
|
||||
{
|
||||
super(minimumKey, maximumKey, keys.size());
|
||||
this.keyQueue = Iterators.peekingIterator(keys.iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performSkipTo(PrimaryKey nextKey)
|
||||
{
|
||||
while (keyQueue.hasNext())
|
||||
{
|
||||
if (keyQueue.peek().compareTo(nextKey) >= 0)
|
||||
break;
|
||||
keyQueue.next();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
|
||||
@Override
|
||||
protected PrimaryKey computeNext()
|
||||
{
|
||||
return keyQueue.hasNext() ? keyQueue.next() : endOfData();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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.iterators;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
|
||||
/**
|
||||
* An iterator that consumes a chunk of {@link PrimaryKey}s from the {@link KeyRangeIterator}, passes them to the
|
||||
* {@link Function} to filter the chunk of {@link PrimaryKey}s and then pass the results to next consumer.
|
||||
* The PKs are currently returned in {@link PrimaryKey} order, but that contract may change.
|
||||
*/
|
||||
@NotThreadSafe
|
||||
public class KeyRangeOrderingIterator extends KeyRangeIterator
|
||||
{
|
||||
private final KeyRangeIterator input;
|
||||
private final int chunkSize;
|
||||
private final Function<List<PrimaryKey>, KeyRangeIterator> nextRangeFunction;
|
||||
private final ArrayList<PrimaryKey> nextKeys;
|
||||
private KeyRangeIterator nextIterator;
|
||||
|
||||
public KeyRangeOrderingIterator(KeyRangeIterator input, int chunkSize, Function<List<PrimaryKey>, KeyRangeIterator> nextRangeFunction)
|
||||
{
|
||||
super(input);
|
||||
this.input = input;
|
||||
this.chunkSize = chunkSize;
|
||||
this.nextRangeFunction = nextRangeFunction;
|
||||
this.nextKeys = new ArrayList<>(chunkSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrimaryKey computeNext()
|
||||
{
|
||||
if (nextIterator == null || !nextIterator.hasNext())
|
||||
{
|
||||
do
|
||||
{
|
||||
if (!input.hasNext())
|
||||
return endOfData();
|
||||
nextKeys.clear();
|
||||
do
|
||||
{
|
||||
nextKeys.add(input.next());
|
||||
}
|
||||
while (nextKeys.size() < chunkSize && input.hasNext());
|
||||
// Get the next iterator before closing this one to prevent releasing the resource.
|
||||
var previousIterator = nextIterator;
|
||||
// If this results in an exception, previousIterator is closed in close() method.
|
||||
nextIterator = nextRangeFunction.apply(nextKeys);
|
||||
if (previousIterator != null)
|
||||
FileUtils.closeQuietly(previousIterator);
|
||||
// nextIterator might not have any rows due to shadowed primary keys
|
||||
}
|
||||
while (!nextIterator.hasNext());
|
||||
}
|
||||
return nextIterator.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performSkipTo(PrimaryKey nextToken)
|
||||
{
|
||||
input.skipTo(nextToken);
|
||||
nextIterator.skipTo(nextToken);
|
||||
}
|
||||
|
||||
public void close()
|
||||
{
|
||||
FileUtils.closeQuietly(input);
|
||||
FileUtils.closeQuietly(nextIterator);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.apache.cassandra.db.Clustering;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.db.PartitionPosition;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
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.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.Pair;
|
||||
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class MemoryIndex implements MemtableOrdering
|
||||
{
|
||||
protected final IndexContext indexContext;
|
||||
|
||||
protected MemoryIndex(IndexContext indexContext)
|
||||
{
|
||||
this.indexContext = indexContext;
|
||||
}
|
||||
|
||||
public abstract long add(DecoratedKey key, Clustering<?> clustering, ByteBuffer value);
|
||||
|
||||
public abstract long update(DecoratedKey key, Clustering<?> clustering, ByteBuffer oldValue, ByteBuffer newValue);
|
||||
|
||||
public abstract KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds<PartitionPosition> keyRange);
|
||||
|
||||
public abstract boolean isEmpty();
|
||||
|
||||
public abstract ByteBuffer getMinTerm();
|
||||
|
||||
public abstract ByteBuffer getMaxTerm();
|
||||
|
||||
/**
|
||||
* Iterate all Term->PrimaryKeys mappings in sorted order
|
||||
*/
|
||||
public abstract Iterator<Pair<ByteComparable, PrimaryKeys>> iterator();
|
||||
|
||||
public abstract SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor,
|
||||
IndexContext indexContext,
|
||||
Function<PrimaryKey, Integer> postingTransformer) throws IOException;
|
||||
}
|
||||
|
|
@ -18,30 +18,37 @@
|
|||
|
||||
package org.apache.cassandra.index.sai.memory;
|
||||
|
||||
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;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.db.PartitionPosition;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
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.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKeys;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||
|
||||
public class MemtableIndex
|
||||
public class MemtableIndex implements MemtableOrdering
|
||||
{
|
||||
private final TrieMemoryIndex index;
|
||||
private final MemoryIndex index;
|
||||
private final LongAdder writeCount = new LongAdder();
|
||||
private final LongAdder estimatedMemoryUsed = new LongAdder();
|
||||
|
||||
public MemtableIndex(IndexContext indexContext)
|
||||
{
|
||||
this.index = new TrieMemoryIndex(indexContext);
|
||||
this.index = indexContext.isVector() ? new VectorMemoryIndex(indexContext) : new TrieMemoryIndex(indexContext);
|
||||
}
|
||||
|
||||
public long writeCount()
|
||||
|
|
@ -56,7 +63,7 @@ public class MemtableIndex
|
|||
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return getMinTerm() == null;
|
||||
return index.isEmpty();
|
||||
}
|
||||
|
||||
public ByteBuffer getMinTerm()
|
||||
|
|
@ -80,13 +87,31 @@ public class MemtableIndex
|
|||
return ram;
|
||||
}
|
||||
|
||||
public KeyRangeIterator search(Expression expression, AbstractBounds<PartitionPosition> keyRange)
|
||||
public long update(DecoratedKey key, Clustering<?> clustering, ByteBuffer oldValue, ByteBuffer newValue)
|
||||
{
|
||||
return index.search(expression, keyRange);
|
||||
return index.update(key, clustering, oldValue, newValue);
|
||||
}
|
||||
|
||||
public KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds<PartitionPosition> keyRange)
|
||||
{
|
||||
return index.search(queryContext, expression, keyRange);
|
||||
}
|
||||
|
||||
public Iterator<Pair<ByteComparable, PrimaryKeys>> iterator()
|
||||
{
|
||||
return index.iterator();
|
||||
}
|
||||
|
||||
public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor,
|
||||
IndexContext indexContext,
|
||||
Function<PrimaryKey, Integer> postingTransformer) throws IOException
|
||||
{
|
||||
return index.writeDirect(indexDescriptor, indexContext, postingTransformer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyRangeIterator limitToTopResults(List<PrimaryKey> primaryKeys, Expression expression, int limit)
|
||||
{
|
||||
return index.limitToTopResults(primaryKeys, expression, limit);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ package org.apache.cassandra.index.sai.memory;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -36,9 +37,11 @@ import org.apache.cassandra.db.memtable.Memtable;
|
|||
import org.apache.cassandra.db.rows.Row;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeUnionIterator;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.utils.Clock;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
|
|
@ -88,6 +91,22 @@ public class MemtableIndexManager
|
|||
return bytes;
|
||||
}
|
||||
|
||||
public long update(DecoratedKey key, Row oldRow, Row newRow, Memtable memtable)
|
||||
{
|
||||
if (!indexContext.isVector())
|
||||
{
|
||||
return index(key, newRow, memtable);
|
||||
}
|
||||
|
||||
MemtableIndex target = liveMemtableIndexMap.get(memtable);
|
||||
if (target == null)
|
||||
return 0;
|
||||
|
||||
ByteBuffer oldValue = indexContext.getValueOf(key, oldRow, FBUtilities.nowInSeconds());
|
||||
ByteBuffer newValue = indexContext.getValueOf(key, newRow, FBUtilities.nowInSeconds());
|
||||
return target.update(key, oldRow.clustering(), oldValue, newValue);
|
||||
}
|
||||
|
||||
public void renewMemtable(Memtable renewed)
|
||||
{
|
||||
for (Memtable memtable : liveMemtableIndexMap.keySet())
|
||||
|
|
@ -115,7 +134,7 @@ public class MemtableIndexManager
|
|||
.orElse(null);
|
||||
}
|
||||
|
||||
public KeyRangeIterator searchMemtableIndexes(Expression e, AbstractBounds<PartitionPosition> keyRange)
|
||||
public KeyRangeIterator searchMemtableIndexes(QueryContext queryContext, Expression e, AbstractBounds<PartitionPosition> keyRange)
|
||||
{
|
||||
Collection<MemtableIndex> memtableIndexes = liveMemtableIndexMap.values();
|
||||
|
||||
|
|
@ -128,7 +147,26 @@ public class MemtableIndexManager
|
|||
|
||||
for (MemtableIndex memtableIndex : memtableIndexes)
|
||||
{
|
||||
builder.add(memtableIndex.search(e, keyRange));
|
||||
builder.add(memtableIndex.search(queryContext, e, keyRange));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public KeyRangeIterator limitToTopResults(QueryContext context, List<PrimaryKey> source, Expression e)
|
||||
{
|
||||
Collection<MemtableIndex> memtables = liveMemtableIndexMap.values();
|
||||
|
||||
if (memtables.isEmpty())
|
||||
{
|
||||
return KeyRangeIterator.empty();
|
||||
}
|
||||
|
||||
KeyRangeUnionIterator.Builder builder = KeyRangeUnionIterator.builder(memtables.size());
|
||||
|
||||
for (MemtableIndex index : memtables)
|
||||
{
|
||||
builder.add(index.limitToTopResults(source, e, context.vectorContext().limit()));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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.util.List;
|
||||
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
|
||||
/**
|
||||
* Analogue of {@link org.apache.cassandra.index.sai.disk.v1.segment.SegmentOrdering}, but for memtables.
|
||||
*/
|
||||
public interface MemtableOrdering
|
||||
{
|
||||
/**
|
||||
* Filter the given list of {@code PrimaryKey} results to the top `limit` results corresponding to the given expression,
|
||||
* Returns an iterator over the results that is put back in token order.
|
||||
* <p>
|
||||
* Assumes that the given list spans the same rows as the implementing index's segment.
|
||||
*/
|
||||
default KeyRangeIterator limitToTopResults(List<PrimaryKey> primaryKeys, Expression expression, int limit)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,9 @@ import java.util.Map;
|
|||
import java.util.PriorityQueue;
|
||||
import java.util.SortedSet;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -39,6 +41,8 @@ import org.apache.cassandra.db.tries.Trie;
|
|||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.analyzer.AbstractAnalyzer;
|
||||
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.plan.Expression;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKey;
|
||||
import org.apache.cassandra.index.sai.utils.PrimaryKeys;
|
||||
|
|
@ -53,15 +57,12 @@ import org.apache.cassandra.utils.bytecomparable.ByteSourceInverse;
|
|||
* This is an in-memory index using the {@link InMemoryTrie} to store a {@link ByteComparable}
|
||||
* representation of the indexed values. Data is stored on-heap or off-heap and follows the
|
||||
* settings of the {@link TrieMemtable} to determine where.
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class TrieMemoryIndex
|
||||
public class TrieMemoryIndex extends MemoryIndex
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(TrieMemoryIndex.class);
|
||||
private static final int MAX_RECURSIVE_KEY_LENGTH = 128;
|
||||
|
||||
private final IndexContext indexContext;
|
||||
private final InMemoryTrie<PrimaryKeys> data;
|
||||
private final PrimaryKeysReducer primaryKeysReducer;
|
||||
private final AbstractAnalyzer.AnalyzerFactory analyzerFactory;
|
||||
|
|
@ -73,7 +74,7 @@ public class TrieMemoryIndex
|
|||
|
||||
public TrieMemoryIndex(IndexContext indexContext)
|
||||
{
|
||||
this.indexContext = indexContext;
|
||||
super(indexContext);
|
||||
this.data = new InMemoryTrie<>(TrieMemtable.BUFFER_TYPE);
|
||||
this.primaryKeysReducer = new PrimaryKeysReducer();
|
||||
// The use of the analyzer is within a synchronized block so can be considered thread-safe
|
||||
|
|
@ -90,6 +91,7 @@ public class TrieMemoryIndex
|
|||
* @param value indexed value
|
||||
* @return amount of heap allocated by the new value
|
||||
*/
|
||||
@Override
|
||||
public synchronized long add(DecoratedKey key, Clustering<?> clustering, ByteBuffer value)
|
||||
{
|
||||
AbstractAnalyzer analyzer = analyzerFactory.create();
|
||||
|
|
@ -139,15 +141,21 @@ public class TrieMemoryIndex
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long update(DecoratedKey key, Clustering<?> clustering, ByteBuffer oldValue, ByteBuffer newValue)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an expression in the in-memory index within the {@link AbstractBounds} defined
|
||||
* by keyRange. This can either be an exact match or a range match.
|
||||
|
||||
* <p>
|
||||
* @param expression the {@link Expression} to search for
|
||||
* @param keyRange the {@link AbstractBounds} containing the key range to restrict the search to
|
||||
* @return a {@link KeyRangeIterator} containing the search results
|
||||
*/
|
||||
public KeyRangeIterator search(Expression expression, AbstractBounds<PartitionPosition> keyRange)
|
||||
public KeyRangeIterator search(QueryContext queryContext, Expression expression, AbstractBounds<PartitionPosition> keyRange)
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
logger.trace("Searching memtable index on expression '{}'...", expression);
|
||||
|
|
@ -171,10 +179,11 @@ public class TrieMemoryIndex
|
|||
*
|
||||
* @return the iterator containing the trie data
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Pair<ByteComparable, PrimaryKeys>> iterator()
|
||||
{
|
||||
Iterator<Map.Entry<ByteComparable, PrimaryKeys>> iterator = data.entrySet().iterator();
|
||||
return new Iterator<Pair<ByteComparable, PrimaryKeys>>()
|
||||
return new Iterator<>()
|
||||
{
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
|
|
@ -191,11 +200,27 @@ public class TrieMemoryIndex
|
|||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor,
|
||||
IndexContext indexContext,
|
||||
Function<PrimaryKey, Integer> postingTransformer)
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return minTerm == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getMinTerm()
|
||||
{
|
||||
return minTerm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getMaxTerm()
|
||||
{
|
||||
return maxTerm;
|
||||
|
|
@ -259,7 +284,7 @@ public class TrieMemoryIndex
|
|||
// This allows for receiving a stream of wide range queries where the queue size
|
||||
// is larger than we would want to default the size to.
|
||||
// TODO Investigate using a decaying histogram here to avoid the effect of outliers.
|
||||
private static final FastThreadLocal<Integer> lastQueueSize = new FastThreadLocal<Integer>()
|
||||
private static final FastThreadLocal<Integer> lastQueueSize = new FastThreadLocal<>()
|
||||
{
|
||||
protected Integer initialValue()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
* 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.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import io.github.jbellis.jvector.util.Bits;
|
||||
import org.apache.cassandra.db.Clustering;
|
||||
import org.apache.cassandra.db.DecoratedKey;
|
||||
import org.apache.cassandra.db.PartitionPosition;
|
||||
import org.apache.cassandra.dht.AbstractBounds;
|
||||
import org.apache.cassandra.index.sai.IndexContext;
|
||||
import org.apache.cassandra.index.sai.QueryContext;
|
||||
import org.apache.cassandra.index.sai.VectorQueryContext;
|
||||
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.OnHeapGraph;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeIterator;
|
||||
import org.apache.cassandra.index.sai.iterators.KeyRangeListIterator;
|
||||
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.index.sai.utils.RangeUtil;
|
||||
import org.apache.cassandra.index.sai.utils.TypeUtil;
|
||||
import org.apache.cassandra.tracing.Tracing;
|
||||
import org.apache.cassandra.utils.Pair;
|
||||
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
|
||||
|
||||
import static java.lang.Math.log;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
import static java.lang.Math.pow;
|
||||
|
||||
public class VectorMemoryIndex extends MemoryIndex
|
||||
{
|
||||
private final OnHeapGraph<PrimaryKey> graph;
|
||||
private final LongAdder writeCount = new LongAdder();
|
||||
|
||||
private PrimaryKey minimumKey;
|
||||
private PrimaryKey maximumKey;
|
||||
|
||||
private final NavigableSet<PrimaryKey> primaryKeys = new ConcurrentSkipListSet<>();
|
||||
|
||||
public VectorMemoryIndex(IndexContext indexContext)
|
||||
{
|
||||
super(indexContext);
|
||||
this.graph = new OnHeapGraph<>(indexContext.getValidator(), indexContext.getIndexWriterConfig());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized long add(DecoratedKey key, Clustering<?> clustering, ByteBuffer value)
|
||||
{
|
||||
if (value == null || value.remaining() == 0)
|
||||
return 0;
|
||||
|
||||
var primaryKey = indexContext.hasClustering() ? indexContext.keyFactory().create(key, clustering)
|
||||
: indexContext.keyFactory().create(key);
|
||||
return index(primaryKey, value);
|
||||
}
|
||||
|
||||
private long index(PrimaryKey primaryKey, ByteBuffer value)
|
||||
{
|
||||
updateKeyBounds(primaryKey);
|
||||
|
||||
writeCount.increment();
|
||||
primaryKeys.add(primaryKey);
|
||||
return graph.add(value, primaryKey, OnHeapGraph.InvalidVectorBehavior.FAIL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long update(DecoratedKey key, Clustering<?> clustering, ByteBuffer oldValue, ByteBuffer newValue)
|
||||
{
|
||||
int oldRemaining = oldValue == null ? 0 : oldValue.remaining();
|
||||
int newRemaining = newValue == null ? 0 : newValue.remaining();
|
||||
if (oldRemaining == 0 && newRemaining == 0)
|
||||
return 0;
|
||||
|
||||
boolean different;
|
||||
if (oldRemaining != newRemaining)
|
||||
{
|
||||
assert oldRemaining == 0 || newRemaining == 0; // one of them is null
|
||||
different = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
different = IntStream.range(0, oldRemaining).anyMatch(i -> oldValue.get(i) != newValue.get(i));
|
||||
}
|
||||
|
||||
long bytesUsed = 0;
|
||||
if (different)
|
||||
{
|
||||
var primaryKey = indexContext.hasClustering() ? indexContext.keyFactory().create(key, clustering)
|
||||
: indexContext.keyFactory().create(key);
|
||||
// update bounds because only rows with vectors are included in the key bounds,
|
||||
// so if the vector was null before, we won't have included it
|
||||
updateKeyBounds(primaryKey);
|
||||
|
||||
// make the changes in this order, so we don't have a window where the row is not in the index at all
|
||||
if (newRemaining > 0)
|
||||
bytesUsed += graph.add(newValue, primaryKey, OnHeapGraph.InvalidVectorBehavior.FAIL);
|
||||
if (oldRemaining > 0)
|
||||
bytesUsed -= graph.remove(oldValue, primaryKey);
|
||||
|
||||
// remove primary key if it's no longer indexed
|
||||
if (newRemaining <= 0 && oldRemaining > 0)
|
||||
primaryKeys.remove(primaryKey);
|
||||
}
|
||||
return bytesUsed;
|
||||
}
|
||||
|
||||
private void updateKeyBounds(PrimaryKey primaryKey)
|
||||
{
|
||||
if (minimumKey == null)
|
||||
minimumKey = primaryKey;
|
||||
else if (primaryKey.compareTo(minimumKey) < 0)
|
||||
minimumKey = primaryKey;
|
||||
if (maximumKey == null)
|
||||
maximumKey = primaryKey;
|
||||
else if (primaryKey.compareTo(maximumKey) > 0)
|
||||
maximumKey = primaryKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyRangeIterator search(QueryContext queryContext, Expression expr, AbstractBounds<PartitionPosition> keyRange)
|
||||
{
|
||||
assert expr.getOp() == Expression.IndexOperator.ANN : "Only ANN is supported for vector search, received " + expr.getOp();
|
||||
|
||||
VectorQueryContext vectorQueryContext = queryContext.vectorContext();
|
||||
|
||||
var buffer = expr.lower.value.raw;
|
||||
float[] qv = TypeUtil.decomposeVector(indexContext, buffer);
|
||||
|
||||
Bits bits;
|
||||
if (!RangeUtil.coversFullRing(keyRange))
|
||||
{
|
||||
// if left bound is MIN_BOUND or KEY_BOUND, we need to include all token-only PrimaryKeys with same token
|
||||
boolean leftInclusive = keyRange.left.kind() != PartitionPosition.Kind.MAX_BOUND;
|
||||
// if right bound is MAX_BOUND or KEY_BOUND, we need to include all token-only PrimaryKeys with same token
|
||||
boolean rightInclusive = keyRange.right.kind() != PartitionPosition.Kind.MIN_BOUND;
|
||||
// if right token is MAX (Long.MIN_VALUE), there is no upper bound
|
||||
boolean isMaxToken = keyRange.right.getToken().isMinimum(); // max token
|
||||
|
||||
PrimaryKey left = indexContext.keyFactory().create(keyRange.left.getToken()); // lower bound
|
||||
PrimaryKey right = isMaxToken ? null : indexContext.keyFactory().create(keyRange.right.getToken()); // upper bound
|
||||
|
||||
Set<PrimaryKey> resultKeys = isMaxToken ? primaryKeys.tailSet(left, leftInclusive) : primaryKeys.subSet(left, leftInclusive, right, rightInclusive);
|
||||
if (!vectorQueryContext.getShadowedPrimaryKeys().isEmpty())
|
||||
resultKeys = resultKeys.stream().filter(pk -> !vectorQueryContext.containsShadowedPrimaryKey(pk)).collect(Collectors.toSet());
|
||||
|
||||
if (resultKeys.isEmpty())
|
||||
return KeyRangeIterator.empty();
|
||||
|
||||
int bruteForceRows = maxBruteForceRows(vectorQueryContext.limit(), resultKeys.size(), graph.size());
|
||||
Tracing.trace("Search range covers {} rows; max brute force rows is {} for memtable index with {} nodes, LIMIT {}",
|
||||
resultKeys.size(), bruteForceRows, graph.size(), vectorQueryContext.limit());
|
||||
if (resultKeys.size() < Math.max(vectorQueryContext.limit(), bruteForceRows))
|
||||
return new ReorderingRangeIterator(new PriorityQueue<>(resultKeys));
|
||||
else
|
||||
bits = new KeyRangeFilteringBits(keyRange, vectorQueryContext.bitsetForShadowedPrimaryKeys(graph));
|
||||
}
|
||||
else
|
||||
{
|
||||
// partition/range deletion won't trigger index update, so we have to filter shadow primary keys in memtable index
|
||||
bits = queryContext.vectorContext().bitsetForShadowedPrimaryKeys(graph);
|
||||
}
|
||||
|
||||
var keyQueue = graph.search(qv, queryContext.vectorContext().limit(), bits);
|
||||
if (keyQueue.isEmpty())
|
||||
return KeyRangeIterator.empty();
|
||||
return new ReorderingRangeIterator(keyQueue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyRangeIterator limitToTopResults(List<PrimaryKey> primaryKeys, Expression expression, int limit)
|
||||
{
|
||||
if (minimumKey == null)
|
||||
// This case implies maximumKey is empty too.
|
||||
return KeyRangeIterator.empty();
|
||||
|
||||
List<PrimaryKey> results = primaryKeys.stream()
|
||||
.dropWhile(k -> k.compareTo(minimumKey) < 0)
|
||||
.takeWhile(k -> k.compareTo(maximumKey) <= 0)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int maxBruteForceRows = maxBruteForceRows(limit, results.size(), graph.size());
|
||||
Tracing.trace("SAI materialized {} rows; max brute force rows is {} for memtable index with {} nodes, LIMIT {}",
|
||||
results.size(), maxBruteForceRows, graph.size(), limit);
|
||||
if (results.size() <= maxBruteForceRows)
|
||||
{
|
||||
if (results.isEmpty())
|
||||
return KeyRangeIterator.empty();
|
||||
return new KeyRangeListIterator(minimumKey, maximumKey, results);
|
||||
}
|
||||
|
||||
ByteBuffer buffer = expression.lower.value.raw;
|
||||
float[] qv = TypeUtil.decomposeVector(indexContext, buffer);
|
||||
var bits = new KeyFilteringBits(results);
|
||||
var keyQueue = graph.search(qv, limit, bits);
|
||||
if (keyQueue.isEmpty())
|
||||
return KeyRangeIterator.empty();
|
||||
return new ReorderingRangeIterator(keyQueue);
|
||||
}
|
||||
|
||||
private int maxBruteForceRows(int limit, int nPermittedOrdinals, int graphSize)
|
||||
{
|
||||
int expectedNodesVisited = expectedNodesVisited(limit, nPermittedOrdinals, graphSize);
|
||||
int expectedComparisons = indexContext.getIndexWriterConfig().getMaximumNodeConnections() * expectedNodesVisited;
|
||||
// in-memory comparisons are cheaper than pulling a row off disk and then comparing
|
||||
// VSTODO this is dramatically oversimplified
|
||||
// larger dimension should increase this, because comparisons are more expensive
|
||||
// lower chunk cache hit ratio should decrease this, because loading rows is more expensive
|
||||
double memoryToDiskFactor = 0.25;
|
||||
return (int) max(limit, memoryToDiskFactor * expectedComparisons);
|
||||
}
|
||||
|
||||
/**
|
||||
* All parameters must be greater than zero. nPermittedOrdinals may be larger than graphSize.
|
||||
*/
|
||||
public static int expectedNodesVisited(int limit, int nPermittedOrdinals, int graphSize)
|
||||
{
|
||||
// constants are computed by Code Interpreter based on observed comparison counts in tests
|
||||
// https://chat.openai.com/share/2b1d7195-b4cf-4a45-8dce-1b9b2f893c75
|
||||
var sizeRestriction = min(nPermittedOrdinals, graphSize);
|
||||
var raw = (int) (0.7 * pow(log(graphSize), 2) *
|
||||
pow(graphSize, 0.33) *
|
||||
pow(log(limit), 2) *
|
||||
pow(log((double) graphSize / sizeRestriction), 2) / pow(sizeRestriction, 0.13));
|
||||
// we will always visit at least min(limit, graphSize) nodes, and we can't visit more nodes than exist in the graph
|
||||
return min(max(raw, min(limit, graphSize)), graphSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Pair<ByteComparable, PrimaryKeys>> iterator()
|
||||
{
|
||||
// This method is only used when merging an in-memory index with a RowMapping. This is done a different
|
||||
// way with the graph using the writeData method below.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public SegmentMetadata.ComponentMetadataMap writeDirect(IndexDescriptor indexDescriptor,
|
||||
IndexContext indexContext,
|
||||
Function<PrimaryKey, Integer> postingTransformer) throws IOException
|
||||
{
|
||||
return graph.writeData(indexDescriptor, indexContext, postingTransformer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return graph.isEmpty();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ByteBuffer getMinTerm()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ByteBuffer getMaxTerm()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private class KeyRangeFilteringBits implements Bits
|
||||
{
|
||||
private final AbstractBounds<PartitionPosition> keyRange;
|
||||
@Nullable
|
||||
private final Bits bits;
|
||||
|
||||
public KeyRangeFilteringBits(AbstractBounds<PartitionPosition> keyRange, @Nullable Bits bits)
|
||||
{
|
||||
this.keyRange = keyRange;
|
||||
this.bits = bits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int ordinal)
|
||||
{
|
||||
if (bits != null && !bits.get(ordinal))
|
||||
return false;
|
||||
|
||||
var keys = graph.keysFromOrdinal(ordinal);
|
||||
return keys.stream().anyMatch(k -> keyRange.contains(k.partitionKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return graph.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ReorderingRangeIterator extends KeyRangeIterator
|
||||
{
|
||||
private final PriorityQueue<PrimaryKey> keyQueue;
|
||||
|
||||
ReorderingRangeIterator(PriorityQueue<PrimaryKey> keyQueue)
|
||||
{
|
||||
super(minimumKey, maximumKey, keyQueue.size());
|
||||
this.keyQueue = keyQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
// VSTODO maybe we can abuse "current" to avoid having to pop and re-add the last skipped key
|
||||
protected void performSkipTo(PrimaryKey nextKey)
|
||||
{
|
||||
while (!keyQueue.isEmpty() && keyQueue.peek().compareTo(nextKey) < 0)
|
||||
keyQueue.poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
|
||||
@Override
|
||||
protected PrimaryKey computeNext()
|
||||
{
|
||||
if (keyQueue.isEmpty())
|
||||
return endOfData();
|
||||
return keyQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyFilteringBits implements Bits
|
||||
{
|
||||
private final List<PrimaryKey> results;
|
||||
|
||||
public KeyFilteringBits(List<PrimaryKey> results)
|
||||
{
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean get(int i)
|
||||
{
|
||||
var pk = graph.keysFromOrdinal(i);
|
||||
return results.stream().anyMatch(pk::contains);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return results.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ package org.apache.cassandra.index.sai.plan;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -38,7 +37,7 @@ public class Expression
|
|||
|
||||
public enum IndexOperator
|
||||
{
|
||||
EQ, RANGE, CONTAINS_KEY, CONTAINS_VALUE;
|
||||
EQ, RANGE, CONTAINS_KEY, CONTAINS_VALUE, ANN;
|
||||
|
||||
public static IndexOperator valueOf(Operator operator)
|
||||
{
|
||||
|
|
@ -59,6 +58,9 @@ public class Expression
|
|||
case GTE:
|
||||
return RANGE;
|
||||
|
||||
case ANN:
|
||||
return ANN;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -80,7 +82,6 @@ public class Expression
|
|||
public final IndexContext context;
|
||||
public final AbstractType<?> validator;
|
||||
|
||||
@VisibleForTesting
|
||||
protected IndexOperator operator;
|
||||
|
||||
public Bound lower, upper;
|
||||
|
|
@ -160,6 +161,11 @@ public class Expression
|
|||
else
|
||||
lower = new Bound(value, validator, lowerInclusive);
|
||||
break;
|
||||
case ANN:
|
||||
operator = IndexOperator.ANN;
|
||||
lower = new Bound(value, validator, true);
|
||||
upper = lower;
|
||||
break;
|
||||
}
|
||||
|
||||
assert operator != null;
|
||||
|
|
@ -172,6 +178,11 @@ public class Expression
|
|||
*/
|
||||
public boolean isSatisfiedBy(ByteBuffer columnValue)
|
||||
{
|
||||
// If the expression represents an ANN ordering then we return true because the actual result
|
||||
// is approximate and will rarely / never match the expression value
|
||||
if (validator.isVector())
|
||||
return true;
|
||||
|
||||
if (!TypeUtil.isValid(columnValue, validator))
|
||||
{
|
||||
logger.error(context.logMessage("Value is not valid for indexed column {} with {}"), context.getColumnName(), validator);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue