写点什么

全文搜索引擎技术详解之 Apache Solr 的使用

发布于: 2021 年 05 月 27 日
全文搜索引擎技术详解之Apache 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
# 创建CoreWORKDIR /opt/solr/server/solrRUN mkdir ik_coreWORKDIR /opt/solr/server/solr/ik_coreRUN echo 'name=ik_core' > core.propertiesRUN mkdir dataRUN cp -r ../configsets/sample_techproducts_configs/conf/ .
# 安装中文分词WORKDIR /opt/solr/server/solr-webapp/webapp/WEB-INF/libADD ik-analyzer-solr5-5.x.jar .ADD solr-analyzer-ik-5.1.0.jar .WORKDIR /opt/solr/server/solr-webapp/webapp/WEB-INFADD 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;
@Respositorypublic interface TbItemResultMapper { List<TbItemResult> selectAll();}
复制代码


Spring的四大注解:1. @Controller2. @Service3. @Component4. @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;
@RestControllerpublic 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) }}
复制代码


发布于: 2021 年 05 月 27 日阅读数: 239
用户头像

一位攻城狮的自我修养 2021.04.06 加入

分享技术干货,面试题和攻城狮故事。 你的关注支持是我持续进步的最大动力! https://github.com/ChovaVea

评论 (2 条评论)

发布
用户头像
 IK 5.x版本的分词对solr 7.X以上就有兼容性问题了吧~ 个人感觉solr addBean的方式会比addDocument的方式更好一点,容错率会更小
2021 年 05 月 28 日 11:39
回复
感谢你的建议!以前在项目中是用得addDocument,刚刚再看了一下文档,的确是用addBean更好一点🤙🏻
2021 年 05 月 28 日 13:08
回复
没有更多了
全文搜索引擎技术详解之Apache Solr的使用