SpringDataSolr
Spring Data Solr就是为了方便Solr的开发所研制的一个框架,其底层是对SolrJ(官方API)的封装
一、maven开发环境搭建
SpringDataSolr工程项目结构.png
1). 项目依赖pox.xml
<!-- Spring Data Solr 坐标 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-solr</artifactId>
<version>1.5.5.RELEASE</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
2). spring容器配置 applicationContext-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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- solr服务器地址,配置此标签会自动创建 solrServer bean,不需要再手动配置bean -->
<solr:solr-server id="solrServer" url="http://192.168.74.129:8080/solr" />
<!--<solr:solr-server id="solrServer" url="http://127.0.0.1:8080/solr" />-->
<!-- solr模板,使用solr模板可对索引库进行CRUD的操作 -->
<bean id="solrTemplate" class="org.springframework.data.solr.core.SolrTemplate">
<constructor-arg ref="solrServer" />
</bean>
</beans>
3). 文档对应的pojo以及域注解标注
只将于域相关的属性标注出来 【@Field注解 -> 静态/动态域 (复制域没有对应的字段,因为复制域多个静态域的逻辑组成)】
package com.lingting;
import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.solr.core.mapping.Dynamic;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;
public class TbItem implements Serializable {
/** @Field 注解,配置域 */
/** 添加一个动态域,专门为动态域设置一个Map集合,让solr根据key创建对应的域
标识动态域,与solr仓库中配置的动态名一样
*/
@Dynamic
@Field("item_spec_*")
private Map<String, String> specMap;
/** ~~~ 其他静态域 ~~~ */
/** 【【【solr 也是根据一个 唯一标识(主键)对一个bean所涉及的域进行增删改查】】】 */
@Field
private Long id;
@Field("item_title")
private String title;
@Field("item_image")
private String image;
@Field("item_price")
private BigDecimal price;
@Field("item_updatetime")
private Date updateTime;
@Field("item_goodsid")
private Long goodsId;
@Field("item_category")
private String category;
@Field("item_brand")
private String brand;
@Field("item_seller")
private String seller;
/** ~~ 其他字段以及get/set略,需要自行补齐 ~~ */
}
4). 测试类模板 TestTemplate
测试类模板
package com.lingting;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.data.solr.core.query.*;
import org.springframework.data.solr.core.query.result.*;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
import java.util.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-solr.xml")
public class TestTemplate {
/** 注入 SolrTemplate */
@Autowired
private SolrTemplate solrTemplate;
/** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
下方各个功能模块代码写在此处
~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
}
二、SpringDataSolr的基本增删改查
1). 向solr中添加文档
如果设置的
@Field
标识的主键一致,将会将solr中的对应文档进行覆盖
- 添加一个文档
/** 向solr中添加一个【文档】 */
@Test
public void addOne() {
// 创建被 Filed 注解过的bean
TbItem item = new TbItem();
// 向 域 对应的字段 设置数据,模拟从数据库中查询
item.setId(1L);
item.setTitle("华为P20 Plus 电信5G 32G内存 128G外存 荣耀金");
item.setImage("http://pic.netbian.com/tupian/23594.html");
item.setPrice(BigDecimal.valueOf(8000.01));
item.setUpdateTime(new Date());
item.setGoodsId(17899888899L);
item.setCategory("手机");
item.setBrand("华为");
item.setGoodsId(1L);
item.setSeller("华为2号专卖店");
// 模拟动态域
Map<String, String> map = new HashMap<>();
map.put("运行内存", "32G");
map.put("网络制式", "电信5G");
item.setSpecMap(map);
// 向solr中添加 一个文档
solrTemplate.saveBean(item);
// 注意,必须得提交!!!!!!!!
solrTemplate.commit();
System.out.println("数据插入完成");
}
- 添加多个文档
/** 批量插入文档 solrTemplate.saveBeans */
@Test
public void addMany () {
List<TbItem> list = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
TbItem item = new TbItem();
item.setId(1L + i);
item.setTitle("华为P" + (20 + i) + " Plus 电信" + (i % 6) + "G " + (32 + i) + "G内存 " + (128 + i) + "G外存 荣耀金");
item.setImage("http://pic.netbian.com/tupian/23594.html");
item.setPrice(BigDecimal.valueOf(8000.01 + i));
item.setUpdateTime(new Date());
item.setGoodsId(17899888899L + i);
item.setCategory("手机");
item.setBrand("华为");
item.setSeller("华为2号专卖店");
// 模拟动态域
Map<String, String> map = new HashMap<>();
map.put("运行内存", (32 + i) + "G");
map.put("网络制式", "电信" + (i % 6) + "G");
item.setSpecMap(map);
list.add(item);
}
// 向solr中添加 一个文档 集合
solrTemplate.saveBeans(list);
solrTemplate.commit();
}
2). 向solr中删除文档
- 按照主键删除
/** 按照主键删除, 传入一个字符串,solr会自动转换 */
@Test
public void deleteOne() {
// 删除一个
solrTemplate.deleteById("1");
// 删除多个
List<String> list = new ArrayList<>();
list.add("2");
list.add("3");
list.add("4");
solrTemplate.deleteById(list);
solrTemplate.commit();
}
- 全部清空
/** 删除全部数据 */
@Test
public void deleteAll () {
Query query = new SimpleQuery("*:*");
solrTemplate.delete(query);
solrTemplate.commit();
}
3). solr中的简单查询
结果打印方法 showList: 打印部分信息
// 显示记录数据
private void showList(ScoredPage<TbItem> page) {
for (TbItem item : page.getContent()) {
System.out.println("商品名称:" + item.getTitle() + " 商品价格:" + item.getPrice());
}
System.out.println("总记录数:" + page.getTotalElements());
System.out.println("总页数:" + page.getTotalPages());
}
- 按照主键查询
/** 按照 主键查询*/
@Test
public void findOne() {
TbItem item = solrTemplate.getById(1, TbItem.class);
System.out.println(item.getTitle());
}
- 分页查询
/** 分页查询 */
@Test
public void pageQuery () {
// 建立查询, 有 查询 表达式: `*:*` 表示查询所有
Query query = new SimpleQuery("*:*");
// 开始索引 (默认 0)
query.setOffset(20);
// 每页记录数 (默认 10)
query.setRows(20);
// 进行查询
ScoredPage<TbItem> page = solrTemplate.queryForPage(query, TbItem.class);
// 展示查询的数据内容
showList(page);
}
- 条件查询【重要】
/** 条件查询 */
@Test
public void pageQueryMulti () {
Query query = new SimpleQuery();
/* 拼接查询条件,底层封装了表达式
链式编程,每次调用返回一个新拼接的对象,必须 赋值给原对象,因为返回值是一个新的对象
拼接条件可以根据需要 查看API; 也可以根据提供的方法进行猜测!
*/
Criteria criteria = new Criteria("item_title").contains("3");
criteria = criteria.and("item_price").between(100, 20000);
query.addCriteria(criteria);
// 分页查询设置【偏移量】以及【每页数量】
query.setOffset(1);
query.setRows(20);
ScoredPage<TbItem> page = solrTemplate.queryForPage(query, TbItem.class);
showList(page);
}
三、SpringDataSolr的高级查询方法
不得不吐槽,SpringDataSolr接口设计很繁琐、复杂;一点都不友好;恶心死你!
1). 高亮/条件过滤/排序/分页综合查询
/** 综合案例查询:条件/高亮/分页 查询*/
@Test
public void complexQuery() {
// 1. 高亮分页查询
Map searchMap = new HashMap();
searchMap.put("keywords", "手机");
searchMap.put("category", "手机");
searchMap.put("brand", "华为");
Map specMap = new HashMap();
specMap.put("网络制式", "电信5G");
searchMap.put("spec", specMap);
searchMap.put("price", "8000-*");
searchMap.put("pageNo", 1);
searchMap.put("pageSize", 10);
searchMap.put("sort", "DESC");
searchMap.put("sortField", "price");
Map map = searchList(searchMap);
List<TbItem> list = (List<TbItem>)map.get("rows");
for (TbItem item : list) {
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
System.out.println(item.getTitle());
System.out.println(item.getSpecMap());
System.out.println(item.getPrice());
}
System.out.println("总页数: " + map.get("totalPages"));
System.out.println("总记录数: " + map.get("totalPages"));
}
/** 【很繁琐】按照关键字查询 复制域item_keywords 并 高亮显示 标题域item_title */
private Map searchList (Map searchMap) {
// 创建结果集容器
Map map = new HashMap<>();
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~ 高亮配置 ~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// 1.创建对象高亮查询
HighlightQuery query = new SimpleHighlightQuery();
// 设置高亮的域,可能有很多个域。可以通过链式编程添加域
HighlightOptions highlightOptions = new HighlightOptions().addField("item_title");
// 高亮前缀
highlightOptions.setSimplePrefix("<em style='color:red'>");
// 高亮后缀
highlightOptions.setSimplePostfix("</em>");
// 将高亮配置 赋予 给查询对象
query.setHighlightOptions(highlightOptions);
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~ 配置查询的域 ~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// 2. 配置查询的域【复制域,有很多的域,包含下方条件/排序的各个域】并将配置赋予给 查询对象
Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords"));
query.addCriteria(criteria);
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~ 按照条件进行删选过滤 ~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// 【a.】 根据 分类 进行过滤
if (!"".equals(searchMap.get("category"))) {
Criteria filterCriteria = new Criteria("item_category").is(searchMap.get("category"));
FilterQuery filterQuery = new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
// 【b.】 根据 品牌 进行过滤
if (!"".equals(searchMap.get("brand"))) {
Criteria filterCriteria = new Criteria("item_brand").is(searchMap.get("brand"));
FilterQuery filterQuery = new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
// 【c.】 根据 规格 进行过滤, 有多个规格,需要遍历操作!
if (searchMap.get("spec") != null) {
Map<String, String> specMap = (Map)(searchMap.get("spec"));
for (String key : specMap.keySet()) {
Criteria filterCriteria = new Criteria("item_spec_" + key).is(specMap.get(key));
FilterQuery filterQuery = new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
}
// 【d.】 根据价格进行过滤,尽量让搜索结果多
if (!"".equals(searchMap.get("price"))) {
String[] price = ((String)searchMap.get("price")).split("-");
if (!"0".equals(price[0])) {
Criteria filterCriteria = new Criteria("item_price").greaterThan(price[0]);
FilterQuery filterQuery = new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
if (!"*".equals(price[1])) {
Criteria filterCriteria = new Criteria("item_price").lessThan(price[1]);
FilterQuery filterQuery = new SimpleFilterQuery(filterCriteria);
query.addFilterQuery(filterQuery);
}
}
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~ 排序处理 ~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// 获取前端传入的排序规则 ASC DESC; 排序的字段【价格/跟新时间...】
String sortValue = (String) searchMap.get("sort");
String sortField = (String) searchMap.get("sortField");
if (sortValue != null && !"".equals(sortValue) && sortField != null && !"".equals(sortField)) {
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);
}
}
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~ 分页处理 ~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// 分页查询 需要提高容错性
// 提取页码
Integer pageNo = (Integer) searchMap.get("pageNo");
if (pageNo == null || pageNo <= 0) {
pageNo = 1;
}
// 每页记录数,根据前端排版美观的角度设计
Integer pageSize = (Integer) searchMap.get("pageSize");
if (pageSize == null || pageSize <= 0) {
pageSize = 20;
}
// 根据查询的页数与每页显示条数计算开始索引
query.setOffset((pageNo - 1) * pageSize);
query.setRows(pageSize);
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~ 查询并获取结果 ~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// 4. 按照关键字进行查询 获取返回结果
HighlightPage<TbItem> page = solrTemplate.queryForHighlightPage(query, TbItem.class);
// 5. 循环【高亮入口】集合,获取每个【文档集合 对应 Items 实体类集合】
// 注意:page.getContent() 获得的是原始没有高亮的内容
for (HighlightEntry<TbItem> h : page.getHighlighted()) {
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// @@@@ ~:~API设计原理解释及演示~:~ @@@@ 继续上面的步骤!
// 【Items 实体类集合 】,获取【域集合 -> 字段/属性】
List<HighlightEntry.Highlight> h2 = h.getHighlights();
// 遍历【域集合】,比如 复制域 ,与中有多个原始域【Map -> (key:value)】 ,所以需要集合存储!
for (HighlightEntry.Highlight h3 : h2) {
List<String> finalResult = h3.getSnipplets();
System.out.println(finalResult);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
// 获取原实体类,将高亮的的域的内容封装到 原实体类中
// h.getEntity() 与 page.getContent() 获取的是同一个对象!
TbItem item = h.getEntity();
// 当前业务逻辑决定了如此判断!
if (h.getHighlights().size() >0 &&
h.getHighlights().get(0).getSnipplets().size() > 0) {
// 设置高亮结果
item.setTitle(h.getHighlights().get(0).getSnipplets().get(0));
}
}
map.put("rows", page.getContent());
// 返回总页数
map.put("totalPages", page.getTotalPages());
// 返回总记录数
map.put("total", page.getTotalElements());
return map;
}
2). 分类查询
根据品牌查询该品牌下的所有商品分类名称
@Test
public void category() {
Map searchMap = new HashMap();
searchMap.put("keywords", "华为");
List<String> list = searchCategoryList(searchMap);
for (String categoryName : list) {
System.out.println("~~~~~~~~~~~");
System.out.println(categoryName);
}
}
/** 【很繁琐】按照关键字查询 复制域item_keywords 并 根据 分类域item_category 查询 分类列表
* */
private List searchCategoryList(Map searchMap) {
// 创建结果集
List<String> list = new ArrayList<>();
// 1、创建查询对象
Query query = new SimpleQuery();
// 2、按照关键字进行查询
Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords"));
query.addCriteria(criteria);
// 3、设置分组选项;支持链式编程,可以添加 多个分组 的条件,得到多个分组的结果
GroupOptions groupOptions = new GroupOptions().addGroupByField("item_category");
query.setGroupOptions(groupOptions);
// 4、得到查询分组页
GroupPage<TbItem> page = solrTemplate.queryForGroupPage(query, TbItem.class);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 复杂设计导致的繁琐 ~~~~~~~~~~~~~~~~~~~~~~~~~~
// 5~~~、获得指定 分组选项 的结果集,指定的分组条件是 上面第【3、】步中指定的分组条件之一!
// 注意!((GroupPage<TbItem>)page).getContent() 是空内容,接口设计的不足问题导致的,必须实现的空方法!
GroupResult<TbItem> groupResult = page.getGroupResult("item_category");
// 6~~~、得到分组结果入口页
Page<GroupEntry<TbItem>> groupEntries = groupResult.getGroupEntries();
// 7~~~、得到分组入口集合
List<GroupEntry<TbItem>> entryList = groupEntries.getContent();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 8、遍历入口,将分组结果的名称封装到返回值中
for (GroupEntry<TbItem> entry : entryList) {
list.add(entry.getGroupValue());
}
return list;
}