概括:
最近一个项目需要基于LBS查询附近的商铺信息,看了一下网上都是基于Solr和ELS方式来实现, 本来想使用ELS来实现的,但是由于项目以前用的是Solr, 所以就去调研了一下基于Solr来实现地理位置的搜索,并且在实现的时候整理了一下实现的笔记。
在开发中如果需要对带经纬度的数据进行检索,比如查找当前所在位置附近1000米的,一种简单的方法就是:获取数据库中的所有酒店数据,按经纬度计算距离,返回距离小于1000米的数据。
这种方式在数据量小的时候比较有效,但是当数据量大的时候,检索的效率是会十分十分低的,本文介绍使用Solr的Spatial Query进行空间搜索。本文简述基于Solr实现空间范围搜索
基于Mysql 实现地理位置排序:
例如:
公式如下,单位米:
第一点经纬度:lng1 lat1
第二点经纬度:lng2 lat2
round(6378.138*2*asin(sqrt(pow(sin( (lat1*pi()/180-lat2*pi()/180)/2),2)+cos(lat1*pi()/180)*cos(lat2*pi()/180)* pow(sin( (lng1*pi()/180-lng2*pi()/180)/2),2)))*1000)
例如:
SELECT store_id,lng,lat, ROUND(6378.138*2*ASIN(SQRT(POW(SIN((22.299439*PI()/180- lat*PI()/180)/2),2)+COS(22.299439*PI()/180)*COS(lat*PI()/180)*POW(SIN((114.173881*PI()/180-lng*PI()/180)/2),2)))*1000) AS juli FROM store_info
ORDER BY juli DESC
LIMIT 316
基于Solr实现地理位置排序:
没学过的Solr 这里有一份官网手册:
http://lucene.apache.org/solr/guide/6_6/spatial-search.html
原理:
具体的空间搜索原理和算法不多介绍,空间搜索算法都是很成熟的
https://www.cnblogs.com/hanhuibing/articles/5680616.html
https://www.cnblogs.com/keleyu/p/4993039.html
基于Solr的空间搜索实战:
Solr已经提供了3种filedType来进行空间搜索:
有四种可用于空间搜索的主要字段类型:
LatLonPointSpatialField
LatLonType (现已弃用)及其非大地双胞胎 PointType
SpatialRecursivePrefixTreeFieldType(简称RPT),包括RptWithGeometrySpatialField衍生产品
BBoxField
LatLonPointSpatialField是经纬度点数据最常见使用情况的理想字段类型。它取代了为了向后兼容而仍然存在的LatLonType。RPT提供了更多高级/自定义用例以及诸如多边形和热图等选项的更多功能。
RptWithGeometrySpatialField用于索引和搜索非点数据,尽管它也可以做点。它不能做排序/提升。
BBoxField 用于索引边界框,通过框查询,指定搜索谓词(相交,内部,包含,不相交,等于)以及相关性排序/提升类似overlapRatio或简单区域。
LatLonPointSpatialField
<fieldType name="location" class="solr.LatLonPointSpatialField" docValues="true"/>
Indexing Points使用(x y) 或者(lat,lon) 格式进行保存;不然会报错
用于地理空间搜索的空间Solr“查询分析器”:geofilt和bbox
geofilt
该geofilt过滤器允许您根据给定点的地理空间距离(也称为“大圆距离”)检索结果。另一种看待它的方式是创建一个圆形滤镜。例如,要查找给定纬度/经度点五公里内的所有文件,可以输入。该过滤器返回给定半径的圆周内的所有结果:
&q=:&fq={!geofilt sfield=store}&pt=45.15,-93.85&d=5
bbox
该bbox过滤器与geofilt使用计算的圆的边界框非常相似。请参阅下图中的蓝色框。它采用与geofilt相同的参数。
以下是一个示例查询:
&q=:&fq={!bbox sfield=store}&pt=45.15,-93.85&d=5
矩形形状的计算速度更快,所以它有时可以用来替代geofilt返回半径之外的点的可接受范围。但是,如果理想目标是一个圆圈,但您希望它运行得更快,那么请考虑使用RPT字段并尝试一个较大的distErrPct值0.1(如10%半径)。这将返回半径之外的结果,但它将在形状周围有点均匀。
当边界框包含一个极点时,边界框最终成为一个“边界盆”(球冠),如果边界框接触北极(或最高纬度的南部,如果有),则该边界包括圆的最低纬度以北的所有值它接触南极)。
通过任意矩形进行过滤
有时,空间搜索要求要求在矩形区域中查找所有内容,例如用户正在查看的地图所覆盖的区域。对于这种情况,geofilt和bbox不会剪切它。这是一个窍门,但是你可以使用Solr的范围查询语法来提供左下角作为范围的开始和右上角作为范围的结束。
这是一个例子:
&q=:&fq=store:[45,-94 TO 46,-93]
优化:缓存与否
将空间查询放入“fq”参数中是最常见的过滤器查询。默认情况下,Solr会将查询缓存在过滤器缓存中。
如果您知道过滤器查询(无论是否为空间)是相当独特的,并且不太可能获得缓存命中,则指定cache="false"为本地参数,如以下示例中所示
&q=…mykeywords…&fq=…someotherfilters…&fq={!geofilt cache=false}&sfield=store&pt=45.15,-93.85&d=5
距离排序或提升(函数查询)
有四个距离函数查询:
geodist,见下文,通常是最合适的;
dist计算多维向量之间的p范数距离;
hsin,计算球体上两点之间的距离;
sqedist,来计算两点之间的平方欧几里德距离。
有关这些功能的查询的详细信息,请参阅该部分功能查询。
geodist
geodist是一个距离函数,需要三个可选参数:(sfield,latitude,longitude)。您可以使用该geodist功能按距离或得分返回结果对结果进行排序。
例如,要按距离递增对结果进行排序,请输入。…&q=:&fq={!geofilt}&sfield=store&pt=45.15,-93.85&d=50&sort=geodist() asc
要将距离作为文档分数返回,请输入…&q={!func}geodist()&sfield=store&pt=45.15,-93.85&sort=score+asc。
更多示例
以下是一些您可以在Solr中进行空间搜索的更有用的示例。
1) 按距离排序,距离越近排名越高,加上score=distance,其中distance是索引点到坐标点之间的弧度值,系统根据弧度值排序。
&fl=*,score&sort=score asc&q={!geofilt score=distance sfield=poi_location_p pt=54.729696,-98.525391 d=10}。
2) 按距离排序,距离越远排名越高,加上score=reciDistance,其中reciDistance 范围是0~1 采用倒数的方式计算,故与distance的排序刚好相反
&fl=*,score&sort=score asc&q={!geofilt score=reciDistance sfield=poi_location_p pt=54.729696,-98.525391 d=10}
3) 距离仅作排序不做过滤,在条件中设置filter=false,其中d只是确定形状的作用,不起限制作用。
&fl=*,score&sort=score asc&q={!geofilt score=distance filter=false sfield=poi_location_p pt=54.729696,-98.525391 d=10}
4) 结合关键词查询和距离排序,此时关键字只能作为过滤条件(fq)不能作为查询条件,仅作为过滤域。
&fl=*,score& fq=store_name:农业&sort=score asc&q={!geofilt score=distance sfield=poi_location_p pt=54.729696,-98.525391 d=10}
5) 当关键字和距离都作为排序条件时,可以用qf参数设置权重
&fl=*,score& fq=store_name:农业&sort=score asc&q={!geofilt score=distance sfield=poi_location_p pt=54.729696,-98.525391
SpatialRecursivePrefixTreeFieldType:
本文重点介绍使用SpatialRecursivePrefixTreeFieldType,不仅可以用点,也可以用于多边形的查询。
1、配置Solr
个人使用的solr6.6.2:
<fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType" spatialContextFactory="org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory" autoIndex="true" validationRule="repairBuffer0" distErrPct="0.025" maxDistErr="0.001" distanceUnits="kilometers" /> <field name="station_position" type="location_rpt" indexed="true" stored="true" multiValued="false" />
对solr.SpatialRecursivePrefixTreeFieldType的配置说明:SpatialRecursivePrefixTreeFieldType
用于深度遍历前缀树的FieldType,主要用于获得基于Lucene中的RecursivePrefixTreeStrategy。
geo
默认为true,值为true的情况下坐标基于球面坐标系,采用Geohash的方式;值为false的情况下坐标基于2D平面的坐标系,采用Euclidean/Cartesian的方式。
distErrPct
定义非Point图形的精度,范围在0-0.5之间。该值决定了非Point的图形索引或查询时的level(如geohash模式时就是geohash编码的长度)。当为0时取maxLevels,即精度最大,精度越大将花费更多的空间和时间去建索引。
maxDistErr/maxLevels:maxDistErr
定义了索引数据的最高层maxLevels,上述定义为0.000009,根据 GeohashUtils.lookupHashLenForWidthHeight(0.000009, 0.000009)算出编码长度为11位,精度在1米左右,直接决定了Point索引的term数。maxLevels优先级高于maxDistErr, 即有maxLevels的话maxDistErr失效。详见SpatialPrefixTreeFactory.init()方法。不过一般使用 maxDistErr。
distanceUnits 单位是degrees(度)
worldBounds
世界坐标值:”minX minY maxX maxY”。 geo=true即geohash模式时,该值默认为”-180 -90 180 90”。geo=false即quad时,该值为Java double类型的正负边界,此时需要指定该值,设置成”-180 -90 180 90”。
实战代码:
public class SolrGEO { public static void main(String[] args) { /*SolrQuery solrQuery = new SolrQuery(); solrQuery.set("d", value + ""); // 搜索半径 solrQuery.set("sfield", CommonConst.LOCATION_FIELD);// 存储地理位置的字段名称 solrQuery.set("pt", searchContent.getShopLocation()); // 经纬度参数格式为 纬度,经度 solrQuery.set("fl", "*,distance:geodist()"); // 文档中用distance表示距离字段 solrQuery.addSort("geodist()", ORDER.asc);// 按照从近到远排序 */ //addIndex(); queryIndex(); } public static void queryIndex() { try { HttpSolrClient server = SolrServer.getServer(); SolrQuery params=new SolrQuery(); /*params.setParam("q", "{!geofilt score=distance filter=false sfield=station_position pt=23.104487,113.375981 d=1}"); params.addSort("score",ORDER.asc);*/ params.setParam("q","*:*"); params.setParam("start", "0");//记录开始位置 params.setParam("rows", "10");//查询的行数 params.set("fq", "{!geofilt}"); //距离过滤函数 params.set("pt", "23.104487,113.375981"); //当前经纬度 params.set("sfield", "station_position"); //经纬度的字段 params.set("d", "1"); //就近 d 20km的所有数据 params.set("score", "distance"); params.addSort("geodist()",ORDER.asc);//按照从近到远排序 /** * geofilt : //距离过滤函数 “大圆距离”)检索结果 * sfield : 坐标字段 * d : 空间范围 公里 默认使用单位为千米(1km=0.009度) */ /* String query=" {!geofilt score=distance sfield=station_position d=1 } "; params.addFilterQuery(query); */ QueryResponse resp = server.query(params); System.out.println(params.toQueryString()); SolrDocumentList docs = resp.getResults(); for(int i=0;i<docs.size();i++){ SolrDocument sid=docs.get(i); String title = (String) sid.getFieldValue("title"); System.out.println(title); String station_position= (String) sid.getFieldValue("station_position"); String [] positions=station_position.split(","); double lat2= Double.valueOf(positions[0]); double lng2 = Double.valueOf(positions[1]); double range=getDistance(23.104487d,113.375981d,lat2,lng2); System.out.println("与当前坐标距离:"+range); System.out.println(sid); } } catch (Exception e) { e.printStackTrace(); } } private static double EARTH_RADIUS = 6378.137; private static double rad(double d) { return d * Math.PI / 180.0; } /** * 通过经纬度获取距离(单位:米) * @param lat1 * @param lng1 * @param lat2 * @param lng2 * @return */ public static double getDistance(double lat1, double lng1, double lat2, double lng2) { double radLat1 = rad(lat1); double radLat2 = rad(lat2); double a = radLat1 - radLat2; double b = rad(lng1) - rad(lng2); double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); s = s * EARTH_RADIUS; s = Math.round(s * 10000d) / 10000d; s = s*1000; return s; } /** * 添加索引,准备测试数据 */ public static void addIndex() { HttpSolrClient server = SolrServer.getServer(); /** * 删除旧索引 */ try { server.deleteByQuery("*:*"); } catch (SolrServerException e1) { } catch (IOException e1) { e1.printStackTrace(); } List<ProductBO> beans = new ArrayList<ProductBO>(); // 家电类 ProductBO dianshiBO1 = new ProductBO(); dianshiBO1.setId("1001"); dianshiBO1.setTitle("海尔模卡(MOOKA) 65K5 65英寸安卓智能网络窄边框全高清LED液晶电视"); dianshiBO1.setMajor_s("家用电器"); dianshiBO1.setSubMajor_s("电视"); dianshiBO1.setBrand_s("海尔"); dianshiBO1.setModel_s("65K5"); dianshiBO1.setPrice_i(5400); dianshiBO1.setStation_address("广东省广州市海珠区新港东路1068号"); //lat,lon dianshiBO1.setStation_position("23.104487,113.375981"); beans.add(dianshiBO1); ProductBO dianshiBO2 = new ProductBO(); dianshiBO2.setId("1002"); dianshiBO2.setTitle("三星(SAMSUNG)UA55JU5900JXXZ 55英寸 4K超高清智能 LED液晶电视 黑色"); dianshiBO2.setMajor_s("家用电器"); dianshiBO2.setSubMajor_s("电视"); dianshiBO2.setBrand_s("三星"); dianshiBO2.setModel_s("UA55JU5900"); dianshiBO2.setPrice_i(6400); dianshiBO2.setStation_address("广东省广州市海珠区凤浦中路741号"); dianshiBO2.setStation_position("23.103373,113.374265"); beans.add(dianshiBO2); dianshiBO2 = new ProductBO(); dianshiBO2.setId("1002"); dianshiBO2.setTitle("三星(SAMSUNG)UA55JU5900JXXZ 55英寸 4K超高清智能 LED液晶电视 黑色"); dianshiBO2.setMajor_s("家用电器"); dianshiBO2.setSubMajor_s("电视"); dianshiBO2.setBrand_s("三星"); dianshiBO2.setModel_s("UA55JU5900"); dianshiBO2.setPrice_i(6400); dianshiBO2.setStation_address("广东省广州市海珠区凤浦中路741号"); dianshiBO2.setStation_position("23.101811,113.376673"); beans.add(dianshiBO2); ProductBO kongtiaoBO1 = new ProductBO(); kongtiaoBO1.setId("2001"); kongtiaoBO1.setTitle("格力(GREE) 大1匹 变频 Q铂 壁挂式冷暖空调 KFR-26GW/(26596)FNAa-A3"); kongtiaoBO1.setMajor_s("家用电器"); kongtiaoBO1.setSubMajor_s("空调"); kongtiaoBO1.setBrand_s("格力"); kongtiaoBO1.setModel_s("KFR-26GW/(26596)FNAa-A3"); kongtiaoBO1.setPrice_i(7700); kongtiaoBO1.setStation_address("广东省广州市海珠区会展南五路"); kongtiaoBO1.setStation_position("23.102359,113.377374"); beans.add(kongtiaoBO1); ProductBO kongtiaoBO2 = new ProductBO(); kongtiaoBO2.setId("2002"); kongtiaoBO2.setTitle("奥克斯(AUX)正1.5匹 冷暖 定速 隐藏式显示屏 壁挂式 空调 KFR-35GW/HFJ+3"); kongtiaoBO2.setMajor_s("家用电器"); kongtiaoBO2.setSubMajor_s("空调"); kongtiaoBO2.setBrand_s("奥克斯"); kongtiaoBO2.setModel_s("KFR-35GW/HFJ+3"); kongtiaoBO2.setPrice_i(6600); kongtiaoBO2.setStation_address("广东省广州市海珠区新港东路1070号"); kongtiaoBO2.setStation_position("23.103265,113.376718"); beans.add(kongtiaoBO2); ProductBO kongtiaoBO3 = new ProductBO(); kongtiaoBO3.setId("2003"); kongtiaoBO3.setTitle("海尔(Haier)1.5匹 变频 静音空调 冷暖 壁挂式空调 KFR-35GW/01JDA23A"); kongtiaoBO3.setMajor_s("家用电器"); kongtiaoBO3.setSubMajor_s("空调"); kongtiaoBO3.setBrand_s("海尔"); kongtiaoBO3.setModel_s("KFR-35GW/01JDA23A"); kongtiaoBO3.setPrice_i(9600); kongtiaoBO3.setStation_address("广东省广州市海珠区AAAAAAA"); kongtiaoBO3.setStation_position("23.102667,113.376511"); beans.add(kongtiaoBO3); ProductBO kongtiaoBO4 = new ProductBO(); kongtiaoBO4.setId("2004"); kongtiaoBO4.setTitle("广东省广州市天河区车陂,天园,棠下"); kongtiaoBO4.setMajor_s("家用电器"); kongtiaoBO4.setSubMajor_s("空调"); kongtiaoBO4.setBrand_s("海尔"); kongtiaoBO4.setModel_s("KFR-35GW/01JDA23A"); kongtiaoBO4.setPrice_i(9600); kongtiaoBO4.setStation_address("广东省广州市天河区车陂,天园,棠下"); kongtiaoBO4.setStation_position("23.125202,113.390858"); beans.add(kongtiaoBO4); ProductBO kongtiaoBO5 = new ProductBO(); kongtiaoBO5.setId("2005"); kongtiaoBO5.setTitle("广东省广州市天河区科新路 棠下,天园,天河公园"); kongtiaoBO5.setMajor_s("家用电器"); kongtiaoBO5.setSubMajor_s("空调"); kongtiaoBO5.setBrand_s("海尔"); kongtiaoBO5.setModel_s("KFR-35GW/01JDA23A"); kongtiaoBO5.setPrice_i(9600); kongtiaoBO5.setStation_address("广东省广州市天河区科新路 棠下,天园,天河公园"); kongtiaoBO5.setStation_position("23.126299,113.387848"); beans.add(kongtiaoBO5); ProductBO kongtiaoBO6 = new ProductBO(); kongtiaoBO6.setId("2006"); kongtiaoBO6.setTitle("广东省广州市海珠区阅江中路686"); kongtiaoBO6.setMajor_s("家用电器"); kongtiaoBO6.setSubMajor_s("空调"); kongtiaoBO6.setBrand_s("海尔"); kongtiaoBO6.setModel_s("KFR-35GW/01JDA23A"); kongtiaoBO6.setPrice_i(9600); kongtiaoBO6.setStation_address("广东省广州市海珠区阅江中路686"); kongtiaoBO6.setStation_position("23.10929,113.377472"); beans.add(kongtiaoBO6); kongtiaoBO6 = new ProductBO(); kongtiaoBO6.setId("2007"); kongtiaoBO6.setTitle("广东省广州市海珠区凤浦中路681号"); kongtiaoBO6.setMajor_s("家用电器"); kongtiaoBO6.setSubMajor_s("空调"); kongtiaoBO6.setBrand_s("海尔"); kongtiaoBO6.setModel_s("KFR-35GW/01JDA23A"); kongtiaoBO6.setPrice_i(9600); kongtiaoBO6.setStation_address("广东省广州市海珠区凤浦中路681号"); kongtiaoBO6.setStation_position("23.101877,113.370349"); beans.add(kongtiaoBO6); kongtiaoBO6 = new ProductBO(); kongtiaoBO6.setId("2008"); kongtiaoBO6.setTitle("广东省广州市海珠区会展南三路"); kongtiaoBO6.setMajor_s("家用电器"); kongtiaoBO6.setSubMajor_s("空调"); kongtiaoBO6.setBrand_s("海尔"); kongtiaoBO6.setModel_s("KFR-35GW/01JDA23A"); kongtiaoBO6.setPrice_i(9600); kongtiaoBO6.setStation_address("广东省广州市海珠区会展南三路"); kongtiaoBO6.setStation_position("23.10319,113.370088"); beans.add(kongtiaoBO6); try { server.addBeans(beans); server.commit(); System.out.println("提交完毕!"); } catch (Exception e) { e.printStackTrace(); } } } /** * * @author Tony * */ public class SolrServer { private static SolrServer solrServer = null; private static HttpSolrClient server = null; private static String url = "http://localhost:8881/solr/test"; public static synchronized SolrServer getInstance() { if (solrServer == null) { solrServer = new SolrServer(); } return solrServer; } public static HttpSolrClient getServer() { if (server == null) { server = new HttpSolrClient.Builder(url).build(); server.setDefaultMaxConnectionsPerHost(1000); server.setMaxTotalConnections(10000); server.setConnectionTimeout(60000);// 设置连接超时时间(单位毫秒) 1000 server.setSoTimeout(60000);//// 设置读数据超时时间(单位毫秒) 1000 server.setFollowRedirects(false);// 遵循从定向 server.setAllowCompression(true);// 允许压缩 } return server; } }
这里使用“经度 纬度”这样的字符串格式将经纬度索引到station_position字段
3、查询
查询语法示例:
q={!geofilt pt=45.15,-93.85 sfield=poi_location_p d=5 score=distance}
q={!bbox pt=45.15,-93.85 sfield=poi_location_p d=5 score=distance}
q=poi_location_p:"Intersects(-74.093 41.042 -69.347 44.558)" //a bounding box (not in WKT)
q=poi_location_p:"Intersects(POLYGON((-10 30, -40 40, -10 -20, 40 20, 0 0, -10 30)))" //a WKT example
涉及到的字段说明:
字段 |
含义 |
q |
查询条件,如 q=poi_id:134567 |
fq |
过滤条件,如 fq=store_name:农业 |
fl |
返回字段,如fl=poi_id,store_name |
pt |
坐标点,如pt=54.729696,-98.525391 |
d |
搜索半径,如 d=10表示10km范围内 |
sfield |
指定坐标索引字段,如sfield=geo |
defType |
指定查询类型可以取 dismax和edismax,edismax支持boost函数相乘作用,dismax是通过累加方式计算最后的score. |
qf |
指定权重字段:qf=store_name^10+poi_location_p^5 |
score |
排序字段根据qf定义的字段defType定义的方式计算得到score排序输出 |
其中有几种常见的Solr支持的几何操作:
WITHIN:在内部
CONTAINS:包含关系
DISJOINT:不相交
Intersects:相交(存在交集)
1)点查询
测试代码:查询距离某个点pt距离为d的集合
从这部分结果集中可以看出,数据是离目标点" 23.104487,113.375981 "最近的
多边形查询:
JtsSpatialContextFactory
当有Polygon多边形时会使用jts(需要把jts.jar放到solr webapp服务的lib下)。基本形状使用SpatialContext (spatial4j的类)。
Jts下载:https://repo1.maven.org/maven2/com/vividsolutions/jts-core/
测试代码:
SolrQuery params = new SolrQuery();
//q=geo:"Intersects(POLYGON((-10 30, -40 40, -10 -20, 40 20, 0 0, -10 30)))"
params.set("q", "station_position:\"Intersects(POLYGON((118 40, 118.5 40, 118.5 38, 118.3 35, 118 38,118 40)))\"");
params.set("start", "0"); //记录开始位置
params.set("rows", "100"); //查询的行数
params.set("fl", "*");
返回在这个POLYGON内的所有结果集。
管方文档:
http://lucene.apache.org/solr/guide/6_6/spatial-search.html
经纬度工具:
http://www.hhlink.com/%E7%BB%8F%E7%BA%AC%E5%BA%A6
百度地图API工具:
http://lbsyun.baidu.com/index.php?title=jspopular
查看原文: http://www.dczou.com/viemall/793.html