solr-8.0.0 学solr, 看我就够了 单机\集群\中文分词\spring data solr
欢迎转载,转载请注明网址:https://blog.csdn.net/qq_41910280
简介:solr-8.0.0入门教程, 包括solr cloud的安装部署以及使用、中文分词器配置、基于spring boot的spring data solr的使用。
文章目录
1. 环境
OS: Windows 10
solr: 8.0.0
IK-Analyzer: 7.7.1(原2012基础上增加动态词典功能, 支持Lucene7.7.1)
spring data solr:4.0.5
spring boot: 2.1.3.RELEASE
2. solr cloud使用
solr cloud部署与启动
solr解压后, 进入bin目录, 启动命令solr.cmd start -e cloud
当看到如下信息表示solr实例启动成功, 不过solr cloud还没搭完
接下来会打印出集群的配置, 包括为我们启动zookeeper(端口9983)并注册节点, 以及分片和备份的选项, 如果你学过kafka, 那就so easy了.
这里我们唯一没有使用默认值的就是configuration, 我们在这里选择sample_techproducts_configs
接下来访问http://localhost:8983/solr/#/吧
还是熟悉的页面…不截图了
如果你访问http://localhost:8983/solr/#/~cloud?view=graph 你将看到
绿色代表active, N代表NRT
Solr cloud提供了三种分片模式,分别为:
NRT: This is the default. A NRT replica (NRT = NearRealTime) maintains a transaction log and writes new documents to it’s indexes locally. Any replica of this type is eligible to become a leader. Traditionally, this was the only type supported by Solr.
TLOG: This type of replica maintains a transaction log but does not index document changes locally. This type helps speed up indexing since no commits need to occur in the replicas. When this type of replica needs to update its index, it does so by replicating the index from the leader. This type of replica is also eligible to become a shard leader; it would do so by first processing its transaction log. If it does become a leader, it will behave the same as if it was a NRT type of replica.
PULL: This type of replica does not maintain a transaction log nor index document changes locally. It only replicates the index from the shard leader. It is not eligible to become a shard leader and doesn’t participate in shard leader election at all.
这三种模式的主要分别是,NRT可以做主片,可以使用近实时索引(支持SOFT COMMIT),同步索引靠数据转发;TLOG也可以做主片,当为主片是和NRT一致,不能近实时索引,从片需要和主片同步的时候,只是从从片同步索引文件;PULL不能做主片,仅从主片同步索引文件。
创建分片的时候,副本默认使用的NRT模式。
Solr cloud可推荐使用的分片组合方式:
1、全部NRT:适用于小到中级的集群;更新吞吐量不太高的大型集群;
2、全部TLOG:不需要 实时索引;每一个分片的副本数较多;同时需要所有分片都能切换为主片;
3、TLOG+PULL:不需要 实时索引;每一个分片的副本数较多;提高查询能力,能够容忍短时的过期数据。
创建并导入数据建立索引
Linux下执行bin/post -c gettingstarted example/exampledocs/*
Windows下执行java -jar -Dc=gettingstarted -Dauto example\exampledocs\post.jar example\exampledocs*
我们的solr有数据了!
你可以访问http://localhost:8983/solr/#/gettingstarted/query 进入UI查询, 点击Execute Query按钮, 结果如下
共有52条记录( “numFound”:52 )
或者你可以直接访问http://localhost:8983/solr/gettingstarted/select?q=*%3A* (%3A是”:”的url编码), 上图标红的地方分别是查询条件 和 分页条件, 例如我们将q改为foundation, 结果如下
如果你在fl中输入id, 结果将只会保留id字段
如果你将q定义为name:*, 你就看到31条结果, 过滤掉了21 (52-31)条记录
如果你将q定义为name:game, 结果如下(可以看出是不区分大小写的)
{
"responseHeader":{
"zkConnected":true,
"status":0,
"QTime":11,
"params":{
"q":"name:Game",
"_":"1553663260779"}},
"response":{"numFound":2,"start":0,"maxScore":1.1696943,"docs":[
{
"id":"0812550706",
"cat":["book"],
"name":"Ender's Game",
"price":6.99,
"price_c":"6.99,USD",
"inStock":true,
"author":"Orson Scott Card",
"author_s":"Orson Scott Card",
"series_t":"Ender",
"sequence_i":1,
"genre_s":"scifi",
"_version_":1629127895195582464,
"price_c____l_ns":699},
{
"id":"0553573403",
"cat":["book"],
"name":"A Game of Thrones",
"price":7.99,
"price_c":"7.99,USD",
"inStock":true,
"author":"George R.R. Martin",
"author_s":"George R.R. Martin",
"series_t":"A Song of Ice and Fire",
"sequence_i":1,
"genre_s":"fantasy",
"_version_":1629127895133716480,
"price_c____l_ns":799}]
}}
你可以使用空格来搜索多个短语, 你可以试试name:"Samsung+Game"和name:"Samsung"的区别
你还可以试试+electronics +music和electronics+music的区别, 注意没有指定name, 这里+表示必须包含 -可以表示必须不包含
更多内容请参见https://lucene.apache.org/solr/guide/7_7/query-syntax-and-parsing.html#query-syntax-and-parsing
其他重要命令, 后面我们会用到
删除某集合:
bin/solr delete -c yourCollection
创建一个新的集合:
bin/solr create -c -s 2 -rf 2
要停止我们启动的两个Solr节点,请发出以下命令:
bin/solr stop -all
启动并连接到ZooKeeper
solr start -c -p 8983 -s …/example/cloud/node1/solr
solr start -c -p 7574 -s …/example/cloud/node2/solr -z localhost:9983
删除特定文档
bin/post -c yourCollection -d “123”
“delete-by-query”命令
bin/post -c localDocs -d “:”
接下来我们来学习修改scheme吧!
(solr有2个重要配置: schema.xml和solrconfig.xml)
使用如下命令创建一个新的collection并指定分区数和副本数, 因为没有执行configset 所以会采用默认的_deafult
bin/solr create -c films -s 2 -rf 2
打开example\films\films,json, 你可以看到第一个films的name是” .45”, solr自动猜测类型的功能可能会将name属性当做浮点数, 之后的插入的数据就会出现异常, 如果不希望这种事情发生, 我们最好手动指定类型(你也可以通过配置文件或者cmd命令去配置, 也可以通过java客户端配置, 部分后面会讲, 配置文件可以去了解一下), 如下
创建复制字段如下: 该字段将从所有字段中获取所有数据并将其索引到一个名为的字段中_text_
在我们查询之前的documents时,我们不必指定要搜索的字段,因为我们使用的配置sample_techproducts_configs设置为将字段复制到text字段中,并且当没有其他字段时,该字段是默认字段在查询中定义
我们开始导入数据吧
java -jar -Dc=films -Dauto example\exampledocs\post.jar example\films*.json
哇哦, 1100条数据
将q输入comedy, 你将查询到417条记录
Facet功能
勾选facet, field改为genre_str, 执行查询, 你将看到每个genre使用的数量, 下面是结果的一部分
"genre_str":[
"Drama",552,
"Comedy",389,
"Romance Film",270,
"Thriller",259,
"Action Film",196,
"Crime Fiction",170,
"World cinema",167,
"Indie film",144,
"Horror",120,
这是一个重要的功能, 划分facet除了离散之外我们还可以改为范围(UI不支持, 可以使用curl), 这更常用, 后面有java实现
你可以在https://lucene.apache.org/solr/guide/7_7/faceting.html#faceting 学习更多facet的用法
…
3. 中文分词器
中文分词器下载地址
配置步骤:
1.将jar包放入Solr服务的Jetty或Tomcat的webapp/WEB-INF/lib/目录下;
2.将resources目录下的5个配置文件放入solr服务的Jetty或Tomcat的webapp/WEB-INF/classes/目录下;
① IKAnalyzer.cfg.xml
② ext.dic
③ stopword.dic
④ ik.conf
⑤ dynamicdic.txt
3.配置Solr的managed-schema,添加ik分词器,示例如下;
<!-- ik分词器 -->
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false" conf="ik.conf"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="true" conf="ik.conf"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
那么我修改之前sample_techproducts_configs中的managed-schema, 然后删掉之前的gettingstarted, 重新创建并重新导入数据吧!
当然可以这么做, 但是也许我们应该有更聪明的做法, 下面我介绍直接不需要删除甚至不需要重启就可以更新scheme的方法
先了解一下windows上solr自带的zookeeper怎么用吧, 给个例子
执行solr zk -z localhost:9983 ls /configs
(我们也可以通过server\scripts\cloud-scripts\zkcli.bat来操作zookeeper)
上传配置的命令如下:
D:\develop\solr\solr-8.0.0\bin>solr zk upconfig -n gettingstarted -d D:\develop\solr\solr-8.0.0\server\solr\configsets\sample_techproducts_configs -z localhost:9983
INFO - 2019-03-27 17:51:06.912; org.apache.solr.util.configuration.SSLCredentialProviderFactory; Processing SSL Credential Provider chain: env;sysprop
Uploading D:\develop\solr\solr-8.0.0\server\solr\configsets\sample_techproducts_configs\conf for config gettingstarted to ZooKeeper at localhost:9983
我们成功了, 没有reload, 更没有restart !!! (新增servlet容器的jar和配置文件还是要重启)
4. spring data solr
solrJ教程 https://lucene.apache.org/solr/guide/7_7/using-solrj.html#using-solrj
启动 solr start, 添加core如下
添加失败….好吧, 我在调试许久之后发现只能用命令的方式创建
solr create -c mycore
之后在server\solr\mycore\conf\managed-schema中添加(里面的几个字段我们稍后会用到)
<!-- 测试spring data solr -->
<field name="item_name" type="text_ik" indexed="true" stored="true"/>
<field name="item_price" type="pdouble" indexed="true" stored="true"/>
<field name="item_category" type="string" indexed="true" stored="true" />
<field name="item_brand" type="string" indexed="true" stored="true" />
<field name="item_keywords" type="text_ik" indexed="true" stored="false" multiValued="true"/>
<copyField source="item_category" dest="item_keywords"/>
<copyField source="item_brand" dest="item_keywords"/>
<copyField source="item_name" dest="item_keywords"/>
<dynamicField name="item_spec_*" type="string" indexed="true" stored="true" />
<!-- IK分词器 -->
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false" conf="ik.conf"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="true" conf="ik.conf"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
之后我们试试这个core里有没有我们添加的text_ik吧
对比一下我试的另一个ik吧, 显然我们选用的ik分词效果更好, 为我们的明智欢呼吧 !
创建项目, 版本开篇说过我就不墨迹了
Domain类
其中specMap上的两个注解表明这个字段是动态域(又叫动态字段)
另外, 非常重要的一点 除了solr scheme中的field配置外, 通过domain字段上的@Index可以重定义字段属性
package com.example.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.solr.client.solrj.beans.Field;
import org.springframework.data.solr.core.mapping.Dynamic;
import org.springframework.data.solr.core.mapping.SolrDocument;
import java.io.Serializable;
import java.util.Map;
@AllArgsConstructor
@Data
@NoArgsConstructor
public class Item implements Serializable {
@Field
private long id;
@Field("item_name")
private String name;
@Field("item_price")
private double price;
@Field("item_category")
private String category;
@Field("item_brand")
private String brand;
@Dynamic
@Field("item_spec_*")
private Map<String, String> specMap;
}
application.yml配置
spring:
data:
solr:
host: http://localhost:8983/solr
删除全部数据的方法, 方便测试, 我们可能会用到
<delete><query>*:*</query></delete>
<commit/>
代码
这是solrTemplate(solrclient用法类似)的部分使用方法, 直接看代码和注释就行了
(CRUD+FACET+GROUP+highlight 高亮有问题 希望知道的告知一下, 交流才是王道, 不要把自己的路走窄了)
@RunWith(SpringRunner.class)
@SpringBootTest
public class SolrStartApplicationTests {
@Autowired
private SolrTemplate solrTemplate;
@Test
public void contextLoads() {
Item item = new Item();
item.setName("小米9 故宫特别版");
item.setBrand("小米");
item.setPrice(9973.0 / 3);
item.setCategory("手机");
item.setId(UUID.randomUUID().getLeastSignificantBits());
Map<String, String> map = new HashMap<>();
map.put("屏幕尺寸", "5寸,6寸");
map.put("RAM大小", "8G,12G");
map.put("ROM大小", "128G,256G");
item.setSpecMap(map);
Item item2 = new Item();
item2.setName("华为P30 pro 保时捷版");
item2.setBrand("华而不实, 为所欲为");
item2.setPrice(9973.0);
item2.setCategory("手机");
item2.setId(UUID.randomUUID().getLeastSignificantBits());
map.put("屏幕尺寸", "6寸,7寸");
map.put("RAM大小", "6G,8G");
map.put("ROM大小", "64G,128G");
item2.setSpecMap(map);
solrTemplate.saveBeans("mycore", Arrays.asList(item, item2));
solrTemplate.commit("mycore");
}
@Test
public void testCRUD() {
// 查询
Optional<Item> itemOptional = solrTemplate.getById("mycore", "-5362329406904687672", Item.class);
Item item = itemOptional.get();
System.out.println(item);
// 修改
item.setPrice(998);
solrTemplate.saveBean("mycore", item);
solrTemplate.commit("mycore");
// 部分修改
PartialUpdate update = new PartialUpdate("id", "-5329718594856453427");
update.add("item_category", "除了手机外, 小米还有暖宝宝的功能哦 ~ ");
solrTemplate.saveBean("mycore", update);
solrTemplate.commit("mycore");
// 条件查询或者删除
Query query = new SimpleQuery();
Criteria criteria = new Criteria("item_brand").contains("小米");
criteria.and("item_name").contains("小米");
query.addCriteria(criteria);
Page<Item> items = solrTemplate.query("mycore", query, Item.class);
for (Item item1 : items) {
System.out.println(item1);
}
System.out.println("--------------");
// 还记得之前的复制字段吗?
Query query2 = new SimpleQuery();
Criteria criteria2 = new Criteria("item_keywords").contains("手机");
query2.addCriteria(criteria2);
Page<Item> items2 = solrTemplate.query("mycore", query2, Item.class);
for (Item item2 : items2) {
System.out.println(item2);
}
// 删除
solrTemplate.delete("mycore", query, Item.class);
solrTemplate.commit("mycore");
}
@Test
public void testFacetAndGroupAndHighLight() {
// facet
// 离散
// FacetQuery query = new SimpleFacetQuery(new Criteria(Criteria.WILDCARD).expression(Criteria.WILDCARD))
// .setFacetOptions(new FacetOptions().addFacetOnField("item_category").setFacetLimit(5));
// FacetPage<Item> page = solrTemplate.queryForFacetPage("mycore", query, Item.class);
// for (Page<FacetFieldEntry> facetResultPage : page.getFacetResultPages()) {
// facetResultPage.forEach(facetFieldEntry -> System.out.println(facetFieldEntry.getValue()
// + " : " + facetFieldEntry.getValueCount()));
// }
// 范围
// FacetQuery query = new SimpleFacetQuery(new SimpleStringCriteria("*:*"))
// .setFacetOptions(new FacetOptions().addFacetByRange(
// new FacetOptions.FieldWithNumericRangeParameters("item_price", 0, 10000,
// 1000) .setHardEnd(true)
// .setInclude(FacetParams.FacetRangeInclude.ALL)).setFacetMinCount(1).setFacetLimit(10));
// FacetPage<Item> page = solrTemplate.queryForFacetPage("mycore", query, Item.class);
// for (FacetFieldEntry facetFieldEntry : page.getRangeFacetResultPage("item_price")) {
// System.out.println(facetFieldEntry.getValue() + " : " + facetFieldEntry.getValueCount());
// }
// group(group拿到的查询结果是分组的, 而facet拿到的分组信息和结果是分开的)
/*
结果分组有如下3类 可以同时使用多种分类方式 这里仅仅使用其中一种
Field field = new SimpleField("popularity");
Function func = ExistsFunction.exists("description");
Query query = new SimpleQuery("inStock:true");
*/
// Field field = new SimpleField("item_category");
// SimpleQuery query = new SimpleQuery(new SimpleStringCriteria("*:*"));
// GroupOptions groupOptions =
// new GroupOptions().addGroupByField(field).setOffset(0).setLimit(10);// 每组只查询前10个
// query.setGroupOptions(groupOptions);
//
// GroupPage<Item> page = solrTemplate.queryForGroupPage("mycore", query, Item.class);
// GroupResult<Item> fieldGroup = page.getGroupResult(field);
// Page<GroupEntry<Item>> groupEntries = fieldGroup.getGroupEntries();
// for (GroupEntry<Item> groupEntry : groupEntries) {
// System.out.println(groupEntry.getGroupValue() + "-----------------");
// groupEntry.getResult().forEach(System.out::println);
// System.out.println("----------------------------------");
// }
// highlight 关键字高亮显示 准确来说叫突出显示
String keyword = "手机真好";
// SimpleHighlightQuery query = new SimpleHighlightQuery(new SimpleStringCriteria(
// "item_keywords:" + keyword));
HighlightQuery query = new SimpleHighlightQuery();
Criteria criteria=new Criteria("item_keywords").is(keyword);
query.addCriteria(criteria);
HighlightOptions highlightOptions =
new HighlightOptions().addHighlightParameter("hl.highlightMultiTerm ","true").addField("item_category")/*
.addFields(Arrays.asList
("item_category","item_name","item_brand"))*/.setSimplePrefix(
"<b>").setSimplePostfix("</b>");
// 可以用addHighlightParameter()选择要高亮显示的字段
query.setHighlightOptions(highlightOptions);
HighlightPage<Item> page = solrTemplate.queryForHighlightPage("mycore", query,
Item.class);
for (HighlightEntry<Item> h : page.getHighlighted()) {
Item item = h.getEntity();
List<HighlightEntry.Highlight> highlights = h.getHighlights();
for (HighlightEntry.Highlight highlight : highlights) {
System.out.print(highlight.getField() + " === ");
highlight.getSnipplets().forEach(s -> System.out.print(s + " | "));
}
System.out.println(item);
}
}
有没有发现明明我们都已经注解了字段名还要手动去写(复制字段 动态字段除外), 接下来介绍 solr repository, 可以简化操作, 学过JPA的应该比较好理解
- 先写一个接口继承SolrCrudRepository
public abstract interface ItemRepository extends SolrCrudRepository<Item, Long> {
}
- 启动类上加上@EnableSolrRepositories(“com.example.dao”) (可省略, sb框架启动时会自动尝试几十个环境的配置)
- 实体类Item上加上@SolrDocument(collection = “mycore”)
测试代码
在接口中新增方法:
@Query("item_keywords:*?0* AND item_price:[?1 TO ?2]")
public List<Item> findByQueryAnnotation(String keyword, double bottomPrice, double topPrice,
PageRequest pageRequest);
@Highlight(prefix = "<b>", postfix = "</b>",fields = {"item_category","item_brand","item_name"})
@Query("item_keywords:?0")
public HighlightPage<Item> findByItemKeywods(String keyword, Pageable pageable);
@Test
public void testRepository() {
// 分页查询
Page<Item> all = repository.findAll(PageRequest.of(0, 20));
for (Item item : all) {
System.out.println(item);
}
// 排序 可以指定多个属性, 也可以and多个sort
Sort sort = new Sort(Sort.Direction.ASC, "price");// solr repository使用的是java字段名
Iterable<Item> sortAll = repository.findAll(sort);
for (Item item : sortAll) {
System.out.println(item);
}
// 关键字匹配 加 范围筛选 加 分页 加 排序 (不加排序会按query匹配得分排序)
List<Item> querys = repository.findByQueryAnnotation("水果", 0, 1.0 / 0,// 1.0/0表示无限大
PageRequest.of(1, 10, sort));
for (Item aQuery : querys) {
System.out.println(aQuery);
}
// 高亮查询
HighlightPage<Item> items = repository.findByItemKeywods("手机", PageRequest.of(0, 10));
// 增删改: 略
}
想了解Spring data solr更多内容请阅读 https://docs.spring.io/spring-data/solr/docs/4.0.5.RELEASE/reference/html/#solr.misc
5. 遗留问题
- 动态字段插入中文会变成下划线
- 似乎不再支持BigDecimal (以前使用solr4的时候可以直接转double)
- highlight的问题
希望有知道的大佬不吝赐教。
神奇的小尾巴:本人邮箱:zhouyouchn@126.com zhoooooouyou@gmail.com
zhouyou@whut.edu.cn 欢迎交流,共同进步。
欢迎转载,转载请注明本网址。