现在手机APP满天飞,我想大家都用过这个功能:【搜索我附近的饭店或宾馆】之类的功能,类似这样的地理位置搜索功能非常适用,因为它需要利用到用户当前的地理位置数据,是以用户角度出发,找到符合用户自身需求的信息,应用返回的信息对于用户来说满意度会比较高,可见,地理位置空间搜索在提高用户体验方面有至关重要的作用。在Lucene中,地理位置空间搜索是借助Spatial模块来实现的。
要实现地理位置空间搜索,我们首先需要对地理位置数据创建索引,比较容易想到的就是把经度和纬度存入索引,可是这样做,有个弊端,因为地理位置数据(经纬度)是非常精细的,一般两个地点相差就0.0几,这样我们需要构建的索引体积会很大,这会显著减慢你的搜索速度。在精确度上采取折衷的方法通常是将纬度和经度封装到层中。您可以将每个层看作是地图的特定部分的缩放级别,比如位于美国中央上方的第 2 层几乎包含了整个北美,而第 19 层可能只是某户人家的后院。尤其是,每个层都将地图分成 2层 # 的箱子或网格。然后给每个箱子分配一个号码并添加到文档索引中。如果希望使用一个字段,那么可以使用 Geohash编码方式将纬度/经度编码到一个 String 中。Geohash 的好处是能够通过切去散列码末尾的字符来实现任意的精度。在许多情况下,相邻的位置通常有相同的前缀。
同样比较重要的,就是距离计算,给定两个坐标点需要你计算这两个点之间的距离,至于怎么计算,这取决于你对地球怎么进行建模,一般对于距离计算精度要求不是很精确的(误差在10-20米范围内能接受的话)
采用平面模型就够了。当然你也可以计算球面模型,这样计算精度更精确,但更耗CPU,意味着计算时间更长,需要自己去优化。
下面给出一个Spatial使用示例代码:
package com.yida.framework.lucene5.spatial; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.NumericDocValuesField; 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.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.SortField; 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.RAMDirectory; import org.wltea.analyzer.lucene.IKAnalyzer; import com.spatial4j.core.context.SpatialContext; import com.spatial4j.core.distance.DistanceUtils; import com.spatial4j.core.shape.Point; import com.spatial4j.core.shape.Shape; /** * Lucene地理位置查询测试 * * @author Lanxiaowei * */ public class LuceneSpatialTest { /** Spatial上下文 */ private SpatialContext ctx; /** 提供索引和查询模型的策略接口 */ private SpatialStrategy strategy; /** 索引目录 */ private Directory directory; /** * Spatial初始化 */ protected void init() { // SpatialContext也可以通过SpatialContextFactory工厂类来构建 this.ctx = SpatialContext.GEO; //网格最大11层 int maxLevels = 11; // SpatialPrefixTree也可以通过SpatialPrefixTreeFactory工厂类构建 SpatialPrefixTree grid = new GeohashPrefixTree(ctx, maxLevels); this.strategy = new RecursivePrefixTreeStrategy(grid, "myGeoField"); // 初始化索引目录 this.directory = new RAMDirectory(); } private void indexPoints() throws Exception { IndexWriterConfig iwConfig = new IndexWriterConfig(new IKAnalyzer()); IndexWriter indexWriter = new IndexWriter(directory, iwConfig); //这里的x,y即经纬度,x为Longitude(经度),y为Latitude(纬度) indexWriter.addDocument(newSampleDocument(2, ctx.makePoint(-80.93, 33.77))); /** WKT表示法:POINT(Longitude,Latitude)*/ indexWriter.addDocument(newSampleDocument(4, ctx.readShapeFromWkt("POINT(60.9289094 -50.7693246)"))); indexWriter.addDocument(newSampleDocument(20, ctx.makePoint(0.1, 0.1), ctx.makePoint(0, 0))); indexWriter.close(); } /** * 创建Document索引对象 * * @param id * @param shapes * @return */ private Document newSampleDocument(int id, Shape... shapes) { Document doc = new Document(); doc.add(new StoredField("id", id)); doc.add(new NumericDocValuesField("id", id)); for (Shape shape : shapes) { for (Field f : strategy.createIndexableFields(shape)) { doc.add(f); } Point pt = (Point) shape; doc.add(new StoredField(strategy.getFieldName(), pt.getX() + " " + pt.getY())); } return doc; } /** * 地理位置搜索 * @throws Exception */ private void search() throws Exception { IndexReader indexReader = DirectoryReader.open(directory); IndexSearcher indexSearcher = new IndexSearcher(indexReader); // 按照id升序排序 Sort idSort = new Sort(new SortField("id", SortField.Type.INT)); //搜索方圆200千米范围以内,这里-80.0, 33.0分别是当前位置的经纬度,以当前位置为圆心,200千米为半径画圆 //注意后面的EARTH_MEAN_RADIUS_KM表示200的单位是千米,看到KM了么。 SpatialArgs args = new SpatialArgs(SpatialOperation.Intersects, ctx.makeCircle(-80.0, 33.0, DistanceUtils.dist2Degrees(200, DistanceUtils.EARTH_MEAN_RADIUS_KM))); //根据SpatialArgs参数创建过滤器 Filter filter = strategy.makeFilter(args); //开始搜索 TopDocs docs = indexSearcher.search(new MatchAllDocsQuery(), filter, 10, idSort); Document doc1 = indexSearcher.doc(docs.scoreDocs[0].doc); String doc1Str = doc1.getField(strategy.getFieldName()).stringValue(); int spaceIdx = doc1Str.indexOf(' '); double x = Double.parseDouble(doc1Str.substring(0, spaceIdx)); double y = Double.parseDouble(doc1Str.substring(spaceIdx + 1)); double doc1DistDEG = ctx .calcDistance(args.getShape().getCenter(), x, y); System.out.println("(Longitude,latitude):" + "(" + x + "," + y + ")"); System.out.println("doc1DistDEG:" + doc1DistDEG * DistanceUtils.DEG_TO_KM); System.out.println(DistanceUtils.degrees2Dist(doc1DistDEG,DistanceUtils.EARTH_MEAN_RADIUS_KM)); //定义一个坐标点(x,y)即(经度,纬度)即当前用户所在地点 Point pt = ctx.makePoint(60, -50); //计算当前用户所在坐标点与索引坐标点中心之间的距离即当前用户地点与每个待匹配地点之间的距离,DEG_TO_KM表示以KM为单位 ValueSource valueSource = strategy.makeDistanceValueSource(pt, DistanceUtils.DEG_TO_KM); //根据命中点与当前位置坐标点的距离远近降序排,距离数字大的排在前面,false表示降序,true表示升序 Sort distSort = new Sort(valueSource.getSortField(false)) .rewrite(indexSearcher); TopDocs topdocs = indexSearcher.search(new MatchAllDocsQuery(), 10, distSort); ScoreDoc[] scoreDocs = topdocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { int docId = scoreDoc.doc; Document document = indexSearcher.doc(docId); int gotid = document.getField("id").numericValue().intValue(); String geoField = document.getField(strategy.getFieldName()).stringValue(); int xy = geoField.indexOf(' '); double xPoint = Double.parseDouble(geoField.substring(0, xy)); double yPoint = Double.parseDouble(geoField.substring(xy + 1)); double distDEG = ctx .calcDistance(args.getShape().getCenter(), xPoint, yPoint); double juli = DistanceUtils.degrees2Dist(distDEG,DistanceUtils.EARTH_MEAN_RADIUS_KM); System.out.println("docId:" + docId + ",id:" + gotid + ",distance:" + juli + "KM"); } /*args = new SpatialArgs(SpatialOperation.Intersects, ctx.makeCircle( -80.0, 33.0, 1)); SpatialArgs args2 = new SpatialArgsParser().parse( "Intersects(BUFFER(POINT(-80 33),1))", ctx); System.out.println("args2:" + args2.toString());*/ indexReader.close(); } public static void main(String[] args) throws Exception { LuceneSpatialTest luceneSpatialTest = new LuceneSpatialTest(); luceneSpatialTest.init(); luceneSpatialTest.indexPoints(); luceneSpatialTest.search(); } }
最后列出一些补充学习资料,关于Spatial具体更深入的学习,需要自己去研究,我只是说了个大概,其实地理位置搜索实现起来并不难,难在数据量巨大时索引数据体积庞大导致的查询速度损耗问题如何解决,动态计算两点之间的距离算法如何优化至在1-10ms内返回等等:
有关WTK 空间数据表示法参考资料:
WKT - 概念
WKT(Well-known text)是一种文本标记语言,用于表示矢量几何对象、空间参照系统及空间参照系统之间的转换。它的二进制表示方式,亦即WKB(well-known binary)则胜于在传输和在数据库中存储相同的信息。该格式由开放地理空间联盟(OGC)制定。
WKT - 几何对象
WKT可以表示的几何对象包括:点,线,多边形,TIN(不规则三角网)及多面体。可以通过几何集合的方式来表示不同维度的几何对象。
几何物体的坐标可以是2D(x,y),3D(x,y,z),4D(x,y,z,m),加上一个属于线性参照系统的m值。
以下为几何WKT字串样例:
POINT(6 10)
LINESTRING(3 4,10 50,20 25)
POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))
MULTIPOINT(3.5 5.6, 4.8 10.5)
MULTILINESTRING((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))
MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2)),((6 3,9 2,9 4,6 3)))
GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))
POINT ZM (1 1 5 60)
POINT M (1 1 80)
POINT EMPTY
MULTIPOLYGON EMPTY
WKT - 空间参照系统
一个表示空间参照系统的WKT字串描述了空间物体的测地基准、大地水准面、坐标系统及地图投影。
WKT在许多GIS程序中被广泛采用。ESRI亦在其shape文件格式(*.prj)中使用WKT。
以下是空间参照系统的WKT表示样例:
COMPD_CS["OSGB36 / British National Grid + ODN",
PROJCS["OSGB 1936 / British National Grid",
GEOGCS["OSGB 1936",
DATUM["OSGB_1936",
spheroid["Airy 1830",6377563.396,299.3249646,AUTHORITY["EPSG","7001"]],
TOWGS84[375,-111,431,0,0,0,0],
AUTHORITY["EPSG","6277"]],
PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],
UNIT["DMSH",0.0174532925199433,AUTHORITY["EPSG","9108"]],
AXIS["Lat",NORTH],
AXIS["Long",EAST],
AUTHORITY["EPSG","4277"]],
PROJECTION["Transverse_Mercator"],
PARAMETER["latitude_of_origin",49],
PARAMETER["central_meridian",-2],
PARAMETER["scale_factor",0.999601272],
PARAMETER["false_easting",400000],
PARAMETER["false_northing",-100000],
UNIT["metre",1,AUTHORITY["EPSG","9001"]],
AXIS["E",EAST],
AXIS["N",NORTH],
AUTHORITY["EPSG","27700"]],
VERT_CS["Newlyn",
VERT_DATUM["Ordnance Datum Newlyn",2005,AUTHORITY["EPSG","5101"]],
UNIT["metre",1,AUTHORITY["EPSG","9001"]],
AXIS["Up",UP],
AUTHORITY["EPSG","5701"]],
AUTHORITY["EPSG","7405"]]
关于如何对地球进行建模方面的知识,请自己Google学习,比如平面建模,球面建模,曼哈顿距离等等,平面建模一般采用勾股定理或其变体就能解决,球面建模一般是采用求大圆弧长来解决,在Lucene Spatial中有Haversine 和 Geohash Haversine两个公式实现。至于Haversine公式的算法以及GeoaHash编码的算法啊自己Google学习去吧。Demo源码请看底下的附件!!!
如果你还有什么问题请加我Q-Q:7-3-6-0-3-1-3-0-5,
或者加裙
一起交流学习!