aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
284 lines (281 loc) • 8.58 kB
JavaScript
import { Pinecone } from '@pinecone-database/pinecone';
import OpenAI from 'openai';
import { CacheService } from './cache-service.js';
import { ErrorHandler } from './error-handler.js';
// @ts-nocheck - Optional Pinecone and OpenAI dependencies
class SemanticSearchService {
constructor(config) {
this.config = config;
this.pinecone = new Pinecone({
apiKey: config.pinecone.apiKey
});
this.openai = new OpenAI({
apiKey: config.openai.apiKey
});
this.cache = new CacheService(config.redis);
this.errorHandler = new ErrorHandler();
}
async initialize() {
try {
await this.cache.connect();
const indexes = await this.pinecone.listIndexes();
const indexExists = indexes.indexes?.some(idx => idx.name === this.config.pinecone.indexName);
if (!indexExists) {
await this.createIndex();
}
this.index = this.pinecone.index(this.config.pinecone.indexName);
} catch (error) {
this.errorHandler.handleError(error, {
service: 'SemanticSearch',
operation: 'initialize'
});
throw error;
}
}
async createIndex() {
await this.pinecone.createIndex({
name: this.config.pinecone.indexName,
dimension: 1536,
metric: 'cosine',
spec: {
serverless: {
cloud: 'aws',
region: 'us-east-1'
}
}
});
await this.waitForIndexReady();
}
async waitForIndexReady(maxAttempts = 30) {
for (let i = 0; i < maxAttempts; i++) {
const indexes = await this.pinecone.listIndexes();
const index = indexes.indexes?.find(idx => idx.name === this.config.pinecone.indexName);
if (index?.status?.ready) {
return;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error('Index creation timeout');
}
async indexDocuments(documents) {
if (!this.index) {
await this.initialize();
}
try {
const batchSize = 100;
const batches = this.chunkArray(documents, batchSize);
for (const batch of batches) {
const vectors = await Promise.all(batch.map(async doc => {
const embedding = await this.generateEmbedding(doc.content);
return {
id: doc.id,
values: embedding,
metadata: {
content: doc.content.substring(0, 1000),
title: doc.title || '',
...doc.metadata,
tags: doc.tags?.join(',') || ''
}
};
}));
await this.index.upsert(vectors);
}
await this.cache.delete('search:*');
} catch (error) {
this.errorHandler.handleError(error, {
service: 'SemanticSearch',
operation: 'indexDocuments',
metadata: {
documentCount: documents.length
}
});
throw error;
}
}
async search(query, options = {}) {
if (!this.index) {
await this.initialize();
}
const {
topK = 10,
filter,
includeMetadata = true,
namespace
} = options;
const cacheKey = `search:${query}:${JSON.stringify(options)}`;
if (this.config.costOptimization.enableCaching) {
const cached = await this.cache.get(cacheKey);
if (cached) return cached;
}
try {
const queryEmbedding = await this.generateEmbedding(query);
const queryResponse = await this.index.namespace(namespace || '').query({
vector: queryEmbedding,
topK,
includeMetadata,
filter
});
const results = queryResponse.matches.map(match => ({
id: match.id,
content: match.metadata?.content || '',
metadata: match.metadata || {},
score: match.score || 0,
highlights: this.generateHighlights(query, match.metadata?.content || '')
}));
if (this.config.costOptimization.enableCaching) {
await this.cache.set(cacheKey, results, 300);
}
return results;
} catch (error) {
return this.errorHandler.handleWithFallback(error, () => this.fallbackSearch(query, options), {
service: 'SemanticSearch',
operation: 'search',
metadata: {
query
}
});
}
}
async hybridSearch(query, options = {}) {
const {
semanticWeight = 0.7,
keywordWeight = 0.3,
topK = 10,
filter
} = options;
const [semanticResults, keywordResults] = await Promise.all([this.search(query, {
topK: topK * 2,
filter
}), this.keywordSearch(query, {
topK: topK * 2,
filter
})]);
const scoreMap = new Map();
const resultMap = new Map();
semanticResults.forEach(result => {
const score = result.score * semanticWeight;
scoreMap.set(result.id, score);
resultMap.set(result.id, result);
});
keywordResults.forEach(result => {
const existingScore = scoreMap.get(result.id) || 0;
const newScore = existingScore + result.score * keywordWeight;
scoreMap.set(result.id, newScore);
if (!resultMap.has(result.id)) {
resultMap.set(result.id, result);
}
});
const combinedResults = Array.from(resultMap.values()).map(result => ({
...result,
score: scoreMap.get(result.id) || 0
})).sort((a, b) => b.score - a.score).slice(0, topK);
return combinedResults;
}
async keywordSearch(query, options = {}) {
const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length > 2);
if (!this.index) {
return [];
}
try {
const results = await this.index.namespace('').query({
vector: new Array(1536).fill(0),
topK: options.topK || 10,
includeMetadata: true,
filter: {
...options.filter,
$or: keywords.map(keyword => ({
content: {
$contains: keyword
}
}))
}
});
return results.matches.map(match => {
const content = match.metadata?.content || '';
const keywordScore = this.calculateKeywordScore(keywords, content);
return {
id: match.id,
content,
metadata: match.metadata || {},
score: keywordScore,
highlights: this.generateHighlights(query, content)
};
});
} catch (error) {
console.error('Keyword search error:', error);
return [];
}
}
calculateKeywordScore(keywords, content) {
const lowerContent = content.toLowerCase();
let score = 0;
keywords.forEach(keyword => {
const occurrences = (lowerContent.match(new RegExp(keyword, 'g')) || []).length;
score += occurrences * (1 / keywords.length);
});
return Math.min(score / 10, 1);
}
async generateEmbedding(text) {
try {
const response = await this.openai.embeddings.create({
model: 'text-embedding-ada-002',
input: text.substring(0, 8000)
});
return response.data[0].embedding;
} catch (error) {
this.errorHandler.handleError(error, {
service: 'SemanticSearch',
operation: 'generateEmbedding'
});
throw error;
}
}
generateHighlights(query, content) {
const words = query.toLowerCase().split(/\s+/);
const sentences = content.split(/[.!?]+/);
const highlights = [];
sentences.forEach(sentence => {
const lowerSentence = sentence.toLowerCase();
const hasMatch = words.some(word => lowerSentence.includes(word));
if (hasMatch && sentence.trim().length > 20) {
highlights.push(sentence.trim());
}
});
return highlights.slice(0, 3);
}
fallbackSearch(query, options) {
console.warn('Falling back to basic search implementation');
return [{
id: 'fallback-1',
content: 'Search service is temporarily unavailable. Please try again later.',
metadata: {
fallback: true
},
score: 0.5,
highlights: []
}];
}
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
async deleteDocument(id) {
if (!this.index) {
await this.initialize();
}
await this.index.deleteOne(id);
await this.cache.delete('search:*');
}
async deleteAllDocuments(namespace) {
if (!this.index) {
await this.initialize();
}
await this.index.namespace(namespace || '').deleteAll();
await this.cache.delete('search:*');
}
}
export { SemanticSearchService };
//# sourceMappingURL=semantic-search-service.js.map