移動互聯網時代,LBS (Location-Based Services) 應用爆發式增長。核心技術挑戰:海量用戶實時位置的高效儲存、範圍查詢、距離排序。本文基於 Symfony2 + Doctrine2 ODM + MongoDB 組合,深度解析地理位置查詢方案選型與實作。
SELECT id,
(6371 * ACOS(
COS(RADIANS(37)) * COS(RADIANS(lat)) * COS(RADIANS(lng) - RADIANS(-122)) +
SIN(RADIANS(37)) * SIN(RADIANS(lat))
)) AS distance
FROM places
HAVING distance < 25
ORDER BY distance
LIMIT 0, 100;
優點:無遷移成本、SQL 標準、關聯查詢靈活。
缺點:全表掃描計算距離,CPU 密集,無法使用索引,百萬級資料查詢秒級以上,不可用於生產。
INDEX(geohash),查詢時計算目標 GeoHash 及相鄰 8 格前綴,WHERE geohash LIKE 'wx4g0%'。POINT(lat, lng) 地理類型,建立 SPATIAL INDEX (R-Tree)。MBRContains(GeomFromText('Polygon(...))', location) 或 ST_Distance_Sphere (MySQL 5.7+)。
db.places.createIndex({ location: "2dsphere" })
$near / $nearSphere:按距離排序返回最近文檔。$geoWithin + $centerSphere / $box / $polygon:範圍查詢不排序。$geoIntersects:幾何相交判斷。
db.places.find({
location: {
$nearSphere: {
$geometry: { type: "Point", coordinates: [121.5, 25.0] }, // [lng, lat]
$maxDistance: 5000 // 米
}
},
type: "cafe"
}).limit(20)
db.places.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [121.5, 25.0] },
distanceField: "dist.calculated",
maxDistance: 5000,
spherical: true,
query: { type: "cafe" }
}
},
{ $limit: 20 }
])
| 維度 | MongoDB 2dsphere | MySQL Spatial | GeoHash |
|---|---|---|---|
| 索引類型 | R-Tree (2dsphere) | R-Tree (SPATIAL) | B-Tree (字串前綴) |
| 球面計算 | 原生支援 | 5.7+ 支援 | 應用層近似 |
| 距離排序 | $nearSphere 原生 | ST_Distance_Sphere | 應用層計算 |
| 多邊形查詢 | $geoWithin $polygon | ST_Within | 困難 |
| 寫入效能 | 極高 (無鎖、追加寫入) | 中等 | 中等 |
| 水平擴展 | 原生 Sharding | 困難 | 需應用層分片 |
/**
* @ODM\Document(collection="places")
* @ODM\Index(keys={"location"="2dsphere"})
*/
class Place {
/** @ODM\Id */
protected $id;
/** @ODM\Field(type="string") */
protected $name;
/** @ODM\Field(type="string") */
protected $type; // cafe, restaurant, atm...
/**
* @ODM\EmbedOne(targetDocument="GeoPoint")
* @ODM\Index(type="2dsphere")
*/
protected $location;
// Getter/Setter...
}
/** @ODM\EmbeddedDocument */
class GeoPoint {
/** @ODM\Field(type="string") */
protected $type = "Point";
/** @ODM\Field(type="collection") */
protected $coordinates = []; // [lng, lat]
}
class PlaceRepository extends DocumentRepository {
public function findNearby(GeoPoint $point, $maxDistance = 5000, $limit = 20, $type = null) {
$qb = $this->createQueryBuilder()
->field('location')->nearSphere($point->getCoordinates())
->maxDistance($maxDistance)
->limit($limit);
if ($type) $qb->field('type')->equals($type);
return $qb->getQuery()->execute();
}
public function findWithinPolygon(array $coordinates, $limit = 50) {
// $coordinates = [[lng,lat], [lng,lat], ...] 封閉多邊形
return $this->createQueryBuilder()
->field('location')->geoWithin(['$geometry' => ['type' => 'Polygon', 'coordinates' => [$coordinates]]])
->limit($limit)
->getQuery()->execute();
}
}
class PlaceController extends Controller {
public function nearbyAction(Request $request) {
$lng = $request->query->get('lng');
$lat = $request->query->get('lat');
$radius = $request->query->get('radius', 5000);
$point = new GeoPoint();
$point->setCoordinates([$lng, $lat]);
$places = $this->get('doctrine_mongodb')
->getRepository('AcmeDemoBundle:Place')
->findNearby($point, $radius);
$serializer = $this->get('jms_serializer');
return new Response($serializer->serialize($places, 'json'));
}
}
{type: 1, location: "2dsphere"} 加速類型過濾 + 空間查詢。location 為 Shard Key (配合 hashed sharding) 水平擴展。connections.default.options.connectTimeoutMS=3000、serverSelectionTimeoutMS=5000。->select('name', 'type', 'location') 減少網路傳輸。| 問題 | 原因 | 解決 |
|---|---|---|
| 查詢無結果 / 結果錯誤 | 座標順序顛倒 | MongoDB GeoJSON 強制 [longitude, latitude] (x, y),勿搞反 |
| $near 不支援排序 + 過濾 | $near 自帶排序,不可再 sort() | 用 aggregation $geoNear 管線,可額外 $match、$project |
| 2dsphere 索引建立失敗 | 資料含非法幾何 (如 coordinates 非數字、缺失) | 寫入前驗證、清洗髒資料、使用 partialFilterExpression 忽略無效文檔 |
| 跨日期線/兩極查詢異常 | 球面幾何邊界條件 | 拆分查詢區域、或使用 $geoWithin $box 矩形近似 |
MongoDB 原生 2dsphere 索引配合 $nearSphere/$geoNear 管線,提供開箱即用、高效能、標準化的地理空間查詢能力,是 LBS 應用的最佳資料庫選擇。Symfony2 + Doctrine2 ODM 提供優雅的物件映射與查詢建構器,讓開發者專注業務邏輯而非底層 SQL/查詢語法。生產環境務必關注索引策略、Sharding 規劃、讀寫分離與監控告警,確保百萬級用戶實時位置服務穩定運行。
