In this blog post, i am going to explain how to implement GeoSpatial Search using Lucene 4.x.
Problem I am trying to Implement :
Finding Nearest Places Within Certain Radius Given the Location of User.
Sample Input Places Data :
id name latitude longitude
1 Bangalore 12.9558 77.620979
2 Cubbon Park 12.974045 77.591995
3 Tipu Palace 12.959365 77.573792
4 Bangalore Palace 12.998095 77.592041
5 Monkey Bar 12.97018 77.61219
6 Cafe Cofee Day 12.992189 80.2348618
Code :
import java.io.File;
import java.io.IOException;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.Version;
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceUtils;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Shape;
public class SpatialSearch {
private IndexWriter indexWriter;
private IndexReader indexReader;
private IndexSearcher searcher;
private SpatialContext ctx;
private SpatialStrategy strategy;
public SpatialSearch(String indexPath) {
StandardAnalyzer a = new StandardAnalyzer(Version.LUCENE_43);
IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_43, a);
Directory directory;
try {
directory = new SimpleFSDirectory(new File(indexPath));
indexWriter = new IndexWriter(directory, iwc);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.ctx = SpatialContext.GEO;
SpatialPrefixTree grid = new GeohashPrefixTree(ctx, 11);
this.strategy = new RecursivePrefixTreeStrategy(grid, "location");
}
public void indexDocuments() throws IOException {
indexWriter.addDocument(newGeoDocument(1, "Bangalore", ctx.makePoint(12.9558, 77.620979)));
indexWriter.addDocument(newGeoDocument(2, "Cubbon Park", ctx.makePoint(12.974045, 77.591995)));
indexWriter.addDocument(newGeoDocument(3, "Tipu palace", ctx.makePoint(12.959365, 77.573792)));
indexWriter.addDocument(newGeoDocument(4, "Bangalore palace", ctx.makePoint(12.998095, 77.592041)));
indexWriter.addDocument(newGeoDocument(5, "Monkey Bar", ctx.makePoint(12.97018, 77.61219)));
indexWriter.addDocument(newGeoDocument(6, "Cafe Cofee day", ctx.makePoint(12.992189, 80.2348618)));
indexWriter.commit();
indexWriter.close();
}
private Document newGeoDocument(int id, String name, Shape shape) {
FieldType ft = new FieldType();
ft.setIndexed(true);
ft.setStored(true);
Document doc = new Document();
doc.add(new IntField("id", id, Store.YES));
doc.add(new Field("name", name, ft));
for(IndexableField f:strategy.createIndexableFields(shape)) {
doc.add(f);
}
doc.add(new StoredField(strategy.getFieldName(), ctx.toString(shape)));
return doc;
}
public void setSearchIndexPath(String indexPath) throws IOException{
this.indexReader = DirectoryReader.open(new SimpleFSDirectory(new File(indexPath)));
this.searcher = new IndexSearcher(indexReader);
}
public void search(Double lat, Double lng, int distance) throws IOException{
Point p = ctx.makePoint(lat, lng);
SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects,
ctx.makeCircle(lat, lng, DistanceUtils.dist2Degrees(distance, DistanceUtils.EARTH_MEAN_RADIUS_KM)));
Filter filter = strategy.makeFilter(args);
ValueSource valueSource = strategy.makeDistanceValueSource(p);
Sort distSort = new Sort(valueSource.getSortField(false)).rewrite(searcher);
int limit = 10;
TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), filter, limit, distSort);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for(ScoreDoc s: scoreDocs) {
Document doc = searcher.doc(s.doc);
Point docPoint = (Point) ctx.readShape(doc.get(strategy.getFieldName()));
double docDistDEG = ctx.getDistCalc().distance(args.getShape().getCenter(), docPoint);
double docDistInKM = DistanceUtils.degrees2Dist(docDistDEG, DistanceUtils.EARTH_EQUATORIAL_RADIUS_KM);
System.out.println(doc.get("id") + "\t" + doc.get("name") + "\t" + docDistInKM + " km ");
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
String indexPath = "/home/vishnu/lucene_practices/geo_spatial_index";
SpatialSearch s = new SpatialSearch(indexPath);
//Indexes sample documents
s.indexDocuments();
s.setSearchIndexPath(indexPath);
//Get Places Within 4 kilometers from cubbon park.
s.search(12.974045,77.591995, 4);
}
}
2 Cubbon Park 0.0 km
4 Bangalore palace 0.5752837176108795 km
3 Tipu palace 2.056590450260357 km
5 Monkey Bar 2.2499941525889806 km
1 Bangalore 3.255797190098085 km
In this blog post we learned how to implement
Basic GeoSpatial Queries like whether a point contains within a circle or not.
My Next Blog Post will talk about how to implement advanced spatial queries like
geoInterseting - where one polygon intersects with another polygon/line.
geoWithIn - where one polygon lies completely within another polygon.
Hi,
ReplyDeleteI'm trying to use this script for my project, but haven't a good result(no result...). But i not use in my code your method indexDocuments because i have another process for saved. the question is, i need necessary use a method like indexDocuments? beacuase i save an id:String, lat:Double and lng:Double, and more other info, but i don't save other type. can u help me please?
Hi Francesco,
ReplyDeleteSorry for the late reply.
You should use _id:String and Shape object instead of lat:Double and lng:Double.
I mean you should create Shape Object using lat,lng "ctx.makePoint(12.9558, 77.620979)" as in my program.
Otherwise just explore what 'ctx.toString(ctx.makePoint(12.9558, 77.620979))' returns and store in the new field.
Using direct lat:double and lng:double wont work in lucene 4.x i guess.
Hi all,
ReplyDeleteI think that in your code you may have mixed up latitude and longitude in the calls to "makePoint". The code for this methods is
public Point makePoint(double x, double y) {
verifyX(x);
verifyY(y);
return new PointImpl(x, y, this);
}
and the verification (when you use the world bounds for GEO) are -180<= x <= 180, and -90 <= y <= 90, which suggests to me that one should make calls such as
ctx.makePoint(longitude, latitude)
such as ctx.makePoint(77.620979, 12.9558) and *not* ctx.makePoint(12.9558, 77.620979). This came up as I was working with longitudes > 90.0.