diff --git a/autotests/dfm-search-tests/tst_search_output.cpp b/autotests/dfm-search-tests/tst_search_output.cpp index 39107504..8515d3e4 100644 --- a/autotests/dfm-search-tests/tst_search_output.cpp +++ b/autotests/dfm-search-tests/tst_search_output.cpp @@ -127,6 +127,8 @@ private Q_SLOTS: void jsonOutput_contentAndFilenameCharCountContract(); void jsonOutput_ocrAndSemanticCharCountContract(); void textOutput_contentAndSemanticCharCountContract(); + void jsonOutput_filenamePinyinContract(); + void textOutput_filenamePinyinContract(); }; void tst_SearchOutput::previewOutputHelpers_includeCharCount() @@ -298,6 +300,70 @@ void tst_SearchOutput::textOutput_contentAndSemanticCharCountContract() QVERIFY(semanticText.contains("charCount: 30")); } +void tst_SearchOutput::jsonOutput_filenamePinyinContract() +{ + SearchOptions options; + options.setDetailedResultsEnabled(true); + + SearchResult result("/tmp/baogao.txt"); + FileNameResultAPI api(result); + api.setFilename(QString::fromUtf8("报告.txt")); + api.setPinyin("baogao"); + api.setPinyinAcronym("bg"); + + dfmsearch::JsonOutput output(false); + output.setSearchOptions(options); + output.setSearchContext("bg", "/tmp", SearchType::FileName, SearchMethod::Indexed); + const QString jsonText = captureStdout([&]() { + output.outputSearchStarted(); + output.outputSearchFinished({ result }); + }); + const QJsonObject root = QJsonDocument::fromJson(jsonText.toUtf8()).object(); + const QJsonObject item = root.value("results").toArray().first().toObject(); + QCOMPARE(item.value("pinyin").toString(), QString("baogao")); + QCOMPARE(item.value("pinyinAcronym").toString(), QString("bg")); + + // 无拼音字段的结果不应输出空键 + SearchResult plainResult("/tmp/readme.txt"); + FileNameResultAPI plainApi(plainResult); + plainApi.setFilename("readme.txt"); + + dfmsearch::JsonOutput plainOutput(false); + plainOutput.setSearchOptions(options); + plainOutput.setSearchContext("readme", "/tmp", SearchType::FileName, SearchMethod::Indexed); + const QString plainText = captureStdout([&]() { + plainOutput.outputSearchStarted(); + plainOutput.outputSearchFinished({ plainResult }); + }); + const QJsonObject plainRoot = QJsonDocument::fromJson(plainText.toUtf8()).object(); + const QJsonObject plainItem = plainRoot.value("results").toArray().first().toObject(); + QVERIFY(!plainItem.contains("pinyin")); + QVERIFY(!plainItem.contains("pinyinAcronym")); +} + +void tst_SearchOutput::textOutput_filenamePinyinContract() +{ + SearchOptions options; + options.setDetailedResultsEnabled(true); + + SearchResult result("/tmp/baogao.txt"); + FileNameResultAPI api(result); + api.setFilename(QString::fromUtf8("报告.txt")); + api.setFileType("doc"); + api.setPinyin("baogao"); + api.setPinyinAcronym("bg"); + + dfmsearch::TextOutput output; + output.setSearchOptions(options); + output.setVerbose(true); + output.setSearchContext("bg", "/tmp", SearchType::FileName, SearchMethod::Indexed); + const QString text = captureStdout([&]() { + output.outputSearchFinished({ result }); + }); + QVERIFY(text.contains("Pinyin: baogao")); + QVERIFY(text.contains("Pinyin acronym: bg")); +} + QObject *create_tst_SearchOutput() { return new tst_SearchOutput(); diff --git a/autotests/dfm-search-tests/tst_search_utils.cpp b/autotests/dfm-search-tests/tst_search_utils.cpp index c962d449..73ab169e 100644 --- a/autotests/dfm-search-tests/tst_search_utils.cpp +++ b/autotests/dfm-search-tests/tst_search_utils.cpp @@ -16,6 +16,7 @@ #include #include #include +#include using namespace DFMSEARCH; @@ -30,6 +31,7 @@ private Q_SLOTS: void testPinyin(); void testPinyinAcronym(); void testAnythingStatus(); + void testFileNameIndexStatusMapping(); void testFileNameBlacklistMatcher(); void testNGramSearchQuery(); @@ -238,15 +240,16 @@ void tst_SearchUtils::testAnythingStatus() // 测试状态获取 auto status = Global::fileNameIndexStatus(); - // 结果验证 - QVERIFY2(status.has_value(), "Could not retrieve status (file missing/permission error?)"); + if (!status.has_value()) { + QWARN("filename index status unavailable (no local index) - skipping runtime check"); + return; + } - // 定义有效状态列表(小写) + // 新状态映射返回 "scanning"/"updating"/"monitoring"("loading"/"closed" 不再出现) static const QSet validStatuses { - "loading", "scanning", - "monitoring", - "closed" + "updating", + "monitoring" }; // 状态有效性检查 @@ -261,6 +264,89 @@ void tst_SearchUtils::testAnythingStatus() qInfo() << "Test Passed. Current anything status:" << currentStatus; } +void tst_SearchUtils::testFileNameIndexStatusMapping() +{ + struct Case { + QString name; + QByteArray json; + std::optional expectedStatus; + bool expectedReady; + }; + + const QList cases = { + // 首次创建中(Create 启动时 removeIndexStatusFile,最小字段) + { "create-in-progress", + R"({"createInProgress": true})", + QStringLiteral("scanning"), false }, + // 中断后继续创建(版本重建,无 lastUpdateTime) + { "create-resumed", + R"({"createInProgress": true, "version": 1})", + QStringLiteral("scanning"), false }, + // dirty 重启后的恢复 Update 中(索引滞后不可信 → 降级) + { "recovery-update", + R"({"state": "dirty", "createInProgress": false, "updateInProgress": true, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("scanning"), false }, + // 运行中 needsRebuild 触发的 rebuild Update 中(新路径文件未索引 → 降级) + { "rebuild-update", + R"({"state": "dirty", "updateInProgress": true, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("scanning"), false }, + // Update 失败/中断后标志保持(持续降级直到恢复成功) + { "update-failed-persisted", + R"({"state": "dirty", "updateInProgress": true, "needsRebuild": true, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("scanning"), false }, + // 大规模事件积压处理中(backlogExceeded → "updating" → 降级 Realtime) + { "backlog-exceeded", + R"({"state": "dirty", "backlogExceeded": true, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("updating"), false }, + // 崩溃时正在积压(dirty + backlog 残留,启动时保持 → 持续降级直到恢复完成) + { "backlog-crash-residue", + R"({"state": "dirty", "backlogExceeded": true, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("updating"), false }, + // 积压处理完成(backlogExceeded 清除 → 恢复索引搜索) + { "backlog-resolved", + R"({"state": "clean", "backlogExceeded": false, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("monitoring"), true }, + // 普通事件增量(dirty 但无恢复/积压标志,索引可用) + { "incremental-dirty", + R"({"state": "dirty", "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("monitoring"), true }, + // 恢复/重建/积压处理完成 + { "clean", + R"({"state": "clean", "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("monitoring"), true }, + // 从未成功完成过任务(无 lastUpdateTime、无进行中标志) + { "never-completed", + R"({"state": "dirty"})", + QStringLiteral("scanning"), false }, + // 双标志同时置位(createInProgress 优先级最高) + { "both-flags", + R"({"createInProgress": true, "updateInProgress": true, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("scanning"), false }, + // backlog + update 同时置位(updateInProgress 优先级高于 backlogExceeded) + { "update-plus-backlog", + R"({"updateInProgress": true, "backlogExceeded": true, "lastUpdateTime": "2026-09-18T10:00:00"})", + QStringLiteral("scanning"), false }, + // JSON 无效 + { "invalid-json", + "not-json", + std::nullopt, false }, + // 空内容 + { "empty-content", + "", + std::nullopt, false }, + }; + + for (const Case &c : cases) { + const auto actualStatus = Global::fileNameIndexStatusFromJson(c.json); + QVERIFY2(actualStatus == c.expectedStatus, + qPrintable(QString("case '%1': status mismatch, got %2") + .arg(c.name, + actualStatus.has_value() ? actualStatus.value() : QStringLiteral("nullopt")))); + QVERIFY2(Global::fileNameIndexReadyForSearchFromJson(c.json) == c.expectedReady, + qPrintable(QString("case '%1': ready mismatch").arg(c.name))); + } +} + void tst_SearchUtils::doFileNameBlacklistMatchTest(const QString &caseName, const QString &inputPath, const QStringList &blacklistEntries, diff --git a/include/dfm-search/dfm-search/dsearch_global.h b/include/dfm-search/dfm-search/dsearch_global.h index 37a4c582..63e70ebb 100644 --- a/include/dfm-search/dfm-search/dsearch_global.h +++ b/include/dfm-search/dfm-search/dsearch_global.h @@ -148,22 +148,26 @@ bool isFileNameIndexDirectoryAvailable(); /** * @brief Check if the filename index is ready for search operations. - * This function checks both the physical existence of the index and its status to ensure - * it's in the "monitoring" state and ready for search operations. + * Ready when the index physically exists, lastUpdateTime is present in + * index_status.json, and none of createInProgress, updateInProgress, or + * backlogExceeded is set (full build / recovery / rebuild / large event + * backlog in progress means the index is not trustworthy). The dirty/clean + * state is not checked: ordinary incremental updates keep the index searchable. * @return True if the filename index is ready for search, false otherwise. */ bool isFileNameIndexReadyForSearch(); /** - * @brief Returns the current indexing status of the file name database. + * @brief Returns the current indexing status of the file name index, + * mapped from index_status.json (legacy semantics preserved). * * Possible status values: - * - "loading" : Initial loading of existing index - * - "scanning" : Actively scanning filesystem for changes - * - "monitoring" : Scan complete, now watching for filesystem events - * - "closed" : Normal shutdown state (anything terminated properly) + * - std::nullopt : index missing or status unavailable + * - "scanning" : full build / recovery / rebuild in progress (index not trustworthy) + * - "updating" : large event backlog in progress (index severely lagged) + * - "monitoring" : index usable (ordinary incremental updates may still be running) * - * @return QString The current status string (lowercase) + * @return The current status string (lowercase), or nullopt if unavailable */ std::optional fileNameIndexStatus(); diff --git a/include/dfm-search/dfm-search/filenamesearchapi.h b/include/dfm-search/dfm-search/filenamesearchapi.h index 4093f34d..5fe7f2b9 100644 --- a/include/dfm-search/dfm-search/filenamesearchapi.h +++ b/include/dfm-search/dfm-search/filenamesearchapi.h @@ -251,6 +251,32 @@ class FileNameResultAPI */ qint64 fileSizeBytes() const; + // ==================== Pinyin (filename index) ==================== + + /** + * @brief Get the pinyin (full spelling) of the file name + * @return Pinyin string, empty if not stored in the index + */ + QString pinyin() const; + + /** + * @brief Set the pinyin (full spelling) of the file name + * @param pinyin The pinyin string + */ + void setPinyin(const QString &pinyin); + + /** + * @brief Get the pinyin acronym (initials) of the file name + * @return Pinyin acronym string, empty if not stored in the index + */ + QString pinyinAcronym() const; + + /** + * @brief Set the pinyin acronym (initials) of the file name + * @param acronym The pinyin acronym string + */ + void setPinyinAcronym(const QString &acronym); + private: SearchResult &m_result; }; diff --git a/src/dfm-search/dfm-search-client/output/json_output.cpp b/src/dfm-search/dfm-search-client/output/json_output.cpp index e46871e4..c50db243 100644 --- a/src/dfm-search/dfm-search-client/output/json_output.cpp +++ b/src/dfm-search/dfm-search-client/output/json_output.cpp @@ -202,6 +202,16 @@ QJsonValue JsonOutput::resultToJson(const SearchResult &result) obj["isHidden"] = resultAPI.isHidden(); + // 拼音(文件名索引字段,非拼音索引为空) + QString pinyin = resultAPI.pinyin(); + if (!pinyin.isEmpty()) { + obj["pinyin"] = pinyin; + } + QString pinyinAcronym = resultAPI.pinyinAcronym(); + if (!pinyinAcronym.isEmpty()) { + obj["pinyinAcronym"] = pinyinAcronym; + } + // 修改时间(包含时间戳和时间字符串) qint64 modifyTs = resultAPI.modifyTimestamp(); if (modifyTs > 0) { diff --git a/src/dfm-search/dfm-search-client/output/text_output.cpp b/src/dfm-search/dfm-search-client/output/text_output.cpp index 33168268..afb2f8aa 100644 --- a/src/dfm-search/dfm-search-client/output/text_output.cpp +++ b/src/dfm-search/dfm-search-client/output/text_output.cpp @@ -136,6 +136,16 @@ void TextOutput::printSearchResult(const SearchResult &result) // 隐藏状态 std::cout << " Hidden: " << (resultAPI.isHidden() ? "Yes" : "No") << std::endl; + // 拼音(文件名索引字段,非拼音索引为空) + QString pinyin = resultAPI.pinyin(); + if (!pinyin.isEmpty()) { + std::cout << " Pinyin: " << pinyin.toStdString() << std::endl; + } + QString pinyinAcronym = resultAPI.pinyinAcronym(); + if (!pinyinAcronym.isEmpty()) { + std::cout << " Pinyin acronym: " << pinyinAcronym.toStdString() << std::endl; + } + // 修改时间(同时输出时间戳和时间字符串) qint64 modifyTs = resultAPI.modifyTimestamp(); if (modifyTs > 0) { diff --git a/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp b/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp index c47de316..18bbe3e5 100644 --- a/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp +++ b/src/dfm-search/dfm-search-lib/filenamesearch/filenamesearchapi.cpp @@ -180,4 +180,26 @@ qint64 FileNameResultAPI::fileSizeBytes() const return m_result.customAttribute("fileSizeBytes").toLongLong(); } +// ==================== Pinyin (filename index) ==================== + +QString FileNameResultAPI::pinyin() const +{ + return m_result.customAttribute("pinyin").toString(); +} + +void FileNameResultAPI::setPinyin(const QString &pinyin) +{ + m_result.setCustomAttribute("pinyin", pinyin); +} + +QString FileNameResultAPI::pinyinAcronym() const +{ + return m_result.customAttribute("pinyinAcronym").toString(); +} + +void FileNameResultAPI::setPinyinAcronym(const QString &acronym) +{ + m_result.setCustomAttribute("pinyinAcronym", acronym); +} + DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp index 0119d216..1bad5a39 100644 --- a/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp +++ b/src/dfm-search/dfm-search-lib/filenamesearch/filenamestrategies/indexedstrategy.cpp @@ -553,6 +553,10 @@ SearchResult FileNameIndexedStrategy::processDetailedSearchResult( } } + // 拼音与拼音首字母 + api.setPinyin(QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kPinyin))); + api.setPinyinAcronym(QString::fromStdWString(doc->get(LuceneFieldNames::FileName::kPinyinAcronym))); + return result; } diff --git a/src/dfm-search/dfm-search-lib/utils/searchutility.cpp b/src/dfm-search/dfm-search-lib/utils/searchutility.cpp index 59318a00..ea159583 100644 --- a/src/dfm-search/dfm-search-lib/utils/searchutility.cpp +++ b/src/dfm-search/dfm-search-lib/utils/searchutility.cpp @@ -738,87 +738,110 @@ bool isFileNameIndexDirectoryAvailable() } } -bool isFileNameIndexReadyForSearch() +namespace { + +struct FileNameIndexStatusFields { + bool parsed { false }; + bool createInProgress { false }; + bool updateInProgress { false }; + bool backlogExceeded { false }; + QString lastUpdateTime; +}; + +FileNameIndexStatusFields parseFileNameIndexStatusJson(const QByteArray &jsonContent) { - // First check if the index physically exists - if (!isFileNameIndexDirectoryAvailable()) { - qDebug() << "Index directory does not exist physically."; - return false; - } + FileNameIndexStatusFields fields; - // Then check the status to ensure it's in monitoring state - std::optional currentStatus = fileNameIndexStatus(); - if (!currentStatus) { - qWarning() << "Failed to get file name index status."; - return false; - } + QJsonParseError jsonError; + const QJsonDocument doc = QJsonDocument::fromJson(jsonContent, &jsonError); + if (jsonError.error != QJsonParseError::NoError || !doc.isObject()) + return fields; - const QStringList &validStatus = { "monitoring" }; - const QString &status = currentStatus.value(); - if (!validStatus.contains(status)) { - qDebug() << "Index status is '" << status - << "', expected or 'monitoring'. Index not ready for search."; - return false; - } + const QJsonObject root = doc.object(); + fields.parsed = true; + fields.createInProgress = root.value("createInProgress").toBool(false); + fields.updateInProgress = root.value("updateInProgress").toBool(false); + fields.backlogExceeded = root.value("backlogExceeded").toBool(false); + fields.lastUpdateTime = root.value("lastUpdateTime").toString(); + return fields; +} - return true; +QByteArray readFileNameIndexStatusFile() +{ + const QString &statusFilePath = QDir(fileNameIndexDirectory()).filePath("index_status.json"); + QFile statusFile(statusFilePath); + if (!statusFile.exists() || !statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return {}; + return statusFile.readAll(); } -std::optional fileNameIndexStatus() +} // namespace + +std::optional fileNameIndexStatusFromJson(const QByteArray &jsonContent) { - if (!isFileNameIndexDirectoryAvailable()) { - qWarning() << "Index directory not available"; + const FileNameIndexStatusFields fields = parseFileNameIndexStatusJson(jsonContent); + if (!fields.parsed) return std::nullopt; - } - const QString &statusFilePath = QDir(fileNameIndexDirectory()).filePath("status.json"); - QFile statusFile(statusFilePath); + // 检查优先级:createInProgress > updateInProgress > backlogExceeded > state + // 全量创建/版本重建/断点续建中:索引滞后不可信 + if (fields.createInProgress || fields.updateInProgress) + return QStringLiteral("scanning"); - // 检查文件是否存在和可读 - if (!statusFile.exists()) { - qWarning() << "Status file does not exist:" << statusFilePath; - return std::nullopt; - } - if (!statusFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - qWarning() << "Failed to open status file:" << statusFile.errorString(); - return std::nullopt; - } + // 大规模事件积压处理中(复刻旧 updating 语义):索引严重滞后,降级 Realtime + if (fields.backlogExceeded) + return QStringLiteral("updating"); - // 读取和解析JSON - QJsonParseError jsonError; - const QJsonDocument doc = QJsonDocument::fromJson(statusFile.readAll(), &jsonError); - statusFile.close(); + // lastUpdateTime 只在任务成功后写入;普通增量(dirty)期间索引仍可搜索 + if (!fields.lastUpdateTime.isEmpty()) + return QStringLiteral("monitoring"); - if (jsonError.error != QJsonParseError::NoError) { - qWarning() << "JSON parse error:" << jsonError.errorString() - << "at offset:" << jsonError.offset; - return std::nullopt; - } + // 从未成功完成过任务 + return QStringLiteral("scanning"); +} - // 检查JSON结构 - if (!doc.isObject()) { - qWarning() << "Invalid JSON format: root is not an object"; - return std::nullopt; +bool fileNameIndexReadyForSearchFromJson(const QByteArray &jsonContent) +{ + const FileNameIndexStatusFields fields = parseFileNameIndexStatusJson(jsonContent); + return fields.parsed + && !fields.createInProgress + && !fields.updateInProgress + && !fields.backlogExceeded + && !fields.lastUpdateTime.isEmpty(); +} + +bool isFileNameIndexReadyForSearch() +{ + // 1. 索引物理存在 + if (!isFileNameIndexDirectoryAvailable()) { + qDebug() << "Index directory does not exist physically."; + return false; } - const QJsonObject root = doc.object(); - if (!root.contains("status") || !root["status"].isString()) { - qWarning() << "Missing or invalid 'status' field"; + // 2. lastUpdateTime 非空 3. !createInProgress 4. !updateInProgress 5. !backlogExceeded + // 不检查 dirty/clean state(普通增量期间索引仍可搜索) + return fileNameIndexReadyForSearchFromJson(readFileNameIndexStatusFile()); +} + +std::optional fileNameIndexStatus() +{ + if (!isFileNameIndexDirectoryAvailable()) { + qWarning() << "Index directory not available"; return std::nullopt; } - // 返回小写状态 - return root["status"].toString().toLower(); + return fileNameIndexStatusFromJson(readFileNameIndexStatusFile()); } QString fileNameIndexDirectory() { - return QString("/run/user/%1/deepin-anything-server").arg(getuid()); + return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + + "/deepin/dde-file-manager/filename-index"; } int fileNameIndexVersion() { - return readIndexVersion(fileNameIndexDirectory(), "status.json"); + return readIndexVersion(fileNameIndexDirectory(), "index_status.json"); } int contentIndexVersion() diff --git a/src/dfm-search/dfm-search-lib/utils/searchutility.h b/src/dfm-search/dfm-search-lib/utils/searchutility.h index 9c76c862..15d87451 100644 --- a/src/dfm-search/dfm-search-lib/utils/searchutility.h +++ b/src/dfm-search/dfm-search-lib/utils/searchutility.h @@ -8,6 +8,7 @@ #include #include #include +#include #include // SPDX-FileCopyrightText: 2025 - 2026 UnionTech Software Technology Co., Ltd. @@ -21,6 +22,36 @@ DFM_SEARCH_BEGIN_NS /** * @brief The SearchUtility namespace provides utility functions for search operations */ +namespace Global { + +/** + * @brief Map filename index status JSON content (index_status.json) to the legacy status string + * + * Mapping (keeps legacy semantics): + * - std::nullopt : JSON missing/invalid + * - "scanning" : createInProgress or updateInProgress is set (full build / recovery / + * rebuild update in progress, index not trustworthy), or no successful + * task has ever completed (empty lastUpdateTime) + * - "updating" : backlogExceeded is set (large event backlog in progress, index + * severely lagged — replicates legacy "updating" semantics) + * - "monitoring" : lastUpdateTime present and no in-progress/backlog flags (index + * searchable, ordinary incremental updates may still be running) + */ +std::optional fileNameIndexStatusFromJson(const QByteArray &jsonContent); + +/** + * @brief Check whether index_status.json content marks the filename index ready for search + * + * Five conditions: valid JSON && lastUpdateTime non-empty && !createInProgress + * && !updateInProgress && !backlogExceeded. The dirty/clean state is not checked: + * ordinary incremental updates keep the index searchable, while recovery/rebuild + * updates and large event backlogs do set their respective flags and must degrade + * search to realtime traversal. + */ +bool fileNameIndexReadyForSearchFromJson(const QByteArray &jsonContent); + +} // namespace Global + namespace SearchUtility { /**