ES-仿京东搜索

2023-09-30

项目说明

本项目是一个基于ES基本知识和原生api使用构建的仿京东搜索开源项目,本人在B站上寻找学习elastsearch找到的相关教程视频,看完视频后自己敲出来的代码,也都一一实现了功能。

本人学习博客:https://bo-vane.github.io/

项目技术环境

  • springboot+vue前后端分离
  • 使用Jsoup爬取京东数据
  • docker 安装elasticsearch
  • docker安装kibana
  • springboot集成es实现高亮搜索

项目成果展示

image-20231126125438017

点击搜索:

image-20231126125632069

爬虫(jsoup)

数据获取:数据库、消息队列、爬虫、…

①搜索京东搜索页面,并分析页面

http://search.jd.com/search?keyword=java
页面如下

image-20231121151422606

审查页面元素

image-20231121154712989

页面列表id:J_goodsList

②爬取数据(获取请求返回的页面信息,筛选出可用的)

创建HtmlParseUtil,并简单编写
public class HtmlParseUtil {
    public static void main(String[] args) throws IOException {
        /// 使用前需要联网
        // 请求url
        String url = "http://search.jd.com/search?keyword=java";
        // 1.解析网页(jsoup 解析返回的对象是浏览器Document对象)
        Document document = Jsoup.parse(new URL(url), 30000);
        // 使用document可以使用在js对document的所有操作
        // 2.获取元素(通过id)
        Element j_goodsList = document.getElementById("J_goodsList");
        // 3.获取J_goodsList ul 每一个 li
        Elements lis = j_goodsList.getElementsByTag("li");
        // 4.获取li下的 img、price、name
        for (Element li : lis) {
            String img = li.getElementsByTag("img").eq(0).attr("src");// 获取li下 第一张图片
            String name = li.getElementsByClass("p-name").eq(0).text();
            String price = li.getElementsByClass("p-price").eq(0).text();
            System.out.println("=======================");
            System.out.println("img : " + img);
            System.out.println("name : " + name);
            System.out.println("price : " + price);
        }
    }
}

image-20231121155653241

成功爬取数据,但是上面图片img是空的,为什么

原因是啥?

一般图片特别多的网站,所有的图片都是通过延迟加载的

// 打印标签内容
Elements lis = j_goodsList.getElementsByTag("li");
System.out.println(lis);

打印所有li标签,发现img标签中并没有属性src的设置,只是data-lazy-ing设置图片加载的地址

image-20231121160020417

所以我们要获取的图片地址应该是data-lazy-img的地址

更改图片获取属性为 data-lazy-img后成功获取img的地址

image-20231121170511844

创建HtmlParseUtil、改写
  • 更改图片获取属性为 data-lazy-img
  • 与实体类结合,实体类如下
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Content implements Serializable {
    private String name;
    private String img;
    private String price;
}
  • 封装为方法
public class HtmlParseUtil {
    public static void main(String[] args) throws IOException {
        System.out.println(parseJD("java"));
    }
    public static List<Content> parseJD(String keyword) throws IOException {
        /// 使用前需要联网
        // 请求url
        String url = "http://search.jd.com/search?keyword=" + keyword;
        // 1.解析网页(jsoup 解析返回的对象是浏览器Document对象)
        Document document = Jsoup.parse(new URL(url), 30000);
        // 使用document可以使用在js对document的所有操作
        // 2.获取元素(通过id)
        Element j_goodsList = document.getElementById("J_goodsList");
        // 3.获取J_goodsList ul 每一个 li
        Elements lis = j_goodsList.getElementsByTag("li");
//        System.out.println(lis);
        // 4.获取li下的 img、price、name
        // list存储所有li下的内容
        List<Content> contents = new ArrayList<Content>();
        for (Element li : lis) {
            // 由于网站图片使用懒加载,将src属性替换为data-lazy-img
            String img = li.getElementsByTag("img").eq(0).attr("data-lazy-img");// 获取li下 第一张图片
            String name = li.getElementsByClass("p-name").eq(0).text();
            String price = li.getElementsByClass("p-price").eq(0).text();
            // 封装为对象
            Content content = new Content(name,img,price);
            // 添加到list中
            contents.add(content);
        }
//        System.out.println(contents);
        // 5.返回 list
        return contents;
    }
}

image-20231121175714560

4、测试index中添加解析数据和搜索高亮

在3、的基础上添加内容

①ContentService //未完成搜索高亮

@Service
public class ContentServiceImpl implements ContentService {

    private final HTMLParseUtil htmlParseUtil;

    private final RestHighLevelClient client;

    public ContentServiceImpl(HTMLParseUtil htmlParseUtil, RestHighLevelClient client) {
        this.htmlParseUtil = htmlParseUtil;
        this.client = client;
    }

    @Override
    public boolean parseContent(String keyword) throws Exception {
        return htmlParseUtil.putIntoIndex(keyword,"jd_goods");
    }

    //未完成搜索高亮
    @Override
    public List<Map<String, Object>> highlightSearch(String keyword, Integer pageIndex, Integer pageSize) throws IOException {


        return null;
    }


}
获取ES中的数据实现搜索功能
@Override
public List<Map<String, Object>> searchPage(String keyword, int pageNo, int pageSize) throws IOException {
    if (pageNo <= 1){
        pageNo = 1;
    }

    //条件搜索
    SearchRequest request = new SearchRequest("jd_goods");
    // 创建搜索源建造者对象
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

    searchSourceBuilder.from(pageNo);
    searchSourceBuilder.size(pageSize);

    // 条件采用:精确查询 通过keyword查字段name,注意这个QueryBuilders的类型,不能用spring的那个
    TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", keyword);
    searchSourceBuilder.query(termQueryBuilder);
    searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));// 60s

    //执行搜索
    request.source(searchSourceBuilder);
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    ArrayList<Map<String,Object>> list = new ArrayList<>();
    //解析结果,都在hits里
    for (SearchHit hit : response.getHits().getHits()) {
        list.add(hit.getSourceAsMap());
    }

    return list;
}

在controller写个接口测试:

②ContentController

@RestController
public class ContentController {

    private final ContentService contentService;

    public ContentController(ContentService contentService) {
        this.contentService = contentService;
    }

    @GetMapping("/parse/{keyword}")
    public boolean parseInto(@PathVariable String keyword) throws Exception {
        return contentService.parseContent(keyword);
    }

}

image-20231121194400539

第二个搜索功能的controller方法测试:

@GetMapping("/search/{keyword}/{pageNo}/{pageSize}")
public List<Map<String,Object>> search(@PathVariable int pageNo, @PathVariable int pageSize, @PathVariable String keyword) throws IOException {
    return contentService.searchPage(keyword, pageNo, pageSize);
}

测试搜索一到十条数据:

image-20231123132313429

关于分词索引和查询

简单解释一下 text 、keyword、term、match text和keyword是数据类型,表示在创建的时候是否会分词建立索引。 term和match是指查询时是否启用分词查询。

例如:现在有一个数据 “my cat”是text类型。 那么,使用term查询“my cat”时,失败,因为“my cat”在被创建时分词器将其索引建立为“my”和“cat”。 使用term查询“my”或“cat”时,则会成功,因为索引又能够精确匹配的数据。 使用match查询“my cat”也能成功,因为match是模糊查询,查询语句“my cat”会被分词成为“my”和“cat”和“my cat”,只要有任意一个满足就会返回数据。

如果“my cat”是keyword,那么建立时,只会有一个索引“my cat” 所以此时,不管是term还是match都只有查询“my cat”时才会返回数据,其余的都查询不到。

③结果展示

5、前后端分离(简单使用Vue)

导入vue依赖:

现在js目录只有一个jQuery,需要导入vue和axios

image-20231123133207346

image-20231123134315466

导入完之后开始写axios

后面前端的东西很容易,就不写了