Solr实现全文搜索




Solr

  • Solr是一个可扩展的,可部署,搜索,存储引擎,优化搜索大量以文本为中心的数据库

  • Solr是开源搜索平台,用于构建搜索应用程序
  • 建立在Lucene(全文搜索引擎)之上
  • Solr是企业级的,快速的和高度可扩展的,使用Solr构建的应用程序可以提供高性能,但是非常复杂
  • Solr可以和Hadoop一起使用:由于Hadoop处理大量数据,Solr可以从大的数据源中找到所需信息.
  • Solr不仅限于搜索,也可以用于存储.和其它NoSQL数据库一样,是一种非关系数据存储和处理技术

Apache Solr特点

Solr是Lucene的Java API包装,使用Solr,就可以使用Lucene的所有功能


  • RESTful API:​ 要与Solr通信,可以使用RESTful服务与Solr通信,可以使用XML,JSON,CSV等格式的文件作为输入文档,并以相同的文件格式获取结果
  • 全文搜索:​ Solr提供了全文搜索所需的所有功能:​令牌,短语,拼写检查,通配符,自动完成
  • 企业准备:​ 根据企业或组织的需要,Solr可以部署在任何类型的系统:​独立,分布式,云
  • 灵活可扩展:​ 通过扩展Java类并进行相关配置,可以定制Solr组件
  • NoSQL数据库:​ Solr可以用作大数量级的NoSQL数据库,可以沿着集群分布搜索任务

搜索引擎

  • 搜索引擎:

  • 搜索引擎是庞大的​互联网资源数据库​,如网页,新闻组,程序,图像等
  • 有助于​在网上定位信息
  • 用户可以通过以关键字或短语的形式将查询传递到搜索引擎中来搜索信息,然后搜索引擎搜索其数据库并向用户返回相关链接

搜索引擎组件

搜索引擎有三个组件:


  • Web爬虫:​ 一个收集网络信息的软件组件
  • 数据库:​ Web上的所有信息都存储在数据库中,包含大量的Web资源
  • 搜索接口:​ 这个组件是用户和数据库之间的接口,帮助用户搜索数据库

搜索引擎工作流程


  • 获取原始内容:​ 任何搜索应用程序的第一步是收集要进行搜索的目标内容
  • 构建文档:​ 从原始内容构建文档,让搜索应用程序可以很容易的理解和解释
  • 分析文档:​ 在索引开始之前,将对文档进行分析
  • 索引文档:​ 当文档被构建和分析后,下一步是对文档建立索引,以便可以基于特定键而不是文档的全部内容来检索该文档.索引类似于在书开始页或末尾处的目录索引,其中常见单词以页码显示,使得这些单词可以快速追踪,而不是搜索整本书
  • 用于搜索的用户接口:​ 当索引数据库就绪,应用程序就可以执行搜索操作.为了帮助用户进行搜索,应用必须提供用户接口,用户可以在用户接口中输入文本并启动搜索过程
  • 构建查询:​ 当用户做出搜索文本的请求,应用程序应该使用该文本准备查询对象,然后可以使该查询对象来查询索引数据库以获得相关细节
  • 搜索查询:​ 使用查询对象,检查索引数据库以获取相关详细信息和内容文档
  • 渲染结果:​ 当收到所需结果,应用程序应决定如何使用用户界面向用户显示搜索结果

分词技术

  • 分词技术:​ 搜索引擎针对用户提交查询的关键词串进行的查询处理后,根据用户的关键词串用各种匹配方法进行分词的一种技术

中文分词算法

基于字符串匹配

  • 基于字符串匹配:

  • 即扫描字符串,如果发现​字符串的子串和词相同,就算匹配
  • 这类分词通常会加入一些启发式规则:​正向/反向最大匹配​,​长词优先​等

  • 基于字符串匹配算法优点:

  • 速度快
  • 都是O(n)时间复杂度
  • 实现简单
  • 效果尚可

  • 基于字符串匹配算法缺点:
  • 对歧义和未登录词处理不好
  • ikanalyzer,paoding等就是基于字符串匹配的分词

基于统计及机器学习的分词方式

  • 基于统计及机器学习的分词方式:

  • 基于人工标注的词性和统计特征,对中文进行建模.​ 即根据观测到的数据(标注好的语料)对模型参数进行估计.即 ​训练
  • 在分词阶段再​通过模型计算各种分词出现的概率,将概率最大的分词结果作为最终结果
  • 常见的​序列标注模型:HMM,CRF

  • 基于统计及机器学习的分词方式优点:

  • 可以很好地处理歧义和未登录问题
  • 效果比基于字符串匹配算法更好

  • 基于统计及机器学习的分词方式缺点:

  • 需要大量的人工标注数据
  • 较慢的分词速度


IKAnalyzer


  • IKAnalyzer是一个开源的,基于Java语言开发的轻量级中文分词工具包
  • 基于文本匹配,不需要投入大量的人力进行训练和标注
  • 可以自定词典,方便加入特定领域的词语,能分出多粒度的结果

部署Solr并安装IKAnalyzer

  • 创建/usr/local/docker/solr/ikanalyzer目录
/usr/local/docker/solr        用于存放docker-compose.yml配置文件
/usr/local/docker/solr/ikanalyzer 用于存放Dockerfile镜像配置文件
  • docker-compose.yml
version: '3.1'
services:
solr:
build: ikanalyzer
restart: always
container_name: solr
ports:
- 8983:8983
volumes:
- ./solrdata:/opt/solrdata
  • Dockerfile​(在/usr/local/docker/solr/ikanalyzer中需要有文件:​ik-analyzer-solr5-5.x.jar,solr-analyzer-ik-5.1.0.jar,ext.dic,stopword.dic,IKAnalyzer.cfg.xml,managed-schema​)
FROM solr

# 创建Core
WORKDIR /opt/solr/server/solr
RUN mkdir ik_core
WORKDIR /opt/solr/server/solr/ik_core
RUN echo 'name=ik_core' > core.properties
RUN mkdir data
RUN cp -r ../configsets/sample_techproducts_configs/conf/ .

# 安装中文分词
WORKDIR /opt/solr/server/solr-webapp/webapp/WEB-INF/lib
ADD ik-analyzer-solr5-5.x.jar .
ADD solr-analyzer-ik-5.1.0.jar .
WORKDIR /opt/solr/server/solr-webapp/webapp/WEB-INF
ADD ext.dic .
ADD stopword.dic .
ADD IKAnalyzer.cfg.xml .

# 增加分词配置
COPY managed-schema /opt/solr/server/solr/ik_core/conf

WORKDIR /opt/solr
  • 构建镜像:​ 在/usr/local/docker/solr中执行命令
docker-compose up -d

Solr分析功能

修改managed-schema配置业务系统字段

  • Solr中自带的相同字段无需再添加,其它字段需要手动添加Solr字段​(通过编辑managed-schema配置文件来手动添加Solr字段)
<!-- 字段域 -->
<field name="tb_item_cid" type="plong" indexed="true" stored="true" />
<field name="tb_item_cname" type="text_ik" indexed="true" stored="true" />
<field name="tb_item_title" type="text_ik" indexed="true" stored="true" />
<field name="tb_item_sell_point" type="text_ik" indexed="true" stored="true" />
<field name="tb_item_desc" type="text_ik" indexed="true" stored="true" />

<!-- 复制域:Solr的搜索优化功能,,将多个字段复制到一个域,提高查询效率 -->
<field name="tb_item_keywords" type="text_ik" indexed="true" stored="false" multiValued="true" />
<copyField source="tb_item_cname" dest="tb_item_keywords">
<copyField source="tb_item_title" dest="tb_item_keywords">
<copyField source="tb_item_sell_point" dest="tb_item_keywords">
<copyField source="tb_item_desc" dest="tb_item_keywords">

复制配置到容器

docker cp managed-schema solr:/opt/solr/server/solr/ik_core/conf

重启容器

docker-compose restart
  • 在Solr的Web界面可以进行CRUD操作

SpringBoot整合Solr

创建搜索服务接口


  • 创建myshop-service-search-api项目,该项目只负责定义定义接口
  • pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.funtl</groupId>
<artifactId>myshop-dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../myshop-dependencies/pom.xml</relativePath>
</parent>

<artifactId>myshop-service-search-api</artifacteId>
<packaging>jar<packaging>
</project>
  • 在项目中创建SearchService接口
package com.oxford.myshop.service.search.api;

public interface SearchService {
List<TbItemResult> search(String query,int page,int rows);
}
  • 创建TbItemResult用于返回Solr结果集
package com.oxford.myshop.service.search.domain;

import java.io.Serializable;

public class TbItemResult implements Serializable {
private long id;
private long tbTtemCid;
private String tbItemCname;
private String tbItemTitle;
private String tbItemSellPoint;
private String tbItemDesc;

public long getId(){
return id;
}

public void setId(long id){
this.id=id;
}

public long getTbTtemCid(){
return tbTtemCid;
}

public void setTbTtemCid(long tbTtemCid){
this.tbTtemCid=tbTtemCid;
}

public String getTbItemCname(){
return tbItemCname;
}

public void setTbItemCname(String tbItemCname){
this.tbItemCname=tbItemCname;
}

public String getTbItemTitle(){
return tbItemTitle;
}

public void setTbItemTitle(String tbItemTitle){
this.tbItemTitle=tbItemTitle;
}

public String getTbItemSellPoint(){
return tbItemSellPoint;
}

public void setTbItemSellPoint(String tbItemSellPoint){
this.tbItemSellPoint=tbItemSellPoint;
}

public String getTbItemDesc(){
return tbItemDesc;
}

public void setTbItemDesc(String tbItemDesc){
this.tbItemDesc=tbItemDesc;
}
}

创建搜索服务提供者


  • 创建​myshop-service-search-provider​服务提供者项目
  • MyShopServiceSearchProviderApplication

package com.oxford.myshop.service.search.provider;

@EnableHystrix
@EnableHystrixDashboard
@SpringBootApplication(scanBasePackages="com.oxfrod.myshop")
@MapperScan(basePackages="com.oxford.myshop.service.search.provider.mapper")
public class MyShopServiceSearchProviderApplication {
public static void main(String[] args) {
SpringApplication.run(MyShopServiceSearchProviderApplication.class,args);
Main.main(args);
}
}
  • 在项目中创建​TbItemResultMapper​接口用于查询MySQL中的数据,用于插入到Solr数据库中
package com.oxford.myshop.service.search.provider.mapper;

@Respository
public interface TbItemResultMapper {
List<TbItemResult> selectAll();
}
Spring的四大注解:
1. @Controller
2. @Service
3. @Component
4. @Repository
  • 在resource中创建mapper包用于创建​TbContentCategoryMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.oxford.myshop.service.search.provider.mapper.TbItemResultMapper">
<resultMap id="BaseResultMap" type="com.oxford.myshop.service.search.domainTbItemResult">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="tb_item_cid" jdbcType="BIGINT" property="tbItemCid" />
<result column="tb_item_cname" jdbcType="VARCHAR" property="tbItemCname" />
<result column="tb_item_title" jdbcType="VARCHAR" property="tbItemTitle" />
<result column="tb_item_sell_point" jdbcType="VARCHAR" property="tbItemSellPoint" />
<result column="tb_item_desc" jdbcType="VARCHAR" property="tbItemDesc" />
</reslutMap>

<select id="selectAll" resultMap="BaseResultMap">
select
a.id,
a.title as tb_item_title,
a.sell_point as tb_item_sell_point,
a.cid as tb_item_cid,
b.name as tb_item_cname,
c.item_desc as tb_item_desc
from
tb_item as a
left join tb_item_cat as b
on a.cid=b.id
left join tb_item_desc as c
on a.id=c.item_id
</select>
</mapper>
初始化Solr:

public void initSolr() {
List<TbItemResult> tbItemResult=tbItemResultMapper.selectAll();

try{
SolrInputDocument document=null;
for(TbItemResult tbItemResult:tbItemResults){
document=new SolrInputDocument();
document.addFiled("id",tbItemResult.getId());
document.addFiled("tb_item_cid",tbItemResult.getTbItemCid());
document.addFiled("tb_item_cname",tbItemResult.getTbItemCname());
document.addFiled("tb_item_title",tbItemResult.getTbItemTitle());
document.addFiled("tb_item_sell_point",tbItemResult.getTbItemSellPoint());
document.addFiled("tb_item_desc",tbItemResult.getTbItemDesc());

solrClient.add(document);
solrClient.commit();
}
}catch(SolrServerException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
搜索Solr:

public void searchSolr(){
SolrQuery query=new SolrQuery();
// 设置查询条件
query.setQuery("手机");
// 分页查询
query.setStart(0);
query.setRows(10);
// 设置查询的默认域
query.set("df","tb_item_keywords");
// 设置高亮显示
query.setHighlight(true);
query.addHighlightField("tb_item_title");
query.setHighlightSimplePre("<span style='color:red;'>");
query.setHighlightSimplePost("</span>");

// 开始查询
try{
QueryResponse queryResponse=solrClient.query(query);
SolrDocumentList results=queryResponse.getResults();
// 获取高亮
Map<String,Map<String,List<String>>> highlighting=queryResponse.getHighlighting();
for(SolrDocument result:results){
List<String> strings=highlighting.get(result.get("id")).get(result.get("tb_item_title"))
if(strings!=null&&strings.size()>0){
String title=strings.get(0);
System.out.println(title);
}
}
}catch(SolrServerException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}
}
  • 创建SearchServiceImpl实现SearchService接口
package com.oxford.myshop.service.search.provider.api.impl;

@Service(version="${services.versions.search.v1}")
public class SearchServiceImpl implements SearchService{

@Autowired
private SolrClient solrClient;

@Override
public List<TbItemResult> search(String query,int page,int rows){
List<TbItemResult> searchResults=Lists.newArrayList();
SolrQuery query=new SolrQuery();
// 设置查询条件
query.setQuery("手机");
// 分页查询
query.setStart((page-1)*rows);
query.setRows(rows);
// 设置查询的默认域
query.set("df","tb_item_keywords");
// 设置高亮显示
query.setHighlight(true);
query.addHighlightField("tb_item_title");
query.setHighlightSimplePre("<span style='color:red;'>");
query.setHighlightSimplePost("</span>");

// 开始查询
try{
QueryResponse queryResponse=solrClient.query(query);
SolrDocumentList results=queryResponse.getResults();
// 获取高亮
Map<String,Map<String,List<String>>> highlighting=queryResponse.getHighlighting();
for(SolrDocument solrDocument:solrDocuments){
TbItemResult result=new TbResult();
result.setId(Long.parseLong(String.valueOf(solrDocument.get("id"))));
result.setTbItemCid(Long.parseLong(String.valueOf(solrDocument.get("tb_item_cid"))));
result.setTbItemCname((String)solrDocument.get("tb_item_cname"));
result.setTbItemTitle((String)solrDocument.get("tb_item_title"));
result.setTbItemSellPoint((String)solrDocument.get("tb_item_sell_point"));
result.setTbItemDesc((String)solrDocument.get("tb_item_desc"));

String tbItemTitle="";
List<String> list=highlighting.get(result.get("id")).get(result.get("tb_item_title"))
if(list!=null&&lsit.size()>0){
String title=list.get(0);
}else{
tbItemTitle=(String)solrDocument.get("tb_item_title");
}
result.setTbItemTitle(tbItemTitle);
searchResults.add(result);
}
}catch(SolrServerException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}
return searchResults;
}
}

创建搜索服务消费者


  • 创建搜索服务消费者​myshop-service-search-consumer​对Solr数据库中的数据进行检索
  • MyShopServiceSearchConsumerApplication

package com.oxford.myshop.service.search.consumer;

@EnableHystrix
@EnableHystrixDashboard
@SpringBootApplication(scanBasePackages="com.oxford.myshop",exclude=DataSourceAutoConfiguration.class)
public class MyShopServiceSearchConsumerApplication{
public static void main(String[] args){
SpringApplication.run(MyShopServiceSearchConsumerApplication.class,args);
Main.main(args);
}
}
  • SearchController
package com.oxford.myshop.service.search.consumer.controller;

@RestController
public class SearchController{
@Reference(version="${services.versions.search.v1}")
private SearchService searchService;

@RequestMapping(value="search/{query}/{page}/{rows}",method=RequestMethod.GET)
public List<TbItemResult> search(
@PathVariable(required=true) String query,
@PathVariable(required=true) int page,
@PathVariable(required=true) int rows
){
return searchService.search(query,page,rows)
}
}