如何从按距离排序的JPA实体获得结果?

我目前正在编写一个移动应用程序,用户必须从列表中选择一个位置。 使用来自Play应用程序的JPA将所有位置存储在Postgres数据库中。

我想要做的是在应用程序中获取用户位置,然后请求获取离该用户最近的前20或50个位置。

如果我使用自己的数据结构,我会使用KD-Tree。 但是,我对JPA / Play / PostgreSQL很新,所以我不确定如何手动处理数据持久性。

我能用现有的知识来思考的唯一事情就是查看每个位置并确定它的距离,但是在如此庞大的数据库中,这个速度会非常慢。

有没有一个查询我可以说“给我X的第一个结果按照这个纬度和经度的距离排序?

编辑:我正在使用Heroku,因为应用程序处于开发的早期阶段,如果你想在你的应用程序中使用PostGIS,我宁愿不必支付每天200美元的Heroku费用。

真的不想为此推出自己的数据结构,但幸运的是你正在使用PostgreSQL,所以你很幸运。 使用PostGIS 。 它会比你在合理的时间内构建的任何东西快几个数量级。

这是我在大约3年前构建的应用程序中使用的函数的大部分简化版本。 适应了手头的问题。

  • 使用方框在点的周边查找位置。 人们可以用圆圈来做到这一点以获得更准确的结果,但这只是一个开始的近似值。

  • 忽视世界不平坦的事实。 我的申请仅适用于几百公里的当地地区。 搜索范围仅跨越几公里。 让世界变得平坦就足够了。 (Todo:根据地理位置的比率lat / lon的更好近似值可能会有所帮助。)

  • 使用Google地图中的地理编码进行操作。

  • 没有扩展名的情况下使用标准PostgreSQL(不需要PostGis),在PostgreSQL 9.1和9.2上测试。

如果没有索引,则必须计算基表中每一行的距离并过滤最接近的行。 大桌子非常昂贵。

编辑:
我重新检查并且当前的实现允许点上的GisT索引(Postgres 9.1或更高版本)。 相应地简化了代码。

主要技巧是使用function的GiST索引 ,即使列只是一个点。 这使得可以使用现有的GiST实现 。

通过这种(非常快速)搜索,我们可以获得一个盒子内的所有位置。 剩下的问题是:我们知道行数,但我们不知道它们所在的盒子的大小。这就像知道部分答案,而不是问题。

我在dba.SE上的相关答案中使用了类似的反向查找方法。 (只是,我这里没有使用部分索引 – 实际上可能也有效)。

迭代一系列预定义的搜索步骤,从非常小到“足够大以至少保持足够的位置”。 意味着我们必须运行几个(非常快)的查询才能达到搜索框的大小。

然后使用此框搜索基表,并仅计算从索引返回的几行的实际距离。 因为我们发现盒子至少有足够的位置,所以通常会有一些剩余。 通过采用最接近的,我们有效地围绕框的角落。 您可以通过使框更大一些(在函数中乘以sqrt(2)得到完全准确的结果来强制此效果,但我不会全力以赴,因为这是接近开始)。

使用最新版本的PostgreSQL中提供的SP GiST索引,这将更快更简单。 但我不知道这是否可能。 我们需要一个实际的数据类型实现,我没有时间深入研究它。 如果您找到方法,请承诺报告!

给定这个带有一些示例值( adr .. address)的简化表:

 CREATE TABLE adr(adr_id int, adr text, geocode point); INSERT INTO adr (adr_id, adr, geocode) VALUES (1, 'adr1', '(48.20117,16.294)'), (2, 'adr2', '(48.19834,16.302)'), (3, 'adr3', '(48.19755,16.299)'), (4, 'adr4', '(48.19727,16.303)'), (5, 'adr5', '(48.19796,16.304)'), (6, 'adr6', '(48.19791,16.302)'), (7, 'adr7', '(48.19813,16.304)'), (8, 'adr8', '(48.19735,16.299)'), (9, 'adr9', '(48.19746,16.297)'); 

索引看起来像这样:

 CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode); 

– > SQLfiddle

您必须根据需要调整家庭区域,步数和缩放系数。 只要你在一个点周围几公里的盒子中搜索,一个平坦的地球就足够了。

你需要很好地理解plpgsql才能使用它。 我觉得我在这里做得很好。

 CREATE OR REPLACE FUNCTION f_find_around(_lat double precision, _lon double precision, _limit bigint = 50) RETURNS TABLE(adr_id int, adr text, distance int) AS $func$ DECLARE _homearea CONSTANT box := '(49.05,17.15),(46.35,9.45)'::box; -- box around legal area -- 100m = 0.0008892 250m, 340m, 450m, 700m,1000m,1500m,2000m,3000m,4500m,7000m _steps CONSTANT real[] := '{0.0022,0.003,0.004,0.006,0.009,0.013,0.018,0.027,0.040,0.062}'; -- find optimum _steps by experimenting geo2m CONSTANT integer := 73500; -- ratio geocode(lon) to meter (found by trial & error with google maps) lat2lon CONSTANT real := 1.53; -- ratio lon/lat (lat is worth more; found by trial & error with google maps in (Vienna) _radius real; -- final search radius _area box; -- box to search in _count bigint := 0; -- count rows _point point := point($1,$2); -- center of search _scalepoint point := point($1 * lat2lon, $2); -- lat scaled to adjust BEGIN -- Optimize _radius IF (_point <@ _homearea) THEN FOREACH _radius IN ARRAY _steps LOOP SELECT INTO _count count(*) FROM adr a WHERE a.geocode <@ box(point($1 - _radius, $2 - _radius * lat2lon) , point($1 + _radius, $2 + _radius * lat2lon)); EXIT WHEN _count >= _limit; END LOOP; END IF; IF _count = 0 THEN -- nothing found or not in legal area EXIT; ELSE IF _radius IS NULL THEN _radius := _steps[array_upper(_steps,1)]; -- max. _radius END IF; _area := box(point($1 - _radius, $2 - _radius * lat2lon) , point($1 + _radius, $2 + _radius * lat2lon)); END IF; RETURN QUERY SELECT a.adr_id ,a.adr ,((point (a.geocode[0] * lat2lon, a.geocode[1]) <-> _scalepoint) * geo2m)::int4 AS distance FROM adr a WHERE a.geocode <@ _area ORDER BY distance, a.adr, a.adr_id LIMIT _limit; END $func$ LANGUAGE plpgsql; 

呼叫:

 SELECT * FROM f_find_around (48.2, 16.3, 20); 

如果在定义的最大搜索区域中有足够的位置,则返回$3位置的列表。
按实际距离排序。

进一步改进

构建如下函数:

 CREATE OR REPLACE FUNCTION f_geo2m(double precision, double precision) RETURNS point AS $BODY$ SELECT point($1 * 111200, $2 * 111400 * cos(radians($1))); $BODY$ LANGUAGE sql IMMUTABLE; COMMENT ON FUNCTION f_geo2m(double precision, double precision) IS 'Project geocode to approximate metric coordinates. SELECT f_geo2m(48.20872, 16.37263) --'; 

(字面上)全局常数111200111400是根据经度 的长度和纬度的长度针对我的地区(奥地利)进行优化的,但基本上只是在世界各地工作。

使用它将缩放的地理编码添加到基表,理想情况下是生成的列,如本答案中所述:
你怎么做数学忽略年份?
请参阅3. Black magic版本 ,我将引导您完成整个过程。
然后,您可以更多地简化function:缩放输入值一次并删除冗余计算。