MongoDB sắp xếp theo khoảng cách

Trong loạt bài viết này, tôi sẽ mô tả một số kỹ thuật cần thiết để truy vấn dữ liệu không gian địa lý trong MongoDB, điều này có thể hữu ích nếu bạn muốn ứng dụng hoặc API của mình cung cấp quyền truy cập vào thông tin được sắp xếp dựa trên khoảng cách từ một số vị trí cụ thể. Ví dụ

  • kinh doanh (nhà hàng, cửa hàng vv. ) hoặc các địa điểm yêu thích khác,
  • những người dùng khác của ứng dụng (như trong trường hợp ứng dụng hẹn hò)

Các kỹ thuật này sẽ cho phép dịch vụ của bạn mở rộng quy mô và duy trì hiệu quả vì chúng cho phép truy cập dữ liệu theo thời gian liên tục (bất kể lượng dữ liệu trong cơ sở dữ liệu của bạn là bao nhiêu) và giảm thiểu yêu cầu bộ nhớ đệm ở phía máy khách

Bạn sẽ học được gì

Trong các phần sau tôi sẽ chỉ cho bạn cách

  1. lưu trữ dữ liệu vị trí (cặp kinh độ, vĩ độ) trong tài liệu MongoDB
  2. truy vấn các tài liệu đó bằng cách sử dụng dữ liệu vị trí, với các kết quả được sắp xếp theo khoảng cách từ một điểm cụ thể (từ gần nhất đến xa nhất)
  3. trang hiệu quả thông qua kết quả của các truy vấn như vậy

Bạn sẽ có thể làm theo hướng dẫn này ngay cả khi bạn chưa từng sử dụng MongoDB trước đây. Mặt khác, nếu 2 điểm đầu tiên nghe có vẻ quen thuộc, bạn có thể chuyển thẳng sang phần 3

Chúng tôi sẽ sử dụng Node. js và trình điều khiển Javascript MongoDB chính thức. Các đoạn mã sẽ có trong Coffeescript 2

Nếu bạn muốn chạy các ví dụ mã cục bộ, hãy sao chép repo đi kèm và làm theo hướng dẫn trong README.md

Lưu trữ dữ liệu vị trí

Đối với các cặp kinh độ, vĩ độ, chúng ta cần sử dụng định dạng đối tượng

doc ở trên là những gì bạn muốn chèn vào bộ sưu tập mongo, nó có một trường có tên là location có giá trị là một đối tượng Điểm GeoJSON. Tên của trường là không quan trọng, chúng tôi có thể đã sử dụng bất kỳ tên khóa hợp lệ nào khác thay vì location. Chúng tôi cũng có thể lồng GeoJSON sâu hơn vào cấu trúc đối tượng, tuy nhiên, đặt typecoordinates ở cấp cao nhất của doc sẽ không hoạt động. Ví dụ, cũng có thể lưu trữ nhiều đối tượng GeoJSON trong một tài liệu

Lưu ý rằng

  • kinh độ đến trước (điều này ngược lại với những gì bạn có thể được sử dụng, ví dụ: truy vấn bản đồ google)
  • giá trị kinh độ cần nằm trong khoảng từ -180 đến 180 (bao gồm cả hai)
  • giá trị vĩ độ cần nằm trong khoảng từ -90 đến 90 (bao gồm cả hai)

Bây giờ bạn đã biết những điều cơ bản, hãy tạo một số dữ liệu để làm việc với

Chúng tôi đã tạo 6 tài liệu, với các vị trí bắt đầu từ đường xích đạo, tăng vĩ độ theo các khoảng thời gian bằng nhau, trong khi vẫn giữ cố định kinh độ ở 0. Dưới đây là các điểm được vẽ trên một hình cầu

Xem mã được sử dụng để tạo mã này trên JSFiddle

Lưu ý rằng trong các ví dụ mã sau đây, chúng tôi sẽ bỏ qua bản soạn sẵn cần thiết để lấy đối tượng bộ sưu tập mongodb và chèn tài liệu vào đó. Trong repo đi kèm, bản tóm tắt này đã được chứa trong các hàm trợ giúp with_collectionwith_collection_with_points

Truy vấn tài liệu dựa trên vị trí

Để truy vấn các tài liệu dựa trên khoảng cách của chúng từ một điểm cụ thể, chúng ta sẽ sử dụng toán tử truy vấn $near. Nhưng trước khi làm điều này, chúng ta cần tạo một chỉ mục 2dsphere trên trường chứa các đối tượng Điểm GeoJSON

ví dụ có thể chạy được trên github

Đặt tùy chọn nền là rất quan trọng khi tạo chỉ mục trên cơ sở dữ liệu trực tiếp, vì theo mặc định sẽ chặn tất cả các hoạt động khác trên cơ sở dữ liệu trong khi chỉ mục đang được tạo (có thể mất một lúc nếu bộ sưu tập lớn)

Bây giờ, truy vấn cơ bản, trả về tất cả tài liệu được sắp xếp theo khoảng cách ( chính xác là khoảng cách vòng tròn lớn ) từ điểm doc0, dựa trên dữ liệu tại trường location trong tài liệu

ví dụ có thể chạy được trên github

Trừ khi ý định của bạn là xử lý tất cả các tài liệu trong bộ sưu tập ( trong trường hợp đó, bạn có thể gọi là ____1_______2 thay vì ____1_______3 ), bạn sẽ muốn giới hạn số lượng tài liệu được trả lại. Đây là cách nó được thực hiện

ví dụ có thể chạy được trên github

Trong hình minh họa bên dưới, điểm màu trắng đánh dấu vị trí được sử dụng trong truy vấn trên ( doc0 ) và các điểm được khoanh tròn màu xanh lá cây biểu thị kết quả của truy vấn với doc5 được đặt thành doc6

xem mã được sử dụng để tạo mã này trên Phân trang JSFiddle thông qua các kết quả, từ gần nhất đến xa nhất (chuyển tiếp)

Ghi chú. kỹ thuật được thảo luận trong phần này đã được mô tả trước đây bởi A. Jesse Jiryu Davis, người đã triển khai tính năng MondoDB giúp cho kỹ thuật này trở nên khả thi. Bài viết của anh ấy đi vào chi tiết về lý do tại sao phương pháp này hoạt động hiệu quả, vì vậy nó đáng để đọc, tuy nhiên các ví dụ về mã là trong python, do đó, chúng tôi sẽ thực hiện từng bước tại đây vì lợi ích của Node. cộng đồng js

Dựa trên truy vấn mà chúng tôi đã xác định trong phần trước, cách đơn giản nhất để triển khai phân trang là sử dụng doc5 để kiểm soát kích thước trang/lô và phương pháp doc8 để đặt độ lệch trang mong muốn. Ví dụ

ví dụ có thể chạy được trên github

Truy vấn trên sẽ trả về trang kết quả thứ 2 khi truy vấn dữ liệu thử nghiệm của chúng tôi, như minh họa bên dưới

xem mã được sử dụng để tạo mã này trên JSFiddle

Tuy nhiên, hiệu suất của doc9 giảm tuyến tính khi phần bù tăng ( như đã trình bày trong bài viết đã đề cập ở trên ), do máy chủ MongoDB cần quét qua tất cả các kết quả truy vấn ngay từ đầu cho đến khi đạt được phần bù

Một giải pháp thay thế thời gian không đổi liên quan đến việc sử dụng toán tử truy vấn location0, để loại trừ các kết quả nằm trong bán kính nhất định khỏi điểm truy vấn

Giả sử rằng chúng ta biết khoảng cách giữa điểm truy vấn và tài liệu xa nhất từ ​​một trang kết quả nhất định, chúng ta có thể truy vấn trang tiếp theo như sau

Nhưng chúng ta lấy khoảng cách từ đâu? . Hoặc sử dụng một cái gì đó như location1. Tuy nhiên, chúng tôi phải đảm bảo rằng triển khai đã chọn của chúng tôi phù hợp với triển khai được sử dụng bởi MongoDB. May mắn thay, chúng tôi không phải trải qua tất cả những rắc rối đó vì chúng tôi có thể yêu cầu MongoDB đính kèm khoảng cách được tính toán động cho từng tài liệu trong kết quả truy vấn. Chúng tôi sẽ chỉ phải chuyển đổi truy vấn location2 của mình thành một quy trình tổng hợp tương đương, sử dụng giai đoạn $geoNear với tùy chọn location3 của nó

Bây giờ chúng ta có thể sử dụng thuộc tính location4 của tài liệu cuối cùng từ kết quả truy vấn (vì chúng được sắp xếp theo khoảng cách, theo thứ tự tăng dần), để tìm nạp trang tiếp theo. Hãy sử dụng chức năng trên để tìm nạp hai trang đầu tiên

ví dụ có thể chạy được trên github

Và đây là hình ảnh trực quan về kết quả của lệnh gọi thứ hai tới location5, với vòng tròn màu vàng bao quanh khu vực bị loại trừ khỏi truy vấn bằng cách sử dụng location6

xem mã được sử dụng để tạo mã này trên JSFiddle

Tuy nhiên, đây không phải là chính xác những gì chúng tôi muốn. tài liệu cuối cùng của trang trước được đưa vào trang tiếp theo, vì location6 chỉ loại trừ tài liệu, khoảng cách nào nhỏ hơn giá trị đã cho. Để ngăn chặn điều này, chúng tôi cần thêm truy vấn sau vào giai đoạn tổng hợp location8 của chúng tôi

ví dụ có thể chạy được trên github

Truy vấn sử dụng toán tử location9 để bỏ qua các tài liệu với các ____2_______0 đã cho khi thu thập kết quả. Chúng ta xong chưa? . Xét tập hợp các điểm sau

Lưu ý rằng điểm thứ 2 và thứ 3 nằm ở cùng một khoảng cách chính xác từ điểm doc0. Bây giờ, nếu chúng ta đặt location2 thành doc0 và location4 thành location5, thì kết quả tìm nạp trang thứ hai sẽ như thế nào?

xem mã được sử dụng để tạo mã này trên JSFiddle

Điều này là do cả location6 và location8 đều không loại trừ tài liệu có tọa độ location9. Do đó, thay vì chỉ sử dụng location0 của tài liệu cuối cùng, chúng ta cần thu thập location0 của tất cả các tài liệu có khoảng cách bằng khoảng cách của type2. Để tất cả chúng cùng nhau

Đưa ra type3, đây là cách bạn tìm nạp cái tiếp theo

ví dụ có thể chạy được trên github

Lưu ý rằng logic trong type4 và type5 rất có thể sẽ được thực thi trên máy khách, do đó nó không được đưa vào hàm location5, hàm dự kiến ​​​​_______4_______7 và type8 được tính toán trước sẽ được chuyển vào. Điều này là để giảm thiểu lượng dữ liệu được gửi qua dây. Ngoài ra, máy chủ có thể bao gồm Tiêu đề liên kết HTTP với tất cả thông tin cần thiết để tìm nạp trang tiếp theo, trong trường hợp đó, tất cả mã trên sẽ được thực thi trên máy chủ

Cuối cùng, chúng ta cần xử lý trường hợp có quá nhiều tài liệu có cùng khoảng cách, đến nỗi tài liệu cuối cùng trong một trang có cùng khoảng cách với type8 được sử dụng để tìm nạp trang đó. Ví dụ: khi tìm nạp trang thứ 3 từ tập hợp các điểm sau ( với coordinates0 và coordinates1 )

Sử dụng triển khai ở trên, chúng tôi sẽ nhận được điểm coordinates2 thay vì coordinates3 mong muốn, vì location0 của điểm cuối cùng từ trang 1 ( location6 ), không bị loại trừ mặc dù khoảng cách của nó giống như location6. Do đó, trong những trường hợp như vậy, chúng ta cần chuyển type7 từ truy vấn trước sang truy vấn tiếp theo

ví dụ có thể chạy được trên github

Điều đáng chú ý là trong trường hợp cực đoan, tất cả các tài liệu có cùng khoảng cách, kích thước của mảng type7, và do đó, lượng dữ liệu được gửi qua dây theo từng yêu cầu, sẽ tăng tuyến tính khi chúng tôi chuyển qua các trang. Trên hết, bản thân hiệu suất truy vấn sẽ giảm theo cách tương tự (và tôi cá là nó sẽ tệ hơn khi triển khai ngây thơ chỉ dựa vào doc9). Để giảm thiểu điều này, thay vì tích lũy type7, chúng tôi có thể giữ số lượng tài liệu để bỏ qua (và sử dụng nó cùng với location6). Điều này sẽ giữ cho kích thước của yêu cầu không đổi và hiệu suất truy vấn nằm trong giới hạn của giải pháp doc9 đơn giản. Tuy nhiên, việc theo dõi các location0 để loại trừ có một số lợi thế so với việc sử dụng bỏ qua trong các trường hợp dữ liệu được truy vấn có tính động cao ( khi các thay đổi dự kiến ​​sẽ xảy ra giữa các lần tìm nạp), mà chúng ta có thể thảo luận trong các bài viết sau

Cuối cùng, tôi muốn bạn lưu ý rằng phương pháp trên không hỗ trợ chuyển trực tiếp đến một trang cụ thể ( mà không tìm nạp tất cả các trang ở giữa ). Tuy nhiên, điều này có thể đạt được bằng cách thêm doc9 ( bằng doc5 ) thích hợp vào truy vấn. Việc sử dụng doc9 rõ ràng sẽ bị phạt về hiệu suất tỷ lệ thuận với số lượng tài liệu bị bỏ qua, nhưng thực sự không có cách nào khác (trừ khi thay vì chỉ định số lượng trang/tài liệu cần bỏ qua, trường hợp sử dụng của bạn có thể thực hiện bằng cách sử dụng location6 để bỏ qua một mục không xác định . May mắn thay, chúng tôi chỉ cần trả mức giá này một lần cho mỗi lần nhảy trang, vì một khi trang mong muốn được tìm nạp bằng cách sử dụng doc9, trang tiếp theo có thể được truy vấn dựa trên chỉ bằng cách sử dụng location6 và type7

Phần kết luận

Chúng tôi đã trình bày cách lưu trữ và truy vấn dữ liệu không gian địa lý trong mongodb và thảo luận cách chuyển trang hiệu quả qua lượng lớn dữ liệu đó (từ vị trí gần nhất đến vị trí xa nhất). Trong Phần 2 của loạt bài này, bạn sẽ học cách sử dụng một thủ thuật tiện lợi để lật trang qua các vị trí theo thứ tự ngược lại

Đừng quên sao chép repo đi kèm và chơi với các ví dụ mã có thể dễ dàng chạy từ dòng lệnh