座標查詢:半徑篩選、連結原點與最近鄰分頁
自 backend 5.21.0 起,紀錄查詢可以只保留座標落在某點半徑內的紀錄(within)、落在任一連結原點紀錄半徑內的紀錄(within_any),也可以依與某點的距離排序(sort[].distance_from)。伺服器為每個可索引的座標欄維護持久化 cell 索引。索引只負責挑出候選紀錄:是否命中由精確的幾何 predicate 決定,且包含邊界。在 stored_filters 與 any_of 中,座標 cell 的一般純量比較仍會被 ct.coordinate_sql_unsupported 拒絕;只有 is_null 與 is_not_null 可當作存在性篩選,對座標欄使用一般的 sort_by 或 sort key 也同樣被拒絕。舊版 filters map 不檢查座標欄,所以不要用它篩選座標欄。
這些 predicate 屬於具型別的 stored_filters 與 any_of 詞彙。它們可用於公司、部門與聊天室 scope 的私有 POST .../records/search 與 POST .../records/export body、saved view 的 config(透過同一條管線套用),以及 public-read 的查詢 body。Agent 工具箱的 records_query 與 records_export 工具也在 stored_filters 參數中接受它們。Row policy、rollup filter 與 aggregate filters 完全沒有座標運算子;publish session 的 replace_filter 在解析請求時接受 within 與 within_any,但開啟 session 時會拒絕,回 400 Invalid replace_filter: ct.coordinate_query_context_required。規則與 trigger 條件、Command 與公式則改用座標運算子。
半徑篩選:within
把下列 body 送到該表的一般紀錄查詢路由,例如 POST /private/module/custom_tables/company/tables/{table_id}/records/search。col_aaaaaaaa 代表既有 geographic 座標欄的內部鍵,請從欄位清單回應或 settings.column_mapping 取得;stored_filters 不會轉譯顯示名稱。
{
"stored_filters": [
{
"column": "col_aaaaaaaa",
"op": "within",
"value": {
"origin": {"lat": "25.0339640", "lon": "121.5644680"},
"radius": "150",
"metric": "geodesic"
}
}
],
"sort": [
{
"distance_from": {
"column": "col_aaaaaaaa",
"point": {"lat": "25.0339640", "lon": "121.5644680"}
},
"direction": "asc"
}
],
"limit": 10,
"offset": 0
}value 欄位 | 規則 |
|---|---|
origin | 必填。包含所有已宣告軸且沒有其他軸的完整點,每個分量都是符合該軸規定的小數字串(規則見下),或 "$row.<座標欄鍵>" |
radius | 必填。非負小數字串,或 "$row.<decimal 欄鍵>"。Geographic 半徑以公尺計,Cartesian 半徑使用欄位的軸單位;使用 squared 時,半徑是平方距離 |
metric | 選填。Cartesian:euclidean(預設)、squared、manhattan、chebyshev。Geographic:geodesic(預設)、haversine。使用另一種座標系統的 metric,在評估到紀錄時會被 400 ct.geometry_metric_mismatch 拒絕 |
accuracy_policy、sources、max_accuracy_m、max_capture_age_s | 選填的擷取品質選項,見下文 |
literal 點會像已儲存的 cell 一樣被正規化,origin 與 sort[].distance_from.point 都一樣。每個分量必須是符合該軸 precision 與 scale 的純小數字串(可有 -,不可有 +,不可有多餘的前導零,小數點前後都要有數字)。預設 geographic 軸(precision 10、scale 7)允許 7 位小數,所以超過 7 位的分量會被拒絕,即使多出來的位數全是 0 也一樣:請先把裝置回報的原始座標捨入到該軸的 scale 再送出。緯度必須在 ±90 內、經度必須在 ±180 內,若該軸宣告了 min 與 max,分量也必須落在其中。負零(-0)會被拒絕。這些失敗都是 400,代碼放在字串 detail:小數字串不是上述純粹形式(例如 +25、025、.5 或 -0)、小數位數超過 scale 或不符合 precision 時為 ct.coordinate_component_invalid,超出 bounds 時為 ct.coordinate_bounds_invalid,缺少軸時為 ct.coordinate_incomplete(欄位沒有宣告的軸則為 ct.coordinate_axes_mismatch)。literal 點沒有可取得單位的資料列,所以在 length_unit 使用 unit_column 的 Cartesian 欄位上會被 400 ct.coordinate_unit_missing 拒絕;該處仍可使用 $row 原點。
分量與半徑必須是小數字串:JSON number,或根本不是小數的字串(例如 1e5),在私有路由會得到 422。public-read 查詢 body 由路由自行解析,所以那裡每個驗證錯誤都是 400,字串 detail 以 invalid body: 開頭,後面接驗證訊息。未知 key(例如 weights、include_altitude 或 disclose_nearest)以同樣方式被拒絕,所以查詢沒有權重、海拔或最近原點揭露選項。負的 literal 半徑也以同樣方式被拒絕,驗證訊息為 Value error, ct.geometry_invalid_radius。在 geographic 欄位上,恰等於 180 的原點經度會在 cell 查找與精確判斷之前正規化為 -180;這不是一般性的經度環繞,也不適用於 Cartesian 欄位。
同列運算元
origin 與 radius 可以改為指向被判斷那一列的欄位,例如 {"origin": "$row.<座標鍵>", "radius": "$row.<decimal 鍵>"}。每個被引用的欄位都必須把 column_acl.write 設為 managers 或 none;否則請求以 400 與 {"detail":{"code":"ct.coordinate_reference_untrusted"}} 拒絕,且不回傳任何紀錄。原點欄的座標系統、有序軸與長度單位必須與被篩選欄位相同(ct.coordinate_system_mismatch、ct.coordinate_axes_mismatch、ct.coordinate_unit_mismatch),半徑欄必須是 decimal(ct.geometry_invalid_radius)。呼叫者無權讀取的欄位會被視同不存在的欄位,回報 Column '<key>' does not exist in table schema。被引用的原點或半徑為空的那一列,會讓查詢變成不確定,見下文。
任一連結原點:within_any
origins.via 是被查詢表上的原生 link 欄。原點表是 link 的目標表:其座標、decimal 半徑與 boolean 啟用欄具有所示內部鍵。它是被查詢表上的單一 link 欄,不是多跳的點號路徑。
{
"stored_filters": [
{
"column": "col_aaaaaaaa",
"op": "within_any",
"value": {
"origins": {
"via": "col_bbbbbbbb",
"location": "col_cccccccc",
"radius": "col_dddddddd",
"where": [{"column": "col_eeeeeeee", "op": "eq", "value": true}]
},
"accuracy_policy": "ignore"
}
}
],
"limit": 20
}origins 欄位 | 規則 |
|---|---|
via | 被查詢表上 link 欄的鍵(否則為 ct.coordinate_origins_require_link) |
location | 連結表上座標欄的鍵,座標系統、軸與長度單位須與被篩選欄位相同(否則為 ct.coordinate_system_mismatch) |
radius | 連結表上 decimal 欄的鍵(否則為 ct.geometry_invalid_radius),或非負小數字串 literal |
where | 選填,最多 10 個連結表欄位上的 predicate:{column, op, value},op 為 eq、neq、gt、gte、lt、lte、in、contains、is_null、is_not_null、between 之一 |
只要紀錄的座標落在至少一個合格原點的半徑內就命中。回應是被查詢表的命中紀錄;不會在每一列加上選中原點的 id 或距離。適用與 within 相同的 metric 與擷取品質選項。
原點以呼叫者在連結表上的一般讀取權限讀取,而不是像座標 check 規則那樣使用 kernel authority。只有呼叫者 row policy 看得到、且符合 where 的 live 連結列才合格,連結表的 location、radius 與 where 欄位也必須是呼叫者可讀的。對連結表沒有讀取權的呼叫者,以及任何 public-read 呼叫者,會得到 400 ct.coordinate_origin_read_authority_required。
除了讀取權限,還需要寫入保護:被查詢表的 via link、連結表的 location、欄位型的 radius 與每個 where 欄位,都必須有 column_acl.write 為 managers 或 none,否則請求回 400 ct.coordinate_reference_untrusted。Literal 半徑不需要半徑欄。呼叫者不必是 manager。
擷取品質選項
accuracy_policy(預設 ignore,或 strict)、sources、max_accuracy_m(正的小數字串)與 max_capture_age_s(1 到 86400 的整數)會依各紀錄座標 cell 中儲存的證據(src、acc_m、at)檢查。查詢自己的原點不帶任何證據。
sources:儲存的src不在清單中時以capture_untrusted失敗。max_accuracy_m:儲存的 accuracy 缺少或較大時以capture_accuracy失敗。strict在儲存的 accuracy 缺少時同樣以capture_accuracy失敗,否則會在半徑判斷前把儲存的 accuracy 加到距離上。max_capture_age_s:儲存的擷取時間缺少時以capture_missing_time失敗;cell 比上限舊或比現在晚超過 5 秒時以capture_stale失敗。- 較弱的 LINE opt-in 把
channel:line列入sources並省略max_accuracy_m:此時幾何對channel:line的 cell 使用零不確定度,其儲存的 accuracy 仍為空,即使在strict下也是如此。
sources 項目必須符合 channel:line、app:device、client、import、iac、manual 或 command:<id>,其他值是驗證錯誤(私有路由為 422,public-read 查詢 body 為 400 invalid body: …)。裝置擷取實際儲存的值是 device_geolocation,這個 schema 不接受它,所以查詢無法限定只看裝置擷取的 cell。出勤閘門請用接受 device_geolocation 的規則或 Command;查詢只負責找出紀錄。
不確定的結果會拒絕整個查詢
stored_filters 中的 within 或 within_any 篩選絕不會把未知當成「在範圍外」。只要範圍內任何一筆紀錄座標為空、沒有可用原點,或未通過擷取選項,整個請求就以 400 回應,且不回傳任何紀錄:
{"detail": {"code": "ct.coordinate_indeterminate", "reason": "null_value"}}reason | 原因 |
|---|---|
null_value | 範圍內有紀錄的座標 cell 為空 |
origin_missing | $row 原點或半徑為空、連結原點的位置或半徑為空,或某筆紀錄沒有合格的連結原點 |
capture_untrusted | 儲存的 src 不在 sources 中 |
capture_accuracy | 儲存的 accuracy 缺少或高於 max_accuracy_m,或 strict 找不到 accuracy |
capture_missing_time | 設了 max_capture_age_s,但儲存的擷取時間缺少 |
capture_stale | 儲存的擷取時間比 max_capture_age_s 舊,或比現在晚超過 5 秒 |
範圍是指一般篩選與 row policy 套用之後、呼叫者本來就能讀取的紀錄,所以看不到的紀錄不會造成拒絕。可能有空值時,請在同一個請求用一般篩選縮小範圍:座標為空用 {"column": "col_aaaaaaaa", "op": "is_not_null"},within_any 則在 link 欄上用 is_not_empty。對既有的表新增座標欄時,每筆紀錄一開始都是空的,請先填值,再依賴沒有額外篩選的 within。
距離排序與最近鄰分頁
- 每個接受
sort的介面最多請送 3 個 key,public-read 查詢 body 也一樣。每個 key 恰好指定column(儲存的純量欄或系統欄)與distance_from(座標欄column、完整 literalpoint、選填metric)其中一個。一般的座標columnkey 會被ct.coordinate_sql_unsupported拒絕,但只限sort沒有distance_fromkey 時:混合的清單由伺服器自行排序而不做這項檢查,所以不要把一般座標 key 放在距離 key 旁邊(可能以500失敗)。 direction是order的別名;兩者同時送出且值不同,在私有路由是422(public-read 查詢 body 為400 invalid body: …)。在 search 上,sort不能與sort_by併用(422);public-read 查詢 body 的sort優先於sort_by,不會報錯。- 距離在比較前會先捨入:geographic 到小數 3 位(公釐),Cartesian 到已宣告軸中最大的 scale。這與
within使用的未捨入比較不同。值相同時依序比下一個 key,最後比紀錄 id(以文字升序)。座標為空的紀錄在升降序都排最後。 - 升序的第一個距離 key 會透過逐步擴大的索引 cover 向外搜尋,直到有
offset + limit筆紀錄合格,但只有索引完整時才如此。索引待處理,或欄位沒有索引(四軸以上)時,伺服器改為載入整個範圍,與降序 key 相同:最多 50,000 筆,否則回ct.coordinate_query_cap且limit為 50,000。不論哪種情況,offset + limit都不可超過 20,000(ct.coordinate_query_cap),所以大 offset 和其他候選一樣消耗候選額度。 - 降序的距離 key,或不是第一個的距離 key,會對範圍內所有紀錄排序:範圍必須落在候選上限內(索引完整時 20,000,索引待處理時 50,000),否則請求回
ct.coordinate_query_cap。 - 在 search 上,
limit為 1 到 1,000(預設 100),offset為 0 或更大;total計算所有符合篩選的紀錄。Public-read 查詢 body 改用skip與limit(1 到 100,預設 50)。Export 兩者都不接受:它會以每次 1,000 列逐頁讀取排序後的結果,每一頁都計入同一個offset + limit額度。
Command select 中的最近鄰
第 2 版 Command 的 select 步驟可以回傳最近的 k 筆紀錄。它需要恰好一個升序 $distance order expression、單一 table source 與 limit;joins、grouping、distinct、having 與有界時間 where 在儲存 Command 時就會被拒絕,aggregate projection 則在 step 執行時以 invalid_expression 錯誤被拒絕。把 origin 宣告為相容的座標 input,並使用雙 operand 形式:
{
"kind": "select",
"name": "nearest_sites",
"from": {"table": "Sites", "as": "r"},
"select": {"site_id": {"$metadata": {"source": "r", "name": "id"}}},
"order_by": [
{
"expr": {"$distance": [{"$column": {"source": "r", "id": "col_aaaaaaaa"}}, "$input.origin"]},
"direction": "asc"
}
],
"limit": 10,
"max_rows": 10
}原點必須是綁定的常數,例如已宣告的座標 $input。同距離時依紀錄 id 決定順序,座標為空的紀錄排最後。取回的候選會計入 Command 的 work-row 額度,並受 statement timeout 限制:ct.coordinate_query_cap 會以 query_limit_exceeded 呈現,ct.coordinate_query_timeout 以 query_timeout 呈現。步驟語法請見 Command definition v2。
座標索引
每個 geographic 座標欄,以及軸數最多三個的 Cartesian 欄,都會自動建立索引;沒有開關設定。Cartesian cell 使用已宣告的軸 bounds,沒有 bounds 時則使用由 precision 與 scale 推得的範圍。四軸以上的 Cartesian 欄沒有索引,永遠走未索引路徑。索引儲存偶數層級 0、2、… 24 的 cell(geographic 欄為線性經緯度 quadkey,不是 Web Mercator 圖磚)。伺服器在每次紀錄寫入的同一個 transaction 內更新索引(包含軟刪除與還原),而且索引只會在呼叫者本來就能讀取的列之內縮小候選。
每個已索引欄位不是完整就是待處理:
- 建立表時就帶有座標欄的表,立即完整。
- 在預設設定下,對最多 500 筆紀錄(含垃圾桶中的紀錄)的表新增座標欄,會在回應回傳前完成索引。若表的紀錄(含垃圾桶中的)超過這個數量而新增仍是同步進行,索引會維持待處理。只有 REST 新增路由(公司、部門與聊天室 scope 的
POST .../columns)能在背景執行:501 到 10,000 筆 live 紀錄時,REST 新增會以背景欄位遷移執行,工作在表被鎖定期間分批建立索引,所以鎖解除且 migration 狀態為completed時欄位已完整。超過 10,000 筆 live 紀錄時,REST 新增完成後索引仍是待處理。IaC 的column行與 Agent 工具箱的columns_add工具一律同步新增,行為就像上面的同步新增:在儲存紀錄(含垃圾桶中的)超過 500 筆的表上,新增的座標欄會維持待處理,直到 operator 回填。 - 變更座標欄的 precision、scale 或軸 bounds,或還原垃圾桶中的表,對最多 500 筆紀錄(含垃圾桶中的)的表會就地重建索引,超過則維持待處理。
索引待處理期間,查詢仍然可用,答案仍然精確。它們不探測 cell,而是對已授權的紀錄做精確 predicate 判斷,因此候選上限是 50,000 而不是 20,000。一般篩選與 row policy 之後仍超過這個數量時,請求回 400 與 {"detail":{"code":"ct.coordinate_query_cap","limit":50000}}。請用一般篩選縮小範圍,或請 operator 回填索引。查詢回應沒有受支援的就緒旗標。
索引狀態由伺服器擁有,任何資料表讀取都不會回傳它。Backend 5.21.0 仍會把它放在 settings._coordinate_cells_v1 回給每一位讀取資料表中繼資料的人(每個座標欄一份:簽章、進度游標、complete 旗標,以及封存它們的證明)。自後端 5.22.0 起,資料表 GET、建立回應、各 scope 的清單、紀錄清單與搜尋回應中的 table 摘要、MCP 的資料表讀取(tables_get、tables_list、tables_shared_with_me)與 IaC export 都不再帶這個 key,所以索引是否待處理,只能從上述查詢回應的候選上限看出來。寫入也無法設定它:REST 建立與 bootstrap 收到含這個 key 的 settings 時回 422(Coordinate index state is server-owned),所以請不要把舊版回應中的 settings 原樣複製進建立請求。帶有這個 key 的 IaC 文件在 plan 與 apply 仍會被接受,key 則被忽略;5.22.0 之前上傳的 package input 是例外,見 Settings 照寫入內容保存。
| 上限 | 值 |
|---|---|
| 已索引查詢的候選 | 20,000 |
| 未索引(索引待處理)的候選 | 50,000 |
| 單次查詢讀取的連結原點列 | 20,000 |
| 每筆紀錄的合格連結原點 | 50 |
每個 origins 的 where predicate | 10 |
| 查詢執行時間 | 空間篩選與距離分頁合計 5 秒 |
| 分頁大小(search) | 1 到 1,000 |
| 排序 key | 3 |
literal 原點與半徑的單純 within 只探測 cover 內的 cell。within_any、$row 運算元與擷取選項必須驗證整個已授權範圍,所以上限適用於該範圍。within_any 查詢最多讀取 1,020,001 筆「紀錄到原點」的連結,並且不檢查這個上限是否溢出,所以產生更多連結的查詢可能對不完整的連結集合求值。這只會發生在索引待處理(或欄位沒有索引)時,因為此時範圍可達 50,000 筆紀錄;索引完整時範圍上限是 20,000 筆,且 50 個原點的拒絕會先發生。用一般篩選縮小範圍就能避開。這些是讀取查詢的上限,不是 Command 寫入上限。
Operator 回填
僅供平台 operator 使用。下列路由需要 CloudManager root 憑證(X-ADMIN-TOKEN、X-SUPER-TOKEN 與 X-CLOUD-TOKEN);租戶 UI 或整合不得呼叫。
Operator 以可續跑的回填一次補完一張表的待處理索引。company_id 是擁有該表的公司:
curl -X POST \
"$BASE_URL/root/hotfix/custom-table-coordinate-cells/$COMPANY_ID/$TABLE_ID" \
-H "X-ADMIN-TOKEN: $ADMIN_TOKEN" \
-H "X-SUPER-TOKEN: $SUPER_TOKEN" \
-H "X-CLOUD-TOKEN: $CLOUD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"batch_size": 500, "sleep_ms": 50, "max_batches": 20}'JSON body 本身是必填(要全部採用預設值就送 {};沒有 body 的 POST 會得到 422),但其中的欄位皆為選填:column_key(內部 col_<hex> 鍵,用來把這次執行限定在單一已索引欄位)、batch_size(1 到 1000,預設 500)、sleep_ms(0 到 1000,預設 50)與 max_batches(1 到 100,預設 20)。每個批次在表格 row lock 下各自 commit,所以一次呼叫對每個未完成的已索引欄位最多處理 batch_size 乘以 max_batches 筆紀錄(垃圾桶中的紀錄也計入批次),並且可以重複呼叫。請重複呼叫直到回應顯示 complete: true;已完成的表再呼叫會回相同狀態。要在不改動任何東西的情況下讀取進度,對同一路徑使用 GET(可選的 column_key query 參數):
{
"company_id": "11111111-1111-4111-8111-111111111111",
"table_id": "22222222-2222-4222-8222-222222222222",
"columns": [
{"column_key": "col_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "records_done": 10000, "records_remaining": 2500, "complete": false}
],
"complete": false
}records_done 與 records_remaining 計算 live 紀錄。不在該公司的表會回 404 與 {"detail":{"code":"ct.coordinate_table_not_found"}};不是該表已索引座標欄的 column_key 會回 400 ct.coordinate_column_not_indexed;body 值超出範圍是 422。某欄回報完成後,下一次查詢就會走已索引路徑。
拒絕
| 代碼 | 狀態與意義 |
|---|---|
ct.coordinate_query_requires_coordinate | 400:被篩選的欄位、$row 原點或 distance_from 欄位不是座標欄 |
ct.coordinate_origins_require_link | 400:via 不是被查詢表的 link 欄 |
ct.coordinate_origin_read_authority_required | 400:呼叫者無法讀取連結的原點表,或 public-read 查詢使用 within_any(public-read 的 within_any 可能先以 ct.coordinate_origins_require_link、ct.coordinate_reference_untrusted 或 ct.coordinate_indeterminate 失敗) |
ct.coordinate_reference_untrusted | 400,detail.code:$row 運算元或連結原點欄位缺少 managers 或 none 的寫入保護 |
ct.coordinate_indeterminate | 400,detail.code 與 detail.reason:原因見上表 |
ct.coordinate_system_mismatch、ct.coordinate_axes_mismatch、ct.coordinate_unit_mismatch | 400:座標運算元不相容 |
ct.coordinate_component_invalid、ct.coordinate_bounds_invalid、ct.coordinate_incomplete | 400,字串 detail:literal origin 或 distance_from.point 的分量不是符合該軸 precision 與 scale 的純小數字串、分量超出緯度、經度或已宣告的軸 bounds,或缺少軸 |
ct.geometry_metric_mismatch | 400:metric 不符合座標系統,在評估到紀錄時回報 |
ct.geometry_invalid_radius | within 的負 literal 半徑在私有路由為 422,在 public-read 查詢 body 為 400 且字串 detail 以 invalid body: … 開頭;origins.radius 的負 literal,或欄位型半徑無效、不是 decimal 欄時為 400 |
ct.coordinate_query_cap | 400,detail.code 與 detail.limit:候選超過 20,000(已索引)或 50,000(待處理,或沒有索引)。在任何索引狀態下,limit 也會是 20,000:合格的連結原點列超過 20,000 筆、升序第一個距離 key 的 offset + limit 超過 20,000,或最近鄰搜尋中嘗試的某個 cover 含有超過 20,000 筆紀錄時 |
ct.geofence_origin_cap | 400,detail.code 與 detail.limit 為 50:某筆紀錄的合格連結原點超過 50 個 |
ct.coordinate_query_timeout | 400,detail.code 與 detail.limit_seconds 為 5:空間查詢用完執行時間 |
ct.coordinate_sql_unsupported | 400:在 stored_filters 或 any_of 對座標欄使用 is_null、is_not_null、within、within_any 以外的運算子,或在 sort 沒有 distance_from key 時對座標欄使用一般的 sort_by 或 sort key |
Schema 形狀錯誤在私有路由是 422,在 public-read 查詢 body 是 400 {"detail": "invalid body: …"}。多數執行期拒絕使用帶 code 的物件 detail;回傳單純字串的拒絕(例如上列 400 的不相容與需求代碼)則把代碼放在 detail 文字。請依代碼處理,不要假設只有一種 envelope。