major performance improvements map

This commit is contained in:
Christian Beutel
2025-04-14 20:22:39 +02:00
parent a1296ebacd
commit 5390b9cf72
9 changed files with 165 additions and 17 deletions

View File

@@ -0,0 +1,30 @@
package migrations
import (
"os"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
client := meilisearch.New(os.Getenv("MEILI_URL"), meilisearch.WithAPIKey(os.Getenv("MEILI_MASTER_KEY")))
m.Register(func(app core.App) error {
searchableAttributes := []string{
"author_name",
"name",
"description",
"location",
"tags",
}
client.Index("trails").UpdateSearchableAttributes(&searchableAttributes)
return nil
}, func(app core.App) error {
// add down queries...
return nil
})
}

View File

@@ -1,15 +1,19 @@
package util package util
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"github.com/meilisearch/meilisearch-go" "github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/twpayne/go-gpx"
"github.com/twpayne/go-polyline"
) )
func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares bool) map[string]interface{} { func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record, includeShares bool) map[string]interface{} {
photos := r.GetStringSlice("photos") photos := r.GetStringSlice("photos")
thumbnail := "" thumbnail := ""
if len(photos) > 0 { if len(photos) > 0 {
@@ -27,6 +31,11 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
tags[i] = v.GetString("name") tags[i] = v.GetString("name")
} }
polyline, err := getPolyline(app, r)
if err != nil {
return nil
}
document := map[string]interface{}{ document := map[string]interface{}{
"id": r.Id, "id": r.Id,
"author": r.GetString("author"), "author": r.GetString("author"),
@@ -48,6 +57,7 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
"thumbnail": thumbnail, "thumbnail": thumbnail,
"gpx": r.GetString("gpx"), "gpx": r.GetString("gpx"),
"tags": tags, "tags": tags,
"polyline": polyline,
"_geo": map[string]float64{ "_geo": map[string]float64{
"lat": r.GetFloat("lat"), "lat": r.GetFloat("lat"),
"lng": r.GetFloat("lon"), "lng": r.GetFloat("lon"),
@@ -61,6 +71,44 @@ func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares
return document return document
} }
func getPolyline(app core.App, r *core.Record) (string, error) {
gpxPath := r.GetString("gpx")
if len(gpxPath) == 0 {
return "", nil
}
avatarKey := r.BaseFilesPath() + "/" + gpxPath
fsys, err := app.NewFilesystem()
if err != nil {
return "", err
}
defer fsys.Close()
gpxFile, err := fsys.GetFile(avatarKey)
if err != nil {
return "", err
}
defer gpxFile.Close()
content := new(bytes.Buffer)
_, err = io.Copy(content, gpxFile)
if err != nil {
return "", err
}
gpxData, err := gpx.Read(content)
if err != nil {
return "", err
}
coordinates := make([][]float64, 4)
for _, trk := range gpxData.Trk {
for _, seg := range trk.TrkSeg {
for _, pt := range seg.TrkPt {
coordinates = append(coordinates, []float64{pt.Lat, pt.Lon})
}
}
}
return string(polyline.EncodeCoords(coordinates)), nil
}
func documentFromListRecord(r *core.Record, includeShares bool) map[string]interface{} { func documentFromListRecord(r *core.Record, includeShares bool) map[string]interface{} {
document := map[string]interface{}{ document := map[string]interface{}{
"id": r.Id, "id": r.Id,
@@ -85,7 +133,7 @@ func IndexTrail(app core.App, r *core.Record, author *core.Record, client meilis
return fmt.Errorf("failed to expand: %v", errs) return fmt.Errorf("failed to expand: %v", errs)
} }
documents := []map[string]interface{}{documentFromTrailRecord(r, author, true)} documents := []map[string]interface{}{documentFromTrailRecord(app, r, author, true)}
if _, err := client.Index("trails").AddDocuments(documents); err != nil { if _, err := client.Index("trails").AddDocuments(documents); err != nil {
return err return err
@@ -100,7 +148,7 @@ func UpdateTrail(app core.App, r *core.Record, author *core.Record, client meili
return fmt.Errorf("failed to expand: %v", errs) return fmt.Errorf("failed to expand: %v", errs)
} }
documents := documentFromTrailRecord(r, author, false) documents := documentFromTrailRecord(app, r, author, false)
if _, err := client.Index("trails").UpdateDocuments(documents); err != nil { if _, err := client.Index("trails").UpdateDocuments(documents); err != nil {
return err return err

View File

@@ -12,6 +12,7 @@
createPopupFromTrail, createPopupFromTrail,
FontawesomeMarker, FontawesomeMarker,
} from "$lib/util/maplibre_util"; } from "$lib/util/maplibre_util";
import { polylineToGeoJSON } from "$lib/util/polyline_util";
import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control"; import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control"; import { FullscreenControl } from "$lib/vendor/maplibre-fullscreen/fullscreen-control";
import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule"; import MaplibreGraticule from "$lib/vendor/maplibre-graticule/maplibre-graticule";
@@ -182,8 +183,11 @@
} }
const r: GeoJSON[] = []; const r: GeoJSON[] = [];
console.time("start decode")
trails.forEach((t) => { trails.forEach((t) => {
if (t.expand?.gpx_data) { if (t.polyline) {
r.push(polylineToGeoJSON(t.polyline, 5));
} else if (t.expand?.gpx_data) {
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON); r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
} else if (t.lat !== null && t.lon !== null) { } else if (t.lat !== null && t.lon !== null) {
r.push({ r.push({
@@ -197,6 +201,8 @@
} as GeoJSON); } as GeoJSON);
} }
}); });
console.timeEnd("start decode")
return r; return r;
} }

View File

@@ -27,6 +27,7 @@ class Trail {
tags: string[]; tags: string[];
waypoints: string[]; waypoints: string[];
summit_logs: string[]; summit_logs: string[];
polyline?: string;
expand?: { expand?: {
tags?: Tag[] tags?: Tag[]
category?: Category; category?: Category;
@@ -165,6 +166,7 @@ interface TrailSearchResult {
created: number; created: number;
public: boolean; public: boolean;
thumbnail: string; thumbnail: string;
polyline?: string;
shares?: string[]; shares?: string[];
tags?: string[] tags?: string[]
gpx: string; gpx: string;
@@ -174,6 +176,29 @@ interface TrailSearchResult {
}; };
} }
export const defaultTrailSearchAttributes = [
"id",
"author",
"author_name",
"author_avatar",
"name",
"description",
"location",
"distance",
"elevation_gain",
"elevation_loss",
"duration",
"difficulty",
"category",
"completed",
"date",
"created",
"public",
"thumbnail",
"gpx",
"tags",
"_geo",]
export { Trail }; export { Trail };

View File

@@ -1,4 +1,5 @@
import { env } from "$env/dynamic/public"; import { env } from "$env/dynamic/public";
import { defaultTrailSearchAttributes } from "$lib/models/trail";
import { APIError } from "$lib/util/api_util"; import { APIError } from "$lib/util/api_util";
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch"; import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
@@ -97,6 +98,7 @@ export async function searchTrails(q: string, options: SearchParams): Promise<Hi
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
q, q,
attributesToRetrieve: defaultTrailSearchAttributes,
options options
}), }),
}); });
@@ -161,7 +163,7 @@ function getLocationDescription(address: Address) {
description = `${address.city}, ` + description description = `${address.city}, ` + description
} else if (address.town) { } else if (address.town) {
description = `${address.town}, ` + description description = `${address.town}, ` + description
}else if (address.hamlet) { } else if (address.hamlet) {
description = `${address.hamlet}, ` + description description = `${address.hamlet}, ` + description
} else if (address.village) { } else if (address.village) {
description = `${address.village}, ` + description description = `${address.village}, ` + description

View File

@@ -1,5 +1,5 @@
import type { SummitLog } from "$lib/models/summit_log"; import type { SummitLog } from "$lib/models/summit_log";
import { Trail, type TrailFilter, type TrailFilterValues, type TrailSearchResult } from "$lib/models/trail"; import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailFilterValues, type TrailSearchResult } from "$lib/models/trail";
import type { Waypoint } from "$lib/models/waypoint"; import type { Waypoint } from "$lib/models/waypoint";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { deepEqual } from "$lib/util/deep_util"; import { deepEqual } from "$lib/util/deep_util";
@@ -65,6 +65,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
q: filter.q, q: filter.q,
options: { options: {
filter: filterText, filter: filterText,
attributesToRetrieve: defaultTrailSearchAttributes,
sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`], sort: [`${filter.sort}:${filter.sortOrder == "+" ? "asc" : "desc"}`],
hitsPerPage: 12, hitsPerPage: 12,
page: page page: page
@@ -89,7 +90,7 @@ export async function trails_search_filter(filter: TrailFilter, page: number = 1
} }
export async function trails_search_bounding_box(northEast: M.LngLat, southWest: M.LngLat, filter?: TrailFilter, page: number = 1, loadGPX: boolean = true) { export async function trails_search_bounding_box(northEast: M.LngLat, southWest: M.LngLat, filter?: TrailFilter, page: number = 1, includePolyline: boolean = true) {
let filterText: string = ""; let filterText: string = "";
@@ -106,6 +107,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
`_geoBoundingBox([${northEast.lat}, ${northEast.lng}], [${southWest.lat}, ${southWest.lng}])`, `_geoBoundingBox([${northEast.lat}, ${northEast.lng}], [${southWest.lat}, ${southWest.lng}])`,
filterText filterText
], ],
attributesToRetrieve: [...defaultTrailSearchAttributes, ...(includePolyline ? ["polyline"] : [])],
hitsPerPage: 500, hitsPerPage: 500,
page: page page: page
} }
@@ -118,7 +120,7 @@ export async function trails_search_bounding_box(northEast: M.LngLat, southWest:
return { trails: [], ...result } return { trails: [], ...result }
} }
const resultTrails: Trail[] = await searchResultToTrailList(result.hits, loadGPX) const resultTrails: Trail[] = await searchResultToTrailList(result.hits)
trails = page > 1 ? trails.concat(resultTrails) : resultTrails trails = page > 1 ? trails.concat(resultTrails) : resultTrails
@@ -444,7 +446,7 @@ export async function fetchGPX(trail: { gpx?: string } & Record<string, any>, f:
return gpxData return gpxData
} }
async function searchResultToTrailList(hits: Hits<TrailSearchResult>, loadGPX: boolean = false): Promise<Trail[]> { async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Promise<Trail[]> {
const trails: Trail[] = [] const trails: Trail[] = []
for (const h of hits) { for (const h of hits) {
const t: Trail & RecordModel = { const t: Trail & RecordModel = {
@@ -472,6 +474,7 @@ async function searchResultToTrailList(hits: Hits<TrailSearchResult>, loadGPX: b
lon: h._geo.lng, lon: h._geo.lng,
location: h.location, location: h.location,
gpx: h.gpx, gpx: h.gpx,
polyline: h.polyline,
thumbnail: 0, thumbnail: 0,
expand: { expand: {
author: { author: {
@@ -489,10 +492,6 @@ async function searchResultToTrailList(hits: Hits<TrailSearchResult>, loadGPX: b
} }
} }
if (loadGPX) {
const gpxData: string = await fetchGPX(t);
t.expand!.gpx_data = gpxData;
}
trails.push(t) trails.push(t)
} }

View File

@@ -1,3 +1,7 @@
import type { GeoJSON } from "geojson";
import { bbox } from "./geojson_util";
function py2_round(value: number) { function py2_round(value: number) {
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1); return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
} }
@@ -77,4 +81,34 @@ export function encodePolyline(coordinates: number[][], precision: number = 6) {
} }
return output; return output;
};
function flipped(coords: number[][]) {
var flipped = [];
for (var i = 0; i < coords.length; i++) {
var coord = coords[i].slice();
flipped.push([coord[1], coord[0]]);
}
return flipped;
}
export function polylineToGeoJSON(str: string, precision: number = 6) {
var coords = decodePolyline(str, precision);
const geojson = {
type: "FeatureCollection",
features: [
{
properties: {},
type: "Feature",
geometry: {
type: "LineString",
coordinates: flipped(coords),
},
}
]
} as GeoJSON;
geojson.bbox = bbox(geojson)
return geojson
}; };

View File

@@ -8,6 +8,7 @@
import CategoryCard from "$lib/components/category_card.svelte"; import CategoryCard from "$lib/components/category_card.svelte";
import Scene from "$lib/components/scene.svelte"; import Scene from "$lib/components/scene.svelte";
import TrailCard from "$lib/components/trail/trail_card.svelte"; import TrailCard from "$lib/components/trail/trail_card.svelte";
import { defaultTrailSearchAttributes } from "$lib/models/trail.js";
import { categories } from "$lib/stores/category_store"; import { categories } from "$lib/stores/category_store";
import { import {
searchMulti, searchMulti,
@@ -30,6 +31,7 @@
queries: [ queries: [
{ {
indexUid: "trails", indexUid: "trails",
attributesToRetrieve: defaultTrailSearchAttributes,
q: q, q: q,
limit: 3, limit: 3,
}, },

View File

@@ -11,10 +11,11 @@
import TrailCard from "$lib/components/trail/trail_card.svelte"; import TrailCard from "$lib/components/trail/trail_card.svelte";
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte"; import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
import type { Settings } from "$lib/models/settings"; import type { Settings } from "$lib/models/settings";
import type { import {
Trail, defaultTrailSearchAttributes,
TrailBoundingBox, type Trail,
TrailFilter, type TrailBoundingBox,
type TrailFilter,
} from "$lib/models/trail"; } from "$lib/models/trail";
import { categories } from "$lib/stores/category_store"; import { categories } from "$lib/stores/category_store";
import { import {
@@ -67,6 +68,7 @@
queries: [ queries: [
{ {
indexUid: "trails", indexUid: "trails",
attributesToRetrieve: defaultTrailSearchAttributes,
q: q, q: q,
limit: 3, limit: 3,
}, },