SolrJ 是操作 Solr 的 Java 客户端,它提供了增加、修改、删除、查询 Solr 索引的 Java 接口。SolrJ 针对 Solr 提供了 REST 的 Http 接口进行了封装, SolrJ 底层是通过使用 HttpClient 来完成 Solr 的操作。

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<dependencies>
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
</dependency>
</dependencies>

SQL 脚本(MySQL)

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(200) NOT NULL COMMENT '商品名称',
`sort_name` varchar(128) NOT NULL COMMENT '分类名称',
`sub_sort_name` varchar(128) NOT NULL COMMENT '子分类名称',
`price` decimal(10,0) NOT NULL COMMENT '价格',
`sales` int(11) DEFAULT '0' COMMENT '销量',
`area` varchar(64) NOT NULL COMMENT '地区',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=742 DEFAULT CHARSET=utf8 COMMENT='商品表';

点此下载数据库脚本(数据从爱淘宝网站中爬取)

建立数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package org.fanlychie.model;
import org.apache.solr.client.solrj.beans.Field;
public class Product {
/**
* 主键
*/
@Field("id")
private Integer id;
/**
* 商品名称
*/
@Field("name")
private String name;
/**
* 分类名称
*/
@Field("sortName")
private String sortName;
/**
* 子分类名称
*/
@Field("subSortName")
private String subSortName;
/**
* 价格
*/
@Field("price")
private Double price;
/**
* 销量
*/
@Field("sales")
private Integer sales;
/**
* 地区
*/
@Field("area")
private String area;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSortName() {
return sortName;
}
public void setSortName(String sortName) {
this.sortName = sortName;
}
public String getSubSortName() {
return subSortName;
}
public void setSubSortName(String subSortName) {
this.subSortName = subSortName;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getSales() {
return sales;
}
public void setSales(Integer sales) {
this.sales = sales;
}
public String getArea() {
return area;
}
public void setArea(String area) {
this.area = area;
}
@Override
public String toString() {
return "Product [id=" + id + ", name=" + name + ", sortName="
+ sortName + ", subSortName=" + subSortName + ", price="
+ price + ", sales=" + sales + ", area=" + area + "]";
}
}

@Field("id") 与 schema.xml 中的 <field name="id" /> 节点相呼应

建立索引文件时,SolrJ 会将 @Field 注解的属性转换成 Solr 文档对象的字段

在检索的时候,SolrJ 会将 Solr 文档对象的字段转换成 @Field 注解的 Bean 的属性

schema.xml 配置片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<schema name="core1" version="1.5">
[ . . . . . . ]
<field name="_version_" type="long" indexed="true" stored="true"/>
<!-- 商品 ID -->
<field name="id" type="int" indexed="true" stored="true" required="true"/>
<!-- 商品名称 -->
<field name="name" type="text_ik" indexed="true" stored="true" required="true"/>
<!-- 商品一级分类 -->
<field name="sortName" type="string" indexed="true" stored="true" required="true"/>
<!-- 商品二级分类 -->
<field name="subSortName" type="string" indexed="true" stored="true" required="true"/>
<!-- 商品价格 -->
<field name="price" type="double" indexed="true" stored="true" required="true"/>
<!-- 商品销量 -->
<field name="sales" type="int" indexed="true" stored="true" required="true"/>
<!-- 发货地 -->
<field name="area" type="string" indexed="true" stored="true" required="true"/>
<!-- 检索域 -->
<field name="text" type="text_ik" indexed="true" stored="false" multiValued="true" required="false"/>
<!-- 唯一键 -->
<uniqueKey>id</uniqueKey>
<!-- 把需要检索的字段, 拷贝到 text 字段中 -->
<copyField source="name" dest="text"/>
<!-- 把需要检索的字段, 拷贝到 text 字段中 -->
<copyField source="sortName" dest="text"/>
<!-- 把需要检索的字段, 拷贝到 text 字段中 -->
<copyField source="subSortName" dest="text"/>
<!-- 采用 IK 中文分词的字段类型 -->
<fieldType name="text_ik" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false" />
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="true" />
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
[ . . . . . . ]
</schema>

Solr 服务启动报错:

Caused by: org.apache.solr.common.SolrException: Invalid Number: MA147LL/A

解决办法:

将 $SOLR_HOME/core1/conf/elevate.xml(竞价排名)配置文件中的 id 的值改为整型值即可

使用 JDBC 从数据库获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package org.fanlychie.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import org.fanlychie.model.Product;
public class ProductDao {
public static List<Product> getAll() {
Connection conn = null;
try {
String username = "root";
String password = "root";
String url = "jdbc:mysql://localhost:3306/product_repo";
conn = DriverManager.getConnection(url, username, password);
PreparedStatement pstmt = conn.prepareStatement("select * from product");
ResultSet rs = pstmt.executeQuery();
List<Product> products = new ArrayList<Product>();
while (rs.next()) {
Product product = new Product();
product.setId(rs.getInt("id"));
product.setSales(rs.getInt("sales"));
product.setArea(rs.getString("area"));
product.setName(rs.getString("name"));
product.setPrice(rs.getDouble("price"));
product.setSortName(rs.getString("sort_name"));
product.setSubSortName(rs.getString("sub_sort_name"));
products.add(product);
}
return products;
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
if (conn != null) {
try {
conn.close();
} catch (Exception e) {}
}
}
}
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

log4j.xml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "http://toolkit.alibaba-inc.com/dtd/log4j/log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%n%p %d{yyyy-MM-dd HH:mm:ss} %c : %L %n%m%n%n" />
</layout>
</appender>
<logger name="org.apache" additivity="true">
<level value="WARN" />
</logger>
<logger name="org.apache.http.impl.conn.DefaultClientConnection" additivity="true">
<level value="DEBUG" />
</logger>
<root>
<level value="DEBUG" />
<appender-ref ref="console" />
</root>
</log4j:configuration>

建立索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private static final int RESPONSE_STATUS_OK = 0;
public static void main(String[] args) throws Throwable {
// 创建一个 Solr 客户端
SolrClient solrClient = new HttpSolrClient("http://192.168.1.102:8081/solr/core1");
// 文档对象绑定器
DocumentObjectBinder binder = solrClient.getBinder();
// Solr 输入文档
List<SolrInputDocument> documents = new ArrayList<SolrInputDocument>();
// 从数据库中取得需要建立索引的数据
List<Product> products = ProductDao.getAll();
for (Product product : products) {
// 将 Bean 转换成 Solr 文档
documents.add(binder.toSolrInputDocument(product));
}
// 添加文档到客户端
solrClient.add(documents);
// 提交事务
UpdateResponse response = solrClient.commit();
if (response.getStatus() == RESPONSE_STATUS_OK) {
System.out.println("创建索引成功!");
} else {
System.out.println("创建索引失败!");
}
// 关闭
solrClient.close();
}

检索文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) throws Throwable {
// 创建一个 Solr 客户端
SolrClient solrClient = new HttpSolrClient("http://192.168.1.102:8081/solr/core1");
// 创建一个 Solr 查询
SolrQuery solrQuery = new SolrQuery();
// 设置查询串
solrQuery.setQuery("打底加绒上衣男");
// 执行查询得到查询响应对象
QueryResponse response = solrClient.query(solrQuery);
// 从查询响应对象中获取查询结果
SolrDocumentList documentList = response.getResults();
// 文档对象绑定器
DocumentObjectBinder binder = solrClient.getBinder();
List<Product> products = new ArrayList<Product>();
for (SolrDocument document : documentList) {
// 将 Solr 文档对象转换成 Bean 对象
products.add(binder.getBean(Product.class, document));
}
// 关闭客户端
solrClient.close();
// 打印消息
System.out.println(products);
}

搜索结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DEBUG 2015-12-02 21:51:06 org.apache.http.impl.conn.DefaultClientConnection : 268
Sending request: GET /solr/core1/select?q=%E6%89%93%E5%BA%95%E5%8A%A0%E7%BB%92%E4%B8%8A%E8%A1%A3%E7%94%B7&wt=javabin&version=2 HTTP/1.1
DEBUG 2015-12-02 21:51:06 org.apache.http.impl.conn.DefaultClientConnection : 253
Receiving response: HTTP/1.1 200 OK
DEBUG 2015-12-02 21:51:06 org.apache.http.impl.conn.DefaultClientConnection : 176
Connection 0.0.0.0:53836<->192.168.1.102:8081 closed
DEBUG 2015-12-02 21:51:06 org.apache.http.impl.conn.DefaultClientConnection : 176
Connection 0.0.0.0:53836<->192.168.1.102:8081 closed
[
Product [id=371, name=男士长袖t恤男装高领紧身秋衣男青少年加绒加厚打底衫韩版上衣服, sortName=男装, subSortName=T恤, price=30.0, sales=7681, area=浙江 杭州],
Product [id=310, name=男装长袖T恤冬季加绒加厚保暖衣青少年V领打底上衣潮男冬装加大码, sortName=男装, subSortName=T恤, price=48.0, sales=631, area=广东 深圳]
]

POST 请求

1
QueryResponse response = solrClient.query(solrQuery, SolrRequest.METHOD.POST);

最小匹配

1
solrQuery.setQuery("打底加绒上衣男");

执行查询请求,服务器端记录的日志信息

1
[core1] webapp=/solr path=/select params={q=打底加绒上衣男&wt=javabin&version=2} hits=2 status=0 QTime=1

hits = 2,即该请求匹配到 2 个文档。

1
2
3
solrQuery.setQuery("打底加绒上衣男");
solrQuery.setParam("mm", "2");

mm(minimal should match)最小应该匹配多少个短语(查询串分词后的短语)。

再次执行查询请求,服务器端记录的日志信息

1
[core1] webapp=/solr path=/select params={mm=2&q=打底加绒上衣男&wt=javabin&version=2} hits=120 status=0 QTime=4

hits = 120,即该请求匹配到 120 个文档。

查询参数

1
solrQuery.setQuery("sortName:男装 AND area:广东\\ 广州");

查询分类是男装,发货地是广东广州的商品(广东广州有空格,需要转义)

1
[core1] webapp=/solr path=/select params={q=sortName:男装+AND+area:广东\+广州&wt=javabin&version=2} hits=19 status=0 QTime=3

结果排序

1
2
3
4
5
solrQuery.setQuery("羽绒服女");
solrQuery.addSort("price", SolrQuery.ORDER.asc);
solrQuery.addSort("sales", SolrQuery.ORDER.desc);

先按价格升序排序,价格相同按销量降序排序。注意不能用 setSort,如

1
2
3
4
5
solrQuery.setQuery("羽绒服女");
solrQuery.setSort("price", SolrQuery.ORDER.asc);
solrQuery.setSort("sales", SolrQuery.ORDER.desc);

该方式只会按销量降序排序,价格的排序被覆盖掉不起作用。

facet 查询

Facet 是 solr 的高级搜索功能之一,在检索文档的同时,能够按照 Facet 的域(字段)进行分组统计。Facet 的字段必须被索引,一般来说该字段无需分词,无需存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static void main(String[] args) throws Throwable {
// 创建一个 Solr 客户端
SolrClient solrClient = new HttpSolrClient("http://192.168.1.102:8081/solr/core1");
// 创建一个 Solr 查询
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(Integer.MAX_VALUE);
// 设置查询串
solrQuery.setQuery("女装");
// facet 查询
solrQuery.setFacet(true);
// 每个分组中的数据至少有一个值才返回
solrQuery.setFacetMinCount(1);
// 不统计 NULL 的值
solrQuery.setFacetMissing(false);
// 排序
solrQuery.setFacetSort(FacetParams.FACET_SORT_COUNT);
// facet 结果的返回行数
solrQuery.setFacetLimit(200);
// 分组统计的域
solrQuery.addFacetField("sortName", "subSortName");
// 执行查询得到查询响应对象
QueryResponse response = solrClient.query(solrQuery, SolrRequest.METHOD.POST);
List<FacetField> facetFieldList = response.getFacetFields();
for (FacetField facetField : facetFieldList) {
System.out.println(facetField.getName());
System.out.println("---------------------------------------------------");
List<Count> counts = facetField.getValues();
for (Count count : counts) {
System.out.println(count.getName() + " : " + count.getCount());
}
System.out.println();
}
solrClient.close();
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
sortName
---------------------------------------------------
女装 : 348
男装 : 1
subSortName
---------------------------------------------------
羽绒服 : 76
T恤 : 75
毛呢外套 : 75
连衣裙 : 75
鞋子 : 48