The poly-index library is a lightweight Java utility providing simple in-memory indexing for objects, with multi-index lookup capabilities. It is basically a wrapper around standard java maps, but also allows Lucene-based in-memory indices. This allows the retrieval of objects using various indexing keys without manually managing multiple maps.
User interaction is straightforward: you "push" objects into the store where they are automatically indexed according to predefined keys. You can then retrieve them by querying the store with a specific key and a matching value, or even a list of keys to implement fallback or priority-based lookup logic.
Add the following in your pom.xml:
<dependency>
<groupId>tech.illuin</groupId>
<artifactId>poly-index</artifactId>
<version>0.10</version>
</dependency>Simple example: exact match
// Define a key based on a field
Key<User> EMAIL_KEY = Key.of(User::email);
// Initialize store and push data
IndexedStore<User> store = new MapStore<>(Index.of(EMAIL_KEY));
store.push(new User("john.doe@example.com", "John Doe"));
// Retrieve by value
Optional<User> user = store.getFirst("john.doe@example.com", EMAIL_KEY);More complex example: priority matching with multiple keys
// Define multiple keys (exact match and combination)
Key<Vehicle> PLATE_KEY = Key.of(Vehicle::plate);
Key<Vehicle> BRAND_MODEL_KEY = MapCombinationKey.of(
MapCombinationKey.requires(Vehicle::brand, Vehicle::model),
MapIndexType.FIRST
);
IndexedStore<Vehicle> store = new MapStore<>(Index.of(PLATE_KEY, BRAND_MODEL_KEY));
store.pushAll(vehicles);
// Query using the "exemplar pattern" ; try each key in order until a match is found
Vehicle search = new Vehicle("ABC-123", "Toyota", "Corolla");
Optional<Vehicle> match = store.getFirstMatch(search, List.of(PLATE_KEY, BRAND_MODEL_KEY));The general workflow when using poly-index is as follows:
- Define index keys: specify how identifiers are extracted from your objects
- Use
Key.of(Function)for simple field matching. - Use
MapCombinationKey.of(...)for composite keys (multiple fields).
- Initialize the store: Create an
IndexedStoreby providing it with anIndexregistry containing all the keys you intend to use. - Populate the store: Use
push(T)orpushAll(Collection<T>)to add objects. They will be automatically indexed across all registered keys. - Query the store:
get(value, key): returns all matches for a given key and value.getFirst(value, key): returns the first match for a given key and value.getFirstMatch(exemplar, keys): (exemplar pattern) takes a template object and a list of keys, trying each key in order.
The poly-index library provides several ways to define how your objects are indexed, ranging from simple field lookups to more complex composite and multi-valued keys.
Simple keys are best for exact matching on single fields or simple transformations.
// Simple field-based key
Key<User> EMAIL_KEY = Key.of(User::email);
// Named key (useful for debugging and custom indexing logic)
Key<User> NAME_KEY = Key.of("user-name", User::name);
// Key with a custom transformation
Key<Product> BRAND_MODEL_KEY = Key.of(p -> p.brand() + ":" + p.model());
// Key with specific MapIndexType (stores all matches for the same key)
Key<Product> CAT_KEY = Key.of(Product::category, MapIndexType.ALL);MapCombinationKey is the primary way to define indices based on multiple fields. It can accept a requires clause (fields that must be non-null) and/or excludes clause (fields must be null).
Basic composite key:
// Matches ONLY if both brand and model are present
public static final Key<Vehicle> BRAND_MODEL_KEY = MapCombinationKey.of(
requires(Vehicle::brand, Vehicle::model),
MapIndexType.FIRST
);Using requirements and exclusions:
public record Product(String category, String subCategory, String sku, String tag) {
/* Matches ONLY if category, subCategory and sku are all present */
public static final Key<Product> CAT_SUB_SKU_KEY = MapCombinationKey.of(
requires(Product::category, Product::subCategory, Product::sku),
MapIndexType.FIRST
);
/* Matches if category and subCategory are present, but ONLY if sku is NULL */
public static final Key<Product> CAT_SUB_KEY = MapCombinationKey.of(
requires(Product::category, Product::subCategory),
excludes(Product::sku),
MapIndexType.FIRST
);
}A single key can support multiple "variants" of indexing requirements.
// A key that matches EITHER (brand + model) OR (brand + licensePlate)
Key<Vehicle> MULTI_VARIANT_KEY = MapCombinationKey.of(List.of(
MapCombinationKey.variant(requires(Vehicle::brand, Vehicle::model)),
MapCombinationKey.variant(requires(Vehicle::brand, Vehicle::licensePlate))
), MapIndexType.FIRST);When indexing multiple objects that might share the same key value, MapIndexType (or custom MapIndexStrategy) defines which ones are kept:
ALL: (default) stores all objects matching the key in a list.FIRST: only stores the first object encountered for a given key value. Subsequent objects are ignored.LAST: only stores the last object encountered. Each new object replaces the previous one for that key.
For scenarios like matching against a list of tags or variants, use custom functions returning an IndexKeyCollection.
public static final Key<Product> TAG_KEY = MapCombinationKey.of(
requires(Product::category),
new IndexFirstStrategy<>(entity -> {
// Generate multiple index entries from a single field
List<String> tags = Arrays.asList(entity.tag().split(","));
return IndexKeyCollection.of(tags);
})
);For more advanced search requirements like fuzzy matching or prefix-based lookups, you can use Lucene-based keys. These leverage an in-memory Lucene index while maintaining the same IndexedStore API.
// Fuzzy match: matches "Doe" even if searched as "Doo"
Key<Person> FUZZY_KEY = Key.ofLucene("fuzzy", Person::lastName, new FuzzyMatchStrategy());
// Prefix match: matches "Doe" if searched as "Do"
Key<Person> PREFIX_KEY = Key.ofLucene("prefix", Person::lastName, new PartialMatchStrategy());
// Querying remains consistent with map-based stores
Optional<Person> p = store.getFirst("Doo", FUZZY_KEY);Key.ofLuceneQuery provides full control over how Lucene Documents are created and how Query objects are parsed.
public static final Key<Person> ADVANCED_KEY = Key.ofLuceneQuery(
person -> List.of(
new StringField("fname", person.firstName(), Field.Store.YES),
new StringField("lname", person.lastName(), Field.Store.YES)
),
(parser, criteria) -> parser.parse(
"fname:" + criteria.firstName() + " AND lname:" + criteria.lastName()
)
);This project will require you to have the following:
- Java 17+
- Git (versioning)
- Maven (dependency resolving, publishing and packaging)