Repository: komoot/photon
Pull request #648 (2022-03-01 08:27:54)

Reorganise code to properly encapsulate elasticsearch
This shifts around the existing code, so that access to the elasticsearch library is now completely restricted to the `de.komoot.photon.elasticsearch` package. The `sever` class becomes the main component responsible for creating any objects related to ES processing. The query classes are reorganised to have a clean break between webserver and database access. The `*ElasticSearcher` classes are gone and it's code is merged into the ES-specific classes.

There is still some reorganisation work to do for the result handling. The conversion to a Geocode JSON result shouldn't be done in the ES-specific part.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 49c1087..bed3a1e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -109,7 +109,7 @@ jobs:
 
       - name: Prepare import environment
         run: |
-            if [ ! -f monaco.osm.pbf ]; then
+            if [ ! -f monaco-latest.osm.pbf ]; then
                 wget --no-verbose https://download.geofabrik.de/europe/monaco-latest.osm.pbf
             fi
             mkdir data-env
diff --git a/src/main/java/de/komoot/photon/App.java b/src/main/java/de/komoot/photon/App.java
index fcf514b..9d78eee 100644
--- a/src/main/java/de/komoot/photon/App.java
+++ b/src/main/java/de/komoot/photon/App.java
@@ -1,15 +1,12 @@
 package de.komoot.photon;
 
-
 import com.beust.jcommander.JCommander;
 import com.beust.jcommander.ParameterException;
-import de.komoot.photon.elasticsearch.DatabaseProperties;
 import de.komoot.photon.elasticsearch.Server;
 import de.komoot.photon.nominatim.NominatimConnector;
 import de.komoot.photon.nominatim.NominatimUpdater;
 import de.komoot.photon.utils.CorsFilter;
 import lombok.extern.slf4j.Slf4j;
-import org.elasticsearch.client.Client;
 import spark.Request;
 import spark.Response;
 
@@ -33,32 +30,30 @@ public class App {
         boolean shutdownES = false;
         final Server esServer = new Server(args.getDataDirectory()).start(args.getCluster(), args.getTransportAddresses());
         try {
-            Client esClient = esServer.getClient();
-
             log.info("Make sure that the ES cluster is ready, this might take some time.");
-            esClient.admin().cluster().prepareHealth().setWaitForYellowStatus().get();
+            esServer.waitForReady();
             log.info("ES cluster is now ready.");
 
             if (args.isNominatimImport()) {
                 shutdownES = true;
-                startNominatimImport(args, esServer, esClient);
+                startNominatimImport(args, esServer);
                 return;
             }
 
             // Working on an existing installation.
             // Update the index settings in case there are any changes.
             esServer.updateIndexSettings(args.getSynonymFile());
-            esClient.admin().cluster().prepareHealth().setWaitForYellowStatus().get();
+            esServer.waitForReady();
 
             if (args.isNominatimUpdate()) {
                 shutdownES = true;
-                final NominatimUpdater nominatimUpdater = setupNominatimUpdater(args, esClient);
+                final NominatimUpdater nominatimUpdater = setupNominatimUpdater(args, esServer);
                 nominatimUpdater.update();
                 return;
             }
 
             // no special action specified -> normal mode: start search API
-            startApi(args, esClient);
+            startApi(args, esServer);
         } finally {
             if (shutdownES) esServer.shutdown();
         }
@@ -111,13 +106,9 @@ public class App {
 
 
     /**
-     * take nominatim data to fill elastic search index
-     *
-     * @param args
-     * @param esServer
-     * @param esNodeClient
+     * Read all data from a Nominatim database and import it into a Photon database.
      */
-    private static void startNominatimImport(CommandLineArgs args, Server esServer, Client esNodeClient) {
+    private static void startNominatimImport(CommandLineArgs args, Server esServer) {
         DatabaseProperties dbProperties;
         try {
             dbProperties = esServer.recreateIndex(args.getLanguages()); // clear out previous data
@@ -126,41 +117,33 @@ public class App {
         }
 
         log.info("starting import from nominatim to photon with languages: " + String.join(",", dbProperties.getLanguages()));
-        de.komoot.photon.elasticsearch.Importer importer = new de.komoot.photon.elasticsearch.Importer(esNodeClient, dbProperties.getLanguages(), args.getExtraTags());
         NominatimConnector nominatimConnector = new NominatimConnector(args.getHost(), args.getPort(), args.getDatabase(), args.getUser(), args.getPassword());
-        nominatimConnector.setImporter(importer);
+        nominatimConnector.setImporter(esServer.createImporter(dbProperties.getLanguages(), args.getExtraTags()));
         nominatimConnector.readEntireDatabase(args.getCountryCodes());
 
         log.info("imported data from nominatim to photon with languages: " + String.join(",", dbProperties.getLanguages()));
     }
 
     /**
-     * Prepare Nominatim updater
-     *
-     * @param args
-     * @param esNodeClient
+     * Prepare Nominatim updater.
      */
-    private static NominatimUpdater setupNominatimUpdater(CommandLineArgs args, Client esNodeClient) {
+    private static NominatimUpdater setupNominatimUpdater(CommandLineArgs args, Server server) {
         // Get database properties and ensure that the version is compatible.
         DatabaseProperties dbProperties = new DatabaseProperties();
-        dbProperties.loadFromDatabase(esNodeClient);
+        server.loadFromDatabase(dbProperties);
 
         NominatimUpdater nominatimUpdater = new NominatimUpdater(args.getHost(), args.getPort(), args.getDatabase(), args.getUser(), args.getPassword());
-        Updater updater = new de.komoot.photon.elasticsearch.Updater(esNodeClient, dbProperties.getLanguages(), args.getExtraTags());
-        nominatimUpdater.setUpdater(updater);
+        nominatimUpdater.setUpdater(server.createUpdater(dbProperties.getLanguages(), args.getExtraTags()));
         return nominatimUpdater;
     }
 
     /**
-     * start api to accept search requests via http
-     *
-     * @param args
-     * @param esNodeClient
+     * Start API server to accept search requests via http.
      */
-    private static void startApi(CommandLineArgs args, Client esNodeClient) {
+    private static void startApi(CommandLineArgs args, Server server) {
         // Get database properties and ensure that the version is compatible.
         DatabaseProperties dbProperties = new DatabaseProperties();
-        dbProperties.loadFromDatabase(esNodeClient);
+        server.loadFromDatabase(dbProperties);
         if (args.getLanguages().length > 0) {
             dbProperties.restrictLanguages(args.getLanguages());
         }
@@ -178,14 +161,15 @@ public class App {
         }
 
         // setup search API
-        get("api", new SearchRequestHandler("api", esNodeClient, dbProperties.getLanguages(), args.getDefaultLanguage()));
-        get("api/", new SearchRequestHandler("api/", esNodeClient, dbProperties.getLanguages(), args.getDefaultLanguage()));
-        get("reverse", new ReverseSearchRequestHandler("reverse", esNodeClient, dbProperties.getLanguages(), args.getDefaultLanguage()));
-        get("reverse/", new ReverseSearchRequestHandler("reverse/", esNodeClient, dbProperties.getLanguages(), args.getDefaultLanguage()));
+        String[] langs = dbProperties.getLanguages();
+        get("api", new SearchRequestHandler("api", server.createSearchHandler(langs), langs, args.getDefaultLanguage()));
+        get("api/", new SearchRequestHandler("api/", server.createSearchHandler(langs), langs, args.getDefaultLanguage()));
+        get("reverse", new ReverseSearchRequestHandler("reverse", server.createReverseHandler(), dbProperties.getLanguages(), args.getDefaultLanguage()));
+        get("reverse/", new ReverseSearchRequestHandler("reverse/", server.createReverseHandler(), dbProperties.getLanguages(), args.getDefaultLanguage()));
 
         if (args.isEnableUpdateApi()) {
             // setup update API
-            final NominatimUpdater nominatimUpdater = setupNominatimUpdater(args, esNodeClient);
+            final NominatimUpdater nominatimUpdater = setupNominatimUpdater(args, server);
             get("/nominatim-update", (Request request, Response response) -> {
                 new Thread(() -> nominatimUpdater.update()).start();
                 return "nominatim update started (more information in console output) ...";
diff --git a/src/main/java/de/komoot/photon/DatabaseProperties.java b/src/main/java/de/komoot/photon/DatabaseProperties.java
new file mode 100644
index 0000000..5422c8a
--- /dev/null
+++ b/src/main/java/de/komoot/photon/DatabaseProperties.java
@@ -0,0 +1,71 @@
+package de.komoot.photon;
+
+import lombok.extern.slf4j.Slf4j;
+import java.util.*;
+
+/**
+ * Class collecting database global properties.
+ *
+ * The server is responsible for making the data persistent throught the Photon database.
+ */
+@Slf4j
+public class DatabaseProperties {
+
+
+    private String[] languages = null;
+
+    /**
+     * Return the list of languages for which the database is configured.
+     * @return
+     */
+    public String[] getLanguages() {
+        if (languages == null) {
+            return new String[]{"en", "de", "fr", "it"};
+        }
+
+        return languages;
+    }
+
+    /**
+     * Replace the language list with the given list.
+     *
+     * @param languages Array of two-letter language codes.
+     *
+     * @return This object for chaining.
+     */
+    public DatabaseProperties setLanguages(String[] languages) {
+        this.languages = languages;
+        return this;
+    }
+
+    /**
+     * Set language list to the intersection between the existing list and the given list.
+     *
+     * The final list will use the same order as the given list.
+     *
+     * @param languageList Comma-separated list of two-letter language codes.
+     */
+    public void restrictLanguages(String[] languageList) {
+        if (languages == null) {
+            // Special case for versions that did not yet have a language list set
+            // in the database: Use the given list as is.
+            languages = languageList;
+        } else {
+            Set<String> currentLanguageSet = new HashSet<>(Arrays.asList(languages));
+            List<String> newLanguageList = new ArrayList<>();
+
+            for (String lang : languageList) {
+                if (currentLanguageSet.contains(lang)) {
+                    newLanguageList.add(lang);
+                }
+            }
+
+            if (newLanguageList.isEmpty()) {
+                throw new RuntimeException("Language list '" + languageList.toString() +
+                        "not compatible with languages in database(" + languages.toString() + ")");
+            }
+
+            languages = newLanguageList.toArray(new String[]{});
+        }
+    }
+}
diff --git a/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java b/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java
index 2b953d7..0cfb19c 100644
--- a/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java
+++ b/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java
@@ -3,10 +3,8 @@ package de.komoot.photon;
 import de.komoot.photon.query.BadRequestException;
 import de.komoot.photon.query.ReverseRequest;
 import de.komoot.photon.query.ReverseRequestFactory;
-import de.komoot.photon.searcher.ReverseElasticsearchSearcher;
-import de.komoot.photon.searcher.ReverseRequestHandler;
+import de.komoot.photon.searcher.ReverseHandler;
 import de.komoot.photon.utils.ConvertToGeoJson;
-import org.elasticsearch.client.Client;
 import org.json.JSONObject;
 import spark.Request;
 import spark.Response;
@@ -22,15 +20,15 @@ import static spark.Spark.halt;
  */
 public class ReverseSearchRequestHandler extends RouteImpl {
     private final ReverseRequestFactory reverseRequestFactory;
-    private final ReverseRequestHandler requestHandler;
+    private final ReverseHandler requestHandler;
     private final ConvertToGeoJson geoJsonConverter;
 
-    ReverseSearchRequestHandler(String path, Client esNodeClient, String[] languages, String defaultLanguage) {
+    ReverseSearchRequestHandler(String path, ReverseHandler dbHandler, String[] languages, String defaultLanguage) {
         super(path);
         List<String> supportedLanguages = Arrays.asList(languages);
         this.reverseRequestFactory = new ReverseRequestFactory(supportedLanguages, defaultLanguage);
         this.geoJsonConverter = new ConvertToGeoJson();
-        this.requestHandler = new ReverseRequestHandler(new ReverseElasticsearchSearcher(esNodeClient));
+        this.requestHandler = dbHandler;
     }
 
     @Override
@@ -43,7 +41,7 @@ public class ReverseSearchRequestHandler extends RouteImpl {
             json.put("message", e.getMessage());
             halt(e.getHttpStatus(), json.toString());
         }
-        List<JSONObject> results = requestHandler.handle(photonRequest);
+        List<JSONObject> results = requestHandler.reverse(photonRequest);
         JSONObject geoJsonResults = geoJsonConverter.convert(results);
         if (request.queryParams("debug") != null)
             return geoJsonResults.toString(4);
diff --git a/src/main/java/de/komoot/photon/SearchRequestHandler.java b/src/main/java/de/komoot/photon/SearchRequestHandler.java
index 1a020dc..468b026 100644
--- a/src/main/java/de/komoot/photon/SearchRequestHandler.java
+++ b/src/main/java/de/komoot/photon/SearchRequestHandler.java
@@ -3,10 +3,8 @@ package de.komoot.photon;
 import de.komoot.photon.query.BadRequestException;
 import de.komoot.photon.query.PhotonRequest;
 import de.komoot.photon.query.PhotonRequestFactory;
-import de.komoot.photon.searcher.BaseElasticsearchSearcher;
-import de.komoot.photon.searcher.PhotonRequestHandler;
+import de.komoot.photon.searcher.SearchHandler;
 import de.komoot.photon.utils.ConvertToGeoJson;
-import org.elasticsearch.client.Client;
 import org.json.JSONObject;
 import spark.Request;
 import spark.Response;
@@ -22,15 +20,15 @@ import static spark.Spark.halt;
  */
 public class SearchRequestHandler extends RouteImpl {
     private final PhotonRequestFactory photonRequestFactory;
-    private final PhotonRequestHandler requestHandler;
+    private final SearchHandler requestHandler;
     private final ConvertToGeoJson geoJsonConverter;
 
-    SearchRequestHandler(String path, Client esNodeClient, String[] languages, String defaultLanguage) {
+    SearchRequestHandler(String path, SearchHandler dbHandler, String[] languages, String defaultLanguage) {
         super(path);
         List<String> supportedLanguages = Arrays.asList(languages);
         this.photonRequestFactory = new PhotonRequestFactory(supportedLanguages, defaultLanguage);
         this.geoJsonConverter = new ConvertToGeoJson();
-        this.requestHandler = new PhotonRequestHandler(new BaseElasticsearchSearcher(esNodeClient), supportedLanguages);
+        this.requestHandler = dbHandler;
     }
 
     @Override
@@ -43,7 +41,7 @@ public class SearchRequestHandler extends RouteImpl {
             json.put("message", e.getMessage());
             halt(e.getHttpStatus(), json.toString());
         }
-        List<JSONObject> results = requestHandler.handle(photonRequest);
+        List<JSONObject> results = requestHandler.search(photonRequest);
         JSONObject geoJsonResults = geoJsonConverter.convert(results);
         if (photonRequest.getDebug()) {
             JSONObject debug = new JSONObject();
diff --git a/src/main/java/de/komoot/photon/elasticsearch/DatabaseProperties.java b/src/main/java/de/komoot/photon/elasticsearch/DatabaseProperties.java
deleted file mode 100644
index aa11d25..0000000
--- a/src/main/java/de/komoot/photon/elasticsearch/DatabaseProperties.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package de.komoot.photon.elasticsearch;
-
-import lombok.extern.slf4j.Slf4j;
-import org.elasticsearch.action.get.GetResponse;
-import org.elasticsearch.client.Client;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.XContentFactory;
-
-import java.io.IOException;
-import java.util.*;
-
-/**
- * Class responsible for loading and saving database global properties.
- *
- * The properties are saved in a separate document in the Photon index that does not contain
- * any indexed data.
- */
-@Slf4j
-public class DatabaseProperties {
-    /**
-     * Database version created by new imports with the current code.
-     *
-     * Format must be: major.minor.patch-dev
-     *
-     * Increase to next to be released version when the database layout
-     * changes in an incompatible way. If it is alredy at the next released
-     * version, increase the dev version.
-     */
-    private static final String DATABASE_VERSION = "0.3.6-0";
-    public static final String PROPERTY_DOCUMENT_ID = "DATABASE_PROPERTIES";
-
-    private static final String BASE_FIELD = "document_properties";
-    private static final String FIELD_VERSION = "database_version";
-    private static final String FIELD_LANGUAGES = "indexed_languages";
-
-    private String[] languages = null;
-
-    /**
-     * Return the list of languages for which the database is configured.
-     * @return
-     */
-    public String[] getLanguages() {
-        if (languages == null) {
-            return new String[]{"en", "de", "fr", "it"};
-        }
-
-        return languages;
-    }
-
-    /**
-     * Replace the language list with the given list.
-     *
-     * @param languages Array of two-letter language codes.
-     *
-     * @return This object for chaining.
-     */
-    public DatabaseProperties setLanguages(String[] languages) {
-        this.languages = languages;
-        return this;
-    }
-
-    /**
-     * Set language list to the intersection between the existing list and the given list.
-     *
-     * The final list will use the same order as the given list.
-     *
-     * @param languageList Comma-separated list of two-letter language codes.
-     */
-    public void restrictLanguages(String[] languageList) {
-        if (languages == null) {
-            // Special case for versions that did not yet have a language list set
-            // in the database: Use the given list as is.
-            languages = languageList;
-        } else {
-            Set<String> currentLanguageSet = new HashSet<>(Arrays.asList(languages));
-            List<String> newLanguageList = new ArrayList<>();
-
-            for (String lang : languageList) {
-                if (currentLanguageSet.contains(lang)) {
-                    newLanguageList.add(lang);
-                }
-            }
-
-            if (newLanguageList.isEmpty()) {
-                throw new RuntimeException("Language list '" + languageList.toString() +
-                        "not compatible with languages in database(" + languages.toString() + ")");
-            }
-
-            languages = newLanguageList.toArray(new String[]{});
-        }
-    }
-
-    /**
-     * Save the global properties to the database.
-     *
-     * The function saved properties available as members and the database version
-     * as currently defined in DATABASE_VERSION.
-     */
-    public void saveToDatabase(Client client) throws IOException  {
-        final XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject(BASE_FIELD)
-                        .field(FIELD_VERSION, DATABASE_VERSION)
-                        .field(FIELD_LANGUAGES, String.join(",", languages))
-                        .endObject().endObject();
-
-        client.prepareIndex(PhotonIndex.NAME, PhotonIndex.TYPE).
-                    setSource(builder).setId(PROPERTY_DOCUMENT_ID).execute().actionGet();
-    }
-
-    /**
-     * Load the global properties from the database.
-     *
-     * The function first loads the database version and throws an exception if it does not correspond
-     * to the version as defined in DATABASE_VERSION.
-     *
-     * Currently does nothing when the property entry is missing. Later versions with a higher
-     * database version will then fail.
-     */
-    public void loadFromDatabase(Client client) {
-        GetResponse response = client.prepareGet(PhotonIndex.NAME, PhotonIndex.TYPE, PROPERTY_DOCUMENT_ID).execute().actionGet();
-
-        // We are currently at the database version where versioning was introduced.
-        if (!response.isExists()) {
-            return;
-        }
-
-        Map<String, String> properties = (Map<String, String>) response.getSource().get(BASE_FIELD);
-
-        if (properties == null) {
-            throw new RuntimeException("Found database properties but no '" + BASE_FIELD +"' field. Database corrupt?");
-        }
-
-        String version = properties.getOrDefault(FIELD_VERSION, "");
-        if (!DATABASE_VERSION.equals(version)) {
-            log.error("Database has incompatible version '" + version + "'. Expected: " + DATABASE_VERSION);
-            throw new RuntimeException("Incompatible database.");
-        }
-
-        String langString = properties.get(FIELD_LANGUAGES);
-        if (langString == null) {
-            languages = null;
-        } else {
-            languages = langString.split(",");
-        }
-    }
-}
diff --git a/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchReverseHandler.java b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchReverseHandler.java
new file mode 100644
index 0000000..b705b04
--- /dev/null
+++ b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchReverseHandler.java
@@ -0,0 +1,61 @@
+package de.komoot.photon.elasticsearch;
+
+import com.vividsolutions.jts.geom.Point;
+import de.komoot.photon.query.ReverseRequest;
+import de.komoot.photon.searcher.ReverseHandler;
+import de.komoot.photon.utils.ConvertToJson;
+import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.search.SearchType;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.sort.SortBuilders;
+import org.elasticsearch.search.sort.SortOrder;
+import org.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * @author svantulden
+ */
+public class ElasticsearchReverseHandler implements ReverseHandler {
+    private Client client;
+
+    public ElasticsearchReverseHandler(Client client) {
+        this.client = client;
+    }
+
+    @Override
+    public List<JSONObject> reverse(ReverseRequest photonRequest) {
+        ReverseQueryBuilder queryBuilder = buildQuery(photonRequest);
+        SearchResponse results = search(queryBuilder.buildQuery(), photonRequest.getLimit(), photonRequest.getLocation(),
+                photonRequest.getLocationDistanceSort());
+        List<JSONObject> resultJsonObjects = new ConvertToJson(photonRequest.getLanguage()).convert(results, false);
+        if (resultJsonObjects.size() > photonRequest.getLimit()) {
+            resultJsonObjects = resultJsonObjects.subList(0, photonRequest.getLimit());
+        }
+        return resultJsonObjects;
+    }
+
+
+    private SearchResponse search(QueryBuilder queryBuilder, Integer limit, Point location,
+                                 Boolean locationDistanceSort) {
+        TimeValue timeout = TimeValue.timeValueSeconds(7);
+
+        SearchRequestBuilder builder = client.prepareSearch(PhotonIndex.NAME).setSearchType(SearchType.QUERY_THEN_FETCH)
+                .setQuery(queryBuilder).setSize(limit).setTimeout(timeout);
+
+        if (locationDistanceSort)
+            builder.addSort(SortBuilders.geoDistanceSort("coordinate", new GeoPoint(location.getY(), location.getX()))
+                    .order(SortOrder.ASC));
+
+        return builder.execute().actionGet();
+    }
+
+    private ReverseQueryBuilder buildQuery(ReverseRequest photonRequest) {
+        return ReverseQueryBuilder.builder(photonRequest.getLocation(), photonRequest.getRadius(), photonRequest.getQueryStringFilter());
+    }
+
+}
diff --git a/src/main/java/de/komoot/photon/searcher/PhotonRequestHandler.java b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchSearchHandler.java
similarity index 62%
rename from src/main/java/de/komoot/photon/searcher/PhotonRequestHandler.java
rename to src/main/java/de/komoot/photon/elasticsearch/ElasticsearchSearchHandler.java
index 7aca78d..a16a182 100644
--- a/src/main/java/de/komoot/photon/searcher/PhotonRequestHandler.java
+++ b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchSearchHandler.java
@@ -1,40 +1,42 @@
-package de.komoot.photon.searcher;
+package de.komoot.photon.elasticsearch;
 
-import de.komoot.photon.query.PhotonQueryBuilder;
 import de.komoot.photon.query.PhotonRequest;
+import de.komoot.photon.searcher.SearchHandler;
+import de.komoot.photon.searcher.StreetDupesRemover;
 import de.komoot.photon.utils.ConvertToJson;
 import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.search.SearchType;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.index.query.QueryBuilder;
 import org.json.JSONObject;
 
 import java.util.List;
 
 /**
- * Given a {@link PhotonRequest photon request}, execute the search, process it (for example, de-duplicate) and respond with results formatted in a list of {@link JSONObject json
- * object}s.
- * <p/>
  * Created by Sachin Dole on 2/12/2015.
  */
-public class PhotonRequestHandler {
-
-    private final BaseElasticsearchSearcher elasticsearchSearcher;
-    private final List<String> supportedLanguages;
+public class ElasticsearchSearchHandler implements SearchHandler {
+    private final Client client;
+    private final String[] supportedLanguages;
     private boolean lastLenient = false;
 
-    public PhotonRequestHandler(BaseElasticsearchSearcher elasticsearchSearcher, List<String> supportedLanguages) {
-        this.elasticsearchSearcher = elasticsearchSearcher;
-        this.supportedLanguages = supportedLanguages;
+    public ElasticsearchSearchHandler(Client client, String[] languages) {
+        this.client = client;
+        this.supportedLanguages = languages;
     }
 
-    public List<JSONObject> handle(PhotonRequest photonRequest) {
+    @Override
+    public List<JSONObject> search(PhotonRequest photonRequest) {
         lastLenient = false;
         PhotonQueryBuilder queryBuilder = buildQuery(photonRequest, false);
         // for the case of deduplication we need a bit more results, #300
         int limit = photonRequest.getLimit();
         int extLimit = limit > 1 ? (int) Math.round(photonRequest.getLimit() * 1.5) : 1;
-        SearchResponse results = elasticsearchSearcher.search(queryBuilder.buildQuery(), extLimit);
+        SearchResponse results = sendQuery(queryBuilder.buildQuery(), extLimit);
         if (results.getHits().getTotalHits() == 0) {
             lastLenient = true;
-            results = elasticsearchSearcher.search(buildQuery(photonRequest, true).buildQuery(), extLimit);
+            results = sendQuery(buildQuery(photonRequest, true).buildQuery(), extLimit);
         }
         List<JSONObject> resultJsonObjects = new ConvertToJson(photonRequest.getLanguage()).convert(results, photonRequest.getDebug());
         StreetDupesRemover streetDupesRemover = new StreetDupesRemover(photonRequest.getLanguage());
@@ -62,4 +64,16 @@ public class PhotonRequestHandler {
                 withLocationBias(photonRequest.getLocationForBias(), photonRequest.getScaleForBias(), photonRequest.getZoomForBias()).
                 withBoundingBox(photonRequest.getBbox());
     }
+
+    private SearchResponse sendQuery(QueryBuilder queryBuilder, Integer limit) {
+        TimeValue timeout = TimeValue.timeValueSeconds(7);
+        return client.prepareSearch(PhotonIndex.NAME).
+                setSearchType(SearchType.QUERY_THEN_FETCH).
+                setQuery(queryBuilder).
+                setSize(limit).
+                setTimeout(timeout).
+                execute().
+                actionGet();
+
+    }
 }
diff --git a/src/main/java/de/komoot/photon/query/PhotonQueryBuilder.java b/src/main/java/de/komoot/photon/elasticsearch/PhotonQueryBuilder.java
similarity index 94%
rename from src/main/java/de/komoot/photon/query/PhotonQueryBuilder.java
rename to src/main/java/de/komoot/photon/elasticsearch/PhotonQueryBuilder.java
index d3fb3bf..e762db7 100644
--- a/src/main/java/de/komoot/photon/query/PhotonQueryBuilder.java
+++ b/src/main/java/de/komoot/photon/elasticsearch/PhotonQueryBuilder.java
@@ -1,4 +1,4 @@
-package de.komoot.photon.query;
+package de.komoot.photon.elasticsearch;
 
 
 import com.google.common.collect.ImmutableSet;
@@ -19,13 +19,13 @@ import static com.google.common.collect.Maps.newHashMap;
 
 
 /**
- * There are four {@link de.komoot.photon.query.PhotonQueryBuilder.State states} that this query builder goes through before a query can be executed on elastic search. Of
+ * There are four {@link PhotonQueryBuilder.State states} that this query builder goes through before a query can be executed on elastic search. Of
  * these, three are of importance.
  * <ul>
- * <li>{@link de.komoot.photon.query.PhotonQueryBuilder.State#PLAIN PLAIN} The query builder is being used to build a query without any tag filters.</li>
- * <li>{@link de.komoot.photon.query.PhotonQueryBuilder.State#FILTERED FILTERED} The query builder is being used to build a query that has tag filters and can no longer
+ * <li>{@link PhotonQueryBuilder.State#PLAIN PLAIN} The query builder is being used to build a query without any tag filters.</li>
+ * <li>{@link PhotonQueryBuilder.State#FILTERED FILTERED} The query builder is being used to build a query that has tag filters and can no longer
  * be used to build a PLAIN filter.</li>
- * <li>{@link de.komoot.photon.query.PhotonQueryBuilder.State#FINISHED FINISHED} The query builder has been built and the query has been placed inside a
+ * <li>{@link PhotonQueryBuilder.State#FINISHED FINISHED} The query builder has been built and the query has been placed inside a
  * {@link QueryBuilder filtered query}. Further calls to any methods will have no effect on this query builder.</li>
  * </ul>
  * <p/>
@@ -51,7 +51,7 @@ public class PhotonQueryBuilder {
     protected ArrayList<FilterFunctionBuilder> alFilterFunction4QueryBuilder = new ArrayList<>(1);
 
 
-    private PhotonQueryBuilder(String query, String language, List<String> languages, boolean lenient) {
+    private PhotonQueryBuilder(String query, String language, String[] languages, boolean lenient) {
         BoolQueryBuilder query4QueryBuilder = QueryBuilders.boolQuery();
 
         // 1. All terms of the quey must be contained in the place record somehow. Be more lenient on second try.
@@ -97,7 +97,7 @@ public class PhotonQueryBuilder {
         }));
 
         // 3. Either the name or housenumber must be in the query terms.
-        String defLang = "default".equals(language) ? languages.get(0) : language;
+        String defLang = "default".equals(language) ? languages[0] : language;
         MultiMatchQueryBuilder nameNgramQuery = QueryBuilders.multiMatchQuery(query)
                 .type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
                 .fuzziness(lenient ? Fuzziness.ONE : Fuzziness.ZERO)
@@ -150,7 +150,7 @@ public class PhotonQueryBuilder {
      * @param language
      * @return An initialized {@link PhotonQueryBuilder photon query builder}.
      */
-    public static PhotonQueryBuilder builder(String query, String language, List<String> languages, boolean lenient) {
+    public static PhotonQueryBuilder builder(String query, String language, String[] languages, boolean lenient) {
         return new PhotonQueryBuilder(query, language, languages, lenient);
     }
 
diff --git a/src/main/java/de/komoot/photon/query/ReverseQueryBuilder.java b/src/main/java/de/komoot/photon/elasticsearch/ReverseQueryBuilder.java
similarity index 97%
rename from src/main/java/de/komoot/photon/query/ReverseQueryBuilder.java
rename to src/main/java/de/komoot/photon/elasticsearch/ReverseQueryBuilder.java
index 6dd2deb..99e8de4 100644
--- a/src/main/java/de/komoot/photon/query/ReverseQueryBuilder.java
+++ b/src/main/java/de/komoot/photon/elasticsearch/ReverseQueryBuilder.java
@@ -1,4 +1,4 @@
-package de.komoot.photon.query;
+package de.komoot.photon.elasticsearch;
 
 import com.vividsolutions.jts.geom.Point;
 import org.elasticsearch.common.unit.DistanceUnit;
diff --git a/src/main/java/de/komoot/photon/elasticsearch/Server.java b/src/main/java/de/komoot/photon/elasticsearch/Server.java
index 72e5987..b68dfe1 100644
--- a/src/main/java/de/komoot/photon/elasticsearch/Server.java
+++ b/src/main/java/de/komoot/photon/elasticsearch/Server.java
@@ -1,11 +1,20 @@
 package de.komoot.photon.elasticsearch;
 
+import de.komoot.photon.DatabaseProperties;
+import de.komoot.photon.Importer;
+import de.komoot.photon.Updater;
+import de.komoot.photon.searcher.ReverseHandler;
+import de.komoot.photon.searcher.SearchHandler;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.SystemUtils;
+import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
+import org.elasticsearch.action.get.GetResponse;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.client.transport.TransportClient;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.transport.InetSocketTransportAddress;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.node.InternalSettingsPreparer;
 import org.elasticsearch.node.Node;
@@ -21,10 +30,7 @@ import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.LinkedList;
-import java.util.List;
+import java.util.*;
 
 /**
  * Helper class to start/stop elasticsearch node and get elasticsearch clients
@@ -33,6 +39,22 @@ import java.util.List;
  */
 @Slf4j
 public class Server {
+    /**
+     * Database version created by new imports with the current code.
+     *
+     * Format must be: major.minor.patch-dev
+     *
+     * Increase to next to be released version when the database layout
+     * changes in an incompatible way. If it is alredy at the next released
+     * version, increase the dev version.
+     */
+    private static final String DATABASE_VERSION = "0.3.6-0";
+    public static final String PROPERTY_DOCUMENT_ID = "DATABASE_PROPERTIES";
+
+    private static final String BASE_FIELD = "document_properties";
+    private static final String FIELD_VERSION = "database_version";
+    private static final String FIELD_LANGUAGES = "indexed_languages";
+
     private Node esNode;
 
     private Client esClient;
@@ -104,6 +126,10 @@ public class Server {
         return this;
     }
 
+    public void waitForReady() {
+        esClient.admin().cluster().prepareHealth().setWaitForYellowStatus().get();
+    }
+
     /**
      * stops the elasticsearch node
      */
@@ -118,12 +144,6 @@ public class Server {
         }
     }
 
-    /**
-     * returns an elasticsearch client
-     */
-    public Client getClient() {
-        return esClient;
-    }
 
     private void setupDirectories(URL directoryName) throws IOException, URISyntaxException {
         final File mainDirectory = new File(directoryName.toURI());
@@ -159,14 +179,12 @@ public class Server {
     public DatabaseProperties recreateIndex(String[] languages) throws IOException {
         deleteIndex();
 
-        final Client client = this.getClient();
+        loadIndexSettings().createIndex(esClient, PhotonIndex.NAME);
 
-        loadIndexSettings().createIndex(client, PhotonIndex.NAME);
-
-        new IndexMapping().addLanguages(languages).putMapping(client, PhotonIndex.NAME, PhotonIndex.TYPE);
+        new IndexMapping().addLanguages(languages).putMapping(esClient, PhotonIndex.NAME, PhotonIndex.TYPE);
 
         DatabaseProperties dbProperties = new DatabaseProperties().setLanguages(languages);
-        dbProperties.saveToDatabase(client);
+        saveToDatabase(dbProperties);
 
         return dbProperties;
     }
@@ -176,16 +194,16 @@ public class Server {
         // version. If the version is wrong, we should not be messing with the
         // index.
         DatabaseProperties dbProperties = new DatabaseProperties();
-        dbProperties.loadFromDatabase(getClient());
+        loadFromDatabase(dbProperties);
 
-        loadIndexSettings().setSynonymFile(synonymFile).updateIndex(getClient(), PhotonIndex.NAME);
+        loadIndexSettings().setSynonymFile(synonymFile).updateIndex(esClient, PhotonIndex.NAME);
 
         // Sanity check: legacy databases don't save the languages, so there is no way to update
         //               the mappings consistently.
         if (dbProperties.getLanguages() != null) {
             new IndexMapping()
                     .addLanguages(dbProperties.getLanguages())
-                    .putMapping(getClient(), PhotonIndex.NAME, PhotonIndex.TYPE);
+                    .putMapping(esClient, PhotonIndex.NAME, PhotonIndex.TYPE);
         }
     }
 
@@ -195,7 +213,7 @@ public class Server {
 
     public void deleteIndex() {
         try {
-            this.getClient().admin().indices().prepareDelete(PhotonIndex.NAME).execute().actionGet();
+            esClient.admin().indices().prepareDelete(PhotonIndex.NAME).execute().actionGet();
         } catch (IndexNotFoundException e) {
             // ignore
         }
@@ -209,8 +227,82 @@ public class Server {
      * @param shards the maximum number of shards
      * @return this Server instance for chaining
      */
-    public Server setMaxShards(int shards) {
+    public Server setMaxShards(Integer shards) {
         this.shards = shards;
         return this;
     }
+
+
+       /**
+     * Save the global properties to the database.
+     *
+     * The function saved properties available as members and the database version
+     * as currently defined in DATABASE_VERSION.
+     */
+    public void saveToDatabase(DatabaseProperties dbProperties) throws IOException  {
+        final XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject(BASE_FIELD)
+                        .field(FIELD_VERSION, DATABASE_VERSION)
+                        .field(FIELD_LANGUAGES, String.join(",", dbProperties.getLanguages()))
+                        .endObject().endObject();
+
+        esClient.prepareIndex(PhotonIndex.NAME, PhotonIndex.TYPE).
+                    setSource(builder).setId(PROPERTY_DOCUMENT_ID).execute().actionGet();
+    }
+
+    /**
+     * Load the global properties from the database.
+     *
+     * The function first loads the database version and throws an exception if it does not correspond
+     * to the version as defined in DATABASE_VERSION.
+     *
+     * Currently does nothing when the property entry is missing. Later versions with a higher
+     * database version will then fail.
+     */
+    public void loadFromDatabase(DatabaseProperties dbProperties) {
+        GetResponse response = esClient.prepareGet(PhotonIndex.NAME, PhotonIndex.TYPE, PROPERTY_DOCUMENT_ID).execute().actionGet();
+
+        // We are currently at the database version where versioning was introduced.
+        if (!response.isExists()) {
+            return;
+        }
+
+        Map<String, String> properties = (Map<String, String>) response.getSource().get(BASE_FIELD);
+
+        if (properties == null) {
+            throw new RuntimeException("Found database properties but no '" + BASE_FIELD +"' field. Database corrupt?");
+        }
+
+        String version = properties.getOrDefault(FIELD_VERSION, "");
+        if (!DATABASE_VERSION.equals(version)) {
+            log.error("Database has incompatible version '" + version + "'. Expected: " + DATABASE_VERSION);
+            throw new RuntimeException("Incompatible database.");
+        }
+
+        String langString = properties.get(FIELD_LANGUAGES);
+        dbProperties.setLanguages(langString == null ? null : langString.split(","));
+    }
+
+    public GetResponse getById(int id) {
+        return esClient.prepareGet(PhotonIndex.NAME,PhotonIndex.TYPE, String.valueOf(id)).execute().actionGet();
+    }
+
+    public void refresh() {
+        esClient.admin().indices().refresh(new RefreshRequest(PhotonIndex.NAME)).actionGet();
+    }
+
+    public Importer createImporter(String[] languages, String[] extraTags) {
+        return new de.komoot.photon.elasticsearch.Importer(esClient, languages, extraTags);
+    }
+
+    public Updater createUpdater(String[] languages, String[] extraTags) {
+        return new de.komoot.photon.elasticsearch.Updater(esClient, languages, extraTags);
+    }
+
+    public SearchHandler createSearchHandler(String[] languages) {
+        return new ElasticsearchSearchHandler(esClient, languages);
+    }
+
+    public ReverseHandler createReverseHandler() {
+        return new ElasticsearchReverseHandler(esClient);
+    }
 }
diff --git a/src/main/java/de/komoot/photon/query/PhotonRequest.java b/src/main/java/de/komoot/photon/query/PhotonRequest.java
index 00cecd3..e9b2644 100644
--- a/src/main/java/de/komoot/photon/query/PhotonRequest.java
+++ b/src/main/java/de/komoot/photon/query/PhotonRequest.java
@@ -42,6 +42,10 @@ public class PhotonRequest implements Serializable {
         this.debug = debug;
     }
 
+    public PhotonRequest(String query) {
+        this(query, 10, null, null, 0.2, 14, "default", false);
+    }
+
     public String getQuery() {
         return query;
     }
diff --git a/src/main/java/de/komoot/photon/searcher/BaseElasticsearchSearcher.java b/src/main/java/de/komoot/photon/searcher/BaseElasticsearchSearcher.java
deleted file mode 100644
index 47f0c6a..0000000
--- a/src/main/java/de/komoot/photon/searcher/BaseElasticsearchSearcher.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.komoot.photon.searcher;
-
-import de.komoot.photon.elasticsearch.PhotonIndex;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.client.Client;
-import org.elasticsearch.common.unit.TimeValue;
-import org.elasticsearch.index.query.QueryBuilder;
-
-/**
- * Created by Sachin Dole on 2/12/2015.
- */
-public class BaseElasticsearchSearcher {
-
-    private Client client;
-
-    public BaseElasticsearchSearcher(Client client) {
-        this.client = client;
-    }
-
-    public SearchResponse search(QueryBuilder queryBuilder, Integer limit) {
-        TimeValue timeout = TimeValue.timeValueSeconds(7);
-        return client.prepareSearch(PhotonIndex.NAME).
-                setSearchType(SearchType.QUERY_AND_FETCH).
-                setQuery(queryBuilder).
-                setSize(limit).
-                setTimeout(timeout).
-                execute().
-                actionGet();
-
-    }
-
-}
diff --git a/src/main/java/de/komoot/photon/searcher/ReverseElasticsearchSearcher.java b/src/main/java/de/komoot/photon/searcher/ReverseElasticsearchSearcher.java
deleted file mode 100644
index b080518..0000000
--- a/src/main/java/de/komoot/photon/searcher/ReverseElasticsearchSearcher.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package de.komoot.photon.searcher;
-
-import com.vividsolutions.jts.geom.Point;
-import de.komoot.photon.elasticsearch.PhotonIndex;
-import org.elasticsearch.action.search.SearchRequestBuilder;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.client.Client;
-import org.elasticsearch.common.geo.GeoPoint;
-import org.elasticsearch.common.unit.TimeValue;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.sort.SortBuilders;
-import org.elasticsearch.search.sort.SortOrder;
-
-/**
- * @author svantulden
- */
-public class ReverseElasticsearchSearcher {
-    private Client client;
-
-    public ReverseElasticsearchSearcher(Client client) {
-        this.client = client;
-    }
-
-    public SearchResponse search(QueryBuilder queryBuilder, Integer limit, Point location,
-                                 Boolean locationDistanceSort) {
-        TimeValue timeout = TimeValue.timeValueSeconds(7);
-
-        SearchRequestBuilder builder = client.prepareSearch(PhotonIndex.NAME).setSearchType(SearchType.QUERY_AND_FETCH)
-                .setQuery(queryBuilder).setSize(limit).setTimeout(timeout);
-
-        if (locationDistanceSort)
-            builder.addSort(SortBuilders.geoDistanceSort("coordinate", new GeoPoint(location.getY(), location.getX()))
-                    .order(SortOrder.ASC));
-
-        return builder.execute().actionGet();
-    }
-}
diff --git a/src/main/java/de/komoot/photon/searcher/ReverseHandler.java b/src/main/java/de/komoot/photon/searcher/ReverseHandler.java
new file mode 100644
index 0000000..3f2e9d6
--- /dev/null
+++ b/src/main/java/de/komoot/photon/searcher/ReverseHandler.java
@@ -0,0 +1,11 @@
+package de.komoot.photon.searcher;
+
+import de.komoot.photon.query.ReverseRequest;
+import org.json.JSONObject;
+
+import java.util.List;
+
+public interface ReverseHandler {
+
+    List<JSONObject> reverse(ReverseRequest photonRequest);
+}
diff --git a/src/main/java/de/komoot/photon/searcher/ReverseRequestHandler.java b/src/main/java/de/komoot/photon/searcher/ReverseRequestHandler.java
deleted file mode 100644
index 4e81820..0000000
--- a/src/main/java/de/komoot/photon/searcher/ReverseRequestHandler.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package de.komoot.photon.searcher;
-
-import de.komoot.photon.query.ReverseQueryBuilder;
-import de.komoot.photon.query.ReverseRequest;
-import de.komoot.photon.utils.ConvertToJson;
-import org.elasticsearch.action.search.SearchResponse;
-import org.json.JSONObject;
-
-import java.util.List;
-
-public class ReverseRequestHandler {
-    private final ReverseElasticsearchSearcher elasticsearchSearcher;
-
-    public ReverseRequestHandler(ReverseElasticsearchSearcher elasticsearchSearcher) {
-        this.elasticsearchSearcher = elasticsearchSearcher;
-    }
-
-    public List<JSONObject> handle(ReverseRequest photonRequest) {
-        ReverseQueryBuilder queryBuilder = buildQuery(photonRequest);
-        SearchResponse results = elasticsearchSearcher.search(queryBuilder.buildQuery(), photonRequest.getLimit(), photonRequest.getLocation(),
-                photonRequest.getLocationDistanceSort());
-        List<JSONObject> resultJsonObjects = new ConvertToJson(photonRequest.getLanguage()).convert(results, false);
-        if (resultJsonObjects.size() > photonRequest.getLimit()) {
-            resultJsonObjects = resultJsonObjects.subList(0, photonRequest.getLimit());
-        }
-        return resultJsonObjects;
-    }
-
-    public ReverseQueryBuilder buildQuery(ReverseRequest photonRequest) {
-        return ReverseQueryBuilder.builder(photonRequest.getLocation(), photonRequest.getRadius(), photonRequest.getQueryStringFilter());
-    }
-}
diff --git a/src/main/java/de/komoot/photon/searcher/SearchHandler.java b/src/main/java/de/komoot/photon/searcher/SearchHandler.java
new file mode 100644
index 0000000..a79e0d2
--- /dev/null
+++ b/src/main/java/de/komoot/photon/searcher/SearchHandler.java
@@ -0,0 +1,13 @@
+package de.komoot.photon.searcher;
+
+import de.komoot.photon.query.PhotonRequest;
+import org.json.JSONObject;
+
+import java.util.List;
+
+public interface SearchHandler {
+
+    List<JSONObject> search(PhotonRequest photonRequest);
+
+    String dumpQuery(PhotonRequest photonRequest);
+}
diff --git a/src/main/java/de/komoot/photon/utils/QueryToJson.java b/src/main/java/de/komoot/photon/utils/QueryToJson.java
deleted file mode 100644
index d0fd2e8..0000000
--- a/src/main/java/de/komoot/photon/utils/QueryToJson.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package de.komoot.photon.utils;
-
-import lombok.extern.slf4j.Slf4j;
-import org.elasticsearch.common.bytes.BytesReference;
-import org.elasticsearch.common.xcontent.ToXContent;
-import org.elasticsearch.common.xcontent.json.JsonXContent;
-import org.elasticsearch.index.query.QueryBuilder;
-
-import java.io.IOException;
-
-/**
- * Created by Sachin Dole on 2/28/2015.
- */
-@Slf4j
-public class QueryToJson {
-
-    public String convert(QueryBuilder anItem) {
-        try {
-            BytesReference bytes = anItem.toXContent(JsonXContent.contentBuilder(), new ToXContent.MapParams(null))
-                    .bytes();
-
-            return bytes.utf8ToString();
-        } catch (IOException e) {
-            log.error("Unable to transform querybuilder to a json string due to an exception", e);
-            throw new RuntimeException("Unable to transform querybuilder to a json string due to an exception", e);
-        }
-    }
-}


Test patch:
diff --git a/src/test/java/de/komoot/photon/ApiIntegrationTest.java b/src/test/java/de/komoot/photon/ApiIntegrationTest.java
index 8fb2bcd..0da6d4b 100644
--- a/src/test/java/de/komoot/photon/ApiIntegrationTest.java
+++ b/src/test/java/de/komoot/photon/ApiIntegrationTest.java
@@ -1,6 +1,5 @@
 package de.komoot.photon;
 
-import de.komoot.photon.elasticsearch.Importer;
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.junit.jupiter.api.AfterEach;
diff --git a/src/test/java/de/komoot/photon/ESBaseTester.java b/src/test/java/de/komoot/photon/ESBaseTester.java
index 87baf7b..bb65198 100644
--- a/src/test/java/de/komoot/photon/ESBaseTester.java
+++ b/src/test/java/de/komoot/photon/ESBaseTester.java
@@ -5,14 +5,9 @@ import com.vividsolutions.jts.geom.Coordinate;
 import com.vividsolutions.jts.geom.GeometryFactory;
 import com.vividsolutions.jts.geom.Point;
 import com.vividsolutions.jts.geom.PrecisionModel;
-import de.komoot.photon.elasticsearch.Importer;
-import de.komoot.photon.elasticsearch.PhotonIndex;
 import de.komoot.photon.elasticsearch.Server;
-import de.komoot.photon.elasticsearch.Updater;
 import lombok.extern.slf4j.Slf4j;
-import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
 import org.elasticsearch.action.get.GetResponse;
-import org.elasticsearch.client.Client;
 import org.junit.jupiter.api.AfterEach;
 
 import java.io.File;
@@ -37,7 +32,7 @@ public class ESBaseTester {
     }
 
     protected GetResponse getById(int id) {
-        return getClient().prepareGet(PhotonIndex.NAME,PhotonIndex.TYPE, String.valueOf(id)).execute().actionGet();
+        return server.getById(id);
     }
 
 
@@ -63,35 +58,35 @@ public class ESBaseTester {
     }
 
     protected Importer makeImporter() {
-        return new Importer(getClient(), new String[]{"en"}, new String[]{});
+        return server.createImporter(new String[]{"en"}, new String[]{});
     }
 
     protected Importer makeImporterWithExtra(String... extraTags) {
-        return new Importer(getClient(), new String[]{"en"}, extraTags);
+        return server.createImporter(new String[]{"en"}, extraTags);
     }
 
     protected Importer makeImporterWithLanguages(String... languages) {
-        return new Importer(getClient(), languages, new String[]{});
+        return server.createImporter(languages, new String[]{});
     }
 
     protected Updater makeUpdater() {
-        return new Updater(getClient(), new String[]{"en"}, new String[]{});
+        return server.createUpdater(new String[]{"en"}, new String[]{});
     }
 
     protected Updater makeUpdaterWithExtra(String... extraTags) {
-        return new Updater(getClient(), new String[]{"en"}, extraTags);
+        return server.createUpdater(new String[]{"en"}, extraTags);
     }
 
-    protected Client getClient() {
+    protected Server getServer() {
         if (server == null) {
             throw new RuntimeException("call setUpES before using getClient");
         }
 
-        return server.getClient();
+        return server;
     }
 
     protected void refresh() {
-        getClient().admin().indices().refresh(new RefreshRequest(PhotonIndex.NAME)).actionGet();
+        server.refresh();
     }
 
     /**
diff --git a/src/test/java/de/komoot/photon/elasticsearch/DatabasePropertiesTest.java b/src/test/java/de/komoot/photon/elasticsearch/DatabasePropertiesTest.java
index c410b23..f665460 100644
--- a/src/test/java/de/komoot/photon/elasticsearch/DatabasePropertiesTest.java
+++ b/src/test/java/de/komoot/photon/elasticsearch/DatabasePropertiesTest.java
@@ -1,5 +1,6 @@
 package de.komoot.photon.elasticsearch;
 
+import de.komoot.photon.DatabaseProperties;
 import de.komoot.photon.ESBaseTester;
 import org.junit.jupiter.api.Test;
 
@@ -51,19 +52,4 @@ public class DatabasePropertiesTest extends ESBaseTester {
         assertArrayEquals(new String[]{"de", "en"}, prop.getLanguages());
     }
 
-    @Test
-    public void testSaveAndLoadFromDatabase() throws IOException {
-        setUpES();
-
-        DatabaseProperties prop = new DatabaseProperties();
-        prop.setLanguages(new String[]{"en", "de", "fr"});
-        prop.saveToDatabase(getClient());
-
-        prop = new DatabaseProperties();
-        prop.loadFromDatabase(getClient());
-
-        assertArrayEquals(new String[]{"en", "de", "fr"}, prop.getLanguages());
-
-
-    }
 }
diff --git a/src/test/java/de/komoot/photon/elasticsearch/ImporterTest.java b/src/test/java/de/komoot/photon/elasticsearch/ImporterTest.java
index 11712a6..521a568 100644
--- a/src/test/java/de/komoot/photon/elasticsearch/ImporterTest.java
+++ b/src/test/java/de/komoot/photon/elasticsearch/ImporterTest.java
@@ -2,6 +2,7 @@ package de.komoot.photon.elasticsearch;
 
 import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.PhotonDoc;
+import de.komoot.photon.Importer;
 import org.elasticsearch.action.get.GetResponse;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
diff --git a/src/test/java/de/komoot/photon/elasticsearch/ServerTest.java b/src/test/java/de/komoot/photon/elasticsearch/ServerTest.java
new file mode 100644
index 0000000..329a179
--- /dev/null
+++ b/src/test/java/de/komoot/photon/elasticsearch/ServerTest.java
@@ -0,0 +1,28 @@
+package de.komoot.photon.elasticsearch;
+
+import de.komoot.photon.DatabaseProperties;
+import de.komoot.photon.ESBaseTester;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ServerTest extends ESBaseTester {
+
+    @Test
+    public void testSaveAndLoadFromDatabase() throws IOException {
+        setUpES();
+
+        DatabaseProperties prop = new DatabaseProperties();
+        prop.setLanguages(new String[]{"en", "de", "fr"});
+        getServer().saveToDatabase(prop);
+
+        prop = new DatabaseProperties();
+        getServer().loadFromDatabase(prop);
+
+        assertArrayEquals(new String[]{"en", "de", "fr"}, prop.getLanguages());
+
+
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/de/komoot/photon/elasticsearch/UpdaterTest.java b/src/test/java/de/komoot/photon/elasticsearch/UpdaterTest.java
index 5d0a7ce..02ee605 100644
--- a/src/test/java/de/komoot/photon/elasticsearch/UpdaterTest.java
+++ b/src/test/java/de/komoot/photon/elasticsearch/UpdaterTest.java
@@ -2,6 +2,8 @@ package de.komoot.photon.elasticsearch;
 
 import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.PhotonDoc;
+import de.komoot.photon.Importer;
+import de.komoot.photon.Updater;
 import org.elasticsearch.action.get.GetResponse;
 import org.junit.jupiter.api.Test;
 
diff --git a/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java b/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java
index bed41f3..e2cfda6 100644
--- a/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java
@@ -1,25 +1,23 @@
 package de.komoot.photon.query;
 
-import com.google.common.collect.ImmutableMap;
 import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.Importer;
 import de.komoot.photon.PhotonDoc;
-import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.search.SearchHits;
+import org.json.JSONObject;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
- * Tests that the {@link PhotonQueryBuilder} produces query which can find all
- * expected results at all. These tests do not check relevance.
+ * Tests that the database backend produces queries which can find all
+ * expected results. These tests do not check relevance.
  */
 public class QueryBasicSearchTest extends ESBaseTester {
     private int testDocId = 10000;
@@ -40,25 +38,10 @@ public class QueryBasicSearchTest extends ESBaseTester {
         return new PhotonDoc(testDocId, "N", testDocId, "place", "city").names(nameMap);
     }
 
-    private SearchHits search(String query) {
-        QueryBuilder builder = PhotonQueryBuilder.builder(query, "en", Collections.singletonList("en"), false).buildQuery();
-        return getClient().prepareSearch("photon")
-                .setSearchType(SearchType.QUERY_THEN_FETCH)
-                .setQuery(builder)
-                .execute()
-                .actionGet()
-                .getHits();
+    private List<JSONObject> search(String query) {
+        return getServer().createSearchHandler(new String[]{"en"}).search(new PhotonRequest(query, 10, null, null, 0.2, 14, "en", false));
     }
 
-    private SearchHits searchLenient(String query) {
-        QueryBuilder builder = PhotonQueryBuilder.builder(query, "en", Collections.singletonList("en"), true).buildQuery();
-        return getClient().prepareSearch("photon")
-                .setSearchType(SearchType.QUERY_THEN_FETCH)
-                .setQuery(builder)
-                .execute()
-                .actionGet()
-                .getHits();
-    }
 
     @Test
     public void testSearchByDefaultName() {
@@ -67,20 +50,16 @@ public class QueryBasicSearchTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("muffle flu").getTotalHits());
-        assertEquals(1, search("flu").getTotalHits());
-        assertEquals(1, search("muffle").getTotalHits());
-        assertEquals(0, search("mufle flu").getTotalHits());
-
-        assertEquals(1, searchLenient("mufle flu").getTotalHits());
-        assertEquals(1, searchLenient("muffle flu 9").getTotalHits());
-        assertEquals(0, searchLenient("huffle fluff").getTotalHits());
+        assertEquals(1, search("muffle flu").size());
+        assertEquals(1, search("flu").size());
+        assertEquals(1, search("muffle").size());
+        assertEquals(1, search("mufle flu").size());
+        assertEquals(1, search("muffle flu 9").size());
+        assertEquals(0, search("huffle fluff").size());
     }
 
     @Test
     public void testSearchByAlternativeNames() {
-        final String[] alt_names = new String[]{"alt", "int", "loc", "old", "reg", "housename"};
-
         Importer instance = makeImporter();
         instance.add(createDoc("name", "original", "alt_name", "alt", "old_name", "older", "int_name", "int",
                                "loc_name", "local", "reg_name", "regional", "addr:housename", "house",
@@ -88,14 +67,14 @@ public class QueryBasicSearchTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("original").getTotalHits());
-        assertEquals(1, search("alt").getTotalHits());
-        assertEquals(1, search("older").getTotalHits());
-        assertEquals(1, search("int").getTotalHits());
-        assertEquals(1, search("local").getTotalHits());
-        assertEquals(1, search("regional").getTotalHits());
-        assertEquals(1, search("house").getTotalHits());
-        assertEquals(0, search("other").getTotalHits());
+        assertEquals(1, search("original").size());
+        assertEquals(1, search("alt").size());
+        assertEquals(1, search("older").size());
+        assertEquals(1, search("int").size());
+        assertEquals(1, search("local").size());
+        assertEquals(1, search("regional").size());
+        assertEquals(1, search("house").size());
+        assertEquals(0, search("other").size());
     }
 
     @Test
@@ -113,13 +92,12 @@ public class QueryBasicSearchTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("castillo").getTotalHits());
-        assertEquals(1, search("castillo callino").getTotalHits());
-        assertEquals(1, search("castillo quartier madrid").getTotalHits());
-        assertEquals(1, search("castillo block montagna estado").getTotalHits());
+        assertEquals(1, search("castillo").size());
+        assertEquals(1, search("castillo callino").size());
+        assertEquals(1, search("castillo quartier madrid").size());
+        assertEquals(1, search("castillo block montagna estado").size());
 
-        assertEquals(0, search("castillo state").getTotalHits());
-        //assertEquals(0, search("block montagna estado").getTotalHits());
+        assertEquals(0, search("castillo state thing").size());
     }
 
     @Test
@@ -129,11 +107,10 @@ public class QueryBasicSearchTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("hauptstrasse 5").getTotalHits());
-        assertEquals(1, search("edeka, hauptstrasse").getTotalHits());
-        assertEquals(1, search("edeka, hauptstrasse 5").getTotalHits());
-        assertEquals(1, search("edeka, hauptstr 5").getTotalHits());
-        //assertEquals(0, search("hauptstrasse").getTotalHits());
+        assertEquals(1, search("hauptstrasse 5").size());
+        assertEquals(1, search("edeka, hauptstrasse 5").size());
+        assertEquals(1, search("edeka, hauptstr 5").size());
+        assertEquals(1, search("edeka, hauptstrasse").size());
     }
 
     @Test
@@ -143,7 +120,7 @@ public class QueryBasicSearchTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("hauptstrasse 5").getTotalHits());
-        assertEquals(0, search("hauptstrasse").getTotalHits());
+        assertEquals(1, search("hauptstrasse 5").size());
+        assertEquals(0, search("hauptstrasse").size());
     }
 }
diff --git a/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java b/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java
index 97d8326..8e344f7 100644
--- a/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java
@@ -1,25 +1,27 @@
 package de.komoot.photon.query;
 
-import com.google.common.collect.ImmutableMap;
 import de.komoot.photon.*;
-import de.komoot.photon.elasticsearch.IndexSettings;
-import de.komoot.photon.elasticsearch.PhotonIndex;
+import lombok.extern.slf4j.Slf4j;
 import org.elasticsearch.action.get.GetResponse;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.index.query.QueryBuilder;
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
+import java.io.FileWriter;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Collections;
+import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.*;
 
-
+@Slf4j
 public class QueryByClassificationTest extends ESBaseTester {
+    @TempDir
+    static Path sharedTempDir;
+
     private int testDocId = 10000;
 
     @BeforeEach
@@ -28,19 +30,44 @@ public class QueryByClassificationTest extends ESBaseTester {
     }
 
     private PhotonDoc createDoc(String key, String value, String name) {
-       ImmutableMap<String, String> nameMap = ImmutableMap.of("name", name);
-
         ++testDocId;
-        return new PhotonDoc(testDocId, "W", testDocId, key, value).names(nameMap);
+        return new PhotonDoc(testDocId, "W", testDocId, key, value).names(Collections.singletonMap("name", name));
+    }
+
+    private List<JSONObject> search(String query) {
+        return getServer().createSearchHandler(new String[]{"en"}).search(new PhotonRequest(query));
     }
 
-    private SearchResponse search(String query) {
-        QueryBuilder builder = PhotonQueryBuilder.builder(query, "en", Collections.singletonList("en"), false).buildQuery();
-        return getClient().prepareSearch("photon")
-                .setSearchType(SearchType.QUERY_THEN_FETCH)
-                .setQuery(builder)
-                .execute()
-                .actionGet();
+    private void updateClassification(String key, String value, String... terms) {
+        JSONArray jsonTerms = new JSONArray();
+        for (String term : terms) {
+            jsonTerms.put(term);
+        }
+
+        JSONObject json = new JSONObject()
+                .put("classification_terms", new JSONArray()
+                        .put(new JSONObject()
+                                .put("key", key)
+                                .put("value", value)
+                                .put("terms", terms)
+                        ));
+
+        Path synonymPath = sharedTempDir.resolve("synonym.json");
+        try {
+            FileWriter fw = new FileWriter(synonymPath.toFile());
+            fw.write(json.toString(3));
+            fw.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        try {
+            getServer().setMaxShards(null);
+            getServer().updateIndexSettings(synonymPath.toString());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        getServer().waitForReady();
     }
 
     @Test
@@ -58,10 +85,10 @@ public class QueryByClassificationTest extends ESBaseTester {
         String classification = (String) response.getSource().get(Constants.CLASSIFICATION);
         assertEquals(classification, class_term);
 
-        SearchResponse result = search(class_term + " curli");
+        List<JSONObject> result = search(class_term + " curli");
 
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId), result.getHits().getHits()[0].getId());
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
     }
 
     @Test
@@ -71,23 +98,16 @@ public class QueryByClassificationTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        JSONArray terms = new JSONArray()
-                .put(new JSONObject()
-                        .put("key", "amenity")
-                        .put("value", "restaurant")
-                        .put("terms", new JSONArray().put("pub").put("kneipe"))
-                );
-        new IndexSettings().setClassificationTerms(terms).updateIndex(getClient(), PhotonIndex.NAME);
-        getClient().admin().cluster().prepareHealth().setWaitForYellowStatus().get();
+        updateClassification("amenity", "restaurant", "pub", "kneipe");
 
-        SearchResponse result = search("pub curli");
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId), result.getHits().getHits()[0].getId());
+        List<JSONObject> result = search("pub curli");
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
 
 
         result = search("curliflower kneipe");
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId), result.getHits().getHits()[0].getId());
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
     }
 
 
@@ -99,23 +119,16 @@ public class QueryByClassificationTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        JSONArray terms = new JSONArray()
-                .put(new JSONObject()
-                        .put("key", "aeroway")
-                        .put("value", "terminal")
-                        .put("terms", new JSONArray().put("airport"))
-                );
-        new IndexSettings().setClassificationTerms(terms).updateIndex(getClient(), PhotonIndex.NAME);
-        getClient().admin().cluster().prepareHealth().setWaitForYellowStatus().get();
+        updateClassification("aeroway", "terminal", "airport");
 
-        SearchResponse result = search("airport");
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId - 1), result.getHits().getHits()[0].getId());
+        List<JSONObject> result = search("airport");
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId - 1, result.get(0).getJSONObject("properties").getInt("osm_id"));
 
 
         result = search("airport houston");
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId), result.getHits().getHits()[0].getId());
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
     }
 
     @Test
@@ -126,29 +139,44 @@ public class QueryByClassificationTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        JSONArray terms = new JSONArray()
-                .put(new JSONObject()
-                        .put("key", "railway")
-                        .put("value", "station")
-                        .put("terms", new JSONArray().put("Station"))
-                ).put(new JSONObject()
-                        .put("key", "railway")
-                        .put("value", "halt")
-                        .put("terms", new JSONArray().put("Station").put("Stop"))
-                );
-        new IndexSettings().setClassificationTerms(terms).updateIndex(getClient(), PhotonIndex.NAME);
-        getClient().admin().cluster().prepareHealth().setWaitForYellowStatus().get();
-
-        SearchResponse result = search("Station newtown");
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId - 1), result.getHits().getHits()[0].getId());
+        JSONObject json = new JSONObject()
+                .put("classification_terms", new JSONArray()
+                        .put(new JSONObject()
+                                .put("key", "railway")
+                                .put("value", "station")
+                                .put("terms", new JSONArray().put("Station"))
+                        ).put(new JSONObject()
+                                .put("key", "railway")
+                                .put("value", "halt")
+                                .put("terms", new JSONArray().put("Station").put("Stop"))
+                        ));
+        Path synonymPath = sharedTempDir.resolve("synonym.json");
+        try {
+            FileWriter fw = new FileWriter(synonymPath.toFile());
+            fw.write(json.toString(3));
+            fw.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        try {
+            getServer().setMaxShards(null);
+            getServer().updateIndexSettings(synonymPath.toString());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        getServer().waitForReady();
+
+        List<JSONObject> result = search("Station newtown");
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId - 1, result.get(0).getJSONObject("properties").getInt("osm_id"));
 
         result = search("newtown stop");
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId - 1), result.getHits().getHits()[0].getId());
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId - 1, result.get(0).getJSONObject("properties").getInt("osm_id"));
 
         result = search("king's cross Station");
-        assertTrue(result.getHits().getTotalHits() > 0);
-        assertEquals(Integer.toString(testDocId), result.getHits().getHits()[0].getId());
+        assertTrue(result.size() > 0);
+        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
     }
 }
diff --git a/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java b/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java
index 0b42681..ab04e97 100644
--- a/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java
@@ -2,14 +2,12 @@ package de.komoot.photon.query;
 
 import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.PhotonDoc;
-import de.komoot.photon.elasticsearch.Importer;
+import de.komoot.photon.Importer;
 import de.komoot.photon.nominatim.model.AddressType;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.index.query.QueryBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.json.JSONObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.EmptySource;
 import org.junit.jupiter.params.provider.EnumSource;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -21,12 +19,13 @@ import java.util.*;
 /**
  * Tests for queries in different languages.
  */
+@Slf4j
 public class QueryByLanguageTest extends ESBaseTester {
     private int testDocId = 10001;
-    private List<String> languageList;
+    private String[] languageList;
 
     private Importer setup(String... languages) throws IOException {
-        languageList = Arrays.asList(languages);
+        languageList = languages;
         setUpES(languages);
         return makeImporterWithLanguages(languages);
     }
@@ -42,13 +41,8 @@ public class QueryByLanguageTest extends ESBaseTester {
         return new PhotonDoc(testDocId, "W", testDocId, "place", "city").names(nameMap);
     }
 
-    private SearchResponse search(String query, String lang) {
-        QueryBuilder builder = PhotonQueryBuilder.builder(query, lang, languageList, false).buildQuery();
-        return getClient().prepareSearch("photon")
-                .setSearchType(SearchType.QUERY_THEN_FETCH)
-                .setQuery(builder)
-                .execute()
-                .actionGet();
+    private List<JSONObject> search(String query, String lang) {
+        return getServer().createSearchHandler(languageList).search(new PhotonRequest(query, 10, null, null, 0.2, 14, lang, true));
     }
 
     @Test
@@ -60,12 +54,12 @@ public class QueryByLanguageTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("original", "en").getHits().getTotalHits());
-        assertEquals(1, search("finish", "en").getHits().getTotalHits());
-        assertEquals(0, search("russian", "en").getHits().getTotalHits());
+        assertEquals(1, search("original", "en").size());
+        assertEquals(1, search("finish", "en").size());
+        assertEquals(0, search("russian", "en").size());
 
-        float enScore = search("finish", "en").getHits().getHits()[0].getScore();
-        float fiScore = search("finish", "fi").getHits().getHits()[0].getScore();
+        float enScore = search("finish", "en").get(0).getFloat("score");
+        float fiScore = search("finish", "fi").get(0).getFloat("score");
 
         assertTrue(fiScore > enScore);
     }
@@ -77,9 +71,9 @@ public class QueryByLanguageTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("simple", "de").getHits().getTotalHits());
-        assertEquals(1, search("einfach", "de").getHits().getTotalHits());
-        assertEquals(1, search("ancient", "de").getHits().getTotalHits());
+        assertEquals(1, search("simple", "de").size());
+        assertEquals(1, search("einfach", "de").size());
+        assertEquals(1, search("ancient", "de").size());
 
     }
 
@@ -101,7 +95,7 @@ public class QueryByLanguageTest extends ESBaseTester {
         instance.finish();
         refresh();
 
-        assertEquals(1, search("here, original", "de").getHits().getTotalHits());
-        assertEquals(1, search("here, Deutsch", "de").getHits().getTotalHits());
+        assertEquals(1, search("here, original", "de").size());
+        assertEquals(1, search("here, Deutsch", "de").size());
     }
 }
diff --git a/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java b/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java
index f722d98..7a8605c 100644
--- a/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java
@@ -2,26 +2,22 @@ package de.komoot.photon.query;
 
 import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.PhotonDoc;
-import de.komoot.photon.elasticsearch.Importer;
+import de.komoot.photon.Importer;
 
-import java.util.Arrays;
-import java.util.Collections;
+import java.util.List;
 
-import de.komoot.photon.elasticsearch.PhotonIndex;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import org.json.JSONObject;
+import org.junit.jupiter.api.*;
 
 import static org.junit.jupiter.api.Assertions.*;
 
-
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 public class QueryFilterTagValueTest extends ESBaseTester {
-    private static final String[] TAGS = new String[]{"tourism", "attraction", "tourism", "hotel", "tourism", "museum", "tourism", "information", "amenity",
-            "parking", "amenity", "restaurant", "amenity", "information", "food", "information", "railway", "station"};
+    private static final String[] TAGS = new String[]{"tourism", "attraction", "tourism", "hotel", "tourism", "museum",
+                                                      "tourism", "information", "amenity", "parking", "amenity", "restaurant",
+                                                      "amenity", "information", "food", "information", "railway", "station"};
 
-    @BeforeEach
+    @BeforeAll
     public void setUp() throws Exception {
         setUpES();
         Importer instance = makeImporter();
@@ -43,12 +39,22 @@ public class QueryFilterTagValueTest extends ESBaseTester {
         refresh();
     }
 
-    private PhotonQueryBuilder baseQueryBerlin() {
-        return PhotonQueryBuilder.builder("berlin", "en", Arrays.asList("en"), true);
+    @AfterAll
+    @Override
+    public void tearDown() {
+        deleteIndex();
+        shutdownES();
+    }
+
+    private PhotonRequest queryBerlinWithTags(String... params) {
+        PhotonRequest request = new PhotonRequest("berlin", 100, null, null, 0.2, 14, "en", false);
+        request.setUpTagFilters(params);
+
+        return request;
     }
 
-    private SearchResponse search(QueryBuilder queryBuilder) {
-        return getClient().prepareSearch(PhotonIndex.NAME).setSearchType(SearchType.QUERY_THEN_FETCH).setQuery(queryBuilder).execute().actionGet();
+    private List<JSONObject> search(PhotonRequest request) {
+        return getServer().createSearchHandler(new String[]{"en"}).search(request);
     }
 
     /**
@@ -56,13 +62,9 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testFilterWithTagTourismAttraction() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withTags(Collections.singletonMap("tourism", Collections.singleton("attraction")))
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism:attraction"));
 
-        assertEquals(2l, searchResponse.getHits().getTotalHits());
+        assertEquals(2l, searchResponse.size());
     }
 
     /**
@@ -70,13 +72,9 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testValueAttraction() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withValues("attraction")
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+        List<JSONObject> searchResponse = search(queryBerlinWithTags(":attraction"));
 
-        assertEquals(2l, searchResponse.getHits().getTotalHits());
+        assertEquals(2l, searchResponse.size());
     }
 
     /**
@@ -84,13 +82,9 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyTourism() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withKeys("tourism")
-                .buildQuery();
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism"));
 
-        SearchResponse searchResponse = search(queryBuilder);
-
-        assertEquals(8l, searchResponse.getHits().getTotalHits());
+        assertEquals(8l, searchResponse.size());
     }
 
     /**
@@ -98,13 +92,9 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testFilterWithoutTagTourismAttraction() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withoutTags(Collections.singletonMap("tourism", Collections.singleton("attraction")))
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism:attraction"));
 
-        assertEquals(16l, searchResponse.getHits().getTotalHits());
+        assertEquals(16l, searchResponse.size());
     }
 
     /**
@@ -112,62 +102,39 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testValueNotInformation() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withoutValues("information")
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("!:information"));
 
-        assertEquals(12l, searchResponse.getHits().getTotalHits());
+        assertEquals(12l, searchResponse.size());
     }
 
     /**
-     * Find me all places named "berlin" that do not have the key "tourism" in their TAGS
+     * Find me all places named "berlin" that do not have the value "information" in their TAGS - no matter what key
      */
     @Test
-    public void testKeyNotTourism() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withoutKeys("tourism")
-                .buildQuery();
+    public void testValueNotInformationAlt() {
+        List<JSONObject> searchResponse = search(queryBerlinWithTags(":!information"));
 
-        SearchResponse searchResponse = search(queryBuilder);
-
-        assertEquals(10l, searchResponse.getHits().getTotalHits());
+        assertEquals(12l, searchResponse.size());
     }
 
     /**
-     * Find me all places named "berlin" that are tagged with the key "tourism" but not tagged with value "information".
-     * <p/>
-     * Note: This is a different method of achieving the same result as
-     * {@link QueryFilterTagValueTest#testKeyTourismButValueNotInformation()}
+     * Find me all places named "berlin" that do not have the key "tourism" in their TAGS
      */
     @Test
-    public void testKeyTourismAndValueNotInformation() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withKeys("tourism")
-                .withoutValues("information")
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+    public void testKeyNotTourism() {
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism"));
 
-        assertEquals(6l, searchResponse.getHits().getTotalHits());
+        assertEquals(10l, searchResponse.size());
     }
 
     /**
      * Find me all places named "berlin" that are tagged with the key "tourism" but not tagged with value "information".
-     * <p/>
-     * Note: This is a different method of achieving the same result as
-     * {@link QueryFilterTagValueTest#testKeyTourismAndValueNotInformation}.
      */
     @Test
-    public void testKeyTourismButValueNotInformation() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withTagsNotValues(Collections.singletonMap("tourism", Collections.singleton("information")))
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+    public void testKeyTourismAndValueNotInformation() {
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism:!information"));
 
-        assertEquals(6l, searchResponse.getHits().getTotalHits());
+        assertEquals(6l, searchResponse.size());
     }
 
     /**
@@ -175,13 +142,9 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyNotTourismAndKeyNotAmenity() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withoutKeys("tourism", "amenity")
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism", "!amenity"));
 
-        assertEquals(4l, searchResponse.getHits().getTotalHits());
+        assertEquals(4l, searchResponse.size());
     }
 
     /**
@@ -191,14 +154,9 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyTourismAndKeyNotAmenity() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withKeys("tourism")
-                .withoutKeys("amenity")
-                .buildQuery();
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism", "!amenity"));
 
-        SearchResponse searchResponse = search(queryBuilder);
-
-        assertEquals(8l, searchResponse.getHits().getTotalHits());
+        assertEquals(8l, searchResponse.size());
     }
 
     /**
@@ -206,14 +164,9 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testValueInformationButKeyNotAmenity() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withValues("information")
-                .withoutKeys("amenity")
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+        List<JSONObject> searchResponse = search(queryBerlinWithTags(":information", "!amenity"));
 
-        assertEquals(4l, searchResponse.getHits().getTotalHits());
+        assertEquals(4l, searchResponse.size());
     }
 
     /**
@@ -221,12 +174,8 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testTagNotTourismAttraction() {
-        QueryBuilder queryBuilder = baseQueryBerlin()
-                .withoutTags(Collections.singletonMap("tourism", Collections.singleton("attraction")))
-                .buildQuery();
-
-        SearchResponse searchResponse = search(queryBuilder);
+        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism:attraction"));
 
-        assertEquals(16l, searchResponse.getHits().getTotalHits());
+        assertEquals(16l, searchResponse.size());
     }
 }
diff --git a/src/test/java/de/komoot/photon/utils/ConvertToJsonTest.java b/src/test/java/de/komoot/photon/utils/ConvertToJsonTest.java
index 357d192..436b7fd 100644
--- a/src/test/java/de/komoot/photon/utils/ConvertToJsonTest.java
+++ b/src/test/java/de/komoot/photon/utils/ConvertToJsonTest.java
@@ -2,36 +2,33 @@ package de.komoot.photon.utils;
 
 import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.PhotonDoc;
-import de.komoot.photon.elasticsearch.DatabaseProperties;
-import de.komoot.photon.elasticsearch.Importer;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.index.query.QueryBuilders;
+import de.komoot.photon.Importer;
+import de.komoot.photon.query.PhotonRequest;
 import org.json.JSONObject;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 public class ConvertToJsonTest extends ESBaseTester {
 
-    private SearchResponse databaseFromDoc(PhotonDoc doc) throws IOException {
+    private List<JSONObject> databaseFromDoc(PhotonDoc doc) throws IOException {
+        final String name = "abcde";
+        doc.names(Collections.singletonMap("name", name));
+
         setUpES();
         Importer instance = makeImporterWithExtra("maxspeed","website");
         instance.add(doc);
         instance.finish();
         refresh();
 
-        return getClient().prepareSearch("photon")
-                .setSearchType(SearchType.QUERY_THEN_FETCH)
-                .setQuery(QueryBuilders.boolQuery().must(QueryBuilders.matchAllQuery())
-                        .filter(QueryBuilders.boolQuery().mustNot(QueryBuilders.idsQuery().addIds(DatabaseProperties.PROPERTY_DOCUMENT_ID))))
-                .execute()
-                .actionGet();
+        // Indirectly calls converttojson
+        return getServer().createSearchHandler(new String[]{"en", "de", "fr"}).search(new PhotonRequest(name));
     }
 
     @Test
@@ -40,9 +37,7 @@ public class ConvertToJsonTest extends ESBaseTester {
         extratags.put("website", "foo");
         extratags.put("maxspeed", "100 mph");
 
-        SearchResponse response = databaseFromDoc(new PhotonDoc(1234, "N", 1000, "place", "city").extraTags(extratags));
-
-        List<JSONObject> json = new ConvertToJson("de").convert(response, false);
+        List<JSONObject> json = databaseFromDoc(new PhotonDoc(1234, "N", 1000, "place", "city").extraTags(extratags));
 
         JSONObject extra = json.get(0).getJSONObject("properties").getJSONObject("extra");
 
@@ -54,9 +49,7 @@ public class ConvertToJsonTest extends ESBaseTester {
 
     @Test
     public void testConvertWithoutExtraTags() throws IOException {
-        SearchResponse response = databaseFromDoc(new PhotonDoc(1234, "N", 1000, "place", "city"));
-
-        List<JSONObject> json = new ConvertToJson("de").convert(response, false);
+        List<JSONObject> json = databaseFromDoc(new PhotonDoc(1234, "N", 1000, "place", "city"));
 
         assertNull(json.get(0).getJSONObject("properties").optJSONObject("extra"));
     }