使用SSM+Solr優雅的實現電商項目中的搜索功能

在學習了Redis&Spring-Data-Redis入門Solr&Spring-Data-Solr入門後,接下來就該是項目實戰了。此次咱們用Vue.JS和ElementUI寫前端頁面,優雅的整合SSM-Shiro-Redis-Solr框架。javascript

手摸手教你優雅的實現電商項目中的Solr搜索功能,整合SSM框架和Shiro安全框架;教你用Vue.JS和ElementUI寫出超漂亮的頁面html

項目開源地址:SSM整合Solr實現電商項目中的搜索功能前端

<!--more-->vue

<br/>java

準備

Shiro

關於Shiro,我這裏寫了詳細的SSM框架整合Shiro安全框架的文檔,利用SSM框架+Shiro框架實現用戶-角色-權限管理系統;git

詳細的文檔地址:SSM整合Shiro框架後的開發github

項目Github源碼地址:SSM整合Shiro框架後的開發, 歡迎starweb

<br/>redis

Solr & Spring-Data-Solr

** 關於Solr安裝配置和Spring-Data-Solr的入門Demo請查看博文:Solr和Spring-Data-Solr的入門學習 **spring

Solr須要單獨部署到Tomcat服務器上,我這裏提供本身已經安裝和配置好的Tomcat和Solr: Github

注意事項:

  1. 部署Solr的Tomcat端口和本地項目的端口不能相同,會衝突。

  2. 注意Github倉庫solr-tomcat/webapps/solr/WEB-INF/web.xml中solrhome的位置要修改成本身的。

<br/>

Redis & Spring-Data-Redis

關於Redis安裝配置和Spring-Data-Redis的入門Demo請查看博文:Redis和Spring-Data-Redis的入門學習

<br/>

起步

啓動Solr和Redis

若是訪問localhost:8080/solr/index.html出現Solr Admin頁則啓動成功。

<br/>

初始化表結構

CREATE DATABASE ssm_redis DEFAULT CHARACTER SET utf8;

具體的數據庫約束文件和表數據請看:ssm-redis-solr/db

這裏咱們模擬添加了934條商品數據

<br/>

搭建SSM-Shiro集成環境

具體的SSM整合Shiro的教程請看我這個項目: 手摸手教你SSM整合Shiro。這裏再也不說SSM整合Shiro的教程,默認認爲已經你們已經完成。

<br/>

搭建SSM-Redis集成環境

集成SSM-Redis開發環境,首先你須要安裝Redis並啓動Redis-Server,而後就是在項目中搭建Spring-Data-Redis的運行環境:

<br/>

建立redis-config.properties配置文件

redis.host=127.0.0.1
redis.port=6379
redis.pass=
redis.database=0
redis.maxIdle=300
redis.maxWait=3000
redis.testOnBorrow=true

建立spring-redis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:other/*.properties"/>
    <!-- redis 相關配置 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大空閒數 -->
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <!-- 鏈接時最大的等待時間(毫秒) -->
        <property name="maxWaitMillis" value="${redis.maxWait}"/>
        <!-- 在提取一個jedis實例時,是否提早進行驗證操做;若是爲true,則獲得的jedis實例均是可用的 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="${redis.host}"/>
        <property name="port" value="${redis.port}"/>
        <property name="password" value="${redis.pass}"/>
        <property name="poolConfig" ref="poolConfig"/>
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <!-- 序列化策略 推薦使用StringRedisSerializer -->
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
        </property>
    </beans>
</beans>

配置文件咱們已經在博文Redis&Spring-Data-Redis入門中介紹過了,其中最須要關注的就是hashKeySerializer序列化配置。

若是你不添加序列化配置也是沒影響的,可是存入Redis數據庫中的KEY和VALUE值都是亂碼的,固然是用Spring-Data-Redis是毫無影響的。

若是添加了序列化配置:配置值類型數據用StringRedisSerializer序列化方式;配置Hash類型數據用JdkSerializationRedisSerializer序列化方式。

<br/>

測試

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring/spring*.xml"})
public class TestRedisTemplate {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void setValue(){
        redisTemplate.boundValueOps("name").set("tycoding");
    }
}

而後在redis-cli命令行窗口中輸入get name就能獲得VALUE爲:tycoding。

至此,SSM集成Redis已經完成。

<br/>

搭建SSM-Solr集成環境

建立spring-solr.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:solr="http://www.springframework.org/schema/data/solr"
       xsi:schemaLocation="http://www.springframework.org/schema/data/solr
  		http://www.springframework.org/schema/data/solr/spring-solr-1.0.xsd
		http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- solr服務器地址 -->
    <solr:solr-server id="solrServer" url="http://127.0.0.1:8080/solr/new_core"/>
    <!-- solr模板,使用solr模板可對索引庫進行CRUD的操做 -->
    <bean id="solrTemplate" class="org.springframework.data.solr.core.SolrTemplate">
        <constructor-arg ref="solrServer"/>
    </bean>
</beans>

spring-solr配置文件咱們再博文Solr&Spring-Data-Solr入門中咱們已經介紹過了。其中最須要注意的就是solr服務器url地址的配置,組成結構必定要是:Ip + 端口 + solr項目名稱 + core實例名稱

測試

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-solr.xml")
public class TestSolrTemplate {
    @Autowired
    private SolrTemplate solrTemplate;
    @Test
    public void testAdd() {
        Goods goods = new Goods(1L, "IPhone SE", "120", "手機", "Apple", "Apple");
        solrTemplate.saveBean(goods);
        solrTemplate.commit(); //提交
    }
}

關於Goods實體類定義,請看/java/cn/tycoding/entity/Goods.java 而後咱們在瀏覽器中訪問localhost:8080/solr/index.html,點擊Query能夠看到:

<br/>

開始

數據庫數據批量導入Solr索引庫

上面已經完成了SSM-Shiro-Redis-Solr的集成環境配置,那麼思考:既然用Solr完成搜索功能,那麼怎麼實現呢?

之前咱們直接請求數據庫用concat()模糊查詢實現搜索功能,可是這種方式有很大的弊端:1.給數據庫形成的訪問壓力很大;2.沒法識別用戶查詢的數據究竟是title字段仍是price字段... 若是用Solr完成搜索功能,就很容易解決了這些問題。那麼咱們須要往Solr索引庫中添加數據,Sorl才能搜索出來數據呀:

@Component
public class SolrUtil {
    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private SolrTemplate solrTemplate;

    /**
     * 實現將數據庫中的數據批量導入到Solr索引庫中
     */
    public void importGoodsData() {
        List<Goods> list = goodsMapper.findAll();
        System.out.println("====商品列表====");
        for (Goods goods : list) {
            System.out.println(goods.getTitle());
        }
        solrTemplate.saveBeans(list);
        solrTemplate.commit(); //提交
        System.out.println("====結束====");
    }
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring*.xml");
        SolrUtil solrUtil = (SolrUtil) context.getBean("solrUtil");
        solrUtil.importGoodsData();
    }
}

對應的Mapper.xml

<select id="findAll" resultType="cn.tycoding.entity.Goods">
    SELECT * FROM tb_item
</select>

目的就是查詢數據庫中全部數據,而後調用solrTemplate.saveBean(List)將查詢到的List集合數據添加到Solr索引庫中,以後咱們在localhost:8080/solr/index.html中能查詢出來共有934條記錄數據。

<br/>

實現Solr搜索功能

前端

因爲前端使用了Vue.JSElementUI,若是不瞭解的話請仔細閱讀官方文檔,或者,你能夠查看個人博文記錄: Vue學習學習筆記

index.html中定義:

<el-input placeholder="請輸入內容" type="text" class="input-with-select" @keyup.enter.native="search" v-model="searchMap.keywords">
    <el-button slot="append" icon="el-icon-search" @click="search"></el-button>
</el-input>

index.js中定義:

data() {
  return {
    searchMap: { keywords: '' }
  }
},
methods: {
  search() {
      this.$http.post('goods/search.do', this.searchMap).then(result => {
          this.goods = result.body.rows;
      });
  },
}

後端

GoodsController.java中定義:

@RequestMapping("/search")
public Map<String, Object> search(@RequestBody Map<String, Object> searchMap) {
    return goodsService.search(searchMap);
}

爲何要用Map接收前端數據?

若是前端傳來的數據不止一個,且不屬於後端的任何一個實體類對象,前端傳來的僅是一個自定義的對象(xx:{});那麼後端勢必不能經過實體類對象來接收,且是POST請求,後端必須也使用對象類型來接收數據且必須用@RequestBody標識對象,緣由:

  1. 前端傳來的數據不止一種。
  2. 前端傳來的數據封裝在對象中。

因此,綜上,Map<K, V>這種數據結構最適合做爲接收對象類型。

<br/>

GoodsServiceImpl.xml中定義:

public Map<String, Object> search(Map searchMap) {
  Map<String, Object> map = new HashMap<String, Object>();
  Query query = new SimpleQuery();
  //添加查詢條件
  Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords"));
  query.addCriteria(criteria);
  ScoredPage<Goods> page = solrTemplate.queryForPage(query, Goods.class);
  map.put("rows", page.getContent()); //返回查詢到的數據
  return map;
}

啓動項目,再搜索框中輸入蘋果回車即出現:

可是發現頁面中只顯示10條數據(實際咱們添加的蘋果手機數據不止10條),爲了更優雅的顯示,咱們實現分頁

<br/>

實現分頁查詢

在後端中咱們用limit方式顯示分頁,或者更簡單的用Mybatis的PageHelper分頁查詢實現分頁查詢。 而,咱們在博文Solr&Spring-Data-Solr入門學習中講了Solr分頁查詢的方式:

修改GoodsServiceImpl.java

public Map<String, Object> search(Map searchMap) {
    Map<String, Object> map = new HashMap<String, Object>();
    Query query = new SimpleQuery();
    //添加查詢條件
    Criteria criteria = new Criteria("item_keywords");
    if (searchMap.get("keywords") != null && searchMap.get("keywords") != ""){
        System.out.println("執行了...");
        criteria.is(searchMap.get("keywords"));
    }
    query.addCriteria(criteria);

    //分頁查詢
    Integer pageCode = (Integer) searchMap.get("pageCode");
    if (pageCode == null) {
        pageCode = 1; //默認第一頁
    }
    Integer pageSize = (Integer) searchMap.get("pageSize");
    if (pageSize == null) {
        pageSize = 18; //默認18
    }
    query.setOffset((pageCode - 1) * pageSize); //從第幾條記錄開始查詢:= 當前頁 * 每頁的記錄數
    query.setRows(pageSize);
    ScoredPage<Goods> page = solrTemplate.queryForPage(query, Goods.class);
    map.put("rows", page.getContent()); //返回查詢到的數據
    map.put("totalPage", page.getTotalPages()); //返回總頁數
    map.put("total", page.getTotalElements()); //返回總記錄數
    return map;
}
前端

ElementUI提供了分頁查詢的插件,咱們僅須要傳給它幾個參數,便可實現分頁,詳細的文檔介紹請看我這篇博文:Vue+ElementUI實現分頁

index.html中添加:

<el-pagination
        background
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="pageConf.pageCode"
        :page-sizes="pageConf.pageOption"
        :page-size="pageConf.pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="pageConf.totalPage">
</el-pagination>

index.js中添加:

data() {
  return {
    searchMap: {
        keywords: '',

        //分頁選項
        pageCode: '', //當前頁
        pageSize: '', //每頁的記錄數
    }
    pageConf: {
        //設置一些初始值(會被覆蓋)
        pageCode: 1, //當前頁
        pageSize: 18, //每頁顯示的記錄數
        totalPage: 20, //總記錄數
        pageOption: [18, 25, 30], //分頁選項
    },
  }
},
methods: {
    //pageSize改變時觸發的函數
    handleSizeChange(val) {
        console.log(val);
        this.searchMap.pageSize = val;
        this.searchMap.pageCode = this.pageConf.pageCode;
        this.search(this.pageConf.pageCode, val);
    },
    //當前頁改變時觸發的函數
    handleCurrentChange(val) {
        console.log(val);
        this.searchMap.pageCode = val;
        this.searchMap.pageSize = this.pageConf.pageSize;
        this.search(val, this.searchMap.pageSize);
    },
}

咱們只須要修改以上JavaScript代碼便可,由於search方法傳給後臺的是searchMap這個對象數據,只要其中包含了pageCodepageSize這兩個參數,就會被封裝到後端接收的Map集合中。 其中點擊上一頁,下一頁按鈕觸發的函數在<el-pagination>中已經定義了,若是點擊上一頁下一頁觸發handleCurrentChange()函數,若是點擊每頁5條記錄變成10條記錄就會觸發handleSizeChange()函數;兩個函數都又調用了search()方法,當點擊分頁按鈕時就發送數據給後端,其中包含當前點擊的pageCodepageSize的值,後端接收到這兩個值進行分頁計算,並將數據返回給前端。

<br/>

實現條件過濾

上面講了分頁查詢,僅僅是單方面的查詢,並無任何條件限制。這一功能在SSM中咱們直接經過concat()模糊條件過濾,但在Solr中提供了FilterQuery()實現條件過濾查詢:

修改GoodsServiceImpl.java文件,在search()方法中添加:

if (searchMap.get("category") != null) {
          if (!searchMap.get("category").equals("")) {
              System.out.println("執行了category");
              FilterQuery filterQuery = new SimpleFilterQuery();
              Criteria filterCriteria = new Criteria("item_category").is(searchMap.get("category"));
              filterQuery.addCriteria(filterCriteria);
              query.addFilterQuery(filterQuery);
          }
      }

      //按品牌過濾
      if (searchMap.get("brand") != null) {
          if (!searchMap.get("brand").equals("")) {
              System.out.println("執行了brand...");
              FilterQuery filterQuery = new SimpleFilterQuery();
              Criteria filterCriteria = new Criteria("item_brand").is(searchMap.get("brand"));
              filterQuery.addCriteria(filterCriteria);
              query.addFilterQuery(filterQuery);
          }
      }
前端

修改index.html

<el-checkbox-group :max="1" v-model="change.category" @change="selectMethod">
    <el-checkbox v-for="classify in classifyData.category" :label="classify" border size="mini">{{classify}}</el-checkbox>
</el-checkbox-group>
<el-checkbox-group :max="1" v-model="change.brand" @change="selectMethod">
    <el-checkbox v-for="classify in classifyData.brand" :label="classify" border size="mini">{{classify}}</el-checkbox>
</el-checkbox-group>

修改index.js

searchMap: {
  brand: '',
  price: '',
}

//checkbox選擇的選項
change: {
    category: [],
    brand: [],
    price: []
},

selectMethod(val) {
    this.searchMap.category = this.change.category[0];
    this.searchMap.brand = this.change.brand[0];
    this.search(); //每次點擊後都進行查詢
},

<br/>

實現商品價格升降過濾

修改GoodsServiceImpl.java中的search方法:

//按價格的升降序查詢
  if (searchMap.get("sort") != null) {
      if (!searchMap.get("sort").equals("")) {
          String sortValue = (String) searchMap.get("sort");
          String sortField = (String) searchMap.get("field");
          if (sortValue != null && !sortValue.equals("")) {
              if (sortValue.equals("asc")) {
                  Sort sort = new Sort(Sort.Direction.ASC, "item_" + sortField);
                  query.addSort(sort);
              }
              if (sortValue.equals("desc")) {
                  Sort sort = new Sort(Sort.Direction.DESC, "item_" + sortField);
                  query.addSort(sort);
              }
          }
      }
  }
前端

修改index.html

<el-checkbox-group v-model="change.price" :max="1" @change="selectMethod">
        <el-checkbox :label="'0-500'" border size="mini">0-500元</el-checkbox>
        <el-checkbox :label="'500-1000'" border size="mini">500-1000元</el-checkbox>
        ...
</el-checkbox-group>

修改index.js

searchMap: {
  price: '',
}

selectMethod(val) {
  this.searchMap.price = this.change.sort[0];
}

<br/>

實現查詢結果高亮顯示

實現高亮顯示,咱們先看一下效果:

可看到根據關鍵字蘋果查詢出來的記錄中蘋果字樣都被標記爲紅色斜體樣式,這便是咱們的目的,你能夠查看淘寶的查詢,也是查詢字樣以紅色顯示出來。

若是使用高亮查詢,那麼以前的Query query = new SimpleQuery()就不在知足需求了;高亮查詢使用HighlightQuery query = new SimpleHighlightQuery();

修改GoodsServiceImpl.java中的Search()方法:

HighlightQuery query = new SimpleHighlightQuery();
    HighlightOptions highlightOptions = new HighlightOptions().addField("item_title"); //設置高亮的域
    highlightOptions.setSimplePrefix("<em style='color: red'>"); //設置高亮前綴
    highlightOptions.setSimplePostfix("</em>"); //設置高亮後綴
    query.setHighlightOptions(highlightOptions); //設置高亮選項

初始化高亮查詢HighlightQuery類,利用其包含的setSimplePerfix設置查詢結果的前綴html標籤,利用setSimplePostfix設置查詢結果後綴HTML標籤。

循環高亮查詢結果集合數據:

HighlightPage<Goods> page = solrTemplate.queryForHighlightPage(query, Goods.class);
    //循環高亮入口集合
    for (HighlightEntry<Goods> h : page.getHighlighted()) {
        Goods goods = h.getEntity(); //獲取原實體類
        if (h.getHighlights().size() > 0 && h.getHighlights().get(0).getSnipplets().size() > 0) {
            goods.setTitle(h.getHighlights().get(0).getSnipplets().get(0)); //設置高亮的結果
        }
    }

以前咱們能夠直接經過query.getContent()獲取到查詢結果,可是使用了高亮查詢HighlightQuery就不能直接經過getContent()獲取數據了,咱們要手動遍歷HighlightPage這個對象,他是一個層層嵌套的集合數據,詳細數據結構這裏再也不說了。

<br/>

更新索引

既然使用了Solr管理商品數據,查詢時直接從Solr索引庫中查詢數據,而不是查詢數據庫中的數據,那麼若是修改了商品的信息:添加、刪除、修改。那麼就必須同步使Solr索引庫中的數據和數據庫中的數據保持一致才能實現查詢出來的數據是真實的。

因此咱們要在對商品添加、刪除、修改的時候同步更新Solr索引庫數據:

修改GoodsServiceImpl.javacreatedeleteupdate方法,更新Solr索引庫:

@Override
    public void create(Goods goods) {
        goodsMapper.create(goods);

        Long id = goodsMapper.maxId(); //獲取根據主鍵自增插入的最新一條記錄的ID值
        goods.setId(id);

        //更新索引庫
        solrTemplate.saveBean(goods);
        solrTemplate.commit();
    }

    @Override
    public void update(Goods goods) {
        goodsMapper.update(goods);

        //更新索引庫
        solrTemplate.deleteById(String.valueOf(goods.getId()));
        solrTemplate.commit();
        List<Goods> list = new ArrayList<Goods>();
        list.add(goods);
        solrTemplate.saveBeans(list);
        solrTemplate.commit();
    }

    @Override
    public void delete(Long... ids) {
        for (Long id : ids) {
            goodsMapper.delete(id);

            //更新索引庫
            solrTemplate.deleteById(String.valueOf(id));
            solrTemplate.commit();
        }
    }

注意,在添加商品時,傳來的實體類中並不包含Id屬性數據,由於咱們使用了MySql的自增主鍵;而想要向Solr索引庫中添加新數據又必須制定Id屬性值,由於Solr不會自增主鍵呀。因此咱們在goodsMapper.create()後調用MaxId()方法獲取最新自增的主鍵值,其在Mapper.xml中定義:

<select id="maxId" resultType="Long">
    SELECT MAX(id) FROM tb_item
</select>

<br/>

關於Redis

眼看着教程就結束了,可是爲何教程中沒有解釋Redis的應用呢?

解釋

關於SSM整合Redis的教程請仔細看個人博文:Redis和Spring-Data-Redis入門學習

由於Redis在本項目中並無用到實際的應用中,爲什麼?首先咱們要考慮爲何要用Redis?

Redis緩存嘛,不就是緩存哪些大批量的數據減輕服務器壓力的。可是,並非說全部的大批量的數據都得去緩存,雖然咱們項目中的商品管理功能中已經出現了900多條數據,可是這些數據都須要頻繁的正刪改,而緩存技術中一個難點就是緩存同步問題,你的緩存數據必須時刻和數據庫(真實的數據)保持一致,若是每修改一條記錄就去更新一遍緩存勢必會給緩存服務器形成很大壓力。

什麼數據適合放進緩存?不常修改的數據。好比商品的分類列表數據:

可是原諒我,我只是想簡單的實現如下Solr的搜索功能,真實項目中商品的分類數據確定是存在另外一張表中,因爲我並無實現,因此Redis在本項目中的應用就比較少了。

<br/>

項目截圖

<br/>

交流

若是你們有興趣,歡迎你們加入個人Java交流羣:671017003 ,一塊兒交流學習Java技術。博主目前一直在自學JAVA中,技術有限,若是能夠,會盡力給你們提供一些幫助,或是一些學習方法,固然羣裏的大佬都會積極給新手答疑的。因此,別猶豫,快來加入咱們吧!

<br/>

聯繫

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

<br/>

交流

若是你們有興趣,歡迎你們加入個人Java交流羣:671017003 ,一塊兒交流學習Java技術。博主目前一直在自學JAVA中,技術有限,若是能夠,會盡力給你們提供一些幫助,或是一些學習方法,固然羣裏的大佬都會積極給新手答疑的。因此,別猶豫,快來加入咱們吧!

<br/>

聯繫

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

相關文章
相關標籤/搜索