Repository: gisaia/ARLAS-server (language: python)

Issue:
Support for SQL/JDBC databases
As an ARLAS user I want to be able to use a SQL database so that I can choose my database provider.

TODO:
- [x] `SelectRequest.formatLikeCondition`: use fulltext index. **Warning: add documentation on indexing / default config name**
- [x] `PostgisSelectRequest.addSelectClause(Geotile...)`: implement Geotile aggregation. **Warning: bad performance!**
- [x] `JdbiExploreService.getFeatures`: pagination
- [x] implements same limit of 10000 docs as in ES : **pb is how do we know the number of remaining docs?**
- [ ] `JdbiExploreService.search`: implement method
- [ ] `JdbiExploreService.searchAsRaw`: implement method
- [ ] `JdbiExploreService.getRawDoc`: implement method
- [ ] `JdbiExploreService.formatAggregationResult`: other qualifiers?
- [ ] `JdbiFluidSearch.setRawGeometries`: not possible, investigate further?
- [ ] `JdbiFluidSearch.setHitsToFetch`: not possible, investigate further?
- [ ] Integration tests
- [ ] Documentation
- [ ] BigQuery implementation
- [ ] Performance test and memory tweaking


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index e00ba3402..b312fe633 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ save*
 .DS_Store
 dependency-reduced-pom.xml
 *.log
+*.log.gz
 *.versionsBackup
 *.bak
 tmp/
diff --git a/arlas-core/pom.xml b/arlas-core/pom.xml
index d7787e3de..da6088840 100644
--- a/arlas-core/pom.xml
+++ b/arlas-core/pom.xml
@@ -80,7 +80,7 @@
         </dependency>
 
         <!-- ____________________________________________________ -->
-        <!-- DATABASE -->
+        <!-- ELASTICSEARCH -->
         <!-- ____________________________________________________ -->
 
         <dependency>
@@ -101,6 +101,82 @@
             <version>${elastic-apm.version}</version>
         </dependency>
 
+        <!-- ____________________________________________________ -->
+        <!-- JDBI -->
+        <!-- ____________________________________________________ -->
+
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jdbi3</artifactId>
+            <version>${dropwizard.version}</version>
+            <exclusions>
+                <exclusion>
+                    <artifactId>jdbi3-core</artifactId>
+                    <groupId>org.jdbi</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>jdbi3-guava</artifactId>
+                    <groupId>org.jdbi</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>jdbi3-jodatime2</artifactId>
+                    <groupId>org.jdbi</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>jdbi3-sqlobject</artifactId>
+                    <groupId>org.jdbi</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi3-core</artifactId>
+            <version>${jdbi3.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi3-guava</artifactId>
+            <version>${jdbi3.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi3-jodatime2</artifactId>
+            <version>${jdbi3.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi3-sqlobject</artifactId>
+            <version>${jdbi3.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi3-jackson2</artifactId>
+            <version>${jdbi3.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi3-postgres</artifactId>
+            <version>${jdbi3.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>net.postgis</groupId>
+            <artifactId>postgis-jdbc</artifactId>
+            <version>${postgis-jdbc.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <version>${postgresql.version}</version>
+        </dependency>
+
         <!-- ____________________________________________________ -->
         <!-- GEO -->
         <!-- ____________________________________________________ -->
diff --git a/arlas-core/src/main/java/io/arlas/server/app/ArlasBaseConfiguration.java b/arlas-core/src/main/java/io/arlas/server/app/ArlasBaseConfiguration.java
index 815a05ccd..bbe6ba092 100644
--- a/arlas-core/src/main/java/io/arlas/server/app/ArlasBaseConfiguration.java
+++ b/arlas-core/src/main/java/io/arlas/server/app/ArlasBaseConfiguration.java
@@ -23,9 +23,15 @@
 import com.smoketurner.dropwizard.zipkin.ZipkinFactory;
 import io.arlas.server.exceptions.ArlasConfigurationException;
 import io.dropwizard.Configuration;
+import io.dropwizard.db.DataSourceFactory;
 import io.federecio.dropwizard.swagger.SwaggerBundleConfiguration;
 
+import javax.validation.Valid;
+
 public class ArlasBaseConfiguration extends Configuration {
+    @Valid
+    @JsonProperty("database")
+    public DataSourceFactory database = new DataSourceFactory();
 
     @JsonProperty("zipkin")
     public ZipkinFactory zipkinConfiguration;
@@ -51,9 +57,7 @@ public class ArlasBaseConfiguration extends Configuration {
     public static final String FLATTEN_CHAR = "_";
 
     public void check() throws ArlasConfigurationException {
-        elasticConfiguration.check();
-
-        if (zipkinConfiguration == null) {
+       if (zipkinConfiguration == null) {
             throw new ArlasConfigurationException("Zipkin configuration missing in config file.");
         }
         if (swaggerBundleConfiguration == null) {
@@ -65,7 +69,7 @@ public void check() throws ArlasConfigurationException {
         } else {
             arlasAuthConfiguration.check();
         }
-        if(arlarsCorsConfiguration == null) {
+        if (arlarsCorsConfiguration == null) {
             arlarsCorsConfiguration = new ArlasCorsConfiguration();
             arlarsCorsConfiguration.enabled = false;
         }
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/elastic/services/ElasticExploreService.java b/arlas-core/src/main/java/io/arlas/server/impl/elastic/services/ElasticExploreService.java
index 6159dd6c0..a8e61d5da 100644
--- a/arlas-core/src/main/java/io/arlas/server/impl/elastic/services/ElasticExploreService.java
+++ b/arlas-core/src/main/java/io/arlas/server/impl/elastic/services/ElasticExploreService.java
@@ -21,7 +21,6 @@
 
 import io.arlas.server.exceptions.ArlasException;
 import io.arlas.server.impl.elastic.core.ElasticDocument;
-import io.arlas.server.impl.elastic.core.ElasticFluidSearch;
 import io.arlas.server.impl.elastic.utils.ElasticClient;
 import io.arlas.server.impl.elastic.utils.GeoTypeMapper;
 import io.arlas.server.managers.CollectionReferenceManager;
@@ -36,7 +35,10 @@
 import io.arlas.server.services.CollectionReferenceService;
 import io.arlas.server.services.ExploreService;
 import io.arlas.server.services.FluidSearchService;
-import io.arlas.server.utils.*;
+import io.arlas.server.utils.BoundingBox;
+import io.arlas.server.utils.GeoTileUtil;
+import io.arlas.server.utils.MapExplorer;
+import io.arlas.server.utils.UriInfoWrapper;
 import org.apache.commons.collections.CollectionUtils;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.geo.GeoPoint;
@@ -62,10 +64,6 @@
 
 public class ElasticExploreService extends ExploreService {
 
-    private static final String FEATURE_TYPE_KEY = "feature_type";
-    private static final String FEATURE_TYPE_VALUE = "hit";
-    private static final String FEATURE_GEOMETRY_PATH = "geometry_path";
-
     protected ElasticClient client;
 
     public ElasticExploreService(ElasticClient client, CollectionReferenceService collectionReferenceService, String baseUri, int arlasRestCacheTimeout) {
@@ -78,8 +76,8 @@ public ElasticClient getClient() {
     }
 
     @Override
-    public FluidSearchService getFluidSearch() {
-        return new ElasticFluidSearch().setClient(client);
+    public FluidSearchService getFluidSearch(CollectionReference collectionReference) {
+        return new ElasticFluidSearch(collectionReference).setClient(client);
     }
 
     @Override
@@ -250,8 +248,7 @@ public List<Map<String, Object>> searchAsRaw(MixedRequest request, CollectionRef
     }
 
     private SearchHits getSearchHits(MixedRequest request, CollectionReference collectionReference) throws ArlasException {
-        ElasticFluidSearch fluidSearch = (ElasticFluidSearch) getFluidSearch();
-        fluidSearch.setCollectionReference(collectionReference);
+        ElasticFluidSearch fluidSearch = (ElasticFluidSearch) getFluidSearch(collectionReference);
         applyFilter(collectionReference.params.filter, fluidSearch);
         applyFilter(request.basicRequest.filter, fluidSearch);
         applyFilter(request.headerRequest.filter, fluidSearch);
@@ -261,8 +258,9 @@ private SearchHits getSearchHits(MixedRequest request, CollectionReference colle
     }
 
     @Override
-    public FeatureCollection getFeatures(MixedRequest request, CollectionReference collectionReference, boolean flat) throws ArlasException {
-        SearchHits searchHits = getSearchHits(request, collectionReference);
+    public FeatureCollection getFeatures(MixedRequest request, CollectionReference collectionReference,
+                                         FluidSearchService fluidSearch, boolean flat) throws ArlasException {
+        SearchHits searchHits = ((ElasticFluidSearch) fluidSearch).exec().getHits();
         Search searchRequest = (Search) request.basicRequest;
         FeatureCollection fc = new FeatureCollection();
         List<SearchHit> results = Arrays.asList(searchHits.getHits());
@@ -291,76 +289,11 @@ public FeatureCollection getFeatures(MixedRequest request, CollectionReference c
         return fc;
     }
 
-    private Feature getFeatureFromHit(Hit arlasHit, String path, GeoJsonObject geometry) {
-        Feature feature = new Feature();
-
-        // Setting geometry of geojson
-        feature.setGeometry(geometry);
-
-        // setting the properties of the geojson
-        feature.setProperties(new HashMap<>(arlasHit.getDataAsMap()));
-
-        // Setting the Metadata (md) in properties of geojson. Only id, timestamp and centroid are set in the MD. The geometry is already returned in the geojson.
-        MD md = new MD();
-        md.id = arlasHit.md.id;
-        md.timestamp = arlasHit.md.timestamp;
-        md.centroid = arlasHit.md.centroid;
-        if (!arlasHit.isFlat()) {
-            feature.setProperty(MD.class.getSimpleName().toLowerCase(), md);
-        } else {
-            feature.setProperty(MD.class.getSimpleName().toLowerCase(), md.toFlatString());
-        }
-
-        // Setting the feature type of the geojson
-        feature.setProperty(FEATURE_TYPE_KEY, FEATURE_TYPE_VALUE);
-        feature.setProperty(FEATURE_GEOMETRY_PATH, path);
-        return feature;
-    }
-
     @Override
     public Map<String, Object> getRawDoc(CollectionReference collectionReference, String identifier, String[] includes) throws ArlasException {
         return new ElasticDocument(client).getSource(collectionReference, identifier, includes);
     }
 
-    private void paginate(Page page, CollectionReference collectionReference, ElasticFluidSearch fluidSearch) throws ArlasException {
-        setPageSizeAndFrom(page, fluidSearch);
-        searchAfterPage(page, collectionReference.params.idPath, fluidSearch);
-        if (page != null) {
-            if (page.before != null) {
-                Page newPage = page;
-                newPage.sort = Arrays.stream(page.sort.split(","))
-                        .map(field -> field.startsWith("-") ? field.substring(1) : "-".concat(field)).collect(Collectors.joining(","));
-                sortPage(newPage, fluidSearch);
-            } else {
-                sortPage(page, fluidSearch);
-            }
-        }
-    }
-
-    protected void setPageSizeAndFrom(Page page, ElasticFluidSearch fluidSearch) throws ArlasException {
-        if (page != null) {
-            if (page.size == null) {
-                page.size = SEARCH_DEFAULT_PAGE_SIZE;
-            }
-            if (page.from == null) {
-                page.from = SEARCH_DEFAULT_PAGE_FROM;
-            }
-            CheckParams.checkPageSize(page);
-            CheckParams.checkPageFrom(page);
-            fluidSearch.filterSize(page.size, page.from);
-        }
-    }
-
-    protected void searchAfterPage(Page page, String idCollectionField, ElasticFluidSearch fluidSearch) throws ArlasException {
-        if (page != null && page.after != null) {
-            CheckParams.checkPageAfter(page, idCollectionField);
-            fluidSearch = fluidSearch.searchAfter(page.after);
-        }
-        if (page != null && page.before != null) {
-            CheckParams.checkPageAfter(page, idCollectionField);
-            fluidSearch.searchAfter(page.before);
-        }
-    }
 
     @Override
     public AggregationResponse aggregate(CollectionReference collectionReference,
@@ -403,7 +336,7 @@ private AggregationResponse formatAggregationResult(MultiBucketsAggregation aggr
                                 returnedGeometry.reference = g.value();
                                 returnedGeometry.isRaw = false;
                                 if (g.isCellAgg()) {
-                                    returnedGeometry.geometry = createPolygonFromRectangle(GeohashUtils.decodeBoundary(element.keyAsString.toString(), SpatialContext.GEO));
+                                    returnedGeometry.geometry = createPolygonFromGeohash(element.keyAsString.toString());
                                 } else {
                                     returnedGeometry.geometry = new Point(geoPoint.getLon(), geoPoint.getLat());
                                 }
@@ -599,18 +532,6 @@ private GeoPoint getTileCentre(BoundingBox bbox) {
         return new GeoPoint(lat, lon);
     }
 
-    private Polygon createBox(BoundingBox bbox) {
-        Polygon box = new Polygon();
-        List<LngLatAlt> bounds = new ArrayList<>();
-        bounds.add(new LngLatAlt(bbox.getWest(), bbox.getNorth()));
-        bounds.add(new LngLatAlt(bbox.getEast(), bbox.getNorth()));
-        bounds.add(new LngLatAlt(bbox.getEast(), bbox.getSouth()));
-        bounds.add(new LngLatAlt(bbox.getWest(), bbox.getSouth()));
-        box.add(bounds);
-
-        return box;
-    }
-
     private Polygon createBox(GeoBounds subAggregation) {
         Polygon box = new Polygon();
         GeoPoint topLeft = subAggregation.topLeft();
@@ -624,15 +545,4 @@ private Polygon createBox(GeoBounds subAggregation) {
 
         return box;
     }
-
-    private Polygon createPolygonFromRectangle(Rectangle rectangle) {
-        Polygon polygon = new Polygon();
-        List<LngLatAlt> bounds = new ArrayList<>();
-        bounds.add(new LngLatAlt(rectangle.getMinX(), rectangle.getMaxY()));
-        bounds.add(new LngLatAlt(rectangle.getMaxX(), rectangle.getMaxY()));
-        bounds.add(new LngLatAlt(rectangle.getMaxX(), rectangle.getMinY()));
-        bounds.add(new LngLatAlt(rectangle.getMinX(), rectangle.getMinY()));
-        polygon.add(bounds);
-        return polygon;
-    }
 }
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/elastic/core/ElasticFluidSearch.java b/arlas-core/src/main/java/io/arlas/server/impl/elastic/services/ElasticFluidSearch.java
similarity index 98%
rename from arlas-core/src/main/java/io/arlas/server/impl/elastic/core/ElasticFluidSearch.java
rename to arlas-core/src/main/java/io/arlas/server/impl/elastic/services/ElasticFluidSearch.java
index 657111a0d..b782f580c 100644
--- a/arlas-core/src/main/java/io/arlas/server/impl/elastic/core/ElasticFluidSearch.java
+++ b/arlas-core/src/main/java/io/arlas/server/impl/elastic/services/ElasticFluidSearch.java
@@ -17,8 +17,7 @@
  * under the License.
  */
 
-package io.arlas.server.impl.elastic.core;
-
+package io.arlas.server.impl.elastic.services;
 
 import io.arlas.server.app.ArlasServerConfiguration;
 import io.arlas.server.exceptions.*;
@@ -27,10 +26,7 @@
 import io.arlas.server.managers.CollectionReferenceManager;
 import io.arlas.server.model.CollectionReference;
 import io.arlas.server.model.enumerations.*;
-import io.arlas.server.model.request.Aggregation;
-import io.arlas.server.model.request.Expression;
-import io.arlas.server.model.request.Metric;
-import io.arlas.server.model.request.MultiValueFilter;
+import io.arlas.server.model.request.*;
 import io.arlas.server.model.response.FieldType;
 import io.arlas.server.model.response.TimestampType;
 import io.arlas.server.services.FluidSearchService;
@@ -77,21 +73,21 @@ public class ElasticFluidSearch extends FluidSearchService {
     private SearchSourceBuilder searchSourceBuilder;
     private BoolQueryBuilder boolQueryBuilder;
 
-    public ElasticFluidSearch() {
-        super();
+    public ElasticFluidSearch(CollectionReference collectionReference) {
+        super(collectionReference);
+        request = new SearchRequest(collectionReference.params.indexName);
         boolQueryBuilder = QueryBuilders.boolQuery();
         searchSourceBuilder = new SearchSourceBuilder();
     }
 
+    public ElasticClient getClient() {
+        return client;
+    }
+
     public ElasticFluidSearch setClient(ElasticClient client) {
         this.client = client;
         return this;
     }
-    @Override
-    public void setCollectionReference(CollectionReference collectionReference) {
-        super.setCollectionReference(collectionReference);
-        request = new SearchRequest(collectionReference.params.indexName);
-    }
 
     public BoolQueryBuilder getBoolQueryBuilder() {
         return boolQueryBuilder;
@@ -101,7 +97,7 @@ public SearchResponse exec() throws ArlasException {
         searchSourceBuilder.query(boolQueryBuilder);
         searchSourceBuilder.trackTotalHits(true);
 
-        Pair<String[], String[]> includeExclude = computeIncludeExclude();
+        Pair<String[], String[]> includeExclude = computeIncludeExclude(false);
         searchSourceBuilder = searchSourceBuilder.fetchSource(includeExclude.getLeft(), includeExclude.getRight());
 
         //Get Elasticsearch response
@@ -112,7 +108,8 @@ public SearchResponse exec() throws ArlasException {
         return result;
     }
 
-    public ElasticFluidSearch filter(MultiValueFilter<Expression> f, String dateFormat) throws ArlasException {
+    @Override
+    public FluidSearchService filter(MultiValueFilter<Expression> f, String dateFormat) throws ArlasException {
         BoolQueryBuilder orBoolQueryBuilder = QueryBuilders.boolQuery();
         for (Expression fFilter : f) {
             orBoolQueryBuilder = orBoolQueryBuilder
@@ -177,7 +174,7 @@ private BoolQueryBuilder filter(Expression expression, String dateFormat) throws
                 ret = ret.filter(ltRangeQuery);
                 break;
             case like:
-                //TODO: if field type is fullText, use matchPhraseQuery instead of regexQuery
+                // TODO: if field type is fullText, use matchPhraseQuery instead of regexQuery
                 ret = ret
                         .filter(QueryBuilders.regexpQuery(field, ".*" + value + ".*"));
                 break;
@@ -289,7 +286,8 @@ protected RangeQueryBuilder getRangeQueryBuilder(String field, String value, Str
 
     }
 
-    public ElasticFluidSearch filterQ(MultiValueFilter<String> q) throws ArlasException {
+    @Override
+    public FluidSearchService filterQ(MultiValueFilter<String> q) throws ArlasException {
         BoolQueryBuilder orBoolQueryBuilder = QueryBuilders.boolQuery();
         for (String qFilter : q) {
             String operands[] = qFilter.split(":",2);
@@ -385,13 +383,14 @@ public GeoShapeQueryBuilder filterGIntersect(String field, String geometry) thro
         }
     }
 
-    public ElasticFluidSearch filterSize(Integer size, Integer from) {
+    @Override
+    public FluidSearchService filterSize(Integer size, Integer from) {
         searchSourceBuilder = searchSourceBuilder.size(size).from(from);
         return this;
     }
 
-
-    public ElasticFluidSearch searchAfter(String after) {
+    @Override
+    public FluidSearchService searchAfter(Page page, String after) {
         searchSourceBuilder = searchSourceBuilder.searchAfter(after.split(","));
         return this;
     }
@@ -470,7 +469,7 @@ public FluidSearchService aggregate(List<Aggregation> aggregations, Boolean isGe
         return this;
     }
 
-    public ElasticFluidSearch getFieldRange(String field) {
+    public FluidSearchService getFieldRange(String field) {
         boolQueryBuilder = boolQueryBuilder.filter(QueryBuilders.existsQuery(field));
         MinAggregationBuilder minAggregationBuilder = AggregationBuilders.min(FIELD_MIN_VALUE).field(field);
         MaxAggregationBuilder maxAggregationBuilder = AggregationBuilders.max(FIELD_MAX_VALUE).field(field);
@@ -478,7 +477,8 @@ public ElasticFluidSearch getFieldRange(String field) {
         return this;
     }
 
-    public ElasticFluidSearch compute(String field, ComputationEnum metric) {
+    @Override
+    public FluidSearchService compute(String field, ComputationEnum metric) {
         boolQueryBuilder = boolQueryBuilder.filter(QueryBuilders.existsQuery(field));
         switch (metric) {
             case AVG:
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/AggregatedGeoSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/AggregatedGeoSelectClause.java
new file mode 100644
index 000000000..cfd0640f6
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/AggregatedGeoSelectClause.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+public class AggregatedGeoSelectClause extends MetricSelectClause {
+
+    public AggregatedGeoSelectClause(String columnName, String asName, String function) {
+        super(columnName, asName, function);
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/AggregationSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/AggregationSelectClause.java
new file mode 100644
index 000000000..152b87f4f
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/AggregationSelectClause.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+import static io.arlas.server.impl.jdbi.model.SelectRequest.*;
+import static io.arlas.server.utils.StringUtil.concat;
+
+public abstract class AggregationSelectClause extends SelectClause {
+    public String asNameCount;
+
+    protected AggregationSelectClause(String columnName, String asName) {
+        super(columnName, asName);
+    }
+
+    @Override
+    public void setNames(String columnName, String asName) {
+        this.columnName = columnName;
+        this.asName = concat(asName, US, QKEY);
+        this.asNameCount = concat(asName, US, QCOUNT);
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/DateAggregationSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/DateAggregationSelectClause.java
new file mode 100644
index 000000000..2a800f682
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/DateAggregationSelectClause.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+import io.arlas.server.exceptions.NotAllowedException;
+import io.arlas.server.model.enumerations.UnitEnum;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class DateAggregationSelectClause extends AggregationSelectClause {
+    private static final List<UnitEnum> biggerThanWeek = Arrays.asList(UnitEnum.year, UnitEnum.quarter, UnitEnum.month, UnitEnum.week);
+    public UnitEnum intervalUnit;
+    public Integer intervalValue;
+    public String format;
+
+    public DateAggregationSelectClause(String columnName, String asName, UnitEnum intervalUnit, Integer intervalValue, String format) throws NotAllowedException {
+        super(columnName, asName);
+        this.intervalUnit = intervalUnit;
+        this.intervalValue = intervalValue;
+        this.format = format;
+
+        if (biggerThanWeek.contains(intervalUnit)
+                && intervalValue > 1) {
+            throw new NotAllowedException("The size must be equal to 1 for the unit " + intervalUnit + ".");
+        }
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/GeohashAggregationSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/GeohashAggregationSelectClause.java
new file mode 100644
index 000000000..f82a99844
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/GeohashAggregationSelectClause.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+public class GeohashAggregationSelectClause extends AggregationSelectClause {
+    public String precision;
+
+    public GeohashAggregationSelectClause(String columnName, String asName, String precision) {
+        super(columnName, asName);
+        this.precision = precision;
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/GeotileAggregationSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/GeotileAggregationSelectClause.java
new file mode 100644
index 000000000..1cdafa982
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/GeotileAggregationSelectClause.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+public class GeotileAggregationSelectClause extends AggregationSelectClause {
+    public String precision;
+
+    public GeotileAggregationSelectClause(String columnName, String asName, String precision) {
+        super(columnName, asName);
+        this.precision = precision;
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/HistogramAggregationSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/HistogramAggregationSelectClause.java
new file mode 100644
index 000000000..f6a97a531
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/HistogramAggregationSelectClause.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+public class HistogramAggregationSelectClause extends AggregationSelectClause {
+    public Double intervalValue;
+
+    public HistogramAggregationSelectClause(String columnName, String asName, Double intervalValue) {
+        super(columnName, asName);
+        this.intervalValue = intervalValue;
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/MetricSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/MetricSelectClause.java
new file mode 100644
index 000000000..b27bb0d99
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/MetricSelectClause.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+import io.arlas.server.model.enumerations.CollectionFunction;
+
+public class MetricSelectClause extends SelectClause {
+    public String function;
+
+    public MetricSelectClause(String columnName, String asName, String function) {
+        super(columnName, asName);
+        this.function = function;
+    }
+
+    public boolean isGeo() {
+        return function.equalsIgnoreCase(CollectionFunction.GEOBBOX.name())
+                || function.equalsIgnoreCase(CollectionFunction.GEOCENTROID.name());
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/SelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/SelectClause.java
new file mode 100644
index 000000000..917e8c4ab
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/SelectClause.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+public abstract class SelectClause {
+    public String columnName;
+    public String asName;
+
+    protected SelectClause(String columnName, String asName) {
+        this.setNames(columnName, asName);
+    }
+
+    public void setNames(String columnName, String asName) {
+        this.columnName = columnName;
+        this.asName = asName;
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/TermAggregationSelectClause.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/TermAggregationSelectClause.java
new file mode 100644
index 000000000..5969cc34d
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/clause/TermAggregationSelectClause.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.clause;
+
+public class TermAggregationSelectClause extends AggregationSelectClause {
+
+    public TermAggregationSelectClause(String columnName, String asName) {
+        super(columnName, asName);
+    }
+}
+
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao.java
new file mode 100644
index 000000000..671e8780c
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.dao;
+
+import io.arlas.server.impl.jdbi.model.ColumnDefinition;
+import io.arlas.server.model.CollectionReference;
+import org.jdbi.v3.json.Json;
+import org.jdbi.v3.sqlobject.config.RegisterFieldMapper;
+import org.jdbi.v3.sqlobject.customizer.BindList;
+import org.jdbi.v3.sqlobject.locator.UseClasspathSqlLocator;
+import org.jdbi.v3.sqlobject.statement.SqlQuery;
+import org.jdbi.v3.sqlobject.statement.SqlUpdate;
+import org.jdbi.v3.sqlobject.transaction.Transaction;
+
+import java.util.List;
+import java.util.Optional;
+
+@UseClasspathSqlLocator
+public interface CollectionReferenceDao {
+    @SqlUpdate
+    void createTable();
+
+    @SqlQuery
+    @Json
+    List<CollectionReference> selectAll(String arlasIndex);
+
+    @SqlQuery
+    @Json
+    Optional<CollectionReference> selectOne(String arlasIndex, String collectionName);
+
+    @SqlUpdate
+    boolean delete(String arlasIndex, String collectionName);
+
+    @SqlUpdate
+    void insert(String arlasIndex, String collectionName, String indexName, @Json CollectionReference description);
+
+    @Transaction
+    default void deleteAndInsert(String arlasIndex, String collectionName, String indexName, @Json CollectionReference description) {
+        delete(arlasIndex, collectionName);
+        insert(arlasIndex, collectionName, indexName, description);
+    }
+
+    @SqlQuery
+    @RegisterFieldMapper(ColumnDefinition.class)
+    List<ColumnDefinition> describeOne(String tableName);
+
+    @SqlQuery
+    @RegisterFieldMapper(ColumnDefinition.class)
+    List<ColumnDefinition> describeAll(String arlasIndex);
+
+    @SqlQuery
+    @RegisterFieldMapper(ColumnDefinition.class)
+    List<ColumnDefinition> describeGeographyType(@BindList("tableNames") List<String> indexNames);
+
+    @SqlQuery
+    @RegisterFieldMapper(ColumnDefinition.class)
+    List<ColumnDefinition> describeGeometryType(@BindList("tableNames") List<String> indexNames);
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/dao/DataDao.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/dao/DataDao.java
new file mode 100644
index 000000000..62bfc1f9b
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/dao/DataDao.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.dao;
+
+import org.jdbi.v3.core.mapper.MapMapper;
+import org.jdbi.v3.core.result.ResultIterator;
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
+import org.jdbi.v3.sqlobject.customizer.BindMap;
+import org.jdbi.v3.sqlobject.customizer.Define;
+import org.jdbi.v3.sqlobject.customizer.FetchSize;
+import org.jdbi.v3.sqlobject.statement.SqlQuery;
+
+import java.util.Map;
+
+public interface DataDao {
+    @SqlQuery("select count(*) from <index_name> <where_clause>")
+    int count(@Define("index_name") String index_name,
+              @Define("where_clause") String where_clause,
+              @BindMap Map<String, Object> params);
+
+    @SqlQuery("select <select_clause> from <index_name> <where_clause> <group_clause> <order_clause> <limit_clause>")
+    @RegisterRowMapper(MapMapper.class)
+    @FetchSize(1000)
+    ResultIterator<Map<String,Object>> select(@Define("index_name") String indexName,
+                                              @Define("select_clause") String selectClause,
+                                              @Define("where_clause") String whereClause,
+                                              @Define("group_clause") String groupClause,
+                                              @Define("order_clause") String orderClause,
+                                              @Define("limit_clause") String limitClause,
+                                              @BindMap Map<String, Object> params);
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ClauseParam.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ClauseParam.java
new file mode 100644
index 000000000..3a4c40451
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ClauseParam.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.model;
+
+public class ClauseParam {
+    public String field;
+    public Object value;
+
+    public ClauseParam(String field, Object value) {
+        this.field = field;
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return "ClauseParam{'" + field + "'='" + value + "'}";
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ColumnDefinition.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ColumnDefinition.java
new file mode 100644
index 000000000..6372b3f33
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ColumnDefinition.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.model;
+
+public class ColumnDefinition {
+    public String tableName; // is the same as index_name in ES
+    public String columnName; // is a field in ES but flatten
+    public String dataType;
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ColumnQualifier.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ColumnQualifier.java
new file mode 100644
index 000000000..779f9c621
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/ColumnQualifier.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.model;
+
+import io.arlas.server.model.enumerations.AggregatedGeometryEnum;
+import io.arlas.server.model.enumerations.CollectionFunction;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static io.arlas.server.impl.jdbi.model.SelectRequest.US;
+import static io.arlas.server.services.FluidSearchService.AGGREGATED_GEOMETRY_SUFFIX;
+import static io.arlas.server.utils.StringUtil.concat;
+
+public class ColumnQualifier {
+    private static List<String> metrics = Arrays.asList(
+            CollectionFunction.AVG.name().toLowerCase(),
+            CollectionFunction.CARDINALITY.name().toLowerCase(),
+            CollectionFunction.MAX.name().toLowerCase(),
+            CollectionFunction.MIN.name().toLowerCase(),
+            CollectionFunction.SUM.name().toLowerCase(),
+            CollectionFunction.GEOCENTROID.name().toLowerCase(),
+            CollectionFunction.GEOBBOX.name().toLowerCase()
+            );
+
+    private static List<String> geoMetrics = Arrays.asList(
+            CollectionFunction.GEOCENTROID.name().toLowerCase(),
+            CollectionFunction.GEOBBOX.name().toLowerCase()
+            );
+
+    private static String bbox = concat(AggregatedGeometryEnum.BBOX.value(), AGGREGATED_GEOMETRY_SUFFIX).replace(US, "");
+    private static String centroid = concat(AggregatedGeometryEnum.CENTROID.value(), AGGREGATED_GEOMETRY_SUFFIX).replace(US, "");
+
+    private static List<String> aggGeos = Arrays.asList(
+            bbox,
+            centroid,
+            concat(AggregatedGeometryEnum.GEOHASH.value(), AGGREGATED_GEOMETRY_SUFFIX).replace(US, ""),
+            concat(AggregatedGeometryEnum.GEOHASH_CENTER.value(), AGGREGATED_GEOMETRY_SUFFIX).replace(US, ""),
+            concat(AggregatedGeometryEnum.CELL.value(), AGGREGATED_GEOMETRY_SUFFIX).replace(US, ""),
+            concat(AggregatedGeometryEnum.CELLCENTER.value(), AGGREGATED_GEOMETRY_SUFFIX).replace(US, "")
+            );
+
+    public static String getQualifier(CollectionFunction fct, String field) {
+        return concat(fct.name().toLowerCase(),
+                US, field.replace(".", ""));
+    }
+
+    public static String getQualifier(AggregatedGeometryEnum ag, String field) {
+        return concat(concat(ag.value(), AGGREGATED_GEOMETRY_SUFFIX).replace(US, ""),
+                US, field.replace(".", ""));
+    }
+
+    public static String[] splitQualifier(String qualifier) {
+        return qualifier.split(US, 2);
+    }
+    
+    public static boolean isMetric(String name) {
+        return metrics.contains(name.split(US)[0]);
+    }
+
+    public static boolean isGeoMetric(String name) {
+        return geoMetrics.contains(name.split(US)[0]);
+    }
+
+    public static boolean isGeoMetricBbox(String name) {
+        return CollectionFunction.GEOBBOX.name().toLowerCase().equals(name.split(US)[0]);
+    }
+
+    public static boolean isGeoMetricCentroid(String name) {
+        return CollectionFunction.GEOCENTROID.name().toLowerCase().equals(name.split(US)[0]);
+    }
+
+    public static boolean isAggregatedGeometry(String name) {
+        return aggGeos.contains(name.split(US)[0]);
+    }
+
+    public static boolean isAggregatedGeoBbox(String name) {
+        return bbox.equals(name.split(US)[0]);
+    }
+
+    public static boolean isAggregatedGeoCentroid(String name) {
+        return centroid.equals(name.split(US)[0]);
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/JdbiAggregationResult.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/JdbiAggregationResult.java
new file mode 100644
index 000000000..ca12f86c2
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/JdbiAggregationResult.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.model;
+
+import org.jdbi.v3.core.result.ResultIterable;
+
+import java.util.*;
+
+import static io.arlas.server.impl.jdbi.model.SelectRequest.QKEY;
+
+public class JdbiAggregationResult {
+    public static final String AGG_NAME = "aggName";
+    private Map<String, Map> results = new LinkedHashMap<>();
+
+    public JdbiAggregationResult(int maxDepth, ResultIterable<Map<String, Object>> rows) {
+        rows.forEach(row -> {
+            final Map<String, Map<String, Object>> dataPerAgg = new TreeMap<>();
+            row.forEach((colName, value) -> {
+                String[] p = colName.split("_", 3); // aggName_depth_qualifier
+                Map<String, Object> colValues = Optional.ofNullable(dataPerAgg.get(p[1])).orElse(new HashMap<>());
+                colValues.put(p[2], value);
+                colValues.put(AGG_NAME, p[0]);
+                dataPerAgg.put(p[1], colValues);
+            }); // map { aggIndex -> map { qualifier -> value } }
+
+            Map tmpResults = results;
+            for (int i = 0; i < maxDepth; i++) {
+                Map values = dataPerAgg.get(String.valueOf(i));
+                Map bucket = (Map) Optional.ofNullable(tmpResults.get(values.get(QKEY))).orElse(new LinkedHashMap());
+                bucket.put("values", values);
+                Map subAgg = (Map) Optional.ofNullable(bucket.get("subAgg")).orElse(new LinkedHashMap());
+                bucket.put("subAgg", subAgg);
+                tmpResults.put(values.get(QKEY), bucket);
+                tmpResults = subAgg;
+            }
+        });
+    }
+
+    public Map getResults() {
+        return results;
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/RequestFactory.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/RequestFactory.java
new file mode 100644
index 000000000..4fd63cb5e
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/RequestFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.model;
+
+public interface RequestFactory {
+    SelectRequest getNewRequest();
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/SelectRequest.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/SelectRequest.java
new file mode 100644
index 000000000..4e84e8a21
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/model/SelectRequest.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.model;
+
+import io.arlas.server.app.ArlasBaseConfiguration;
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.impl.jdbi.clause.*;
+import io.arlas.server.model.enumerations.OperatorEnum;
+import io.arlas.server.services.FluidSearchService;
+import org.locationtech.jts.geom.Geometry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static io.arlas.server.utils.StringUtil.concat;
+
+public abstract class SelectRequest {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SelectRequest.class);
+    public static final String COLON = ":";
+    public static final String COMMA = ",";
+    public static final String QUOTE = "\"";
+    public static final String LP = "(";
+    public static final String RP = ")";
+    public static final String OR = " or ";
+    public static final String AND = " and ";
+    public static final String IN = " in ";
+    public static final String NOTIN = " not in ";
+    public static final String NOT = " not ";
+    public static final String GT = ">";
+    public static final String GTE = ">=";
+    public static final String LT = "<";
+    public static final String LTE = "<=";
+    public static final String EQ = "=";
+    public static final String NE = "<>";
+    public static final String DIV = "/";
+    public static final String LIKE = " like ";
+    public static final String AVG = "avg";
+    public static final String SUM = "sum";
+    public static final String MIN = "min";
+    public static final String MAX = "max";
+    public static final String COUNT = "count";
+    public static final String AS = " as ";
+    public static final String WHERE = " where ";
+    public static final String LIMIT = " limit ";
+    public static final String OFFSET = " offset ";
+    public static final String GROUP_BY = " group by ";
+    public static final String ORDER_BY = " order by ";
+    public static final String DESC = " desc";
+    public static final String ASC = " asc";
+    public static final String ST_ASTEXT = "st_astext";
+    public static final String ST_COVERS = "st_covers";
+    public static final String ST_GEOHASH = "st_geohash";
+    public static final String ST_EXTENT = "st_extent";
+    public static final String ST_CENTROID = "st_centroid";
+    public static final String ST_DISTANCE = "st_distance";
+    public static final String ST_INTERSECTS = "st_intersects";
+    public static final String QKEY = "key";
+    public static final String QCOUNT = "count";
+    public static final String US /* UnderScore */ = ArlasBaseConfiguration.FLATTEN_CHAR;
+    public static final String SRID = "SRID=4326;";
+    public static final String IS_NOT_NULL = " is not null";
+    public static final String FLOOR_LP = "FLOOR(";
+    public static final String FLOOR_RP = ") * ";
+
+    protected final LinkedList<String> selectClauses = new LinkedList<>();
+    protected final List<String> whereAndConditions = new ArrayList<>();
+    protected final Map<String, ClauseParam> whereParams = new HashMap<>();
+    protected final List<String> groupBy = new ArrayList<>();
+    protected final List<String> orderBy = new ArrayList<>();
+    public Integer limit = 10000; // default limit as in ES
+    public Integer offset = 0;
+
+    public SelectRequest() {
+    }
+
+    // Adds the value in a map and returns ":index" (e.g. ":1", ":2"...) with the "index" of the inserted value in the map
+    public String mapWhereParam(ClauseParam v) {
+        String k = String.valueOf(whereParams.size());
+        whereParams.put(k, v);
+        return concat(COLON, k);
+    }
+
+    public Map<String, ClauseParam> getWhereParams() {
+        LOGGER.debug(concat("getWhereParams() returns: ", whereParams.toString()));
+        return whereParams;
+    }
+
+    public void addWhereFieldIsNotNull(String field) {
+        whereAndConditions.add(concat(field, IS_NOT_NULL));
+    }
+
+    public void addOrWhereClauses(List<String> orConditions) {
+        whereAndConditions.add(concat(LP, formatOrClauses(orConditions), RP));
+    }
+
+    public String formatOrClauses(List<String> orConditions) {
+        return String.join(OR, orConditions);
+    }
+
+    public String getWhereClause() {
+        String where = whereAndConditions.isEmpty() ? "" : concat(WHERE, String.join(AND, whereAndConditions));
+        LOGGER.debug(concat("getWhereCondition() returns: ", where));
+        return where;
+    }
+
+    public String getGroupClause() {
+        String ret = groupBy.isEmpty() ? "" : concat(GROUP_BY, String.join(COMMA, groupBy));
+        LOGGER.debug(concat("getGroupClause() returns: ", ret));
+        return ret;
+    }
+
+    public String getLimitClause() {
+        String ret = limit == 0 ? "" : concat(LIMIT, limit.toString(), OFFSET, offset.toString());
+        LOGGER.debug(concat("getLimitClause() returns: ", ret));
+        return ret;
+    }
+
+    public void addOrderClause(String field, boolean asc) {
+        orderBy.add(concat(field, asc ? ASC : DESC));
+        LOGGER.debug(concat("addOrderClause() : ", orderBy.get(orderBy.size()-1)));
+    }
+
+    public String getOrderClause() {
+        String ret = orderBy.isEmpty() ? "" : concat(ORDER_BY, String.join(COMMA, orderBy));
+        LOGGER.debug(concat("getOrderClause() returns: ", ret));
+        return ret;
+    }
+
+    public String getSelectClause() {
+        String select = selectClauses.isEmpty() ? "" : String.join(COMMA, selectClauses);
+        LOGGER.debug(concat("getSelectPredicates() returns: ", select));
+        return select;
+    }
+
+    public abstract String getDateFieldAsEpochMilli(String field);
+
+    // ------
+
+    public void addSelectClause(MetricSelectClause c) {
+        selectClauses.add(concat(c.function, LP, c.columnName, RP, AS, c.asName));
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+    public abstract void addSelectClause(AggregatedGeoSelectClause c);
+
+    public abstract void addSelectClause(DateAggregationSelectClause c);
+
+    public abstract void addSelectClause(GeotileAggregationSelectClause c);
+
+    public abstract void addSelectClause(FluidSearchService fluidSearchService, String[] includes) throws ArlasException;
+
+    public void addSelectClause(GeohashAggregationSelectClause c) {
+        selectClauses.add(concat(ST_GEOHASH, LP, c.columnName, COMMA, c.precision, RP, AS, c.asName,
+                COMMA, COUNT, LP, c.columnName, RP, AS, c.asNameCount));
+        groupBy.add(c.asName);
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+
+    public void addSelectClause(HistogramAggregationSelectClause c) {
+        // select FLOOR(field/interval)*interval
+        selectClauses.add(concat(FLOOR_LP, c.columnName, DIV, String.valueOf(c.intervalValue), FLOOR_RP, String.valueOf(c.intervalValue), AS, c.asName,
+                COMMA, COUNT, LP, c.columnName, RP, AS, c.asNameCount));
+        groupBy.add(c.asName);
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+
+    public void addSelectClause(TermAggregationSelectClause c) {
+        // select field, count(field)
+        selectClauses.add(concat(c.columnName, AS, c.asName, COMMA, COUNT, LP, c.columnName, RP, AS, c.asNameCount));
+        groupBy.add(c.asName);
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+
+    // ------
+
+    public String formatInCondition(String field, String[] fieldValues) {
+        if (fieldValues.length > 1) {
+            return concat(field, IN, LP, Arrays.stream(fieldValues)
+                    .map(v -> mapWhereParam(new ClauseParam(field, v))).collect(Collectors.joining(COMMA)), RP);
+        } else {
+            return concat(field, EQ, mapWhereParam(new ClauseParam(field, fieldValues[0])));
+        }
+    }
+
+    public String formatNotInCondition(String field, String[] fieldValues) {
+        return concat(field, NOTIN, LP, Arrays.stream(fieldValues).map(v -> mapWhereParam(new ClauseParam(field, v))).collect(Collectors.joining(COMMA)), RP);
+    }
+
+    public String formatGTECondition(String field, String value) {
+        return concat(field, GTE, mapWhereParam(new ClauseParam(field, value)));
+    }
+
+    public String formatGTCondition(String field, String value) {
+        return concat(field, GT, mapWhereParam(new ClauseParam(field, value)));
+    }
+
+    public String formatLTECondition(String field, String value) {
+        return concat(field, LTE, mapWhereParam(new ClauseParam(field, value)));
+    }
+
+    public String formatLTCondition(String field, String value) {
+        return concat(field, LT, mapWhereParam(new ClauseParam(field, value)));
+    }
+
+    public String formatLikeCondition(String field, String value) {
+        // !! WARNING : never a good thing on a SQL database !! To be discussed
+        return concat(field, "::text", LIKE, mapWhereParam(new ClauseParam("", concat("%", value, "%"))));
+    }
+
+    public String formatRangeCondition(String field, boolean incMin, String min, boolean incMax, String max) {
+        return concat(LP, field, incMin ? GTE : GT, mapWhereParam(new ClauseParam(field, min)),
+                AND, field, incMax ? LTE : LT, mapWhereParam(new ClauseParam(field, max)), RP);
+    }
+
+    public String formatGeoCondition(String field, OperatorEnum op, Geometry jtsGeo) throws ArlasException {
+        switch (op) {
+            case notintersects:
+                return formatGeoIntersectCondition(field, jtsGeo, true);
+            case notwithin:
+                return formatGeoWithinCondition(field, jtsGeo, true);
+            case intersects:
+                return formatGeoIntersectCondition(field, jtsGeo, false);
+            case within:
+                return formatGeoWithinCondition(field, jtsGeo, false);
+            default:
+                throw new ArlasException(op + " op on geo field '" + field + " is not supported");
+        }
+    }
+
+    public String formatGeoWithinCondition(String field, Geometry jtsGeo, boolean not) throws ArlasException {
+        // ST_COVERS includes the borders while ST_WITHIN excludes them
+        return concat(not ? NOT : "", ST_COVERS, LP, mapWhereParam(new ClauseParam(field, jtsToDBgeo(jtsGeo))), COMMA, field, RP);
+    }
+
+    public String formatGeoIntersectCondition(String field, Geometry jtsGeo, boolean not) throws ArlasException {
+        return concat(not ? NOT : "", ST_INTERSECTS, LP, mapWhereParam(new ClauseParam(field, jtsToDBgeo(jtsGeo))), COMMA, field, RP);
+    }
+
+    public String formatGeoDistance(String field, Geometry jtsGeo) throws ArlasException {
+        return concat(ST_DISTANCE, LP, field, mapWhereParam(new ClauseParam(field, jtsToDBgeo(jtsGeo))), RP);
+    }
+
+    protected abstract Object jtsToDBgeo(Geometry jtsGeo) throws ArlasException;
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/postgis/PostgisRequestFactory.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/postgis/PostgisRequestFactory.java
new file mode 100644
index 000000000..5d4c14ae1
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/postgis/PostgisRequestFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.postgis;
+
+import io.arlas.server.impl.jdbi.model.RequestFactory;
+import io.arlas.server.impl.jdbi.model.SelectRequest;
+
+public class PostgisRequestFactory implements RequestFactory {
+    public PostgisRequestFactory() {
+    }
+
+    @Override
+    public SelectRequest getNewRequest() {
+        return new PostgisSelectRequest();
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/postgis/PostgisSelectRequest.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/postgis/PostgisSelectRequest.java
new file mode 100644
index 000000000..78dbd6e77
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/postgis/PostgisSelectRequest.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.postgis;
+
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.impl.jdbi.clause.AggregatedGeoSelectClause;
+import io.arlas.server.impl.jdbi.clause.DateAggregationSelectClause;
+import io.arlas.server.impl.jdbi.clause.GeotileAggregationSelectClause;
+import io.arlas.server.impl.jdbi.model.ClauseParam;
+import io.arlas.server.impl.jdbi.model.SelectRequest;
+import io.arlas.server.model.enumerations.AggregatedGeometryEnum;
+import io.arlas.server.services.FluidSearchService;
+import org.locationtech.jts.geom.Geometry;
+import org.postgis.PGgeometry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.arlas.server.utils.StringUtil.concat;
+
+public class PostgisSelectRequest extends SelectRequest {
+    private static final Logger LOGGER = LoggerFactory.getLogger(PostgisSelectRequest.class);
+    public static final String EPOCH_LP = "EXTRACT('epoch' FROM ";
+    public static final String EPOCH_RP_MILLI = ") * 1000";
+    public static final String G1 = "concat(";
+    public static final String G2 = ", '/', lon2Xtile(st_x(st_transform(";
+    public static final String G3 = ",4326)),";
+    public static final String G4 = "), '/', lat2Ytile(st_y(st_transform(";
+    public static final String G5 = "concat(";
+    public static final String S1 = "to_tsvector(";
+    public static final String S2 = ") @@ to_tsquery(";
+
+    public String getDateFieldAsEpochMilli(String field) {
+        return concat(EPOCH_LP, field, EPOCH_RP_MILLI);
+    }
+
+    @Override
+    public void addSelectClause(DateAggregationSelectClause c) {
+        String fct;
+        Integer i = c.intervalValue;
+        switch (c.intervalUnit) {
+            case year:
+            case quarter:
+            case month:
+            case week:
+                fct = concat(EPOCH_LP, " date_trunc('", c.intervalUnit.name(), "',", c.columnName, ")", EPOCH_RP_MILLI);
+                break;
+            case day:
+                i *= 24;
+            case hour:
+                i *= 60;
+            case minute:
+                i *= 60;
+            case second:
+                i *= 1000;
+            default:
+                fct = concat("FLOOR((", EPOCH_LP, c.columnName, EPOCH_RP_MILLI, " / ",
+                        String.valueOf(i), " )) * ", String.valueOf(i));
+        }
+        selectClauses.add(concat(fct, AS, c.asName, COMMA, COUNT, LP, c.columnName, RP, AS, c.asNameCount));
+        groupBy.add(c.asName);
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+
+    @Override
+    public void addSelectClause(GeotileAggregationSelectClause c) {
+        // concat(<z>, '/', lon2Xtile(st_x(st_transform(<mygeo>,4326)),<z>), '/', lat2Ytile(st_y(st_transform(<mygeo>,4326)),<z>))
+        selectClauses.add(concat(G1, c.precision, G2, c.columnName, G3, c.precision, G4, c.columnName, G3, c.precision, RP, RP, AS, c.asName,
+                COMMA, COUNT, LP, c.columnName, RP, AS, c.asNameCount));
+        groupBy.add(c.asName);
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+
+    @Override
+    public void addSelectClause(AggregatedGeoSelectClause c) {
+        if (AggregatedGeometryEnum.BBOX.value().equals(c.function)) {
+            //ST_Extent(mygeo)::geometry
+            selectClauses.add(concat(ST_ASTEXT, LP, ST_EXTENT, LP, c.columnName, RP, RP, AS, c.asName));
+        } else if (AggregatedGeometryEnum.CENTROID.value().equals(c.function)) {
+            //ST_Centroid(ST_Extent(mygeo))
+            selectClauses.add(concat(ST_ASTEXT, LP, ST_CENTROID, LP, ST_EXTENT, LP, c.columnName, RP, RP, RP, AS, c.asName));
+        }
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+
+    @Override
+    public void addSelectClause(FluidSearchService fluidSearchService, String[] includes) throws ArlasException {
+        List<String> selects = new ArrayList();
+        for (String include : includes) {
+            if (fluidSearchService.isGeoField(include)) {
+                selects.add(concat(ST_ASTEXT, LP, include, RP, AS, include));
+            } else {
+                selects.add(include);
+            }
+        }
+        selectClauses.add(String.join(COMMA, selects));
+        LOGGER.debug("Adding selectClause:" + selectClauses.getLast());
+    }
+
+    @Override
+    public String formatLikeCondition(String field, String value) {
+        // https://www.postgresql.org/docs/13/textsearch-intro.html
+        // https://www.postgresql.org/docs/13/textsearch-tables.html
+        // to_tsvector(field) @@ to_tsquery('q');
+        return concat(S1,field, S2, mapWhereParam(new ClauseParam("", value)), RP);
+    }
+        @Override
+    protected Object jtsToDBgeo(Geometry jtsGeo) throws ArlasException {
+        try {
+            return new PGgeometry(concat(SRID, jtsGeo.toText()));
+        } catch (SQLException e) {
+            throw new ArlasException("Could not create PGgeometry from JTS geometry", e);
+        }
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiCollectionReferenceService.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiCollectionReferenceService.java
new file mode 100644
index 000000000..527e4b762
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiCollectionReferenceService.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.service;
+
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.exceptions.InvalidParameterException;
+import io.arlas.server.exceptions.NotFoundException;
+import io.arlas.server.impl.jdbi.dao.CollectionReferenceDao;
+import io.arlas.server.impl.jdbi.model.ColumnDefinition;
+import io.arlas.server.managers.CacheManager;
+import io.arlas.server.model.CollectionReference;
+import io.arlas.server.model.response.FieldType;
+import io.arlas.server.services.CollectionReferenceService;
+import io.arlas.server.utils.ColumnFilterUtil;
+import org.jdbi.v3.core.Jdbi;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+public class JdbiCollectionReferenceService extends CollectionReferenceService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JdbiCollectionReferenceService.class);
+
+    final CollectionReferenceDao dao;
+
+    public JdbiCollectionReferenceService(Jdbi jdbi, String arlasIndex, CacheManager cacheManager) {
+        super(arlasIndex, cacheManager);
+        this.dao = jdbi.onDemand(CollectionReferenceDao.class);
+    }
+
+    @Override
+    public void initCollectionDatabase() {
+        LOGGER.info("Create table if not exists");
+        dao.createTable();
+    }
+
+    @Override
+    protected CollectionReference getCollectionReferenceFromDao(String ref) throws ArlasException {
+        return dao.selectOne(this.arlasIndex, ref).orElseThrow(() -> new NotFoundException("Collection " + ref + " not found."));
+    }
+
+    @Override
+    protected Map<String, LinkedHashMap> getMappingFromDao(String indexName) throws ArlasException {
+        List<ColumnDefinition> input = dao.describeOne(indexName);
+        input.addAll(dao.describeGeographyType(Collections.singletonList(indexName)));
+        input.addAll(dao.describeGeometryType(Collections.singletonList(indexName)));
+        Map<String, LinkedHashMap> mapping = getMappingFromColumnDefinition(input);
+        if (mapping.size() == 0) {
+            throw new NotFoundException("Table not found: " + indexName);
+        }
+        return mapping;
+    }
+
+    // @Override
+    protected Map<String, LinkedHashMap> getAllMappingsFromDao(String arlasIndex) {
+        List<ColumnDefinition> input = dao.describeAll(arlasIndex);
+        List<String> indexNames = input.stream().map(c -> c.tableName).distinct().collect(Collectors.toList());
+        input.addAll(dao.describeGeographyType(indexNames));
+        input.addAll(dao.describeGeometryType(indexNames));
+        return getMappingFromColumnDefinition(dao.describeAll(arlasIndex));
+    }
+
+    private Map<String, LinkedHashMap> getMappingFromColumnDefinition(List<ColumnDefinition> definition) {
+        Map<String, LinkedHashMap> mappings = new HashMap<>();
+        definition.forEach(c -> {
+            LinkedHashMap<String, Object> properties = mappings.get(c.tableName);
+            if (properties == null) {
+                properties = new LinkedHashMap<>();
+            }
+            properties.put(c.columnName, Collections.singletonMap("type", c.dataType));
+            mappings.put(c.tableName, properties);
+        });
+        return mappings;
+    }
+
+    @Override
+    public List<CollectionReference> getAllCollectionReferences(Optional<String> columnFilter) {
+        Set<String> allowedCollections = ColumnFilterUtil.getAllowedCollections(columnFilter);
+        List<CollectionReference> all = dao.selectAll(this.arlasIndex);
+        List<CollectionReference> result = new ArrayList<>();
+        for (CollectionReference cr : all) {
+            for (String c : allowedCollections) {
+                if ((c.endsWith("*") && cr.collectionName.startsWith(c.substring(0, c.indexOf("*"))))
+                        || cr.collectionName.equals(c)){
+                    result.add(cr);
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void deleteCollectionReference(String collectionName) throws ArlasException {
+        if (dao.delete(this.arlasIndex, collectionName)) {
+            cacheManager.removeCollectionReference(collectionName);
+        } else {
+            throw new NotFoundException("collection " + collectionName + " not found.");
+        }
+    }
+
+    @Override
+    protected void putCollectionReferenceWithDao(CollectionReference c) throws ArlasException {
+        if (c.params.indexName.contains("-") || c.params.indexName.contains(".")) {
+            throw new InvalidParameterException("index_name cannot contain '-' or '.' characters in SQL");
+        }
+        dao.deleteAndInsert(this.arlasIndex, c.collectionName, c.params.indexName, c);
+    }
+}
\ No newline at end of file
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiExploreService.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiExploreService.java
new file mode 100644
index 000000000..fa64d8a93
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiExploreService.java
@@ -0,0 +1,392 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.exceptions.NotImplementedException;
+import io.arlas.server.impl.jdbi.dao.DataDao;
+import io.arlas.server.impl.jdbi.model.ClauseParam;
+import io.arlas.server.impl.jdbi.model.JdbiAggregationResult;
+import io.arlas.server.impl.jdbi.model.RequestFactory;
+import io.arlas.server.impl.jdbi.model.SelectRequest;
+import io.arlas.server.model.CollectionReference;
+import io.arlas.server.model.enumerations.AggregatedGeometryEnum;
+import io.arlas.server.model.enumerations.ComputationEnum;
+import io.arlas.server.model.request.Aggregation;
+import io.arlas.server.model.request.MixedRequest;
+import io.arlas.server.model.request.RawGeometry;
+import io.arlas.server.model.request.Search;
+import io.arlas.server.model.response.*;
+import io.arlas.server.services.CollectionReferenceService;
+import io.arlas.server.services.ExploreService;
+import io.arlas.server.services.FluidSearchService;
+import io.arlas.server.utils.BoundingBox;
+import io.arlas.server.utils.GeoTileUtil;
+import io.arlas.server.utils.GeoUtil;
+import io.arlas.server.utils.ParamsParser;
+import org.apache.commons.collections.CollectionUtils;
+import org.geojson.*;
+import org.jdbi.v3.core.Jdbi;
+import org.jdbi.v3.core.result.ResultIterable;
+import org.jdbi.v3.core.result.ResultIterator;
+import org.locationtech.jts.io.geojson.GeoJsonWriter;
+import org.locationtech.spatial4j.context.SpatialContext;
+import org.locationtech.spatial4j.io.GeohashUtils;
+import org.locationtech.spatial4j.shape.Rectangle;
+
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static io.arlas.server.impl.jdbi.model.ColumnQualifier.*;
+import static io.arlas.server.impl.jdbi.model.SelectRequest.*;
+import static io.arlas.server.model.enumerations.ComputationEnum.GEOBBOX;
+import static io.arlas.server.model.enumerations.ComputationEnum.GEOCENTROID;
+import static io.arlas.server.services.FluidSearchService.*;
+
+public class JdbiExploreService extends ExploreService {
+    private static ObjectReader reader = new ObjectMapper().readerFor(GeoJsonObject.class);
+    private static GeoJsonWriter geoJsonWriter = new GeoJsonWriter();
+
+    final private DataDao dao;
+    final private RequestFactory requestFactory;
+
+    public JdbiExploreService(Jdbi jdbi, CollectionReferenceService collectionReferenceService,
+                              String baseUri, int arlasRestCacheTimeout, RequestFactory requestFactory) {
+        super(baseUri, arlasRestCacheTimeout, collectionReferenceService);
+        this.dao = jdbi.onDemand(DataDao.class);
+        this.requestFactory = requestFactory;
+    }
+
+    @Override
+    public FluidSearchService getFluidSearch(CollectionReference collectionReference) throws ArlasException {
+        return new JdbiFluidSearch(collectionReference, requestFactory);
+    }
+
+    @Override
+    public Hits count(CollectionReference collectionReference,
+                      FluidSearchService fluidSearch) throws ArlasException {
+        SelectRequest req = ((JdbiFluidSearch) fluidSearch).getRequest();
+        Hits hits = new Hits(collectionReference.collectionName);
+        hits.totalnb = dao.count(collectionReference.params.indexName, req.getWhereClause(),
+                getParamMap(collectionReference, req.getWhereParams()));
+        hits.nbhits = hits.totalnb;
+        return hits;
+    }
+
+    @Override
+    public ComputationResponse compute(CollectionReference collectionReference,
+                                       FluidSearchService fluidSearch,
+                                       String field, ComputationEnum metric) throws ArlasException {
+        SelectRequest req = ((JdbiFluidSearch) fluidSearch).getRequest();
+        ComputationResponse computationResponse = new ComputationResponse();
+        computationResponse.field = field;
+        computationResponse.metric = metric;
+        try (ResultIterator<Map<String,Object>> iter = dao.select(collectionReference.params.indexName,
+                req.getSelectClause(), req.getWhereClause(), req.getGroupClause(), req.getOrderClause(),
+                req.getLimitClause(), getParamMap(collectionReference, req.getWhereParams()))) {
+
+            long startQueryTimestamp = System.nanoTime();
+            computationResponse.value = null;
+            computationResponse.geometry = null;
+            Map<String, Object> response = ResultIterable.of(iter).first();
+            computationResponse.totalnb = (Long) response.get(COUNT);
+            if (computationResponse.totalnb > 0) {
+                switch (metric) {
+                    case AVG:
+                        computationResponse.value = Double.parseDouble(response.get(AVG).toString());
+                        break;
+                    case CARDINALITY:
+                        computationResponse.value = Double.parseDouble(response.get(COUNT).toString());
+                        break;
+                    case MAX:
+                        computationResponse.value = Double.parseDouble(response.get(MAX).toString());
+                        break;
+                    case MIN:
+                        computationResponse.value = Double.parseDouble(response.get(MIN).toString());
+                        break;
+                    case SPANNING:
+                        computationResponse.value = Double.parseDouble(response.get(MAX).toString()) - Double.parseDouble(response.get(MIN).toString());
+                        break;
+                    case SUM:
+                        computationResponse.value = Double.parseDouble(response.get(SUM).toString());
+                        break;
+                    case GEOBBOX:
+                        computationResponse.geometry = createGeoJsonObject((String)response.get(GEOBBOX.value()));
+                        break;
+                    case GEOCENTROID:
+                        computationResponse.geometry = createGeoJsonObject((String)response.get(GEOCENTROID.value()));
+                        break;
+                }
+            }
+
+            computationResponse.queryTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startQueryTimestamp);
+        }
+        return computationResponse;
+    }
+
+    @Override
+    public FeatureCollection getFeatures(MixedRequest request, CollectionReference collectionReference, FluidSearchService fluidSearch, boolean flat) throws ArlasException {
+        SelectRequest req = ((JdbiFluidSearch) fluidSearch).getRequest(true);
+        Search searchRequest = (Search) request.basicRequest;
+        FeatureCollection fc = new FeatureCollection();
+        try (ResultIterator<Map<String,Object>> iter = dao.select(collectionReference.params.indexName,
+                req.getSelectClause(), req.getWhereClause(), req.getGroupClause(), req.getOrderClause(),
+                req.getLimitClause(), getParamMap(collectionReference, req.getWhereParams()))) {
+            while (iter.hasNext()) {
+                Map<String, Object> source = iter.next();
+                Hit arlasHit = new Hit(collectionReference, source, searchRequest.returned_geometries, flat, true);
+                if (searchRequest.returned_geometries != null) {
+                    for (String path : searchRequest.returned_geometries.split(",")) {
+                        GeoJsonObject g = arlasHit.getGeometry(path);
+                        if (g != null) {
+                            fc.add(getFeatureFromHit(arlasHit, path, g));
+                        }
+                    }
+                } else {
+                    //Apply geometry or centroid to geo json feature
+                    if (arlasHit.md.geometry != null) {
+                        fc.add(getFeatureFromHit(arlasHit, collectionReference.params.geometryPath, arlasHit.md.geometry));
+                    } else if (arlasHit.md.centroid != null) {
+                        fc.add(getFeatureFromHit(arlasHit, collectionReference.params.centroidPath, arlasHit.md.centroid));
+                    }
+                }
+            }
+        }
+        if (searchRequest.page != null && searchRequest.page.before != null) {
+            Collections.reverse(fc.getFeatures());
+        }
+
+        return fc;
+    }
+
+    @Override
+    public Hits search(MixedRequest request, CollectionReference collectionReference, Boolean flat, UriInfo uriInfo, String method) throws ArlasException {
+        // TODO search
+        throw new NotImplementedException();
+    }
+
+    @Override
+    public List<Map<String, Object>> searchAsRaw(MixedRequest request, CollectionReference collectionReference) throws ArlasException {
+        // TODO searchAsRaw
+        throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, Object> getRawDoc(CollectionReference collectionReference, String identifier, String[] includes) throws ArlasException {
+        // TODO getRawDoc
+        throw new NotImplementedException();
+    }
+
+    @Override
+    protected AggregationResponse aggregate(CollectionReference collectionReference,
+                                            List<Aggregation> aggregationsRequests,
+                                            int aggTreeDepth,
+                                            Long startQuery,
+                                            FluidSearchService fluidSearch) throws ArlasException {
+        SelectRequest req = ((JdbiFluidSearch) fluidSearch).getRequest();
+        AggregationResponse aggregationResponse = new AggregationResponse();
+        try (ResultIterator<Map<String,Object>> iter = dao.select(collectionReference.params.indexName,
+                req.getSelectClause(), req.getWhereClause(), req.getGroupClause(), req.getOrderClause(), req.getLimitClause(),
+                getParamMap(collectionReference, req.getWhereParams()))) {
+
+            aggregationResponse.queryTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startQuery);
+            JdbiAggregationResult aggResult = new JdbiAggregationResult(aggregationsRequests.size(), ResultIterable.of(iter));
+            aggregationResponse = formatAggregationResult(aggResult.getResults(), aggregationResponse, collectionReference, aggregationsRequests, aggTreeDepth);
+        }
+        return aggregationResponse;
+    }
+
+    private AggregationResponse formatAggregationResult(Map<Object, Map> buckets, AggregationResponse aggregationResponse,
+                                                        CollectionReference collection, List<Aggregation> aggregationsRequest,
+                                                        int aggTreeDepth) {
+        List<RawGeometry> rawGeometries = aggregationsRequest.size() > aggTreeDepth ? aggregationsRequest.get(aggTreeDepth).rawGeometries : null;
+        List<AggregatedGeometryEnum> aggregatedGeometries = aggregationsRequest.size() > aggTreeDepth ? aggregationsRequest.get(aggTreeDepth).aggregatedGeometries : null;
+        aggregationResponse.elements = new ArrayList<>();
+        buckets.forEach((key, bucket) -> {
+            Map<String, Object> bucketValues = (Map<String, Object>) bucket.get("values");
+            if (aggregationResponse.name == null) {
+                aggregationResponse.name = FluidSearchService.getAggregationName((String) bucketValues.get("aggName"));
+            }
+            if (aggregationResponse.totalnb == null) {
+                aggregationResponse.totalnb = 0l;
+            }
+            AggregationResponse element = new AggregationResponse();
+            element.count = (Long) bucketValues.get("count");
+            element.keyAsString = bucketValues.get("key");
+            element.key = element.keyAsString;
+            if (aggregationResponse.name.equals(GEOHASH_AGG)) {
+                Point geoPoint = getGeohashCentre(element.keyAsString.toString());
+                element.key = geoPoint;
+                if (!CollectionUtils.isEmpty(aggregatedGeometries)) {
+                    aggregatedGeometries.stream()
+                            .filter(g -> g.isCellOrCellCenterAgg())
+                            .forEach(g -> {
+                                ReturnedGeometry returnedGeometry = new ReturnedGeometry();
+                                returnedGeometry.reference = g.value();
+                                returnedGeometry.isRaw = false;
+                                if (g.isCellAgg()) {
+                                    returnedGeometry.geometry = createPolygonFromGeohash(element.keyAsString.toString());
+                                } else {
+                                    returnedGeometry.geometry = geoPoint;
+                                }
+                                if (element.geometries == null) {
+                                    element.geometries = new ArrayList<>();
+                                }
+                                element.geometries.add(returnedGeometry);
+                            });
+                }
+            } else if (aggregationResponse.name.equals(GEOTILE_AGG)) {
+                List<Integer> zxy = Stream.of(element.keyAsString.toString().split("/"))
+                        .map (elem -> Integer.valueOf(elem))
+                        .collect(Collectors.toList());
+                BoundingBox tile = GeoTileUtil.getBoundingBox(zxy.get(1), zxy.get(2), zxy.get(0));
+                Point geoPoint = getTileCentre(tile);
+                element.key = geoPoint;
+                if (!CollectionUtils.isEmpty(aggregatedGeometries)) {
+                    aggregatedGeometries.stream()
+                            .filter(g -> g.isCellOrCellCenterAgg())
+                            .forEach(g -> {
+                                ReturnedGeometry returnedGeometry = new ReturnedGeometry();
+                                returnedGeometry.reference = g.value();
+                                returnedGeometry.isRaw = false;
+                                returnedGeometry.geometry = (g.isCellAgg()) ? createBox(tile) : geoPoint;
+                                if (element.geometries == null) {
+                                    element.geometries = new ArrayList<>();
+                                }
+                                element.geometries.add(returnedGeometry);
+
+                            });
+                }
+            } else if (aggregationResponse.name.startsWith(DATEHISTOGRAM_AGG)) {
+                element.keyAsString = DateTimeFormatter.ofPattern(ParamsParser.getValidAggregationFormat(aggregationsRequest.get(0).format))
+                        .withZone(ZoneId.from(ZoneOffset.UTC)).format(Instant.ofEpochMilli(((Double)element.key).longValue()));
+            }
+
+            bucketValues.forEach((qualifier, value) -> {
+                if (isMetric(qualifier)) {
+                    AggregationMetric aggregationMetric = new AggregationMetric();
+                    String[] q = splitQualifier(qualifier);
+                    aggregationMetric.type = q[0];
+                    aggregationMetric.field = q[1];
+                    if (isGeoMetric(qualifier)) {
+                        FeatureCollection fc = new FeatureCollection();
+                        Feature feature = new Feature();
+                        feature.setGeometry(createGeoJsonObject((String)value));
+                        fc.add(feature);
+                        aggregationMetric.value = fc;
+                    } else {
+                        aggregationMetric.value = value;
+                    }
+                    if (element.metrics == null) {
+                        element.metrics = new ArrayList<>();
+                    }
+                    element.metrics.add(aggregationMetric);
+                } else if (isAggregatedGeometry(qualifier)) {
+                    ReturnedGeometry returnedGeometry = new ReturnedGeometry();
+                    returnedGeometry.isRaw = false;
+                    returnedGeometry.geometry = createGeoJsonObject((String)value);
+                    if (isAggregatedGeoBbox(qualifier)) {
+                        returnedGeometry.reference = AggregatedGeometryEnum.BBOX.value();
+                    } else if (isAggregatedGeoCentroid(qualifier)) {
+                        returnedGeometry.reference = AggregatedGeometryEnum.CENTROID.value();
+                    }
+                    if (element.geometries == null) {
+                        element.geometries = new ArrayList<>();
+                    }
+                    element.geometries.add(returnedGeometry);
+                } else {
+                    // TODO other qualifiers
+                    if (!Arrays.asList("key","count","aggName").contains(qualifier)) {
+                        LOGGER.warn("column qualifier not implemented yet: " + qualifier);
+                    }
+                }
+            });
+
+            Map<Object, Map> subAgg = (Map) bucket.get("subAgg");
+            if (!subAgg.isEmpty()) {
+                element.elements = new ArrayList<>();
+                element.elements.add(formatAggregationResult(subAgg, new AggregationResponse(), collection, aggregationsRequest, aggTreeDepth+1));
+            }
+            aggregationResponse.totalnb += element.count;
+            aggregationResponse.elements.add(element);
+
+
+            if (aggregationResponse.name.equals(TERM_AGG)) {
+//                aggregationResponse.sumotherdoccounts = ((Terms) aggregation).getSumOfOtherDocCounts();
+                aggregationResponse.sumotherdoccounts = 0l; // all buckets are fetched in JDBC (should we change this behaviour?)
+            }
+        });
+        return aggregationResponse;
+    }
+
+    private Map<String, Object> getParamMap(CollectionReference collectionReference, Map<String, ClauseParam> params) {
+        Map<String, Object> ret = new HashMap<>();
+        // JDBC infers the column types from the object type, so we need to identify the field types
+        params.forEach((k, p) -> {
+            try {
+                ret.put(k, collectionReferenceService.getType(collectionReference, p.field, false)
+                        .getJavaObject(p.value));
+            } catch (ArlasException arlasException) {
+                // will not happen with the parameter "throwException" set to false
+            }
+        });
+        return ret;
+    }
+
+    private Point getGeohashCentre(String geohash) {
+        Rectangle bbox = GeohashUtils.decodeBoundary(geohash, SpatialContext.GEO);
+
+        Double maxLon = bbox.getMaxX();
+        Double minLon = bbox.getMinX();
+        double lon = (maxLon + minLon) / 2;
+
+        Double maxLat = bbox.getMaxY();
+        Double minLat = bbox.getMinY();
+        double lat = (maxLat + minLat) / 2;
+
+        return new Point(lon, lat);
+    }
+
+    private Point getTileCentre(BoundingBox bbox) {
+        double lon = (bbox.getEast() + bbox.getWest()) / 2;
+        double lat = (bbox.getNorth() + bbox.getSouth()) / 2;
+
+        return new Point(lon, lat);
+    }
+
+    private GeoJsonObject createGeoJsonObject(String geo) {
+        try {
+            return reader.readValue(geoJsonWriter.write(GeoUtil.readWKT(geo)));
+        } catch (IOException | ArlasException e) {
+            throw new RuntimeException("Unable to parse " + geo);
+        }
+    }
+
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiFluidSearch.java b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiFluidSearch.java
new file mode 100644
index 000000000..b08b5239c
--- /dev/null
+++ b/arlas-core/src/main/java/io/arlas/server/impl/jdbi/service/JdbiFluidSearch.java
@@ -0,0 +1,555 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl.jdbi.service;
+
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.exceptions.BadRequestException;
+import io.arlas.server.exceptions.InvalidParameterException;
+import io.arlas.server.exceptions.NotAllowedException;
+import io.arlas.server.impl.jdbi.clause.*;
+import io.arlas.server.impl.jdbi.model.ColumnQualifier;
+import io.arlas.server.impl.jdbi.model.RequestFactory;
+import io.arlas.server.impl.jdbi.model.SelectRequest;
+import io.arlas.server.managers.CollectionReferenceManager;
+import io.arlas.server.model.CollectionReference;
+import io.arlas.server.model.enumerations.*;
+import io.arlas.server.model.request.*;
+import io.arlas.server.model.response.FieldType;
+import io.arlas.server.services.FluidSearchService;
+import io.arlas.server.utils.*;
+import org.apache.commons.lang3.tuple.Pair;
+import org.locationtech.jts.geom.Geometry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static io.arlas.server.impl.jdbi.model.SelectRequest.*;
+import static io.arlas.server.model.enumerations.ComputationEnum.GEOBBOX;
+import static io.arlas.server.model.enumerations.ComputationEnum.GEOCENTROID;
+import static io.arlas.server.utils.CheckParams.GEO_AGGREGATION_TYPE_ENUMS;
+import static io.arlas.server.utils.StringUtil.concat;
+import static io.arlas.server.utils.StringUtil.isNullOrEmpty;
+
+public class JdbiFluidSearch extends FluidSearchService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JdbiFluidSearch.class);
+
+    private final SelectRequest req;
+
+    public JdbiFluidSearch(CollectionReference collectionReference, RequestFactory requestFactory) throws ArlasException {
+        super(collectionReference);
+        this.req = requestFactory.getNewRequest();
+    }
+
+    public SelectRequest getRequest() throws ArlasException {
+        return getRequest(false);
+    }
+
+    public SelectRequest getRequest(boolean setSelectFields) throws ArlasException {
+        if (setSelectFields) {
+            Pair<String[], String[]> includeExclude = computeIncludeExclude(true);
+            req.addSelectClause(this, includeExclude.getLeft());
+        }
+        return req;
+    }
+
+    @Override
+    public FluidSearchService filter(MultiValueFilter<Expression> f, String dateFormat) throws ArlasException {
+        List<String> orConditions = new ArrayList<>();
+        for (Expression fFilter : f) {
+            orConditions.add(filter(fFilter, dateFormat));
+        }
+        req.addOrWhereClauses(orConditions);
+        return this;
+    }
+
+    private String filter(Expression e, String dateFormat) throws ArlasException {
+        if (isNullOrEmpty(e.field) || e.op == null || isNullOrEmpty(e.value)) {
+            throw new InvalidParameterException(INVALID_PARAMETER_F);
+        }
+        String field = e.field;
+        String value = e.value;
+        String[] fieldValues = value.split(COMMA);
+        switch (e.op) {
+            case eq:
+                return req.formatInCondition(e.field, fieldValues);
+            case gte:
+                if (isDateField(field) && !isNullOrEmpty(dateFormat)) {
+                    value = ParamsParser.parseDate(e.value, dateFormat, false);
+                }
+                return req.formatGTECondition(e.field, value);
+            case gt:
+                if (isDateField(field) && !isNullOrEmpty(dateFormat)) {
+                    value = ParamsParser.parseDate(e.value, dateFormat, false);
+                }
+                return req.formatGTCondition(e.field, value);
+            case lte:
+                if (isDateField(field) && !isNullOrEmpty(dateFormat)) {
+                    value = ParamsParser.parseDate(e.value, dateFormat, false);
+                }
+                return req.formatLTECondition(e.field, value);
+            case lt:
+                if (isDateField(field) && !isNullOrEmpty(dateFormat)) {
+                    value = ParamsParser.parseDate(e.value, dateFormat, false);
+                }
+                return req.formatLTCondition(e.field, value);
+            case like:
+                return req.formatLikeCondition(e.field, value);
+            case ne:
+                return req.formatNotInCondition(e.field, fieldValues);
+            case range:
+                field = ParamsParser.getFieldFromFieldAliases(field, collectionReference);
+                if (fieldValues.length > 1) {
+                    List<String> orCond = new ArrayList<>();
+                    for (String valueInValues : fieldValues) {
+                        CheckParams.checkRangeValidity(valueInValues);
+                        orCond.add(getRangeCondition(field, valueInValues, dateFormat));
+                    }
+                    return req.formatOrClauses(orCond);
+                } else {
+                    CheckParams.checkRangeValidity(value);
+                    return getRangeCondition(field, value, dateFormat);
+                }
+            case notintersects:
+            case notwithin:
+            case intersects:
+            case within:
+                FieldType wType = collectionReferenceManager.getType(collectionReference, field, true);
+                switch (wType) {
+                    case POINT:
+                    case LINESTRING:
+                    case POLYGON:
+                    case MULTIPOINT:
+                    case MULTILINESTRING:
+                    case MULTIPOLYGON:
+                    case GEOMETRYCOLLECTION:
+                    case GEO_SHAPE:
+                    case GEO_POINT:
+                        Geometry geo;
+                        if (CheckParams.isBboxMatch(value)) {
+                            double[] tlbr = CheckParams.toDoubles(value);
+                            geo = GeoTileUtil.toPolygon(new BoundingBox(tlbr[3], tlbr[1], tlbr[0], tlbr[2]));
+                        } else {
+                            geo = GeoUtil.readWKT(value);
+                        }
+                        return req.formatGeoCondition(e.field, e.op, geo);
+                    default:
+                        throw new ArlasException(e.op + " op on field '" + field + "' of type '" + wType + "' is not supported");
+                }
+            default:
+                throw new InvalidParameterException(INVALID_OPERATOR);
+        }
+    }
+
+    @Override
+    public FluidSearchService filterQ(MultiValueFilter<String> q) throws ArlasException {
+        List<String> orConditions = new ArrayList<>();
+        for (String qFilter : q) {
+            String[] operands = qFilter.split(":",2);
+            if (operands.length == 2) {
+                orConditions.add(req.formatLikeCondition(operands[0], operands[1]));
+            } else if (operands.length == 1) {
+                throw new ArlasException("Text search without specifying a field is not supported in SQL");
+            } else {
+                throw new InvalidParameterException(INVALID_Q_FILTER);
+            }
+        }
+        req.addOrWhereClauses(orConditions);
+        return this;
+    }
+
+    @Override
+    public FluidSearchService sort(String sort) throws ArlasException {
+        String[] fieldList = sort.split(",");
+        String field;
+        boolean orderAsc;
+        for (String signedField : fieldList) {
+            if (!signedField.equals("")) {
+                if (signedField.charAt(0) == '-') {
+                    field = signedField.substring(1);
+                    orderAsc = false;
+                } else {
+                    field = signedField;
+                    orderAsc = true;
+                }
+                if (field.split(" ").length > 1) {
+                    geoDistanceSort(field, orderAsc);
+                } else {
+                    req.addOrderClause(field, orderAsc);
+                }
+            }
+        }
+        return this;
+    }
+
+    @Override
+    public FluidSearchService filterSize(Integer size, Integer from) {
+        req.limit = size;
+        req.offset = from;
+        return this;
+    }
+
+    @Override
+    public FluidSearchService searchAfter(Page page, String after) {
+        String[] fieldList = page.sort.split(",");
+        String[] valueList = after.split(",");
+        for (int i=0; i<fieldList.length; i++) {
+            String signedField = fieldList[i];
+            String value = valueList[i];
+            if (signedField.startsWith("-")) {
+                req.addOrWhereClauses(Collections.singletonList(req.formatLTCondition(signedField.substring(1), value)));
+            } else {
+                req.addOrWhereClauses(Collections.singletonList(req.formatGTCondition(signedField, value)));
+            }
+        }
+        return this;
+    }
+
+    private void geoDistanceSort(String geoSort, boolean orderAsc) throws ArlasException {
+        Pair<Double, Double> latLon = ParamsParser.getGeoSortParamsAsLatLon(geoSort);
+        String geoSortField = collectionReference.params.centroidPath;
+        req.addOrderClause(req.formatGeoDistance(geoSortField, GeoUtil.getPoint(latLon.getRight(), latLon.getLeft())), orderAsc);
+    }
+
+    @Override
+    public FluidSearchService aggregate(List<Aggregation> aggregations, Boolean isGeoAggregate) throws ArlasException {
+        for (int i=0; i < aggregations.size(); i++) {
+            //check the agg syntax is correct
+            Aggregation aggregationModel = aggregations.get(i);
+            aggregationModel.index = i;
+            if (isGeoAggregate && i == 0) {
+                if (!GEO_AGGREGATION_TYPE_ENUMS.contains(aggregationModel.type)
+                        && aggregationModel.rawGeometries == null
+                        && aggregationModel.aggregatedGeometries == null) {
+                    throw new NotAllowedException("'" + aggregationModel.type.name() +"' aggregation type is not allowed in _geoaggregate service if at least `aggregated_geometries` or `raw_geometries` parameters are not specified");
+                }
+            }
+            if (aggregationModel.type != AggregationTypeEnum.datehistogram
+                    && aggregationModel.format != null) {
+                throw new BadRequestException(NO_FORMAT_TO_SPECIFY);
+            }
+            // ex: datehistogram_0
+            String aggName = concat(aggregationModel.type.name(), US, String.valueOf(aggregationModel.index));
+            switch (aggregationModel.type) {
+                case datehistogram:
+                    buildDateHistogramAggregation(aggName, aggregationModel);
+                    break;
+                case geohash:
+                    buildGeohashAggregation(aggName, aggregationModel);
+                    break;
+                case geotile:
+                    buildGeotileAggregation(aggName, aggregationModel);
+                    break;
+                case histogram:
+                    buildHistogramAggregation(aggName, aggregationModel);
+                    break;
+                case term:
+                    buildTermsAggregation(aggName, aggregationModel);
+                    break;
+            }
+        }
+
+        return this;
+    }
+
+    @Override
+    public FluidSearchService compute(String f, ComputationEnum metric) throws ArlasException {
+        req.addWhereFieldIsNotNull(f);
+        String field = isDateField(f) ? req.getDateFieldAsEpochMilli(f) : f;
+        req.addSelectClause(new MetricSelectClause(field, COUNT, COUNT));
+        switch (metric) {
+            case AVG:
+                req.addSelectClause(new MetricSelectClause(field, AVG, AVG));
+                break;
+            case MAX:
+                req.addSelectClause(new MetricSelectClause(field, MAX, MAX));
+                break;
+            case MIN:
+                req.addSelectClause(new MetricSelectClause(field, MIN, MIN));
+                break;
+            case SUM:
+                req.addSelectClause(new MetricSelectClause(field, SUM, SUM));
+            case SPANNING:
+                req.addSelectClause(new MetricSelectClause(field, MIN, MIN));
+                req.addSelectClause(new MetricSelectClause(field, MAX, MAX));
+                break;
+            case GEOBBOX:
+                req.addSelectClause(new AggregatedGeoSelectClause(field, GEOBBOX.value(), AggregatedGeometryEnum.BBOX.value()));
+                break;
+            case GEOCENTROID:
+                req.addSelectClause(new AggregatedGeoSelectClause(field, GEOCENTROID.value(), AggregatedGeometryEnum.CENTROID.value()));
+                break;
+        }
+        return this;
+    }
+
+    private void buildDateHistogramAggregation(String aggName, Aggregation aggregationModel) throws ArlasException {
+        if (StringUtil.isNullOrEmpty(aggregationModel.field)) {
+            aggregationModel.field = collectionReference.params.timestampPath;
+        }
+
+        DateAggregationSelectClause sp = new DateAggregationSelectClause(aggregationModel.field,
+                aggName, aggregationModel.interval.unit, (Integer) aggregationModel.interval.value,
+                ParamsParser.getValidAggregationFormat(aggregationModel.format));
+        req.addSelectClause(sp);
+
+        //get the field, format, collect_field, collect_fct, order, on
+        setAggregationParameters(aggName, aggregationModel, sp);
+        setHitsToFetch(aggName, aggregationModel, sp);
+        setAggregatedGeometries(aggName, aggregationModel, sp);
+        setRawGeometries(aggName, aggregationModel, sp);
+    }
+
+    private void buildGeohashAggregation(String aggName, Aggregation aggregationModel) throws ArlasException {
+        GeohashAggregationSelectClause sp = new GeohashAggregationSelectClause(aggregationModel.field,
+                aggName, aggregationModel.interval.value.toString());
+        req.addSelectClause(sp);
+
+        //get the field, format, collect_field, collect_fct, order, on
+        setAggregationParameters(aggName, aggregationModel, sp);
+        setAggregatedGeometries(aggName, aggregationModel, sp);
+        setRawGeometries(aggName, aggregationModel, sp);
+        setHitsToFetch(aggName, aggregationModel, sp);
+    }
+
+    private void buildGeotileAggregation(String aggName, Aggregation aggregationModel) throws ArlasException {
+        GeotileAggregationSelectClause sp = new GeotileAggregationSelectClause(aggregationModel.field,
+                aggName, aggregationModel.interval.value.toString());
+        req.addSelectClause(sp);
+
+        //get the field, format, collect_field, collect_fct, order, on
+        setAggregationParameters(aggName, aggregationModel, sp);
+        setAggregatedGeometries(aggName, aggregationModel, sp);
+        setRawGeometries(aggName, aggregationModel, sp);
+        setHitsToFetch(aggName, aggregationModel, sp);
+    }
+
+    private void buildHistogramAggregation(String aggName, Aggregation aggregationModel) throws ArlasException {
+        HistogramAggregationSelectClause sp = new HistogramAggregationSelectClause(aggregationModel.field,
+                aggName, (Double) aggregationModel.interval.value);
+        req.addSelectClause(sp);
+
+        //get the field, format, collect_field, collect_fct, order, on
+        setAggregationParameters(aggName, aggregationModel, sp);
+        setHitsToFetch(aggName, aggregationModel, sp);
+        setAggregatedGeometries(aggName, aggregationModel, sp);
+        setRawGeometries(aggName, aggregationModel, sp);
+    }
+
+    private void buildTermsAggregation(String aggName, Aggregation aggregationModel) throws ArlasException {
+        TermAggregationSelectClause sp = new TermAggregationSelectClause(aggregationModel.field, aggName);
+
+        if (aggregationModel.include != null && !aggregationModel.include.isEmpty()) {
+            req.addOrWhereClauses(Collections.singletonList(req.formatInCondition(aggregationModel.field, aggregationModel.include.split(COMMA))));
+        }
+        req.addSelectClause(sp);
+        //get the field, format, collect_field, collect_fct, order, on
+        setAggregationParameters(aggName, aggregationModel, sp);
+        setHitsToFetch(aggName, aggregationModel, sp);
+        setAggregatedGeometries(aggName, aggregationModel, sp);
+        setRawGeometries(aggName, aggregationModel, sp);
+    }
+
+    private void setAggregationParameters(String aggName, Aggregation aggregationModel, SelectClause sp) throws ArlasException {
+        // firstMetricAggregationBuilder is the aggregation builder on which the order aggregation will be applied
+        MetricSelectClause firstMetric = null;
+        if (aggregationModel.metrics != null) {
+            for (Metric m: aggregationModel.metrics) {
+                if (m.collectFct == null) {
+                    throw new BadRequestException(COLLECT_FCT_NOT_SPECIFIED);
+                } else if (m.collectField == null) {
+                    throw new BadRequestException(COLLECT_FIELD_NOT_SPECIFIED);
+                }
+                MetricSelectClause metricPredicate = null;
+                switch (m.collectFct) {
+                    case AVG:
+                        metricPredicate = new MetricSelectClause(
+                                m.collectField,
+                                concat(aggName, US, ColumnQualifier.getQualifier(CollectionFunction.AVG, m.collectField)),
+                                AVG);
+                        break;
+                    case CARDINALITY:
+                        metricPredicate = new MetricSelectClause(
+                                m.collectField,
+                                concat(aggName, US, ColumnQualifier.getQualifier(CollectionFunction.CARDINALITY, m.collectField)),
+                                COUNT);
+                        break;
+                    case MAX:
+                        metricPredicate = new MetricSelectClause(
+                                m.collectField,
+                                concat(aggName, US, ColumnQualifier.getQualifier(CollectionFunction.MAX, m.collectField)),
+                                MAX);
+                        break;
+                    case MIN:
+                        metricPredicate = new MetricSelectClause(
+                                m.collectField,
+                                concat(aggName, US, ColumnQualifier.getQualifier(CollectionFunction.MIN, m.collectField)),
+                                MIN);
+                        break;
+                    case SUM:
+                        metricPredicate = new MetricSelectClause(
+                                m.collectField,
+                                concat(aggName, US, ColumnQualifier.getQualifier(CollectionFunction.SUM, m.collectField)),
+                                SUM);
+                        break;
+                    case GEOCENTROID:
+                        setGeoMetricAggregationCollectField(m);
+                        // We calculate this metric only if it wasn't requested as a geometry to return in `aggregatedGeometries` parameter
+                        if (!(aggregationModel.aggregatedGeometries != null
+                                && aggregationModel.aggregatedGeometries.contains(AggregatedGeometryEnum.CENTROID)
+                                && aggregationModel.field.equals(m.collectField))) {
+                            metricPredicate = new AggregatedGeoSelectClause(
+                                    m.collectField,
+                                    concat(aggName, US, ColumnQualifier.getQualifier(CollectionFunction.GEOCENTROID, m.collectField)),
+                                    AggregatedGeometryEnum.CENTROID.value());
+                        }
+                        break;
+                    case GEOBBOX:
+                        setGeoMetricAggregationCollectField(m);
+                        // We calculate this metric only if it wasn't requested as a geometry to return in `aggregatedGeometries` parameter
+                        if (!(aggregationModel.aggregatedGeometries != null
+                                && aggregationModel.aggregatedGeometries.contains(AggregatedGeometryEnum.BBOX)
+                                && aggregationModel.field.equals(m.collectField))) {
+                            metricPredicate = new AggregatedGeoSelectClause(
+                                    m.collectField,
+                                    concat(aggName, US, ColumnQualifier.getQualifier(CollectionFunction.GEOBBOX, m.collectField)),
+                                    AggregatedGeometryEnum.BBOX.value());
+                        }
+                        break;
+                }
+                if (metricPredicate != null) {
+                    req.addSelectClause(metricPredicate);
+                    // Getting the first metric aggregation builder that is different from GEOBBOX and GEOCENTROID, on which the order will be applied
+                    if (firstMetric == null
+                            && m.collectFct != CollectionFunction.GEOBBOX
+                            && m.collectFct != CollectionFunction.GEOCENTROID) {
+                        firstMetric = metricPredicate;
+                    }
+                }
+            }
+        }
+
+        if (aggregationModel.size != null) {
+            this.req.limit = ParamsParser.getValidAggregationSize(aggregationModel.size);
+        }
+
+        setOrder(aggregationModel, firstMetric, sp);
+    }
+
+    private void setGeoMetricAggregationCollectField(Metric metric) throws ArlasException {
+        FieldType fieldType = CollectionReferenceManager.getInstance().getType(collectionReference, metric.collectField, true);
+        if (!Arrays.asList(FieldType.POINT, FieldType.GEO_POINT).contains(fieldType)) {
+            throw new InvalidParameterException("collect_field: `" + metric.collectField + "` is not a point field. " + "`" + metric.collectFct.name() + "` is applied to points only.");
+        }
+    }
+
+    private void setAggregatedGeometries(String aggName, Aggregation aggregationModel, SelectClause sp) {
+        if (aggregationModel.aggregatedGeometries != null) {
+            String aggregationGeoField = GEO_AGGREGATION_TYPE_ENUMS.contains(aggregationModel.type) ? aggregationModel.field : collectionReference.params.centroidPath;
+            aggregationModel.aggregatedGeometries.forEach(ag -> {
+                if (ag == AggregatedGeometryEnum.BBOX || ag == AggregatedGeometryEnum.CENTROID) {
+                    req.addSelectClause(new AggregatedGeoSelectClause(aggregationGeoField,
+                            concat(aggName, US, ColumnQualifier.getQualifier(ag, aggregationGeoField)), ag.value()));
+                }
+            });
+        }
+    }
+
+    private void setRawGeometries(String aggName, Aggregation aggregationModel, SelectClause sp) throws ArlasException {
+        if (aggregationModel.rawGeometries != null) {
+            LOGGER.warn("Fetching raw geometries is not possible in a SQL aggregation"); // TODO: investigate further
+        }
+    }
+
+    private void setHitsToFetch(String aggName, Aggregation aggregationModel, SelectClause sp) throws ArlasException {
+        if (aggregationModel.fetchHits != null) {
+            LOGGER.warn("Fetching hits is not possible in a SQL aggregation"); // TODO: investigate further
+        }
+    }
+
+    private void setOrder(Aggregation aggregationModel, MetricSelectClause metricAggregation, SelectClause sp) throws ArlasException {
+        Order order = aggregationModel.order;
+        OrderOn on = aggregationModel.on;
+        boolean isGeo = GEO_AGGREGATION_TYPE_ENUMS.contains(aggregationModel.type);
+        if (order == null && on == null && !isGeo) {
+            if (aggregationModel.type == AggregationTypeEnum.term) {
+                order = Order.desc;
+                on = OrderOn.count;
+            } else {
+                order = Order.asc;
+                on = OrderOn.field;
+            }
+        }
+        boolean orderAsc = order == Order.asc;
+        if (order != null && on != null) {
+            if (!isGeo) {
+                switch (on) {
+                    case field:
+                        req.addOrderClause(sp.asName, orderAsc);
+                        break;
+                    case count:
+                        req.addOrderClause(((AggregationSelectClause)sp).asNameCount, orderAsc);
+                        break;
+                    case result:
+                        if (metricAggregation != null) {
+                            // ORDER ON RESULT IS NOT ALLOWED ON COORDINATES (CENTROID) OR BOUNDING BOX
+                            if (!metricAggregation.isGeo()) {
+                                req.addOrderClause(metricAggregation.asName, orderAsc);
+                            } else {
+                                throw new BadRequestException(ORDER_ON_GEO_RESULT_NOT_ALLOWED);
+                            }
+                        } else {
+                            throw new BadRequestException(ORDER_ON_RESULT_NOT_ALLOWED);
+                        }
+                        break;
+                }
+            } else {
+                throw new NotAllowedException(ORDER_PARAM_NOT_ALLOWED);
+            }
+        } else if (order != null) {
+            if (isGeo)
+                throw new NotAllowedException(ORDER_PARAM_NOT_ALLOWED);
+            else
+                throw new BadRequestException(ON_NOT_SPECIFIED);
+        } else if (on != null) {
+            if (isGeo)
+                throw new NotAllowedException(ORDER_PARAM_NOT_ALLOWED);
+            else
+                throw new BadRequestException(ORDER_NOT_SPECIFIED);
+        }
+    }
+
+    protected String getRangeCondition(String field, String value, String dateFormat) throws ArlasException {
+        boolean incMin = value.startsWith("[");
+        boolean incMax = value.endsWith("]");
+        String min = value.substring(1, value.lastIndexOf("<"));
+        String max = value.substring(value.lastIndexOf("<") + 1, value.length() - 1);
+
+        if (isDateField(field) && !StringUtil.isNullOrEmpty(dateFormat)) {
+            min = ParamsParser.parseDate(min, dateFormat, false);
+            max = ParamsParser.parseDate(max, dateFormat, false);
+        }
+
+        return req.formatRangeCondition(field, incMin, min, incMax, max);
+    }
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/model/response/FieldType.java b/arlas-core/src/main/java/io/arlas/server/model/response/FieldType.java
index 59ee570a2..9a0dd05c2 100644
--- a/arlas-core/src/main/java/io/arlas/server/model/response/FieldType.java
+++ b/arlas-core/src/main/java/io/arlas/server/model/response/FieldType.java
@@ -74,6 +74,10 @@ public boolean isDateField() {
         return Arrays.asList(DATE, TIMESTAMP, TIME).contains(this);
     }
 
+    public boolean isGeoField() {
+        return Arrays.asList(GEO_POINT, GEO_SHAPE).contains(this);
+    }
+
     @Override
     public String toString() {
         return fieldType.toString();
diff --git a/arlas-core/src/main/java/io/arlas/server/services/CollectionReferenceService.java b/arlas-core/src/main/java/io/arlas/server/services/CollectionReferenceService.java
index c5e792ae7..3a137078e 100644
--- a/arlas-core/src/main/java/io/arlas/server/services/CollectionReferenceService.java
+++ b/arlas-core/src/main/java/io/arlas/server/services/CollectionReferenceService.java
@@ -32,6 +32,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -57,8 +58,6 @@ public CollectionReferenceService(String arlasIndex, CacheManager cacheManager)
 
     abstract protected void putCollectionReferenceWithDao(CollectionReference collectionReference) throws ArlasException;
 
-    abstract public boolean isDateField(String field, String index) throws ArlasException;
-
     abstract public void initCollectionDatabase() throws ArlasException;
 
     abstract public List<CollectionReference> getAllCollectionReferences(Optional<String> columnFilter) throws ArlasException;
@@ -314,4 +313,23 @@ private FieldType getUnknownType(String parentField, String collectionName, bool
         }
     }
 
+    public boolean isDateField(String field, String index) throws ArlasException {
+        return getFieldType(field, index).isDateField();
+    }
+
+    public boolean isGeoField(String field, String index) throws ArlasException {
+        return getFieldType(field, index).isGeoField();
+    }
+
+    private FieldType getFieldType(String field, String index) throws ArlasException  {
+        AtomicReference<FieldType> ret = new AtomicReference<>(FieldType.UNKNOWN);
+        Optional.ofNullable(getMapping(index).get(index))
+                .ifPresent(e -> Optional.ofNullable(e.get(field))
+                        .ifPresent(f -> Optional.ofNullable(((Map)f).get("type"))
+                                .ifPresent(t -> ret.set(FieldType.getType(t)))
+                        )
+                );
+        return ret.get();
+    }
+
 }
diff --git a/arlas-core/src/main/java/io/arlas/server/services/ExploreService.java b/arlas-core/src/main/java/io/arlas/server/services/ExploreService.java
index 5792ea64c..a69a76388 100644
--- a/arlas-core/src/main/java/io/arlas/server/services/ExploreService.java
+++ b/arlas-core/src/main/java/io/arlas/server/services/ExploreService.java
@@ -25,12 +25,12 @@
 import io.arlas.server.model.enumerations.ComputationEnum;
 import io.arlas.server.model.enumerations.OperatorEnum;
 import io.arlas.server.model.request.*;
-import io.arlas.server.model.response.AggregationResponse;
-import io.arlas.server.model.response.CollectionReferenceDescription;
-import io.arlas.server.model.response.ComputationResponse;
-import io.arlas.server.model.response.Hits;
+import io.arlas.server.model.response.*;
 import io.arlas.server.utils.*;
-import org.geojson.FeatureCollection;
+import org.geojson.*;
+import org.locationtech.spatial4j.context.SpatialContext;
+import org.locationtech.spatial4j.io.GeohashUtils;
+import org.locationtech.spatial4j.shape.Rectangle;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,6 +45,10 @@ public abstract class ExploreService {
 
     public static final Integer SEARCH_DEFAULT_PAGE_SIZE = 10;
     public static final Integer SEARCH_DEFAULT_PAGE_FROM = 0;
+    private static final String FEATURE_TYPE_KEY = "feature_type";
+    private static final String FEATURE_TYPE_VALUE = "hit";
+    private static final String FEATURE_GEOMETRY_PATH = "geometry_path";
+
 
     private String baseUri;
     protected CollectionReferenceService collectionReferenceService;
@@ -170,6 +174,32 @@ private boolean filterFHasDateQuery(Filter filter, CollectionReference collectio
                 );
     }
 
+    protected Feature getFeatureFromHit(Hit arlasHit, String path, GeoJsonObject geometry) {
+        Feature feature = new Feature();
+
+        // Setting geometry of geojson
+        feature.setGeometry(geometry);
+
+        // setting the properties of the geojson
+        feature.setProperties(new HashMap<>(arlasHit.getDataAsMap()));
+
+        // Setting the Metadata (md) in properties of geojson. Only id, timestamp and centroid are set in the MD. The geometry is already returned in the geojson.
+        MD md = new MD();
+        md.id = arlasHit.md.id;
+        md.timestamp = arlasHit.md.timestamp;
+        md.centroid = arlasHit.md.centroid;
+        if (!arlasHit.isFlat()) {
+            feature.setProperty(MD.class.getSimpleName().toLowerCase(), md);
+        } else {
+            feature.setProperty(MD.class.getSimpleName().toLowerCase(), md.toFlatString());
+        }
+
+        // Setting the feature type of the geojson
+        feature.setProperty(FEATURE_TYPE_KEY, FEATURE_TYPE_VALUE);
+        feature.setProperty(FEATURE_GEOMETRY_PATH, path);
+        return feature;
+    }
+
     protected void sortPage(Page page, FluidSearchService fluidSearch) throws ArlasException {
         if (page != null && page.sort != null) {
             fluidSearch.sort(page.sort);
@@ -193,6 +223,72 @@ protected void applyProjection(Projection projection, FluidSearchService fluidSe
         }
     }
 
+    protected void paginate(Page page, CollectionReference collectionReference, FluidSearchService fluidSearch) throws ArlasException {
+        setPageSizeAndFrom(page, fluidSearch);
+        if (page != null) {
+            if (page.before != null) {
+                Page newPage = page;
+                newPage.sort = Arrays.stream(page.sort.split(","))
+                        .map(field -> field.startsWith("-") ? field.substring(1) : "-".concat(field)).collect(Collectors.joining(","));
+                sortPage(newPage, fluidSearch);
+                searchAfterPage(newPage, collectionReference.params.idPath, fluidSearch);
+            } else {
+                sortPage(page, fluidSearch);
+                searchAfterPage(page, collectionReference.params.idPath, fluidSearch);
+            }
+        }
+    }
+
+    protected void setPageSizeAndFrom(Page page, FluidSearchService fluidSearch) throws ArlasException {
+        if (page != null) {
+            if (page.size == null) {
+                page.size = SEARCH_DEFAULT_PAGE_SIZE;
+            }
+            if (page.from == null) {
+                page.from = SEARCH_DEFAULT_PAGE_FROM;
+            }
+            CheckParams.checkPageSize(page);
+            CheckParams.checkPageFrom(page);
+            fluidSearch.filterSize(page.size, page.from);
+        }
+    }
+
+    protected void searchAfterPage(Page page, String idCollectionField, FluidSearchService fluidSearch) throws ArlasException {
+        if (page != null && page.after != null) {
+            CheckParams.checkPageAfter(page, idCollectionField);
+            fluidSearch = fluidSearch.searchAfter(page, page.after);
+        }
+        if (page != null && page.before != null) {
+            CheckParams.checkPageAfter(page, idCollectionField);
+            fluidSearch.searchAfter(page, page.before);
+        }
+    }
+
+    protected Polygon createPolygonFromGeohash(String geohash) {
+        Rectangle rectangle = GeohashUtils.decodeBoundary(geohash, SpatialContext.GEO);
+        Polygon polygon = new Polygon();
+        List<LngLatAlt> bounds = new ArrayList<>();
+        bounds.add(new LngLatAlt(rectangle.getMinX(), rectangle.getMaxY()));
+        bounds.add(new LngLatAlt(rectangle.getMaxX(), rectangle.getMaxY()));
+        bounds.add(new LngLatAlt(rectangle.getMaxX(), rectangle.getMinY()));
+        bounds.add(new LngLatAlt(rectangle.getMinX(), rectangle.getMinY()));
+        polygon.add(bounds);
+        return polygon;
+    }
+
+    protected Polygon createBox(BoundingBox bbox) {
+        Polygon box = new Polygon();
+        List<LngLatAlt> bounds = new ArrayList<>();
+        bounds.add(new LngLatAlt(bbox.getWest(), bbox.getNorth()));
+        bounds.add(new LngLatAlt(bbox.getEast(), bbox.getNorth()));
+        bounds.add(new LngLatAlt(bbox.getEast(), bbox.getSouth()));
+        bounds.add(new LngLatAlt(bbox.getWest(), bbox.getSouth()));
+        box.add(bounds);
+
+        return box;
+    }
+
+
     public CollectionReferenceService getCollectionReferenceService() { return collectionReferenceService; }
 
     public List<CollectionReferenceDescription> describeAllCollections(List<CollectionReference> collectionReferenceList,
@@ -212,8 +308,7 @@ public AggregationResponse aggregate(MixedRequest request,
                                          int aggTreeDepth,
                                          Long startQuery) throws ArlasException {
         CheckParams.checkAggregationRequest(request.basicRequest, collectionReference);
-        FluidSearchService fluidSearch = getFluidSearch();
-        fluidSearch.setCollectionReference(collectionReference);
+        FluidSearchService fluidSearch = getFluidSearch(collectionReference);
         applyFilter(collectionReference.params.filter, fluidSearch);
         applyFilter(request.basicRequest.filter, fluidSearch);
         applyFilter(request.headerRequest.filter, fluidSearch);
@@ -227,8 +322,7 @@ public AggregationResponse aggregate(MixedRequest request,
     public ComputationResponse compute(MixedRequest request,
                                        CollectionReference collectionReference) throws ArlasException {
         CheckParams.checkComputationRequest(request.basicRequest, collectionReference);
-        FluidSearchService fluidSearch = getFluidSearch();
-        fluidSearch.setCollectionReference(collectionReference);
+        FluidSearchService fluidSearch = getFluidSearch(collectionReference);
         applyFilter(collectionReference.params.filter, fluidSearch);
         applyFilter(request.basicRequest.filter, fluidSearch);
         applyFilter(request.headerRequest.filter, fluidSearch);
@@ -240,14 +334,31 @@ public ComputationResponse compute(MixedRequest request,
 
     public Hits count(MixedRequest request,
                       CollectionReference collectionReference) throws ArlasException {
-        FluidSearchService fluidSearch = getFluidSearch();
-        fluidSearch.setCollectionReference(collectionReference);
+        FluidSearchService fluidSearch = getFluidSearch(collectionReference);
         applyFilter(collectionReference.params.filter, fluidSearch);
         applyFilter(request.basicRequest.filter, fluidSearch);
         applyFilter(request.headerRequest.filter, fluidSearch);
         return count(collectionReference, fluidSearch);
     }
 
+    public FeatureCollection getFeatures(MixedRequest request,
+                                         CollectionReference collectionReference,
+                                         boolean flat) throws ArlasException {
+        FluidSearchService fluidSearch = getSearchRequest(request, collectionReference);
+        return getFeatures(request, collectionReference, fluidSearch, flat);
+    }
+
+
+    private FluidSearchService getSearchRequest(MixedRequest request, CollectionReference collectionReference) throws ArlasException {
+        FluidSearchService fluidSearch = getFluidSearch(collectionReference);
+        applyFilter(collectionReference.params.filter, fluidSearch);
+        applyFilter(request.basicRequest.filter, fluidSearch);
+        applyFilter(request.headerRequest.filter, fluidSearch);
+        paginate(((Search) request.basicRequest).page, collectionReference, fluidSearch);
+        applyProjection(((Search) request.basicRequest).projection, fluidSearch, request.columnFilter, collectionReference);
+        return fluidSearch;
+    }
+
     // -----------------
 
     protected abstract AggregationResponse aggregate(CollectionReference collectionReference,
@@ -259,7 +370,7 @@ protected abstract AggregationResponse aggregate(CollectionReference collectionR
     public abstract Hits count(CollectionReference collectionReference,
                                FluidSearchService fluidSearch) throws ArlasException;
 
-    public abstract FluidSearchService getFluidSearch();
+    public abstract FluidSearchService getFluidSearch(CollectionReference collectionReference) throws ArlasException;
 
     public abstract ComputationResponse compute(CollectionReference collectionReference,
                                                 FluidSearchService fluidSearch,
@@ -267,6 +378,7 @@ public abstract ComputationResponse compute(CollectionReference collectionRefere
 
     public abstract FeatureCollection getFeatures(MixedRequest request,
                                                   CollectionReference collectionReference,
+                                                  FluidSearchService fluidSearch,
                                                   boolean flat) throws ArlasException;
 
     public abstract Hits search(MixedRequest request,
diff --git a/arlas-core/src/main/java/io/arlas/server/services/FluidSearchService.java b/arlas-core/src/main/java/io/arlas/server/services/FluidSearchService.java
index 5d561337f..68dec1a17 100644
--- a/arlas-core/src/main/java/io/arlas/server/services/FluidSearchService.java
+++ b/arlas-core/src/main/java/io/arlas/server/services/FluidSearchService.java
@@ -26,6 +26,7 @@
 import io.arlas.server.model.request.Aggregation;
 import io.arlas.server.model.request.Expression;
 import io.arlas.server.model.request.MultiValueFilter;
+import io.arlas.server.model.request.Page;
 import org.apache.commons.lang3.tuple.Pair;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -82,11 +83,8 @@ public abstract class FluidSearchService {
     protected List<String> include = new ArrayList<>();
     protected List<String> exclude = new ArrayList<>();
 
-    public FluidSearchService() {
+    public FluidSearchService(CollectionReference collectionReference) {
         this.collectionReferenceManager = CollectionReferenceManager.getInstance();
-    }
-
-    public void setCollectionReference(CollectionReference collectionReference) {
         this.collectionReference = collectionReference;
     }
 
@@ -103,7 +101,7 @@ public List<String> getCollectionPaths() {
 
     // -------
 
-    public Pair<String[], String[]> computeIncludeExclude() {
+    public Pair<String[], String[]> computeIncludeExclude(boolean expand) throws ArlasException {
         if (collectionReference.params.excludeFields != null && !collectionReference.params.excludeFields.isEmpty()) {
             if (exclude.isEmpty()) {
                 exclude(collectionReference.params.excludeFields);
@@ -116,14 +114,13 @@ public Pair<String[], String[]> computeIncludeExclude() {
         }
         List<String> includeFieldList = new ArrayList<>();
         if (!include.isEmpty()) {
-            for (String includeField : include) {
-                includeFieldList.add(includeField);
-            }
+            includeFieldList.addAll(include);
             for (String path : getCollectionPaths()) {
                 boolean alreadyIncluded = false;
                 for (String includeField : include) {
                     if (includeField.equals("*") || path.startsWith(includeField)) {
                         alreadyIncluded = true;
+                        break;
                     }
                 }
                 if (!alreadyIncluded) {
@@ -131,9 +128,12 @@ public Pair<String[], String[]> computeIncludeExclude() {
                 }
             }
         }
-        String[] includeFields = includeFieldList.toArray(new String[includeFieldList.size()]);
+        String[] includeFields = new HashSet<>(includeFieldList).toArray(new String[0]);
         if (includeFields.length == 0) {
-            includeFields = new String[]{"*"};
+            includeFields = expand ? collectionReferenceManager
+                    .collectionReferenceService
+                    .getCollectionFields(collectionReference, Optional.empty())
+                    .toArray(new String[0]): new String[]{"*"};
         }
         String[] excludeFields = exclude.toArray(new String[exclude.size()]);
         if (excludeFields.length == 0) {
@@ -156,12 +156,18 @@ public boolean isDateField(String field) throws ArlasException {
         return collectionReferenceManager.getCollectionReferenceService().isDateField(field, collectionReference.params.indexName);
     }
 
+    public boolean isGeoField(String field) throws ArlasException {
+        return collectionReferenceManager.getCollectionReferenceService().isGeoField(field, collectionReference.params.indexName);
+    }
+
     public static String getAggregationName(String aggName) {
         switch (AggregationTypeEnum.valueOf(aggName)) {
             case datehistogram:
                 return DATEHISTOGRAM_AGG;
             case geohash:
                 return GEOHASH_AGG;
+            case geotile:
+                return GEOTILE_AGG;
             case histogram:
                 return HISTOGRAM_AGG;
             case term:
@@ -183,5 +189,8 @@ public static String getAggregationName(String aggName) {
     abstract public FluidSearchService filterQ(MultiValueFilter<String> q) throws ArlasException;
 
     abstract public FluidSearchService sort(String sort) throws ArlasException;
-}
 
+    abstract public FluidSearchService filterSize(Integer size, Integer from);
+
+    abstract public FluidSearchService searchAfter(Page page, String after);
+}
diff --git a/arlas-core/src/main/java/io/arlas/server/utils/GeoUtil.java b/arlas-core/src/main/java/io/arlas/server/utils/GeoUtil.java
index 291b6b0b5..9c13c450e 100644
--- a/arlas-core/src/main/java/io/arlas/server/utils/GeoUtil.java
+++ b/arlas-core/src/main/java/io/arlas/server/utils/GeoUtil.java
@@ -38,6 +38,13 @@ public class GeoUtil {
     public static final String INVALID_WKT_RANGE = "Invalid geometry. Coordinates must be contained in the Envelope -360, 360, -180, 180";
     public static final String INVALID_WKT = "Invalid geometry ";
     public static final String POLYGON_EMPTY = "POLYGON EMPTY";
+
+    private static final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
+
+    public static Point getPoint(Double lon, Double lat) {
+        return geometryFactory.createPoint(new Coordinate(lon, lat));
+    }
+
     /**
      *
      * @param bbox 'west,south,east,north'
@@ -132,7 +139,6 @@ public void filter(Coordinate coordinate) {
      * @throws ArlasException
      */
     public static Geometry readWKT(String geometry) throws ArlasException {
-        GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
         WKTReader wkt = new WKTReader(geometryFactory);
         Geometry geom = null;
         try {
@@ -145,7 +151,6 @@ public static Geometry readWKT(String geometry) throws ArlasException {
     }
 
     public static void checkWKT(String wktString) throws InvalidParameterException {
-        GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
         Envelope affectedBounds = new Envelope(-360, 360, -180, 180);
         WKTReader wkt = new WKTReader(geometryFactory);
         Geometry geom = null;
@@ -178,7 +183,6 @@ public static Tuple2<List<Polygon>, Geometry> splitPolygon(Polygon polygon) thro
         List<Polygon> geometries = new ArrayList<>();
         double envelopeEast = envelope.getMaxX();
         double envelopeWest = envelope.getMinX();
-        GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
         Geometry middleWest = geometryFactory.toGeometry(new Envelope(-180, 0, -90, 90));
         Geometry middleEast = geometryFactory.toGeometry(new Envelope(0, 180, -90, 90));
         if (envelopeEast <= 180 && envelopeWest >= -180) {
diff --git a/arlas-core/src/main/java/io/arlas/server/utils/ParamsParser.java b/arlas-core/src/main/java/io/arlas/server/utils/ParamsParser.java
index 8210f7ebc..302b4003a 100644
--- a/arlas-core/src/main/java/io/arlas/server/utils/ParamsParser.java
+++ b/arlas-core/src/main/java/io/arlas/server/utils/ParamsParser.java
@@ -30,6 +30,7 @@
 import io.arlas.server.model.response.FieldType;
 import io.dropwizard.jersey.params.IntParam;
 import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
 import org.elasticsearch.common.geo.GeoPoint;
 import org.joda.time.format.DateTimeFormat;
 import org.locationtech.jts.algorithm.Orientation;
@@ -39,6 +40,7 @@
 import org.locationtech.jts.operation.valid.TopologyValidationError;
 
 import java.io.IOException;
+import java.text.SimpleDateFormat;
 import java.time.format.DateTimeParseException;
 import java.util.*;
 import java.util.regex.Matcher;
@@ -236,7 +238,7 @@ public static String getFieldFromFieldAliases(String fieldAlias, CollectionRefer
     }
 
     public static String getValidAggregationFormat(String aggFormat) {
-        //TODO: check if format is in DateTimeFormat (joda)
+        // TODO: check if format is in DateTimeFormat (joda)
         if (aggFormat != null) {
             return aggFormat;
         } else {
@@ -520,6 +522,12 @@ public static Page getPage(IntParam size, IntParam from, String sort, String aft
     }
 
     public static GeoPoint getGeoSortParams(String geoSort) throws ArlasException {
+        Pair<Double, Double> latLon = getGeoSortParamsAsLatLon(geoSort);
+        return new GeoPoint(latLon.getLeft(), latLon.getRight());
+    }
+
+    // returns Pair(lat, lon)
+    public static Pair<Double, Double> getGeoSortParamsAsLatLon(String geoSort) throws ArlasException {
         List<String> geoSortList = Arrays.asList(geoSort.split(":"));
         String geoDistance;
         String latLon;
@@ -537,7 +545,7 @@ public static GeoPoint getGeoSortParams(String geoSort) throws ArlasException {
             Double lat = tryParseDouble(geoSortLatLon[0]);
             Double lon = tryParseDouble(geoSortLatLon[1]);
             if (lat != null && lon != null) {
-                return new GeoPoint(lat, lon);
+                return Pair.of(lat, lon);
             } else {
                 throw new InvalidParameterException(INVALID_GEOSORT_LAT_LON);
             }
@@ -602,5 +610,4 @@ public static Long tryParseLong(String text) {
             return null;
         }
     }
-
 }
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/createTable.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/createTable.sql
new file mode 100644
index 000000000..8668d4bb0
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/createTable.sql
@@ -0,0 +1,39 @@
+CREATE TABLE IF NOT EXISTS collection_reference (
+    date_inserted TIMESTAMP DEFAULT NOW(),
+    arlas_index VARCHAR(255) NOT NULL,
+    collection_name VARCHAR(255) NOT NULL,
+    index_name VARCHAR(255) NOT NULL,
+    description JSON NOT NULL
+);
+CREATE INDEX IF NOT EXISTS arlas_index ON collection_reference (arlas_index);
+CREATE UNIQUE INDEX IF NOT EXISTS collection_name ON collection_reference (arlas_index, collection_name);
+
+CREATE OR REPLACE FUNCTION lon2Xtile(lon DOUBLE PRECISION, zoom INTEGER)
+  RETURNS INTEGER AS
+$BODY$
+    SELECT
+   		CASE
+            WHEN x < 0 THEN 0
+            WHEN x >= (1 << zoom) THEN ((1 << zoom)-1)
+            ELSE x
+		  END from (select CASE
+		                    WHEN lon = 180 THEN ((1 << zoom)-1)
+		                    ELSE FLOOR( (lon + 180) / 360 * (1 << zoom) )::INTEGER
+		                   END AS x) t;
+$BODY$
+  LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION lat2Ytile(lat double precision, zoom integer)
+  RETURNS integer AS
+$BODY$
+    SELECT
+   		CASE
+            WHEN y < 0 THEN 0
+            WHEN y >= (1 << zoom) THEN ((1 << zoom)-1)
+            ELSE y
+		  END from (select CASE
+		                    WHEN lat = -90 THEN ((1 << zoom)-1)
+		                    ELSE floor((1.0 - ln(tan(radians(lat)) + 1.0 / cos(radians(lat))) / pi()) / 2.0 * (1 << zoom) )::integer
+		                   END AS y) t;
+$BODY$
+  LANGUAGE sql IMMUTABLE;
\ No newline at end of file
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/delete.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/delete.sql
new file mode 100644
index 000000000..59e2b3c7c
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/delete.sql
@@ -0,0 +1,1 @@
+DELETE FROM collection_reference WHERE arlas_index = ? AND collection_name = ?;
\ No newline at end of file
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeAll.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeAll.sql
new file mode 100644
index 000000000..bed316eba
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeAll.sql
@@ -0,0 +1,10 @@
+select table_name,column_name,case
+       when (data_type = 'USER-DEFINED') then udt_name
+	   when (data_type like 'timestamp %') then 'timestamp'
+	   when (data_type like 'time %') then 'time'
+	   when (data_type like 'varchar%') then 'varchar'
+	   when (data_type like 'character%') then 'character'
+	   when (data_type like 'double%') then 'double'
+       else data_type
+    end as data_type
+from INFORMATION_SCHEMA.COLUMNS where table_name in (select index_name from collection_reference where arlas_index = ?);
\ No newline at end of file
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeGeographyType.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeGeographyType.sql
new file mode 100644
index 000000000..27c261868
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeGeographyType.sql
@@ -0,0 +1,8 @@
+SELECT
+    f_table_name AS table_name,
+    f_geography_column AS column_name,
+    type AS data_type
+    FROM geography_columns
+WHERE f_table_name IN (<tableNames>)
+-- "f_table_catalog","f_table_schema","f_table_name","f_geography_column","coord_dimension","srid","type"
+-- "arlas","public","myindex","mygeo",2,4326,"Point"
\ No newline at end of file
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeGeometryType.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeGeometryType.sql
new file mode 100644
index 000000000..4aed2a7ae
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeGeometryType.sql
@@ -0,0 +1,15 @@
+SELECT
+    f_table_name AS table_name,
+    f_geometry_column AS column_name,
+--    type AS data_type
+    CASE
+        WHEN (type = 'POINT') THEN 'GEO_POINT'
+        WHEN (type = 'MULTIPOLYGON') THEN 'GEO_SHAPE'
+        WHEN (type = 'POLYGON') THEN 'GEO_SHAPE'
+        WHEN (type = 'LINESTRING') THEN 'GEO_SHAPE'
+        ELSE type
+    END AS data_type
+FROM geometry_columns
+WHERE f_table_name IN (<tableNames>)
+-- "f_table_catalog","f_table_schema","f_table_name","f_geometry_column","coord_dimension","srid","type"
+-- "arlas","public","myindex","mygeo",2,4326,"Point"
\ No newline at end of file
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeOne.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeOne.sql
new file mode 100644
index 000000000..9b034d583
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/describeOne.sql
@@ -0,0 +1,14 @@
+SELECT
+    table_name,
+    column_name,
+    CASE
+        WHEN (data_type = 'USER-DEFINED') THEN udt_name
+	    WHEN (data_type LIKE 'timestamp %') THEN 'timestamp'
+	    WHEN (data_type LIKE 'time %') THEN 'time'
+	    WHEN (data_type LIKE 'varchar%') THEN 'varchar'
+	    WHEN (data_type LIKE 'character var%') THEN 'varchar'
+	    WHEN (data_type like 'double%') then 'double'
+        ELSE data_type
+    END AS data_type
+FROM INFORMATION_SCHEMA.COLUMNS
+WHERE table_name = ?;
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/insert.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/insert.sql
new file mode 100644
index 000000000..4f5a25621
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/insert.sql
@@ -0,0 +1,6 @@
+INSERT INTO collection_reference(
+    arlas_index,
+    collection_name,
+    index_name,
+    description
+) VALUES(?,?,?,CAST(? as json));
\ No newline at end of file
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/selectAll.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/selectAll.sql
new file mode 100644
index 000000000..7a69fbbb0
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/selectAll.sql
@@ -0,0 +1,5 @@
+SELECT
+    description
+FROM collection_reference
+WHERE arlas_index = ?
+ORDER BY collection_name ASC;
diff --git a/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/selectOne.sql b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/selectOne.sql
new file mode 100644
index 000000000..a523fe3aa
--- /dev/null
+++ b/arlas-core/src/main/resources/io/arlas/server/impl/jdbi/dao/CollectionReferenceDao/selectOne.sql
@@ -0,0 +1,4 @@
+SELECT
+    description
+FROM collection_reference
+WHERE arlas_index = ? AND collection_name = ?;
diff --git a/arlas-rest/src/main/java/io/arlas/server/rest/explore/aggregate/GeoAggregateRESTService.java b/arlas-rest/src/main/java/io/arlas/server/rest/explore/aggregate/GeoAggregateRESTService.java
index 4a029354c..a044a31c4 100644
--- a/arlas-rest/src/main/java/io/arlas/server/rest/explore/aggregate/GeoAggregateRESTService.java
+++ b/arlas-rest/src/main/java/io/arlas/server/rest/explore/aggregate/GeoAggregateRESTService.java
@@ -522,7 +522,7 @@ private AggregationResponse merge(List<AggregationResponse> aggResponses) {
         AggregationResponse result = new AggregationResponse();
         if (aggResponses.size() > 1) {
             result.name = aggResponses.get(0).name;
-            result.totalnb = aggResponses.stream().mapToLong(r -> r.totalnb).sum();
+            result.totalnb = aggResponses.stream().filter(r -> r.totalnb != null).mapToLong(r -> r.totalnb).sum();
             result.elements = aggResponses.stream().map(r -> r.elements).filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
             result.metrics = aggResponses.stream().map(r -> r.metrics).filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
             result.hits = aggResponses.stream().map(r -> r.hits).filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList());
diff --git a/arlas-server/src/main/java/io/arlas/server/app/ArlasServer.java b/arlas-server/src/main/java/io/arlas/server/app/ArlasServer.java
index d57389b06..6bae96f7b 100644
--- a/arlas-server/src/main/java/io/arlas/server/app/ArlasServer.java
+++ b/arlas-server/src/main/java/io/arlas/server/app/ArlasServer.java
@@ -64,7 +64,7 @@
 import io.dropwizard.assets.AssetsBundle;
 import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
 import io.dropwizard.configuration.SubstitutingSourceProvider;
-//import io.dropwizard.jdbi3.bundles.JdbiExceptionsBundle;
+import io.dropwizard.jdbi3.bundles.JdbiExceptionsBundle;
 import io.dropwizard.lifecycle.setup.ScheduledExecutorServiceBuilder;
 import io.dropwizard.setup.Bootstrap;
 import io.dropwizard.setup.Environment;
@@ -108,7 +108,7 @@ public ZipkinFactory getZipkinFactory(ArlasServerConfiguration configuration) {
             }
         });
         bootstrap.addBundle(new AssetsBundle("/assets/", "/", "index.html"));
-//        bootstrap.addBundle(new JdbiExceptionsBundle());
+        bootstrap.addBundle(new JdbiExceptionsBundle());
     }
 
     @Override
diff --git a/arlas-server/src/main/java/io/arlas/server/impl/JdbiDatabaseToolsFactory.java b/arlas-server/src/main/java/io/arlas/server/impl/JdbiDatabaseToolsFactory.java
new file mode 100644
index 000000000..971082840
--- /dev/null
+++ b/arlas-server/src/main/java/io/arlas/server/impl/JdbiDatabaseToolsFactory.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl;
+
+import com.codahale.metrics.health.HealthCheck;
+import io.arlas.server.app.ArlasServerConfiguration;
+import io.arlas.server.app.DatabaseToolsFactory;
+import io.arlas.server.exceptions.ArlasConfigurationException;
+import io.arlas.server.impl.jdbi.model.RequestFactory;
+import io.arlas.server.impl.jdbi.service.JdbiCollectionReferenceService;
+import io.arlas.server.impl.jdbi.service.JdbiExploreService;
+import io.arlas.server.managers.CacheManager;
+import io.arlas.server.ogc.common.dao.JdbiOGCCollectionReferenceDao;
+import io.arlas.server.ogc.common.dao.OGCCollectionReferenceDao;
+import io.arlas.server.ogc.common.model.Service;
+import io.arlas.server.ogc.wfs.services.JdbiWFSToolService;
+import io.arlas.server.ogc.wfs.services.WFSToolService;
+import io.arlas.server.services.CollectionReferenceService;
+import io.arlas.server.services.ExploreService;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.jdbi3.JdbiFactory;
+import io.dropwizard.setup.Environment;
+import org.jdbi.v3.core.Jdbi;
+import org.jdbi.v3.jackson2.Jackson2Plugin;
+import org.jdbi.v3.postgres.PostgresPlugin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class JdbiDatabaseToolsFactory extends DatabaseToolsFactory {
+    private final Jdbi jdbi;
+    private final CacheManager cacheManager;
+    private final ExploreService exploreService;
+    private final CollectionReferenceService collectionReferenceService;
+    private OGCCollectionReferenceDao ogcDao;
+    private WFSToolService wfsService;
+
+    public JdbiDatabaseToolsFactory(Environment environment, ArlasServerConfiguration configuration, CacheManager cacheManager) throws ArlasConfigurationException {
+        super(configuration);
+        if (configuration.database == null) {
+            throw new ArlasConfigurationException("Database configuration missing in config file.");
+        }
+        configuration.database.setDriverClass(getDriverClass());
+        configuration.database.setUrl(getJdbcUrl(configuration.database));
+        this.jdbi = new JdbiFactory().build(environment, configuration.database, "jdbi");
+        this.jdbi.installPlugin(new Jackson2Plugin());
+        this.jdbi.installPlugin(new PostgresPlugin());
+        this.cacheManager = cacheManager;
+        this.collectionReferenceService = new JdbiCollectionReferenceService(jdbi, configuration.arlasIndex, cacheManager);
+        this.exploreService = new JdbiExploreService(jdbi, collectionReferenceService, configuration.arlasBaseUri, configuration.arlasRestCacheTimeout, getRequestFactory());
+        if (configuration.arlasServiceCSWEnabled) {
+            this.ogcDao = new JdbiOGCCollectionReferenceDao(jdbi, collectionReferenceService, configuration.arlasIndex, Service.CSW);
+        }
+        if (configuration.arlasServiceWFSEnabled) {
+            this.wfsService = new JdbiWFSToolService((JdbiExploreService) exploreService);
+        }
+    }
+
+    @Override
+    public ExploreService getExploreService() {
+        return this.exploreService;
+    }
+
+    @Override
+    public CollectionReferenceService getCollectionReferenceService() {
+        return this.collectionReferenceService;
+    }
+
+    @Override
+    public OGCCollectionReferenceDao getOGCCollectionReferenceDao() {
+        return ogcDao;
+    }
+
+    @Override
+    public WFSToolService getWFSToolService() {
+        return wfsService;
+    }
+
+    @Override
+    public Map<String, HealthCheck> getHealthChecks() {
+        Map<String, HealthCheck> ret = new HashMap<>();
+        // dropwizard-jdbi3 already provides a healthcheck
+        return ret;
+    }
+
+    protected abstract String getDriverClass();
+
+    protected abstract String getJdbcUrl(DataSourceFactory configuration);
+
+    protected abstract RequestFactory getRequestFactory();
+}
diff --git a/arlas-server/src/main/java/io/arlas/server/impl/PostgisDatabaseToolsFactory.java b/arlas-server/src/main/java/io/arlas/server/impl/PostgisDatabaseToolsFactory.java
new file mode 100644
index 000000000..90a3ab5f4
--- /dev/null
+++ b/arlas-server/src/main/java/io/arlas/server/impl/PostgisDatabaseToolsFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.impl;
+
+import io.arlas.server.app.ArlasServerConfiguration;
+import io.arlas.server.exceptions.ArlasConfigurationException;
+import io.arlas.server.impl.jdbi.model.RequestFactory;
+import io.arlas.server.impl.jdbi.postgis.PostgisRequestFactory;
+import io.arlas.server.managers.CacheManager;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.setup.Environment;
+
+public class PostgisDatabaseToolsFactory extends JdbiDatabaseToolsFactory {
+    public PostgisDatabaseToolsFactory(Environment environment, ArlasServerConfiguration configuration, CacheManager cacheManager) throws ArlasConfigurationException {
+        super(environment, configuration, cacheManager);
+    }
+
+    @Override
+    protected String getDriverClass() {
+        return "org.postgis.DriverWrapper";
+    }
+
+    @Override
+    protected String getJdbcUrl(DataSourceFactory configuration) {
+        return "jdbc:postgresql_postGIS://"+configuration.getUrl();
+    }
+
+    @Override
+    protected RequestFactory getRequestFactory() {
+        return new PostgisRequestFactory();
+    }
+}
diff --git a/conf/configuration.yaml b/conf/configuration.yaml
index 50aeb97b9..63b72f84c 100644
--- a/conf/configuration.yaml
+++ b/conf/configuration.yaml
@@ -58,8 +58,11 @@ logging:
     "org.eclipse.jetty": ${ARLAS_LOGGING_JETTY_LEVEL:-INFO}
     "org.elasticsearch.client": ${ARLAS_LOGGING_ELASTICSEARCH_LEVEL:-DEBUG}
     "org.reflections": ${ARLAS_LOGGING_REFLECTIONS_LEVEL:-INFO}
+    "org.glassfish": ${ARLAS_LOGGING_GLASSFISH_LEVEL:-INFO}
     "io.dropwizard": ${ARLAS_LOGGING_DROPWIZARD_LEVEL:-INFO}
     "io.swagger": ${ARLAS_LOGGING_SWAGGER_LEVEL:-INFO}
+    "javax.management.mbeanserver": ${ARLAS_LOGGING_MBEAN_LEVEL:-INFO}
+    "com.hazelcast": ${ARLAS_LOGGING_HAZELCAST_LEVEL:-INFO}
   appenders:
     - type: console
       threshold:  ${ARLAS_LOGGING_CONSOLE_LEVEL:-INFO}
@@ -173,4 +176,29 @@ arlas_cors:
   exposed_headers: ${ARLAS_CORS_EXPOSED_HEADERS:-"Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,Location,WWW-Authenticate"}
 
 arlas_database_factory_class: ${ARLAS_DATABASE_FACTORY_CLASS:-io.arlas.server.impl.ElasticDatabaseToolsFactory}
+#arlas_database_factory_class: ${ARLAS_DATABASE_FACTORY_CLASS:-io.arlas.server.impl.PostgisDatabaseToolsFactory}
 arlas_cache_factory_class: ${ARLAS_CACHE_FACTORY_CLASS:-io.arlas.server.impl.HazelcastCacheFactory}
+
+# In case of JDBI implementation used in `arlas_database_factory_class`, update the following section accordingly
+database:
+  driverClass: do.not.set
+  user: ${ARLAS_JDBC_USER:-postgres}
+  password: ${ARLAS_JDBC_PASSWORD:-postgres}
+  url: ${ARLAS_JDBC_URL:-localhost:5432/arlas}
+  # any properties specific to your JDBC driver:
+  properties:
+    charSet: UTF-8
+  # the maximum amount of time to wait on an empty pool before throwing an exception
+  maxWaitForConnection: 1s
+  # the SQL query to run when validating a connection's liveness
+  validationQuery: "/* MyApplication Health Check */ SELECT 1"
+  # the minimum number of connections to keep open
+  minSize: 8
+  # the maximum number of connections to keep open
+  maxSize: 32
+  # whether or not idle connections should be validated
+  checkConnectionWhileIdle: false
+  # the amount of time to sleep between runs of the idle connection validation, abandoned cleaner and idle pool resizing
+  evictionInterval: 10s
+  # the minimum amount of time an connection must sit idle in the pool before it is eligible for eviction
+  minIdleTime: 1 minute
\ No newline at end of file
diff --git a/docker-compose-elasticsearch.yml b/docker-compose-elasticsearch.yml
index 903beae6e..613858aa7 100644
--- a/docker-compose-elasticsearch.yml
+++ b/docker-compose-elasticsearch.yml
@@ -17,4 +17,4 @@ services:
 #      - /var/lib/elasticsearch:/usr/share/elasticsearch/data
       - ${ELASTIC_DATADIR}:/usr/share/elasticsearch/data
     ports:
-      - 19200:9200
\ No newline at end of file
+      - 9200:9200
\ No newline at end of file
diff --git a/docker-compose-postgis.yml b/docker-compose-postgis.yml
new file mode 100644
index 000000000..1fb2ca9e8
--- /dev/null
+++ b/docker-compose-postgis.yml
@@ -0,0 +1,31 @@
+version: "3"
+# docker-compose -f docker-compose-postgis.yml up -d
+# docker-compose -f docker-compose-postgis.yml down -v
+services:
+  postgis:
+    container_name: postgis
+    restart: unless-stopped
+    image: postgis/postgis:latest
+    environment:
+      POSTGRES_PASSWORD: postgres
+    ports:
+    - "5432:5432"
+    volumes:
+    #- /home/user/projects/postgis/data:/var/lib/postgresql/data
+    - ${PG_DATADIR}:/var/lib/postgresql/data
+  pgadmin:
+    image: dpage/pgadmin4:latest
+    container_name: pgadmin
+    restart: always
+    environment:
+      PGADMIN_DEFAULT_EMAIL: tech@gisaia.com
+      PGADMIN_DEFAULT_PASSWORD: secret
+      PGADMIN_LISTEN_PORT: 80
+    ports:
+    - "8080:80"
+    volumes:
+    # on your local host: chmod -R 777 /home/user/projects/postgis/pgadmin
+    #- /home/user/projects/postgis/pgadmin/servers.json:/pgadmin4/servers.json
+    #- /home/user/projects/postgis/pgadmin/data:/var/lib/pgadmin
+    - ${PGA_CONFIG}:/pgadmin4/servers.json
+    - ${PGA_DATADIR}:/var/lib/pgadmin
diff --git a/ogc-common/src/main/java/io/arlas/server/ogc/common/dao/JdbiOGCCollectionReferenceDao.java b/ogc-common/src/main/java/io/arlas/server/ogc/common/dao/JdbiOGCCollectionReferenceDao.java
new file mode 100644
index 000000000..630d4f3c0
--- /dev/null
+++ b/ogc-common/src/main/java/io/arlas/server/ogc/common/dao/JdbiOGCCollectionReferenceDao.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.ogc.common.dao;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import io.arlas.server.services.CollectionReferenceService;
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.exceptions.NotImplementedException;
+import io.arlas.server.model.CollectionReference;
+import io.arlas.server.model.CollectionReferenceParameters;
+import io.arlas.server.model.CollectionReferences;
+import io.arlas.server.ogc.common.model.Service;
+import io.arlas.server.utils.BoundingBox;
+import org.jdbi.v3.core.Jdbi;
+
+import java.io.IOException;
+
+public class JdbiOGCCollectionReferenceDao implements OGCCollectionReferenceDao {
+
+    private final String arlasIndex;
+    private final Service service;
+    private final CollectionReferenceService collectionReferenceService;
+
+    private static ObjectMapper mapper;
+    private static ObjectReader reader;
+
+    static {
+        mapper = new ObjectMapper();
+        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
+        reader = mapper.readerFor(CollectionReferenceParameters.class);
+    }
+
+    public JdbiOGCCollectionReferenceDao(Jdbi jdbi, CollectionReferenceService collectionReferenceService, String index, Service service) {
+        this.collectionReferenceService = collectionReferenceService;
+        this.arlasIndex = index;
+        this.service = service;
+    }
+
+    @Override
+    public CollectionReferences getCollectionReferences(String[] includes, String[] excludes, int size, int from, String[] ids, String q, String constraint, BoundingBox boundingBox) throws ArlasException, IOException {
+        throw new NotImplementedException();
+    }
+
+    @Override
+    public CollectionReferences getCollectionReferencesExceptOne(String[] includes, String[] excludes, int size, int from, String[] ids, String q, String constraint, BoundingBox boundingBox, CollectionReference collectionReferenceToRemove) throws ArlasException, IOException {
+        throw new NotImplementedException();
+    }
+
+    @Override
+    public CollectionReferences getAllCollectionReferencesExceptOne(String[] includes, String[] excludes, int size, int from, CollectionReference collectionReferenceToRemove) throws ArlasException, IOException {
+        throw new NotImplementedException();
+    }
+}
diff --git a/ogc-wfs/src/main/java/io/arlas/server/ogc/wfs/services/ElasticWFSToolService.java b/ogc-wfs/src/main/java/io/arlas/server/ogc/wfs/services/ElasticWFSToolService.java
index aaa7880a8..10e3dcef9 100644
--- a/ogc-wfs/src/main/java/io/arlas/server/ogc/wfs/services/ElasticWFSToolService.java
+++ b/ogc-wfs/src/main/java/io/arlas/server/ogc/wfs/services/ElasticWFSToolService.java
@@ -21,7 +21,7 @@
 import io.arlas.server.exceptions.ArlasException;
 import io.arlas.server.exceptions.OGC.OGCException;
 import io.arlas.server.exceptions.OGC.OGCExceptionCode;
-import io.arlas.server.impl.elastic.core.ElasticFluidSearch;
+import io.arlas.server.impl.elastic.services.ElasticFluidSearch;
 import io.arlas.server.impl.elastic.services.ElasticExploreService;
 import io.arlas.server.model.CollectionReference;
 import io.arlas.server.model.request.Filter;
@@ -31,7 +31,6 @@
 import io.arlas.server.ogc.common.utils.GeoFormat;
 import io.arlas.server.ogc.wfs.utils.WFSConstant;
 import io.arlas.server.ogc.wfs.utils.WFSRequestType;
-import io.arlas.server.services.FluidSearchService;
 import io.arlas.server.utils.ColumnFilterUtil;
 import io.arlas.server.utils.MapExplorer;
 import io.arlas.server.utils.ParamsParser;
@@ -145,8 +144,7 @@ private void buildWFSQuery(WFSRequestType requestType, String id, String bbox, S
                                CollectionReference collectionReference, Optional<String> columnFilter) throws ArlasException, IOException{
 
         wfsQuery = QueryBuilders.boolQuery();
-        ElasticFluidSearch fluidSearch = (ElasticFluidSearch) exploreServices.getFluidSearch();
-        fluidSearch.setCollectionReference(getCollectionReferenceDescription(collectionReference));
+        ElasticFluidSearch fluidSearch = (ElasticFluidSearch) exploreServices.getFluidSearch(getCollectionReferenceDescription(collectionReference));
         addCollectionFilter(fluidSearch, collectionReference);
         if (constraint != null) {
             wfsQuery.filter(ElasticFilter.filter(constraint, getCollectionReferenceDescription(collectionReference), Service.WFS, columnFilter));
diff --git a/ogc-wfs/src/main/java/io/arlas/server/ogc/wfs/services/JdbiWFSToolService.java b/ogc-wfs/src/main/java/io/arlas/server/ogc/wfs/services/JdbiWFSToolService.java
new file mode 100644
index 000000000..7f62f9948
--- /dev/null
+++ b/ogc-wfs/src/main/java/io/arlas/server/ogc/wfs/services/JdbiWFSToolService.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.ogc.wfs.services;
+
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.exceptions.NotImplementedException;
+import io.arlas.server.impl.jdbi.service.JdbiExploreService;
+import io.arlas.server.model.CollectionReference;
+import io.arlas.server.model.response.CollectionReferenceDescription;
+import net.opengis.wfs._2.ValueCollectionType;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class JdbiWFSToolService implements WFSToolService {
+    JdbiExploreService exploreServices;
+    BoolQueryBuilder wfsQuery = QueryBuilders.boolQuery();
+
+
+    public JdbiWFSToolService(JdbiExploreService exploreServices) {
+        this.exploreServices = exploreServices;
+    }
+
+    @Override
+    public CollectionReferenceDescription getCollectionReferenceDescription(CollectionReference collectionReference) throws ArlasException {
+        return exploreServices.getCollectionReferenceService().describeCollection(collectionReference);
+    }
+
+    @Override
+    public Map<String, Object> getFeature(String id, String bbox, String constraint, String resourceid, String storedquery_id, String partitionFilter, CollectionReference collectionReference, String[] excludes, Optional<String> columnFilter) throws ArlasException, IOException {
+        throw new NotImplementedException();
+    }
+
+    @Override
+    public List<Map<String, Object>> getFeatures(String id, String bbox, String constraint, String resourceid, String partitionFilter, CollectionReference collectionReference, String[] excludes, Integer startindex, Integer count,
+                                    Optional<String> columnFilter) throws ArlasException, IOException {
+        throw new NotImplementedException();
+    }
+
+    @Override
+    public ValueCollectionType getPropertyValue(String id, String bbox, String constraint, String resourceid, String storedquery_id, String partitionFilter,
+                                                CollectionReference collectionReference, String include, String[] excludes, Integer startindex, Integer count,
+                                                Optional<String> columnFilter) throws ArlasException, IOException {
+
+        throw new NotImplementedException();
+    }
+}
diff --git a/pom.xml b/pom.xml
index b8fbdf38e..639d61603 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
 
   <properties>
     <!-- PROJECT-->
-    <log4j.version>2.9.1</log4j.version>
+    <log4j.version>2.13.2</log4j.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <skipTests>true</skipTests>
     <surefire.version>2.20.1</surefire.version>
@@ -42,7 +42,7 @@
     <cyclops.version>10.0.0-M7</cyclops.version>
 
     <!-- TESTS -->
-    <junit.version>4.12</junit.version>
+    <junit.version>4.13.1</junit.version>
     <org.hamcrest.version>1.3</org.hamcrest.version>
     <io.rest-assured.version>3.3.0</io.rest-assured.version>
 
@@ -58,7 +58,12 @@
 
     <!-- ELASTICSEARCH-->
     <elastic.version>7.9.2</elastic.version>
-    <elastic-apm.version>1.19.0</elastic-apm.version>
+    <elastic-apm.version>1.20.0</elastic-apm.version>
+
+    <postgresql.version>42.2.19</postgresql.version>
+    <postgis-jdbc.version>2.5.0</postgis-jdbc.version>
+    <!-- do not upgrade jdbi3-core to 3.19.0 because it needs java 11 to compile -->
+    <jdbi3.version>3.18.1</jdbi3.version>
 
     <!-- GEO-->
     <geotools.version>18.0</geotools.version>
@@ -66,7 +71,7 @@
     <geoapi.version>3.0.1</geoapi.version>
     <geojson.jackson.version>1.8.1</geojson.jackson.version>
     <spatial4j.version>0.7</spatial4j.version>
-    <jts.version>1.15.0</jts.version>
+    <jts.version>1.18.1</jts.version>
 
     <!-- AUTH -->
     <java-jwt.version>3.8.2</java-jwt.version>


Test patch:
diff --git a/arlas-tests/src/test/java/io/arlas/server/data/AbstractDataInjector.java b/arlas-tests/src/test/java/io/arlas/server/data/AbstractDataInjector.java
new file mode 100644
index 000000000..879ab32e7
--- /dev/null
+++ b/arlas-tests/src/test/java/io/arlas/server/data/AbstractDataInjector.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.data;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.arlas.server.Data;
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.model.RasterTileURL;
+import org.geojson.LngLatAlt;
+import org.geojson.Polygon;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+public abstract class AbstractDataInjector {
+
+    static Logger LOGGER = LoggerFactory.getLogger(AbstractDataInjector.class);
+
+    public final static String DATA_SINK_NAME ="dataset";
+
+    //TODO make these fields consistent between implementations (Elasticsearch, Postgis, ...)
+    public final static String DATASET_ID_PATH="id";
+    public final static String DATASET_GEO_PARAMS="geo_params";
+    public final static String DATASET_GEOMETRY_PATH="geo_params_geometry";
+    public final static String DATASET_WKT_GEOMETRY_PATH="geo_params_wktgeometry";
+    public final static String DATASET_CENTROID_PATH="geo_params_centroid";
+    public final static String DATASET_TIMESTAMP_PATH="params_startdate";
+    public final static String DATASET_EXCLUDE_FIELDS = "params_ci*";
+    public final static String DATASET_EXCLUDE_WFS_FIELDS="params_country";
+    public final static String DATASET_TIMESTAMP_FORMAT = "epoch_millis";
+    public static final String DATASET_INSPIRE_LINEAGE = "Dataset loaded for testing";
+    public static final String DATASET_INSPIRE_TOPIC_CATEGORY = "biota";
+    public static final String DATASET_DUBLIN_CORE_TITLE = "geodata";
+    public static final String DATASET_DUBLIN_CORE_DESCRIPTION = "geodata set for testing";
+    public static final String DATASET_DUBLIN_CORE_LANGUAGE = "eng";
+    public final static RasterTileURL DATASET_TILE_URL = new RasterTileURL(Optional.ofNullable(System.getenv("ARLAS_TILE_URL")).orElse("{id}/{z}/{x}/{y}.png"),5,18,true);
+
+    public static final String[] jobs = {"Actor", "Announcers", "Archeologists", "Architect", "Brain Scientist", "Chemist", "Coach", "Coder", "Cost Estimator", "Dancer", "Drafter"};
+    public static final String[] cities = {"Paris", "London", "New York", "Tokyo", "Toulouse", "Marseille", "Lyon", "Bordeaux", "Lille", "Albi", "Calais"};
+    public static final String[] countries = {"Afghanistan",
+            "Albania",
+            "Algeria",
+            "Andorra",
+            "Angola",
+            "Antigua",
+            "Barbuda",
+            "Argentina",
+            "Armenia",
+            "Aruba",
+            "Australia",
+            "Austria"
+    };
+    public static boolean WKT_GEOMETRIES = false;
+
+    public void loadDataSet() throws IOException, ArlasException {
+        //Create a single sink with all data
+        clearDataSink(DATA_SINK_NAME);
+        createDataSink(DATA_SINK_NAME);
+        fillDataSink(DATA_SINK_NAME,-170,170,-80,80);
+        LOGGER.info("Data sink created : " + DATA_SINK_NAME);
+    }
+
+    public void clearDataSet() throws IOException, ArlasException {
+        //Create a single sink with all data
+        clearDataSink(DATA_SINK_NAME);
+        LOGGER.info("Data sink cleared : " + DATA_SINK_NAME);
+    }
+
+    public abstract void createDataSink(String dataSinkName);
+    public abstract void writeData(String dataSinkName, Data data);
+    public abstract void clearDataSink(String dataSinkName);
+
+    public void fillDataSink(String dataSinkName, int lonMin, int lonMax, int latMin, int latMax) throws JsonProcessingException, ArlasException {
+        Data data;
+
+        for (int i = lonMin; i <= lonMax; i += 10) {
+            for (int j = latMin; j <= latMax; j += 10) {
+                int i2 = i + 6;
+                int j2 = j + 6;
+                data = new Data();
+                data.id = String.valueOf("ID_" + i + "_" + j + "DI").replace("-", "_");
+                data.fullname = "My name is " + data.id;
+                data.params.age = Math.abs(i * j);
+                data.params.startdate = 1l * (i + 1000) * (j + 1000);
+                if (data.params.startdate >= 1013600) {
+                    data.params.weight = (i + 10) * (j + 10);
+                }
+                data.params.stopdate = 1l * (i + 1000) * (j + 1000) + 100;
+                data.geo_params.centroid = j + "," + i;
+                data.geo_params.other_geopoint = j2 + "," + i2;
+                data.params.job = jobs[((Math.abs(i) + Math.abs(j)) / 10) % (jobs.length - 1)];
+                data.params.country = countries[((Math.abs(i) + Math.abs(j)) / 10) % (countries.length - 1)];
+                data.params.city = cities[((Math.abs(i) + Math.abs(j)) / 10) % (cities.length - 1)];
+                List<LngLatAlt> coords = new ArrayList<>();
+                List<LngLatAlt> second_coords = new ArrayList<>();
+                String wktGeometry = "POLYGON ((";
+                coords.add(new LngLatAlt(i - 1, j + 1));
+                second_coords.add(new LngLatAlt(i2 - 1, j2 + 1));
+                wktGeometry += (i - 1) + " " + (j + 1) + ",";
+                coords.add(new LngLatAlt(i + 1, j + 1));
+                second_coords.add(new LngLatAlt(i2 + 1, j2 + 1));
+                wktGeometry += " " + (i + 1) + " " + (j + 1) + ",";
+                coords.add(new LngLatAlt(i + 1, j - 1));
+                second_coords.add(new LngLatAlt(i2 + 1, j2 - 1));
+                wktGeometry += " " + (i + 1) + " " + (j - 1) + ",";
+                coords.add(new LngLatAlt(i - 1, j - 1));
+                second_coords.add(new LngLatAlt(i2 - 1, j2 - 1));
+                wktGeometry += " " + (i - 1) + " " + (j - 1) + ",";
+                coords.add(new LngLatAlt(i - 1, j + 1));
+                second_coords.add(new LngLatAlt(i2 - 1, j2 + 1));
+                wktGeometry += " " + (i - 1) + " " + (j + 1) + "))";
+
+                data.geo_params.geometry = new Polygon(coords);
+                data.geo_params.second_geometry = new Polygon(second_coords);
+                data.geo_params.wktgeometry = wktGeometry;
+                writeData(dataSinkName, data);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/arlas-tests/src/test/java/io/arlas/server/data/CollectionInjector.java b/arlas-tests/src/test/java/io/arlas/server/data/CollectionInjector.java
new file mode 100644
index 000000000..599cc3978
--- /dev/null
+++ b/arlas-tests/src/test/java/io/arlas/server/data/CollectionInjector.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.data;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.arlas.server.AbstractTestContext;
+import io.arlas.server.exceptions.ArlasException;
+import io.arlas.server.model.CollectionReferenceParameters;
+import io.arlas.server.model.DublinCoreElementName;
+import io.arlas.server.model.Inspire;
+import io.arlas.server.model.enumerations.OperatorEnum;
+import io.arlas.server.model.request.Expression;
+import io.arlas.server.model.request.Filter;
+import io.arlas.server.model.request.MultiValueFilter;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+
+public class CollectionInjector extends AbstractTestContext {
+
+    //TODO refactor Elasticsearch implementation as a regular CollectionInjector sink
+    public static final String POSTGIS_SINK = "postgis";
+
+    public static String COLLECTION_NAME = "geodata";
+    public static String COLLECTION_NAME_ACTOR = "geodata_actor";
+
+    public static AbstractDataInjector getDataInjector(String sink) {
+        switch(sink) {
+            case POSTGIS_SINK:
+                return new PostgisDataInjector();
+            default:
+                return null;
+        }
+    }
+
+    public static void main(String[] args) throws IOException, ArlasException {
+        // RUN this class to load POSTGIS dataset with following arguments
+        switch (args[0]) {
+            case "load":
+                new CollectionInjector().load();
+                break;
+            case "loadcsw":
+                new CollectionInjector().loadCsw(0);
+                break;
+            case "delete":
+                new CollectionInjector().delete();
+                break;
+            case "deletecsw":
+                new CollectionInjector().deleteCsw();
+                break;
+        }
+    }
+
+    @Test
+    public  void load() throws ArlasException {
+        this.load(0);
+    }
+
+    public  void load(long sleepAfter) throws ArlasException {
+
+        try {
+            getDataInjector(POSTGIS_SINK).loadDataSet();
+        } catch (UnknownHostException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw e;
+        }
+
+        CollectionReferenceParameters params = new CollectionReferenceParameters();
+        params.indexName = AbstractDataInjector.DATA_SINK_NAME;
+        params.idPath = AbstractDataInjector.DATASET_ID_PATH;
+        params.geometryPath = AbstractDataInjector.WKT_GEOMETRIES ? AbstractDataInjector.DATASET_WKT_GEOMETRY_PATH:AbstractDataInjector.DATASET_GEOMETRY_PATH;
+        params.centroidPath = AbstractDataInjector.DATASET_CENTROID_PATH;
+        params.timestampPath = AbstractDataInjector.DATASET_TIMESTAMP_PATH;
+        params.excludeFields = AbstractDataInjector.DATASET_EXCLUDE_FIELDS;
+        params.excludeWfsFields = AbstractDataInjector.DATASET_EXCLUDE_WFS_FIELDS;
+        params.rasterTileURL = AbstractDataInjector.DATASET_TILE_URL;
+        params.rasterTileWidth=256;
+        params.rasterTileHeight=256;
+        params.inspire = new Inspire();
+        params.inspire.lineage = AbstractDataInjector.DATASET_INSPIRE_LINEAGE;
+        params.inspire.topicCategories = Arrays.asList(AbstractDataInjector.DATASET_INSPIRE_TOPIC_CATEGORY);
+        params.dublinCoreElementName = new DublinCoreElementName();
+        params.dublinCoreElementName.title = AbstractDataInjector.DATASET_DUBLIN_CORE_TITLE;
+        params.dublinCoreElementName.description = AbstractDataInjector.DATASET_DUBLIN_CORE_DESCRIPTION;
+        params.dublinCoreElementName.language = AbstractDataInjector.DATASET_DUBLIN_CORE_LANGUAGE;
+
+
+        // PUT new collection
+        given().contentType("application/json").body(params).when().put(getUrlPath()).then().statusCode(200);
+        Filter filter = new Filter();
+        filter.f =Arrays.asList(new MultiValueFilter<>(new Expression("params.job", OperatorEnum.eq, AbstractDataInjector.jobs[0])));
+        params.filter =filter;
+        given().contentType("application/json").body(params).when().put(arlasPath + "collections/" + COLLECTION_NAME_ACTOR).then().statusCode(200);
+
+        try {
+            Thread.sleep(sleepAfter);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public  void loadCsw() throws IOException {
+        this.loadCsw(0);
+    }
+
+    public  void loadCsw(long sleepAfter) throws IOException {
+        try {
+            getDataInjector(POSTGIS_SINK).loadDataSet();
+        } catch (UnknownHostException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (ArlasException e) {
+            e.printStackTrace();
+        }
+
+        InputStreamReader dcelementForCollection = new InputStreamReader(CollectionInjector.class.getClassLoader().getResourceAsStream("csw.collection.dcelements.json"));
+        ObjectMapper objectMapper = new ObjectMapper();
+        DublinCoreElementName[] dcelements = objectMapper.readValue(dcelementForCollection, DublinCoreElementName[].class);
+        Arrays.asList(dcelements).forEach(dublinCoreElementName -> {
+                    CollectionReferenceParameters params = new CollectionReferenceParameters();
+                    params.indexName = AbstractDataInjector.DATA_SINK_NAME;
+                    params.idPath = AbstractDataInjector.DATASET_ID_PATH;
+                    params.geometryPath = AbstractDataInjector.DATASET_GEOMETRY_PATH;
+                    params.centroidPath = AbstractDataInjector.DATASET_CENTROID_PATH;
+                    params.timestampPath = AbstractDataInjector.DATASET_TIMESTAMP_PATH;
+                    params.excludeFields = AbstractDataInjector.DATASET_EXCLUDE_FIELDS;
+                    params.excludeWfsFields = AbstractDataInjector.DATASET_EXCLUDE_WFS_FIELDS;
+                    params.rasterTileURL = AbstractDataInjector.DATASET_TILE_URL;
+                    params.dublinCoreElementName=dublinCoreElementName;
+                    params.inspire = new Inspire();
+                    params.inspire.lineage = AbstractDataInjector.DATASET_INSPIRE_LINEAGE;
+                    params.inspire.topicCategories = Arrays.asList(AbstractDataInjector.DATASET_INSPIRE_TOPIC_CATEGORY);
+                    String url = arlasPath + "collections/" + dublinCoreElementName.title.split(" ")[0].toLowerCase();
+                    // PUT new collection
+                    given().contentType("application/json").body(params).when().put(url).then().statusCode(200);
+                }
+        );
+        try {
+            Thread.sleep(sleepAfter);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public  void delete() throws IOException, ArlasException {
+        getDataInjector(POSTGIS_SINK).clearDataSet();
+        //DELETE collection
+        when().delete(getUrlPath()).then().statusCode(200);
+        when().delete(arlasPath + "collections/" + COLLECTION_NAME_ACTOR).then().statusCode(200);
+    }
+
+    public  void deleteCsw() throws IOException, ArlasException {
+        getDataInjector(POSTGIS_SINK).clearDataSet();
+        InputStreamReader dcelementForCollection = new InputStreamReader(CollectionInjector.class.getClassLoader().getResourceAsStream("csw.collection.dcelements.json"));
+        ObjectMapper objectMapper = new ObjectMapper();
+        DublinCoreElementName[] dcelements = objectMapper.readValue(dcelementForCollection, DublinCoreElementName[].class);
+        Arrays.asList(dcelements).forEach(dublinCoreElementName -> {
+                    String url = arlasPath + "collections/" + dublinCoreElementName.title.split(" ")[0].toLowerCase();
+                    //DELETE collection
+                    when().delete(url).then().statusCode(200);
+                }
+        );
+    }
+
+    @Override
+    protected String getUrlPath(String collection) {
+        return getUrlPath();
+    }
+
+    protected static String getUrlPath() {
+        return arlasPath + "collections/" + COLLECTION_NAME;
+    }
+}
diff --git a/arlas-tests/src/test/java/io/arlas/server/data/PostgisDataInjector.java b/arlas-tests/src/test/java/io/arlas/server/data/PostgisDataInjector.java
new file mode 100644
index 000000000..2394f97d9
--- /dev/null
+++ b/arlas-tests/src/test/java/io/arlas/server/data/PostgisDataInjector.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa 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 io.arlas.server.data;
+
+import com.codahale.metrics.MetricRegistry;
+import io.arlas.server.Data;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.db.ManagedDataSource;
+import org.geojson.Polygon;
+import org.jdbi.v3.core.Jdbi;
+import org.jdbi.v3.guava.GuavaPlugin;
+import org.jdbi.v3.jackson2.Jackson2Plugin;
+import org.jdbi.v3.jodatime2.JodaTimePlugin;
+import org.jdbi.v3.postgres.PostgresPlugin;
+import org.jdbi.v3.sqlobject.SqlObjectPlugin;
+import org.jdbi.v3.sqlobject.customizer.BindBean;
+import org.jdbi.v3.sqlobject.customizer.Define;
+import org.jdbi.v3.sqlobject.statement.SqlUpdate;
+import org.postgis.LinearRing;
+import org.postgis.PGgeometry;
+import org.postgis.Point;
+
+import java.time.Instant;
+
+public class PostgisDataInjector extends AbstractDataInjector {
+
+    private static Jdbi jdbi;
+
+    static {
+        DataSourceFactory dataSourceFactory = new DataSourceFactory();
+        dataSourceFactory.setUrl("jdbc:postgresql_postGIS://localhost:5432/arlas");
+        dataSourceFactory.setUser("postgres");
+        dataSourceFactory.setPassword("postgres");
+        dataSourceFactory.setDriverClass("org.postgis.DriverWrapper");
+        ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(),"jdbi");
+        jdbi = Jdbi.create(dataSource)
+                .installPlugin(new PostgresPlugin())
+                .installPlugin(new Jackson2Plugin())
+                .installPlugin(new SqlObjectPlugin())
+                .installPlugin(new JodaTimePlugin())
+                .installPlugin(new GuavaPlugin());
+    }
+
+    @Override
+    public void createDataSink(String dataSinkName) {
+        jdbi.withExtension(FlattenedDataDao.class, dao -> {
+            dao.createTable(dataSinkName);
+            return true;
+        });
+    }
+
+    @Override
+    public void writeData(String dataSinkName, Data data) {
+        jdbi.withExtension(FlattenedDataDao.class, dao -> {
+            dao.insertBean(dataSinkName, new FlattenedData(data));
+            return true;
+        });
+    }
+
+    @Override
+    public void clearDataSink(String dataSinkName) {
+        jdbi.withExtension(FlattenedDataDao.class, dao -> {
+            dao.dropTable(dataSinkName);
+            return true;
+        });
+    }
+
+    public interface FlattenedDataDao {
+        @SqlUpdate("CREATE TABLE <table_name>(" +
+                DATASET_ID_PATH + " VARCHAR PRIMARY KEY," +
+                "fullname VARCHAR, " +
+                "params_job VARCHAR, " +
+                "params_age INTEGER, " +
+                "params_weight INTEGER, " +
+                "params_city VARCHAR, " +
+                "params_country VARCHAR, " +
+                DATASET_TIMESTAMP_PATH + " TIMESTAMP, " +
+                "params_stopdate TIMESTAMP, " +
+                DATASET_GEOMETRY_PATH + " GEOMETRY(Polygon,4326), " +
+                DATASET_WKT_GEOMETRY_PATH + " VARCHAR, " +
+                DATASET_CENTROID_PATH + " GEOMETRY(Point,4326), " +
+                "geo_params_second_geometry GEOMETRY(Polygon,4326), " +
+                "geo_params_other_geopoint GEOMETRY(Point,4326)" +
+                ");")
+        void createTable(@Define("table_name") String tableName);
+
+        @SqlUpdate("DROP TABLE IF EXISTS <table_name>")
+        void dropTable(@Define("table_name") String tableName);
+
+        @SqlUpdate("INSERT INTO <table_name>("+DATASET_ID_PATH+", fullname, params_job, params_age, params_weight, params_city, params_country, "+DATASET_TIMESTAMP_PATH+", params_stopdate, " +
+                DATASET_GEOMETRY_PATH+", "+DATASET_WKT_GEOMETRY_PATH+", "+DATASET_CENTROID_PATH+", geo_params_second_geometry, geo_params_other_geopoint)" +
+                " VALUES ( :id, :fullname, :job, :age, :weight, :city, :country, :startdate, :stopdate, " +
+                ":geometry, :wktgeometry, :centroid, :second_geometry, :other_geopoint)")
+        void insertBean(@Define("table_name") String tableName, @BindBean FlattenedData data);
+    }
+
+    public class FlattenedData {
+        public String id;
+        public String fullname;
+        public String job;
+        public int age;
+        public Integer weight;
+        public String city;
+        public String country;
+        public Instant startdate;
+        public Instant stopdate;
+        public PGgeometry geometry;
+        public String wktgeometry;
+        public PGgeometry centroid;
+        public PGgeometry second_geometry;
+        public PGgeometry other_geopoint;
+
+        public FlattenedData(Data data) {
+            id = data.id;
+            fullname = data.fullname;
+            job = data.params.job;
+            age = data.params.age;
+            weight = data.params.weight;
+            city = data.params.city;
+            country = data.params.country;
+            startdate = Instant.ofEpochSecond(data.params.startdate);
+            stopdate = Instant.ofEpochSecond(data.params.stopdate);
+            geometry = getPGgeometry(data.geo_params.geometry);
+            wktgeometry = data.geo_params.wktgeometry;
+            centroid = getPGgeometry(data.geo_params.centroid);
+            second_geometry = getPGgeometry(data.geo_params.second_geometry);
+            other_geopoint = getPGgeometry(data.geo_params.other_geopoint);
+        }
+
+        private PGgeometry getPGgeometry(Polygon polygon) {
+            Point[] coords = polygon.getExteriorRing().stream().map(coord -> new Point(coord.getLongitude(), coord.getLatitude())).toArray(Point[]::new);
+            org.postgis.Polygon geo = new org.postgis.Polygon(new LinearRing[] {new LinearRing(coords)});
+            return new PGgeometry(geo);
+        }
+
+        private PGgeometry getPGgeometry(String geoPoint) {
+            String[] coords = geoPoint.split(",");
+            return new PGgeometry(new Point(Double.valueOf(coords[1]), Double.valueOf(coords[0])));
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public String getFullname() {
+            return fullname;
+        }
+
+        public void setFullname(String fullname) {
+            this.fullname = fullname;
+        }
+
+        public String getJob() {
+            return job;
+        }
+
+        public void setJob(String job) {
+            this.job = job;
+        }
+
+        public int getAge() {
+            return age;
+        }
+
+        public void setAge(int age) {
+            this.age = age;
+        }
+
+        public Integer getWeight() {
+            return weight;
+        }
+
+        public void setWeight(Integer weight) {
+            this.weight = weight;
+        }
+
+        public String getCity() {
+            return city;
+        }
+
+        public void setCity(String city) {
+            this.city = city;
+        }
+
+        public String getCountry() {
+            return country;
+        }
+
+        public void setCountry(String country) {
+            this.country = country;
+        }
+
+        public Instant getStartdate() {
+            return startdate;
+        }
+
+        public void setStartdate(Instant startdate) {
+            this.startdate = startdate;
+        }
+
+        public Instant getStopdate() {
+            return stopdate;
+        }
+
+        public void setStopdate(Instant stopdate) {
+            this.stopdate = stopdate;
+        }
+
+        public PGgeometry getGeometry() {
+            return geometry;
+        }
+
+        public void setGeometry(PGgeometry geometry) {
+            this.geometry = geometry;
+        }
+
+        public String getWktgeometry() {
+            return wktgeometry;
+        }
+
+        public void setWktgeometry(String wktgeometry) {
+            this.wktgeometry = wktgeometry;
+        }
+
+        public PGgeometry getCentroid() {
+            return centroid;
+        }
+
+        public void setCentroid(PGgeometry centroid) {
+            this.centroid = centroid;
+        }
+
+        public PGgeometry getSecond_geometry() {
+            return second_geometry;
+        }
+
+        public void setSecond_geometry(PGgeometry second_geometry) {
+            this.second_geometry = second_geometry;
+        }
+
+        public PGgeometry getOther_geopoint() {
+            return other_geopoint;
+        }
+
+        public void setOther_geopoint(PGgeometry other_geopoint) {
+            this.other_geopoint = other_geopoint;
+        }
+    }
+}
diff --git a/scripts/tests-integration-stage.sh b/scripts/tests-integration-stage.sh
index 5d895b77e..4fd46c0c0 100755
--- a/scripts/tests-integration-stage.sh
+++ b/scripts/tests-integration-stage.sh
@@ -98,7 +98,7 @@ function test_rest() {
         -e WKT_GEOMETRIES=${WKT_GEOMETRIES} \
         --net arlas_default \
         maven:3.5.0-jdk-8 \
-        mvn "-Dit.test=*,!AuthServiceIT,!CollectionTool,!CSWServiceIT,!WFSService*IT" verify -DskipTests=false -DfailIfNoTests=false -B
+        mvn "-Dit.test=*,!AuthServiceIT,!CollectionTool,!CollectionInjector,!CSWServiceIT,!WFSService*IT" verify -DskipTests=false -DfailIfNoTests=false -B
 }
 
 function test_auth() {