实现模型搜索功能 - 支持名称模糊匹配和完整层级路径显示
新增ModelSearchEngine和ModelSearchHandler模块,提供零件和装配体名称搜索功能: • 支持prefix、contains、fuzzy三种匹配模式 • 从根装配体构建完整模型树层级路径 • 智能去重算法自动去除重复结果,保留最长路径 • 递归搜索支持多层级装配体遍历 • 向后兼容,不影响现有功能 解决搜索重复结果和缺少层级路径问题,提升用户体验。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d5accfe24a
commit
b874b17534
19
CLAUDE.md
19
CLAUDE.md
@ -30,6 +30,11 @@ MFC动态链接库(DLL)项目,作为Creo CAD软件插件运行,文档在项
|
||||
- **层级删除** - 按层级安全删除组件,使用SuppressFeatures
|
||||
- **路径删除** - 按组件路径批量删除
|
||||
|
||||
### 4. 搜索功能
|
||||
- **模型搜索** - 零件和装配体名称模糊匹配,支持三种匹配模式
|
||||
- **层级路径显示** - 返回完整的模型树层级路径
|
||||
- **去重算法** - 自动去除重复结果,保留最完整路径
|
||||
|
||||
## 核心API接口
|
||||
|
||||
### 状态查询
|
||||
@ -53,6 +58,11 @@ POST /api/analysis/shell-analysis # 薄壳化分析
|
||||
POST /api/analysis/geometry-complexity # 几何复杂度分析
|
||||
```
|
||||
|
||||
### 搜索功能
|
||||
```http
|
||||
POST /api/search/models # 模型名称模糊搜索
|
||||
```
|
||||
|
||||
### 删除操作
|
||||
```http
|
||||
POST /api/creo/hierarchy/delete # 层级删除
|
||||
@ -92,6 +102,13 @@ POST /api/creo/shrinkwrap/shell # Shrinkwrap导出(支持动态超
|
||||
- **基于文件名去重**: 使用模型文件名作为唯一标识符进行重复检测
|
||||
- **递归装配体支持**: 正确处理多层级装配体中的重复零件
|
||||
|
||||
### 模型搜索优化
|
||||
- **多种匹配模式**: 支持prefix、contains、fuzzy三种匹配算法
|
||||
- **完整层级路径**: 从根装配体构建完整的模型树路径显示
|
||||
- **智能去重算法**: 自动去除重复结果,保留最长层级路径
|
||||
- **递归搜索**: 支持从根装配体开始的完整层级遍历
|
||||
- **向后兼容**: 不影响现有搜索功能,支持多种搜索范围
|
||||
|
||||
## 构建环境
|
||||
|
||||
- **IDE**: Visual Studio 2022 (v143工具集)
|
||||
@ -116,6 +133,8 @@ POST /api/creo/shrinkwrap/shell # Shrinkwrap导出(支持动态超
|
||||
- Socket超时阻塞 → 30秒超时机制
|
||||
- Shrinkwrap复杂模型500错误 → 动态超时和详细异常处理
|
||||
- 几何复杂度分析重复零件 → 装配体遍历去重机制
|
||||
- 模型搜索重复结果问题 → 智能去重算法保留最完整路径
|
||||
- 搜索结果缺少层级路径 → 从根装配体构建完整模型树路径
|
||||
|
||||
## 下一步计划
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "ShellExportHandler.h"
|
||||
#include "PathDeleteManager.h"
|
||||
#include "GeometryAnalyzer.h"
|
||||
#include "ModelSearchHandler.h"
|
||||
#include <wfcSession.h>
|
||||
#include <wfcGlobal.h>
|
||||
#include <string>
|
||||
@ -1602,6 +1603,7 @@ extern "C" int user_initialize(
|
||||
g_http_server->SetRouteHandler("/api/analysis/geometry-complexity", GeometryComplexityHandler);
|
||||
g_http_server->SetRouteHandler("/api/creo/shrinkwrap/shell", ShrinkwrapShellHandler);
|
||||
g_http_server->SetRouteHandler("/api/creo/component/delete-by-path", PathDeleteHandler);
|
||||
g_http_server->SetRouteHandler("/api/search/models", ModelSearchHandler::HandleModelSearchRequest);
|
||||
|
||||
|
||||
if (g_http_server->Start()) {
|
||||
|
||||
@ -215,6 +215,8 @@ ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<ClCompile Include="Logger.cpp" />
|
||||
<ClCompile Include="MFCCreoDll.cpp" />
|
||||
<ClCompile Include="ModelAnalyzer.cpp" />
|
||||
<ClCompile Include="ModelSearchEngine.cpp" />
|
||||
<ClCompile Include="ModelSearchHandler.cpp" />
|
||||
<ClCompile Include="PathDeleteManager.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
@ -244,6 +246,8 @@ ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<ClInclude Include="Logger.h" />
|
||||
<ClInclude Include="MFCCreoDll.h" />
|
||||
<ClInclude Include="ModelAnalyzer.h" />
|
||||
<ClInclude Include="ModelSearchEngine.h" />
|
||||
<ClInclude Include="ModelSearchHandler.h" />
|
||||
<ClInclude Include="PathDeleteManager.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="pfcExceptions.h" />
|
||||
|
||||
@ -81,6 +81,12 @@
|
||||
<ClCompile Include="GeometryAnalyzer.cpp">
|
||||
<Filter>源文件\src\creo</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ModelSearchHandler.cpp">
|
||||
<Filter>源文件\src\creo</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="ModelSearchEngine.cpp">
|
||||
<Filter>源文件\src\creo</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="MFCCreoDll.def">
|
||||
@ -154,6 +160,12 @@
|
||||
<ClInclude Include="GeometryAnalyzer.h">
|
||||
<Filter>源文件\src\creo</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ModelSearchHandler.h">
|
||||
<Filter>源文件\src\creo</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ModelSearchEngine.h">
|
||||
<Filter>源文件\src\creo</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="MFCCreoDll.rc">
|
||||
|
||||
796
ModelSearchEngine.cpp
Normal file
796
ModelSearchEngine.cpp
Normal file
@ -0,0 +1,796 @@
|
||||
#include "pch.h"
|
||||
#include "ModelSearchEngine.h"
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <cctype>
|
||||
#include <algorithm>
|
||||
|
||||
// Singleton implementation
|
||||
ModelSearchEngine& ModelSearchEngine::Instance() {
|
||||
static ModelSearchEngine instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Main search interface
|
||||
ModelSearchResult ModelSearchEngine::SearchModels(const ModelSearchRequest& request) {
|
||||
ModelSearchResult result;
|
||||
|
||||
// Start timing
|
||||
StartTimer();
|
||||
|
||||
try {
|
||||
// Validate request parameters
|
||||
if (request.query.empty()) {
|
||||
result.error_message = "Search query cannot be empty";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Clear previous search state
|
||||
processed_models_.clear();
|
||||
std::vector<SearchResultItem> all_results;
|
||||
|
||||
// Choose search method based on scope
|
||||
if (request.search_scope == "session" || request.search_scope == "all") {
|
||||
// Search from root assemblies to build complete hierarchy paths
|
||||
SearchFromRootAssemblies(request, all_results);
|
||||
}
|
||||
else if (request.search_scope == "current_model") {
|
||||
SearchCurrentModel(request, all_results);
|
||||
}
|
||||
else {
|
||||
result.error_message = "Invalid search scope: " + request.search_scope;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Sort and filter results
|
||||
SortAndFilterResults(all_results, request);
|
||||
|
||||
// Limit result count
|
||||
result.total_found = static_cast<int>(all_results.size());
|
||||
if (request.max_results > 0 && all_results.size() > static_cast<size_t>(request.max_results)) {
|
||||
all_results.resize(request.max_results);
|
||||
}
|
||||
|
||||
result.results = all_results;
|
||||
result.returned_count = static_cast<int>(result.results.size());
|
||||
result.success = true;
|
||||
result.stats.search_scope_used = request.search_scope;
|
||||
|
||||
}
|
||||
catch (const pfcXToolkitError& e) {
|
||||
result.error_message = "OTK Error occurred during search";
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
result.error_message = "Search error: " + std::string(e.what());
|
||||
}
|
||||
catch (...) {
|
||||
result.error_message = "Unknown error during model search";
|
||||
}
|
||||
|
||||
// Set search time
|
||||
result.search_time_ms = GetElapsedTimeMs();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Search models in current session
|
||||
void ModelSearchEngine::SearchSessionModels(const ModelSearchRequest& request, std::vector<SearchResultItem>& results) {
|
||||
try {
|
||||
pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all models in session
|
||||
pfcModels_ptr models = session->ListModels();
|
||||
if (!models) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate through all models
|
||||
for (int i = 0; i < models->getarraysize(); i++) {
|
||||
pfcModel_ptr model = models->get(i);
|
||||
if (!model) continue;
|
||||
|
||||
// Get model information
|
||||
std::string model_type = GetModelTypeString(model);
|
||||
|
||||
// Get file name for consistent path building
|
||||
std::string model_name;
|
||||
try {
|
||||
xstring filename_xstr = model->GetFileName();
|
||||
std::wstring wfilename = filename_xstr;
|
||||
model_name = std::string(wfilename.begin(), wfilename.end());
|
||||
} catch (...) {
|
||||
model_name = GetModelDisplayName(model); // fallback
|
||||
}
|
||||
|
||||
std::string file_path = GetModelFilePath(model);
|
||||
std::string full_path = GetModelFullPath(model);
|
||||
|
||||
// Check model type filter
|
||||
if (!ShouldIncludeModelType(model_type, request.model_types)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate match score
|
||||
double match_score = CalculateMatchScore(request, model_name, "name");
|
||||
|
||||
// Check similarity threshold
|
||||
if (match_score < request.similarity_threshold) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create search result item
|
||||
SearchResultItem item;
|
||||
item.model_name = model_name;
|
||||
item.display_name = model_name;
|
||||
item.full_path = model_name; // 对于会话中的根模型,路径就是模型名
|
||||
item.file_path = file_path; // 物理文件路径
|
||||
item.model_type = model_type;
|
||||
item.match_score = match_score;
|
||||
item.match_type = "name";
|
||||
item.is_in_session = true;
|
||||
item.is_assembly = (model_type == "ASSEMBLY");
|
||||
|
||||
// Build match reason
|
||||
if (request.match_mode == "prefix") {
|
||||
item.match_reason = "Name starts with '" + request.query + "'";
|
||||
} else if (request.match_mode == "contains") {
|
||||
item.match_reason = "Name contains '" + request.query + "'";
|
||||
} else {
|
||||
item.match_reason = "Fuzzy match similarity: " + std::to_string(static_cast<int>(match_score * 100)) + "%";
|
||||
}
|
||||
|
||||
item.matched_keywords.push_back(request.query);
|
||||
results.push_back(item);
|
||||
|
||||
// If assembly and need to search components
|
||||
if (request.include_components && item.is_assembly) {
|
||||
try {
|
||||
pfcAssembly_ptr assembly = pfcAssembly::cast(model);
|
||||
if (assembly) {
|
||||
std::set<std::string> local_processed;
|
||||
SearchAssemblyComponents(request, assembly, results, local_processed, model_name);
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
// Assembly search failure doesn't affect main results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
// Session search failure doesn't throw exception
|
||||
}
|
||||
}
|
||||
|
||||
// Search from root assemblies to build complete hierarchy paths
|
||||
void ModelSearchEngine::SearchFromRootAssemblies(const ModelSearchRequest& request, std::vector<SearchResultItem>& results) {
|
||||
try {
|
||||
pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible);
|
||||
if (!session) return;
|
||||
|
||||
pfcModels_ptr models = session->ListModels();
|
||||
if (!models) return;
|
||||
|
||||
// Global deduplication map: model_file_path -> longest_hierarchy_path
|
||||
std::map<std::string, SearchResultItem> best_results;
|
||||
|
||||
// Find root assemblies and search recursively
|
||||
for (int i = 0; i < models->getarraysize(); i++) {
|
||||
pfcModel_ptr model = models->get(i);
|
||||
if (!model) continue;
|
||||
|
||||
// Skip if not assembly
|
||||
if (model->GetType() != pfcMDL_ASSEMBLY) continue;
|
||||
|
||||
// Get model file name for root path
|
||||
std::string root_name;
|
||||
try {
|
||||
xstring filename_xstr = model->GetFileName();
|
||||
std::wstring wfilename = filename_xstr;
|
||||
root_name = std::string(wfilename.begin(), wfilename.end());
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if root assembly itself matches
|
||||
double match_score = CalculateMatchScore(request, root_name, "name");
|
||||
if (match_score >= request.similarity_threshold) {
|
||||
SearchResultItem item;
|
||||
item.model_name = root_name;
|
||||
item.display_name = root_name;
|
||||
item.full_path = root_name; // Root assembly path
|
||||
item.file_path = GetModelFilePath(model);
|
||||
item.model_type = "ASSEMBLY";
|
||||
item.match_score = match_score;
|
||||
item.match_type = "name";
|
||||
item.is_in_session = true;
|
||||
item.is_assembly = true;
|
||||
item.match_reason = (request.match_mode == "contains") ?
|
||||
("Name contains '" + request.query + "'") :
|
||||
("Name matches '" + request.query + "'");
|
||||
item.matched_keywords.push_back(request.query);
|
||||
|
||||
// Add to best results with deduplication
|
||||
std::string key = item.file_path;
|
||||
if (best_results.find(key) == best_results.end() ||
|
||||
best_results[key].full_path.length() < item.full_path.length()) {
|
||||
best_results[key] = item;
|
||||
}
|
||||
}
|
||||
|
||||
// Search components recursively with complete path
|
||||
if (request.include_components) {
|
||||
pfcAssembly_ptr assembly = pfcAssembly::cast(model);
|
||||
if (assembly) {
|
||||
std::set<std::string> local_processed;
|
||||
SearchAssemblyComponentsWithDedup(request, assembly, best_results, local_processed, root_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add best results to final results
|
||||
for (auto& pair : best_results) {
|
||||
results.push_back(pair.second);
|
||||
}
|
||||
|
||||
} catch (...) {
|
||||
// Error in root assembly search
|
||||
}
|
||||
}
|
||||
|
||||
// Search current model
|
||||
void ModelSearchEngine::SearchCurrentModel(const ModelSearchRequest& request, std::vector<SearchResultItem>& results) {
|
||||
try {
|
||||
pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
pfcModel_ptr current_model = session->GetCurrentModel();
|
||||
if (!current_model) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check current model itself
|
||||
// Get file name for consistent path building
|
||||
std::string model_name;
|
||||
try {
|
||||
xstring filename_xstr = current_model->GetFileName();
|
||||
std::wstring wfilename = filename_xstr;
|
||||
model_name = std::string(wfilename.begin(), wfilename.end());
|
||||
} catch (...) {
|
||||
model_name = GetModelDisplayName(current_model); // fallback
|
||||
}
|
||||
|
||||
double match_score = CalculateMatchScore(request, model_name, "name");
|
||||
|
||||
if (match_score >= request.similarity_threshold) {
|
||||
std::string file_path = GetModelFilePath(current_model);
|
||||
std::string full_path = GetModelFullPath(current_model);
|
||||
|
||||
SearchResultItem item;
|
||||
item.model_name = model_name;
|
||||
item.display_name = model_name;
|
||||
item.full_path = model_name; // 当前模型的路径就是模型名
|
||||
item.file_path = file_path; // 物理文件路径
|
||||
item.model_type = GetModelTypeString(current_model);
|
||||
item.match_score = match_score;
|
||||
item.match_type = "name";
|
||||
item.is_in_session = true;
|
||||
item.is_assembly = (item.model_type == "ASSEMBLY");
|
||||
item.match_reason = "Current model match";
|
||||
item.matched_keywords.push_back(request.query);
|
||||
results.push_back(item);
|
||||
}
|
||||
|
||||
// If assembly, search its components
|
||||
if (request.include_components) {
|
||||
try {
|
||||
pfcAssembly_ptr assembly = pfcAssembly::cast(current_model);
|
||||
if (assembly) {
|
||||
std::set<std::string> local_processed;
|
||||
SearchAssemblyComponents(request, assembly, results, local_processed, model_name);
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
// Component search failure doesn't affect main results
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
// Current model search failure doesn't throw exception
|
||||
}
|
||||
}
|
||||
|
||||
// Search assembly components (recursive)
|
||||
void ModelSearchEngine::SearchAssemblyComponents(const ModelSearchRequest& request,
|
||||
pfcAssembly_ptr assembly,
|
||||
std::vector<SearchResultItem>& results,
|
||||
std::set<std::string>& processed_models,
|
||||
const std::string& parent_path) {
|
||||
if (!assembly) return;
|
||||
|
||||
try {
|
||||
// Get all component features (including suppressed ones)
|
||||
pfcFeatures_ptr features = assembly->ListFeaturesByType(xfalse, pfcFeatureType::pfcFEATTYPE_COMPONENT);
|
||||
if (!features) return;
|
||||
|
||||
if (features->getarraysize() == 0) return;
|
||||
|
||||
for (int i = 0; i < features->getarraysize(); i++) {
|
||||
pfcFeature_ptr feature = features->get(i);
|
||||
if (!feature) continue;
|
||||
|
||||
try {
|
||||
pfcComponentFeat_ptr comp_feat = pfcComponentFeat::cast(feature);
|
||||
if (!comp_feat) continue;
|
||||
|
||||
pfcModelDescriptor_ptr desc = comp_feat->GetModelDescr();
|
||||
if (!desc) continue;
|
||||
|
||||
pfcModel_ptr comp_model = nullptr;
|
||||
try {
|
||||
pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible);
|
||||
if (session) {
|
||||
comp_model = session->GetModelFromDescr(desc);
|
||||
}
|
||||
} catch (pfcXToolkitError&) {
|
||||
// Model not loaded, skip this component
|
||||
continue;
|
||||
}
|
||||
if (!comp_model) continue;
|
||||
|
||||
// Get file name for component path (like hierarchy analysis)
|
||||
std::string comp_name;
|
||||
try {
|
||||
xstring filename_xstr = desc->GetFileName();
|
||||
std::wstring wfilename = filename_xstr;
|
||||
comp_name = std::string(wfilename.begin(), wfilename.end());
|
||||
} catch (...) {
|
||||
comp_name = GetModelDisplayName(comp_model); // fallback
|
||||
}
|
||||
|
||||
std::string comp_file = GetModelFilePath(comp_model);
|
||||
std::string full_path = GetModelFullPath(comp_model);
|
||||
|
||||
// Prevent duplicate processing of same model
|
||||
if (processed_models.find(comp_file) != processed_models.end()) {
|
||||
continue;
|
||||
}
|
||||
processed_models.insert(comp_file);
|
||||
|
||||
// Check model type filter
|
||||
std::string model_type = GetModelTypeString(comp_model);
|
||||
if (!ShouldIncludeModelType(model_type, request.model_types)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate match score
|
||||
double match_score = CalculateMatchScore(request, comp_name, "component");
|
||||
|
||||
// Build tree path for model hierarchy
|
||||
std::string tree_path;
|
||||
if (parent_path.empty()) {
|
||||
tree_path = comp_name;
|
||||
} else {
|
||||
tree_path = parent_path + "/" + comp_name;
|
||||
}
|
||||
|
||||
if (match_score >= request.similarity_threshold) {
|
||||
SearchResultItem item;
|
||||
item.model_name = comp_name;
|
||||
item.display_name = comp_name;
|
||||
item.full_path = tree_path; // Model tree hierarchy path
|
||||
item.file_path = comp_file; // Physical file path
|
||||
item.model_type = model_type;
|
||||
item.component_path = BuildComponentPath(parent_path, comp_name);
|
||||
item.match_score = match_score;
|
||||
item.match_type = "component";
|
||||
item.is_in_session = true;
|
||||
item.is_assembly = (model_type == "ASSEMBLY");
|
||||
item.match_reason = "Component name match in assembly";
|
||||
item.matched_keywords.push_back(request.query);
|
||||
results.push_back(item);
|
||||
}
|
||||
|
||||
// If this component is also an assembly, search recursively
|
||||
if (model_type == "ASSEMBLY") {
|
||||
try {
|
||||
pfcAssembly_ptr sub_assembly = pfcAssembly::cast(comp_model);
|
||||
if (sub_assembly) {
|
||||
SearchAssemblyComponents(request, sub_assembly, results, processed_models, tree_path);
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
// Sub-assembly search failure doesn't affect other results
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
// Single component processing failure doesn't affect other components
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
// Assembly component search failure doesn't throw exception
|
||||
}
|
||||
}
|
||||
|
||||
// Search assembly components with global deduplication (recursive)
|
||||
void ModelSearchEngine::SearchAssemblyComponentsWithDedup(const ModelSearchRequest& request,
|
||||
pfcAssembly_ptr assembly,
|
||||
std::map<std::string, SearchResultItem>& best_results,
|
||||
std::set<std::string>& processed_models,
|
||||
const std::string& parent_path) {
|
||||
if (!assembly) return;
|
||||
|
||||
try {
|
||||
// Get all component features (including suppressed ones)
|
||||
pfcFeatures_ptr features = assembly->ListFeaturesByType(xfalse, pfcFeatureType::pfcFEATTYPE_COMPONENT);
|
||||
if (!features) return;
|
||||
|
||||
if (features->getarraysize() == 0) return;
|
||||
|
||||
for (int i = 0; i < features->getarraysize(); i++) {
|
||||
pfcFeature_ptr feature = features->get(i);
|
||||
if (!feature) continue;
|
||||
|
||||
try {
|
||||
pfcComponentFeat_ptr comp_feat = pfcComponentFeat::cast(feature);
|
||||
if (!comp_feat) continue;
|
||||
|
||||
pfcModelDescriptor_ptr desc = comp_feat->GetModelDescr();
|
||||
if (!desc) continue;
|
||||
|
||||
pfcModel_ptr comp_model = nullptr;
|
||||
try {
|
||||
pfcSession_ptr session = pfcGetCurrentSessionWithCompatibility(pfcC4Compatible);
|
||||
if (session) {
|
||||
comp_model = session->GetModelFromDescr(desc);
|
||||
}
|
||||
} catch (pfcXToolkitError&) {
|
||||
// Model not loaded, skip this component
|
||||
continue;
|
||||
}
|
||||
if (!comp_model) continue;
|
||||
|
||||
// Get file name for component path (like hierarchy analysis)
|
||||
std::string comp_name;
|
||||
try {
|
||||
xstring filename_xstr = desc->GetFileName();
|
||||
std::wstring wfilename = filename_xstr;
|
||||
comp_name = std::string(wfilename.begin(), wfilename.end());
|
||||
} catch (...) {
|
||||
comp_name = GetModelDisplayName(comp_model); // fallback
|
||||
}
|
||||
|
||||
std::string comp_file = GetModelFilePath(comp_model);
|
||||
std::string full_path = GetModelFullPath(comp_model);
|
||||
|
||||
// Prevent duplicate processing of same model
|
||||
if (processed_models.find(comp_file) != processed_models.end()) {
|
||||
continue;
|
||||
}
|
||||
processed_models.insert(comp_file);
|
||||
|
||||
// Check model type filter
|
||||
std::string model_type = GetModelTypeString(comp_model);
|
||||
if (!ShouldIncludeModelType(model_type, request.model_types)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate match score
|
||||
double match_score = CalculateMatchScore(request, comp_name, "component");
|
||||
|
||||
// Build tree path for model hierarchy
|
||||
std::string tree_path;
|
||||
if (parent_path.empty()) {
|
||||
tree_path = comp_name;
|
||||
} else {
|
||||
tree_path = parent_path + "/" + comp_name;
|
||||
}
|
||||
|
||||
if (match_score >= request.similarity_threshold) {
|
||||
SearchResultItem item;
|
||||
item.model_name = comp_name;
|
||||
item.display_name = comp_name;
|
||||
item.full_path = tree_path; // Model tree hierarchy path
|
||||
item.file_path = comp_file; // Physical file path
|
||||
item.model_type = model_type;
|
||||
item.component_path = BuildComponentPath(parent_path, comp_name);
|
||||
item.match_score = match_score;
|
||||
item.match_type = "component";
|
||||
item.is_in_session = true;
|
||||
item.is_assembly = (model_type == "ASSEMBLY");
|
||||
item.match_reason = "Component name match in assembly";
|
||||
item.matched_keywords.push_back(request.query);
|
||||
|
||||
// Add to best results with deduplication by file path
|
||||
// Keep the result with longest hierarchy path
|
||||
std::string key = comp_file;
|
||||
if (best_results.find(key) == best_results.end() ||
|
||||
best_results[key].full_path.length() < item.full_path.length()) {
|
||||
best_results[key] = item;
|
||||
}
|
||||
}
|
||||
|
||||
// If this component is also an assembly, search recursively
|
||||
if (model_type == "ASSEMBLY") {
|
||||
try {
|
||||
pfcAssembly_ptr sub_assembly = pfcAssembly::cast(comp_model);
|
||||
if (sub_assembly) {
|
||||
SearchAssemblyComponentsWithDedup(request, sub_assembly, best_results, processed_models, tree_path);
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
// Sub-assembly search failure doesn't affect other results
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
// Single component processing failure doesn't affect other components
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
// Assembly component search failure doesn't throw exception
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate match score
|
||||
double ModelSearchEngine::CalculateMatchScore(const ModelSearchRequest& request,
|
||||
const std::string& target_name,
|
||||
const std::string& match_type) {
|
||||
if (request.match_mode == "prefix") {
|
||||
return MatchesPrefix(ToLowerCase(target_name), ToLowerCase(request.query)) ? 1.0 : 0.0;
|
||||
}
|
||||
else if (request.match_mode == "contains") {
|
||||
return MatchesContains(ToLowerCase(target_name), ToLowerCase(request.query)) ? 0.8 : 0.0;
|
||||
}
|
||||
else if (request.match_mode == "fuzzy") {
|
||||
double score = CalculateFuzzyMatch(ToLowerCase(request.query), ToLowerCase(target_name));
|
||||
|
||||
// Adjust score based on match type
|
||||
if (match_type == "name") {
|
||||
score *= 1.0; // Name match no adjustment
|
||||
} else if (match_type == "component") {
|
||||
score *= 0.9; // Component match slightly lower score
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Sort and filter results
|
||||
void ModelSearchEngine::SortAndFilterResults(std::vector<SearchResultItem>& results,
|
||||
const ModelSearchRequest& request) {
|
||||
// Sort by match score in descending order
|
||||
std::sort(results.begin(), results.end(),
|
||||
[](const SearchResultItem& a, const SearchResultItem& b) {
|
||||
if (std::abs(a.match_score - b.match_score) < 0.001) {
|
||||
// Same score, prioritize name matches
|
||||
if (a.match_type != b.match_type) {
|
||||
if (a.match_type == "name") return true;
|
||||
if (b.match_type == "name") return false;
|
||||
}
|
||||
// Then sort by name alphabetically
|
||||
return a.model_name < b.model_name;
|
||||
}
|
||||
return a.match_score > b.match_score;
|
||||
});
|
||||
}
|
||||
|
||||
// Fuzzy matching algorithm implementation
|
||||
double ModelSearchEngine::CalculateFuzzyMatch(const std::string& query, const std::string& target) {
|
||||
if (query.empty() || target.empty()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if (query == target) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Prefix match
|
||||
if (target.find(query) == 0) {
|
||||
return 0.95;
|
||||
}
|
||||
|
||||
// Contains match
|
||||
if (target.find(query) != std::string::npos) {
|
||||
return 0.8;
|
||||
}
|
||||
|
||||
// Edit distance match
|
||||
double edit_distance = CalculateEditDistance(query, target);
|
||||
size_t max_len = (query.length() > target.length()) ? query.length() : target.length();
|
||||
|
||||
if (max_len == 0) return 1.0;
|
||||
|
||||
double similarity = 1.0 - (edit_distance / max_len);
|
||||
return (similarity > 0.0) ? similarity : 0.0;
|
||||
}
|
||||
|
||||
// String matching tools implementation
|
||||
bool ModelSearchEngine::MatchesPrefix(const std::string& text, const std::string& prefix) {
|
||||
return text.size() >= prefix.size() && text.substr(0, prefix.size()) == prefix;
|
||||
}
|
||||
|
||||
bool ModelSearchEngine::MatchesContains(const std::string& text, const std::string& substring) {
|
||||
return text.find(substring) != std::string::npos;
|
||||
}
|
||||
|
||||
double ModelSearchEngine::CalculateEditDistance(const std::string& s1, const std::string& s2) {
|
||||
const size_t len1 = s1.size();
|
||||
const size_t len2 = s2.size();
|
||||
|
||||
std::vector<std::vector<int>> dp(len1 + 1, std::vector<int>(len2 + 1));
|
||||
|
||||
for (size_t i = 0; i <= len1; ++i) dp[i][0] = static_cast<int>(i);
|
||||
for (size_t j = 0; j <= len2; ++j) dp[0][j] = static_cast<int>(j);
|
||||
|
||||
for (size_t i = 1; i <= len1; ++i) {
|
||||
for (size_t j = 1; j <= len2; ++j) {
|
||||
if (s1[i-1] == s2[j-1]) {
|
||||
dp[i][j] = dp[i-1][j-1];
|
||||
} else {
|
||||
int a = dp[i-1][j];
|
||||
int b = dp[i][j-1];
|
||||
int c = dp[i-1][j-1];
|
||||
dp[i][j] = 1 + ((a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<double>(dp[len1][len2]);
|
||||
}
|
||||
|
||||
// Helper methods implementation
|
||||
std::string ModelSearchEngine::GetModelTypeString(pfcModel_ptr model) {
|
||||
if (!model) return "UNKNOWN";
|
||||
|
||||
pfcModelType type = model->GetType();
|
||||
switch (type) {
|
||||
case pfcModelType::pfcMDL_PART: return "PART";
|
||||
case pfcModelType::pfcMDL_ASSEMBLY: return "ASSEMBLY";
|
||||
case pfcModelType::pfcMDL_DRAWING: return "DRAWING";
|
||||
default: return "OTHER";
|
||||
}
|
||||
}
|
||||
|
||||
std::string ModelSearchEngine::GetModelDisplayName(pfcModel_ptr model) {
|
||||
if (!model) return "";
|
||||
|
||||
try {
|
||||
xstring name = model->GetInstanceName();
|
||||
std::wstring wname = name;
|
||||
return std::string(wname.begin(), wname.end());
|
||||
}
|
||||
catch (...) {
|
||||
try {
|
||||
xstring filename = model->GetOrigin();
|
||||
std::wstring wfilename = filename;
|
||||
std::string full_name(wfilename.begin(), wfilename.end());
|
||||
|
||||
// Extract filename part
|
||||
size_t last_slash = full_name.find_last_of("\\/");
|
||||
if (last_slash != std::string::npos) {
|
||||
return full_name.substr(last_slash + 1);
|
||||
}
|
||||
return full_name;
|
||||
}
|
||||
catch (...) {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ModelSearchEngine::GetModelFilePath(pfcModel_ptr model) {
|
||||
if (!model) return "";
|
||||
|
||||
try {
|
||||
xstring origin = model->GetOrigin();
|
||||
std::wstring worigin = origin;
|
||||
return std::string(worigin.begin(), worigin.end());
|
||||
}
|
||||
catch (...) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
std::string ModelSearchEngine::GetModelFullPath(pfcModel_ptr model) {
|
||||
if (!model) return "";
|
||||
|
||||
try {
|
||||
// Try GetFullName first (includes full path)
|
||||
xstring full_name = model->GetFullName();
|
||||
std::wstring wfull_name = full_name;
|
||||
return std::string(wfull_name.begin(), wfull_name.end());
|
||||
}
|
||||
catch (...) {
|
||||
// Fallback to GetOrigin if GetFullName fails
|
||||
try {
|
||||
xstring origin = model->GetOrigin();
|
||||
std::wstring worigin = origin;
|
||||
return std::string(worigin.begin(), worigin.end());
|
||||
}
|
||||
catch (...) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ModelSearchEngine::ShouldIncludeModelType(const std::string& model_type,
|
||||
const std::vector<std::string>& type_filters) {
|
||||
if (type_filters.empty()) {
|
||||
return true; // No filter, include all types
|
||||
}
|
||||
|
||||
for (const auto& filter : type_filters) {
|
||||
if (filter == "MDL_PART" && model_type == "PART") return true;
|
||||
if (filter == "MDL_ASSEMBLY" && model_type == "ASSEMBLY") return true;
|
||||
if (filter == "MDL_DRAWING" && model_type == "DRAWING") return true;
|
||||
if (filter == model_type) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// String processing tools implementation
|
||||
std::string ModelSearchEngine::ToLowerCase(const std::string& str) {
|
||||
std::string result = str;
|
||||
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> ModelSearchEngine::SplitString(const std::string& str, char delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
std::stringstream ss(str);
|
||||
std::string token;
|
||||
|
||||
while (std::getline(ss, token, delimiter)) {
|
||||
tokens.push_back(TrimString(token));
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
std::string ModelSearchEngine::TrimString(const std::string& str) {
|
||||
size_t start = str.find_first_not_of(" \t\n\r");
|
||||
if (start == std::string::npos) return "";
|
||||
|
||||
size_t end = str.find_last_not_of(" \t\n\r");
|
||||
return str.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
std::string ModelSearchEngine::BuildComponentPath(const std::string& parent_path, const std::string& component_name) {
|
||||
if (parent_path.empty()) {
|
||||
return component_name;
|
||||
}
|
||||
return parent_path + " > " + component_name;
|
||||
}
|
||||
|
||||
// Performance timing methods
|
||||
void ModelSearchEngine::StartTimer() {
|
||||
search_start_time_ = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
std::string ModelSearchEngine::GetElapsedTimeMs() {
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - search_start_time_);
|
||||
return std::to_string(duration.count()) + "ms";
|
||||
}
|
||||
180
ModelSearchEngine.h
Normal file
180
ModelSearchEngine.h
Normal file
@ -0,0 +1,180 @@
|
||||
#pragma once
|
||||
|
||||
// 基础OTK头文件
|
||||
#include <pfcGlobal.h>
|
||||
#include <pfcSession.h>
|
||||
#include <wfcSession.h>
|
||||
#include <wfcGlobal.h>
|
||||
#include <pfcModel.h>
|
||||
#include <pfcExceptions.h>
|
||||
#include <pfcFeature.h>
|
||||
#include <pfcComponentFeat.h>
|
||||
#include <pfcSolid.h>
|
||||
#include <wfcSolid.h>
|
||||
#include <pfcAssembly.h>
|
||||
#include <wfcAssembly.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
|
||||
// 搜索请求结构
|
||||
struct ModelSearchRequest {
|
||||
std::string query; // 搜索关键词
|
||||
std::string match_mode; // 匹配模式: "prefix", "contains", "fuzzy"
|
||||
std::string search_scope; // 搜索范围: "session", "current_model", "all"
|
||||
std::vector<std::string> model_types; // 模型类型过滤: ["MDL_PART", "MDL_ASSEMBLY"]
|
||||
int max_results; // 最大返回结果数
|
||||
double similarity_threshold; // 模糊匹配相似度阈值 (0.0-1.0)
|
||||
bool include_components; // 是否包含装配体组件搜索
|
||||
bool include_features; // 是否包含特征名称搜索
|
||||
|
||||
// 构造函数
|
||||
ModelSearchRequest() :
|
||||
match_mode("contains"),
|
||||
search_scope("session"),
|
||||
max_results(50),
|
||||
similarity_threshold(0.3),
|
||||
include_components(true),
|
||||
include_features(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索结果项结构
|
||||
struct SearchResultItem {
|
||||
std::string model_name; // 模型名称
|
||||
std::string display_name; // 显示名称
|
||||
std::string full_path; // 模型树中的完整路径
|
||||
std::string file_path; // 文件路径 (GetOrigin)
|
||||
std::string model_type; // 模型类型 ("PART", "ASSEMBLY", "DRAWING")
|
||||
std::string component_path; // 组件路径(装配体中的位置)
|
||||
double match_score; // 匹配分数 (0.0-1.0)
|
||||
std::string match_reason; // 匹配原因说明
|
||||
std::string match_type; // 匹配类型 ("name", "component", "feature")
|
||||
bool is_in_session; // 是否在当前会话中
|
||||
bool is_assembly; // 是否是装配体
|
||||
int component_count; // 组件数量(装配体)
|
||||
|
||||
// 扩展信息
|
||||
std::string file_size; // 文件大小
|
||||
std::string last_modified; // 最后修改时间
|
||||
std::vector<std::string> matched_keywords; // 匹配的关键词
|
||||
|
||||
// 构造函数
|
||||
SearchResultItem() :
|
||||
match_score(0.0),
|
||||
is_in_session(false),
|
||||
is_assembly(false),
|
||||
component_count(0)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索结果结构
|
||||
struct ModelSearchResult {
|
||||
bool success; // 搜索是否成功
|
||||
std::vector<SearchResultItem> results; // 搜索结果列表
|
||||
int total_found; // 总找到数量
|
||||
int returned_count; // 返回数量
|
||||
std::string search_time_ms; // 搜索耗时(毫秒)
|
||||
std::string error_message; // 错误信息
|
||||
|
||||
// 搜索统计
|
||||
struct SearchStats {
|
||||
int session_models_count; // 会话模型数量
|
||||
int components_searched; // 搜索的组件数量
|
||||
int features_searched; // 搜索的特征数量
|
||||
std::string search_scope_used; // 实际使用的搜索范围
|
||||
} stats;
|
||||
|
||||
// 构造函数
|
||||
ModelSearchResult() :
|
||||
success(false),
|
||||
total_found(0),
|
||||
returned_count(0)
|
||||
{
|
||||
stats.session_models_count = 0;
|
||||
stats.components_searched = 0;
|
||||
stats.features_searched = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 模型搜索引擎类
|
||||
class ModelSearchEngine {
|
||||
public:
|
||||
// 单例模式
|
||||
static ModelSearchEngine& Instance();
|
||||
|
||||
// 主要搜索接口
|
||||
ModelSearchResult SearchModels(const ModelSearchRequest& request);
|
||||
|
||||
// 模糊匹配算法
|
||||
static double CalculateFuzzyMatch(const std::string& query, const std::string& target);
|
||||
|
||||
// 字符串匹配工具
|
||||
static bool MatchesPrefix(const std::string& text, const std::string& prefix);
|
||||
static bool MatchesContains(const std::string& text, const std::string& substring);
|
||||
static double CalculateEditDistance(const std::string& s1, const std::string& s2);
|
||||
|
||||
private:
|
||||
// 构造函数(私有)
|
||||
ModelSearchEngine() = default;
|
||||
~ModelSearchEngine() = default;
|
||||
|
||||
// 删除拷贝构造
|
||||
ModelSearchEngine(const ModelSearchEngine&) = delete;
|
||||
ModelSearchEngine& operator=(const ModelSearchEngine&) = delete;
|
||||
|
||||
// 核心搜索方法
|
||||
void SearchSessionModels(const ModelSearchRequest& request, std::vector<SearchResultItem>& results);
|
||||
void SearchCurrentModel(const ModelSearchRequest& request, std::vector<SearchResultItem>& results);
|
||||
void SearchFromRootAssemblies(const ModelSearchRequest& request, std::vector<SearchResultItem>& results);
|
||||
void SearchAssemblyComponents(const ModelSearchRequest& request,
|
||||
pfcAssembly_ptr assembly,
|
||||
std::vector<SearchResultItem>& results,
|
||||
std::set<std::string>& processed_models,
|
||||
const std::string& parent_path = "");
|
||||
void SearchAssemblyComponentsWithDedup(const ModelSearchRequest& request,
|
||||
pfcAssembly_ptr assembly,
|
||||
std::map<std::string, SearchResultItem>& best_results,
|
||||
std::set<std::string>& processed_models,
|
||||
const std::string& parent_path = "");
|
||||
|
||||
// 匹配评分方法
|
||||
double CalculateMatchScore(const ModelSearchRequest& request,
|
||||
const std::string& target_name,
|
||||
const std::string& match_type);
|
||||
|
||||
// 结果处理方法
|
||||
void SortAndFilterResults(std::vector<SearchResultItem>& results,
|
||||
const ModelSearchRequest& request);
|
||||
|
||||
// 辅助方法
|
||||
std::string GetModelTypeString(pfcModel_ptr model);
|
||||
std::string GetModelDisplayName(pfcModel_ptr model);
|
||||
std::string GetModelFilePath(pfcModel_ptr model);
|
||||
std::string GetModelFullPath(pfcModel_ptr model);
|
||||
bool ShouldIncludeModelType(const std::string& model_type,
|
||||
const std::vector<std::string>& type_filters);
|
||||
|
||||
// 字符串处理工具
|
||||
static std::string ToLowerCase(const std::string& str);
|
||||
static std::vector<std::string> SplitString(const std::string& str, char delimiter);
|
||||
static std::string TrimString(const std::string& str);
|
||||
|
||||
// 组件路径构建
|
||||
std::string BuildComponentPath(const std::string& parent_path, const std::string& component_name);
|
||||
|
||||
// 去重处理
|
||||
std::set<std::string> processed_models_;
|
||||
|
||||
// 性能计时
|
||||
std::chrono::high_resolution_clock::time_point search_start_time_;
|
||||
void StartTimer();
|
||||
std::string GetElapsedTimeMs();
|
||||
};
|
||||
379
ModelSearchHandler.cpp
Normal file
379
ModelSearchHandler.cpp
Normal file
@ -0,0 +1,379 @@
|
||||
#include "pch.h"
|
||||
#include "ModelSearchHandler.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
// Main HTTP request handler
|
||||
HttpResponse ModelSearchHandler::HandleModelSearchRequest(const HttpRequest& request) {
|
||||
HttpResponse response;
|
||||
|
||||
// Validate request method
|
||||
if (request.method != "POST") {
|
||||
return FormatErrorResponse(405, "Method not allowed. Use POST.");
|
||||
}
|
||||
|
||||
// Validate request
|
||||
std::string error_message;
|
||||
if (!ValidateHttpRequest(request, error_message)) {
|
||||
return FormatErrorResponse(400, error_message);
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse search request
|
||||
ModelSearchRequest search_request = ParseSearchRequest(request.body);
|
||||
|
||||
// Execute search
|
||||
ModelSearchResult search_result = ModelSearchEngine::Instance().SearchModels(search_request);
|
||||
|
||||
// Format response
|
||||
if (search_result.success) {
|
||||
response = FormatSuccessResponse(search_result);
|
||||
} else {
|
||||
response = FormatErrorResponse(500, search_result.error_message);
|
||||
}
|
||||
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
response = FormatErrorResponse(500, "Request processing error: " + std::string(e.what()));
|
||||
}
|
||||
catch (...) {
|
||||
response = FormatErrorResponse(500, "Unknown error during model search");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Request validation method
|
||||
bool ModelSearchHandler::ValidateHttpRequest(const HttpRequest& request, std::string& error_message) {
|
||||
if (request.body.empty()) {
|
||||
error_message = "Request body cannot be empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check basic JSON format
|
||||
if (request.body.find("{") == std::string::npos || request.body.find("}") == std::string::npos) {
|
||||
error_message = "Invalid JSON format in request body";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse search request
|
||||
ModelSearchRequest ModelSearchHandler::ParseSearchRequest(const std::string& json_body) {
|
||||
ModelSearchRequest request;
|
||||
|
||||
// Parse required parameters
|
||||
request.query = Trim(ExtractJsonValue(json_body, "query"));
|
||||
if (request.query.empty()) {
|
||||
request.query = Trim(ExtractJsonValue(json_body, "search_query"));
|
||||
}
|
||||
|
||||
// Parse optional parameters
|
||||
std::string match_mode = ExtractJsonValue(json_body, "match_mode");
|
||||
if (!match_mode.empty()) {
|
||||
request.match_mode = match_mode;
|
||||
}
|
||||
|
||||
std::string search_scope = ExtractJsonValue(json_body, "search_scope");
|
||||
if (!search_scope.empty()) {
|
||||
request.search_scope = search_scope;
|
||||
}
|
||||
|
||||
// Parse model type filter
|
||||
std::vector<std::string> model_types = ExtractJsonArrayValue(json_body, "model_types");
|
||||
if (!model_types.empty()) {
|
||||
request.model_types = model_types;
|
||||
}
|
||||
|
||||
// Parse numeric parameters
|
||||
int max_results = ExtractJsonIntValue(json_body, "max_results", 0);
|
||||
if (max_results > 0) {
|
||||
request.max_results = max_results;
|
||||
}
|
||||
|
||||
double similarity_threshold = ExtractJsonDoubleValue(json_body, "similarity_threshold", -1.0);
|
||||
if (similarity_threshold >= 0.0 && similarity_threshold <= 1.0) {
|
||||
request.similarity_threshold = similarity_threshold;
|
||||
}
|
||||
|
||||
// Parse boolean parameters
|
||||
request.include_components = ExtractJsonBoolValue(json_body, "include_components", request.include_components);
|
||||
request.include_features = ExtractJsonBoolValue(json_body, "include_features", request.include_features);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
// Format success response
|
||||
HttpResponse ModelSearchHandler::FormatSuccessResponse(const ModelSearchResult& result) {
|
||||
HttpResponse response;
|
||||
response.status_code = 200;
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{"
|
||||
<< "\"success\": true,"
|
||||
<< "\"data\": {"
|
||||
<< "\"results\": [";
|
||||
|
||||
// Build search results list
|
||||
bool first_item = true;
|
||||
for (const auto& item : result.results) {
|
||||
if (!first_item) json << ",";
|
||||
first_item = false;
|
||||
|
||||
json << "{"
|
||||
<< "\"model_name\": \"" << EscapeJsonString(item.model_name) << "\","
|
||||
<< "\"display_name\": \"" << EscapeJsonString(item.display_name) << "\","
|
||||
<< "\"full_path\": \"" << EscapeJsonString(item.full_path) << "\","
|
||||
<< "\"model_type\": \"" << EscapeJsonString(item.model_type) << "\","
|
||||
<< "\"component_path\": \"" << EscapeJsonString(item.component_path) << "\","
|
||||
<< "\"match_score\": " << item.match_score << ","
|
||||
<< "\"match_reason\": \"" << EscapeJsonString(item.match_reason) << "\","
|
||||
<< "\"match_type\": \"" << EscapeJsonString(item.match_type) << "\","
|
||||
<< "\"is_in_session\": " << (item.is_in_session ? "true" : "false") << ","
|
||||
<< "\"is_assembly\": " << (item.is_assembly ? "true" : "false") << ","
|
||||
<< "\"component_count\": " << item.component_count << ","
|
||||
<< "\"file_size\": \"" << EscapeJsonString(item.file_size) << "\","
|
||||
<< "\"last_modified\": \"" << EscapeJsonString(item.last_modified) << "\","
|
||||
<< "\"matched_keywords\": [";
|
||||
|
||||
// Build matched keywords list
|
||||
bool first_keyword = true;
|
||||
for (const auto& keyword : item.matched_keywords) {
|
||||
if (!first_keyword) json << ",";
|
||||
first_keyword = false;
|
||||
json << "\"" << EscapeJsonString(keyword) << "\"";
|
||||
}
|
||||
|
||||
json << "]"
|
||||
<< "}";
|
||||
}
|
||||
|
||||
json << "],"
|
||||
<< "\"total_found\": " << result.total_found << ","
|
||||
<< "\"returned_count\": " << result.returned_count << ","
|
||||
<< "\"search_time_ms\": \"" << EscapeJsonString(result.search_time_ms) << "\","
|
||||
<< "\"stats\": {"
|
||||
<< "\"session_models_count\": " << result.stats.session_models_count << ","
|
||||
<< "\"components_searched\": " << result.stats.components_searched << ","
|
||||
<< "\"features_searched\": " << result.stats.features_searched << ","
|
||||
<< "\"search_scope_used\": \"" << EscapeJsonString(result.stats.search_scope_used) << "\""
|
||||
<< "}"
|
||||
<< "},"
|
||||
<< "\"error\": null"
|
||||
<< "}";
|
||||
|
||||
response.body = json.str();
|
||||
return response;
|
||||
}
|
||||
|
||||
// Format error response
|
||||
HttpResponse ModelSearchHandler::FormatErrorResponse(int status_code, const std::string& error_message) {
|
||||
HttpResponse response;
|
||||
response.status_code = status_code;
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{"
|
||||
<< "\"success\": false,"
|
||||
<< "\"data\": null,"
|
||||
<< "\"error\": \"" << EscapeJsonString(error_message) << "\""
|
||||
<< "}";
|
||||
|
||||
response.body = json.str();
|
||||
return response;
|
||||
}
|
||||
|
||||
// JSON parsing helper methods
|
||||
std::string ModelSearchHandler::ExtractJsonValue(const std::string& json, const std::string& key) {
|
||||
// Find key-value pair "key": "value" or "key":"value"
|
||||
std::string key_pattern = "\"" + key + "\"";
|
||||
size_t key_pos = json.find(key_pattern);
|
||||
|
||||
if (key_pos != std::string::npos) {
|
||||
// Find colon
|
||||
size_t colon_pos = json.find(":", key_pos);
|
||||
if (colon_pos != std::string::npos) {
|
||||
// Skip spaces to find value start
|
||||
size_t value_start = colon_pos + 1;
|
||||
while (value_start < json.length() &&
|
||||
(json[value_start] == ' ' || json[value_start] == '\t' ||
|
||||
json[value_start] == '\n' || json[value_start] == '\r')) {
|
||||
value_start++;
|
||||
}
|
||||
|
||||
// Check if it's a string value (starts with double quote)
|
||||
if (value_start < json.length() && json[value_start] == '"') {
|
||||
value_start++; // Skip opening quote
|
||||
size_t value_end = json.find('"', value_start);
|
||||
if (value_end != std::string::npos) {
|
||||
return json.substr(value_start, value_end - value_start);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Non-string value, find until comma, brace or string end
|
||||
size_t value_end = value_start;
|
||||
while (value_end < json.length() &&
|
||||
json[value_end] != ',' && json[value_end] != '}' &&
|
||||
json[value_end] != ']' && json[value_end] != '\n') {
|
||||
value_end++;
|
||||
}
|
||||
|
||||
std::string value = Trim(json.substr(value_start, value_end - value_start));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool ModelSearchHandler::ExtractJsonBoolValue(const std::string& json, const std::string& key, bool default_value) {
|
||||
std::string value = ExtractJsonValue(json, key);
|
||||
if (value.empty()) {
|
||||
return default_value;
|
||||
}
|
||||
|
||||
// Convert to lowercase for comparison
|
||||
std::transform(value.begin(), value.end(), value.begin(), ::tolower);
|
||||
|
||||
return (value == "true" || value == "1");
|
||||
}
|
||||
|
||||
int ModelSearchHandler::ExtractJsonIntValue(const std::string& json, const std::string& key, int default_value) {
|
||||
std::string value = ExtractJsonValue(json, key);
|
||||
if (value.empty()) {
|
||||
return default_value;
|
||||
}
|
||||
|
||||
try {
|
||||
return std::stoi(value);
|
||||
}
|
||||
catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
|
||||
double ModelSearchHandler::ExtractJsonDoubleValue(const std::string& json, const std::string& key, double default_value) {
|
||||
std::string value = ExtractJsonValue(json, key);
|
||||
if (value.empty()) {
|
||||
return default_value;
|
||||
}
|
||||
|
||||
try {
|
||||
return std::stod(value);
|
||||
}
|
||||
catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ModelSearchHandler::ExtractJsonArrayValue(const std::string& json, const std::string& key) {
|
||||
std::vector<std::string> result;
|
||||
|
||||
// Find key-value pair "key": [...]
|
||||
std::string key_pattern = "\"" + key + "\"";
|
||||
size_t key_pos = json.find(key_pattern);
|
||||
|
||||
if (key_pos != std::string::npos) {
|
||||
// Find colon
|
||||
size_t colon_pos = json.find(":", key_pos);
|
||||
if (colon_pos != std::string::npos) {
|
||||
// Skip spaces to find array start
|
||||
size_t array_start = colon_pos + 1;
|
||||
while (array_start < json.length() &&
|
||||
(json[array_start] == ' ' || json[array_start] == '\t' ||
|
||||
json[array_start] == '\n' || json[array_start] == '\r')) {
|
||||
array_start++;
|
||||
}
|
||||
|
||||
// Check if it's an array (starts with [)
|
||||
if (array_start < json.length() && json[array_start] == '[') {
|
||||
size_t array_end = json.find(']', array_start);
|
||||
if (array_end != std::string::npos) {
|
||||
std::string array_content = json.substr(array_start + 1, array_end - array_start - 1);
|
||||
|
||||
// Parse array elements
|
||||
size_t pos = 0;
|
||||
while (pos < array_content.length()) {
|
||||
// Skip spaces
|
||||
while (pos < array_content.length() &&
|
||||
(array_content[pos] == ' ' || array_content[pos] == '\t' ||
|
||||
array_content[pos] == '\n' || array_content[pos] == '\r')) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos >= array_content.length()) break;
|
||||
|
||||
// Find string value
|
||||
if (array_content[pos] == '"') {
|
||||
pos++; // Skip opening quote
|
||||
size_t end_quote = array_content.find('"', pos);
|
||||
if (end_quote != std::string::npos) {
|
||||
result.push_back(array_content.substr(pos, end_quote - pos));
|
||||
pos = end_quote + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Find next comma
|
||||
size_t next_comma = array_content.find(',', pos);
|
||||
if (next_comma != std::string::npos) {
|
||||
pos = next_comma + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// JSON escape method
|
||||
std::string ModelSearchHandler::EscapeJsonString(const std::string& input) {
|
||||
std::string escaped;
|
||||
escaped.reserve(input.length() * 2);
|
||||
|
||||
for (char c : input) {
|
||||
switch (c) {
|
||||
case '"': escaped += "\\\""; break;
|
||||
case '\\': escaped += "\\\\"; break;
|
||||
case '\b': escaped += "\\b"; break;
|
||||
case '\f': escaped += "\\f"; break;
|
||||
case '\n': escaped += "\\n"; break;
|
||||
case '\r': escaped += "\\r"; break;
|
||||
case '\t': escaped += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
// Control character escape to Unicode
|
||||
char buffer[7];
|
||||
snprintf(buffer, sizeof(buffer), "\\u%04x", static_cast<unsigned char>(c));
|
||||
escaped += buffer;
|
||||
} else {
|
||||
escaped += c;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
// Helper utility methods
|
||||
std::string ModelSearchHandler::Trim(const std::string& str) {
|
||||
size_t start = str.find_first_not_of(" \t\n\r");
|
||||
if (start == std::string::npos) return "";
|
||||
|
||||
size_t end = str.find_last_not_of(" \t\n\r");
|
||||
return str.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
bool ModelSearchHandler::StartsWith(const std::string& str, const std::string& prefix) {
|
||||
return str.size() >= prefix.size() && str.substr(0, prefix.size()) == prefix;
|
||||
}
|
||||
|
||||
bool ModelSearchHandler::EndsWith(const std::string& str, const std::string& suffix) {
|
||||
return str.size() >= suffix.size() &&
|
||||
str.substr(str.size() - suffix.size()) == suffix;
|
||||
}
|
||||
38
ModelSearchHandler.h
Normal file
38
ModelSearchHandler.h
Normal file
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModelSearchEngine.h"
|
||||
#include "HttpServer.h"
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
|
||||
class ModelSearchHandler {
|
||||
public:
|
||||
// 主要HTTP处理方法
|
||||
static HttpResponse HandleModelSearchRequest(const HttpRequest& request);
|
||||
|
||||
private:
|
||||
// JSON解析辅助方法
|
||||
static std::string ExtractJsonValue(const std::string& json, const std::string& key);
|
||||
static bool ExtractJsonBoolValue(const std::string& json, const std::string& key, bool default_value = false);
|
||||
static int ExtractJsonIntValue(const std::string& json, const std::string& key, int default_value = 0);
|
||||
static double ExtractJsonDoubleValue(const std::string& json, const std::string& key, double default_value = 0.0);
|
||||
static std::vector<std::string> ExtractJsonArrayValue(const std::string& json, const std::string& key);
|
||||
|
||||
// JSON转义辅助方法
|
||||
static std::string EscapeJsonString(const std::string& input);
|
||||
|
||||
// 请求验证方法
|
||||
static bool ValidateHttpRequest(const HttpRequest& request, std::string& error_message);
|
||||
|
||||
// 参数解析方法
|
||||
static ModelSearchRequest ParseSearchRequest(const std::string& json_body);
|
||||
|
||||
// 响应格式化方法
|
||||
static HttpResponse FormatSuccessResponse(const ModelSearchResult& result);
|
||||
static HttpResponse FormatErrorResponse(int status_code, const std::string& error_message);
|
||||
|
||||
// 辅助工具方法
|
||||
static std::string Trim(const std::string& str);
|
||||
static bool StartsWith(const std::string& str, const std::string& prefix);
|
||||
static bool EndsWith(const std::string& str, const std::string& suffix);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user