package com.hanserwei.airobot.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.hanserwei.airobot.model.dto.SearchResultDTO; import com.hanserwei.airobot.service.SearXNGService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** * SearXNG 搜索服务实现类 *

* 该类通过调用 SearXNG 的 API 实现聚合搜索引擎功能,支持从多个搜索引擎获取结果并按评分排序。 *

*/ @Service @Slf4j public class SearXNGServiceImpl implements SearXNGService { @Resource private OkHttpClient okHttpClient; @Resource private ObjectMapper objectMapper; @Value("${searxng.url}") private String searxngUrl; @Value("${searxng.count}") private int count; /** * 根据关键词执行搜索操作 * * @param query 搜索关键词,不能为空 * @return 搜索结果列表,每个元素包含 URL 和评分;若发生异常或无结果则返回空列表 */ @Override public List search(String query) { // 构建 SearXNG API 请求 URL HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse(searxngUrl)).newBuilder() .addQueryParameter("q", query) // 设置搜索关键词 .addQueryParameter("format", "json") // 指定返回 JSON 格式 .addQueryParameter("engines", "wolframalpha,presearch,seznam,mwmbl,encyclosearch,bpb,mojeek,right dao,wikimini,crowdview,searchmysite,bing,naver,360search") // 指定聚合的目标搜索引擎(配置本地网络能够访问的通的搜索引擎) .build(); // 创建 HTTP GET 请求 Request request = new Request.Builder() .url(httpUrl) .get() .build(); // 发送 HTTP 请求 try (Response response = okHttpClient.newCall(request).execute()) { // 判断请求是否成功 if (response.isSuccessful()) { // 拿到返回结果 String result = response.body().string(); log.info("## SearXNG 搜索结果: {}", result); // 解析 JSON 响应 JsonNode root = objectMapper.readTree(result); JsonNode results = root.get("results"); // 获取结果数组节点 // 定义 Record 类型:用于临时存储分数和节点引用 record NodeWithUrlAndScore(double score, JsonNode node) { } // 处理搜索结果流: // 1. 提取评分 // 2. 按评分降序排序 // 3. 限制返回结果数量 List nodesWithScore = StreamSupport.stream(results.spliterator(), false) .map(node -> { // 只提取分数,避免构建完整对象 double score = node.path("score").asDouble(0.0); // 提取评分 return new NodeWithUrlAndScore(score, node); }) .sorted(Comparator.comparingDouble(NodeWithUrlAndScore::score).reversed()) // 按评分降序 .limit(count) // 限制返回结果数量 .toList(); // 转换为 SearchResult 对象集合 return nodesWithScore.stream() .map(n -> { JsonNode node = n.node(); String originalUrl = node.path("url").asText(""); // 提取 URL return SearchResultDTO.builder() .url(originalUrl) .score(n.score()) // 保留评分 .build(); }) .collect(Collectors.toList()); } } catch (Exception e) { log.error("", e); } // 返回空集合 return Collections.emptyList(); } }