From 1154536ec0639c548b7935e94fd50b522a465938 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Tue, 14 Jul 2026 14:11:33 +0800 Subject: [PATCH] feat: enhance semantic search with multiple paths and pattern filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Added support for multiple custom paths in search rules by accepting QVariantList for custom_path field 2. Implemented exclude_patterns to filter search results by file name patterns 3. Added ruleMetadataById method to find rules without knowing their group 4. Improved path expansion logic in ActionExtractor and LocationExtractor 5. Enhanced semantic searcher to apply exclude_patterns when processing results Log: Added support for multiple search paths and result filtering by file patterns Influence: 1. Test search rules with multiple custom paths 2. Verify exclude_patterns filters correct files from results 3. Check backward compatibility with existing single-path rules 4. Validate rule lookups work correctly with new ruleMetadataById 5. Test performance impact with multiple paths and pattern filtering feat: 增强语义搜索支持多路径和模式过滤 1. 在搜索规则中添加对多个自定义路径的支持,支持custom_path字段使用 QVariantList 2. 实现exclude_patterns功能,按文件名模式过滤搜索结果 3. 添加ruleMetadataById方法,无需知道规则组即可查找规则 4. 改进ActionExtractor和LocationExtractor中的路径扩展逻辑 5. 增强语义搜索器在处理结果时应用exclude_patterns Log: 新增支持多个搜索路径及按文件模式过滤结果的功能 Influence: 1. 测试使用多个自定义路径的搜索规则 2. 验证exclude_patterns能正确过滤结果文件 3. 检查与现有单一路径规则的向后兼容性 4. 验证新的ruleMetadataById方法查找规则是否正确 5. 测试多路径和模式过滤对性能的影响 --- .../semantic/extractors/actionextractor.cpp | 30 +++-- .../semantic/extractors/locationextractor.cpp | 25 +++- .../semantic/rules/zh_CN/location_rules.json | 6 +- .../semantic/semanticruleengine.cpp | 12 ++ .../semantic/semanticruleengine.h | 11 ++ .../semantic/semanticsearcher.cpp | 112 +++++++++++------- .../semantic/semanticsearcher_p.h | 4 + 7 files changed, 139 insertions(+), 61 deletions(-) diff --git a/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp b/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp index 1875ccc2..943101de 100644 --- a/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/extractors/actionextractor.cpp @@ -10,6 +10,7 @@ #include #include #include +#include DFM_SEARCH_BEGIN_NS @@ -137,16 +138,29 @@ QStringList ActionExtractor::resolveImReceivedPaths() const } const QString xdgType = meta.value("xdg_type").toString(); - const QString customSubdir = meta.value("custom_path").toString(); + const QVariant customPathVar = meta.value("custom_path"); QStringList resolved; - if (!customSubdir.isEmpty()) { - resolved = LocationExtractor::expandCustomPath( - QDir::homePath(), customSubdir); - } else if (!xdgType.isEmpty()) { - const QString path = LocationExtractor::resolveXdgPath(xdgType); - if (!path.isEmpty()) { - resolved = { path }; + if (customPathVar.userType() == QMetaType::QVariantList) { + for (const QVariant &pathVar : customPathVar.toList()) { + const QString relPath = pathVar.toString(); + if (relPath.isEmpty()) continue; + const QStringList expanded = LocationExtractor::expandCustomPath( + QDir::homePath(), relPath); + for (const QString &p : expanded) { + if (!resolved.contains(p)) resolved.append(p); + } + } + } else { + const QString customSubdir = customPathVar.toString(); + if (!customSubdir.isEmpty()) { + resolved = LocationExtractor::expandCustomPath( + QDir::homePath(), customSubdir); + } else if (!xdgType.isEmpty()) { + const QString path = LocationExtractor::resolveXdgPath(xdgType); + if (!path.isEmpty()) { + resolved = { path }; + } } } diff --git a/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp b/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp index fb20be4f..e677a99b 100644 --- a/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/extractors/locationextractor.cpp @@ -9,6 +9,7 @@ #include #include #include +#include DFM_SEARCH_BEGIN_NS @@ -34,7 +35,7 @@ void LocationExtractor::extract(const QString &input, ParsedIntent &intent) const QVariantMap metadata = m_engine->ruleMetadata("location", ruleIds[i]); const QString xdgType = metadata.value("xdg_type").toString(); - const QString customSubdir = metadata.value("custom_path").toString(); + const QVariant customPathVar = metadata.value("custom_path"); const bool includeHidden = metadata.value("include_hidden", false).toBool(); // Always record the consumed span when a rule matches, @@ -47,12 +48,24 @@ void LocationExtractor::extract(const QString &input, ParsedIntent &intent) intent.consumedSpans().append(span); QStringList resolvedPaths; - if (!customSubdir.isEmpty()) { - resolvedPaths = expandCustomPath(QDir::homePath(), customSubdir); + if (customPathVar.userType() == QMetaType::QVariantList) { + for (const QVariant &pathVar : customPathVar.toList()) { + const QString relPath = pathVar.toString(); + if (relPath.isEmpty()) continue; + const QStringList expanded = expandCustomPath(QDir::homePath(), relPath); + for (const QString &p : expanded) { + if (!resolvedPaths.contains(p)) resolvedPaths.append(p); + } + } } else { - const QString path = resolveXdgPath(xdgType); - if (!path.isEmpty()) { - resolvedPaths = { path }; + const QString customSubdir = customPathVar.toString(); + if (!customSubdir.isEmpty()) { + resolvedPaths = expandCustomPath(QDir::homePath(), customSubdir); + } else { + const QString path = resolveXdgPath(xdgType); + if (!path.isEmpty()) { + resolvedPaths = { path }; + } } } if (resolvedPaths.isEmpty()) { diff --git a/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json b/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json index 123dc24c..cbfb3edc 100644 --- a/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json +++ b/src/dfm-search/dfm-search-lib/semantic/rules/zh_CN/location_rules.json @@ -79,7 +79,11 @@ "enabled": true, "priority": 180, "metadata": { - "custom_path": "Documents/xwechat_files/*/msg/file", + "custom_path": [ + "Documents/xwechat_files/*/msg/file", + "Documents/xwechat_files/*/msg/video" + ], + "exclude_patterns": ["*_thumb.jpg"], "include_hidden": false, "im_received": true } diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp index 8980e5a6..fecca14a 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.cpp @@ -178,6 +178,18 @@ QVariantMap SemanticRuleEngine::ruleMetadata(const QString &group, const QString return {}; } +QVariantMap SemanticRuleEngine::ruleMetadataById(const QString &ruleId) const +{ + for (const RuleGroup &group : m_groups) { + for (const Rule &rule : group.rules) { + if (rule.id == ruleId) { + return rule.metadata; + } + } + } + return {}; +} + bool SemanticRuleEngine::hasGroup(const QString &group) const { return m_groups.contains(group); diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h index 6f172e6b..34ccb8d9 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h +++ b/src/dfm-search/dfm-search-lib/semantic/semanticruleengine.h @@ -86,6 +86,17 @@ class SemanticRuleEngine : public QObject */ QVariantMap ruleMetadata(const QString &group, const QString &ruleId) const; + /** + * @brief Get a rule's metadata by rule ID, searching across all groups. + * @param ruleId The rule ID to look up + * @return The metadata map, or empty if not found + * + * Unlike ruleMetadata(group, ruleId), this searches all groups. + * Useful when the caller has a ruleId from consumedSpans but doesn't + * know which group it belongs to. + */ + QVariantMap ruleMetadataById(const QString &ruleId) const; + /** * @brief Get all rule IDs in a group. */ diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp index df511a5e..85e43dc7 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp +++ b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher.cpp @@ -14,7 +14,11 @@ #include #include #include +#include #include +#include + +#include DFM_SEARCH_BEGIN_NS @@ -47,13 +51,13 @@ int countSemanticDimensions(const ParsedIntent &intent) { int count = 0; if (!intent.searchDirectories().isEmpty()) ++count; // location - if (intent.hiddenOnly()) ++count; // hiddenOnly 属性 - if (intent.recentOnly()) ++count; // recentOnly 属性 - if (intent.timeConstraint().isValid()) ++count; // time 约束 - if (intent.sizeConstraint().isValid()) ++count; // size 约束 - if (!intent.fileExtensions().isEmpty()) ++count; // fileType - if (!intent.keywords().isEmpty()) ++count; // keyword (unconsumed text) - if (hasTargetRuleSpan(intent)) ++count; // target 名词 (文件/文件名/文件夹) + if (intent.hiddenOnly()) ++count; // hiddenOnly 属性 + if (intent.recentOnly()) ++count; // recentOnly 属性 + if (intent.timeConstraint().isValid()) ++count; // time 约束 + if (intent.sizeConstraint().isValid()) ++count; // size 约束 + if (!intent.fileExtensions().isEmpty()) ++count; // fileType + if (!intent.keywords().isEmpty()) ++count; // keyword (unconsumed text) + if (hasTargetRuleSpan(intent)) ++count; // target 名词 (文件/文件名/文件夹) // 当 specific filetype 与 general filetype 同时命中(如 "PPT文档"、"pdf文档"), // general 类型词(文档/表格/幻灯片)充当目标名词,贡献额外维度。 @@ -161,48 +165,22 @@ void SemanticSearcherData::doSearch(const QString &naturalLanguage, const QStrin dirs = QStringList { QDir::homePath() }; } - // Step 5: Set up signal/slot handlers - auto onFinished = [this](const SearchResultList &results) { - // Collect and deduplicate results from each engine's final result list - for (const SearchResult &r : results) { - if (!seenPaths.contains(r.path())) { - seenPaths.insert(r.path()); - allResults.append(r); - } - } - - if (pendingFinishCount.fetch_sub(1) == 1) { - // All engines finished - timeoutTimer->stop(); - - // Truncate final deduplicated results to maxResults - if (maxResults > 0 && allResults.size() > maxResults) { - allResults = allResults.mid(0, maxResults); - } - - if (cancelled.load()) { - status.store(SearchStatus::Cancelled); - Q_EMIT q->statusChanged(SearchStatus::Cancelled); - Q_EMIT q->searchCancelled(); - } else { - status.store(SearchStatus::Finished); - Q_EMIT q->statusChanged(SearchStatus::Finished); - Q_EMIT q->searchFinished(allResults); + excludeNamePatterns.clear(); + for (const MatchSpan &span : intent.consumedSpans()) { + const QVariantMap meta = ruleEngine->ruleMetadataById(span.ruleId()); + const QVariant ep = meta.value("exclude_patterns"); + if (ep.userType() == QMetaType::QVariantList) { + for (const auto &v : ep.toList()) { + const QString p = v.toString(); + if (!p.isEmpty() && !excludeNamePatterns.contains(p)) + excludeNamePatterns.append(p); } } - }; + } - // 引擎错误也必须触发计数归零检查,否则上层会一直阻塞到 60s 超时。 - // 校验失败(如 KeywordIsEmpty)会让 GenericSearchEngine::search 在 - // emit errorOccurred 后直接 return,不会 emit searchFinished —— - // 此时必须由 onError 兜底减计数,把错误视为引擎完成。 - auto onError = [this, onFinished](const SearchError &error) { - qWarning() << "Search error:" << error.message(); - // Don't propagate individual engine errors to caller - // The other engines may still produce valid results - // 但仍需减计数,否则 pendingFinishCount 永不到 0 - onFinished({}); - }; + // Step 5: Set up signal/slot handlers + auto onFinished = [this](const SearchResultList &r) { onEngineFinished(r); }; + auto onError = [this](const SearchError &e) { onEngineError(e); }; // Step 6: Clean up any previous search engines pendingFinishCount.store(0); @@ -307,6 +285,48 @@ void SemanticSearcherData::createAndLaunchEngine( engine->search(query); } +void SemanticSearcherData::onEngineFinished(const SearchResultList &results) +{ + for (const SearchResult &r : results) { + if (!excludeNamePatterns.isEmpty()) { + const QString fileName = QFileInfo(r.path()).fileName(); + bool excluded = std::any_of(excludeNamePatterns.cbegin(), excludeNamePatterns.cend(), + [&fileName](const QString &pattern) { + return QDir::match(pattern, fileName); + }); + if (excluded) continue; + } + if (!seenPaths.contains(r.path())) { + seenPaths.insert(r.path()); + allResults.append(r); + } + } + + if (pendingFinishCount.fetch_sub(1) == 1) { + timeoutTimer->stop(); + + if (maxResults > 0 && allResults.size() > maxResults) { + allResults = allResults.mid(0, maxResults); + } + + if (cancelled.load()) { + status.store(SearchStatus::Cancelled); + Q_EMIT q->statusChanged(SearchStatus::Cancelled); + Q_EMIT q->searchCancelled(); + } else { + status.store(SearchStatus::Finished); + Q_EMIT q->statusChanged(SearchStatus::Finished); + Q_EMIT q->searchFinished(allResults); + } + } +} + +void SemanticSearcherData::onEngineError(const SearchError &error) +{ + qWarning() << "Search error:" << error.message(); + onEngineFinished({}); +} + void SemanticSearcherData::doCancel() { cancelled.store(true); diff --git a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h index facdd74b..c70a6c5d 100644 --- a/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h +++ b/src/dfm-search/dfm-search-lib/semantic/semanticsearcher_p.h @@ -62,6 +62,9 @@ class SemanticSearcherData std::function onFinished, std::function onError); + void onEngineFinished(const SearchResultList &results); + void onEngineError(const SearchError &error); + SemanticSearcher *q = nullptr; // State @@ -89,6 +92,7 @@ class SemanticSearcherData bool detailedResultsEnabled = false; int maxResults = 0; // 0 = unlimited QStringList excludedPaths; // directories to exclude from search + QStringList excludeNamePatterns; // glob patterns to filter from results (e.g. *_thumb.jpg) }; DFM_SEARCH_END_NS