本篇用来简易搭建solr EmbeddedSolrServer嵌入式实例,即将solr嵌入到应用系统中。应用系统使用SpringBoot搭建。
并同时在嵌入式solr下考虑如何使用自定义过滤插件、自定义排序插件。
1、准备
jdk:jdk8
solr版本:4.7.2
SpringBoot版本:2.5.0
2、创建solr core
2.1、创建SOLE_HOME
SOLR_HOME目录:D:\work\workspace\learn\learn_solr4\solr\multicore
2.2、创建core目录
在SOLR_HOME目录下创建两个core:book、buyrecord
-${SOLR_HOME}
|---book 可售书索引引擎,这是目录
|---buyrecord 购买记录索引引擎,这是目录
2.3、创建solr.xml内容
在D:\work\workspace\learn\learn_solr4\solr文件夹下创建solr.xml
<?xml version="1.0" encoding="UTF-8" ?>
<solr persistent="true">
<cores adminPath="/admin/cores">
<core name="book" instanceDir="book">
<property name="solr.data.dir" value="/opt/learn/solr/index2/book" />
</core>
<core name="buyrecord" instanceDir="buyrecord">
<property name="solr.data.dir" value="/opt/learn/solr/index2/buyrecord" />
</core>
</cores>
</solr>
注:
该配置文件定义两个core,并指定core的索引文件目录solr.data.dir
2.4、拷贝索引配置文件
从solr实例中拷贝索引配置到core,book、buyrecord中
从solr-4.7.2\example\solr\collection1下conf文件夹拷贝到multicore/book、multicore/buyrecord下
2.5、配置索引字段
以book这个core为例,配置索引字段
${SOLR_HOME}/book/conf/schema.xml
配置fields、主键uniqueKey
<fields>
<!-- 书id -->
<field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
<!-- 书名 -->
<field name="name" type="string" indexed="true" stored="true"/>
<!-- 门店id -->
<field name="shopid" type="string" indexed="true" stored="true"/>
<!-- 价格,base_price基本价格、shop_price门店价格,pin_price拼团价格 -->
<dynamicField name="*_price" type="tdouble" indexed="true" stored="true"/>
<!-- 是否参与拼团,0-不参与拼团,1-参与拼团 -->
<field name="isPintuan" type="tint" indexed="true" stored="true"/>
</fields>
<uniqueKey>id</uniqueKey>
3、创建SpringBoot应用
创建SpringBoot web应用。
4、应用中嵌入solr引擎
4.1、pom.xml添加solr依赖
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-core</artifactId>
<version>4.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>4.7.2</version>
</dependency>
4.2、添加solr引擎容器Bean
@Component
public class SolrServerContainer {
private CoreContainer coreContainer;
public static final String SOLE_HOME="/work/workspace/learn/learn_solr4/solr/multicore";
public static final String SOLR_CONFIG ="/work/workspace/learn/learn_solr4/solr/solr.xml";
@PostConstruct
public void init(){
File f = new File(SOLR_CONFIG);
coreContainer = CoreContainer.createAndLoad(SOLE_HOME,f);
}
public EmbeddedSolrServer getSolrServer(String core){
return new EmbeddedSolrServer(coreContainer,core);
}
public CoreContainer getCoreContainer() {
return coreContainer;
}
public void setCoreContainer(CoreContainer coreContainer) {
this.coreContainer = coreContainer;
}
}
至此,solr就嵌入到应用系统中了。
后面就看下嵌入式solr的使用
5、嵌入式solr使用
5.1、类似原生http访问
像原生http://127.0.0.1:8080/solr/book/select?q=:这种http访问示例
(1)查看下solr原生web拦截器
solr原生web应用是添加solr web拦截器,拦截/solr路径
web.xml
<filter>
<filter-name>SolrRequestFilter</filter-name>
<filter-class>org.apache.solr.servlet.SolrDispatchFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SolrRequestFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
再看下SolrDispatchFilter源码
public class SolrDispatchFilter extends BaseSolrFilter {
public void init(FilterConfig config) throws ServletException {
log.info("SolrDispatchFilter.init()");
try {
this.pathPrefix = config.getInitParameter("path-prefix");
this.cores = this.createCoreContainer();
log.info("user.dir=" + System.getProperty("user.dir"));
} catch (Throwable var3) {
log.error("Could not start Solr. Check solr/home property and the logs");
SolrCore.log(var3);
if (var3 instanceof Error) {
throw (Error)var3;
}
}
log.info("SolrDispatchFilter.init() done");
}
...
}
solr原生的SolrDispatchFilter,在初始化时init()指定了pathPrefix和cores。
(2)那么我们就可以类似定义拦截器,继承SolrDispatchFilter
@WebFilter(filterName = "mySolrDispatchFilter",urlPatterns = "/solr/*")
public class MySolrDispatchFilter extends SolrDispatchFilter {
@Autowired
private SolrServerContainer solrServerContainer;
@Override
public void init(FilterConfig config) throws ServletException {
System.out.println("MySolrDispatchFilter init");
this.cores = solrServerContainer.getCoreContainer();
this.pathPrefix = "/solr";
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
super.doFilter(request, response, chain);
}
}
并且在SpringBoot启动类中添加注解
@ServletComponentScan
(3)启动SpringBoot应用
(4)向book solr引擎中插入数据
通过solrj客户端插入数据
@Test
public void testInsert() throws IOException, SolrServerException {
String url = "http://127.0.0.1:8080/solr/book";
SolrServer solrServer = new HttpSolrServer(url);
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id","3333");
doc.addField("name","node.js入门与精通");
doc.addField("shopid","s001");
doc.addField("base_price",Double.valueOf(72));
doc.addField("shop_price",Double.valueOf(77));
doc.addField("pin_price",Double.valueOf(74));
doc.addField("isPintuan","1");
solrServer.add(doc);
solrServer.commit();
}
通过solrj共向book引擎中插入3条数据。
(5)查看solr索引
http://127.0.0.1:8080/solr/book/select?q=:
访问结果如下:
5.2、应用内EmbeddedSolrServer访问
通过EmbeddedSolrServer访问应用内引擎
测试用例使用junit测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class SolrServerContainerTest {
@Autowired
private SolrServerContainer solrServerContainer;
@Test
public void test() throws SolrServerException {
//获取嵌入式solr
EmbeddedSolrServer solrServer = solrServerContainer.getSolrServer("book");
SolrQuery request = new SolrQuery();
request.add("q","*:*");
//查询
QueryResponse response = solrServer.query(request);
System.out.println(response);
}
}
6、自定义过滤插件
6.1、模拟这样的场景
书籍有如下几种价格:
基本价:base_price
门店价格:shop_price
拼团价格:pin_price
当某个书籍支持拼团时,isPintuan=1时,取书的价格pin_price>shop_price>base_price
当某个书籍不支持拼团时,isPintuan=0时,取书的价格shop_price>base_price
现在我要取价格在[70~100]之间的书,该如何从索引引擎中获取合适的书籍?
这里定义个自定义过滤插件实现
6.2、定义自定义过滤插件QParserPlugin
(1)定义PriceFilterQParserPlugin,继承QParserPlugin
实现createParser方法,需要QParser对象。
public class PriceFilterQParserPlugin extends QParserPlugin {
@Override
public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
String min = localParams.get("min");
String max = localParams.get("max");
final PriceFilterQuery priceFilterQuery = new PriceFilterQuery(Double.parseDouble(min),Double.parseDouble(max));
return new QParser(qstr, localParams, params, req) {
@Override
public Query parse() throws SyntaxError {
return priceFilterQuery;
}
};
}
@Override
public void init(NamedList namedList) {
}
}
(2)定义PriceFilterQuery
public class PriceFilterQuery extends ExtendedQueryBase implements PostFilter {
private double min;
private double max;
public PriceFilterQuery(double min, double max) {
this.min = min;
this.max = max;
}
@Override
public DelegatingCollector getFilterCollector(IndexSearcher indexSearcher) {
return new DelegatingCollector(){
AtomicReader reader;
@Override
public void collect(int doc) throws IOException {
double price = 0.0;
//取基本价
double base_price = SolrFieldDataUtil.getDouble(reader,doc,"base_price");
//取门店价
double shop_price = SolrFieldDataUtil.getDouble(reader,doc,"shop_price");
//取拼团价
double pin_price = SolrFieldDataUtil.getDouble(reader,doc,"pin_price");
//取是否支持门店价
int isPintuan = SolrFieldDataUtil.getInt(reader,doc,"isPintuan");
if(isPintuan == 1 && pin_price > 0){
price = pin_price;
}else if(shop_price > 0){
price = shop_price;
}else{
price = base_price;
}
if(price >= min && price <= max){
super.collect(doc);
}
}
@Override
public void setNextReader(AtomicReaderContext context) throws IOException {
super.setNextReader(context);
this.reader = context.reader();
}
};
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public void setCache(boolean cache) {
}
@Override
public boolean getCache() {
##这里必须返回false
##solr源码中在只有返回false时,才会执行该插件
return false;
}
@Override
public int getCost() {
return Math.max(super.getCost(), 100);
}
}
(3)定义取solr字段值工具类
public class SolrFieldDataUtil {
private SolrFieldDataUtil() {
}
public static String getString(AtomicReader reader, int doc, String fieldName) throws IOException {
BinaryDocValues values = FieldCache.DEFAULT.getTerms(reader, fieldName, false);
BytesRef byteRef = new BytesRef();
values.get(doc, byteRef);
return byteRef.utf8ToString();
}
public static double getDouble(AtomicReader reader, int doc, String fieldName) throws IOException {
FieldCache.Doubles values = FieldCache.DEFAULT.getDoubles(reader, fieldName, false);
return values.get(doc);
}
public static int getInt(AtomicReader reader, int doc, String fieldName) throws IOException {
FieldCache.Ints values = FieldCache.DEFAULT.getInts(reader, fieldName, false);
return values.get(doc);
}
}
6.3、配置自定义过滤器
在book core的solrconfig.xml根节点下配置
<queryParser name="priceFilter" class="com.learn.solr.query.function.filter.PriceFilterQParserPlugin"/>
6.4、启动SpringBoot,访问solr
http://127.0.0.1:8080/solr/book/select?q=:&fq={!priceFilter min=70 max=100}
此时会报错
java.lang.IllegalArgumentException: Invalid character found in the request target [/solr/book/select?q=*:*&fq={!priceFilter%20min=70%20max=99}]. The valid characters are defined in RFC 7230 and RFC 3986
这是由于tomcat8及以后版本,支持的RFC 7230 and RFC 3986规范定义{}为特殊字符,不可使用。
6.5、解决特殊字符问题
在SpringBoot启动类中,添加如下代码,对这几个特殊字符继续可使用。
@Bean
public ConfigurableServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addConnectorCustomizers((TomcatConnectorCustomizer) connector -> connector.setProperty("relaxedQueryChars", "|{}[]"));
return factory;
}
6.6、再次启动SpringBoot,访问solr
访问结果:
http://127.0.0.1:8080/solr/book/select?q=:&fq={!priceFilter min=70 max=100}
这里fq={!priceFilter min=70 max=100}就是使用价格过滤插件
7、自定义排序插件
7.1、场景
同自定义过滤插件场景,根据价格排序,需要考虑到基本价、门店价、拼团价
7.2、自定义获取价格插件
public class PriceValueSourceParser extends ValueSourceParser {
@Override
public ValueSource parse(FunctionQParser functionQParser) throws SyntaxError {
return new PriceValueSource();
}
private class PriceValueSource extends ValueSource{
@Override
public FunctionValues getValues(Map map, AtomicReaderContext atomicReaderContext) throws IOException {
final AtomicReader reader = atomicReaderContext.reader();
return new FunctionValues() {
@Override
public short shortVal(int doc) {
return (short) doubleVal(doc);
}
@Override
public float floatVal(int doc) {
return (float) doubleVal(doc);
}
@Override
public int intVal(int doc) {
return (int)doubleVal(doc);
}
@Override
public long longVal(int doc) {
return (long)doubleVal(doc);
}
@Override
public double doubleVal(int doc) {
double price = 0.0;
//取价格说明,优先级:pin_price > shop_price > base_price,且pin_price必须在isPintuan=1是有效
try {
double base_price = SolrFieldDataUtil.getDouble(reader,doc,"base_price");
double shop_price = SolrFieldDataUtil.getDouble(reader,doc,"shop_price");
double pin_price = SolrFieldDataUtil.getDouble(reader,doc,"pin_price");
int isPintuan = SolrFieldDataUtil.getInt(reader,doc,"isPintuan");
if(isPintuan == 1 && pin_price > 0){
price = pin_price;
}else if(shop_price > 0){
price = shop_price;
}else{
price = base_price;
}
}catch (Exception e){
e.printStackTrace();
}
return price;
}
@Override
public String toString(int i) {
return "";
}
};
}
@Override
public boolean equals(Object o) {
return this == o;
}
@Override
public int hashCode() {
return 0;
}
@Override
public String description() {
return "PriceValueSource";
}
}
}
7.3、配置插件
在book core的solrconfig.xml根节点下配置
<valueSourceParser name="priceValue" class="com.learn.solr.query.function.value.PriceValueSourceParser"/>
7.4、重启SpringBoot应用
http://127.0.0.1:8080/solr/book/select?q=:&fl=id,name,shopid,base_price,shop_price,pin_price,isPintuan,price:priceValue()&sort=priceValue() asc
按照价格升序排序,同时fl字段中也把实际价格打印出来