From 484322bed96cfd45aae4017eb8c86a3297bb95e6 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Fri, 10 Jul 2026 14:30:44 +0800 Subject: [PATCH 1/2] feat: add precise content preview with offset support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Implement PreviewOptions and PreviewResult classes for precise content extraction 2. Add fetchPreview API to ContentRetriever for offset-based content reading 3. Create previewSnippet utility function in ContentHighlighter 4. Update CLI client to support offset and precise preview commands 5. Add comprehensive unit tests for all new functionality 6. Implement private classes with value semantics using QSharedDataPointer This feature enables applications to: - Read content from specific offsets without keyword searching - Search content starting from an offset position - Get exact content snippets without formatting or simplification - See keyword match positions in the source text Log: Added precise content preview with offset parameter support Influence: 1. Test fetchPreview with various offset values (valid and invalid) 2. Verify keyword searching starting from different offsets 3. Test edge cases (empty content, negative offsets, offsets beyond content length) 4. Verify output formatting and keyword position reporting 5. Test concurrent access to the preview functionality 6. Check CLI interface with new offset parameter feat: 添加带偏移量的精确内容预览功能 1. 实现 PreviewOptions 和 PreviewResult 类用于精确内容提取 2. 在 ContentRetriever 中添加 fetchPreview API 用于基于偏移量的内容读取 3. 在 ContentHighlighter 中创建 previewSnippet 工具函数 4. 更新命令行客户端以支持偏移量和精确预览命令 5. 添加全面的单元测试覆盖新功能 6. 使用 QSharedDataPointer 实现具有值语义的私有类 该功能使应用程序能够: - 从特定偏移量读取内容而无需关键字搜索 - 从偏移位置开始搜索内容 - 获取精确内容片段而无需格式化或简化 - 查看关键字在源文本中的匹配位置 Log: 新增带偏移量参数的内容精确预览功能 Influence: 1. 测试不同偏移量下的 fetchPreview 功能(有效和无效值) 2. 验证从不同偏移位置开始的关键字搜索 3. 测试边界情况(空内容、负偏移量、超出内容长度的偏移量) 4. 验证输出格式和关键字位置报告 5. 测试预览功能的并发访问 6. 检查包含新偏移量参数的命令行接口 --- .../tst_content_retriever.cpp | 130 ++++++++++++++++++ .../dfm-search/dfm-search/contentretriever.h | 77 +++++++++++ .../dfm-search-client/cli_options.cpp | 22 ++- .../dfm-search-client/cli_options.h | 2 + src/dfm-search/dfm-search-client/main.cpp | 34 ++--- .../utils/contenthighlighter.cpp | 29 ++++ .../dfm-search-lib/utils/contenthighlighter.h | 18 +++ .../dfm-search-lib/utils/contentretriever.cpp | 119 ++++++++++++++++ .../dfm-search-lib/utils/previewoptions_p.h | 28 ++++ .../dfm-search-lib/utils/previewresult_p.h | 27 ++++ 10 files changed, 462 insertions(+), 24 deletions(-) create mode 100644 src/dfm-search/dfm-search-lib/utils/previewoptions_p.h create mode 100644 src/dfm-search/dfm-search-lib/utils/previewresult_p.h diff --git a/autotests/dfm-search-tests/tst_content_retriever.cpp b/autotests/dfm-search-tests/tst_content_retriever.cpp index 4aea2fa0..638c55f2 100644 --- a/autotests/dfm-search-tests/tst_content_retriever.cpp +++ b/autotests/dfm-search-tests/tst_content_retriever.cpp @@ -13,6 +13,8 @@ #include #include +#include "utils/contenthighlighter.h" + #include #include #include @@ -91,6 +93,11 @@ private Q_SLOTS: void fetchContent_semanticRoutingFailsWhenNoDConfig(); void fetchHighlight_semanticRoutingFailsWhenNoDConfig(); void concurrentFetch_sharedRetriever(); + void fetchPreview_noKeyword(); + void fetchPreview_withKeyword(); + void fetchPreview_keywordNotFound(); + void fetchPreview_offsetBeyondContent(); + void previewSnippet_basic(); }; void tst_ContentRetriever::fetchContent_single() @@ -225,6 +232,129 @@ void tst_ContentRetriever::concurrentFetch_sharedRetriever() QVERIFY(!failed.load()); } +void tst_ContentRetriever::fetchPreview_noKeyword() +{ + 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(QString()); + options.setOffset(6); // skip "hello " + options.setMaxLength(5); // expect "world" + + const PreviewResult result = retriever.fetchPreview("/tmp/doc-a.txt", + SearchType::Content, + options); + QCOMPARE(result.content(), QString("world")); + QCOMPARE(result.keywordOffset(), -1); +} + +void tst_ContentRetriever::fetchPreview_withKeyword() +{ + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + + const QString contentIndexDir = tempDir.path() + "/content-index"; + createIndex(contentIndexDir, SearchType::Content); + + ContentRetriever retriever; + retriever.setIndexDirectory(SearchType::Content, contentIndexDir); + + // Content of doc-a.txt is "hello world from content index" + // "world" starts at position 6 + PreviewOptions options; + options.setKeyword("world"); + options.setOffset(0); + options.setMaxLength(10); + + const PreviewResult result = retriever.fetchPreview("/tmp/doc-a.txt", + SearchType::Content, + options); + QCOMPARE(result.content(), QString("world from")); + QCOMPARE(result.keywordOffset(), 6); +} + +void tst_ContentRetriever::fetchPreview_keywordNotFound() +{ + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + + const QString contentIndexDir = tempDir.path() + "/content-index"; + createIndex(contentIndexDir, SearchType::Content); + + ContentRetriever retriever; + retriever.setIndexDirectory(SearchType::Content, contentIndexDir); + + // Content of doc-a.txt is "hello world from content index" (30 chars) + // offset=300 is beyond the content, so keyword won't be found + PreviewOptions options; + options.setKeyword("world"); + options.setOffset(300); + options.setMaxLength(10); + + const PreviewResult result = retriever.fetchPreview("/tmp/doc-a.txt", + SearchType::Content, + options); + QVERIFY(result.content().isEmpty()); + QCOMPARE(result.keywordOffset(), -1); +} + +void tst_ContentRetriever::fetchPreview_offsetBeyondContent() +{ + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + + const QString contentIndexDir = tempDir.path() + "/content-index"; + createIndex(contentIndexDir, SearchType::Content); + + ContentRetriever retriever; + retriever.setIndexDirectory(SearchType::Content, contentIndexDir); + + // No keyword, offset beyond content length → empty content + PreviewOptions options; + options.setKeyword(QString()); + options.setOffset(1000); + options.setMaxLength(50); + + const PreviewResult result = retriever.fetchPreview("/tmp/doc-a.txt", + SearchType::Content, + options); + QVERIFY(result.content().isEmpty()); + QCOMPARE(result.keywordOffset(), -1); +} + +void tst_ContentRetriever::previewSnippet_basic() +{ + using DFMSEARCH::ContentHighlighter::previewSnippet; + + const QString content = QStringLiteral("hello world from content index"); + + // No keyword: simple offset + maxLength slice + int kwOff = 42; + QCOMPARE(previewSnippet(content, 6, 5, QString(), &kwOff), QString("world")); + QCOMPARE(kwOff, -1); + + // With keyword: search from offset, return from match position + QCOMPARE(previewSnippet(content, 0, 10, "world", &kwOff), QString("world from")); + QCOMPARE(kwOff, 6); + + // Keyword not found + QVERIFY(previewSnippet(content, 0, 10, "nonexistent", &kwOff).isEmpty()); + QCOMPARE(kwOff, -1); + + // Offset beyond content + QVERIFY(previewSnippet(content, 1000, 10, QString(), &kwOff).isEmpty()); + + // Negative offset → empty + QVERIFY(previewSnippet(content, -1, 10, QString(), &kwOff).isEmpty()); +} + QObject *create_tst_ContentRetriever() { return new tst_ContentRetriever(); diff --git a/include/dfm-search/dfm-search/contentretriever.h b/include/dfm-search/dfm-search/contentretriever.h index 4372d2c1..2143d05e 100644 --- a/include/dfm-search/dfm-search/contentretriever.h +++ b/include/dfm-search/dfm-search/contentretriever.h @@ -15,6 +15,8 @@ DFM_SEARCH_BEGIN_NS class HighlightOptionsPrivate; +class PreviewOptionsPrivate; +class PreviewResultPrivate; /** * @brief Lightweight options for highlight extraction (Pimpl-based, ABI-stable) @@ -47,6 +49,63 @@ class HighlightOptions QSharedDataPointer d; }; +/** + * @brief Options for precise preview extraction (Pimpl-based, ABI-stable) + * + * Controls offset-based content reading: either a raw slice from offset + * (no keyword) or a keyword search starting from offset. + * + * Supports implicit sharing (COW) via QSharedDataPointer. + */ +class PreviewOptions +{ +public: + PreviewOptions(); + ~PreviewOptions(); + PreviewOptions(const PreviewOptions &other); + PreviewOptions &operator=(const PreviewOptions &other); + PreviewOptions(PreviewOptions &&other) noexcept = default; + PreviewOptions &operator=(PreviewOptions &&other) noexcept = default; + + int offset() const; + void setOffset(int offset); + + int maxLength() const; + void setMaxLength(int length); + + QString keyword() const; + void setKeyword(const QString &keyword); + +private: + QSharedDataPointer d; +}; + +/** + * @brief Result of a preview extraction (Pimpl-based, ABI-stable) + * + * Contains the extracted content snippet and the position of the keyword + * in the full document text (-1 if no keyword or not matched). + * + * Supports implicit sharing (COW) via QSharedDataPointer. + */ +class PreviewResult +{ +public: + PreviewResult(); + ~PreviewResult(); + PreviewResult(const PreviewResult &other); + PreviewResult &operator=(const PreviewResult &other); + PreviewResult(PreviewResult &&other) noexcept = default; + PreviewResult &operator=(PreviewResult &&other) noexcept = default; + + QString content() const; + int keywordOffset() const; + +private: + friend class ContentRetriever; + QSharedDataPointer d; +}; + /** * @brief Retrieves highlighted content from Lucene index on demand * @@ -129,6 +188,24 @@ class ContentRetriever : public QObject QMap fetchContents(const QStringList &paths, SearchType type) const; + /** + * @brief Synchronously fetch a precise preview snippet for a single file + * + * Opens the Lucene index, locates the document by path, extracts stored + * text, and runs ContentHighlighter::previewSnippet to produce a raw + * content slice based on offset/maxLength/keyword. + * + * Unlike fetchHighlight, this does NOT simplify, add ellipsis, or + * highlight — it returns the exact content at the requested position. + * + * @param path Absolute file path + * @param type SearchType::Content or SearchType::Ocr + * @param options Preview configuration (offset, maxLength, keyword) + * @return PreviewResult with content snippet and keywordOffset + */ + PreviewResult fetchPreview(const QString &path, SearchType type, + const PreviewOptions &options = {}) const; + private: struct Private; std::unique_ptr d; diff --git a/src/dfm-search/dfm-search-client/cli_options.cpp b/src/dfm-search/dfm-search-client/cli_options.cpp index d8d34bd3..45fba883 100644 --- a/src/dfm-search/dfm-search-client/cli_options.cpp +++ b/src/dfm-search/dfm-search-client/cli_options.cpp @@ -27,6 +27,7 @@ CliOptions::CliOptions() m_fileExtensionsOption(QStringList() << "file-extensions", "Filter by file extensions, comma separated", "extensions"), m_maxResultsOption(QStringList() << "max-results", "Maximum number of results (0 for unlimited)", "number", "0"), m_maxPreviewOption(QStringList() << "max-preview", "Max content preview length", "length", "200"), + m_offsetOption(QStringList() << "offset", "Content offset: start reading from the n-th character (preview only)", "n", "0"), m_filenameOption(QStringList() << "filename", "Search by filename in content/ocr index", "keyword"), m_wildcardOption(QStringList() << "wildcard", "Enable wildcard search with * and ? patterns"), m_jsonOption(QStringList() << "json" @@ -74,6 +75,7 @@ void CliOptions::setupOptions() m_parser.addOption(m_fileExtensionsOption); m_parser.addOption(m_maxResultsOption); m_parser.addOption(m_maxPreviewOption); + m_parser.addOption(m_offsetOption); m_parser.addOption(m_filenameOption); m_parser.addOption(m_wildcardOption); m_parser.addOption(m_jsonOption); @@ -129,6 +131,7 @@ void CliOptions::printHelp() const std::cout << " --file-extensions= Filter by file extensions, comma separated" << std::endl; std::cout << " --max-results= Maximum number of results (0 for unlimited)" << std::endl; std::cout << " --max-preview= Max content preview length (for content/ocr search)" << std::endl; + std::cout << " --offset= Content offset: start reading from the n-th character (preview only, default 0)" << std::endl; std::cout << " --filename= Search by filename in content/ocr index" << std::endl; std::cout << std::endl; std::cout << "Time Range Filter Options:" << std::endl; @@ -183,10 +186,10 @@ void CliOptions::printHelp() const std::cout << " dfm-searcher --size-min=1M --size-max=100M \"video\" /home/user" << std::endl; std::cout << std::endl; std::cout << "Content Preview (on-demand):" << std::endl; - std::cout << " dfm-searcher preview [] [path2 ...] [--type=] [-j]" << std::endl; + std::cout << " dfm-searcher preview [] [path2 ...] [--type=] [--offset=] [--max-preview=] [-j]" << std::endl; std::cout << " Fetch content snippets for specific files without running a full search." << std::endl; - std::cout << " With keyword: returns highlighted snippets matching the keyword." << std::endl; - std::cout << " Without keyword: returns full stored content of each file." << std::endl; + std::cout << " With keyword: returns snippet from the keyword match position (search starts at --offset)." << std::endl; + std::cout << " Without keyword: returns exact content starting from --offset, up to --max-preview characters." << std::endl; std::cout << " Search type is auto-detected by file extension; use --type to force a specific index." << std::endl; std::cout << std::endl; std::cout << " # Fetch highlighted snippet for a single file" << std::endl; @@ -195,6 +198,12 @@ void CliOptions::printHelp() const std::cout << " # Fetch full content (no keyword)" << std::endl; std::cout << " dfm-searcher preview /home/user/doc.txt" << std::endl; std::cout << std::endl; + std::cout << " # Read content from offset 100, 50 chars max" << std::endl; + std::cout << " dfm-searcher preview /home/user/doc.txt --offset=100 --max-preview=50 -j" << std::endl; + std::cout << std::endl; + std::cout << " # Search keyword from offset 150" << std::endl; + std::cout << " dfm-searcher preview \"keyword\" /home/user/doc.txt --offset=150 -j" << std::endl; + std::cout << std::endl; std::cout << " # Batch fetch highlighted snippets with JSON output" << std::endl; std::cout << " dfm-searcher preview \"screenshot\" img1.png img2.png -j" << std::endl; std::cout << std::endl; @@ -273,6 +282,13 @@ bool CliOptions::parse(QCoreApplication &app, SearchCliConfig &config) config.maxPreviewLength = previewLength; } } + if (m_parser.isSet(m_offsetOption)) { + bool ok; + int offsetVal = m_parser.value(m_offsetOption).toInt(&ok); + if (ok && offsetVal >= 0) { + config.offset = offsetVal; + } + } return true; } diff --git a/src/dfm-search/dfm-search-client/cli_options.h b/src/dfm-search/dfm-search-client/cli_options.h index d5823ad5..39f5f1f8 100644 --- a/src/dfm-search/dfm-search-client/cli_options.h +++ b/src/dfm-search/dfm-search-client/cli_options.h @@ -47,6 +47,7 @@ struct SearchCliConfig QStringList fileExtensions; int maxResults = 0; // 0 表示不限制 int maxPreviewLength = 200; + int offset = 0; // 内容偏移值,preview 子命令使用 // 文件名搜索选项 QString filenameKeyword; @@ -107,6 +108,7 @@ class CliOptions QCommandLineOption m_fileExtensionsOption; QCommandLineOption m_maxResultsOption; QCommandLineOption m_maxPreviewOption; + QCommandLineOption m_offsetOption; QCommandLineOption m_filenameOption; QCommandLineOption m_wildcardOption; QCommandLineOption m_jsonOption; diff --git a/src/dfm-search/dfm-search-client/main.cpp b/src/dfm-search/dfm-search-client/main.cpp index f4f434ee..22069de4 100644 --- a/src/dfm-search/dfm-search-client/main.cpp +++ b/src/dfm-search/dfm-search-client/main.cpp @@ -186,8 +186,10 @@ int main(int argc, char *argv[]) // Preview subcommand: fetch content preview on demand if (config.subcommand == "preview") { DFMSEARCH::ContentRetriever retriever; - DFMSEARCH::HighlightOptions hlOptions; - hlOptions.setMaxPreviewLength(config.maxPreviewLength); + DFMSEARCH::PreviewOptions previewOptions; + previewOptions.setOffset(config.offset); + previewOptions.setMaxLength(config.maxPreviewLength); + previewOptions.setKeyword(config.keyword); // Paths are stored as comma-separated in config.searchPath #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) @@ -196,8 +198,6 @@ int main(int argc, char *argv[]) QStringList paths = config.searchPath.split(',', QString::SkipEmptyParts); #endif - bool noKeyword = config.keyword.isEmpty(); - if (config.jsonOutput) { // JSON output QJsonObject root; @@ -205,15 +205,16 @@ int main(int argc, char *argv[]) root["searchType"] = (config.searchType == SearchType::Content) ? "content" : (config.searchType == SearchType::Ocr) ? "ocr" : "semantic"; root["keyword"] = config.keyword; + root["offset"] = config.offset; + root["maxLength"] = config.maxPreviewLength; QJsonArray results; for (const QString &path : paths) { QJsonObject item; item["path"] = path; - if (noKeyword) - item["content"] = retriever.fetchContent(path, config.searchType); - else - item["contentMatch"] = retriever.fetchHighlight(path, config.keyword, config.searchType, hlOptions); + DFMSEARCH::PreviewResult result = retriever.fetchPreview(path, config.searchType, previewOptions); + item["content"] = result.content(); + item["keywordOffset"] = result.keywordOffset(); results.append(item); } @@ -227,20 +228,11 @@ int main(int argc, char *argv[]) QTextStream out(stdout); for (const QString &path : paths) { out << path << "\n"; - if (noKeyword) { - QString content = retriever.fetchContent(path, config.searchType); - if (!content.isEmpty()) { - out << " " << content << "\n"; - } else { - out << " (no content)\n"; - } + DFMSEARCH::PreviewResult result = retriever.fetchPreview(path, config.searchType, previewOptions); + if (!result.content().isEmpty()) { + out << " " << result.content() << "\n"; } else { - QString hl = retriever.fetchHighlight(path, config.keyword, config.searchType, hlOptions); - if (!hl.isEmpty()) { - out << " " << hl << "\n"; - } else { - out << " (no match)\n"; - } + out << " (no content)\n"; } out << Qt::endl; } diff --git a/src/dfm-search/dfm-search-lib/utils/contenthighlighter.cpp b/src/dfm-search/dfm-search-lib/utils/contenthighlighter.cpp index e6f1c703..3b82ca7a 100644 --- a/src/dfm-search/dfm-search-lib/utils/contenthighlighter.cpp +++ b/src/dfm-search/dfm-search-lib/utils/contenthighlighter.cpp @@ -242,6 +242,35 @@ QString customHighlight(const QStringList &keywords, const QString &content, int return resultSnippet; } +QString previewSnippet(const QString &content, int offset, int maxLength, + const QString &keyword, int *keywordOffset) +{ + if (keywordOffset) { + *keywordOffset = -1; + } + + if (content.isEmpty() || maxLength <= 0 || offset < 0) { + return {}; + } + + if (keyword.isEmpty()) { + // 无 keyword:直接从 offset 截取 maxLength + return content.mid(offset, maxLength); + } + + // 有 keyword:从 offset 开始搜索,找到后从匹配位置截取 + int pos = content.indexOf(keyword, offset, Qt::CaseInsensitive); + if (pos == -1) { + return {}; + } + + if (keywordOffset) { + *keywordOffset = pos; + } + + return content.mid(pos, maxLength); +} + } // namespace ContentHighlighter DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/utils/contenthighlighter.h b/src/dfm-search/dfm-search-lib/utils/contenthighlighter.h index fbaffdd5..0595805f 100644 --- a/src/dfm-search/dfm-search-lib/utils/contenthighlighter.h +++ b/src/dfm-search/dfm-search-lib/utils/contenthighlighter.h @@ -40,6 +40,24 @@ namespace ContentHighlighter { */ QString customHighlight(const QStringList &keywords, const QString &content, int maxLength, bool enableHtml, int positioningMaxLength = 30); +/** + * @brief 精确预览:从 offset 位置截取内容,或从 offset 搜索 keyword 后从匹配位置截取 + * + * 无 keyword: content.mid(offset, maxLength) + * 有 keyword: content.indexOf(keyword, offset) → 从匹配位置截取 maxLength + * 不做 simplified,不加省略号,不高亮 + * + * @param content 原始文档内容 + * @param offset 内容偏移值(默认0) + * @param maxLength 最大截取长度 + * @param keyword 搜索关键词(可选) + * @param keywordOffset 输出 keyword 在全文中的匹配位置(-1 if 无 keyword 或未匹配) + * @return 截取的内容片段,找不到 keyword 或 offset 越界时返回空字符串 + */ +QString previewSnippet(const QString &content, int offset, int maxLength, + const QString &keyword = {}, + int *keywordOffset = nullptr); + } // namespace ContentHighlighter DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp b/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp index 9efd3e0a..1b3c0e24 100644 --- a/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp +++ b/src/dfm-search/dfm-search-lib/utils/contentretriever.cpp @@ -8,6 +8,8 @@ #include "utils/contenthighlighter.h" #include "utils/highlightoptions_p.h" +#include "utils/previewoptions_p.h" +#include "utils/previewresult_p.h" #include "utils/searchutility.h" #include @@ -165,6 +167,72 @@ void HighlightOptions::setEnableHtml(bool enable) d->enableHtml = enable; } +// ── PreviewOptions (Pimpl) ───────────────────────────────────────────── + +PreviewOptions::PreviewOptions() + : d(new PreviewOptionsPrivate) +{ +} + +PreviewOptions::~PreviewOptions() = default; + +PreviewOptions::PreviewOptions(const PreviewOptions &other) = default; + +PreviewOptions &PreviewOptions::operator=(const PreviewOptions &other) = default; + +int PreviewOptions::offset() const +{ + return d->offset; +} + +void PreviewOptions::setOffset(int offset) +{ + d->offset = offset; +} + +int PreviewOptions::maxLength() const +{ + return d->maxLength; +} + +void PreviewOptions::setMaxLength(int length) +{ + d->maxLength = length; +} + +QString PreviewOptions::keyword() const +{ + return d->keyword; +} + +void PreviewOptions::setKeyword(const QString &keyword) +{ + d->keyword = keyword; +} + +// ── PreviewResult (Pimpl) ────────────────────────────────────────────── + +PreviewResult::PreviewResult() + : d(new PreviewResultPrivate) +{ +} + +PreviewResult::~PreviewResult() = default; + +PreviewResult::PreviewResult(const PreviewResult &other) = default; + +PreviewResult &PreviewResult::operator=(const PreviewResult &other) = default; + +QString PreviewResult::content() const +{ + return d->content; +} + +int PreviewResult::keywordOffset() const +{ + return d->keywordOffset; +} + // ── ContentRetriever ─────────────────────────────────────────────────── struct ContentRetriever::Private @@ -453,4 +521,55 @@ QMap ContentRetriever::fetchContents(const QStringList &paths, return results; } +PreviewResult ContentRetriever::fetchPreview(const QString &path, SearchType type, + const PreviewOptions &options) const +{ + PreviewResult result; + if (path.isEmpty()) return result; + + // Route SearchType::Semantic to Content or Ocr based on file extension + if (type == SearchType::Semantic) { + std::optional resolved = resolveSemanticType(path); + if (!resolved) { + qWarning() << "ContentRetriever: cannot route Semantic type for" << path + << "- extension does not match doc or pic suffixes"; + return result; + } + type = *resolved; + } else if (type != SearchType::Content && type != SearchType::Ocr) { + return result; + } + + const QString indexDir = indexDirectory(type); + + QMutexLocker locker(&d->mutex); + CachedIndexContext *ctx = d->ensureIndexContext(type, indexDir); + if (!ctx || !ctx->searcher) { + return result; + } + + try { + const DocumentPtr doc = findDocumentByPath(ctx->searcher, path, type); + const QString content = storedContentFromDocument(doc, type); + if (content.isEmpty()) { + return result; + } + + int keywordOffset = -1; + const QString snippet = ContentHighlighter::previewSnippet( + content, options.offset(), options.maxLength(), + options.keyword(), &keywordOffset); + + result.d->content = snippet; + result.d->keywordOffset = keywordOffset; + } catch (const LuceneException &e) { + qWarning() << "ContentRetriever: error fetching preview for" << path + << QString::fromStdWString(e.getError()); + } catch (const std::exception &e) { + qWarning() << "ContentRetriever: std error for" << path << e.what(); + } + + return result; +} + DFM_SEARCH_END_NS diff --git a/src/dfm-search/dfm-search-lib/utils/previewoptions_p.h b/src/dfm-search/dfm-search-lib/utils/previewoptions_p.h new file mode 100644 index 00000000..9a4814b3 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/utils/previewoptions_p.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PREVIEWOPTIONS_P_H +#define PREVIEWOPTIONS_P_H + +#include +#include + +#include + +DFM_SEARCH_BEGIN_NS + +class PreviewOptionsPrivate : public QSharedData +{ +public: + PreviewOptionsPrivate() = default; + PreviewOptionsPrivate(const PreviewOptionsPrivate &other) = default; + + int offset = 0; ///< Content offset: start reading from the n-th character + int maxLength = 200; ///< Maximum snippet length in characters + QString keyword; ///< Search keyword (empty = raw offset read) +}; + +DFM_SEARCH_END_NS + +#endif // PREVIEWOPTIONS_P_H diff --git a/src/dfm-search/dfm-search-lib/utils/previewresult_p.h b/src/dfm-search/dfm-search-lib/utils/previewresult_p.h new file mode 100644 index 00000000..6dd21841 --- /dev/null +++ b/src/dfm-search/dfm-search-lib/utils/previewresult_p.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef PREVIEWRESULT_P_H +#define PREVIEWRESULT_P_H + +#include +#include + +#include + +DFM_SEARCH_BEGIN_NS + +class PreviewResultPrivate : public QSharedData +{ +public: + PreviewResultPrivate() = default; + PreviewResultPrivate(const PreviewResultPrivate &other) = default; + + QString content; ///< Extracted content snippet + int keywordOffset = -1; ///< Position of keyword in full content (-1 if none/not matched) +}; + +DFM_SEARCH_END_NS + +#endif // PREVIEWRESULT_P_H From 89ad96f2850194b2a24b90c2a9d96f6b94f27384 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Fri, 10 Jul 2026 14:45:03 +0800 Subject: [PATCH 2/2] docs: add dfm-searcher guide documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Added comprehensive documentation for dfm-searcher command line tool 2. Covers all major features including filename search, content search, OCR search, semantic search and preview functionality 3. Includes detailed examples for all command options and use cases 4. Provides JSON output structure documentation 5. Contains troubleshooting FAQ section This documentation serves as a complete reference guide for users and developers working with dfm-searcher tool. It explains both basic and advanced usage patterns with clear examples. The document is organized into three main parts covering search functionality, preview operations, and reference sections. Log: Added official documentation for dfm-searcher command line tool Influence: 1. Verify all example commands work as described 2. Test documentation coverage against actual tool functionality 3. Check formatting and accuracy of JSON output examples 4. Validate troubleshooting advice against real issues docs: 添加dfm-searcher使用指南文档 1. 添加了dfm-searcher命令行工具的完整文档 2. 涵盖包括文件名搜索、内容搜索、OCR搜索、语义搜索和预览功能等主要特性 3. 提供所有命令选项和使用场景的详细示例 4. 包含JSON输出结构文档 5. 提供故障排除FAQ部分 本文档作为dfm-searcher工具用户和开发者的完整参考指南。通过清晰示例解释基 础和高级使用模式。文档分为三大部分,涵盖搜索功能、预览操作和参考章节。 Log: 新增dfm-searcher命令行工具的官方文档 Influence: 1. 验证所有示例命令是否按描述工作 2. 测试文档覆盖范围与工具实际功能的匹配度 3. 检查JSON输出示例的格式和准确性 4. 验证故障排除建议是否解决实际问题 --- docs/dfm-searcher-guide.md | 922 +++++++++++++++++++++++++++++++++++++ 1 file changed, 922 insertions(+) create mode 100644 docs/dfm-searcher-guide.md diff --git a/docs/dfm-searcher-guide.md b/docs/dfm-searcher-guide.md new file mode 100644 index 00000000..196fe21a --- /dev/null +++ b/docs/dfm-searcher-guide.md @@ -0,0 +1,922 @@ +# dfm-searcher 使用指南 + +> 从零开始,学会用 `dfm-searcher` 搜索文件和预览文件内容。 + +--- + +## 它是什么 + +`dfm-searcher` 是深度文件管理器的搜索命令行工具,基于 Lucene++ 索引引擎,支持: + +- **按文件名搜索** — 用关键词、通配符、拼音找文件 +- **按文件内容搜索** — 在文档内容中搜索关键词 +- **按 OCR 文字搜索** — 在图片中识别的文字里搜索 +- **语义搜索** — 用自然语言描述需求,自动解析意图 +- **内容预览** — 按需读取已索引文件的内容片段 + +--- + +## 两种模式 + +`dfm-searcher` 有两种工作模式: + +| 模式 | 命令格式 | 用途 | +|------|---------|------| +| **搜索模式** | `dfm-searcher [选项] ` | 搜索目录下匹配的文件 | +| **预览模式** | `dfm-searcher preview [选项] [] [path...]` | 直接读取指定文件的内容片段 | + +--- + +# 第一部分:搜索 + +## 一、文件名搜索 + +### 1. 最基本的搜索 + +```bash +dfm-searcher "报告" /home/user +``` + +在 `/home/user` 目录下搜索文件名包含"报告"的文件。 + +输出示例: + +``` +/home/user/工作报告.txt +/home/user/2026年报告.docx +/home/user/reports/年度报告.pdf +``` + +### 2. 限制结果数量 + +```bash +dfm-searcher --max-results=10 "报告" /home/user +``` + +最多返回 10 条结果。`--max-results=0` 表示不限制(默认)。 + +### 3. 区分大小写 + +```bash +dfm-searcher --case-sensitive "README" /home/user +``` + +默认搜索不区分大小写。加上 `--case-sensitive` 后 "readme" 不会匹配 "README"。 + +### 4. 包含隐藏文件 + +```bash +dfm-searcher --include-hidden "config" /home/user +``` + +默认不搜索隐藏文件(以 `.` 开头的文件或目录)。加上此选项后包含隐藏文件。 + +### 5. 实时搜索 vs 索引搜索 + +```bash +# 索引搜索(默认):快,但依赖预建索引 +dfm-searcher "报告" /home/user + +# 实时搜索:慢,但不需要索引,直接遍历文件系统 +dfm-searcher --method=realtime "报告" /home/user +``` + +| 方法 | 速度 | 需要索引 | 适用场景 | +|------|------|:---:|------| +| `--method=indexed`(默认) | 快 | 是 | 日常使用 | +| `--method=realtime` | 慢 | 否 | 索引未覆盖的路径、最新文件 | + +--- + +## 二、内容搜索 + +### 6. 搜索文件内容 + +```bash +dfm-searcher --type=content "会议纪要" /home/user/Documents +``` + +在 `/home/user/Documents` 下搜索文件**内容**包含"会议纪要"的文件。 + +### 7. 搜索内容并显示预览 + +```bash +dfm-searcher --type=content -v "会议纪要" /home/user/Documents +``` + +加 `-v`(verbose)后,每条结果会附带内容预览片段(高亮显示关键词)。 + +### 8. 控制预览长度 + +```bash +dfm-searcher --type=content --max-preview=500 -v "会议" /home/user/Documents +``` + +预览片段最多 500 个字符(默认 200)。 + +--- + +## 三、OCR 搜索 + +### 9. 搜索图片中的文字 + +```bash +dfm-searcher --type=ocr "登录界面" /home/user/Pictures +``` + +在图片中搜索 OCR 识别出的文字。支持 PNG、JPG 等格式。 + +### 10. OCR 搜索 + 预览 + +```bash +dfm-searcher --type=ocr -v "验证码" /home/user/Pictures +``` + +输出会包含匹配的 OCR 文字片段。 + +--- + +## 四、通配符搜索 + +### 11. 用 * 和 ? 搜索 + +```bash +# 匹配所有以 "report" 开头的文件名 +dfm-searcher --wildcard "report*" /home/user + +# 匹配 "report" + 任意一个字符 + ".txt" +dfm-searcher --wildcard "report?.txt" /home/user + +# 匹配所有 .pdf 文件名 +dfm-searcher --wildcard "*.pdf" /home/user +``` + +| 通配符 | 含义 | 示例 | +|--------|------|------| +| `*` | 任意多个字符 | `report*` 匹配 report2026.txt、reports.pdf | +| `?` | 任意一个字符 | `report?` 匹配 reports、reportA,不匹配 reportAB | + +--- + +## 五、布尔搜索(多关键词) + +### 12. AND 搜索(所有关键词都要匹配) + +```bash +# 用逗号分隔:同时包含 "报告" 和 "2026" +dfm-searcher --query=boolean "报告,2026" /home/user + +# 用 & 分隔(同上) +dfm-searcher --query=boolean "报告&2026" /home/user +``` + +### 13. OR 搜索(任一关键词匹配即可) + +```bash +# 用 | 分隔:包含 "报告" 或 "总结" +dfm-searcher --query=boolean "报告|总结" /home/user +``` + +| 分隔符 | 逻辑 | 示例 | +|--------|------|------| +| `\|` | OR(任一匹配) | `"报告\|总结"` → 匹配报告或总结 | +| `&` | AND(全部匹配) | `"报告&2026"` → 同时包含报告和2026 | +| `,` | AND(全部匹配) | `"报告,2026"` → 同上,向后兼容 | + +--- + +## 六、拼音搜索 + +### 14. 全拼搜索 + +```bash +dfm-searcher --pinyin "baogao" /home/user +``` + +搜索文件名中拼音匹配 "baogao" 的文件,如"报告.txt"。 + +### 15. 拼音首字母搜索 + +```bash +dfm-searcher --pinyin-acronym "bg" /home/user +``` + +"bg" 匹配 "baogao"(报告)、"binggan"(饼干)等拼音首字母为 b+g 的文件名。 + +### 16. 拼音 + 布尔组合 + +```bash +dfm-searcher --pinyin --query=boolean "wendang,xinjian" /home/user +``` + +搜索文件名拼音同时匹配 "wendang"(文档)和 "xinjian"(新建)的文件。 + +--- + +## 七、文件类型过滤 + +### 17. 按类型过滤 + +```bash +dfm-searcher --file-types=doc,pic "报告" /home/user +``` + +只返回文档类型和图片类型的搜索结果。 + +支持的类型: + +| 类型 | 说明 | +|------|------| +| `app` | 应用程序文件 | +| `archive` | 压缩文件 | +| `audio` | 音频文件 | +| `doc` | 文档文件 | +| `pic` | 图片文件 | +| `video` | 视频文件 | +| `dir` | 目录 | +| `other` | 其他 | + +### 18. 按扩展名过滤 + +```bash +dfm-searcher --file-extensions=txt,pdf "报告" /home/user +``` + +只返回 .txt 和 .pdf 文件。 + +### 19. 类型 + 扩展名组合 + +```bash +dfm-searcher --file-types=doc --file-extensions=docx,odt "report" /home/user +``` + +--- + +## 八、时间范围过滤 + +### 20. 最近 N 天 + +```bash +# 最近 3 天内修改的文件 +dfm-searcher --time-last=3d "报告" /home/user + +# 最近 2 小时 +dfm-searcher --time-last=2h "报告" /home/user +``` + +支持的时间单位: + +| 单位 | 含义 | 示例 | +|------|------|------| +| `m` | 分钟 | `--time-last=30m`(最近 30 分钟) | +| `h` | 小时 | `--time-last=2h`(最近 2 小时) | +| `d` | 天 | `--time-last=7d`(最近 7 天) | +| `w` | 周 | `--time-last=2w`(最近 2 周) | +| `M` | 月 | `--time-last=3M`(最近 3 个月) | +| `y` | 年 | `--time-last=1y`(最近 1 年) | + +### 21. 预设时间范围 + +```bash +# 今天的文件 +dfm-searcher --time-today "报告" /home/user + +# 昨天的文件 +dfm-searcher --time-yesterday "报告" /home/user + +# 本周的文件 +dfm-searcher --time-this-week "报告" /home/user + +# 上个月的文件 +dfm-searcher --time-last-month "报告" /home/user + +# 今年的文件 +dfm-searcher --time-this-year "报告" /home/user +``` + +所有预设选项: + +`--time-today` / `--time-yesterday` / `--time-this-week` / `--time-last-week` / `--time-this-month` / `--time-last-month` / `--time-this-year` / `--time-last-year` + +### 22. 自定义时间范围 + +```bash +# 按日期范围 +dfm-searcher --time-range="2025-01-01,2025-12-31" "报告" /home/user + +# 按精确时间范围 +dfm-searcher --time-range="2025-06-01 09:00,2025-06-30 18:00" "报告" /home/user +``` + +### 23. 按创建时间 vs 修改时间 + +```bash +# 默认按修改时间(modify) +dfm-searcher --time-last=7d "报告" /home/user + +# 按创建时间(birth) +dfm-searcher --time-field=birth --time-last=7d "报告" /home/user +``` + +### 24. 时间 + 实时搜索组合 + +```bash +dfm-searcher --method=realtime --time-last=7d "报告" /home/user +``` + +--- + +## 九、文件大小过滤 + +### 25. 按大小范围过滤 + +```bash +# 1MB 到 100MB 之间的文件 +dfm-searcher --size-min=1M --size-max=100M "视频" /home/user +``` + +支持的大小单位: + +| 单位 | 含义 | 示例 | +|------|------|------| +| (无) | 字节 | `--size-min=512`(至少 512 字节) | +| `K` | KB | `--size-min=1K`(至少 1KB) | +| `M` | MB | `--size-min=10M`(至少 10MB) | +| `G` | GB | `--size-max=1G`(最多 1GB) | +| `T` | TB | `--size-max=1T`(最多 1TB) | + +### 26. 只限最小或最大 + +```bash +# 至少 10MB +dfm-searcher --size-min=10M "视频" /home/user + +# 最多 1KB +dfm-searcher --size-max=1K "日志" /home/user +``` + +--- + +## 十、语义搜索 + +### 27. 用自然语言搜索 + +```bash +dfm-searcher -s "最近3天的图片" /home/user +``` + +语义搜索会自动解析你的自然语言描述,提取时间、类型、关键词等维度,转化为搜索条件。 + +### 28. 语义搜索 + JSON + +```bash +dfm-searcher -s -j "最近3天的图片" /home/user +``` + +JSON 输出会包含解析出的意图(intent)信息。 + +### 29. 语义搜索不要求路径 + +```bash +dfm-searcher -s "包含会议纪要的文档" +``` + +语义模式可以不指定搜索路径。 + +### 30. 语义搜索示例集 + +```bash +# 查找最近图片 +dfm-searcher -s "最近的图片" /home/user + +# 查找上周的文档 +dfm-searcher -s "上周的文档" /home/user/Documents + +# 查找大视频 +dfm-searcher -s "大于1G的视频" /home/user + +# 查找特定内容 +dfm-searcher -s "内容包含会议纪要的文件" /home/user + +# 语义 + 详细输出 +dfm-searcher -s -v "最近3天的图片" /home/user +``` + +--- + +## 十一、输出格式 + +### 31. JSON 输出 + +```bash +dfm-searcher -j "报告" /home/user +``` + +输出结构化 JSON,方便脚本解析: + +```json +{ + "type": "search", + "searchType": "filename", + "keyword": "报告", + "totalResults": 2, + "results": [ + { + "path": "/home/user/工作报告.txt", + "filename": "工作报告.txt" + }, + { + "path": "/home/user/2026年报告.docx", + "filename": "2026年报告.docx" + } + ] +} +``` + +### 32. 详细输出(verbose) + +```bash +dfm-searcher -v --type=content "会议" /home/user/Documents +``` + +`-v` 启用详细模式,结果中会包含文件大小、修改时间、高亮内容预览等额外信息。 + +### 33. JSON + verbose 组合 + +```bash +dfm-searcher -j -v --type=content "会议" /home/user/Documents +``` + +--- + +## 十二、组合搜索 + +### 34. 文件名 + 文件类型 + 时间 + +```bash +dfm-searcher --file-types=doc --time-last=7d "报告" /home/user +``` + +最近 7 天内的文档类型文件,文件名包含"报告"。 + +### 35. 内容搜索 + 文件名过滤 + 大小过滤 + +```bash +dfm-searcher --type=content --filename="季度" --size-min=1M "财务" /home/user +``` + +在内容包含"财务"且文件名包含"季度"、大小至少 1MB 的文件中搜索。 + +### 36. 实时搜索 + 时间 + 大小 + 类型 + +```bash +dfm-searcher --method=realtime --time-last=30d --size-min=10M --file-types=video "监控" /home/user +``` + +--- + +# 第二部分:内容预览(preview 子命令) + +`preview` 子命令用来**直接读取**指定文件的内容片段,不需要运行完整搜索。 + +## 十三、基本用法 + +### 37. 读取文件开头内容 + +```bash +dfm-searcher preview /home/user/notes.txt +``` + +默认读取前 200 个字符。 + +``` +/home/user/notes.txt + 2026年7月10日 工作计划:1.完成需求文档 2.代码评审 3.修复bug +``` + +### 38. 读取文件内容(JSON 格式) + +```bash +dfm-searcher preview /home/user/notes.txt -j +``` + +```json +{ + "type": "preview", + "searchType": "semantic", + "keyword": "", + "offset": 0, + "maxLength": 200, + "totalResults": 1, + "results": [ + { + "path": "/home/user/notes.txt", + "content": "2026年7月10日 工作计划:1.完成需求文档 2.代码评审 3.修复bug", + "keywordOffset": -1 + } + ] +} +``` + +--- + +## 十四、用 --offset 和 --max-preview 控制读取范围 + +### 39. 从第 100 个字符开始读 + +```bash +dfm-searcher preview /home/user/notes.txt --offset=100 +``` + +### 40. 只读 50 个字符 + +```bash +dfm-searcher preview /home/user/notes.txt --max-preview=50 +``` + +### 41. 从第 100 个字符开始,只读 50 个字符 + +```bash +dfm-searcher preview /home/user/notes.txt --offset=100 --max-preview=50 +``` + +### 42. 翻页式读取 + +第一次读取: + +```bash +dfm-searcher preview /home/user/notes.txt --max-preview=100 -j +``` + +第二次,offset=100: + +```bash +dfm-searcher preview /home/user/notes.txt --offset=100 --max-preview=100 -j +``` + +第三次,offset=200: + +```bash +dfm-searcher preview /home/user/notes.txt --offset=200 --max-preview=100 -j +``` + +以此类推,每次读 100 个字符。 + +### 43. offset 超过文件长度 → 返回空 + +```bash +dfm-searcher preview /home/user/notes.txt --offset=99999 -j +``` + +```json +{ + "results": [ + { + "content": "", + "keywordOffset": -1 + } + ] +} +``` + +空内容说明已读到文件末尾。 + +--- + +## 十五、用 keyword 搜索并预览 + +### 44. 搜索关键词并预览 + +```bash +dfm-searcher preview "bug" /home/user/notes.txt +``` + +``` +/home/user/notes.txt + bug修复:修复了文件管理器在删除大量文件时的崩溃问题 +``` + +内容从关键词所在位置开始,往后最多 200 个字符。 + +### 45. 搜索关键词 + JSON + +```bash +dfm-searcher preview "bug" /home/user/notes.txt -j +``` + +```json +{ + "type": "preview", + "keyword": "bug", + "offset": 0, + "maxLength": 200, + "results": [ + { + "path": "/home/user/notes.txt", + "content": "bug修复:修复了文件管理器在删除大量文件时的崩溃问题", + "keywordOffset": 156 + } + ] +} +``` + +`keywordOffset: 156` 表示 "bug" 在全文第 156 个字符处。 + +### 46. 从指定位置开始搜索关键词 + +如果 "bug" 在文件里出现多次,用 `--offset` 跳过前面的出现: + +```bash +# 从第 200 个字符开始搜索 "bug" +dfm-searcher preview "bug" /home/user/notes.txt --offset=200 +``` + +### 47. offset 后面没有关键词 → 返回空 + +```bash +dfm-searcher preview "bug" /home/user/notes.txt --offset=500 -j +``` + +```json +{ + "results": [ + { + "content": "", + "keywordOffset": -1 + } + ] +} +``` + +### 48. 搭配搜索结果使用 + +搜索返回 `keywordOffset`,可以用它作为 preview 的 `--offset`: + +```bash +# 假设搜索返回 keywordOffset=320 +dfm-searcher preview "关键词" /home/user/doc.txt --offset=320 --max-preview=200 -j +``` + +--- + +## 十六、批量预览多个文件 + +### 49. 同时预览多个文件 + +```bash +dfm-searcher preview "报告" doc1.txt doc2.txt doc3.txt +``` + +``` +/home/user/doc1.txt + 报告:2026年Q1财务总结… +/home/user/doc2.txt + 报告:2026年Q2项目进展… +/home/user/doc3.txt + (no content) +``` + +### 50. 批量预览 + JSON + +```bash +dfm-searcher preview "报告" doc1.txt doc2.txt doc3.txt -j +``` + +```json +{ + "type": "preview", + "keyword": "报告", + "offset": 0, + "maxLength": 200, + "totalResults": 3, + "results": [ + { + "path": "/home/user/doc1.txt", + "content": "报告:2026年Q1财务总结…", + "keywordOffset": 0 + }, + { + "path": "/home/user/doc2.txt", + "content": "报告:2026年Q2项目进展…", + "keywordOffset": 5 + }, + { + "path": "/home/user/doc3.txt", + "content": "", + "keywordOffset": -1 + } + ] +} +``` + +--- + +## 十七、搜索类型 + +### 51. 自动检测(默认) + +不给 `--type`,程序根据文件扩展名自动判断: +- `.txt` `.doc` `.md` 等 → 用内容索引(content) +- `.png` `.jpg` 等 → 用 OCR 索引(ocr) + +### 52. 强制指定搜索类型 + +```bash +# 强制用 OCR 索引 +dfm-searcher preview --type=ocr "截图" photo.png + +# 强制用内容索引 +dfm-searcher preview --type=content "会议" doc.txt +``` + +### 53. 预览 OCR 图片中的文字 + +```bash +dfm-searcher preview "登录" screenshot.png -j +``` + +```json +{ + "type": "preview", + "searchType": "ocr", + "keyword": "登录", + "results": [ + { + "path": "screenshot.png", + "content": "登录界面请输入用户名和密码", + "keywordOffset": 0 + } + ] +} +``` + +--- + +## 十八、无关键词,纯读取内容 + +### 54. 读取文件前 200 个字符 + +```bash +dfm-searcher preview /home/user/doc.txt +``` + +### 55. 从第 500 个字符读取 100 个字符 + +```bash +dfm-searcher preview /home/user/doc.txt --offset=500 --max-preview=100 +``` + +### 56. 无关键词 + JSON + +```bash +dfm-searcher preview /home/user/doc.txt --offset=500 --max-preview=100 -j +``` + +```json +{ + "keyword": "", + "offset": 500, + "maxLength": 100, + "results": [ + { + "content": "这里是第500到600个字符之间的原始内容", + "keywordOffset": -1 + } + ] +} +``` + +`keywordOffset: -1` 因为没有给关键词。 + +--- + +# 第三部分:参考 + +## 选项速查表 + +### 搜索模式选项 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| `--type=` | `filename` | 搜索类型 | +| `--method=` | `indexed` | 搜索方法 | +| `--query=` | `simple` | 查询类型 | +| `--wildcard` | 关 | 启用通配符搜索 | +| `--case-sensitive` | 关 | 区分大小写 | +| `--include-hidden` | 关 | 包含隐藏文件 | +| `--pinyin` | 关 | 启用拼音搜索 | +| `--pinyin-acronym` | 关 | 启用拼音首字母搜索 | +| `--file-types=` | 无 | 按文件类型过滤 | +| `--file-extensions=` | 无 | 按扩展名过滤 | +| `--max-results=` | `0`(不限) | 最大结果数 | +| `--max-preview=` | `200` | 内容预览长度 | +| `--filename=` | 无 | 在内容/OCR 搜索中按文件名过滤 | +| `--semantic`, `-s` | 关 | 启用语义搜索 | +| `--time-field=` | `modify` | 时间字段 | +| `--time-last=` | 无 | 最近 N 时间段 | +| `--time-today` 等预设 | 无 | 预设时间范围 | +| `--time-range=,` | 无 | 自定义时间范围 | +| `--size-min=` | 无 | 最小文件大小 | +| `--size-max=` | 无 | 最大文件大小 | +| `--json`, `-j` | 关 | JSON 输出 | +| `--verbose`, `-v` | 关 | 详细输出 | +| `--version` | — | 显示版本 | +| `--help` | — | 显示帮助 | + +### Preview 模式选项 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| `--offset=` | `0` | 从文档第几个字符开始阅读/搜索 | +| `--max-preview=` | `200` | 本次最多读取的字符数 | +| `--type=` | 自动 | 强制指定搜索类型 | +| `-j` / `--json` | 关 | JSON 输出 | + +--- + +## JSON 输出字段说明 + +### 搜索模式 JSON + +| 字段 | 位置 | 说明 | +|------|------|------| +| `type` | 顶层 | `"search"` | +| `searchType` | 顶层 | `"filename"` / `"content"` / `"ocr"` / `"semantic"` | +| `keyword` | 顶层 | 搜索关键词 | +| `totalResults` | 顶层 | 结果总数 | +| `path` | result 项 | 文件路径 | +| `filename` | result 项 | 文件名(verbose 模式) | +| `highlightedContent` | result 项 | 高亮内容预览(content/ocr + verbose) | + +### Preview 模式 JSON + +| 字段 | 位置 | 说明 | +|------|------|------| +| `type` | 顶层 | `"preview"` | +| `searchType` | 顶层 | `"content"` / `"ocr"` / `"semantic"` | +| `keyword` | 顶层 | 搜索关键词,空字符串表示无关键词 | +| `offset` | 顶层 | 本次使用的偏移量 | +| `maxLength` | 顶层 | 本次最大读取长度 | +| `totalResults` | 顶层 | 结果总数(等于文件数) | +| `path` | result 项 | 文件路径 | +| `content` | result 项 | 截取到的内容片段 | +| `keywordOffset` | result 项 | keyword 在全文中的位置(-1 = 无 keyword 或未匹配) | + +--- + +## 字符计数说明 + +`--offset` 以**字符**(QChar)为单位,不是字节。 + +一个汉字算一个字符,一个换行符 `\n` 也算一个字符。 + +例如: + +``` +你好\n世界 +``` + +| 位置 | 字符 | +|------|------| +| 0 | 你 | +| 1 | 好 | +| 2 | \n | +| 3 | 世 | +| 4 | 界 | + +要从"世"开始读,`--offset=3`。 + +--- + +## 常见问题 + +### Q: preview 报 "(no content)" 是什么意思? + +可能原因: +1. 文件未被索引(需要先建索引) +2. offset 超出文件长度 +3. 给了 keyword 但文件中没有这个词(或在 offset 之后没有) + +### Q: preview 和普通搜索有什么区别? + +普通搜索搜索整个目录返回匹配文件列表。preview 不搜索,而是直接读取指定文件的内容片段。 + +### Q: 可以预览任意文件吗? + +不能。preview 只能读取已被索引的文件。文件需要先通过文件管理器的内容索引功能建立索引。 + +### Q: --offset 是字节还是字符? + +是字符(QChar)。一个中文字算一个字符,一个换行符也算一个字符。不是字节。 + +### Q: 索引搜索找不到文件怎么办? + +可以改用实时搜索:`--method=realtime`,它会直接遍历文件系统,不需要索引。 + +### Q: --version 和 -v 冲突吗? + +不冲突。`--version` 是长选项显示版本,`-v` 是 `--verbose` 的短选项。两者不冲突。 + +### Q: 选项可以放在任何位置吗? + +可以。`dfm-searcher --type=content "hello" /home/user` 和 `dfm-searcher "hello" /home/user --type=content` 效果一样。