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) };