3939import org .apache .lucene .util .VectorUtil ;
4040import org .elasticsearch .common .ParsingException ;
4141import org .elasticsearch .common .settings .Setting ;
42+ import org .elasticsearch .common .util .FeatureFlag ;
4243import org .elasticsearch .common .xcontent .support .XContentMapValues ;
4344import org .elasticsearch .features .NodeFeature ;
4445import org .elasticsearch .index .IndexVersion ;
4849import org .elasticsearch .index .codec .vectors .ES814HnswScalarQuantizedVectorsFormat ;
4950import org .elasticsearch .index .codec .vectors .ES815BitFlatVectorFormat ;
5051import org .elasticsearch .index .codec .vectors .ES815HnswBitVectorsFormat ;
52+ import org .elasticsearch .index .codec .vectors .IVFVectorsFormat ;
5153import org .elasticsearch .index .codec .vectors .es818 .ES818BinaryQuantizedVectorsFormat ;
5254import org .elasticsearch .index .codec .vectors .es818 .ES818HnswBinaryQuantizedVectorsFormat ;
5355import org .elasticsearch .index .fielddata .FieldDataContext ;
6264import org .elasticsearch .index .mapper .Mapper ;
6365import org .elasticsearch .index .mapper .MapperBuilderContext ;
6466import org .elasticsearch .index .mapper .MapperParsingException ;
67+ import org .elasticsearch .index .mapper .MappingLookup ;
6568import org .elasticsearch .index .mapper .MappingParser ;
6669import org .elasticsearch .index .mapper .NumberFieldMapper ;
6770import org .elasticsearch .index .mapper .SimpleMappedFieldType ;
7881import org .elasticsearch .search .vectors .ESDiversifyingChildrenFloatKnnVectorQuery ;
7982import org .elasticsearch .search .vectors .ESKnnByteVectorQuery ;
8083import org .elasticsearch .search .vectors .ESKnnFloatVectorQuery ;
84+ import org .elasticsearch .search .vectors .IVFKnnFloatVectorQuery ;
8185import org .elasticsearch .search .vectors .RescoreKnnVectorQuery ;
8286import org .elasticsearch .search .vectors .VectorData ;
8387import org .elasticsearch .search .vectors .VectorSimilarityQuery ;
106110import static org .elasticsearch .cluster .metadata .IndexMetadata .SETTING_INDEX_VERSION_CREATED ;
107111import static org .elasticsearch .common .Strings .format ;
108112import static org .elasticsearch .common .xcontent .XContentParserUtils .ensureExpectedToken ;
113+ import static org .elasticsearch .index .codec .vectors .IVFVectorsFormat .MAX_VECTORS_PER_CLUSTER ;
114+ import static org .elasticsearch .index .codec .vectors .IVFVectorsFormat .MIN_VECTORS_PER_CLUSTER ;
109115
110116/**
111117 * A {@link FieldMapper} for indexing a dense vector of floats.
@@ -115,6 +121,8 @@ public class DenseVectorFieldMapper extends FieldMapper {
115121 private static final float EPS = 1e-3f ;
116122 public static final int BBQ_MIN_DIMS = 64 ;
117123
124+ public static final FeatureFlag IVF_FORMAT = new FeatureFlag ("ivf_format" );
125+
118126 public static boolean isNotUnitVector (float magnitude ) {
119127 return Math .abs (magnitude - 1.0f ) > EPS ;
120128 }
@@ -1594,14 +1602,63 @@ public boolean supportsElementType(ElementType elementType) {
15941602 return elementType == ElementType .FLOAT ;
15951603 }
15961604
1605+ @ Override
1606+ public boolean supportsDimension (int dims ) {
1607+ return dims >= BBQ_MIN_DIMS ;
1608+ }
1609+ },
1610+ BBQ_IVF ("bbq_ivf" , true ) {
1611+ @ Override
1612+ public IndexOptions parseIndexOptions (String fieldName , Map <String , ?> indexOptionsMap , IndexVersion indexVersion ) {
1613+ Object clusterSizeNode = indexOptionsMap .remove ("cluster_size" );
1614+ int clusterSize = IVFVectorsFormat .DEFAULT_VECTORS_PER_CLUSTER ;
1615+ if (clusterSizeNode != null ) {
1616+ clusterSize = XContentMapValues .nodeIntegerValue (clusterSizeNode );
1617+ if (clusterSize < MIN_VECTORS_PER_CLUSTER || clusterSize > MAX_VECTORS_PER_CLUSTER ) {
1618+ throw new IllegalArgumentException (
1619+ "cluster_size must be between "
1620+ + MIN_VECTORS_PER_CLUSTER
1621+ + " and "
1622+ + MAX_VECTORS_PER_CLUSTER
1623+ + ", got: "
1624+ + clusterSize
1625+ );
1626+ }
1627+ }
1628+ RescoreVector rescoreVector = RescoreVector .fromIndexOptions (indexOptionsMap , indexVersion );
1629+ if (rescoreVector == null ) {
1630+ rescoreVector = new RescoreVector (DEFAULT_OVERSAMPLE );
1631+ }
1632+ Object nProbeNode = indexOptionsMap .remove ("default_n_probe" );
1633+ int nProbe = -1 ;
1634+ if (nProbeNode != null ) {
1635+ nProbe = XContentMapValues .nodeIntegerValue (nProbeNode );
1636+ if (nProbe < 1 && nProbe != -1 ) {
1637+ throw new IllegalArgumentException (
1638+ "default_n_probe must be at least 1 or exactly -1, got: " + nProbe + " for field [" + fieldName + "]"
1639+ );
1640+ }
1641+ }
1642+ MappingParser .checkNoRemainingFields (fieldName , indexOptionsMap );
1643+ return new BBQIVFIndexOptions (clusterSize , nProbe , rescoreVector );
1644+ }
1645+
1646+ @ Override
1647+ public boolean supportsElementType (ElementType elementType ) {
1648+ return elementType == ElementType .FLOAT ;
1649+ }
1650+
15971651 @ Override
15981652 public boolean supportsDimension (int dims ) {
15991653 return dims >= BBQ_MIN_DIMS ;
16001654 }
16011655 };
16021656
16031657 static Optional <VectorIndexType > fromString (String type ) {
1604- return Stream .of (VectorIndexType .values ()).filter (vectorIndexType -> vectorIndexType .name .equals (type )).findFirst ();
1658+ return Stream .of (VectorIndexType .values ())
1659+ .filter (vectorIndexType -> vectorIndexType != VectorIndexType .BBQ_IVF || IVF_FORMAT .isEnabled ())
1660+ .filter (vectorIndexType -> vectorIndexType .name .equals (type ))
1661+ .findFirst ();
16051662 }
16061663
16071664 private final String name ;
@@ -2100,6 +2157,54 @@ public boolean validateDimension(int dim, boolean throwOnError) {
21002157
21012158 }
21022159
2160+ static class BBQIVFIndexOptions extends QuantizedIndexOptions {
2161+ final int clusterSize ;
2162+ final int defaultNProbe ;
2163+
2164+ BBQIVFIndexOptions (int clusterSize , int defaultNProbe , RescoreVector rescoreVector ) {
2165+ super (VectorIndexType .BBQ_IVF , rescoreVector );
2166+ this .clusterSize = clusterSize ;
2167+ this .defaultNProbe = defaultNProbe ;
2168+ }
2169+
2170+ @ Override
2171+ KnnVectorsFormat getVectorsFormat (ElementType elementType ) {
2172+ assert elementType == ElementType .FLOAT ;
2173+ return new IVFVectorsFormat (clusterSize );
2174+ }
2175+
2176+ @ Override
2177+ boolean updatableTo (IndexOptions update ) {
2178+ return update .type .equals (this .type );
2179+ }
2180+
2181+ @ Override
2182+ boolean doEquals (IndexOptions other ) {
2183+ BBQIVFIndexOptions that = (BBQIVFIndexOptions ) other ;
2184+ return clusterSize == that .clusterSize
2185+ && defaultNProbe == that .defaultNProbe
2186+ && Objects .equals (rescoreVector , that .rescoreVector );
2187+ }
2188+
2189+ @ Override
2190+ int doHashCode () {
2191+ return Objects .hash (clusterSize , defaultNProbe , rescoreVector );
2192+ }
2193+
2194+ @ Override
2195+ public XContentBuilder toXContent (XContentBuilder builder , Params params ) throws IOException {
2196+ builder .startObject ();
2197+ builder .field ("type" , type );
2198+ builder .field ("cluster_size" , clusterSize );
2199+ builder .field ("default_n_probe" , defaultNProbe );
2200+ if (rescoreVector != null ) {
2201+ rescoreVector .toXContent (builder , params );
2202+ }
2203+ builder .endObject ();
2204+ return builder ;
2205+ }
2206+ }
2207+
21032208 public record RescoreVector (float oversample ) implements ToXContentObject {
21042209 static final String NAME = "rescore_vector" ;
21052210 static final String OVERSAMPLE = "oversample" ;
@@ -2411,17 +2516,25 @@ && isNotUnitVector(squaredMagnitude)) {
24112516 adjustedK = Math .min ((int ) Math .ceil (k * oversample ), OVERSAMPLE_LIMIT );
24122517 numCands = Math .max (adjustedK , numCands );
24132518 }
2414- Query knnQuery = parentFilter != null
2415- ? new ESDiversifyingChildrenFloatKnnVectorQuery (
2416- name (),
2417- queryVector ,
2418- filter ,
2419- adjustedK ,
2420- numCands ,
2421- parentFilter ,
2422- knnSearchStrategy
2423- )
2424- : new ESKnnFloatVectorQuery (name (), queryVector , adjustedK , numCands , filter , knnSearchStrategy );
2519+ if (parentFilter != null && indexOptions instanceof BBQIVFIndexOptions ) {
2520+ throw new IllegalArgumentException ("IVF index does not support nested queries" );
2521+ }
2522+ Query knnQuery ;
2523+ if (indexOptions instanceof BBQIVFIndexOptions bbqIndexOptions ) {
2524+ knnQuery = new IVFKnnFloatVectorQuery (name (), queryVector , adjustedK , numCands , filter , bbqIndexOptions .defaultNProbe );
2525+ } else {
2526+ knnQuery = parentFilter != null
2527+ ? new ESDiversifyingChildrenFloatKnnVectorQuery (
2528+ name (),
2529+ queryVector ,
2530+ filter ,
2531+ adjustedK ,
2532+ numCands ,
2533+ parentFilter ,
2534+ knnSearchStrategy
2535+ )
2536+ : new ESKnnFloatVectorQuery (name (), queryVector , adjustedK , numCands , filter , knnSearchStrategy );
2537+ }
24252538 if (rescore ) {
24262539 knnQuery = new RescoreKnnVectorQuery (
24272540 name (),
@@ -2651,6 +2764,19 @@ public FieldMapper.Builder getMergeBuilder() {
26512764 return new Builder (leafName (), indexCreatedVersion ).init (this );
26522765 }
26532766
2767+ @ Override
2768+ public void doValidate (MappingLookup mappers ) {
2769+ if (indexOptions instanceof BBQIVFIndexOptions && mappers .nestedLookup ().getNestedParent (fullPath ()) != null ) {
2770+ throw new IllegalArgumentException (
2771+ "["
2772+ + CONTENT_TYPE
2773+ + "] fields with index type ["
2774+ + indexOptions .type
2775+ + "] cannot be indexed if they're within [nested] mappings"
2776+ );
2777+ }
2778+ }
2779+
26542780 private static IndexOptions parseIndexOptions (String fieldName , Object propNode , IndexVersion indexVersion ) {
26552781 @ SuppressWarnings ("unchecked" )
26562782 Map <String , ?> indexOptionsMap = (Map <String , ?>) propNode ;
0 commit comments