Thanks to visit codestin.com
Credit goes to github.com

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
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ node_modules

/public/workspace/*

.eslintrc.json

.cursorrules

instructions.md

# local env files
*.env*
!example.env
Expand All @@ -38,4 +44,6 @@ yarn-error.log*
# Lock files
# Вызвало сбой сборки докера
# пока вернем до прихода DevOps
# package-lock.json*
# package-lock.json*

public/build/
5 changes: 1 addition & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# syntax = docker/dockerfile:1.3
ARG NODE_VERSION=20


Expand Down Expand Up @@ -28,6 +27,4 @@ EXPOSE 8080


FROM ghcr.io/rabotaru/dochub/nginx:v0.0.3 as nginx
COPY --chown=101 --from=builder /var/www/dist /usr/share/nginx/html


COPY --chown=101 --from=builder /var/www/dist /usr/share/nginx/html
109 changes: 109 additions & 0 deletions instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Global Search Implementation

## Overview
Implement a global search functionality that allows users to search across all entities in the system including components, aspects, and documents with content search.

## Requirements

### UI Requirements
1. Search Input
- Search input in Menu.vue
- Trigger search on Enter key or search button click
- Clear input button
- Placeholder text "Поиск..."

2. Search Results Display
- Results shown in main panel using v-data-table
- Columns:
- Type (with color-coded chips:
- "Компонент" (blue)
- "Аспект" (green)
- "Документ" (light blue))
- Title (clickable link)
- Score (for relevance sorting)
- Content matches shown with:
- "Найдено в содержимом" indicator
- Content snippet with highlighted matches
- Sort results by relevance score
- "Результаты не найдены" message when no matches

### Search Functionality
1. Components Search
- Search in:
- Component ID
- Component title
- Technologies list
- Display with:
- Blue chip labeled "Компонент"
- Link to component page
- Score: 5

2. Aspects Search
- Search in:
- Aspect ID
- Aspect title
- Location
- Display with:
- Green chip labeled "Аспект"
- Link to aspect page
- Score: 5

3. Document Content Search
- Search in:
- Document ID
- Document title/description
- Document content
- Document subjects
- Display with:
- Light blue chip labeled "Документ"
- Link to document
- Content snippet
- Score based on match location

### Technical Implementation
1. Build-time Indexing
- Index documents during build
- Parse markdown content
- Extract headers and keywords
- Store in document-index.json

2. Runtime Search
- Load document index on startup
- Access Vuex store for components/aspects
- Combine search results
- Sort by relevance score
- Remove duplicates

3. Search Result Format
```javascript
{
id: string, // Entity ID
title: string, // Display title
entity: string, // "component" | "aspect" | "document"
link: string, // Router link
score: number, // Relevance score
matchedInContent?: boolean, // For documents only
contentSnippet?: string // For documents only
}
```

## File Structure
- src/frontend/components/Search/SearchResults.vue - Search results component
- src/frontend/manifest/query.js - Search implementation
- src/build/document-indexer.js - Document indexing
- src/build/index-documents.js - Build script

## Language Requirements
All user interface elements must be in Russian:
1. Search input placeholder: "Поиск..."
2. Entity types:
- Component: "Компонент"
- Aspect: "Аспект"
- Document: "Документ"
3. Search indicators:
- "Найдено в содержимом" for content matches
- "Результаты не найдены" for no results
4. Column headers:
- "Тип" for entity type
- "Заголовок" for title
- "Релевантность" for score
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
"backend-up": "node src/backend/main.mjs",
"backend": "export VUE_APP_DOCHUB_MODE=backend && npm run build && node src/backend/main.mjs",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build": "npm run build:index && vue-cli-service build",
"build:package": "node vue.lib.js",
"lint": "vue-cli-service lint",
"test": "jest --watch",
"meta-import": "node src/tools/meta/import.js",
"postinstall": "find plugins -maxdepth 1 -type d -exec sh -c \"cd '{}' && if [ -f package.json ]; then npm install; fi\" \\;"
"postinstall": "mkdir -p plugins || true && find plugins -maxdepth 1 -type d -exec sh -c \"cd '{}' && if [ -f package.json ]; then npm install; fi\" \\; || true",
"build:index": "node src/build/index-documents.js"
},
"dependencies": {
"@asyncapi/react-component": "2.3.4",
Expand Down Expand Up @@ -107,7 +108,9 @@
"vue-template-compiler": "2.7.14",
"webpack-bundle-analyzer": "4.8.0",
"webpack-pwa-manifest": "4.3.0",
"yo": "4.3.0"
"yo": "4.3.0",
"marked": "^11.1.0",
"keyword-extractor": "^0.0.28"
},
"overrides": {
"es5-ext": "0.10.53"
Expand Down
177 changes: 177 additions & 0 deletions src/build/document-indexer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const { marked } = require('marked');
const keywordExtractor = require('keyword-extractor');

class DocumentIndexer {
constructor(baseDir = 'public/documentation') {
this.baseDir = baseDir;
this.docTypes = {
markdown: this.processMarkdown,
OpenAPI: this.processOpenAPI,
plantuml: this.processPlantuml
};

// Configure marked for header extraction
this.headers = [];
marked.use({
headerIds: false,
walkTokens: token => {
if (token.type === 'heading') {
this.headers.push({
level: token.depth,
text: token.text
});
}
}
});
}

// Main indexing function
async indexDocuments() {
const index = {};
const yamlFiles = this.findYamlFiles(this.baseDir);

for (const yamlFile of yamlFiles) {
try {
const yamlContent = yaml.load(fs.readFileSync(yamlFile, 'utf8'));
if (yamlContent.docs) {
const docsIndex = await this.processDocsSection(yamlContent.docs, path.dirname(yamlFile));
Object.assign(index, docsIndex);
}
} catch (error) {
console.error(`Error processing ${yamlFile}:`, error);
}
}

return index;
}

// Find all YAML files recursively
findYamlFiles(dir) {
const files = [];
const items = fs.readdirSync(dir);

for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) {
files.push(...this.findYamlFiles(fullPath));
} else if (item.endsWith('.yaml') || item.endsWith('.yml')) {
files.push(fullPath);
}
}

return files;
}

// Process docs section from YAML
async processDocsSection(docs, yamlDir) {
const index = {};

for (const [docId, doc] of Object.entries(docs)) {
if (!doc.source || !doc.type) continue;

const processor = this.docTypes[doc.type];
if (!processor) {
console.warn(`Unknown document type: ${doc.type} for ${docId}`);
continue;
}

try {
const sourcePath = path.join(yamlDir, doc.source);
const { content, headers, keywords } = await processor.call(this, sourcePath);

index[docId] = {
metadata: {
id: docId,
location: doc.location,
description: doc.description,
type: doc.type,
subjects: doc.subjects || []
},
content,
headers,
keywords,
sourcePath: path.relative(this.baseDir, sourcePath)
};
} catch (error) {
console.error(`Error processing ${docId}:`, error);
}
}

return index;
}

// Extract keywords from text
extractKeywords(text) {
return keywordExtractor.extract(text, {
language: "russian",
remove_digits: true,
return_changed_case: true,
remove_duplicates: true
});
}

// Document type processors
async processMarkdown(filePath) {
const content = await fs.promises.readFile(filePath, 'utf8');

// Reset headers array before parsing
this.headers = [];

// Parse markdown and extract headers
const parsedContent = marked.parse(content);
const headers = [...this.headers]; // Copy headers array

// Extract keywords from raw content
const keywords = this.extractKeywords(content);

return {
content: content, // Store original markdown
parsedContent, // Store HTML version
headers,
keywords
};
}

async processOpenAPI(filePath) {
const content = await fs.promises.readFile(filePath, 'utf8');
const parsedContent = yaml.load(content);

// Extract keywords from title, description and operation summaries
const keywords = this.extractKeywords([
parsedContent.info?.title,
parsedContent.info?.description,
...Object.values(parsedContent.paths || {})
.flatMap(path => Object.values(path)
.map(op => op.summary))
].filter(Boolean).join(' '));

return {
content,
parsedContent,
headers: [],
keywords
};
}

async processPlantuml(filePath) {
const content = await fs.promises.readFile(filePath, 'utf8');

// Extract keywords from PlantUML comments and labels
const keywords = this.extractKeywords(
content.replace(/[@{}]/g, ' ')
);

return {
content,
headers: [],
keywords
};
}
}

module.exports = DocumentIndexer;
28 changes: 28 additions & 0 deletions src/build/index-documents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const DocumentIndexer = require('./document-indexer');
const fs = require('fs');
const path = require('path');

async function buildDocumentIndex() {
try {
const indexer = new DocumentIndexer();
const index = await indexer.indexDocuments();

// Create build directory if it doesn't exist
const buildDir = path.join(__dirname, '../../public/build');
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}

// Write index to file
const indexPath = path.join(buildDir, 'document-index.json');
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));

console.log(`Document index built successfully: ${indexPath}`);
console.log(`Total documents indexed: ${Object.keys(index).length}`);
} catch (error) {
console.error('Error building document index:', error);
process.exit(1);
}
}

buildDocumentIndex();
Loading