Skip to content

Commit 3add744

Browse files
committed
feat: add JSON output option for search client
Added JSON output format support to the dfm-search client with --json flag 1. Implemented two JSON output modes: streaming for realtime search and complete for indexed search 2. Added JSON helper functions for converting search results to structured JSON objects 3. Enhanced command line parser with --json/-j option 4. Maintained backward compatibility with existing text output format 5. Included search metadata, timestamps, and error handling in JSON format Log: Added JSON output option for search results with --json flag Influence: 1. Test both text and JSON output modes with various search types 2. Verify realtime search produces streaming JSON events 3. Check indexed search generates complete JSON document 4. Validate JSON structure contains all required fields 5. Test error handling produces proper JSON error objects 6. Confirm backward compatibility with existing text output feat: 为搜索客户端添加 JSON 输出选项 为 dfm-search 客户端添加了 JSON 输出格式支持,通过 --json 标志启用 1. 实现了两种 JSON 输出模式:实时搜索的流式输出和索引搜索的完整输出 2. 添加了将搜索结果转换为结构化 JSON 对象的辅助函数 3. 在命令行解析器中增加了 --json/-j 选项 4. 保持与现有文本输出格式的向后兼容性 5. JSON 格式包含搜索元数据、时间戳和错误处理 Log: 新增 --json 选项支持搜索结果 JSON 格式输出 Influence: 1. 测试不同搜索类型的文本和 JSON 输出模式 2. 验证实时搜索是否产生流式 JSON 事件 3. 检查索引搜索是否生成完整的 JSON 文档 4. 验证 JSON 结构是否包含所有必需字段 5. 测试错误处理是否产生正确的 JSON 错误对象 6. 确认与现有文本输出的向后兼容性
1 parent a9fde56 commit 3add744

1 file changed

Lines changed: 271 additions & 37 deletions

File tree

  • src/dfm-search/dfm-search-client

src/dfm-search/dfm-search-client/main.cpp

Lines changed: 271 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
#include <QStringList>
1212
#include <QFileInfo>
1313
#include <QDir>
14+
#include <QJsonObject>
15+
#include <QJsonDocument>
16+
#include <QJsonArray>
17+
#include <QDateTime>
18+
#include <QTimer>
1419

1520
#include <dfm-search/dsearch_global.h>
1621
#include <dfm-search/searchengine.h>
@@ -74,6 +79,7 @@ void printUsage()
7479
std::cout << " --file-extensions=<exts> Filter by file extensions, comma separated" << std::endl;
7580
std::cout << " --max-results=<number> Maximum number of results" << std::endl;
7681
std::cout << " --max-preview=<length> Max content preview length (for content search)" << std::endl;
82+
std::cout << " --json, -j Output results in JSON format" << std::endl;
7783
std::cout << " --help Display this help" << std::endl;
7884
}
7985

@@ -100,6 +106,133 @@ void printSearchResult(const SearchResult &result, SearchType searchType)
100106
std::cout << std::endl;
101107
}
102108

109+
//--------------------------------------------------------------------
110+
// JSON Output Helpers
111+
//--------------------------------------------------------------------
112+
113+
QJsonValue resultToJson(const SearchResult &result, SearchType searchType)
114+
{
115+
if (searchType == SearchType::FileName) {
116+
// 文件名搜索:直接返回路径字符串
117+
return result.path();
118+
} else if (searchType == SearchType::Content) {
119+
// 内容搜索:返回包含路径和内容匹配的对象
120+
QJsonObject obj;
121+
obj["path"] = result.path();
122+
ContentResultAPI contentResult(const_cast<SearchResult &>(result));
123+
obj["contentMatch"] = contentResult.highlightedContent();
124+
return obj;
125+
}
126+
return result.path();
127+
}
128+
129+
void printJsonLine(const QJsonObject &obj)
130+
{
131+
QJsonDocument doc(obj);
132+
std::cout << doc.toJson(QJsonDocument::Compact).constData() << std::endl;
133+
}
134+
135+
// 流式模式:输出搜索开始
136+
void printJsonSearchStart(const QString &keyword, const QString &searchPath,
137+
SearchType searchType, SearchMethod searchMethod,
138+
const SearchOptions &options)
139+
{
140+
QJsonObject startObj;
141+
startObj["type"] = "search_started";
142+
143+
QJsonObject searchInfo;
144+
searchInfo["keyword"] = keyword;
145+
searchInfo["searchPath"] = searchPath;
146+
searchInfo["searchType"] = (searchType == SearchType::FileName ? "filename" : "content");
147+
searchInfo["searchMethod"] = (searchMethod == SearchMethod::Indexed ? "indexed" : "realtime");
148+
searchInfo["caseSensitive"] = options.caseSensitive();
149+
searchInfo["includeHidden"] = options.includeHidden();
150+
151+
startObj["search"] = searchInfo;
152+
startObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
153+
154+
printJsonLine(startObj);
155+
}
156+
157+
// 流式模式:输出单个结果
158+
void printJsonResult(const SearchResult &result, SearchType searchType)
159+
{
160+
QJsonObject resultObj;
161+
resultObj["type"] = "result";
162+
resultObj["data"] = resultToJson(result, searchType);
163+
printJsonLine(resultObj);
164+
}
165+
166+
// 流式模式:输出搜索结束
167+
void printJsonSearchEnd(int totalResults, qint64 elapsedMs)
168+
{
169+
QJsonObject endObj;
170+
endObj["type"] = "search_finished";
171+
172+
QJsonObject status;
173+
status["state"] = "success";
174+
status["totalResults"] = totalResults;
175+
176+
endObj["status"] = status;
177+
178+
QJsonObject timestamps;
179+
timestamps["finished"] = QDateTime::currentDateTime().toString(Qt::ISODate);
180+
timestamps["duration"] = elapsedMs;
181+
182+
endObj["timestamps"] = timestamps;
183+
printJsonLine(endObj);
184+
}
185+
186+
// 完整 JSON 输出(非流式)
187+
struct JsonOutputContext
188+
{
189+
QString keyword;
190+
QString searchPath;
191+
SearchType searchType;
192+
SearchMethod searchMethod;
193+
SearchOptions options;
194+
QDateTime startTime;
195+
QJsonArray results;
196+
};
197+
198+
void printJsonComplete(const JsonOutputContext &ctx)
199+
{
200+
QJsonObject root;
201+
202+
// 搜索信息
203+
QJsonObject searchInfo;
204+
searchInfo["keyword"] = ctx.keyword;
205+
searchInfo["searchPath"] = ctx.searchPath;
206+
searchInfo["searchType"] = (ctx.searchType == SearchType::FileName ? "filename" : "content");
207+
searchInfo["searchMethod"] = (ctx.searchMethod == SearchMethod::Indexed ? "indexed" : "realtime");
208+
searchInfo["caseSensitive"] = ctx.options.caseSensitive();
209+
searchInfo["includeHidden"] = ctx.options.includeHidden();
210+
root["search"] = searchInfo;
211+
212+
// 时间戳
213+
QDateTime endTime = QDateTime::currentDateTime();
214+
qint64 duration = ctx.startTime.msecsTo(endTime);
215+
216+
QJsonObject timestamps;
217+
timestamps["started"] = ctx.startTime.toString(Qt::ISODate);
218+
timestamps["finished"] = endTime.toString(Qt::ISODate);
219+
timestamps["duration"] = duration;
220+
root["timestamps"] = timestamps;
221+
222+
// 状态
223+
QJsonObject status;
224+
status["state"] = "success";
225+
status["totalResults"] = ctx.results.size();
226+
root["status"] = status;
227+
228+
// 结果数组
229+
root["results"] = ctx.results;
230+
231+
// 输出完整 JSON
232+
QJsonDocument doc(root);
233+
std::cout << doc.toJson(QJsonDocument::Indented).constData() << std::endl;
234+
}
235+
103236
void testGlobal()
104237
{
105238
std::cout << "================= test global start =================" << std::endl;
@@ -417,6 +550,9 @@ int main(int argc, char *argv[])
417550
QCommandLineOption maxResultsOption(QStringList() << "max-results", "Maximum number of results", "number", "100");
418551
QCommandLineOption maxPreviewOption(QStringList() << "max-preview", "Max content preview length", "length", "200");
419552
QCommandLineOption wildcardOption(QStringList() << "wildcard", "Enable wildcard search with * and ? patterns");
553+
QCommandLineOption jsonOption(QStringList() << "json"
554+
<< "j",
555+
"Output results in JSON format");
420556

421557
parser.addOption(typeOption);
422558
parser.addOption(methodOption);
@@ -430,6 +566,7 @@ int main(int argc, char *argv[])
430566
parser.addOption(maxResultsOption);
431567
parser.addOption(maxPreviewOption);
432568
parser.addOption(wildcardOption);
569+
parser.addOption(jsonOption);
433570

434571
// Setup positional arguments
435572
parser.addPositionalArgument("keyword", "Search keyword");
@@ -576,46 +713,143 @@ int main(int argc, char *argv[])
576713
query.setBooleanOperator(SearchQuery::BooleanOperator::AND);
577714
}
578715

716+
// 检测是否使用 JSON 输出模式
717+
bool useJsonOutput = parser.isSet(jsonOption);
718+
719+
// JSON 模式上下文(用于非流式模式)
720+
JsonOutputContext jsonContext;
721+
if (useJsonOutput && searchMethod == SearchMethod::Indexed) {
722+
// 非流式模式:收集所有结果后统一输出
723+
jsonContext.keyword = keyword;
724+
jsonContext.searchPath = searchPath;
725+
jsonContext.searchType = searchType;
726+
jsonContext.searchMethod = searchMethod;
727+
jsonContext.options = options;
728+
jsonContext.startTime = QDateTime::currentDateTime();
729+
}
730+
579731
// Connect signals
580-
QObject::connect(engine, &SearchEngine::searchStarted, [] {
581-
std::cout << "Search started..." << std::endl;
582-
});
583-
584-
QObject::connect(engine, &SearchEngine::resultsFound, [searchType](const SearchResultList &results) {
585-
for (const auto &result : results)
586-
printSearchResult(result, searchType);
587-
});
588-
589-
QObject::connect(engine, &SearchEngine::searchFinished, [options, searchType](const QList<SearchResult> &results) {
590-
std::cout << "Search finished. Total results: " << results.size() << std::endl;
591-
if (!options.resultFoundEnabled()) {
592-
std::for_each(results.begin(), results.end(), [searchType](const SearchResult &result) {
593-
printSearchResult(result, searchType);
732+
if (useJsonOutput) {
733+
// JSON 输出模式
734+
if (searchMethod == SearchMethod::Realtime) {
735+
// 流式 JSON 输出(实时搜索)
736+
QObject::connect(engine, &SearchEngine::searchStarted, [keyword, searchPath, searchType, searchMethod, &options]() {
737+
printJsonSearchStart(keyword, searchPath, searchType, searchMethod, options);
738+
});
739+
740+
QObject::connect(engine, &SearchEngine::resultsFound, [searchType](const SearchResultList &results) {
741+
for (const auto &result : results)
742+
printJsonResult(result, searchType);
743+
});
744+
745+
// 记录搜索开始时间
746+
QDateTime searchStartTime = QDateTime::currentDateTime();
747+
748+
QObject::connect(engine, &SearchEngine::searchFinished, [searchStartTime](const QList<SearchResult> &results) {
749+
qint64 elapsedMs = searchStartTime.msecsTo(QDateTime::currentDateTime());
750+
printJsonSearchEnd(results.size(), elapsedMs);
751+
QCoreApplication::quit();
752+
});
753+
754+
QObject::connect(engine, &SearchEngine::searchCancelled, [] {
755+
QJsonObject cancelObj;
756+
cancelObj["type"] = "search_cancelled";
757+
cancelObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
758+
printJsonLine(cancelObj);
759+
QCoreApplication::quit();
760+
});
761+
762+
QObject::connect(engine, &SearchEngine::errorOccurred, [](const DFMSEARCH::SearchError &error) {
763+
QJsonObject errorObj;
764+
errorObj["type"] = "error";
765+
errorObj["name"] = error.name();
766+
errorObj["message"] = error.message();
767+
errorObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
768+
printJsonLine(errorObj);
769+
});
770+
771+
} else {
772+
// 完整 JSON 输出(索引搜索)
773+
QObject::connect(engine, &SearchEngine::searchStarted, []() {
774+
// 非流式模式:开始时不输出
775+
});
776+
777+
QObject::connect(engine, &SearchEngine::resultsFound, [&jsonContext, searchType](const SearchResultList &results) {
778+
for (const auto &result : results) {
779+
jsonContext.results.append(resultToJson(result, searchType));
780+
}
781+
});
782+
783+
QObject::connect(engine, &SearchEngine::searchFinished, [&jsonContext, searchType](const QList<SearchResult> &results) {
784+
// 如果结果没有被 resultsFound 收集(禁用了 resultFoundEnabled),则在这里转换
785+
if (jsonContext.results.isEmpty() && !results.isEmpty()) {
786+
for (const auto &result : results) {
787+
jsonContext.results.append(resultToJson(result, searchType));
788+
}
789+
}
790+
printJsonComplete(jsonContext);
791+
QCoreApplication::quit();
792+
});
793+
794+
QObject::connect(engine, &SearchEngine::searchCancelled, [] {
795+
QJsonObject cancelObj;
796+
cancelObj["type"] = "search_cancelled";
797+
cancelObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
798+
printJsonLine(cancelObj);
799+
QCoreApplication::quit();
594800
});
801+
802+
QObject::connect(engine, &SearchEngine::errorOccurred, [](const DFMSEARCH::SearchError &error) {
803+
QJsonObject errorObj;
804+
errorObj["type"] = "error";
805+
errorObj["name"] = error.name();
806+
errorObj["message"] = error.message();
807+
errorObj["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
808+
printJsonLine(errorObj);
809+
});
810+
}
811+
} else {
812+
// 文本输出模式(原有逻辑)
813+
QObject::connect(engine, &SearchEngine::searchStarted, [] {
814+
std::cout << "Search started..." << std::endl;
815+
});
816+
817+
QObject::connect(engine, &SearchEngine::resultsFound, [searchType](const SearchResultList &results) {
818+
for (const auto &result : results)
819+
printSearchResult(result, searchType);
820+
});
821+
822+
QObject::connect(engine, &SearchEngine::searchFinished, [options, searchType](const QList<SearchResult> &results) {
823+
std::cout << "Search finished. Total results: " << results.size() << std::endl;
824+
if (!options.resultFoundEnabled()) {
825+
std::for_each(results.begin(), results.end(), [searchType](const SearchResult &result) {
826+
printSearchResult(result, searchType);
827+
});
828+
}
829+
QCoreApplication::quit();
830+
});
831+
832+
QObject::connect(engine, &SearchEngine::searchCancelled, [] {
833+
std::cout << "Search cancelled" << std::endl;
834+
QCoreApplication::quit();
835+
});
836+
837+
QObject::connect(engine, &SearchEngine::errorOccurred, [](const DFMSEARCH::SearchError &error) {
838+
std::cerr << "[Error]: " << error.code()
839+
<< "[Name]: " << error.name().toStdString()
840+
<< "[Message]:" << error.message().toStdString() << std::endl;
841+
});
842+
843+
// Start search
844+
std::cout << "Searching for: " << keyword.toStdString() << std::endl;
845+
std::cout << "In path: " << searchPath.toStdString() << std::endl;
846+
std::cout << "Search type: " << (searchType == SearchType::FileName ? "Filename" : "Content") << std::endl;
847+
std::cout << "Search method: " << (searchMethod == SearchMethod::Indexed ? "Indexed" : "Realtime") << std::endl;
848+
849+
// Print file extensions if set
850+
if (searchType == SearchType::FileName && parser.isSet(fileExtensionsOption)) {
851+
std::cout << "File extensions filter: " << parser.value(fileExtensionsOption).toStdString() << std::endl;
595852
}
596-
QCoreApplication::quit();
597-
});
598-
599-
QObject::connect(engine, &SearchEngine::searchCancelled, [] {
600-
std::cout << "Search cancelled" << std::endl;
601-
QCoreApplication::quit();
602-
});
603-
604-
QObject::connect(engine, &SearchEngine::errorOccurred, [](const DFMSEARCH::SearchError &error) {
605-
std::cerr << "[Error]: " << error.code()
606-
<< "[Name]: " << error.name().toStdString()
607-
<< "[Message]:" << error.message().toStdString() << std::endl;
608-
});
609-
610-
// Start search
611-
std::cout << "Searching for: " << keyword.toStdString() << std::endl;
612-
std::cout << "In path: " << searchPath.toStdString() << std::endl;
613-
std::cout << "Search type: " << (searchType == SearchType::FileName ? "Filename" : "Content") << std::endl;
614-
std::cout << "Search method: " << (searchMethod == SearchMethod::Indexed ? "Indexed" : "Realtime") << std::endl;
615-
616-
// Print file extensions if set
617-
if (searchType == SearchType::FileName && parser.isSet(fileExtensionsOption)) {
618-
std::cout << "File extensions filter: " << parser.value(fileExtensionsOption).toStdString() << std::endl;
619853
}
620854

621855
engine->search(query);

0 commit comments

Comments
 (0)