Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The Grand United Fields of Theories
This project includes comprehensive quality and hardening features:

- **Config Validation**: Type-safe configuration with Zod validation
- **Vector Search**: Unified search interface for multiple vector databases
- **Telemetry**: Optional OpenTelemetry integration (no vendor lock-in)
- **Security Scanning**: Trivy vulnerability scanning and SBOM generation
- **Pre-commit Hooks**: Automatic linting and formatting
Expand Down Expand Up @@ -82,6 +83,61 @@ npm run validate:config

The validation performs no network calls - only local parsing and validation.

## Vector Search

### Search Functionality

The project provides a unified search interface across multiple vector database backends. You can find items using **both ways**:

1. **Vector-based search**: Search using embedding vectors
2. **Text-based search**: Search using natural language text queries

**Supported Vector Databases:**

- **Pinecone**: Cloud-native vector database
- **Weaviate**: On-premise or cloud vector database
- **Chroma**: Lightweight local/embedded vector database

**Enable Vector Search:**

```bash
# Set in .env file
VECTOR_DB_ENABLED=true
VECTOR_DB_TYPE=chroma # Options: pinecone, weaviate, chroma
VECTOR_DB_ENDPOINT=http://localhost:8000 # Optional - database endpoint
VECTOR_DB_API_KEY=your-api-key # Optional - for cloud services
```

**Usage Example:**

```typescript
import { SearchService } from './search';

// Initialize the search service
const searchService = new SearchService({
enabled: true,
type: 'chroma',
endpoint: 'http://localhost:8000',
});
await searchService.initialize();

// Search by vector (first way)
const vectorResults = await searchService.searchByVector([0.5, 0.5, 0.5], 10);

// Search by text (second way)
const textResults = await searchService.searchByText('find this item', 10);

// Clean up
await searchService.close();
```
Comment on lines +86 to +132
Copy link

Copilot AI Feb 7, 2026

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.

Copilot uses AI. Check for mistakes.

**Features:**

- Abstracted interface works with all supported databases
- Automatic initialization and connection management
- Graceful shutdown handling
- Comprehensive logging and telemetry integration

## Telemetry

### OpenTelemetry Integration
Expand Down Expand Up @@ -249,11 +305,12 @@ Check Issues tab for the Renovate Dependency Dashboard
## Project Structure

```
.
├── src/
│ ├── config/ # Configuration validation module
│ │ ├── index.ts # Config schema and loader
│ │ └── validator.ts # Smoke test script
│ ├── search/ # Vector database search interface
│ │ └── index.ts # SearchService and database adapters
│ ├── telemetry/ # OpenTelemetry integration
│ │ └── index.ts # Tracer and logger setup
│ └── index.ts # Main entry point
Expand Down
21 changes: 21 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SIGTERM/SIGINT handlers are async, but any rejection from searchService.close() or shutdownTelemetry() will become an unhandled promise rejection (event listeners aren’t awaited). Consider wrapping the shutdown sequence in a try/catch (and possibly finally) to ensure errors are logged and the process exits cleanly.

Copilot uses AI. Check for mistakes.
});
Expand Down
240 changes: 240 additions & 0 deletions src/search/index.ts
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
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SearchQuery allows both vector and text to be omitted (or both to be provided), but VectorDatabase.search() doesn’t enforce any invariants. That makes it easy for callers (or future adapter implementations) to accept invalid/ambiguous queries. Consider validating that exactly one of vector/text is provided (and returning a clear error) at the SearchService layer and/or in adapter implementations.

Copilot uses AI. Check for mistakes.

/**
* 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 });
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

searchByText logs the raw query text at info level. Search queries commonly include user-provided content and may contain sensitive data; logging the full text can create unnecessary data exposure. Consider logging only non-sensitive metadata (e.g., topK, text length) and/or lowering this to debug with redaction.

Suggested change
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 uses AI. Check for mistakes.
// TODO: Convert text to vector using embeddings (e.g., HuggingFace Transformers)
return this.database.search({ text, topK });
}

/**
* Close the search service
*/
async close(): Promise<void> {
if (this.database) {
await this.database.close();
Copy link

Copilot AI Feb 7, 2026

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.

Suggested change
await this.database.close();
await this.database.close();
this.database = undefined;

Copilot uses AI. Check for mistakes.
}
}
}
Loading
Loading