CreoOtkPluging/ModelSearchEngine.cpp
sladro 00ddd48082 优化模型搜索返回结果 - 移除冗余字段
移除不必要的字段以简化API响应:
• 删除component_path字段,避免重复路径信息
• 删除display_name字段,model_name已足够
• 删除file_size和last_modified字段,减少响应体积
• 保留核心搜索功能,优化用户体验

JSON响应更简洁,只包含必要的搜索结果信息。

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 18:47:34 +08:00

789 lines
30 KiB
C++

#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.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.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.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.full_path = tree_path; // Model tree hierarchy path
item.file_path = comp_file; // Physical file path
item.model_type = model_type;
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.full_path = tree_path; // Model tree hierarchy path
item.file_path = comp_file; // Physical file path
item.model_type = model_type;
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";
}