-
Notifications
You must be signed in to change notification settings - Fork 0
Implement vector search with dual query interfaces #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,10 @@ | |
|
|
||
| import { loadConfig } from './config'; | ||
| import { initTelemetry, createLogger, shutdownTelemetry } from './telemetry'; | ||
| import { SearchService } from './search'; | ||
|
|
||
| const logger = createLogger('main'); | ||
| let searchService: SearchService | undefined; | ||
|
|
||
| async function main() { | ||
| try { | ||
|
|
@@ -27,6 +29,19 @@ async function main() { | |
| }); | ||
| } | ||
|
|
||
| // Initialize search service if enabled | ||
| if (config.vectorDb?.enabled) { | ||
| logger.info('Initializing search service...'); | ||
| searchService = new SearchService({ | ||
| enabled: true, | ||
| type: config.vectorDb.type, | ||
| apiKey: config.vectorDb.apiKey, | ||
| endpoint: config.vectorDb.endpoint, | ||
| }); | ||
| await searchService.initialize(); | ||
| logger.info('Search service initialized - you can now find items both ways!'); | ||
| } | ||
|
|
||
| logger.info('Application started successfully'); | ||
|
|
||
| // Your application logic here | ||
|
|
@@ -40,12 +55,18 @@ async function main() { | |
| // Graceful shutdown | ||
| process.on('SIGTERM', async () => { | ||
| logger.info('SIGTERM received, shutting down gracefully...'); | ||
| if (searchService) { | ||
| await searchService.close(); | ||
| } | ||
| await shutdownTelemetry(); | ||
| process.exit(0); | ||
| }); | ||
|
|
||
| process.on('SIGINT', async () => { | ||
| logger.info('SIGINT received, shutting down gracefully...'); | ||
| if (searchService) { | ||
| await searchService.close(); | ||
| } | ||
| await shutdownTelemetry(); | ||
| process.exit(0); | ||
|
Comment on lines
56
to
71
|
||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,240 @@ | ||||||||||||||||
| /** | ||||||||||||||||
| * Vector Database Search Interface | ||||||||||||||||
| * Provides a unified search interface across multiple vector database backends | ||||||||||||||||
| */ | ||||||||||||||||
|
|
||||||||||||||||
| import { createLogger } from '../telemetry'; | ||||||||||||||||
|
|
||||||||||||||||
| const logger = createLogger('search'); | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Search result item | ||||||||||||||||
| */ | ||||||||||||||||
| export interface SearchResult { | ||||||||||||||||
| id: string; | ||||||||||||||||
| score: number; | ||||||||||||||||
| metadata?: Record<string, unknown>; | ||||||||||||||||
| content?: string; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Search query parameters | ||||||||||||||||
| */ | ||||||||||||||||
| export interface SearchQuery { | ||||||||||||||||
| vector?: number[]; | ||||||||||||||||
| text?: string; | ||||||||||||||||
| topK?: number; | ||||||||||||||||
| filter?: Record<string, unknown>; | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+23
to
+28
|
||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Abstract vector database interface | ||||||||||||||||
| */ | ||||||||||||||||
| export interface VectorDatabase { | ||||||||||||||||
| /** | ||||||||||||||||
| * Search for similar items in the vector database | ||||||||||||||||
| */ | ||||||||||||||||
| search(query: SearchQuery): Promise<SearchResult[]>; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Initialize the database connection | ||||||||||||||||
| */ | ||||||||||||||||
| initialize(): Promise<void>; | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Close the database connection | ||||||||||||||||
| */ | ||||||||||||||||
| close(): Promise<void>; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Pinecone vector database adapter | ||||||||||||||||
| */ | ||||||||||||||||
| export class PineconeAdapter implements VectorDatabase { | ||||||||||||||||
| private initialized = false; | ||||||||||||||||
|
|
||||||||||||||||
| constructor(private config: { apiKey?: string; endpoint?: string }) {} | ||||||||||||||||
|
|
||||||||||||||||
| async initialize(): Promise<void> { | ||||||||||||||||
| if (this.initialized) return; | ||||||||||||||||
| logger.info('Initializing Pinecone adapter'); | ||||||||||||||||
| // TODO: Initialize Pinecone client when pinecone-client is installed | ||||||||||||||||
| this.initialized = true; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async search(query: SearchQuery): Promise<SearchResult[]> { | ||||||||||||||||
| if (!this.initialized) { | ||||||||||||||||
| await this.initialize(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| logger.debug('Searching Pinecone', { query }); | ||||||||||||||||
| // TODO: Implement actual Pinecone search when pinecone-client is installed | ||||||||||||||||
| return []; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async close(): Promise<void> { | ||||||||||||||||
| logger.info('Closing Pinecone connection'); | ||||||||||||||||
| this.initialized = false; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Weaviate vector database adapter | ||||||||||||||||
| */ | ||||||||||||||||
| export class WeaviateAdapter implements VectorDatabase { | ||||||||||||||||
| private initialized = false; | ||||||||||||||||
|
|
||||||||||||||||
| constructor(private config: { apiKey?: string; endpoint?: string }) {} | ||||||||||||||||
|
|
||||||||||||||||
| async initialize(): Promise<void> { | ||||||||||||||||
| if (this.initialized) return; | ||||||||||||||||
| logger.info('Initializing Weaviate adapter'); | ||||||||||||||||
| // TODO: Initialize Weaviate client when weaviate-ts-client is installed | ||||||||||||||||
| this.initialized = true; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async search(query: SearchQuery): Promise<SearchResult[]> { | ||||||||||||||||
| if (!this.initialized) { | ||||||||||||||||
| await this.initialize(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| logger.debug('Searching Weaviate', { query }); | ||||||||||||||||
| // TODO: Implement actual Weaviate search when weaviate-ts-client is installed | ||||||||||||||||
| return []; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async close(): Promise<void> { | ||||||||||||||||
| logger.info('Closing Weaviate connection'); | ||||||||||||||||
| this.initialized = false; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Chroma vector database adapter | ||||||||||||||||
| */ | ||||||||||||||||
| export class ChromaAdapter implements VectorDatabase { | ||||||||||||||||
| private initialized = false; | ||||||||||||||||
|
|
||||||||||||||||
| constructor(private config: { apiKey?: string; endpoint?: string }) {} | ||||||||||||||||
|
|
||||||||||||||||
| async initialize(): Promise<void> { | ||||||||||||||||
| if (this.initialized) return; | ||||||||||||||||
| logger.info('Initializing Chroma adapter'); | ||||||||||||||||
| // TODO: Initialize Chroma client when chromadb is installed | ||||||||||||||||
| this.initialized = true; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async search(query: SearchQuery): Promise<SearchResult[]> { | ||||||||||||||||
| if (!this.initialized) { | ||||||||||||||||
| await this.initialize(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| logger.debug('Searching Chroma', { query }); | ||||||||||||||||
| // TODO: Implement actual Chroma search when chromadb is installed | ||||||||||||||||
| return []; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| async close(): Promise<void> { | ||||||||||||||||
| logger.info('Closing Chroma connection'); | ||||||||||||||||
| this.initialized = false; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Factory function to create the appropriate vector database adapter | ||||||||||||||||
| */ | ||||||||||||||||
| export function createVectorDatabase( | ||||||||||||||||
| type: 'pinecone' | 'weaviate' | 'chroma', | ||||||||||||||||
| config: { apiKey?: string; endpoint?: string } | ||||||||||||||||
| ): VectorDatabase { | ||||||||||||||||
| switch (type) { | ||||||||||||||||
| case 'pinecone': | ||||||||||||||||
| return new PineconeAdapter(config); | ||||||||||||||||
| case 'weaviate': | ||||||||||||||||
| return new WeaviateAdapter(config); | ||||||||||||||||
| case 'chroma': | ||||||||||||||||
| return new ChromaAdapter(config); | ||||||||||||||||
| default: | ||||||||||||||||
| throw new Error(`Unsupported vector database type: ${type}`); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Search service that manages vector database operations | ||||||||||||||||
| */ | ||||||||||||||||
| export class SearchService { | ||||||||||||||||
| private database?: VectorDatabase; | ||||||||||||||||
|
|
||||||||||||||||
| constructor( | ||||||||||||||||
| private config: { | ||||||||||||||||
| enabled: boolean; | ||||||||||||||||
| type?: 'pinecone' | 'weaviate' | 'chroma'; | ||||||||||||||||
| apiKey?: string; | ||||||||||||||||
| endpoint?: string; | ||||||||||||||||
| } | ||||||||||||||||
| ) {} | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Initialize the search service | ||||||||||||||||
| */ | ||||||||||||||||
| async initialize(): Promise<void> { | ||||||||||||||||
| if (!this.config.enabled) { | ||||||||||||||||
| logger.info('Search service disabled'); | ||||||||||||||||
| return; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (!this.config.type) { | ||||||||||||||||
| throw new Error('Vector database type is required when search is enabled'); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| logger.info(`Initializing search service with ${this.config.type}`); | ||||||||||||||||
| this.database = createVectorDatabase(this.config.type, { | ||||||||||||||||
| apiKey: this.config.apiKey, | ||||||||||||||||
| endpoint: this.config.endpoint, | ||||||||||||||||
| }); | ||||||||||||||||
| await this.database.initialize(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Search for items using vector similarity | ||||||||||||||||
| * This is the "first way" to find items | ||||||||||||||||
| */ | ||||||||||||||||
| async searchByVector(vector: number[], topK = 10): Promise<SearchResult[]> { | ||||||||||||||||
| if (!this.database) { | ||||||||||||||||
| throw new Error('Search service not initialized'); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| logger.info('Searching by vector', { topK }); | ||||||||||||||||
| return this.database.search({ vector, topK }); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Search for items using text query | ||||||||||||||||
| * This is the "second way" to find items | ||||||||||||||||
| * | ||||||||||||||||
| * TODO: Text-to-vector conversion will require an embedding model such as: | ||||||||||||||||
| * - HuggingFace Transformers (e.g., 'sentence-transformers/all-MiniLM-L6-v2') | ||||||||||||||||
| * - OpenAI Embeddings API (e.g., 'text-embedding-ada-002') | ||||||||||||||||
| * - Cohere Embed API | ||||||||||||||||
| * The embedding model should produce vectors matching the dimensions expected | ||||||||||||||||
| * by your vector database index (commonly 384, 768, or 1536 dimensions). | ||||||||||||||||
| */ | ||||||||||||||||
| async searchByText(text: string, topK = 10): Promise<SearchResult[]> { | ||||||||||||||||
| if (!this.database) { | ||||||||||||||||
| throw new Error('Search service not initialized'); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| logger.info('Searching by text', { text, topK }); | ||||||||||||||||
|
||||||||||||||||
| logger.info('Searching by text', { text, topK }); | |
| logger.info('Searching by text', { topK, textLength: text.length }); | |
| logger.debug('Searching by text (redacted)', { | |
| topK, | |
| textLength: text.length, | |
| textPreview: text.length > 32 ? text.slice(0, 32) + '...' : text, | |
| }); |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SearchService.close() closes the underlying adapter but leaves this.database set. Because adapters auto-initialize in search(), calling searchByVector/searchByText after close() will silently re-open and proceed, which is surprising for a closed service. Consider clearing this.database (and/or tracking an explicit initialized/closed state) so searches after close reliably fail until initialize() is called again.
| await this.database.close(); | |
| await this.database.close(); | |
| this.database = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README describes vector search as a feature with a working usage example, but the current adapters are stubs that always return an empty result set (
return []). Consider explicitly documenting that the database integrations are placeholders/TODOs and that searches currently return no results until a real client implementation is added, to avoid misleading users.