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