PostGIS va vektorlar

Qo‘lda yozilgan xaritada chiziladigan va tahrirlanadigan sakkiz xil geometriya turi, metr bilan gradusni farqlaydigan fazoviy filtrlar va pgvector orqali o‘xshashlik bo‘yicha qidiruv.

Mavjud v0.2.0

FastFort PostGIS va pgvector ustunlarini spec qatlamida GeoAlchemy2 yoki pgvector’ni import qilmasdan aniqlaydi. Fazoviy ma’lumoti bo‘lmagan loyiha fazoviy qo‘llab-quvvatlashning mavjudligi uchun hech narsa to‘lamaydi; fazoviy ma’lumoti bor loyihaga esa bazaning kengaytmasidan boshqa hech qanday tizim paketi kerak emas.

Geometriya ustunlari

from geoalchemy2 import Geography, Geometry, WKBElement


class Hub(Base):
    location: Mapped[WKBElement | None] = mapped_column(
        Geography(geometry_type="POINT", srid=4326), default=None
    )
    catchment: Mapped[WKBElement | None] = mapped_column(
        Geometry(geometry_type="POLYGON", srid=4326), default=None
    )

Barcha sakkiz tur chiziladi va tahrirlanadi: POINT, LINESTRING, POLYGON (teshiklari bilan birga), MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, GEOMETRYCOLLECTION va turi ko‘rsatilmagan GEOMETRY.

Har bir maydon ostidagi format bo‘yicha maslahat ustunning o‘z kind’idan tanlanadi — «kenglik va uzunlik» POINT ustida aynan to‘g‘ri, POLYGON ustida esa chalg‘ituvchi bo‘ladi.

Xaritani yoqish

Standart holatda o‘chirilgan, va bu e’tibordan chetda qolgan narsa emas, balki qaror: uni yoqish admin boshqa birovning serveridan rasm yuklab olishi degani. Bu server qaysi qatorlar ko‘rilayotgani va ular taxminan qayerda ekanini biladi, aksariyat tayl xizmatlarida esa bu haqda shartlar bor. URL ko‘rsatish — loyihangizning ularni o‘qib chiqqani haqidagi bayonoti.

FastFortSettings(
    ui={
        "map_tile_url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
        "map_attribution": "© OpenStreetMap contributors",
        "map_center": "41.3111, 69.2797",
    },
)

Bu server — adminning CSP’dagi img-src kengaytiriladigan yagona manba, shu sababli bu sozlama, shablon qo‘sha oladigan narsa emas.

map_tile_url bo‘lmasa, maydon doim bo‘lib kelgan oddiy matn kiritish maydonicha qoladi. Hech narsa buzilmaydi — orqasida shunchaki rasm yo‘q.

Bu qanchalik chuqurlashadi

ui={"map_max_zoom": 19}    # the default

So‘raladigan eng chuqur daraja — bu vidjetning emas, tayl manbaining xususiyati. Serverda mavjud bo‘lgandan chuqurroq so‘rov 404 qaytaradi, muvaffaqiyatsiz tayl esa bo‘sh kvadrat bo‘ladi.

Ko‘rinish yana ikki daraja chuqurroq davom etadi va qo‘lidagi so‘nggi taylni masshtablaydi — har qanday xarita ilovasi o‘z tasvirlaridan tashqarida qanday ishlasa, xuddi shunday: javob berishni to‘xtatgan «+» tugmasi vidjet buzilganday tuyuladi, xaritaning tafsilotlari tugaganday emas.

Xarita nima ekani

Leaflet emas, qo‘lda yozilgan Web Mercator. 256 pikselli taylar transform orqali joylashtiriladi, har bir masshtab darajasi uchun bitta qatlam bo‘lib, shu tufayli masshtablash bir zumda sodir bo‘ladi va yangi taylar yuklanmaguncha oldingi daraja pastda turaveradi.

U yonidagi matn maydonini almashtirmaydi, balki unga yozadi. JavaScript o‘chirilganda ham maydon avvalgidek WKT, EWKT yoki GeoJSON joylash mumkin bo‘lgan qutichaligicha qoladi, forma esa baribir yuboriladi — bu boshqa har qanday boshqaruv elementi amal qiladigan qoidaning aynan o‘zi.

Faqat noto‘g‘ri ketgandan keyingina ravshan bo‘ladigan ikkita tafsilot:

  • Poligonning yopuvchi uchi birinchi uchiga bog‘langan. Birini surib, ikkinchisini surmasangiz, saqlashda halqa yorilib ochiladi.
  • Xarita — uslub jadvalida mantiqiy emas, jismoniy yo‘nalishlarda yozilgan roppa-rosa ikkita joydan biri, chunki bu skript tomonidan jismoniy piksellarda joylashtiriladigan koordinatalar maydoni. (Ikkinchisi — checkbox belgisi: belgi qaysi tilda bo‘lmasin, belgiligicha qoladi.)

Geography — bu geometry emas

Ikkalasi ham bir xil maydon turiga kiradi va bir xil boshqaruv elementini chizadi. Masofa geography uchun metr, geometry uchun esa SRID birliklari — 4326 uchun gradus — degani.

/admin/logistics.vehicle/?last_seen__dwithin=41.3111,69.2797,5000

5000 metr, chunki Vehicle.last_seen — Geography. Xuddi shu filtr Geometry ustunida 5000 ni gradus sifatida o‘qigan bo‘lardi, bu esa sayyoraning yarmiga yaqin radius degani.

GeometrySpec.geography aynan shu ikkisi aralashib ketmasligi uchun mavjud, so‘rov esa kerak bo‘lgan joyda turni o‘zgartiradi.

Fazoviy filtrlar

?catchment__within=POLYGON((69.2 41.3, 69.3 41.3, 69.3 41.2, 69.2 41.3))
?catchment__intersects=POINT(69.28 41.31)
?catchment__contains=POINT(69.28 41.31)
?catchment__bbox=69.1,41.2,69.4,41.4
?last_seen__dwithin=41.3111,69.2797,5000

Shuningdek overlaps, touches va crosses.

Qiymat forma foydalanadigan xuddi shu parser bilan tahlil qilinadi, shu sababli maydon qabul qiladigan har narsani filtr ham qabul qiladi.

Ro‘yxat katagida

Geometriya qisqacha ko‘rinishda chiziladi — Polygon · 14 points, Point · 41.31, 69.28 — sahifani ochgan odamga buzilgan ma’lumotdek ko‘rinadigan WKB o‘n oltilik blob emas.

Eksport atayin boshqacha. Ro‘yxat katagi qisqacha ko‘rsatadi; faylni odam kabi kamida shunchalik dastur ham o‘qiydi, import bergan va’da esa — bu tizim yozgan faylni o‘zi qaytarib o‘qiy olishi. Shu sababli geometriya o‘zining forma boshqaruv elementi ko‘rsatadigan xuddi shu matn sifatida eksport qilinadi: nuqta — lat, lng sifatida, qolgan hammasi — EWKT sifatida.

Vektorli qidiruv

from pgvector.sqlalchemy import Vector


class Product(Base):
    embedding: Mapped[object | None] = mapped_column(Vector(3), default=None)

Adminda faqat o‘qish uchun. Embeddingni hech kim qo‘lda kiritmaydi; ustunni foydali qiladigan narsa — so‘rovning o‘zi:

?embedding__near=[0.4,0.1,0.9]&embedding__k=5

Besh eng yaqini, saralangan holda. Bu tartib sahifadan so‘ralgan boshqa har qanday narsadan ustun turadi — alifbo bo‘yicha emas, «eng yaqini, tenglik bo‘lsa nom bo‘yicha» tartibida, chunki natijalari keyin alifbo bo‘yicha qayta saralanadigan o‘xshashlik bo‘yicha qidiruv — bu endi o‘xshashlik bo‘yicha qidiruv emas.

?embedding__near=[...]&embedding__metric=cosine    # default
?embedding__near=[...]&embedding__metric=l2
?embedding__near=[...]&embedding__metric=l1
?embedding__near=[...]&embedding__metric=inner
?embedding__near=[...]&embedding__within=0.35      # a distance bound

VectorSpec o‘lchamlar sonini va turini (vector, halfvec, sparsevec, bit) o‘zida saqlaydi, shu sababli noto‘g‘ri uzunlikdagi qiymat baza xatosi emas, validatsiya xatosi bo‘ladi.

Kengaytmalarni olish

FastFort hech qachon DDL ishga tushirmaydi. Kengaytmalarni yoqish loyihangizning vazifasi, va bu ataylab shunday: baza kengaytmalarini sezdirmay o‘rnatadigan admin freymvorkini hech kim tekshira olmaydi.

EXTENSIONS = ("postgis", "hstore", "citext", "vector", "pg_trgm", "btree_gist")


async def create_schema() -> None:
    async with engine.begin() as connection:
        if connection.dialect.name == "postgresql":
            for extension in EXTENSIONS:
                await connection.execute(
                    sa.text(f"CREATE EXTENSION IF NOT EXISTS {extension}")
                )
        await connection.run_sync(Base.metadata.create_all)

postgis, hstore, citext va vector — har biri o‘z turini qo‘shadi, shu sababli ularning birini ishlatgan CREATE TABLE birinchi yozuvda emas, DDL bosqichida muvaffaqiyatsiz tugaydi.