From 0aa9d68128134bb863e9a54d2331cede301dc0c4 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Fri, 17 Jul 2026 13:44:31 +0800 Subject: [PATCH] feat: add character count support for search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Added character count tracking for all search result types (Content/ OCR/Semantic) 2. Implemented PreviewResult::charCount() to expose the full content length 3. Added TextSearchResultAPI::setCharCount() and charCount() methods 4. Integrated character count into JSON and text output formats 5. Added preview output utilities for consistent formatting 6. Extended test coverage for character count functionality Log: Added character count display for search preview results Influence: 1. Test search preview results with verbose output to verify character counts 2. Check JSON output contains correct charCount fields 3. Verify text output formatting for character count display 4. Test boundary cases with empty content and various character sets 5. Confirm character count matches actual content length in different search types feat: 为搜索结果添加字符计数支持 1. 为所有搜索结果类型(内容/OCR/语义)添加字符计数跟踪 2. 实现PreviewResult::charCount()以暴露完整内容长度 3. 新增TextSearchResultAPI::setCharCount()和charCount()方法 4. 将字符计数集成到JSON和文本输出格式中 5. 添加预览输出工具函数以确保格式一致性 6. 扩展测试覆盖字符计数功能 Log: 为搜索结果预览添加字符计数显示功能 Influence: 1. 使用详细输出测试搜索结果预览,验证字符计数 2. 检查JSON输出是否包含正确的charCount字段 3. 验证文本输出的字符计数显示格式 4. 测试空内容和各种字符集的边界情况 5. 确认不同搜索类型下字符计数与实际内容长度匹配 --- autotests/dfm-search-tests/CMakeLists.txt | 4 + autotests/dfm-search-tests/main.cpp | 5 + .../tst_content_retriever.cpp | 6 + .../dfm-search-tests/tst_search_output.cpp | 279 ++++++++++++++++++ .../dfm-search-tests/tst_textsearch_api.cpp | 15 + .../dfm-search/dfm-search/contentretriever.h | 1 + include/dfm-search/dfm-search/textsearchapi.h | 12 + .../dfm-search-client/CMakeLists.txt | 2 + src/dfm-search/dfm-search-client/main.cpp | 15 +- .../dfm-search-client/output/json_output.cpp | 10 + .../dfm-search-client/output/text_output.cpp | 6 + .../preview_output_utils.cpp | 31 ++ .../dfm-search-client/preview_output_utils.h | 20 ++ .../contentstrategies/indexedstrategy.cpp | 1 + .../ocrtextstrategies/indexedstrategy.cpp | 1 + .../textsearch/textsearchapi.cpp | 10 + .../dfm-search-lib/utils/contentretriever.cpp | 7 + .../dfm-search-lib/utils/previewresult_p.h | 1 + 18 files changed, 414 insertions(+), 12 deletions(-) create mode 100644 autotests/dfm-search-tests/tst_search_output.cpp create mode 100644 src/dfm-search/dfm-search-client/preview_output_utils.cpp create mode 100644 src/dfm-search/dfm-search-client/preview_output_utils.h diff --git a/autotests/dfm-search-tests/CMakeLists.txt b/autotests/dfm-search-tests/CMakeLists.txt index 03783245..c5091f72 100644 --- a/autotests/dfm-search-tests/CMakeLists.txt +++ b/autotests/dfm-search-tests/CMakeLists.txt @@ -29,6 +29,10 @@ target_sources(dfm-search-test PRIVATE ${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/size_parser.cpp ${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/time_parser.cpp ${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/cli_options.cpp + ${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/preview_output_utils.cpp + ${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/output/output_formatter.h + ${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/output/text_output.cpp + ${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/output/json_output.cpp ${CMAKE_SOURCE_DIR}/3rdparty/testutils/stub-ext/stub-shadow.cpp ) diff --git a/autotests/dfm-search-tests/main.cpp b/autotests/dfm-search-tests/main.cpp index f0fb78b1..b4837d60 100644 --- a/autotests/dfm-search-tests/main.cpp +++ b/autotests/dfm-search-tests/main.cpp @@ -26,6 +26,7 @@ extern QObject *create_tst_FileNameSearchEngine(); extern QObject *create_tst_RecentSearchEngine(); extern QObject *create_tst_CliOptions(); extern QObject *create_tst_DConfigParsing(); +extern QObject *create_tst_SearchOutput(); int main(int argc, char *argv[]) { @@ -116,5 +117,9 @@ int main(int argc, char *argv[]) result |= QTest::qExec(testObj20, argc, argv); delete testObj20; + QObject *testObj21 = create_tst_SearchOutput(); + result |= QTest::qExec(testObj21, argc, argv); + delete testObj21; + return result; } diff --git a/autotests/dfm-search-tests/tst_content_retriever.cpp b/autotests/dfm-search-tests/tst_content_retriever.cpp index 2525a548..8cb84e4e 100644 --- a/autotests/dfm-search-tests/tst_content_retriever.cpp +++ b/autotests/dfm-search-tests/tst_content_retriever.cpp @@ -254,6 +254,7 @@ void tst_ContentRetriever::fetchPreview_noKeyword() SearchType::Content, options); QCOMPARE(result.content(), QString("world")); + QCOMPARE(result.charCount(), QString("hello world from content index").size()); QCOMPARE(result.keywordOffset(), -1); } @@ -279,6 +280,7 @@ void tst_ContentRetriever::fetchPreview_withKeyword() SearchType::Content, options); QCOMPARE(result.content(), QString("world from")); + QCOMPARE(result.charCount(), QString("hello world from content index").size()); QCOMPARE(result.keywordOffset(), 6); } @@ -304,6 +306,7 @@ void tst_ContentRetriever::fetchPreview_keywordNotFound() SearchType::Content, options); QVERIFY(result.content().isEmpty()); + QCOMPARE(result.charCount(), QString("hello world from content index").size()); QCOMPARE(result.keywordOffset(), -1); } @@ -328,6 +331,7 @@ void tst_ContentRetriever::fetchPreview_offsetBeyondContent() SearchType::Content, options); QVERIFY(result.content().isEmpty()); + QCOMPARE(result.charCount(), QString("hello world from content index").size()); QCOMPARE(result.keywordOffset(), -1); } @@ -354,6 +358,7 @@ void tst_ContentRetriever::fetchPreview_unlimitedNoKeyword() SearchType::Content, options); QCOMPARE(result.content(), QString("hello world from content index")); + QCOMPARE(result.charCount(), QString("hello world from content index").size()); QCOMPARE(result.keywordOffset(), -1); } @@ -368,6 +373,7 @@ void tst_ContentRetriever::fetchPreview_unlimitedNoKeyword() SearchType::Content, options); QCOMPARE(result.content(), QString("world from content index")); + QCOMPARE(result.charCount(), QString("hello world from content index").size()); QCOMPARE(result.keywordOffset(), -1); } } diff --git a/autotests/dfm-search-tests/tst_search_output.cpp b/autotests/dfm-search-tests/tst_search_output.cpp new file mode 100644 index 00000000..74d9590e --- /dev/null +++ b/autotests/dfm-search-tests/tst_search_output.cpp @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "output/json_output.h" +#include "output/text_output.h" +#include "preview_output_utils.h" + +using namespace DFMSEARCH; +using namespace Lucene; + +namespace { + +void addStoredDocument(const IndexWriterPtr &writer, + SearchType type, + const QString &path, + const QString &filename, + const QString &content) +{ + DocumentPtr doc = newLucene(); + if (type == SearchType::Ocr) { + doc->add(newLucene(LuceneFieldNames::OcrText::kPath, path.toStdWString(), + Field::STORE_YES, Field::INDEX_NOT_ANALYZED)); + doc->add(newLucene(LuceneFieldNames::OcrText::kFilename, filename.toStdWString(), + Field::STORE_YES, Field::INDEX_NOT_ANALYZED)); + doc->add(newLucene(LuceneFieldNames::OcrText::kOcrContents, content.toStdWString(), + Field::STORE_YES, Field::INDEX_NOT_ANALYZED)); + } else { + doc->add(newLucene(LuceneFieldNames::Content::kPath, path.toStdWString(), + Field::STORE_YES, Field::INDEX_NOT_ANALYZED)); + doc->add(newLucene(LuceneFieldNames::Content::kFilename, filename.toStdWString(), + Field::STORE_YES, Field::INDEX_NOT_ANALYZED)); + doc->add(newLucene(LuceneFieldNames::Content::kContents, content.toStdWString(), + Field::STORE_YES, Field::INDEX_NOT_ANALYZED)); + } + writer->addDocument(doc); +} + +void createIndex(const QString &indexDir, SearchType type) +{ + QDir().mkpath(indexDir); + IndexWriterPtr writer = newLucene( + FSDirectory::open(indexDir.toStdWString()), + newLucene(), + true, + IndexWriter::MaxFieldLengthLIMITED); + + if (type == SearchType::Content) { + addStoredDocument(writer, type, + "/tmp/doc-a.txt", + "doc-a.txt", + "hello world from content index"); + } else { + addStoredDocument(writer, type, + "/tmp/img-a.png", + "img-a.png", + "screenshot text from OCR"); + } + + writer->close(); +} + +QString captureStdout(const std::function &fn) +{ + QTemporaryFile file; + if (!file.open()) { + return {}; + } + + fflush(stdout); + std::cout.flush(); + + const int savedStdout = dup(STDOUT_FILENO); + if (savedStdout < 0) { + return {}; + } + + dup2(file.handle(), STDOUT_FILENO); + fn(); + fflush(stdout); + std::cout.flush(); + + dup2(savedStdout, STDOUT_FILENO); + close(savedStdout); + + file.seek(0); + return QString::fromUtf8(file.readAll()); +} + +} // namespace + +class tst_SearchOutput : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void previewOutputHelpers_includeCharCount(); + void jsonOutput_contentAndFilenameCharCountContract(); + void jsonOutput_ocrAndSemanticCharCountContract(); + void textOutput_contentAndSemanticCharCountContract(); +}; + +void tst_SearchOutput::previewOutputHelpers_includeCharCount() +{ + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + + const QString contentIndexDir = tempDir.path() + "/content-index"; + createIndex(contentIndexDir, SearchType::Content); + + ContentRetriever retriever; + retriever.setIndexDirectory(SearchType::Content, contentIndexDir); + + PreviewOptions options; + options.setKeyword("world"); + options.setOffset(0); + options.setMaxLength(10); + + const PreviewResult result = retriever.fetchPreview("/tmp/doc-a.txt", SearchType::Content, options); + const QJsonObject json = dfmsearch::previewResultToJson("/tmp/doc-a.txt", result); + QCOMPARE(json.value("content").toString(), QString("world from")); + QCOMPARE(json.value("charCount").toInt(), QString("hello world from content index").size()); + QCOMPARE(json.value("keywordOffset").toInt(), 6); + + QString text; + QTextStream stream(&text); + dfmsearch::writePreviewResultText(stream, "/tmp/doc-a.txt", result); + QVERIFY(text.contains("Char count: 30")); + QVERIFY(text.contains("world from")); +} + +void tst_SearchOutput::jsonOutput_contentAndFilenameCharCountContract() +{ + SearchOptions options; + options.setDetailedResultsEnabled(true); + + SearchResult contentResult("/tmp/doc-a.txt"); + ContentResultAPI contentApi(contentResult); + contentApi.setCharCount(30); + contentApi.setFilename("doc-a.txt"); + contentResult.setCustomAttribute("plainContentMatch", QString("world from")); + contentResult.setCustomAttribute("snippetOffset", 6); + + dfmsearch::JsonOutput contentOutput(false); + contentOutput.setSearchOptions(options); + contentOutput.setSearchContext("world", "/tmp", SearchType::Content, SearchMethod::Indexed); + const QString contentJsonText = captureStdout([&]() { + contentOutput.outputSearchStarted(); + contentOutput.outputSearchFinished({ contentResult }); + }); + const QJsonObject contentRoot = QJsonDocument::fromJson(contentJsonText.toUtf8()).object(); + const QJsonObject contentItem = contentRoot.value("results").toArray().first().toObject(); + QCOMPARE(contentItem.value("charCount").toInt(), 30); + + SearchResult filenameResult("/tmp/doc-a.txt"); + FileNameResultAPI fileApi(filenameResult); + fileApi.setFilename("doc-a.txt"); + fileApi.setFileType("txt"); + fileApi.setSize("123"); + filenameResult.setCustomAttribute("charCount", 30); + + dfmsearch::JsonOutput filenameOutput(false); + filenameOutput.setSearchOptions(options); + filenameOutput.setSearchContext("doc", "/tmp", SearchType::FileName, SearchMethod::Indexed); + const QString filenameJsonText = captureStdout([&]() { + filenameOutput.outputSearchStarted(); + filenameOutput.outputSearchFinished({ filenameResult }); + }); + const QJsonObject filenameRoot = QJsonDocument::fromJson(filenameJsonText.toUtf8()).object(); + const QJsonObject filenameItem = filenameRoot.value("results").toArray().first().toObject(); + QVERIFY(!filenameItem.contains("charCount")); +} + +void tst_SearchOutput::jsonOutput_ocrAndSemanticCharCountContract() +{ + SearchOptions options; + options.setDetailedResultsEnabled(true); + + SearchResult ocrResult("/tmp/img-a.png"); + OcrTextResultAPI ocrApi(ocrResult); + ocrApi.setCharCount(24); + ocrApi.setOcrContent("screenshot text from OCR"); + ocrResult.setCustomAttribute("plainContentMatch", QString("text from OCR")); + ocrResult.setCustomAttribute("snippetOffset", 11); + + dfmsearch::JsonOutput ocrOutput(false); + ocrOutput.setSearchOptions(options); + ocrOutput.setSearchContext("text", "/tmp", SearchType::Ocr, SearchMethod::Indexed); + const QString ocrJsonText = captureStdout([&]() { + ocrOutput.outputSearchStarted(); + ocrOutput.outputSearchFinished({ ocrResult }); + }); + const QJsonObject ocrRoot = QJsonDocument::fromJson(ocrJsonText.toUtf8()).object(); + const QJsonObject ocrItem = ocrRoot.value("results").toArray().first().toObject(); + QCOMPARE(ocrItem.value("charCount").toInt(), 24); + + SearchResult semanticResult("/tmp/doc-a.txt"); + semanticResult.setCustomAttribute("charCount", 30); + semanticResult.setCustomAttribute("contentMatch", QString("world from")); + + dfmsearch::JsonOutput semanticOutput(false); + semanticOutput.setSearchOptions(options); + semanticOutput.setSearchContext("world", "/tmp", SearchType::Semantic, SearchMethod::Indexed); + const QString semanticJsonText = captureStdout([&]() { + semanticOutput.outputSearchStarted(); + semanticOutput.outputSearchFinished({ semanticResult }); + }); + const QJsonObject semanticRoot = QJsonDocument::fromJson(semanticJsonText.toUtf8()).object(); + const QJsonObject semanticItem = semanticRoot.value("results").toArray().first().toObject(); + QCOMPARE(semanticItem.value("charCount").toInt(), 30); +} + +void tst_SearchOutput::textOutput_contentAndSemanticCharCountContract() +{ + SearchOptions options; + options.setDetailedResultsEnabled(true); + + SearchResult contentResult("/tmp/doc-a.txt"); + ContentResultAPI contentApi(contentResult); + contentApi.setCharCount(30); + contentResult.setCustomAttribute("plainContentMatch", QString("world from")); + contentResult.setCustomAttribute("snippetOffset", 6); + + dfmsearch::TextOutput contentOutput; + contentOutput.setSearchOptions(options); + contentOutput.setVerbose(true); + contentOutput.setSearchContext("world", "/tmp", SearchType::Content, SearchMethod::Indexed); + const QString contentText = captureStdout([&]() { + contentOutput.outputSearchFinished({ contentResult }); + }); + QVERIFY(contentText.contains("Char count: 30")); + + SearchResult semanticResult("/tmp/doc-a.txt"); + semanticResult.setCustomAttribute("charCount", 30); + semanticResult.setCustomAttribute("contentMatch", QString("world from")); + + dfmsearch::TextOutput semanticOutput; + semanticOutput.setSearchOptions(options); + semanticOutput.setVerbose(true); + semanticOutput.setSearchContext("world", "/tmp", SearchType::Semantic, SearchMethod::Indexed); + const QString semanticText = captureStdout([&]() { + semanticOutput.outputSearchFinished({ semanticResult }); + }); + QVERIFY(semanticText.contains("charCount: 30")); +} + +QObject *create_tst_SearchOutput() +{ + return new tst_SearchOutput(); +} + +#include "tst_search_output.moc" diff --git a/autotests/dfm-search-tests/tst_textsearch_api.cpp b/autotests/dfm-search-tests/tst_textsearch_api.cpp index bc398da2..c98f1011 100644 --- a/autotests/dfm-search-tests/tst_textsearch_api.cpp +++ b/autotests/dfm-search-tests/tst_textsearch_api.cpp @@ -31,6 +31,7 @@ private Q_SLOTS: void textSearchResult_isHidden(); void textSearchResult_modifyTimestamp(); void textSearchResult_birthTimestamp(); + void textSearchResult_charCount(); void textSearchResult_plainSnippetAttributes(); // ContentOptionsAPI tests @@ -185,6 +186,20 @@ void tst_TextSearchAPI::textSearchResult_birthTimestamp() QCOMPARE(api.birthTimestamp(), 0); } +void tst_TextSearchAPI::textSearchResult_charCount() +{ + SearchResult result("/test/path"); + TextSearchResultAPI api(result); + + QCOMPARE(api.charCount(), 0); + + api.setCharCount(128); + QCOMPARE(api.charCount(), 128); + + api.setCharCount(0); + QCOMPARE(api.charCount(), 0); +} + void tst_TextSearchAPI::textSearchResult_plainSnippetAttributes() { SearchResult result("/test/path"); diff --git a/include/dfm-search/dfm-search/contentretriever.h b/include/dfm-search/dfm-search/contentretriever.h index 2143d05e..3d18bb50 100644 --- a/include/dfm-search/dfm-search/contentretriever.h +++ b/include/dfm-search/dfm-search/contentretriever.h @@ -99,6 +99,7 @@ class PreviewResult PreviewResult &operator=(PreviewResult &&other) noexcept = default; QString content() const; + int charCount() const; int keywordOffset() const; private: diff --git a/include/dfm-search/dfm-search/textsearchapi.h b/include/dfm-search/dfm-search/textsearchapi.h index 08c5180d..8df05473 100644 --- a/include/dfm-search/dfm-search/textsearchapi.h +++ b/include/dfm-search/dfm-search/textsearchapi.h @@ -159,6 +159,18 @@ class TextSearchResultAPI */ qint64 fileSizeBytes() const; + /** + * @brief Set the stored full-text character count + * @param count Character count in the same unit as preview offsets + */ + void setCharCount(int count); + + /** + * @brief Get the stored full-text character count + * @return Character count, 0 if not set + */ + int charCount() const; + // ==================== Extended Attributes ==================== /** diff --git a/src/dfm-search/dfm-search-client/CMakeLists.txt b/src/dfm-search/dfm-search-client/CMakeLists.txt index 37103884..a38932ba 100644 --- a/src/dfm-search/dfm-search-client/CMakeLists.txt +++ b/src/dfm-search/dfm-search-client/CMakeLists.txt @@ -6,6 +6,8 @@ set(SRCS main.cpp cli_options.cpp cli_options.h + preview_output_utils.cpp + preview_output_utils.h time_parser.cpp time_parser.h size_parser.cpp diff --git a/src/dfm-search/dfm-search-client/main.cpp b/src/dfm-search/dfm-search-client/main.cpp index e50778c6..6fb3f3f3 100644 --- a/src/dfm-search/dfm-search-client/main.cpp +++ b/src/dfm-search/dfm-search-client/main.cpp @@ -20,6 +20,7 @@ #include #include "cli_options.h" +#include "preview_output_utils.h" #include "output/text_output.h" #include "output/json_output.h" @@ -226,12 +227,8 @@ int main(int argc, char *argv[]) QJsonArray results; for (const QString &path : paths) { - QJsonObject item; - item["path"] = path; DFMSEARCH::PreviewResult result = retriever.fetchPreview(path, config.searchType, previewOptions); - item["content"] = result.content(); - item["keywordOffset"] = result.keywordOffset(); - results.append(item); + results.append(previewResultToJson(path, result)); } root["totalResults"] = results.size(); @@ -243,14 +240,8 @@ int main(int argc, char *argv[]) // Text output QTextStream out(stdout); for (const QString &path : paths) { - out << path << "\n"; DFMSEARCH::PreviewResult result = retriever.fetchPreview(path, config.searchType, previewOptions); - if (!result.content().isEmpty()) { - out << " " << result.content() << "\n"; - } else { - out << " (no content)\n"; - } - out << Qt::endl; + writePreviewResultText(out, path, result); } } return 0; 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 582b2205..e46871e4 100644 --- a/src/dfm-search/dfm-search-client/output/json_output.cpp +++ b/src/dfm-search/dfm-search-client/output/json_output.cpp @@ -230,6 +230,11 @@ QJsonValue JsonOutput::resultToJson(const SearchResult &result) obj["contentMatch"] = plainContentMatch(result); obj["snippetOffset"] = snippetOffset(result); + int charCount = resultAPI.charCount(); + if (charCount > 0) { + obj["charCount"] = charCount; + } + QString filename = resultAPI.filename(); if (!filename.isEmpty()) { obj["filename"] = filename; @@ -272,6 +277,11 @@ QJsonValue JsonOutput::resultToJson(const SearchResult &result) obj["contentMatch"] = plainContentMatch(result); obj["snippetOffset"] = snippetOffset(result); + int charCount = resultAPI.charCount(); + if (charCount > 0) { + obj["charCount"] = charCount; + } + QString ocrContent = resultAPI.ocrContent(); if (!ocrContent.isEmpty()) { obj["ocrContent"] = ocrContent; 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 7d43eb7f..33168268 100644 --- a/src/dfm-search/dfm-search-client/output/text_output.cpp +++ b/src/dfm-search/dfm-search-client/output/text_output.cpp @@ -154,6 +154,9 @@ void TextOutput::printSearchResult(const SearchResult &result) std::cout << " Content match: " << plainContentMatch(result).toStdString() << std::endl; std::cout << " Snippet offset: " << snippetOffset(result) << std::endl; + if (resultAPI.charCount() > 0) { + std::cout << " Char count: " << resultAPI.charCount() << std::endl; + } // 文件名 QString filename = resultAPI.filename(); @@ -188,6 +191,9 @@ void TextOutput::printSearchResult(const SearchResult &result) std::cout << " Content match: " << plainContentMatch(result).toStdString() << std::endl; std::cout << " Snippet offset: " << snippetOffset(result) << std::endl; + if (resultAPI.charCount() > 0) { + std::cout << " Char count: " << resultAPI.charCount() << std::endl; + } QString ocrContent = resultAPI.ocrContent(); if (!ocrContent.isEmpty()) { diff --git a/src/dfm-search/dfm-search-client/preview_output_utils.cpp b/src/dfm-search/dfm-search-client/preview_output_utils.cpp new file mode 100644 index 00000000..f39e9997 --- /dev/null +++ b/src/dfm-search/dfm-search-client/preview_output_utils.cpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "preview_output_utils.h" + +namespace dfmsearch { + +QJsonObject previewResultToJson(const QString &path, const DFMSEARCH::PreviewResult &result) +{ + QJsonObject item; + item["path"] = path; + item["content"] = result.content(); + item["charCount"] = result.charCount(); + item["keywordOffset"] = result.keywordOffset(); + return item; +} + +void writePreviewResultText(QTextStream &out, const QString &path, const DFMSEARCH::PreviewResult &result) +{ + out << path << "\n"; + if (!result.content().isEmpty()) { + out << " " << result.content() << "\n"; + } else { + out << " (no content)\n"; + } + out << " Char count: " << result.charCount() << "\n"; + out << Qt::endl; +} + +} // namespace dfmsearch diff --git a/src/dfm-search/dfm-search-client/preview_output_utils.h b/src/dfm-search/dfm-search-client/preview_output_utils.h new file mode 100644 index 00000000..373d8e9a --- /dev/null +++ b/src/dfm-search/dfm-search-client/preview_output_utils.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PREVIEW_OUTPUT_UTILS_H +#define PREVIEW_OUTPUT_UTILS_H + +#include + +#include +#include + +namespace dfmsearch { + +QJsonObject previewResultToJson(const QString &path, const DFMSEARCH::PreviewResult &result); +void writePreviewResultText(QTextStream &out, const QString &path, const DFMSEARCH::PreviewResult &result); + +} // namespace dfmsearch + +#endif // PREVIEW_OUTPUT_UTILS_H diff --git a/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp b/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp index a940cb6f..2af7bef6 100644 --- a/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp +++ b/src/dfm-search/dfm-search-lib/contentsearch/contentstrategies/indexedstrategy.cpp @@ -419,6 +419,7 @@ void ContentIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr m_keywords, content, previewLen, previewLen); const QString highlightedContent = ContentHighlighter::customHighlight(m_keywords, content, previewLen, enableHTML); resultApi.setHighlightedContent(highlightedContent); + resultApi.setCharCount(content.size()); result.setCustomAttribute("plainContentMatch", plainSnippet.content); result.setCustomAttribute("snippetOffset", plainSnippet.snippetOffset); } diff --git a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp index 4b62082c..b2becabd 100644 --- a/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp +++ b/src/dfm-search/dfm-search-lib/ocrtextsearch/ocrtextstrategies/indexedstrategy.cpp @@ -419,6 +419,7 @@ void OcrTextIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr m_keywords, content, previewLen, previewLen); // 设置原始 OCR 内容 resultApi.setOcrContent(content); + resultApi.setCharCount(content.size()); // 设置高亮内容 const QString highlightedContent = ContentHighlighter::customHighlight( m_keywords, content, previewLen, enableHTML); diff --git a/src/dfm-search/dfm-search-lib/textsearch/textsearchapi.cpp b/src/dfm-search/dfm-search-lib/textsearch/textsearchapi.cpp index e698078e..9a9d3bac 100644 --- a/src/dfm-search/dfm-search-lib/textsearch/textsearchapi.cpp +++ b/src/dfm-search/dfm-search-lib/textsearch/textsearchapi.cpp @@ -77,6 +77,16 @@ qint64 TextSearchResultAPI::fileSizeBytes() const return m_result.customAttribute("fileSizeBytes").toLongLong(); } +void TextSearchResultAPI::setCharCount(int count) +{ + m_result.setCustomAttribute("charCount", count); +} + +int TextSearchResultAPI::charCount() const +{ + return m_result.customAttribute("charCount").toInt(); +} + TextSearchResultAPI::TextSearchResultAPI(SearchResult &result) : m_result(result) { diff --git a/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp b/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp index 1b3c0e24..82d47741 100644 --- a/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp +++ b/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp @@ -228,6 +228,11 @@ QString PreviewResult::content() const return d->content; } +int PreviewResult::charCount() const +{ + return d->charCount; +} + int PreviewResult::keywordOffset() const { return d->keywordOffset; @@ -555,6 +560,8 @@ PreviewResult ContentRetriever::fetchPreview(const QString &path, SearchType typ return result; } + // Keep preview metadata in the same QString character unit as offset/keywordOffset. + result.d->charCount = content.size(); int keywordOffset = -1; const QString snippet = ContentHighlighter::previewSnippet( content, options.offset(), options.maxLength(), diff --git a/src/dfm-search/dfm-search-lib/utils/previewresult_p.h b/src/dfm-search/dfm-search-lib/utils/previewresult_p.h index 6dd21841..1cf798ac 100644 --- a/src/dfm-search/dfm-search-lib/utils/previewresult_p.h +++ b/src/dfm-search/dfm-search-lib/utils/previewresult_p.h @@ -19,6 +19,7 @@ class PreviewResultPrivate : public QSharedData PreviewResultPrivate(const PreviewResultPrivate &other) = default; QString content; ///< Extracted content snippet + int charCount = 0; ///< Full stored content length in QString character units int keywordOffset = -1; ///< Position of keyword in full content (-1 if none/not matched) };