Defining Indices

There are two different ways to define indices for the GigaMap; by implementing or by annotating. Or use both in combination.

The following entities are given:

public class Person
{
    private long           id           ;
    private String         firstName    ;
    private String         lastName     ;
    private LocalDate      dateOfBirth  ;
    private Address        address      ;
    private MaritalStatus  maritalStatus;
    private List<Interest> interests    ;

    // ...
}

public class Address
{
    private String street ;
    private String city   ;
    private String country;

    // ...
}

public enum MaritalStatus
{
    MARRIED,
    WIDOWED,
    SEPARATED,
    DIVORCED,
    SINGLE
}

public enum Interest
{
    SPORTS,
    LITERATURE,
    PARTY,
    CHARITY
}

By Implementation

Implementing indices requires a bit more initial work when defining them compared to using annotations; however, it becomes easier later on when you work with them, such as when creating queries.

We define several indices for the properties of Person. A good practice is to keep the indexer instances as singletons, allowing for easy access later on.

Either define them directly in the entity class or separately.

The Indexer implementations must at least override the method that extracts the key value. By default, the name of the index is derived from its declaration; for example, when written as a constant, the fully qualified name of the constant will be used. If you want to provide a name yourself, simply override the name() method.

Indexer names must be unique within a GigaMap. Registering a second indexer under a name that is already taken throws a RuntimeException at registration time. This is rarely an issue when names come from the default derivation, since declaration-based names are naturally distinct, but it can bite when you override name() manually or use anonymous Indexer instances whose derived names collide. Give each indexer an explicit, unique name() in those cases.

The predefined indexers are usually sufficient; just extend from their Abstract base type.

public class PersonIndices
{
    public final static BinaryIndexerLong<Person> idIndex = new BinaryIndexerLong.Abstract<>()
    {
        // good practice to provide a custom name, but not absolutely necessary
        public String name()
        {
            return "id";
        }

        @Override
        protected Long getLong(final Person entity)
        {
            return entity.getId();
        }
    };

	// ...
}

If you need range queries on numeric types (e.g. lessThan, greaterThan, between), use the byte-decomposed indexers:

public final static ByteIndexerInteger<Person> age = new ByteIndexerInteger.Abstract<>()
{
    @Override
    protected Integer getInteger(final Person entity)
    {
        return entity.getAge();
    }
};

If you want to index a custom type for which there is no predefined indexer available, simply extend from the 'Indexer.Abstract' base type.

Keep in mind that indexers use value equality by default, so when using a custom key type make sure to implement equals and hashCode. In this case we are using an enum which implements it by default.
public final static Indexer<Person, MaritalStatus> maritalStatus = new Indexer.Abstract<>()
{
    @Override
    public Class<MaritalStatus> keyType()
    {
        return MaritalStatus.class;
    }

    @Override
    public MaritalStatus index(Person person)
    {
        return person.getMaritalStatus();
    }
};

Collections can also be indexed using IndexerMultiValue. Everything that implements Iterable is supported, making it suitable for lists, sets, or any other collection type.

This is useful for modeling many-to-many relationships, tags, categories, or any field where an entity can have multiple associated values. Each value in the collection is indexed individually, so an entity with three interests will appear in the index under all three keys.

public final static IndexerMultiValue<Person, Interest> interests = new IndexerMultiValue.Abstract<>()
{
    @Override
    public Class<Interest> keyType()
    {
        return Interest.class;
    }

    @Override
    public Iterable<? extends Interest> indexEntityMultiValue(Person entity)
    {
        return entity.getInterests();
    }
};

In addition to the standard query methods (is, in, not, notIn), multi-value indexers provide the all method, which finds entities whose collection contains all of the specified keys. See Multi-Value Queries for details.

With custom logic, you can define any indexer you can imagine, not just returning values from the entity. For instance, this one creates a generation index.

enum Generation {BOOMER, GEN_X, GEN_Y, GEN_Z, GEN_ALPHA, GEN_BETA}

public final static Indexer<Person, Generation> generation = new Indexer.Abstract<>()
{
    @Override
    public Class<Generation> keyType()
    {
        return Generation.class;
    }

    @Override
    public Generation indexEntity(Person person)
    {
        int year = person.getDateOfBirth().getYear();
        if(year <= 1964) return Generation.BOOMER;
        if(year <= 1980) return Generation.GEN_X;
        // and so on
    }
};

Registration

When creating the GigaMap, the previously defined indices must be registered.

GigaMap<Person> gigaMap = GigaMap.<Person>Builder()
    .withBitmapIdentityIndex(PersonIndices.id)
    .withBitmapIndex(PersonIndices.maritalStatus)
    .withBitmapIndex(PersonIndices.interests)
    // ...
    .build();

With Annotations

Alternatively, indices can be defined by annotating the relevant fields of the entity.

public class Person
{
	@Identity
    private long           id           ;

    @Index
    private String         firstName    ;

    @Index
    private String         lastName     ;

    @Index
    private LocalDate      dateOfBirth  ;

    private Address        address      ;

    private MaritalStatus  maritalStatus;

    @Index
    private List<Interest> interests    ;

    // ...
}

@Unique and @Identity do not require a companion @Index: on their own they generate the appropriate index for the property — a unique index, respectively an index marked as identity (as with the standalone @Identity on id above). They may still be combined with @Index and with each other, e.g. @Index @Unique String email;.

Registration

When creating the GigaMap, the indices must also be registered.

GigaMap<Person> gigaMap = GigaMap.New();
BitmapIndices<Person> bitmapIndices = gigaMap.index().bitmap();
IndexerGenerator.AnnotationBased(Person.class).generateIndices(bitmapIndices);

Since there are no constants in your code, you need a handle to the generated indexers. The simplest way is to keep the GeneratedIndices returned by generateIndices(…​) — a by-name, typed registry of the generated indexers that you can reuse like a hand-written constant:

GeneratedIndices<Person> idx =
    IndexerGenerator.AnnotationBased(Person.class).generateIndices(gigaMap);

gigaMap.query(idx.getIndexerString("firstName").startsWith("J"));
gigaMap.query(idx.getIndexerLocalDate("dateOfBirth").after(LocalDate.of(2000, 1, 1)));

For a GigaMap loaded from existing storage the indexers are already registered; just re-run the (idempotent) generation on the loaded map to obtain a fresh handle — re-registration is a no-op, and the returned indexers query correctly because they resolve against the registered index by name.

To get typed constants at compile time instead of a runtime registry — Person_.firstName.startsWith("J") with IDE autocomplete and refactor-safe names — use the gigamap-codegen annotation processor (see Compile-time metamodel).

Alternatively, retrieve an indexer directly from the BitmapIndices getter API by name:

BitmapIndices<Person> bitmapIndices = gigaMap.index().bitmap();
IndexerString<Person> firstNameIndex = bitmapIndices.getIndexerString("firstName");
// ...
The value-typed getters (getIndexerInteger, getIndexerString, …) match only the low-cardinality (AUTO) variant. An index declared with @Index(binary = true) / kind = BINARY or kind = BIT_SLICED lives in a parallel hierarchy (not an IndexerInteger/IndexerString subtype) and must be fetched via a dedicated getter: a binary numeric index via getBinaryIndexer(name), a binary String via getBinaryIndexerString(name), a binary UUID via getIndexerUUID(name), and a bit-sliced numeric index via getByteIndexerNumber(keyType, name). Dedicated getters also exist for Instant, ZonedDateTime, spatial and comparing (Comparable/Date) indexes.
A @Unique field is generated as a binary index even without @Index(binary = true). In particular a @Unique String (e.g. an e-mail or username) is a BinaryIndexerString and must be retrieved via getBinaryIndexerString(name)getIndexerString(name) would throw a ClassCastException.

Supported types

For each annotated property the generator picks a matching indexer based on the declared type:

  • String, Character

  • the numeric wrapper/primitive types (Byte, Short, Integer, Long, Float, Double)

  • Boolean

  • java.time types: LocalDate, LocalTime, LocalDateTime, YearMonth, Instant, ZonedDateTime

  • UUID (always a binary index)

  • enum types (low-cardinality index)

  • Iterable<T> and arrays (multi-value index)

  • any Comparable type (including java.util.Date) gets a comparing index, so range queries (lessThan, greaterThan, between) work in addition to equality

Any remaining (non-Comparable) type falls back to a generic, equality-based indexer (value equals/hashCode). For such custom key types, make sure equals and hashCode are implemented.

Choosing the index kind

By default the generator picks a suitable bitmap index for the field type. Use @Index(kind = …​) to choose explicitly (see Index Types for the trade-offs):

public class Product
{
    @Index(kind = IndexKind.LOW_CARDINALITY)  // hashing index (default for most types)
    private String category;

    @Index(kind = IndexKind.BINARY)           // high cardinality, equality-only
    private long id;

    @Index(kind = IndexKind.BIT_SLICED)       // high cardinality, supports range queries
    private int price;
}

// range query on the bit-sliced numeric index
ByteIndexerInteger<Product> price = gigaMap.index().bitmap().getIndexer(ByteIndexerInteger.class, "price");
gigaMap.query(price.between(10, 100));

@Index(binary = true) is the backward-compatible shortcut for kind = IndexKind.BINARY. BINARY supports the natural-number types, float/double, String and UUID; BIT_SLICED supports the numeric types and Instant. Requesting either for an unsupported type fails fast at generation time.

Custom indexer via a creator

When none of the built-in mappings fit, supply your own indexer with @Index(creator = …​). The creator is an Indexer.Creator with a public no-argument constructor; the Indexer returned by its create() method is registered as the index for that property:

@Index(creator = MyCreator.class)
private SomeType value;

If the creator implements Indexer.Creator.MemberAware, the generator calls initialize(indexName, member) with the resolved index name and the reflective Field/Method before create(). This lets a single creator be reused across properties and read whichever member it was attached to, instead of hard-coding one:

public class UpperCaseStringCreator implements Indexer.Creator.MemberAware<MyEntity, String>
{
    private String name;
    private Field  field;

    @Override
    public void initialize(final String indexName, final Member member)
    {
        this.name  = indexName;
        this.field = (Field)member;
    }

    @Override
    public Indexer<MyEntity, String> create()
    {
        final String name  = this.name;
        final Field  field = this.field;
        return new IndexerString.Abstract<>()
        {
            @Override public String name() { return name; }
            @Override protected String getString(final MyEntity e)
            {
                try { return ((String)field.get(e)).toUpperCase(); }
                catch(IllegalAccessException ex) { throw new RuntimeException(ex); }
            }
        };
    }
}
As with member access, the creator class must be reachable from the GigaMap module — in a named module, make the creator public and opens its package to org.eclipse.store.gigamap.

Annotating getters

@Index, @Unique and @Identity may be placed on a no-argument getter instead of a field. The index name is derived from the property (the get/is prefix is stripped). This makes annotation-based indexing work for records and immutable types:

public record Product(@Index String sku, @Index int quantity) { }
The generator reads the annotated members reflectively. When your entities live in a named Java module, open their package to the GigaMap module so the values can be read: opens com.example.model to org.eclipse.store.gigamap; (and to the respective integration module — e.g. org.eclipse.store.gigamap.lucene — when using its annotations).

Class-level spatial index

A spatial index spans two coordinate properties and is therefore declared on the type with @SpatialIndex, naming the latitude and longitude members (fields or getters):

@SpatialIndex(latitude = "lat", longitude = "lon")
public class City
{
    private String name;
    private double lat;
    private double lon;
}

SpatialIndexer<City> spatial = gigaMap.index().bitmap().getIndexer(SpatialIndexer.class, "spatial");
gigaMap.query(spatial.near(52.52, 13.405, 100)); // within 100 km

See Spatial Index for the full query API.

Generating across index types

IndexerGenerator.AnnotationBased(…​).generateIndices(BitmapIndices) only generates bitmap indices. To also generate full-text or vector indices declared by integration-module annotations, register the corresponding handler and call the GigaMap overload:

IndexerGenerator.AnnotationBased(Article.class)
    .register(LuceneAnnotationHandler.New())   // from gigamap-lucene
    .register(VectorAnnotationHandler.New())   // from gigamap-jvector
    .generateIndices(gigaMap);                 // bitmap + full-text + vector

See Annotation-based Indexing for the full picture and for adding your own handler. The @Indexed type-level marker can be used to flag entity types whose indices are defined by annotations.

Persistence

Indexer instances are part of the GigaMap’s persistent object graph: when storage is reopened, each registered indexer is restored alongside its bitmap data and remains attached. Plain field-accessor indexers — the common case — need no special treatment. If a custom indexer holds non-configuration state such as a cache, a derived equator, or a service handle, mark those fields transient and reinitialize them lazily on first use, the same way computed vectorizers handle their embedding models.

Determining the index name

Each indexer is stored under a unique name. By default the name is derived from the declaring static field — {outer class FQCN}.{fieldName} — so an indexer like PersonIndices.maritalStatus is registered as org.example.PersonIndices.maritalStatus. The same name is reproduced reliably across JVM runs, which is what makes the index findable after reload. For indexers that are not assigned to a static field (anonymous inner classes created inline, instances created in a method) the fallback name is class-based and may collide with other anonymous instances of the same indexer type.

You can always take full control of the name by overriding the name() method on the indexer itself. This works for any indexer regardless of how it is declared, and is the way to decouple the persisted index name from the Java identifier — useful when refactoring (you can rename or move the constant without invalidating persisted indices), or when you need a name shorter / friendlier than the auto-derived FQCN, or to ensure stability for anonymous instances:

public final static Indexer<Person, MaritalStatus> maritalStatus = new Indexer.Abstract<>()
{
    @Override public String name() { return "person.maritalStatus"; }
    // ...
};

For annotation-based indexers, the name is the annotated property — @Index on firstName registers the index as firstName.

Looking up an index after load

After the GigaMap is loaded from existing storage, indexers are owned and managed by GigaMap — you do not re-register them. If you kept your indexers as static constants you can keep referring to them directly in queries (gigaMap.query(PersonIndices.maritalStatus.is(MARRIED))); GigaMap matches the constant to its persisted index by name. Otherwise, look the index up via BitmapIndices:

BitmapIndices<Person> bitmapIndices = gigaMap.index().bitmap();
BitmapIndex<Person, MaritalStatus> idx = bitmapIndices.get(MaritalStatus.class, "person.maritalStatus");
IndexerString<Person>              firstNameIndex = bitmapIndices.getStringIndex("firstName");

get(…​) returns null if no index with that name is registered.