|
1 | | -# Next Steps: Complete AI Search Implementation |
2 | 1 |
|
3 | | -## What's Done ✅ |
4 | | - |
5 | | -1. **Core Infrastructure** ✅ |
6 | | - - Qdrant vector database setup |
7 | | - - `vector_search_service.py` - Complete CRUD operations |
8 | | - - `search_algorithms.py` - Real semantic search (no more stubs) |
9 | | - - Docker Compose configuration |
10 | | - - All dependencies added |
11 | | - |
12 | | -2. **Integration** ✅ |
13 | | - - Jay's API endpoint (`search_ai.py`) → Your search logic |
14 | | - - Jay's frontend UI → Backend API |
15 | | - - Your embedding service → Vector search service |
16 | | - |
17 | | ---- |
18 | | - |
19 | | -## What's Missing ❌ |
20 | | - |
21 | | -### Priority 1: Canvas Snapshot Generation (Required for Image Embeddings) |
22 | | - |
23 | | -**Problem:** To generate embeddings for canvas content, you need to convert stroke data to images. |
24 | | - |
25 | | -**Options:** |
26 | | - |
27 | | -#### Option A: Text-Based Embeddings (Simplest - Start Here) |
28 | | -Use canvas name + description for embeddings instead of visual content. |
29 | | - |
30 | | -```python |
31 | | -# backend/scripts/populate_embeddings.py |
32 | | -from services.db import rooms_coll |
33 | | -from services.embedding_service import embed_text |
34 | | -from services.vector_search_service import store_canvas_embedding |
35 | | - |
36 | | -def populate_text_embeddings(): |
37 | | - """Generate embeddings from room metadata (name + description).""" |
38 | | - rooms = rooms_coll.find({"archived": {"$ne": True}}) |
39 | | - |
40 | | - for room in rooms: |
41 | | - room_id = str(room['_id']) |
42 | | - name = room.get('name', '') |
43 | | - desc = room.get('description', '') |
44 | | - |
45 | | - # Combine name and description for richer embedding |
46 | | - text = f"{name}. {desc}" if desc else name |
47 | | - |
48 | | - if text.strip(): |
49 | | - embedding = embed_text([text]) |
50 | | - |
51 | | - store_canvas_embedding( |
52 | | - room_id=room_id, |
53 | | - embedding=embedding, |
54 | | - metadata={ |
55 | | - 'name': name, |
56 | | - 'description': desc, |
57 | | - 'type': room.get('type'), |
58 | | - 'ownerName': room.get('ownerName') |
59 | | - } |
60 | | - ) |
61 | | - print(f"✓ Stored embedding for {room_id}: {name}") |
62 | | - |
63 | | -if __name__ == "__main__": |
64 | | - populate_text_embeddings() |
65 | | -``` |
66 | | - |
67 | | -**Pros:** Works immediately, no canvas rendering needed |
68 | | -**Cons:** Doesn't capture visual content |
69 | | -**Use case:** "Find rooms about trees" works, "Find rooms similar to this sketch" won't |
70 | | - |
71 | | -#### Option B: Server-Side Canvas Rendering (Better, More Complex) |
72 | | - |
73 | | -Render strokes to PNG using Pillow: |
74 | | - |
75 | | -```python |
76 | | -# backend/services/canvas_renderer.py |
77 | | -from PIL import Image, ImageDraw |
78 | | -from services.db import strokes_coll |
79 | | - |
80 | | -def render_canvas_to_image(room_id: str, width=800, height=600) -> str: |
81 | | - """Render canvas strokes to PNG file, return path.""" |
82 | | - # Fetch strokes |
83 | | - strokes = list(strokes_coll.find({"roomId": room_id}).sort("ts", 1)) |
84 | | - |
85 | | - # Create image |
86 | | - img = Image.new('RGB', (width, height), 'white') |
87 | | - draw = ImageDraw.Draw(img) |
88 | | - |
89 | | - for stroke in strokes: |
90 | | - points = stroke.get('points', []) |
91 | | - color = stroke.get('color', '#000000') |
92 | | - width = stroke.get('width', 2) |
93 | | - |
94 | | - # Draw lines between points |
95 | | - for i in range(len(points) - 1): |
96 | | - x1, y1 = points[i]['x'], points[i]['y'] |
97 | | - x2, y2 = points[i+1]['x'], points[i+1]['y'] |
98 | | - draw.line([(x1, y1), (x2, y2)], fill=color, width=int(width)) |
99 | | - |
100 | | - # Save to temp file |
101 | | - path = f"/tmp/canvas_{room_id}.png" |
102 | | - img.save(path) |
103 | | - return path |
104 | | -``` |
105 | | - |
106 | | -**Pros:** Captures visual content |
107 | | -**Cons:** Need to understand stroke data format, coordinate systems |
108 | | -**Recommendation:** Start with Option A, add this later |
109 | | - |
110 | | -#### Option C: Frontend Thumbnail Export (Hybrid Approach) |
111 | | - |
112 | | -Let the frontend generate thumbnails and upload them: |
113 | | - |
114 | | -1. Add endpoint: `POST /api/v1/rooms/{room_id}/snapshot` |
115 | | -2. Frontend captures canvas as base64 PNG |
116 | | -3. Backend generates embedding and stores it |
117 | | - |
118 | | -**Pros:** Frontend already knows how to render |
119 | | -**Cons:** Requires frontend changes, manual trigger |
120 | | - |
121 | | ---- |
122 | | - |
123 | | -### Priority 2: Background Embedding Worker |
124 | | - |
125 | | -**Current State:** Embeddings are NOT auto-generated on canvas create/update |
126 | | - |
127 | | -**Solution:** Create a periodic batch processor (simplest approach) |
128 | | - |
129 | | -```python |
130 | | -# backend/workers/embedding_worker.py |
131 | | -import time |
132 | | -import logging |
133 | | -from services.db import rooms_coll |
134 | | -from services.embedding_service import embed_text |
135 | | -from services.vector_search_service import store_canvas_embedding, get_collection_stats |
136 | | - |
137 | | -logger = logging.getLogger(__name__) |
138 | | - |
139 | | -def sync_embeddings_batch(): |
140 | | - """ |
141 | | - Sync embeddings for all canvases that don't have them yet. |
142 | | - Run this periodically (e.g., every 5 minutes). |
143 | | - """ |
144 | | - # Get all room IDs in Qdrant |
145 | | - stats = get_collection_stats() |
146 | | - existing_count = stats.get('points_count', 0) |
147 | | - |
148 | | - # Get all rooms from MongoDB |
149 | | - rooms = list(rooms_coll.find({"archived": {"$ne": True}})) |
150 | | - total_rooms = len(rooms) |
151 | | - |
152 | | - logger.info(f"Found {total_rooms} rooms, {existing_count} embeddings exist") |
153 | | - |
154 | | - new_embeddings = 0 |
155 | | - for room in rooms: |
156 | | - room_id = str(room['_id']) |
157 | | - |
158 | | - # Simple approach: Always regenerate (or add logic to check if exists) |
159 | | - name = room.get('name', '') |
160 | | - desc = room.get('description', '') |
161 | | - text = f"{name}. {desc}" if desc else name |
162 | | - |
163 | | - if text.strip(): |
164 | | - embedding = embed_text([text]) |
165 | | - success = store_canvas_embedding( |
166 | | - room_id=room_id, |
167 | | - embedding=embedding, |
168 | | - metadata={ |
169 | | - 'name': name, |
170 | | - 'description': desc, |
171 | | - 'type': room.get('type'), |
172 | | - 'ownerName': room.get('ownerName') |
173 | | - } |
174 | | - ) |
175 | | - if success: |
176 | | - new_embeddings += 1 |
177 | | - |
178 | | - logger.info(f"Synced {new_embeddings} new embeddings") |
179 | | - return new_embeddings |
180 | | - |
181 | | -def run_worker(interval_seconds=300): |
182 | | - """Run worker in loop.""" |
183 | | - logger.info(f"Starting embedding worker (interval={interval_seconds}s)") |
184 | | - while True: |
185 | | - try: |
186 | | - sync_embeddings_batch() |
187 | | - except Exception as e: |
188 | | - logger.exception(f"Worker error: {e}") |
189 | | - |
190 | | - time.sleep(interval_seconds) |
191 | | - |
192 | | -if __name__ == "__main__": |
193 | | - run_worker() |
194 | | -``` |
195 | | - |
196 | | -**How to run:** |
197 | | -```bash |
198 | | -# In separate terminal |
199 | | -python backend/workers/embedding_worker.py |
200 | | - |
201 | | -# Or add to supervisor/systemd/docker-compose |
202 | | -``` |
203 | | - |
204 | | -**Alternative (Production):** Use Celery for more robust job scheduling |
205 | | - |
206 | | ---- |
207 | | - |
208 | | -### Priority 3: Hook into Canvas Updates |
209 | | - |
210 | | -Trigger embedding regeneration when canvases change: |
211 | | - |
212 | | -```python |
213 | | -# In backend/routes/rooms.py (after canvas update) |
214 | | - |
215 | | -from services.embedding_service import embed_text |
216 | | -from services.vector_search_service import store_canvas_embedding |
217 | | - |
218 | | -@rooms_bp.route('/api/v1/rooms/<room_id>', methods=['PATCH']) |
219 | | -@require_auth |
220 | | -def update_room(room_id): |
221 | | - # ... existing update logic ... |
222 | | - |
223 | | - # After successful update, regenerate embedding |
224 | | - try: |
225 | | - name = updated_room.get('name', '') |
226 | | - desc = updated_room.get('description', '') |
227 | | - text = f"{name}. {desc}" if desc else name |
228 | | - |
229 | | - if text.strip(): |
230 | | - embedding = embed_text([text]) |
231 | | - store_canvas_embedding( |
232 | | - room_id=room_id, |
233 | | - embedding=embedding, |
234 | | - metadata={ |
235 | | - 'name': name, |
236 | | - 'description': desc, |
237 | | - 'type': updated_room.get('type'), |
238 | | - 'ownerName': updated_room.get('ownerName') |
239 | | - } |
240 | | - ) |
241 | | - except Exception as e: |
242 | | - logger.warning(f"Failed to update embedding for {room_id}: {e}") |
243 | | - |
244 | | - return jsonify(updated_room) |
245 | | -``` |
246 | | - |
247 | | ---- |
248 | | - |
249 | | -### Priority 4: Database Indexes (Performance) |
250 | | - |
251 | | -Add indexes for faster queries: |
252 | | - |
253 | | -```python |
254 | | -# In backend/services/db.py (add to existing indexes) |
255 | | - |
256 | | -# For search filtering |
257 | | -rooms_coll.create_index([("type", 1), ("archived", 1)]) |
258 | | -rooms_coll.create_index([("ownerId", 1), ("archived", 1)]) |
259 | | -``` |
260 | | - |
261 | | ---- |
262 | | - |
263 | | -## Recommended Implementation Order |
264 | | - |
265 | | -### Week 1: Get It Working |
266 | | -1. ✅ Setup Qdrant (Done!) |
267 | | -2. ✅ Implement vector_search_service.py (Done!) |
268 | | -3. ✅ Update search_algorithms.py (Done!) |
269 | | -4. ⏳ **Create `populate_embeddings.py` script** (Option A - Text-based) |
270 | | -5. ⏳ **Test search from UI** |
271 | | - |
272 | | -### Week 2: Automate |
273 | | -6. ⏳ Create `embedding_worker.py` (periodic batch sync) |
274 | | -7. ⏳ Add hooks to `rooms.py` for real-time updates |
275 | | -8. ⏳ Add canvas deletion → embedding cleanup |
276 | | - |
277 | | -### Week 3: Visual Search |
278 | | -9. ⏳ Implement canvas rendering (Option B or C) |
279 | | -10. ⏳ Update embeddings to use visual content |
280 | | -11. ⏳ Test image-based search |
281 | | - |
282 | | -### Week 4: Polish |
283 | | -12. ⏳ Add monitoring/logging |
284 | | -13. ⏳ Performance tuning |
285 | | -14. ⏳ Error handling improvements |
286 | | - |
287 | | ---- |
288 | | - |
289 | | -## Quick Test Script |
290 | | - |
291 | | -Save as `backend/scripts/test_vector_search.py`: |
292 | | - |
293 | | -```python |
294 | | -#!/usr/bin/env python3 |
295 | | -"""Quick test script for vector search functionality.""" |
296 | | - |
297 | | -from services.embedding_service import embed_text, embed_image |
298 | | -from services.vector_search_service import ( |
299 | | - store_canvas_embedding, |
300 | | - search_by_embedding, |
301 | | - get_collection_stats |
302 | | -) |
303 | | -import numpy as np |
304 | | - |
305 | | -def test_basic_flow(): |
306 | | - print("🧪 Testing Vector Search...") |
307 | | - |
308 | | - # 1. Store test embeddings |
309 | | - test_data = [ |
310 | | - ("room1", "A beautiful landscape with mountains and trees"), |
311 | | - ("room2", "Abstract geometric shapes in bright colors"), |
312 | | - ("room3", "Portrait of a person with blue eyes"), |
313 | | - ("room4", "Forest scene with tall pine trees"), |
314 | | - ] |
315 | | - |
316 | | - print("\n📝 Storing test embeddings...") |
317 | | - for room_id, description in test_data: |
318 | | - emb = embed_text([description]) |
319 | | - store_canvas_embedding(room_id, emb, {"description": description}) |
320 | | - print(f" ✓ {room_id}: {description[:50]}...") |
321 | | - |
322 | | - # 2. Check stats |
323 | | - print("\n📊 Collection stats:") |
324 | | - stats = get_collection_stats() |
325 | | - print(f" Points: {stats.get('points_count')}") |
326 | | - print(f" Dimension: {stats.get('config', {}).get('dimension')}") |
327 | | - |
328 | | - # 3. Search |
329 | | - print("\n🔍 Searching for 'trees'...") |
330 | | - query_emb = embed_text(["trees"]) |
331 | | - results = search_by_embedding(query_emb, top_k=5) |
332 | | - |
333 | | - print(f"\n Found {len(results)} results:") |
334 | | - for i, r in enumerate(results, 1): |
335 | | - print(f" {i}. {r['room_id']} (score: {r['score']:.3f})") |
336 | | - print(f" {r.get('description', '')[:60]}...") |
337 | | - |
338 | | - print("\n✅ Test complete!") |
339 | | - |
340 | | -if __name__ == "__main__": |
341 | | - test_basic_flow() |
342 | | -``` |
343 | | - |
344 | | -Run with: |
345 | | -```bash |
346 | | -cd backend |
347 | | -python scripts/test_vector_search.py |
348 | | -``` |
349 | | - |
350 | | ---- |
351 | | - |
352 | | -## Summary |
353 | | - |
354 | | -**You have:** Complete Qdrant integration, working vector search, connected UI |
355 | | -**You need:** Populate embeddings (start with text-based), then add automation |
356 | | - |
357 | | -**Fastest path to demo:** |
358 | | -1. Run the test script above |
359 | | -2. Create `populate_embeddings.py` for real rooms |
360 | | -3. Test search in the UI |
361 | | -4. Show Jay it works! 🎉 |
0 commit comments