Skip to content

Better gigamap queries #674

Description

@hrstoyanov

Is your feature request related to a problem? Please describe.

@fh-ms @hg-ms @zdenek-jonas : This is not a bug, but a feature suggestion sketch, that I use in my code and I think the ES community can benefit from, if incorporated in ES.

Currently the GigaMap queries, while comprehensive, expose the indexers to clients that use them. Indexers are consider internal implementation detail for a persistent entity, not to be exposed in queries api - think how indexes work in relation/sql databases - they are invisible to queries.

Describe the solution you'd like

I want to share some piece of code that mitigates this issue, by adding a thin layer shielding the calls sites from having to reference internal indexers for queries. This leads to much nicer-to-read code and the benefits compound if you have lot of queries.
This code also applies to mutating operations as well.

Describe alternatives you've considered

Consider the attached code - I propose this to be incorporated in ES at some point.
There are two components - Store and Query builder.

  1. Store allows for bolier-plate operations - create(if missing) , update, remove. It also creates a Query builders for use by entity clients.
  2. Query Builder provides fluent, indexer-hinding way to construct much nicer queries.

Examples:

Say you have a an product rating entity:

/// Caches the aggregated rating statistics (average score, total reviews) for a Product.
public final class AggregateRating implements WithTsid {
    
    private final Tsid id;
    private final Tsid productId;
    private float averageRating;
    private int totalReviews;
    
    // ... enum Score {TERRIBLE(1),POOR(2),AVERAGE(3),VERY_GOOD(4),EXCELLENT(5);
    private final Map<Rating.Score, Integer> ratingDistribution;

 // some code  omitted

    public static class Store extends AbstractEntityStore<Tsid, AggregateRating> {

       //add your indexers - they are private!
      
        private static final BinaryIndexerTsid<AggregateRating> PRODUCT_INDEXER = new BinaryIndexerTsid<>() {
            @Override
            protected Tsid getTsid(AggregateRating entity) {
                return entity.productTsid();
            }
        };

        private static final IndexerFloat<AggregateRating> AVERAGE_RATING_INDEXER = new IndexerFloat.Abstract<>() {
            @Override
            public Float getFloat(AggregateRating entity) {
                return entity.averageRating();
            }
        };

        private static final IndexerInteger<AggregateRating> TOTAL_REVIEWS_INDEXER = new IndexerInteger.Abstract<>() {
            @Override
            public Integer getInteger(AggregateRating entity) {
                return entity.totalReviews();
            }
        };

        public Store() {
            super(GigaMap.<AggregateRating>Builder()
                    .withBitmapIdentityIndex(WithTsid.TSID_INDEXER_BINARY)
                    .withBitmapIndex(PRODUCT_INDEXER)
                    .withBitmapIndex(AVERAGE_RATING_INDEXER)
                    .withBitmapIndex(TOTAL_REVIEWS_INDEXER)
                    .build());
        }

        @Override
        public Optional<AggregateRating> findById(Tsid id) {
            return WithTsid.findOne(gigaMap, id);
        }

        @Override
        public Optional<AggregateRating> addOrGetExisting(AggregateRating entity) {
            var existing = findByProduct(entity.productTsid());
            if (existing.isPresent()) return existing;
            add(entity);
            return Optional.empty();
        }

        @Override
        public void add(AggregateRating entity) {
            super.add(entity);
            RootData.getEclipseStoreContext().addAllToPersist(entity,gigaMap);
        }

        @Override
        public void update(AggregateRating entity, Consumer<AggregateRating> updater) {
            super.update(entity, updater);
            TourBizData.getEclipseStoreContext().addAllToPersist(entity,gigaMap);
        }

        @Override
        public boolean remove(AggregateRating entity) {
            if (super.remove(entity)) {
                TourBizData.getEclipseStoreContext().addToPersist(gigaMap);
                return true;
            }
            return false;
        }

        public Optional<AggregateRating> findByProduct(Tsid productId) {
            return gigaMap.query(PRODUCT_INDEXER.is(productId)).findFirst();
        }

        public void updateMetric(Tsid productTsid, Consumer<AggregateRating> metricUpdater) {
            var agg = findByProduct(productTsid).orElseGet(() -> {
                var newAgg = new AggregateRating(productTsid);
                add(newAgg);
                return newAgg;
            });
            update(agg, metricUpdater);
        }      

        //Create a query builder off this store 
        public QueryBuilder query() {
            return new QueryBuilder();
        }

        public class QueryBuilder extends GigaQueryBuilder<AggregateRating, QueryBuilder> {
            public QueryBuilder() {
                super(Store.this.gigaMap);
            }

            @Override
            protected QueryBuilder self() {
                return this;
            }

            public QueryBuilder product(Tsid... productIds) {
                return where(PRODUCT_INDEXER, productIds);
            }

            public QueryBuilder averageRatingGreaterThan(float rating) {
                add(AVERAGE_RATING_INDEXER.greaterThan(rating));
                return this;
            }

            public QueryBuilder totalReviewsGreaterThan(int count) {
                add(TOTAL_REVIEWS_INDEXER.greaterThan(count));
                return this;
            }
        }
    }
}

Then you can query it like that:

var store = dataRoot.aggreagteRationsStore()
var results = store.query()
                        .product(1,2,3)
                        .averageRatingGreaterThan(3.0)
                        .totalReviewsGreaterThan(1000)
  .toList();

Additional context

  1. GigaQueryBuilder place nicely with GigaMap queries - it can be extended with inheritance or composition of gigamap queries

  2. At some point the Store/QueryBuilder can be autogenerated much like the code generators for layered entities

  3. If you look at Spring Data and Jakarta EE Data specs - theyr are going in the same directions - convinience by default, that can be overridden and extended.

Note - attached files. EclipseContextStore.java is included in another feature request

Creator.java
AbstractEntityStore.java
GigaQueryBuilder.java

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions