Offline search features
The ability to search for places efficiently is crucial, whether one is connected to the internet or working in an offline environment. To address both scenarios, the HERE SDK offers two versatile search solutions: the online-only SearchEngine and its offline equivalent, the OfflineSearchEngine. The OfflineSearchEngine is designed to provide the same robust search capabilities as its online counterpart, ensuring seamless information retrieval even without an internet connection. Just like the SearchEngine, the OfflineSearchEngine can be easily constructed and integrated into your applications:
try {
// Allows to search on already downloaded or cached map data.
offlineSearchEngine = new OfflineSearchEngine();
} catch (InstantiationErrorException e) {
throw new RuntimeException("Initialization of OfflineSearchEngine failed: " + e.error.name());
}
try {
// Allows to search on already downloaded or cached map data.
offlineSearchEngine = OfflineSearchEngine()
} catch (e: InstantiationErrorException) {
throw RuntimeException("Initialization of OfflineSearchEngine failed: " + e.error.name)
}
The OfflineSearchEngine provides almost the same interfaces as the SearchEngine, but the results may slightly differ as the results are taken from already downloaded or cached map data instead of initiating a new request to a HERE backend service.
This way the data may be, for example, older compared to the data you may receive when using the SearchEngine. On the other hand, this class may provide results faster - as no online connection is necessary.
NoteYou can only search on already cached or preloaded offline maps data. When you use only cached map data, it may happen that not all tiles are loaded. In that case, no results can be found until also these tiles are loaded. With offline maps this cannot happen and the required map data is guaranteed to be available for the downloaded region. Therefore, it is recommended to not rely on cached map data.
Although most of the available OfflineSearchEngine interfaces are also available in the SearchEngine, the opposite is not the case, simply because not every online feature is also accessible from offline data.
In addition, the place ID that identifies the location in the HERE Places API is different for offline search results.
Usually, with a free-form TextQuery, places can be found offline at least within a radius of 62.5 Km. Capitals can be found globally from anywhere. Adding a dedicated location such as a city name to the query string will search nearby that location.
Below we show one possible use case for the OfflineSearchEngine. For example, when you are on the go, your connection can be temporarily lost. In such a case it makes sense to search in the already downloaded map data.
To do so, first you need to check if the device has lost its connectivity. As a second step, you can use the preferred search engine:
private boolean isDeviceConnected() {
// An application may define here a logic to determine whether a device is connected or not.
return isDeviceConnected;
}
...
if (isDeviceConnected()) {
searchEngine.searchByText(query, searchOptions, querySearchCallback);
} else {
offlineSearchEngine.searchByText(query, searchOptions, querySearchCallback);
}
// An application may define here a logic to determine whether a device is connected or not.
// For this example app, the flag is set from UI.
private var isDeviceConnected = true
...
if (isDeviceConnected) {
searchEngine.searchByText(query, searchOptions, querySearchCallback)
} else {
offlineSearchEngine.searchByText(query, searchOptions, querySearchCallback)
}
To handle the search results, you can set the same querySearchCallback (as already shown above).
In a similar fashion, you can also reverse geocode addresses:
if (isDeviceConnected()) {
searchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions, addressSearchCallback);
} else {
offlineSearchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions, addressSearchCallback);
}
if (isDeviceConnected) {
searchEngine.searchByCoordinates(
geoCoordinates,
reverseGeocodingOptions,
addressSearchCallback
)
} else {
offlineSearchEngine.searchByCoordinates(
geoCoordinates,
reverseGeocodingOptions,
addressSearchCallback
)
}
Or you can geocode an address to a geographic coordinate with:
if (isDeviceConnected()) {
searchEngine.searchByAddress(query, options, geocodeAddressSearchCallback);
} else {
offlineSearchEngine.searchByAddress(query, options, geocodeAddressSearchCallback);
}
if (isDeviceConnected) {
searchEngine.searchByAddress(query, options, geocodeAddressSearchCallback)
} else {
offlineSearchEngine.searchByAddress(query, options, geocodeAddressSearchCallback)
}
A possible solution to check the connectivity of your device can be implemented by trying to make an actual connection first - and when that fails, you can switch to the OfflineSearchEngine - or the other way round: you can try offline search first, but when no map data is available, you can try online.
NoteYou can find the full code for this section as part of the
SearchHybridexample app on GitHub.
Use offline search with indexing
In order to improve search results for the OfflineSearchEngine you can set OfflineSearchIndex.Options. By default, indexing is disabled.
Indexing provides a mechanism to find places in installed Region data - even if they are far away from the provided search center. This will help to match the behavior of the SearchEngine which does this, by default, without the need to create a search index as it operates only online.
You can set a OfflineSearchIndexListener to track the index building process.
NoteThis is a beta release of this feature, so there could be a few bugs and unexpected behaviors. APIs may change for new releases without a deprecation process.
Internally, indexing creates additional data on the device to make all installed map content searchable.
If indexing is enabled it will affect all operations that modify persistent storage content:
mapDownloader.downloadRegions(..)mapDownloader.deleteRegions(..)mapDownloader.clearPersistentMapStorage(..)mapDownloader.repairPersistentMap(..)mapUpdater.performMapUpdate(..)mapUpdater.updateCatalog(..)
These methods will ensure that the index is created, deleted or updated to contain entries from Region data that are installed in the persistent storage.
Creating an index takes time and this will make all operations that download or update offline maps longer, but usually not more than a few seconds up to a couple of minutes (depending on the amount of installed offline maps data). The stored index also increases the space occupied by offline maps by around 5%.
The enableOfflineSearchIndexing(...) method demonstrates how to enable indexing for the OfflineSearchEngine and how to implement an IndexListener to monitor the indexing process.
private void enableOfflineSearchIndexing(SDKNativeEngine sdkNativeEngine) {
offlineSearchIndexOptions = new OfflineSearchIndex.Options();
offlineSearchIndexOptions.enabled = true;
offlineSearchIndexListener = new OfflineSearchIndexListener() {
@Override
public void onStarted(@NonNull OfflineSearchIndex.Operation operation) {
Log.d("OfflineSearchIndexListener", "Indexing started. Operation: " + operation);
snackbar.setText("Indexing started: " + operation).show();
}
@Override
public void onProgress(int percentage) {
Log.d("OfflineSearchIndexListener", "Indexing progress: " + percentage + "%");
snackbar.setText("Indexing progress: " + percentage + "%").show();
}
@Override
public void onComplete(@Nullable OfflineSearchIndex.Error error) {
if (error == null) {
Log.d("OfflineSearchIndexListener", "Indexing completed successfully.");
snackbar.setText("Indexing completed successfully.").show();
} else {
Log.e("OfflineSearchIndexListener", "Indexing failed: " + error.name());
snackbar.setText("Indexing failed: " + error.name()).show();
}
}
};
offlineSearchEngine.setIndexOptions(sdkNativeEngine, offlineSearchIndexOptions, offlineSearchIndexListener);
}
private fun enableOfflineSearchIndexing(sdkNativeEngine: SDKNativeEngine) {
offlineSearchIndexOptions = OfflineSearchIndex.Options()
offlineSearchIndexOptions!!.enabled = true
offlineSearchIndexListener = object : OfflineSearchIndexListener {
override fun onStarted(operation: OfflineSearchIndex.Operation) {
Log.d(
"OfflineSearchIndexListener",
"Indexing started. Operation: $operation"
)
snackbar.show("Indexing started: $operation")
}
override fun onProgress(percentage: Int) {
Log.d("OfflineSearchIndexListener", "Indexing progress: $percentage%")
snackbar.show("Indexing progress: $percentage%")
}
override fun onComplete(error: OfflineSearchIndex.Error?) {
if (error == null) {
Log.d("OfflineSearchIndexListener", "Indexing completed successfully.")
snackbar.show("Indexing completed successfully.")
} else {
Log.e("OfflineSearchIndexListener", "Indexing failed: " + error.name)
snackbar.show("Indexing failed: " + error.name)
}
}
}
OfflineSearchEngine.setIndexOptions(
sdkNativeEngine,
offlineSearchIndexOptions, offlineSearchIndexListener
)
}
Above, an OfflineSearchIndexListener is implemented to track the indexing process. This listener provides three callbacks to monitor the status of indexing. The onStarted() callback is called when indexing starts. This happens if map data changes or when OfflineSearchEngine.setIndexOptions() is called. No action is taken if the existing index matches the installed map regions. As indexing progresses, the onProgress() callback reports the current progress as a percentage to indicate the progress of index creation or deletion. Once indexing is completed, the onComplete() callback is invoked. If successful, error is null. If it fails, the error provides details about the problem. Notifications are only sent if the index needs to be rebuilt.
Bring your own places for offline use
The HERE SDK allows to bring your own places at runtime when searching for places offline.
With the OfflineSearchEngine it is possible to inject custom data for places that can be found with the offline. Such personal places can be found by regular queries. The results are ranked as other places coming directly from HERE.
You can create multiple GeoPlace instances that can contain your custom place data. These instances can then be added to a MyPlaces data source:
List<GeoPlace> geoPlaces = Arrays.asList (
GeoPlace.makeMyPlace("Pizza Pino", new GeoCoordinates(52.518032, 13.420632)),
GeoPlace.makeMyPlace("PVR mdh", new GeoCoordinates(52.51772, 13.42038)),
GeoPlace.makeMyPlace("Harley's bar", new GeoCoordinates(52.51764, 13.42062))
);
myPlaces.addPlaces(geoPlaces, new OnTaskCompleted() {
@Override
public void onTaskCompleted(@NonNull TaskOutcome taskOutcome) {
if (taskOutcome == TaskOutcome.COMPLETED) {
// Task completed
} else {
// Task cancelled
}
}
});
This data source can then be attached to the OfflineSearchEngine:
offlineSearchEngine.attach(myPlaces, new OnTaskCompleted() {
@Override
public void onTaskCompleted(@NonNull TaskOutcome taskOutcome) {
if (taskOutcome == TaskOutcome.COMPLETED){
// Task completed
} else {
// Task cancelled
}
}
});
addPlaces() and attach() operate asynchronously as thousands of places may be added at once by an application. Both methods return a TaskHandle, so that the operation can be cancelled. The TaskOutcomeevent informs when the operation has finished.
Below is one example how you can find such places:
int radiusInMeters = 100;
TextQuery.Area queryArea = new TextQuery.Area(new GeoCircle(new GeoCoordinates(52.518032, 13.420632), radiusInMeters));
TextQuery textQuery = new TextQuery("Pizza Pino", queryArea);
offlineSearchEngine.searchByText(textQuery, new SearchOptions(), new SearchCallback() {
@Override
public void onSearchCompleted(@Nullable SearchError searchError, @Nullable List<Place> list) {
// ...
}
});
Note that the added places stay in memory as long as the offlineSearchEngine instance is alive.
Distributed results along corridors
The OfflineSearchEngine supports a search feature called distributedResults that ensures search results are well-distributed along a route corridor. This feature is particularly useful when you want to find points of interest evenly spaced along a travel route, rather than having all results clustered in one area. To use the distributedResults feature, you must set distributedResults = true in your SearchOptions and create a corridor area using GeoCorridor with multiple coordinate points. Note that this feature uses parallelization and is only supported for:
searchByCategorywhenCategoryQuery.Area.corridorAreais providedsearchByTextwhenTextQuery.Area.corridorAreais provided
NoteThe
distributedResultsfeature is only available with theOfflineSearchEngineand requires offline map data to be available for the corridor area.
void testDistributedResultsWithCorridor() {
// Create a corridor area for the search - this is required for distributedResults to work
List<GeoCoordinates> corridorPoints = Arrays.asList(
new GeoCoordinates(52.520798, 13.409408), // Start point (Berlin)
new GeoCoordinates(52.518032, 13.420632), // Middle point
new GeoCoordinates(52.515597, 13.377704), // End point
new GeoCoordinates(52.512000, 13.390000) // Additional point for more complex corridor
);
CategoryQuery.Area queryArea = new CategoryQuery.Area(new GeoCorridor(corridorPoints), new GeoCoordinates(52.518032, 13.420632));
List<PlaceCategory> categoryList = new ArrayList<>();
categoryList.add(new PlaceCategory(PlaceCategory.EAT_AND_DRINK));
categoryList.add(new PlaceCategory(PlaceCategory.SHOPPING_ELECTRONICS));
CategoryQuery categoryQuery = new CategoryQuery(categoryList, queryArea);
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 50;
searchOptions.distributedResults = true;
offlineSearchEngine.searchByCategory(categoryQuery, searchOptions, new SearchCallback() {
@Override
public void onSearchCompleted(SearchError searchError, List<Place> list) {
if (searchError != null) {
// Handle error.
return;
}
// If error is null, list is guaranteed to be not empty.
String numberOfResults = "Search results: " + list.size() + ". See log for details.";
for (int i = 0; i < list.size(); i++) {
Place searchResult = list.get(i);
String name = searchResult.getTitle();
String address = searchResult.getAddress().addressText;
GeoCoordinates coords = searchResult.getGeoCoordinates();
Log.d("DistributedSearch", String.format(
"Result %d/%d: %s at %s (%.6f, %.6f)",
i+1, list.size(), name, address, coords.latitude, coords.longitude
));
}
}
});
}
Updated yesterday