mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-17 05:31:16 +08:00
240 lines
7.3 KiB
JavaScript
240 lines
7.3 KiB
JavaScript
/**
|
|
* DocumentLoader Module
|
|
* Handles loading and managing search documents from JSON index
|
|
*/
|
|
|
|
class DocumentLoader {
|
|
constructor() {
|
|
this.documents = {};
|
|
this.isLoaded = false;
|
|
}
|
|
|
|
/**
|
|
* Load documents from JSON index files
|
|
*/
|
|
async loadDocuments() {
|
|
try {
|
|
const data = await this.fetchDocumentData();
|
|
this.processDocuments(data);
|
|
this.isLoaded = true;
|
|
console.log(`✅ Document loader initialized with ${Object.keys(this.documents).length} documents`);
|
|
} catch (error) {
|
|
console.error('Failed to load search documents:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch document data from various possible paths
|
|
*/
|
|
async fetchDocumentData() {
|
|
// Try different paths to account for different page depths
|
|
const possiblePaths = [
|
|
'./index.json',
|
|
'../index.json',
|
|
'../../index.json',
|
|
'../../../index.json'
|
|
];
|
|
|
|
for (const path of possiblePaths) {
|
|
try {
|
|
const response = await fetch(path);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.log(`✅ Loaded search index from: ${path}`);
|
|
return data;
|
|
}
|
|
} catch (error) {
|
|
console.log(`❌ Failed to load from ${path}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
throw new Error('Failed to load search data from any path');
|
|
}
|
|
|
|
/**
|
|
* Process and filter documents from raw data
|
|
* Supports three formats:
|
|
* 1. Array of documents (new format): [{ id, title, ... }, ...]
|
|
* 2. Object with children (legacy): { children: [...] }
|
|
* 3. Single document (fallback): { id, title, ... }
|
|
*/
|
|
processDocuments(data) {
|
|
let allDocs;
|
|
if (Array.isArray(data)) {
|
|
// New format: root is an array of documents
|
|
allDocs = data;
|
|
} else if (data.children) {
|
|
// Legacy format: object with children array
|
|
allDocs = data.children;
|
|
} else {
|
|
// Fallback: single document
|
|
allDocs = [data];
|
|
}
|
|
|
|
// Filter out problematic documents
|
|
const filteredDocs = allDocs.filter(doc => this.isValidDocument(doc));
|
|
|
|
// Store documents by ID
|
|
filteredDocs.forEach(doc => {
|
|
this.documents[doc.id] = this.sanitizeDocument(doc);
|
|
});
|
|
|
|
console.log(`Processed ${filteredDocs.length} documents (filtered from ${allDocs.length} total)`);
|
|
}
|
|
|
|
/**
|
|
* Check if a document is valid for indexing
|
|
*/
|
|
isValidDocument(doc) {
|
|
const docId = doc.id || '';
|
|
return !docId.toLowerCase().includes('readme') &&
|
|
!docId.startsWith('_') &&
|
|
doc.title &&
|
|
doc.content;
|
|
}
|
|
|
|
/**
|
|
* Sanitize document content for safe indexing
|
|
* Supports both new schema fields and legacy fields
|
|
* Preserves dynamic facets as-is
|
|
*/
|
|
sanitizeDocument(doc) {
|
|
const sanitized = {
|
|
...doc,
|
|
title: this.sanitizeText(doc.title, 200),
|
|
// Add description as separate indexed field (for improved search relevance)
|
|
description: this.sanitizeText(doc.description, 300),
|
|
content: this.sanitizeText(doc.content, 5000),
|
|
summary: this.sanitizeText(doc.summary, 500),
|
|
headings: this.sanitizeHeadings(doc.headings),
|
|
headings_text: this.sanitizeText(doc.headings_text, 1000),
|
|
keywords: this.sanitizeArray(doc.keywords, 300),
|
|
tags: this.sanitizeArray(doc.tags, 200),
|
|
// Support both topics (new) and categories (legacy)
|
|
topics: this.sanitizeArray(doc.topics || doc.categories, 200),
|
|
// Support both audience (new) and personas (legacy)
|
|
audience: this.sanitizeArray(doc.audience || doc.personas, 200),
|
|
// Content type and difficulty
|
|
content_type: this.sanitizeText(doc.content_type, 50),
|
|
difficulty: this.sanitizeText(doc.difficulty, 50),
|
|
doc_type: this.sanitizeText(doc.doc_type, 50),
|
|
section_path: this.sanitizeArray(doc.section_path, 200),
|
|
author: this.sanitizeText(doc.author, 100)
|
|
};
|
|
|
|
// Preserve facets object (dynamic, user-defined keys)
|
|
if (doc.facets && typeof doc.facets === 'object') {
|
|
sanitized.facets = this.sanitizeFacets(doc.facets);
|
|
}
|
|
|
|
// Preserve legacy flat modality if present and no facets.modality
|
|
if (doc.modality && (!doc.facets || !doc.facets.modality)) {
|
|
sanitized.modality = this.sanitizeText(doc.modality, 50);
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Sanitize facets object (dynamic keys with string or array values)
|
|
*/
|
|
sanitizeFacets(facets) {
|
|
const sanitized = {};
|
|
Object.entries(facets).forEach(([key, value]) => {
|
|
if (Array.isArray(value)) {
|
|
sanitized[key] = value.map(v => String(v).substring(0, 100));
|
|
} else if (value) {
|
|
sanitized[key] = String(value).substring(0, 100);
|
|
}
|
|
});
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Sanitize text content with length limits
|
|
*/
|
|
sanitizeText(text, maxLength) {
|
|
if (!text || typeof text !== 'string') return '';
|
|
return text.substring(0, maxLength);
|
|
}
|
|
|
|
/**
|
|
* Sanitize array content
|
|
*/
|
|
sanitizeArray(arr, maxLength) {
|
|
if (!Array.isArray(arr)) return [];
|
|
return arr.map(item => String(item)).join(' ').substring(0, maxLength);
|
|
}
|
|
|
|
/**
|
|
* Sanitize headings array
|
|
*/
|
|
sanitizeHeadings(headings) {
|
|
if (!Array.isArray(headings)) return [];
|
|
return headings.map(heading => ({
|
|
text: this.sanitizeText(heading.text, 200),
|
|
level: Number(heading.level) || 1
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get all loaded documents
|
|
*/
|
|
getDocuments() {
|
|
return this.documents;
|
|
}
|
|
|
|
/**
|
|
* Get a specific document by ID
|
|
*/
|
|
getDocument(id) {
|
|
return this.documents[id];
|
|
}
|
|
|
|
/**
|
|
* Get document count
|
|
*/
|
|
getDocumentCount() {
|
|
return Object.keys(this.documents).length;
|
|
}
|
|
|
|
/**
|
|
* Check if documents are loaded
|
|
*/
|
|
isReady() {
|
|
return this.isLoaded && Object.keys(this.documents).length > 0;
|
|
}
|
|
|
|
/**
|
|
* Get documents as array for indexing
|
|
*/
|
|
getDocumentsArray() {
|
|
return Object.values(this.documents);
|
|
}
|
|
|
|
/**
|
|
* Filter documents by criteria
|
|
*/
|
|
filterDocuments(filterFn) {
|
|
return this.getDocumentsArray().filter(filterFn);
|
|
}
|
|
|
|
/**
|
|
* Get document statistics
|
|
*/
|
|
getStatistics() {
|
|
const docs = this.getDocumentsArray();
|
|
return {
|
|
totalDocuments: docs.length,
|
|
documentsWithSummary: docs.filter(d => d.summary).length,
|
|
documentsWithHeadings: docs.filter(d => d.headings && d.headings.length > 0).length,
|
|
documentsWithTags: docs.filter(d => d.tags && d.tags.length > 0).length,
|
|
averageContentLength: docs.reduce((sum, d) => sum + (d.content?.length || 0), 0) / docs.length
|
|
};
|
|
}
|
|
}
|
|
|
|
// Make DocumentLoader available globally
|
|
window.DocumentLoader = DocumentLoader;
|