Skip to content

Commit 2c319aa

Browse files
committed
now with fuse search
1 parent a0cbae2 commit 2c319aa

File tree

7 files changed

+152
-55
lines changed

7 files changed

+152
-55
lines changed

package-lock.json

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"vite": "^6.2.0"
1515
},
1616
"dependencies": {
17+
"fuse.js": "^7.1.0",
1718
"latlon-geohash": "^2.0.0",
1819
"leaflet": "^1.9.4",
1920
"lokijs": "^1.5.12",

src/App.svelte

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
// State for selected marker
1616
let selectedMarker = null;
1717
let mapZoom = 1;
18+
let mapCenter = null;
1819
1920
// Add a target location state
2021
let targetMapLocation = {
2122
lat: 51.508056,
2223
lon: -0.076111,
23-
zoom: 1,
24+
zoom: 3,
2425
};
2526
2627
// Search state
@@ -73,7 +74,6 @@
7374
}
7475
7576
function addMarkerClasses(entries, hashlevel) {
76-
console.log("selectedMarker", selectedMarker);
7777
if (hashlevel > 7.5) {
7878
for (const entry of entries) {
7979
entry.displayClass = "full";
@@ -99,7 +99,6 @@
9999
}
100100
for (const entry of entries) {
101101
if (selectedMarker && entry.id == selectedMarker.id) {
102-
console.log({ entry });
103102
entry.displayClass = "selected";
104103
}
105104
}
@@ -108,14 +107,13 @@
108107
109108
async function handleBoundsChange(event) {
110109
const center = event.detail.center;
111-
mapZoom = event.detail.zoom;
112-
113-
// Update targetMapLocation with the new center and zoom
114-
const urlTargetMapLocation = {
110+
mapCenter = {
115111
lat: center.lat,
116112
lon: center.lng,
117-
zoom: mapZoom,
118113
};
114+
mapZoom = event.detail.zoom;
115+
// Update targetMapLocation with the new center and zoom
116+
const urlTargetMapLocation = { ...mapCenter, zoom: mapZoom };
119117
updateURLParams(urlTargetMapLocation, selectedMarker);
120118
121119
const bounds = {
@@ -133,7 +131,6 @@
133131
scoreField: "page_len",
134132
});
135133
if (uniqueEntries.length > 400) {
136-
console.log("uniqueEntries", uniqueEntries.length, "so reducing");
137134
uniqueEntries.sort((a, b) => b.page_len - a.page_len);
138135
uniqueEntries = uniqueEntries.slice(0, 400);
139136
}
@@ -143,7 +140,6 @@
143140
) {
144141
uniqueEntries.push(selectedMarker);
145142
}
146-
console.log("nMarkers", uniqueEntries.length);
147143
148144
addMarkerClasses(uniqueEntries, hashlevel);
149145
markers = uniqueEntries;
@@ -189,11 +185,7 @@
189185
on:markerclick={handleMarkerClick}
190186
/>
191187
<div class="search-wrapper">
192-
<SearchBar
193-
entries={allEntriesInRegion}
194-
bind:searchText
195-
on:select={handleSearchSelect}
196-
/>
188+
<SearchBar on:select={handleSearchSelect} />
197189
</div>
198190
</div>
199191
</div>

src/lib/Map.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
map = L.map(mapElement, {
8484
zoomControl: false, // Disable default zoom control
8585
worldCopyJump: true,
86-
}).setView([0, 0], 1);
86+
}).setView([0, 0], 3);
8787
flyTo(targetLocation);
8888
8989
// Add tile layer (OpenStreetMap)

src/lib/SearchBar.svelte

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,58 @@
11
<script>
2-
import { createEventDispatcher } from "svelte";
3-
4-
export let entries = [];
5-
export let searchText = "";
2+
import { createEventDispatcher, onMount, onDestroy } from "svelte";
3+
import { getEntriesfromText } from "./geodata";
4+
export let searchQuery = "";
65
76
const dispatch = createEventDispatcher();
87
9-
let filteredEntries = [];
8+
let searchResults = [];
109
let isActive = false;
11-
10+
let isLoading = false;
11+
let debounceTimer = null;
12+
13+
// Debounced search function
14+
function debouncedSearch() {
15+
isLoading = true;
16+
17+
// Clear any existing timer
18+
if (debounceTimer) clearTimeout(debounceTimer);
19+
20+
// Set a new timer
21+
debounceTimer = setTimeout(async () => {
22+
if (searchQuery && searchQuery.length > 1) {
23+
searchResults = await getEntriesfromText(searchQuery);
24+
isActive = true;
25+
} else {
26+
searchResults = [];
27+
isActive = false;
28+
}
29+
isLoading = false;
30+
}, 300); // 0.3 seconds debounce
31+
}
32+
33+
// Search text reactive statement
1234
$: {
13-
if (searchText && searchText.length > 1) {
14-
const searchLower = searchText.toLowerCase();
15-
filteredEntries = entries
16-
.filter((entry) => entry.page_title.toLowerCase().includes(searchLower))
17-
.sort((a, b) => {
18-
// Sort by whether the title starts with the search text, then by length
19-
const aStarts = a.page_title.toLowerCase().startsWith(searchLower);
20-
const bStarts = b.page_title.toLowerCase().startsWith(searchLower);
21-
22-
if (aStarts && !bStarts) return -1;
23-
if (!aStarts && bStarts) return 1;
24-
25-
return a.page_title.length - b.page_title.length;
26-
})
27-
.slice(0, 10); // Limit to 10 results
28-
isActive = true;
35+
if (searchQuery && searchQuery.length > 1) {
36+
isLoading = true;
37+
searchResults = [];
38+
debouncedSearch();
2939
} else {
30-
filteredEntries = [];
40+
searchResults = [];
3141
isActive = false;
42+
isLoading = false;
43+
// Clear any pending search
44+
if (debounceTimer) clearTimeout(debounceTimer);
3245
}
3346
}
3447
48+
onDestroy(() => {
49+
// Clean up any pending timers when component is destroyed
50+
if (debounceTimer) clearTimeout(debounceTimer);
51+
});
52+
3553
function handleSelect(entry) {
3654
dispatch("select", entry);
37-
searchText = "";
55+
searchQuery = "";
3856
isActive = false;
3957
}
4058
@@ -46,7 +64,7 @@
4664
}
4765
4866
function handleFocus() {
49-
if (searchText && searchText.length > 1) {
67+
if (searchQuery && searchQuery.length > 1) {
5068
isActive = true;
5169
}
5270
}
@@ -56,17 +74,17 @@
5674
<div class="search-input-wrapper">
5775
<input
5876
type="text"
59-
placeholder="Search locations..."
60-
bind:value={searchText}
77+
placeholder="Search locations (regions already visited only)"
78+
bind:value={searchQuery}
6179
on:focus={handleFocus}
6280
on:blur={handleBlur}
6381
class="search-input"
6482
/>
65-
{#if searchText}
83+
{#if searchQuery}
6684
<button
6785
class="clear-button"
6886
on:click={() => {
69-
searchText = "";
87+
searchQuery = "";
7088
isActive = false;
7189
}}
7290
>
@@ -78,9 +96,9 @@
7896
</div>
7997
</div>
8098

81-
{#if isActive && filteredEntries.length > 0}
99+
{#if isActive && searchResults.length > 0}
82100
<div class="suggestions" role="listbox">
83-
{#each filteredEntries as entry}
101+
{#each searchResults as entry}
84102
<div
85103
class="suggestion-item"
86104
on:mousedown={() => handleSelect(entry)}
@@ -96,10 +114,14 @@
96114
</div>
97115
{/each}
98116
</div>
99-
{:else if isActive && searchText.length > 1}
117+
{:else if isActive && searchQuery.length > 1}
100118
<div class="suggestions" role="listbox">
101119
<div class="no-results" role="presentation">
102-
No matching locations found
120+
{#if isLoading}
121+
Searching...
122+
{:else}
123+
No matching locations found
124+
{/if}
103125
</div>
104126
</div>
105127
{/if}

src/lib/geodata.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,31 @@ export async function getGeoEntriesInBounds({minLat, maxLat, minLon, maxLon}) {
3737
return await queryPromise;
3838
}
3939

40+
export async function getEntriesfromText(searchQuery) {
41+
// Make sure worker is initialized
42+
if (!window.geodataWorker) {
43+
initWorker();
44+
}
45+
46+
// Create a unique request ID
47+
const requestId = `query_${Date.now()}_${Math.random()}`;
48+
49+
// Create a promise that will be resolved when the worker returns results
50+
const queryPromise = new Promise((resolve, reject) => {
51+
window.geodataWorkerPromises[requestId] = { resolve, reject };
52+
});
53+
54+
// Send query to worker
55+
window.geodataWorker.postMessage({
56+
type: 'textSearch',
57+
requestId,
58+
searchQuery
59+
});
60+
61+
// Wait for worker to return results
62+
return await queryPromise;
63+
}
64+
4065
// Initialize the worker once
4166
function initWorker() {
4267
window.geodataWorker = new Worker(new URL('./geodataWorker.js', import.meta.url), { type: 'module' });

src/lib/geodataWorker.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pako from 'pako';
77
import ngeohash from 'ngeohash';
88
import Loki from 'lokijs';
9+
import Fuse from 'fuse.js';
910

1011
// Initialize the database in the worker
1112
const db = new Loki('geodata.db', {
@@ -34,6 +35,29 @@ const tinyGeoCollection = db.addCollection('tinygeodata', {
3435
// Keep track of ingested files
3536
const ingestedFiles = [];
3637

38+
// Initialize Fuse instance for search
39+
let fuseIndex = null
40+
let searchableData = [];
41+
42+
// Function to build or rebuild the search index
43+
function buildSearchIndex() {
44+
// Collect all items from both collections for searching
45+
searchableData = geoCollection.chain().data();
46+
47+
// Configure Fuse options for searching
48+
const fuseOptions = {
49+
keys: ['page_title', 'name', 'category'],
50+
threshold: 0.3,
51+
ignoreLocation: false,
52+
includeScore: true,
53+
shouldSort: true,
54+
minMatchCharLength: 2
55+
};
56+
57+
// Create new Fuse index
58+
fuseIndex = new Fuse(searchableData, fuseOptions);
59+
}
60+
3761
// Function to process a CSV file from gzipped data
3862
async function loadCsvGzFile(url) {
3963
const response = await fetch(url);
@@ -80,10 +104,30 @@ function queryGeoTable(table, minLat, maxLat, minLon, maxLon) {
80104
// Set up event listener for messages from the main thread
81105
self.addEventListener('message', async (event) => {
82106
try {
83-
const { type, bounds, basePath } = event.data;
107+
const { type, bounds, basePath, searchQuery, requestId } = event.data;
84108

85-
if (type === 'textSearch') {
86-
console.log("bla")
109+
if (type === 'textSearch') {
110+
// Perform search if we have an index
111+
if (fuseIndex) {
112+
113+
const results = fuseIndex.search(searchQuery, {
114+
threshold: 0.2,
115+
minMatchCharLength: 3,
116+
distance: 10
117+
})
118+
self.postMessage({
119+
type: 'queryResults',
120+
requestId,
121+
results: results.map(result => result.item)
122+
});
123+
} else {
124+
// Return empty result if no index available
125+
self.postMessage({
126+
type: 'queryResults',
127+
requestId,
128+
results: []
129+
});
130+
}
87131
}
88132
else if (type === 'queryBounds') {
89133
// Handle bounds query
@@ -109,7 +153,6 @@ self.addEventListener('message', async (event) => {
109153
table.insert(rows);
110154
ingestedFiles.push(url);
111155
}
112-
113156
// Query the data
114157
const results = queryGeoTable(table, minLat, maxLat, minLon, maxLon);
115158

@@ -119,6 +162,10 @@ self.addEventListener('message', async (event) => {
119162
requestId: event.data.requestId,
120163
results
121164
});
165+
166+
if (needDownload.length > 0) {
167+
buildSearchIndex();
168+
}
122169
}
123170
} catch (error) {
124171
self.postMessage({

0 commit comments

Comments
 (0)