Geosearch

MeiliSearch allows you to filter and sort results based on their geographic location. This can be useful when you only want results within a specific area or when sorting results based on their distance from a specific location.

In order to start filtering and sorting documents based on their geographic location, you must make sure they contain a valid _geo field.

_geo is a reserved field. If you include it in your documents, MeiliSearch expects its value to conform to a specific format.

When using JSON and NDJSON, _geo must contain an object with two keys: lat and lng. Both must contain a floating point number indicating, respectively, latitude and longitude:

  1. {
  2. "_geo": {
  3. "lat": 0.0,
  4. "lng": 0.0
  5. }
  6. }

Examples

Suppose we have a JSON array containing a few restaurants:

  1. [
  2. {
  3. "id": 1,
  4. "name": "Nàpiz' Milano",
  5. "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy",
  6. "type": "pizza",
  7. "rating": 9
  8. },
  9. {
  10. "id": 2,
  11. "name": "Bouillon Pigalle",
  12. "address": "22 Bd de Clichy, 75018 Paris, France",
  13. "type": "french",
  14. "rating": 8
  15. },
  16. {
  17. "id": 3,
  18. "name": "Artico Gelateria Tradizionale",
  19. "address": "Via Dogana, 1, 20123 Milan, Italy",
  20. "type": "ice cream",
  21. "rating": 10
  22. }
  23. ]

Our restaurant dataset looks like this once we add geopositioning data:

  1. [
  2. {
  3. "id": 1,
  4. "name": "Nàpiz' Milano",
  5. "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy",
  6. "type": "pizza",
  7. "rating": 9,
  8. "_geo": {
  9. "lat": 45.4777599,
  10. "lng": 9.1967508
  11. }
  12. },
  13. {
  14. "id": 2,
  15. "name": "Bouillon Pigalle",
  16. "address": "22 Bd de Clichy, 75018 Paris, France",
  17. "type": "french",
  18. "rating": 8,
  19. "_geo": {
  20. "lat": 48.8826517,
  21. "lng": 2.3352748
  22. }
  23. },
  24. {
  25. "id": 3,
  26. "name": "Artico Gelateria Tradizionale",
  27. "address": "Via Dogana, 1, 20123 Milan, Italy",
  28. "type": "ice cream",
  29. "rating": 10,
  30. "_geo": {
  31. "lat": 45.4632046,
  32. "lng": 9.1719421
  33. }
  34. }
  35. ]

WARNING

Trying to index a dataset with one or more documents containing badly formatted _geo values will cause MeiliSearch to throw an invalid_geo_field error. In this case, the update will fail and no documents will be added or modified.

Using _geo with CSV

If your dataset is formatted as CSV, the file header must have a _geo column. Each row in the dataset must then contain a column with a comma-separated string indicating latitude and longitude:

  1. "id:number", "name:string", "address:string", "type:string", "rating:number", "_geo:string"
  2. "1", "Nàpiz Milano", "Viale Vittorio Veneto, 30, 20124, Milan, Italy","pizzeria", 9, "45.4777599,9.1967508"
  3. "2", "Bouillon Pigalle", "22 Bd de Clichy, 75018 Paris, France", "french", 8, "48.8826517,2.3352748"
  4. "3", "Artico Gelateria Tradizionale", "Via Dogana, 1, 20123 Milan, Italy", "ice cream", 10, "48.8826517,2.3352748"

Filtering results with _geoRadius

You can use _geo data to filter queries and make sure you only receive results located within a certain geographic area.

Configuration

In order to filter results based on their location, you must add the _geo attribute to the filterableAttributes list:

<>

cURL

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/restaurants/settings/filterable-attributes' \
  3. -H 'Content-type:application/json' \
  4. --data-binary '[
  5. "_geo",
  6. ]'
  1. client.index('restaurants')
  2. .updateFilterableAttributes([
  3. '_geo'
  4. ])
  1. client.index('restaurants').update_filterable_attributes([
  2. '_geo'
  3. ])
  1. $client->index('restaurants')->updateFilterableAttributes([
  2. '_geo'
  3. ]);
  1. Settings settings = new Settings();
  2. settings.setFilterableAttributes(new String[] {"_geo"});
  3. client.index("restaurants").updateSettings(settings);
  1. client.index('restaurants').update_filterable_attributes(['_geo'])
  1. filterableAttributes := []string{
  2. "_geo",
  3. }
  4. client.Index("restaurants").UpdateFilterableAttributes(&filterableAttributes)
  1. let progress: Progress = restaurants.set_filterable_attributes(&["_geo"]).await.unwrap();
  1. client.index("restaurants").updateFilterableAttributes(["_geo"]) { (result: Result<Update, Swift.Error>) in
  2. switch result {
  3. case .success(let update):
  4. print(update)
  5. case .failure(let error):
  6. print(error)
  7. }
  8. }
  1. await client.index('restaurants').updateFilterableAttributes(['_geo']);

Note that MeiliSearch will rebuild your index whenever you update filterableAttributes. Depending on the size of your dataset, this might take a considerable amount of time.

You can read more about configuring filterableAttributes in our dedicated filtering guide.

Usage

Once you made sure all your documents contain valid geolocation data and added the _geo attribute to the filterableAttributes list, you can use filter and _geoRadius to ensure MeiliSearch only returns results located within a specific geographic area.

_geoRadius establishes a circular area based on a central point and a radius. Results beyond this area will be excluded from your search. This filter rule requires three parameters: lat, lng and distance_in_meters.

  1. _geoRadius(lat, lng, distance_in_meters)

lat and lng must be floating point numbers indicating a geographic position. distance_in_meters must be an integer indicating the radius covered by the _geoRadius filter.

_geoRadius must always be used with the filter search parameter. The following filter expression would only include results within 1km of the Eiffel Tower:

  1. { "filter": "_geoRadius(48.8583701, 2.2922926, 1000)" }

If any of lat, lng, or distance_in_meters are invalid or missing, MeiliSearch will return an invalid_filter error.

You can read more about using filter in our dedicated guide.

WARNING

_geo, _geoDistance, and _geoPoint are not valid filter rules. Trying to use any of them with the filter search parameter will result in an invalid_filter error.

Examples

_geoRadius works like any other filter rule. We can search for places to eat near the centre of Milan:

<>

cURL

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl -X POST 'http://localhost:7700/indexes/restaurants/search' \
  2. -H 'Content-type:application/json' \
  3. --data-binary '{ "filter": "_geoRadius(45.4628328, 9.1076931, 2000)" }'
  1. client.index('restaurants').search('', {
  2. filter: ['_geoRadius(45.4628328, 9.1076931, 2000)'],
  3. })
  1. client.index('restaurants').search('', {
  2. 'filter': '_geoRadius(45.4628328, 9.1076931, 2000)'
  3. })
  1. $client->index('restaurants')->search('', ['filter' => '_geoRadius(45.4628328, 9.1076931, 2000)']);
  1. SearchRequest searchRequest =
  2. new SearchRequest("").setFilter(new String[] {"_geoRadius(45.4628328, 9.1076931, 2000)"});
  3. client.index("restaurants").search(searchRequest);
  1. client.index('restaurants').search('', { filter: '_geoRadius(45.4628328, 9.1076931, 2000)' })
  1. resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{
  2. Filter: "_geoRadius(45.4628328, 9.1076931, 2000)",
  3. })
  1. let results: SearchResults<Restaurant> = restaurants.search()
  2. .with_filter("_geoRadius(45.4628328, 9.1076931, 2000)")
  3. .execute()
  4. .await
  5. .unwrap();
  1. let searchParameters = SearchParameters(
  2. filter: ["_geoRadius(45.4628328, 9.1076931, 2000)"]
  3. )
  4. client.index("restaurants").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await await client
  2. .index('restaurants')
  3. .search('', filter: '_geoRadius(45.4628328, 9.1076931, 2000)');

Using our example dataset, returned results look like this:

  1. [
  2. {
  3. "id": 1,
  4. "name": "Nàpiz' Milano",
  5. "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy",
  6. "type": "pizza",
  7. "rating": 9,
  8. "_geo": {
  9. "lat": 45.4777599,
  10. "lng": 9.1967508
  11. }
  12. },
  13. {
  14. "id": 3,
  15. "name": "Artico Gelateria Tradizionale",
  16. "address": "Via Dogana, 1, 20123 Milan, Italy",
  17. "type": "ice cream",
  18. "rating": 10,
  19. "_geo": {
  20. "lat": 45.4632046,
  21. "lng": 9.1719421
  22. }
  23. }
  24. ]

It is also possible to use _geoRadius together with other filters. We can narrow down our previous search so it only includes pizzerias:

<>

cURL

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl -X POST 'http://localhost:7700/indexes/restaurants/search' \
  2. -H 'Content-type:application/json' \
  3. --data-binary '{ "filter": "_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza" }'
  1. client.index('restaurants').search('', {
  2. filter: ['_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza'],
  3. })
  1. client.index('restaurants').search('', {
  2. 'filter': '_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza'
  3. })
  1. $client->index('restaurants')->search('', ['filter' => '_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza']);
  1. SearchRequest searchRequest =
  2. new SearchRequest("").setFilter(new String[] {"_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza"});
  3. client.index("restaurants").search(searchRequest);
  1. client.index('restaurants').search('', { filter: '_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza' })
  1. resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{
  2. Filter: "_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza",
  3. })
  1. let results: SearchResults<Restaurant> = restaurants.search()
  2. .with_filter("_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza")
  3. .execute()
  4. .await
  5. .unwrap();
  1. let searchParameters = SearchParameters(
  2. filter: ["_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza"]
  3. )
  4. client.index("restaurants").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await await client
  2. .index('restaurants')
  3. .search('', filter: '_geoRadius(45.4628328, 9.1076931, 2000) AND type = pizza');
  1. [
  2. {
  3. "id": 1,
  4. "name": "Nàpiz' Milano",
  5. "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy",
  6. "type": "pizza",
  7. "rating": 9,
  8. "_geo": {
  9. "lat": 45.4777599,
  10. "lng": 9.1967508
  11. }
  12. }
  13. ]

Sorting results with _geoPoint

You can use _geo data to sort results based on their distance from a specific location.

Configuration

Before using geosearch for sorting, you must add the _geo attribute to the sortableAttributes list:

<>

cURL

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl \
  2. -X POST 'http://localhost:7700/indexes/restaurants/settings/sortable-attributes' \
  3. -H 'Content-type:application/json' \
  4. --data-binary '[
  5. "_geo",
  6. ]'
  1. client.index('restaurants').updateSortableAttributes([
  2. '_geo'
  3. ])
  1. client.index('restaurants').update_sortable_attributes([
  2. '_geo'
  3. ])
  1. $client->index('restaurants')->updateSortableAttributes([
  2. '_geo'
  3. ]);
  1. Settings settings = new Settings();
  2. settings.setSortableAttributes(new String[] {"_geo"});
  3. client.index("restaurants").updateSettings(settings);
  1. client.index('restaurants').update_sortable_attributes(['_geo'])
  1. sortableAttributes := []string{
  2. "_geo",
  3. }
  4. client.Index("restaurants").UpdateSortableAttributes(&sortableAttributes)
  1. let progress: Progress = restaurants.set_sortable_attributes(&["_geo"]).await.unwrap();
  1. client.index("restaurants").updateSortableAttributes(["_geo"]) { (result: Result<Update, Swift.Error>) in
  2. switch result {
  3. case .success(let update):
  4. print(update)
  5. case .failure(let error):
  6. print(error)
  7. }
  8. }
  1. await client.index('restaurants').updateSortableAttributes(['_geo']);

Note that MeiliSearch will rebuild your index whenever you update sortableAttributes. Depending on the size of your dataset, this might take a considerable amount of time.

You can read more about configuring sortableAttributes in our dedicated sorting guide.

Usage

Once you made sure all your documents contain valid geolocation data and added the _geo attribute to the sortableAttributes list, you can use _geoPoint and sort to order results based on their distance from a geographic location.

_geoPoint is a sorting function that requires two floating point numbers indicating a location’s latitude and longitude. You must also specify whether the sort should be ascending (asc) or descending (desc):

  1. _geoPoint(0.0, 0.0):asc

Ascending sort will prioritize results closer to the specified location. Conversely, descending sort will bring items more distant from the specified location to the top of the results.

The following sorting rule orders results according to how close they are to the Eiffel Tower:

  1. {
  2. "sort": [
  3. "_geoPoint(48.8583701, 2.2922926):asc"
  4. ]
  5. }

If either lat or lng is invalid or missing, MeiliSearch will return an invalid_sort error. An error will also be thrown if you fail to indicate a sorting order.

You can read more about sorting in our dedicated guide.

WARNING

_geo, _geoDistance, and _geoRadius are not valid sort values. Trying to use any of them with the sort search parameter will result in an invalid_sort error.

Examples

The _geoPoint sorting function can be used like any other sorting rule. We can order documents based on how close they are to the Eiffel Tower:

<>

cURL

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl -X POST 'http://localhost:7700/indexes/restaurants/search' \
  2. -H 'Content-type:application/json' \
  3. --data-binary '{ "sort": ["_geoPoint(48.8583701,2.2922926):asc"] }'
  1. client.index('restaurants').search('', {
  2. sort: ['_geoPoint(48.8583701,2.2922926):asc'],
  3. })
  1. client.index('restaurants').search('', {
  2. 'sort': ['_geoPoint(48.8583701,2.2922926):asc']
  3. })
  1. $client->index('restaurants')->search('', ['sort' => ['_geoPoint(48.8583701,2.2922926):asc']]);
  1. SearchRequest searchRequest =
  2. new SearchRequest("").setSort(new String[] {"_geoPoint(48.8583701,2.2922926):asc"});
  3. client.index("restaurants").search(searchRequest);
  1. client.index('restaurants').search('', { sort: ['_geoPoint(48.8583701,2.2922926):asc'] })
  1. resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{
  2. Sort: []string{
  3. "_geoPoint(48.8583701,2.2922926):asc",
  4. },
  5. })
  1. let results: SearchResults<Restaurant> = restaurants.search()
  2. .with_sort(&["_geoPoint(48.8583701,2.2922926):asc"])
  3. .execute()
  4. .await
  5. .unwrap();
  1. let searchParameters = SearchParameters(
  2. sort: ["_geoPoint(48.8583701,2.2922926):asc"]
  3. )
  4. client.index("restaurants").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await client.index('restaurants').search('', sort: ['_geoPoint(48.8583701,2.2922926):asc']);

With our example dataset, the results look like this:

  1. [
  2. {
  3. "id": 2,
  4. "name": "Bouillon Pigalle",
  5. "address": "22 Bd de Clichy, 75018 Paris, France",
  6. "type": "french",
  7. "rating": 8,
  8. "_geo": {
  9. "lat": 48.8826517,
  10. "lng": 2.3352748
  11. }
  12. },
  13. {
  14. "id": 1,
  15. "name": "Nàpiz' Milano",
  16. "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy",
  17. "type": "pizza",
  18. "rating": 9,
  19. "_geo": {
  20. "lat": 45.4777599,
  21. "lng": 9.1967508
  22. }
  23. },
  24. {
  25. "id": 3,
  26. "name": "Artico Gelateria Tradizionale",
  27. "address": "Via Dogana, 1, 20123 Milan, Italy",
  28. "type": "ice cream",
  29. "rating": 10,
  30. "_geo": {
  31. "lat": 45.4632046,
  32. "lng": 9.1719421
  33. }
  34. }
  35. ]

_geoPoint also works when used together with other sorting rules. We can sort restaurants based on proximity to the Eiffel Tower and good ratings:

<>

cURL

JS

Python

PHP

Java

Ruby

Go

Rust

Swift

Dart

  1. curl -X POST 'http://localhost:7700/indexes/restaurants/search' \
  2. -H 'Content-type:application/json' \
  3. --data-binary '{ "sort": ["_geoPoint(48.8583701,2.2922926):asc", "rating:desc"] }'
  1. client.index('restaurants').search('', {
  2. sort: ['_geoPoint(48.8583701,2.2922926):asc', 'rating:desc'],
  3. })
  1. client.index('restaurants').search('', {
  2. 'sort': ['_geoPoint(48.8583701,2.2922926):asc', 'rating:desc']
  3. })
  1. $client->index('restaurants')->search('', ['sort' => ['_geoPoint(48.8583701,2.2922926):asc', 'rating:desc']]);
  1. SearchRequest searchRequest =
  2. new SearchRequest("").setSort(new String[] {
  3. "_geoPoint(48.8583701,2.2922926):asc",
  4. "rating:desc",
  5. });
  6. client.index("restaurants").search(searchRequest);
  1. client.index('restaurants').search('', { sort: ['_geoPoint(48.8583701,2.2922926):asc', 'rating:desc'] })
  1. resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{
  2. Sort: []string{
  3. "_geoPoint(48.8583701,2.2922926):asc",
  4. "rating:desc",
  5. },
  6. })
  1. let results: SearchResults<Restaurant> = restaurants.search()
  2. .with_sort(&["_geoPoint(48.8583701,2.2922926):asc", "rating:desc"])
  3. .execute()
  4. .await
  5. .unwrap();
  1. let searchParameters = SearchParameters(
  2. sort: ["_geoPoint(48.8583701,2.2922926):asc", "rating:desc"]
  3. )
  4. client.index("restaurants").search(searchParameters) { (result: Result<SearchResult<Movie>, Swift.Error>) in
  5. switch result {
  6. case .success(let searchResult):
  7. print(searchResult)
  8. case .failure(let error):
  9. print(error)
  10. }
  11. }
  1. await client.index('restaurants').search('', sort: ['_geoPoint(48.8583701,2.2922926):asc', 'rating:desc']);
  1. [
  2. {
  3. "id": 2,
  4. "name": "Bouillon Pigalle",
  5. "address": "22 Bd de Clichy, 75018 Paris, France",
  6. "type": "french",
  7. "rating": 8,
  8. "_geo": {
  9. "lat": 48.8826517,
  10. "lng": 2.3352748
  11. }
  12. },
  13. {
  14. "id": 3,
  15. "name": "Artico Gelateria Tradizionale",
  16. "address": "Via Dogana, 1, 20123 Milan, Italy",
  17. "type": "ice cream",
  18. "rating": 10,
  19. "_geo": {
  20. "lat": 45.4632046,
  21. "lng": 9.1719421
  22. }
  23. },
  24. {
  25. "id": 1,
  26. "name": "Nàpiz' Milano",
  27. "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy",
  28. "type": "pizza",
  29. "rating": 9,
  30. "_geo": {
  31. "lat": 45.4777599,
  32. "lng": 9.1967508
  33. }
  34. }
  35. ]

Ranking rules

By default, MeiliSearch emphasizes relevant sorting over exhaustive sorting. This means our engine first finds the most relevant results and only then orders matches based on values given to the sort search parameter.

This means that sorting with _geoPoint will often be a tie-breaker and not the most important factor when deciding which results a user will see first.

Since _geoPoint is used with the sort search parameter, its weight when ranking results is controlled by the position of "sort" ranking rule.

You can read more about the “sort” ranking rule and how to customize it on our dedicated sorting guide.

Finding the distance between a document and a _geoPoint

When using _geoPoint, all returned documents will contain one extra field: _geoDistance. As its name indicates, _geoDistance contains the distance in meters between the specified _geoPoint and a document’s _geo data:

  1. [
  2. {
  3. "id": 1,
  4. "name": "Nàpiz' Milano",
  5. "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy",
  6. "type": "pizza",
  7. "rating": 9,
  8. "_geo": {
  9. "lat": 45.4777599,
  10. "lng": 9.1967508
  11. },
  12. "_geoDistance": 1532
  13. }
  14. ]

WARNING

Using _geoRadius filter will not cause results to include _geoDistance.

_geoDistance will only be computed in a returned document if the query uses _geoPoint and the sort search parameter.