Skip to content

Commit 2268a43

Browse files
committed
embedding fix v2
1 parent cc06496 commit 2268a43

8 files changed

Lines changed: 123 additions & 27 deletions

File tree

‎README.md‎

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ The dashboard allows:
4242
- pausing and resuming existing jobs,
4343
- changing **global queue limit** across all jobs,
4444
- changing **per-job request rate** live from each job card,
45-
- starting, pausing, and resuming **embedding jobs** with configurable embed speed and max-page limit,
45+
- starting and pausing a single **embedding engine** with configurable embed speed and max-page limit,
46+
- clearing all stored embeddings with confirmation when a full reset is needed,
4647
- inspecting detailed job state (frontier preview, counters, recent events),
4748
- viewing global metrics (processed/discovered/duplicates/failed, queue pressure, workers),
4849
- viewing embedding progress metrics (embedded/remaining/failed and `% embedded`).
@@ -57,7 +58,7 @@ The dashboard allows:
5758
- `job_visited` and `job_frontier` for resumable crawl state,
5859
- `job_events` for job timeline details,
5960
- `page_embeddings` for semantic vectors,
60-
- `embedding_jobs` for resumable embedding job state.
61+
- semantic engine runtime state in backend memory, with progress derived from DB counts.
6162

6263
## API Endpoints
6364

@@ -96,23 +97,20 @@ The dashboard allows:
9697

9798
### Embeddings
9899

99-
- `POST /embeddings/jobs/start`
100-
Starts a manual embedding job with configurable speed and max-page scope.
100+
- `GET /embeddings/status`
101+
Returns current embedding engine state and progress.
101102

102-
- `GET /embeddings/jobs`
103-
Lists embedding jobs and progress.
103+
- `POST /embeddings/start`
104+
Starts (or continues) embedding missing pages with configurable speed and max-page scope.
104105

105-
- `GET /embeddings/jobs/{job_id}`
106-
Returns full embedding job status and counters.
106+
- `POST /embeddings/pause`
107+
Pauses the current embedding engine run.
107108

108-
- `POST /embeddings/jobs/{job_id}/pause`
109-
Pauses a running embedding job.
109+
- `POST /embeddings/rate-limit`
110+
Updates embedding speed (`pages/s`) for the engine.
110111

111-
- `POST /embeddings/jobs/{job_id}/resume`
112-
Resumes a paused embedding job.
113-
114-
- `POST /embeddings/jobs/{job_id}/rate-limit`
115-
Updates embedding speed (`pages/s`) for a job.
112+
- `POST /embeddings/clear`
113+
Deletes all stored embeddings so semantic indexing can be rebuilt from zero.
116114

117115
## Backpressure and Load Management
118116

‎backend/app.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ async def update_embedding_engine_rate_limit(request: EmbeddingRateLimitRequest)
211211
return await _serialize_embedding_status()
212212

213213

214+
@app.post("/embeddings/clear", response_model=EmbeddingStatusResponse)
215+
async def clear_embeddings() -> EmbeddingStatusResponse:
216+
await semantic_index_service.clear_embeddings()
217+
return await _serialize_embedding_status()
218+
219+
214220
def _serialize_job(job_id: str) -> JobDetailResponse:
215221
ctx = crawler_service.get_job_context(job_id)
216222
if ctx is None:

‎backend/semantic_index.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
from datetime import datetime
99
from typing import Dict, List, Tuple
1010

11-
from .storage import count_pages, count_page_embeddings, load_embedding_targets, load_page_embeddings, save_page_embedding
11+
from .storage import (
12+
count_page_embeddings,
13+
count_pages,
14+
delete_page_embeddings,
15+
load_embedding_targets,
16+
load_page_embeddings,
17+
save_page_embedding,
18+
)
1219

1320
logger = logging.getLogger(__name__)
1421

@@ -94,6 +101,20 @@ async def update_rate_limit(self, rate_limit_per_sec: float) -> EmbeddingEngineS
94101
self._updated_at = datetime.utcnow()
95102
return await self.get_engine_status()
96103

104+
async def clear_embeddings(self) -> EmbeddingEngineStatus:
105+
if self._task and not self._task.done():
106+
self._task.cancel()
107+
await asyncio.gather(self._task, return_exceptions=True)
108+
delete_page_embeddings(DEFAULT_EMBEDDING_MODEL)
109+
self._embeddings.clear()
110+
self._meta.clear()
111+
self._failed_pages = 0
112+
self._error_message = None
113+
self._status = "idle"
114+
self._updated_at = datetime.utcnow()
115+
self._task = None
116+
return await self.get_engine_status()
117+
97118
async def search(self, query: str, limit: int | None = None) -> List[Tuple[str, str, int, float, str]]:
98119
query = query.strip()
99120
if not query:

‎backend/storage.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,3 +585,18 @@ def count_page_embeddings(model_name: str) -> int:
585585
finally:
586586
conn.close()
587587

588+
589+
def delete_page_embeddings(model_name: str | None = None) -> int:
590+
conn = _connect()
591+
try:
592+
cur = conn.cursor()
593+
if model_name:
594+
cur.execute("DELETE FROM page_embeddings WHERE model_name = ?", (model_name,))
595+
else:
596+
cur.execute("DELETE FROM page_embeddings")
597+
deleted = int(cur.rowcount if cur.rowcount is not None else 0)
598+
conn.commit()
599+
return deleted
600+
finally:
601+
conn.close()
602+

‎frontend/src/App.tsx‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,12 @@ export const App: React.FC = () => {
162162
fetch("/embeddings/status")
163163
.then((res) => res.json())
164164
.then((data: EmbeddingStatus) => {
165-
setEmbeddingStatus(data);
166-
setEmbeddingRateLimit(String(data.rate_limit_per_sec));
165+
setEmbeddingStatus((prev) => {
166+
if (prev === null) {
167+
setEmbeddingRateLimit(String(data.rate_limit_per_sec));
168+
}
169+
return data;
170+
});
167171
})
168172
.catch(() => {
169173
/* ignore */
@@ -285,6 +289,7 @@ export const App: React.FC = () => {
285289
}
286290
const status: EmbeddingStatus = await res.json();
287291
setEmbeddingStatus(status);
292+
setEmbeddingRateLimit(String(status.rate_limit_per_sec));
288293
} catch (e: any) {
289294
setError(e.message ?? String(e));
290295
} finally {
@@ -303,6 +308,7 @@ export const App: React.FC = () => {
303308
}
304309
const status: EmbeddingStatus = await res.json();
305310
setEmbeddingStatus(status);
311+
setEmbeddingRateLimit(String(status.rate_limit_per_sec));
306312
} catch (e: any) {
307313
setError(e.message ?? String(e));
308314
} finally {
@@ -325,6 +331,31 @@ export const App: React.FC = () => {
325331
}
326332
const status: EmbeddingStatus = await res.json();
327333
setEmbeddingStatus(status);
334+
setEmbeddingRateLimit(String(status.rate_limit_per_sec));
335+
} catch (e: any) {
336+
setError(e.message ?? String(e));
337+
} finally {
338+
setIsMutatingEmbeddingJob(false);
339+
}
340+
};
341+
342+
const clearAllEmbeddings = async () => {
343+
const confirmed = window.confirm(
344+
"Are you sure you want to delete all embeddings? This cannot be undone and semantic search will be empty until you embed again."
345+
);
346+
if (!confirmed) {
347+
return;
348+
}
349+
setError(null);
350+
try {
351+
setIsMutatingEmbeddingJob(true);
352+
const res = await fetch("/embeddings/clear", { method: "POST" });
353+
if (!res.ok) {
354+
const data = await res.json();
355+
throw new Error(data.detail ?? "Failed to clear embeddings");
356+
}
357+
const status: EmbeddingStatus = await res.json();
358+
setEmbeddingStatus(status);
328359
} catch (e: any) {
329360
setError(e.message ?? String(e));
330361
} finally {
@@ -481,6 +512,9 @@ export const App: React.FC = () => {
481512
<button className="button-secondary" onClick={updateEmbeddingRateLimit} disabled={isMutatingEmbeddingJob}>
482513
Update Speed
483514
</button>
515+
<button className="button-secondary" onClick={clearAllEmbeddings} disabled={isMutatingEmbeddingJob}>
516+
Delete All Embeddings
517+
</button>
484518
</div>
485519
<div className="metrics-grid">
486520
<div className="metric">
@@ -492,6 +526,12 @@ export const App: React.FC = () => {
492526
<span className="value">
493527
{embeddingStatus ? `${embeddingStatus.progress_percent.toFixed(1)}% of your sites are embedded` : "Loading..."}
494528
</span>
529+
<div className="progress-track" aria-label="Embedding progress">
530+
<div
531+
className="progress-fill"
532+
style={{ width: `${Math.min(100, Math.max(0, embeddingStatus?.progress_percent ?? 0))}%` }}
533+
/>
534+
</div>
495535
</div>
496536
<div className="metric">
497537
<span className="label">Embedded pages</span>

‎frontend/src/styles.css‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,21 @@ button:disabled {
203203
margin-top: 0.15rem;
204204
}
205205

206+
.progress-track {
207+
margin-top: 0.45rem;
208+
width: 100%;
209+
height: 0.45rem;
210+
border-radius: 999px;
211+
background: rgba(148, 163, 184, 0.2);
212+
overflow: hidden;
213+
}
214+
215+
.progress-fill {
216+
height: 100%;
217+
background: linear-gradient(90deg, #6366f1, #a855f7);
218+
transition: width 0.25s ease;
219+
}
220+
206221
.badge {
207222
display: inline-flex;
208223
align-items: center;

‎product_prd.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Building search engines is usually out of reach for a single engineer, but AI-as
1212
- **Live behavior**: allow search while indexing is running so new pages appear in results as soon as they are processed.
1313
- **Visibility**: provide a React dashboard that exposes crawl progress, queue depth, backpressure state, and job list.
1414
- **Local persistence**: store a lightweight snapshot of pages and jobs on disk so that search can resume after a restart.
15-
- **Embedding operations**: provide a separate UI mode to start/pause/resume embedding jobs with speed and max-page controls.
15+
- **Embedding operations**: provide a separate UI mode to control a single embedding engine (start/pause, speed, max-page scope) and reset embeddings when needed.
1616
- **AI-friendly design**: keep the crawler and indexer logic explicit and understandable, avoiding “black box” scraping libraries.
1717

1818
#### Non-goals
@@ -47,11 +47,12 @@ Building search engines is usually out of reach for a single engineer, but AI-as
4747
- Reads directly from the current in-memory index so queries reflect pages as soon as they are indexed.
4848
- `GET /search/semantic?query=...` returns semantic matches from persisted page embeddings with similarity scores.
4949

50-
- **Embedding job endpoints**
51-
- `POST /embeddings/jobs/start` starts a manual embedding run with `{ rate_limit_per_sec, max_pages }`.
52-
- `POST /embeddings/jobs/{job_id}/pause|resume` controls active embedding jobs.
53-
- `POST /embeddings/jobs/{job_id}/rate-limit` updates embedding speed during runtime.
54-
- `GET /embeddings/jobs` and `GET /embeddings/jobs/{job_id}` expose embedding progress and status.
50+
- **Embedding engine endpoints**
51+
- `GET /embeddings/status` returns embedding progress and current engine state.
52+
- `POST /embeddings/start` starts (or continues) embedding missing pages with `{ rate_limit_per_sec, max_pages }`.
53+
- `POST /embeddings/pause` pauses active embedding work.
54+
- `POST /embeddings/rate-limit` updates embedding speed during runtime.
55+
- `POST /embeddings/clear` deletes all stored embeddings after explicit user confirmation in UI.
5556

5657
- **Status/metrics endpoints**
5758
- `GET /jobs/{job_id}` returns job metadata and per-job statistics (status, processed URLs, queue depth estimate, backpressure state).
@@ -60,13 +61,13 @@ Building search engines is usually out of reach for a single engineer, but AI-as
6061
- **React UI**
6162
- **Index control panel**: origin+depth form, “Start Indexing” button, and active job id display.
6263
- **Search panel**: query input with side-by-side classical and semantic result tables.
63-
- **Embeddings panel**: manual embedding controls (start/pause/resume, speed, max-page limit) and live progress cards.
64+
- **Embeddings panel**: manual embedding controls (start/pause, speed, max-page limit, clear-all reset) and live progress cards.
6465
- **System dashboard**: live metrics cards and jobs table, auto-refreshing every few seconds.
6566

6667
- **Persistence**
6768
- Store pages in a local SQLite database (via Python `sqlite3` stdlib) with columns `(url, origin_url, depth, title, body_snippet)`.
6869
- Store job metadata `(id, origin_url, max_depth, created_at, status)` in the same DB.
69-
- Store semantic vectors in `page_embeddings` and embedding-job metadata in `embedding_jobs`.
70+
- Store semantic vectors in `page_embeddings`; progress is derived from `pages` vs `page_embeddings`.
7071
- On startup, rebuild the in-memory index from stored pages and repopulate the job list from stored jobs.
7172

7273
#### Constraints

‎recommendation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### Production deployment recommendations
22

3-
To harden this crawler and searcher for a real production environment, the first step would be to separate concerns into independent services: a crawl scheduler, a fleet of crawler workers, an indexing pipeline, a search API, and a UI/API gateway. The current single-process Python implementation can evolve into a **worker template**: wrap the crawler loop into a process that pulls crawl tasks from a durable queue (for example, Kafka, RabbitMQ, or a managed cloud queue), and writes fetched pages to a durable storage and indexing service. Since the project now supports both classical and semantic retrieval, the next production step is a hybrid search layer: keep lexical ranking in a dedicated search engine (such as OpenSearch/Elasticsearch or a managed search service) and move vector storage/retrieval to a scalable vector-capable backend. For persistence, replace the local SQLite database with a replicated database (PostgreSQL or similar) to hold crawl jobs, page metadata, embedding-job state, and operational rate-limit state, and back it with periodic backups and observability.
3+
To harden this crawler and searcher for a real production environment, the first step would be to separate concerns into independent services: a crawl scheduler, a fleet of crawler workers, an indexing pipeline, a search API, and a UI/API gateway. The current single-process Python implementation can evolve into a **worker template**: wrap the crawler loop into a process that pulls crawl tasks from a durable queue (for example, Kafka, RabbitMQ, or a managed cloud queue), and writes fetched pages to a durable storage and indexing service. Since the project now supports both classical and semantic retrieval, the next production step is a hybrid search layer: keep lexical ranking in a dedicated search engine (such as OpenSearch/Elasticsearch or a managed search service) and move vector storage/retrieval to a scalable vector-capable backend. For persistence, replace the local SQLite database with a replicated database (PostgreSQL or similar) to hold crawl jobs, page metadata, embedding coverage state, and operational rate-limit state, and back it with periodic backups and observability.
44

55
On the operational side, the system should adopt **strong politeness and safety controls** before crawling the public internet. That includes honoring `robots.txt`, enforcing per-domain and global rate limits, respecting crawl budgets per domain, and providing a “kill switch” for misbehaving jobs. The services should run inside containers orchestrated by Kubernetes or another orchestrator, with metrics and tracing exported to Prometheus/Grafana or an equivalent stack. Authentication and authorization should be added around the APIs, and audit logging enabled for search queries if required. Finally, ranking can evolve from the current dual-mode baseline (classical + semantic) into a calibrated hybrid ranker (lexical + vector re-ranking), while still keeping an interpretable “debug view” so engineers and students can understand why particular URLs are returned for a query.
66

0 commit comments

Comments
 (0)