# URL Semantic Search Architecture This document outlines the URL semantic search system that provides similarity-based URL discovery using vector embeddings and semantic search capabilities. ## System Overview The URL semantic search system consists of: 1. **Search Worker** - Processes `CardAddedToLibraryEvent` to index URLs for search 2. **Vector Database Interface** - Abstracts vector storage and similarity search 3. **Search Service** - Coordinates indexing and querying operations 4. **Use Cases** - Command and query operations for search functionality ## Architecture Components ### Core Components ``` ┌─────────────────────────────────────────────────────────────┐ │ Search System │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ IndexUrlForSearch│ │GetSimilarUrlsFor│ │ │ │ UseCase │ │ UrlUseCase │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ │ │ │ v v │ │ ┌─────────────────────────────────────────────────────────┐│ │ │ SearchService ││ │ └─────────────────────────────────────────────────────────┘│ │ │ │ │ │ v v │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ IVectorDatabase │ │ IMetadataService│ │ │ │ Interface │ │ │ │ │ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Event Flow ``` CardAddedToLibraryEvent │ v ┌─────────────────┐ │ Search Worker │ │ │ │ ┌─────────────┐ │ │ │CardAddedTo │ │ │ │LibraryEvent │ │ │ │Handler │ │ │ └─────────────┘ │ │ │ │ │ v │ │ ┌─────────────┐ │ │ │IndexUrlFor │ │ │ │SearchUseCase│ │ │ └─────────────┘ │ └─────────────────┘ │ v ┌─────────────────┐ │ SearchService │ │ │ │ 1. Get metadata │ │ 2. Generate │ │ embeddings │ │ 3. Store in │ │ vector DB │ └─────────────────┘ ``` ## Data Types ### URL View (Search Result) ```typescript export interface UrlView { url: string; metadata: { url: string; title?: string; description?: string; author?: string; thumbnailUrl?: string; }; urlLibraryCount: number; urlInLibrary?: boolean; } ``` ### Search Parameters ```typescript export interface GetSimilarUrlsForUrlParams extends PaginatedParams { url: string; } export interface GetSimilarUrlsForUrlResponse { urls: UrlView[]; pagination: Pagination; } ``` ### Vector Database Interface ```typescript export interface IVectorDatabase { indexUrl(params: IndexUrlParams): Promise>; findSimilarUrls( params: FindSimilarUrlsParams, ): Promise>; deleteUrl(url: string): Promise>; } export interface IndexUrlParams { url: string; title?: string; description?: string; author?: string; content: string; // Combined text for embedding } export interface FindSimilarUrlsParams { url: string; limit: number; threshold?: number; // Similarity threshold } export interface UrlSearchResult { url: string; similarity: number; metadata: { title?: string; description?: string; author?: string; }; } ``` ## Deployment Contexts ### 1. Production (Fly.io) ``` ┌─────────────────┐ ┌─────────────────┐ │ Web Process │ │ Search Worker │ │ │ │ Process │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │GetSimilar │ │ │ │EventHandler │ │ │ │UrlsUseCase │ │ │ │ │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ │ │ │ │ │ v │ │ v │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │SearchService│ │ │ │IndexUrlFor │ │ │ │ │ │ │ │SearchUseCase│ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ │ │ │ │ └────────┼────────┘ └────────┼────────┘ │ │ v v ┌─────────────────────────────────┐ │ Redis │ │ ┌─────────────────────────────┐│ │ │ search Queue ││ │ │ ┌─────────────────────────┐ ││ │ │ │CardAddedToLibraryEvent │ ││ │ │ └─────────────────────────┘ ││ │ └─────────────────────────────┘│ └─────────────────────────────────┘ │ │ v v ┌─────────────┐ ┌─────────────┐ │Vector DB │ │PostgreSQL │ │(Pinecone/ │ │(URL metadata│ │ Weaviate) │ │ + counts) │ └─────────────┘ └─────────────┘ ``` **Configuration:** - `USE_IN_MEMORY_EVENTS=false` - `VECTOR_DB_URL` configured (Pinecone, Weaviate, etc.) - Separate search worker process ### 2. Local Development ``` ┌─────────────────────────────────────┐ │ Combined Process │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │GetSimilar │ │EventHandler │ │ │ │UrlsUseCase │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ v v │ │ ┌─────────────┐ ┌─────────────┐ │ │ │SearchService│ │IndexUrlFor │ │ │ │ │ │SearchUseCase│ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ └────────┼──────────────────┼─────────┘ │ │ v v ┌─────────────────────────────────┐ │ Local Redis │ │ ┌─────────────────────────────┐│ │ │ search Queue ││ │ └─────────────────────────────┘│ └─────────────────────────────────┘ │ │ v v ┌─────────────┐ ┌─────────────┐ │Local Vector │ │PostgreSQL │ │DB (Docker) │ │(Docker) │ └─────────────┘ └─────────────┘ ``` **Configuration:** - `USE_IN_MEMORY_EVENTS=false` - Local vector DB via Docker (Weaviate/Qdrant) - Both web app and search worker in same process ### 3. Local Mock Development ``` ┌─────────────────────────────────────┐ │ Single Process │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │GetSimilar │ │EventHandler │ │ │ │UrlsUseCase │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ v v │ │ ┌─────────────┐ ┌─────────────┐ │ │ │SearchService│ │IndexUrlFor │ │ │ │ │ │SearchUseCase│ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ └──────────────────┘ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ InMemoryVectorDatabase │ │ │ │ - Map-based storage │ │ │ │ - Simple cosine similarity │ │ │ │ - No external dependencies │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────┘ ``` **Configuration:** - `USE_IN_MEMORY_EVENTS=true` - `USE_MOCK_VECTOR_DB=true` - No external vector DB required - Simple in-memory similarity using basic text matching ## Search Worker Implementation ### Event Handler ```typescript export class CardAddedToLibraryEventHandler implements IEventHandler { constructor(private indexUrlForSearchUseCase: IndexUrlForSearchUseCase) {} async handle(event: CardAddedToLibraryEvent): Promise> { // Only index URL cards const cardResult = await this.getCardDetails(event.cardId); if (cardResult.isErr() || !cardResult.value.isUrlCard) { return ok(undefined); } return this.indexUrlForSearchUseCase.execute({ url: cardResult.value.url, cardId: event.cardId.getStringValue(), }); } } ``` ### Search Service ```typescript export class SearchService { constructor( private vectorDatabase: IVectorDatabase, private metadataService: IMetadataService, private cardQueryRepository: ICardQueryRepository, ) {} async indexUrl(url: string): Promise> { // 1. Get metadata const metadataResult = await this.metadataService.fetchMetadata(url); if (metadataResult.isErr()) { return err(metadataResult.error); } // 2. Prepare content for embedding const content = this.prepareContentForEmbedding(metadataResult.value); // 3. Index in vector database return this.vectorDatabase.indexUrl({ url: url.value, title: metadataResult.value.title, description: metadataResult.value.description, author: metadataResult.value.author, content, }); } async findSimilarUrls( url: string, options: { limit: number; threshold?: number }, ): Promise> { // 1. Find similar URLs from vector DB const similarResult = await this.vectorDatabase.findSimilarUrls({ url, limit: options.limit, threshold: options.threshold, }); if (similarResult.isErr()) { return err(similarResult.error); } // 2. Enrich with library counts and user context const enrichedUrls = await this.enrichUrlsWithContext(similarResult.value); return ok(enrichedUrls); } } ``` ## Queue Configuration The search queue is added to the existing queue routing: ```typescript // BullMQEventPublisher.getTargetQueues() switch (eventName) { case EventNames.CARD_ADDED_TO_LIBRARY: return [QueueNames.FEEDS, QueueNames.SEARCH, QueueNames.ANALYTICS]; case EventNames.CARD_ADDED_TO_COLLECTION: return [QueueNames.FEEDS]; default: return [QueueNames.FEEDS]; } ``` ## Vector Database Implementations ### Production: Pinecone/Weaviate ```typescript export class PineconeVectorDatabase implements IVectorDatabase { constructor(private client: PineconeClient) {} async indexUrl(params: IndexUrlParams): Promise> { // Generate embeddings and upsert to Pinecone } async findSimilarUrls( params: FindSimilarUrlsParams, ): Promise> { // Query Pinecone for similar vectors } } ``` ### Local Development: Weaviate/Qdrant (Docker) ```typescript export class WeaviateVectorDatabase implements IVectorDatabase { constructor(private client: WeaviateClient) {} async indexUrl(params: IndexUrlParams): Promise> { // Index in local Weaviate instance } async findSimilarUrls( params: FindSimilarUrlsParams, ): Promise> { // Query local Weaviate instance } } ``` ### Local Mock: In-Memory ```typescript export class InMemoryVectorDatabase implements IVectorDatabase { private urls: Map = new Map(); async indexUrl(params: IndexUrlParams): Promise> { // Store in memory with simple text-based similarity this.urls.set(params.url, { url: params.url, content: params.content, metadata: { title: params.title, description: params.description }, }); return ok(undefined); } async findSimilarUrls( params: FindSimilarUrlsParams, ): Promise> { // Simple text similarity using keyword matching const results = Array.from(this.urls.values()) .filter( (indexed) => this.calculateSimilarity(params.url, indexed.content) > 0.3, ) .map((indexed) => ({ url: indexed.url, similarity: this.calculateSimilarity(params.url, indexed.content), metadata: indexed.metadata, })) .sort((a, b) => b.similarity - a.similarity) .slice(0, params.limit); return ok(results); } private calculateSimilarity(query: string, content: string): number { // Simple keyword-based similarity for mocking const queryWords = query.toLowerCase().split(/\W+/); const contentWords = content.toLowerCase().split(/\W+/); const intersection = queryWords.filter((word) => contentWords.includes(word), ); return ( intersection.length / Math.max(queryWords.length, contentWords.length) ); } } ``` ## Environment Variables ```bash # Vector database configuration VECTOR_DB_TYPE=pinecone|weaviate|qdrant|mock VECTOR_DB_URL=https://your-vector-db-endpoint VECTOR_DB_API_KEY=your-api-key # Search configuration SEARCH_SIMILARITY_THRESHOLD=0.7 SEARCH_DEFAULT_LIMIT=20 # Mock configuration USE_MOCK_VECTOR_DB=true|false ``` ## Local Development Commands ### `npm run dev` (Redis + BullMQ + Vector DB) - Uses real vector database (Docker) - Separate search worker process - Full production-like behavior ### `npm run dev:mock` (In-Memory) - Uses `InMemoryVectorDatabase` - Simple text-based similarity - No external dependencies ### `npm run search-worker` (Dedicated Search Worker) - Runs only the search worker process - For scaling search processing separately ## Performance Considerations ### Indexing Strategy - **Immediate Indexing**: Index URLs as soon as they're added to library - **Batch Processing**: For high-volume scenarios, batch index operations - **Deduplication**: Avoid re-indexing the same URL multiple times ### Query Optimization - **Caching**: Cache similar URL results for popular queries - **Pagination**: Implement cursor-based pagination for large result sets - **Filtering**: Pre-filter results by library counts or user context ### Scaling - **Horizontal Scaling**: Multiple search worker instances - **Vector DB Sharding**: Partition URLs across multiple vector DB instances - **CDN Caching**: Cache search results at CDN level for popular queries ## Integration Points ### With Existing Systems 1. **Card System**: Listens to `CardAddedToLibraryEvent` 2. **Metadata Service**: Reuses existing URL metadata fetching 3. **Query Repository**: Enriches results with library counts 4. **Profile Service**: Adds user context to results ### API Endpoints ```typescript // GET /api/search/similar-urls?url=https://example.com&limit=10 app.get('/api/search/similar-urls', async (req, res) => { const result = await getSimilarUrlsForUrlUseCase.execute({ url: req.query.url, limit: parseInt(req.query.limit) || 10, page: parseInt(req.query.page) || 1, }); res.json(result); }); ``` This 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.