This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / docs / architecture / url_semantic_search.md
19 kB 524 lines
1# URL Semantic Search Architecture 2 3This document outlines the URL semantic search system that provides similarity-based URL discovery using vector embeddings and semantic search capabilities. 4 5## System Overview 6 7The URL semantic search system consists of: 8 91. **Search Worker** - Processes `CardAddedToLibraryEvent` to index URLs for search 102. **Vector Database Interface** - Abstracts vector storage and similarity search 113. **Search Service** - Coordinates indexing and querying operations 124. **Use Cases** - Command and query operations for search functionality 13 14## Architecture Components 15 16### Core Components 17 18``` 19┌─────────────────────────────────────────────────────────────┐ 20│ Search System │ 21│ │ 22│ ┌─────────────────┐ ┌─────────────────┐ │ 23│ │ IndexUrlForSearch│ │GetSimilarUrlsFor│ │ 24│ │ UseCase │ │ UrlUseCase │ │ 25│ └─────────────────┘ └─────────────────┘ │ 26│ │ │ │ 27│ v v │ 28│ ┌─────────────────────────────────────────────────────────┐│ 29│ │ SearchService ││ 30│ └─────────────────────────────────────────────────────────┘│ 31│ │ │ │ 32│ v v │ 33│ ┌─────────────────┐ ┌─────────────────┐ │ 34│ │ IVectorDatabase │ │ IMetadataService│ │ 35│ │ Interface │ │ │ │ 36│ └─────────────────┘ └─────────────────┘ │ 37└─────────────────────────────────────────────────────────────┘ 38``` 39 40### Event Flow 41 42``` 43CardAddedToLibraryEvent 44 45 v 46┌─────────────────┐ 47│ Search Worker │ 48│ │ 49│ ┌─────────────┐ │ 50│ │CardAddedTo │ │ 51│ │LibraryEvent │ │ 52│ │Handler │ │ 53│ └─────────────┘ │ 54│ │ │ 55│ v │ 56│ ┌─────────────┐ │ 57│ │IndexUrlFor │ │ 58│ │SearchUseCase│ │ 59│ └─────────────┘ │ 60└─────────────────┘ 61 62 v 63┌─────────────────┐ 64│ SearchService │ 65│ │ 66│ 1. Get metadata │ 67│ 2. Generate │ 68│ embeddings │ 69│ 3. Store in │ 70│ vector DB │ 71└─────────────────┘ 72``` 73 74## Data Types 75 76### URL View (Search Result) 77 78```typescript 79export interface UrlView { 80 url: string; 81 metadata: { 82 url: string; 83 title?: string; 84 description?: string; 85 author?: string; 86 thumbnailUrl?: string; 87 }; 88 urlLibraryCount: number; 89 urlInLibrary?: boolean; 90} 91``` 92 93### Search Parameters 94 95```typescript 96export interface GetSimilarUrlsForUrlParams extends PaginatedParams { 97 url: string; 98} 99 100export interface GetSimilarUrlsForUrlResponse { 101 urls: UrlView[]; 102 pagination: Pagination; 103} 104``` 105 106### Vector Database Interface 107 108```typescript 109export interface IVectorDatabase { 110 indexUrl(params: IndexUrlParams): Promise<Result<void>>; 111 findSimilarUrls( 112 params: FindSimilarUrlsParams, 113 ): Promise<Result<UrlSearchResult[]>>; 114 deleteUrl(url: string): Promise<Result<void>>; 115} 116 117export interface IndexUrlParams { 118 url: string; 119 title?: string; 120 description?: string; 121 author?: string; 122 content: string; // Combined text for embedding 123} 124 125export interface FindSimilarUrlsParams { 126 url: string; 127 limit: number; 128 threshold?: number; // Similarity threshold 129} 130 131export interface UrlSearchResult { 132 url: string; 133 similarity: number; 134 metadata: { 135 title?: string; 136 description?: string; 137 author?: string; 138 }; 139} 140``` 141 142## Deployment Contexts 143 144### 1. Production (Fly.io) 145 146``` 147┌─────────────────┐ ┌─────────────────┐ 148│ Web Process │ │ Search Worker │ 149│ │ │ Process │ 150│ ┌─────────────┐ │ │ ┌─────────────┐ │ 151│ │GetSimilar │ │ │ │EventHandler │ │ 152│ │UrlsUseCase │ │ │ │ │ │ 153│ └─────────────┘ │ │ └─────────────┘ │ 154│ │ │ │ │ │ 155│ v │ │ v │ 156│ ┌─────────────┐ │ │ ┌─────────────┐ │ 157│ │SearchService│ │ │ │IndexUrlFor │ │ 158│ │ │ │ │ │SearchUseCase│ │ 159│ └─────────────┘ │ │ └─────────────┘ │ 160│ │ │ │ │ │ 161└────────┼────────┘ └────────┼────────┘ 162 │ │ 163 v v 164 ┌─────────────────────────────────┐ 165 │ Redis │ 166 │ ┌─────────────────────────────┐│ 167 │ │ search Queue ││ 168 │ │ ┌─────────────────────────┐ ││ 169 │ │ │CardAddedToLibraryEvent │ ││ 170 │ │ └─────────────────────────┘ ││ 171 │ └─────────────────────────────┘│ 172 └─────────────────────────────────┘ 173 │ │ 174 v v 175 ┌─────────────┐ ┌─────────────┐ 176 │Vector DB │ │PostgreSQL │ 177 │(Pinecone/ │ │(URL metadata│ 178 │ Weaviate) │ │ + counts) │ 179 └─────────────┘ └─────────────┘ 180``` 181 182**Configuration:** 183 184- `USE_IN_MEMORY_EVENTS=false` 185- `VECTOR_DB_URL` configured (Pinecone, Weaviate, etc.) 186- Separate search worker process 187 188### 2. Local Development 189 190``` 191┌─────────────────────────────────────┐ 192│ Combined Process │ 193│ │ 194│ ┌─────────────┐ ┌─────────────┐ │ 195│ │GetSimilar │ │EventHandler │ │ 196│ │UrlsUseCase │ │ │ │ 197│ └─────────────┘ └─────────────┘ │ 198│ │ │ │ 199│ v v │ 200│ ┌─────────────┐ ┌─────────────┐ │ 201│ │SearchService│ │IndexUrlFor │ │ 202│ │ │ │SearchUseCase│ │ 203│ └─────────────┘ └─────────────┘ │ 204│ │ │ │ 205└────────┼──────────────────┼─────────┘ 206 │ │ 207 v v 208 ┌─────────────────────────────────┐ 209 │ Local Redis │ 210 │ ┌─────────────────────────────┐│ 211 │ │ search Queue ││ 212 │ └─────────────────────────────┘│ 213 └─────────────────────────────────┘ 214 │ │ 215 v v 216 ┌─────────────┐ ┌─────────────┐ 217 │Local Vector │ │PostgreSQL │ 218 │DB (Docker) │ │(Docker) │ 219 └─────────────┘ └─────────────┘ 220``` 221 222**Configuration:** 223 224- `USE_IN_MEMORY_EVENTS=false` 225- Local vector DB via Docker (Weaviate/Qdrant) 226- Both web app and search worker in same process 227 228### 3. Local Mock Development 229 230``` 231┌─────────────────────────────────────┐ 232│ Single Process │ 233│ │ 234│ ┌─────────────┐ ┌─────────────┐ │ 235│ │GetSimilar │ │EventHandler │ │ 236│ │UrlsUseCase │ │ │ │ 237│ └─────────────┘ └─────────────┘ │ 238│ │ │ │ 239│ v v │ 240│ ┌─────────────┐ ┌─────────────┐ │ 241│ │SearchService│ │IndexUrlFor │ │ 242│ │ │ │SearchUseCase│ │ 243│ └─────────────┘ └─────────────┘ │ 244│ │ │ │ 245│ └──────────────────┘ │ 246│ │ 247│ ┌─────────────────────────────────┐ │ 248│ │ InMemoryVectorDatabase │ │ 249│ │ - Map-based storage │ │ 250│ │ - Simple cosine similarity │ │ 251│ │ - No external dependencies │ │ 252│ └─────────────────────────────────┘ │ 253└─────────────────────────────────────┘ 254``` 255 256**Configuration:** 257 258- `USE_IN_MEMORY_EVENTS=true` 259- `USE_MOCK_VECTOR_DB=true` 260- No external vector DB required 261- Simple in-memory similarity using basic text matching 262 263## Search Worker Implementation 264 265### Event Handler 266 267```typescript 268export class CardAddedToLibraryEventHandler 269 implements IEventHandler<CardAddedToLibraryEvent> 270{ 271 constructor(private indexUrlForSearchUseCase: IndexUrlForSearchUseCase) {} 272 273 async handle(event: CardAddedToLibraryEvent): Promise<Result<void>> { 274 // Only index URL cards 275 const cardResult = await this.getCardDetails(event.cardId); 276 if (cardResult.isErr() || !cardResult.value.isUrlCard) { 277 return ok(undefined); 278 } 279 280 return this.indexUrlForSearchUseCase.execute({ 281 url: cardResult.value.url, 282 cardId: event.cardId.getStringValue(), 283 }); 284 } 285} 286``` 287 288### Search Service 289 290```typescript 291export class SearchService { 292 constructor( 293 private vectorDatabase: IVectorDatabase, 294 private metadataService: IMetadataService, 295 private cardQueryRepository: ICardQueryRepository, 296 ) {} 297 298 async indexUrl(url: string): Promise<Result<void>> { 299 // 1. Get metadata 300 const metadataResult = await this.metadataService.fetchMetadata(url); 301 if (metadataResult.isErr()) { 302 return err(metadataResult.error); 303 } 304 305 // 2. Prepare content for embedding 306 const content = this.prepareContentForEmbedding(metadataResult.value); 307 308 // 3. Index in vector database 309 return this.vectorDatabase.indexUrl({ 310 url: url.value, 311 title: metadataResult.value.title, 312 description: metadataResult.value.description, 313 author: metadataResult.value.author, 314 content, 315 }); 316 } 317 318 async findSimilarUrls( 319 url: string, 320 options: { limit: number; threshold?: number }, 321 ): Promise<Result<UrlView[]>> { 322 // 1. Find similar URLs from vector DB 323 const similarResult = await this.vectorDatabase.findSimilarUrls({ 324 url, 325 limit: options.limit, 326 threshold: options.threshold, 327 }); 328 329 if (similarResult.isErr()) { 330 return err(similarResult.error); 331 } 332 333 // 2. Enrich with library counts and user context 334 const enrichedUrls = await this.enrichUrlsWithContext(similarResult.value); 335 336 return ok(enrichedUrls); 337 } 338} 339``` 340 341## Queue Configuration 342 343The search queue is added to the existing queue routing: 344 345```typescript 346// BullMQEventPublisher.getTargetQueues() 347switch (eventName) { 348 case EventNames.CARD_ADDED_TO_LIBRARY: 349 return [QueueNames.FEEDS, QueueNames.SEARCH, QueueNames.ANALYTICS]; 350 case EventNames.CARD_ADDED_TO_COLLECTION: 351 return [QueueNames.FEEDS]; 352 default: 353 return [QueueNames.FEEDS]; 354} 355``` 356 357## Vector Database Implementations 358 359### Production: Pinecone/Weaviate 360 361```typescript 362export class PineconeVectorDatabase implements IVectorDatabase { 363 constructor(private client: PineconeClient) {} 364 365 async indexUrl(params: IndexUrlParams): Promise<Result<void>> { 366 // Generate embeddings and upsert to Pinecone 367 } 368 369 async findSimilarUrls( 370 params: FindSimilarUrlsParams, 371 ): Promise<Result<UrlSearchResult[]>> { 372 // Query Pinecone for similar vectors 373 } 374} 375``` 376 377### Local Development: Weaviate/Qdrant (Docker) 378 379```typescript 380export class WeaviateVectorDatabase implements IVectorDatabase { 381 constructor(private client: WeaviateClient) {} 382 383 async indexUrl(params: IndexUrlParams): Promise<Result<void>> { 384 // Index in local Weaviate instance 385 } 386 387 async findSimilarUrls( 388 params: FindSimilarUrlsParams, 389 ): Promise<Result<UrlSearchResult[]>> { 390 // Query local Weaviate instance 391 } 392} 393``` 394 395### Local Mock: In-Memory 396 397```typescript 398export class InMemoryVectorDatabase implements IVectorDatabase { 399 private urls: Map<string, IndexedUrl> = new Map(); 400 401 async indexUrl(params: IndexUrlParams): Promise<Result<void>> { 402 // Store in memory with simple text-based similarity 403 this.urls.set(params.url, { 404 url: params.url, 405 content: params.content, 406 metadata: { title: params.title, description: params.description }, 407 }); 408 return ok(undefined); 409 } 410 411 async findSimilarUrls( 412 params: FindSimilarUrlsParams, 413 ): Promise<Result<UrlSearchResult[]>> { 414 // Simple text similarity using keyword matching 415 const results = Array.from(this.urls.values()) 416 .filter( 417 (indexed) => 418 this.calculateSimilarity(params.url, indexed.content) > 0.3, 419 ) 420 .map((indexed) => ({ 421 url: indexed.url, 422 similarity: this.calculateSimilarity(params.url, indexed.content), 423 metadata: indexed.metadata, 424 })) 425 .sort((a, b) => b.similarity - a.similarity) 426 .slice(0, params.limit); 427 428 return ok(results); 429 } 430 431 private calculateSimilarity(query: string, content: string): number { 432 // Simple keyword-based similarity for mocking 433 const queryWords = query.toLowerCase().split(/\W+/); 434 const contentWords = content.toLowerCase().split(/\W+/); 435 const intersection = queryWords.filter((word) => 436 contentWords.includes(word), 437 ); 438 return ( 439 intersection.length / Math.max(queryWords.length, contentWords.length) 440 ); 441 } 442} 443``` 444 445## Environment Variables 446 447```bash 448# Vector database configuration 449VECTOR_DB_TYPE=pinecone|weaviate|qdrant|mock 450VECTOR_DB_URL=https://your-vector-db-endpoint 451VECTOR_DB_API_KEY=your-api-key 452 453# Search configuration 454SEARCH_SIMILARITY_THRESHOLD=0.7 455SEARCH_DEFAULT_LIMIT=20 456 457# Mock configuration 458USE_MOCK_VECTOR_DB=true|false 459``` 460 461## Local Development Commands 462 463### `npm run dev` (Redis + BullMQ + Vector DB) 464 465- Uses real vector database (Docker) 466- Separate search worker process 467- Full production-like behavior 468 469### `npm run dev:mock` (In-Memory) 470 471- Uses `InMemoryVectorDatabase` 472- Simple text-based similarity 473- No external dependencies 474 475### `npm run search-worker` (Dedicated Search Worker) 476 477- Runs only the search worker process 478- For scaling search processing separately 479 480## Performance Considerations 481 482### Indexing Strategy 483 484- **Immediate Indexing**: Index URLs as soon as they're added to library 485- **Batch Processing**: For high-volume scenarios, batch index operations 486- **Deduplication**: Avoid re-indexing the same URL multiple times 487 488### Query Optimization 489 490- **Caching**: Cache similar URL results for popular queries 491- **Pagination**: Implement cursor-based pagination for large result sets 492- **Filtering**: Pre-filter results by library counts or user context 493 494### Scaling 495 496- **Horizontal Scaling**: Multiple search worker instances 497- **Vector DB Sharding**: Partition URLs across multiple vector DB instances 498- **CDN Caching**: Cache search results at CDN level for popular queries 499 500## Integration Points 501 502### With Existing Systems 503 5041. **Card System**: Listens to `CardAddedToLibraryEvent` 5052. **Metadata Service**: Reuses existing URL metadata fetching 5063. **Query Repository**: Enriches results with library counts 5074. **Profile Service**: Adds user context to results 508 509### API Endpoints 510 511```typescript 512// GET /api/search/similar-urls?url=https://example.com&limit=10 513app.get('/api/search/similar-urls', async (req, res) => { 514 const result = await getSimilarUrlsForUrlUseCase.execute({ 515 url: req.query.url, 516 limit: parseInt(req.query.limit) || 10, 517 page: parseInt(req.query.page) || 1, 518 }); 519 520 res.json(result); 521}); 522``` 523 524This architecture provides a scalable, testable URL semantic search system that integrates seamlessly with the existing event-driven architecture while supporting different deployment contexts from local development to production.