es地理距离查询必须使用geo_point类型字段,存入格式为{"lat":39.9,"lon":116.3}或[116.3,39.9],需显式定义mapping,laravel scout需绕过默认搜索逻辑,用官方sdk构造geo_distance filter dsl。

ES 地理距离查询必须用 geo_point 类型字段
直接对字符串或普通数值字段做距离过滤会失败,Elasticsearch 会报错 failed to find geo_point field。你得确认索引 mapping 中目标字段(比如 location)类型是 geo_point,而不是 text 或 keyword。
常见错误是存入时用 "lat: 39.9, lng: 116.3" 这种字符串,ES 解析不了。正确写法是存成对象:{"lat": 39.9, "lon": 116.3},或者数组格式 [116.3, 39.9](注意顺序:lon, lat)。
- 建索引时显式定义:
"location": {"type": "geo_point"} - 用 Laravel Scout + TNTSearch 的同学注意:TNT 不支持地理查询,必须切到
elasticsearch驱动 - 已存在数据需 reindex,不能仅改 mapping
Laravel Scout + ES 实现 geo_distance 查询
Scout 默认不暴露原生 ES 查询 DSL,得绕过 Scout builder,直接调用 ES 客户端。推荐用 elasticsearch/elasticsearch 官方 PHP SDK,配合 Scout 的 searchableUsing() 或自定义引擎。
关键点不是“怎么搜”,而是“在哪拼 DSL”——别在 toSearchableArray() 里硬塞坐标,而是在搜索发起时动态注入 geo_distance filter:
- 构造 DSL 示例:
{ "query": { "bool": { "filter": [ { "geo_distance": { "distance": "5km", "location": { "lat": 39.9, "lon": 116.3 } } } ] } } } - 用
Client::search()替代Model::search()->get() - 注意:Scout 的
where()不支持地理 filter,强行传会忽略
距离单位、性能与精度取舍
distance 参数支持 km、mi、m、ft,但别用 5000m 代替 5km——ES 内部解析有微小误差,尤其大范围(>100km)时,单位写错会导致结果偏移几百米。
geo_distance 查询本身是过滤器(filter),不参与评分,但范围越大,命中的 doc 越多,后续聚合或分页成本越高。实测:半径从 1km 扩到 10km,响应时间可能翻 3 倍。
- 高频查询建议加
geohash前缀索引(mapping 中配置"precision": "4km") - 用户定位不准时(如只拿到城市名),先用
match_phrase粗筛,再套geo_distance精筛 - 移动端传来的经纬度常带 6 位小数,ES 存储时建议截断到 5 位,减少索引体积
PHP 里处理坐标输入的边界情况
用户提交的 lat/lng 经常非法:空值、超界(纬度 >90)、字符串("39.9000000001")、科学计数法(1.23e-5)。ES 遇到非法坐标会整个 query 失败,返回 illegal_argument_exception。
别依赖 ES 做校验,Laravel 层就得拦住:
- 用
filter_var($lat, FILTER_VALIDATE_FLOAT)+ 范围判断(-90 ) - 经度范围是
-180 ~ 180,但注意:某些地图 SDK 返回的是0~360,得先归一化 - 避免浮点精度问题:
round($lat, 5)再传给 ES,否则可能因尾数差异导致 geohash 计算偏差 - 空坐标直接返回空集合,不要让请求落到 ES 上
地理检索真正卡脖子的从来不是 DSL 写法,而是坐标质量、索引结构和边界兜底——漏掉任意一环,线上就容易出现“搜不到附近店”这种用户一眼感知的问题。











