搜索引擎Solr详解:从0开始搭建、维护及各类使用
一. Solr简介及入门
1.1 什么是搜索引擎
1.1 什么是搜索引擎?
- 专业角度:
- 搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上回搜集信息,在对信答息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。
- 所谓搜索引擎,就是根据用户需求与一定算法,运用特定策略从互联网检索出制定信息反馈给用户的一门检索技术。搜索引擎依托于多种技术,如网络爬虫技术、检索排序技术、网页处理技术、大数据处理技术、自然语言处理技术等,为信息检索用户提供快速、高相关性的信息服务。搜索引擎技术的核心模块一般包括爬虫、索引、检索和排序等,同时可添加其他一系列辅助模块,以为用户创造更好的网络使用环境。
- 一个搜索引擎由搜索器 、索引器 、检索器 和用户接口 四个部分组成。搜索器的功能是在互联网 中漫游,发现和搜集信息。索引器的功能是理解搜索器所搜索的信息,从中抽取出索引项,用于表示文档 以及生成文档库的索引表。检索器的功能是根据用户的查询在索引库中快速检出文档,进行文档与查询的相关度评价,对将要输出的结果进行排序,并实现某种用户相关性反馈机制。用户接口的作用是输入用户查询、显示查询结果、提供用户相关性反馈机制。
- 通俗来说,搜索引擎就是协助用户完成搜索指定内容需求的一个工具。它拥有及其优良的查询性能,优秀的查询适配功能,能够满足大数据中迅速搜索目标数据的能力。
- 常见的搜索有: 百度、谷歌等。他们是最终落地的产品,其内部借助的搜索引擎则是其核心。谷歌搜索就使用到了ElasticSearch作为核心进行搜索。作为更加老牌的搜索引擎产品Solr,也值得学习。
1.2 什么是solr
- Solr是一个开源搜索平台,用于构建搜索应用程序。 它建立在Lucene(全文搜索引擎)之上。 Solr是企业级的,快速的和高度可扩展的。 使用Solr构建的应用程序非常复杂,可提供高性能。
- Solr是一个发展中的,不断演进优化的产品,它虽然老牌,但仍跟随时代。比如自带Netty容器,不需要再继续安装Tomcat才能运行了。支持Solr集群,支持并行查询等。
- Solr是一个Restful 风格的全文检索引擎,所以它可以在任何语言上运行。它使用Java基于Lucene开发的全文检索服务,是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口。用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引;也可以通过Http Get操作提出查找请求,并得到XML格式的返回结果。
- Solr(读作“solar”)是Apache Lucene项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如Word、PDF)的处理。Solr是高度可扩展的,并提供了分布式搜索和索引复制。Solr是最流行的企业级搜索引擎,Solr4 还增加了NoSQL支持。
- Solr是用Java编写、运行在Servlet容器(如 Apache Tomcat 或Jetty)的一个独立的全文搜索服务器。 Solr采用了 Lucene Java 搜索库为核心的全文索引和搜索,并具有类似REST的HTTP/XML和JSON的API。Solr强大的外部配置功能使得无需进行Java编码,便可对 其进行调整以适应多种类型的应用程序。Solr有一个插件架构,以支持更多的高级定制。
- 因为2010年 Apache Lucene 和 Apache Solr 项目合并,两个项目是由同一个Apache软件基金会开发团队制作实现的。提到技术或产品时,Lucene/Solr或Solr/Lucene是一样的。
1.3 Solr与其他产品对比
- 与数据库相比:
- 倒排索引,预先分词,相比于数据库,join和like本身就是性能杀手,所以它在大部分场景下查询效率更快。
- 地理GPS查询、高亮搜索、高级聚合查询,它能够完成数据库有些做得不够好或者做不到的地方。
- 基于Restful风格,兼容性高,可扩展性强,比MySQL更易于使用。
- 与ES对比:
- 当实时建立索引的时候,solr会产生io阻塞,而es则不会,es查询性能要高于solr。
- 在不断动态添加数据的时候,solr的检索效率会变的低下,而es则没有什么变化。
- Solr利用zookeeper进行分布式管理,而es自身带有分布式系统管理功能。Solr一般都要部署到web服务器上,比如tomcat(新版本已自带Web服务器了,可以直接解压后启动)。启动tomcat的时候需要配置tomcat与solr的关联。【Solr 的本质 是一个动态web项目】
- Solr支持更多的格式数据[xml,json,csv等],而es仅支持json文件格式。
- Solr是传统搜索应用的有力解决方案,但是es更适用于新兴的实时搜索应用。 单纯的对已有数据进行检索的时候,solr效率更好,高于es。
- Solr官网提供的功能更多,而es本身更注重于核心功能,高级功能多有第三方插件
ES更适合动态数据处理,海量数据处理。而Solr更适合复杂,多情况的处理,因为它原生提供的功能更多。综合来讲,ES要优于Solr,但是Solr也是一个不错的选择。
1.4 Solr的特点、优缺点、使用场景
- 特点: 1.基于标准的开放接口:Solr搜索服务器支持通过XML、JSON和HTTP查询和获取结果。 2.易管理:Solr可以通过HTML页面管理,Solr配置通过XML完成。 3.可伸缩性:能够有效地复制到另外一个Solr搜索服务器。 4.灵活的插件体系:新功能能够以插件的形式方便的添加到Solr服务器上。 5.强大的数据导入功能:数据库和其他结构化数据源现在都可以导入、映射和转化。
- 优缺点:
- 优点:
- 软件成熟,有广大的用户群体,活跃的社区,更新迭代的团队。
- 支持多种格式的索引,如: HTML、PDF、微软 Office 系列软件格式以及 JSON、XML、CSV 等纯文本格式。
- 功能丰富,满足多种业务场景,是传统搜索应用的有力解决方案。它支持GPS经纬度查询(可实现附近的数据,经纬度范围内的数据等),高亮查询,各类聚合查询等
- 缺点:
- 建立索引时,搜索效率下降,实时索引效率不高。
- 相比于ES,竞争力稍弱。
- 随着数据量的增加,Solr的搜索效率会变得更低,而ElasticSearch却没有明显的变化。
- 使用场景:
- 传统搜索应用的有力助手,如租房平台的多条件房源搜索(附近的房源,商圈内房源,按租金,关键词,户型,朝向,套几等等关键词搜索)
- 可服务于数据量不是特别大,更新频率不是特别快的场景
- 另外可根据Solr的优缺点来看,优点就是适合的场景,缺点就是不适合的场景。
w3c的solr文档:www.w3cschool.cn/solr_doc/
二. Solr搭建及维护
2.1 Windows端搭建Solr
- 进入官网 选择下载合适的版本,可选择下载较为新的版本,此处以 Solr 7.7.2为例
- 解压后进入cmd进入bin目录执行 solr start命令,命令行显示如下,启动成功,默认端口8983,也可通过-p指定端口启动:
- 打开浏览器,输入:
出现如下界面后表示安装成功:http://localhost:8983/solr
Windows需要安装JDK
2.2 Linux端搭建Solr教程
- 确认服务器中是否有Jdk等环境支持。
- 下载Solr:
wget http://mirror.bit.edu.cn/apache/lucene/solr/7.6.0/solr-7.6.0.tgz
- 解压solr:
tar -xzvf solr-7.6.0.tgz
- 输入命令运行Solr:
bin/solr start -force
- 输入命令停止运行Solr:
bin/solr stop -force
也可以直接杀死进程
- 输入命令重启solr:
bin/solr restart -force
2.3 Solr的管理界面的使用
2.3.1 Dashboard(仪表盘)
- 它是一个监控界面,可以直观的看到JVM的运行参数,虚拟机内存使用情况,Solr版本,系统内存等信息:
2.3.2 Logging(日志)
- 它是关于日志以及日志级别相关的,Logging里面显示的是Solr运行出现的异常或错误:
2.3.3 Core Admin(core管理)
- 它是整个库的核心。在这里面:
- Add Core:添加新的库(也可以叫核心、或者Core)
- Reload:重新加载指定的库
- Unload:卸载库(此功能慎用0.0)
- Rename: 重新命名库
- Optimize:优化索引库
它里面还有别的一些功能以及展示一些别的信息:
- 新增索引库时,可注意如下名称含义:
- name: 给core起的名字
- instanceDir: 与我们在配置solr到tomcat里时的solr_home里新建的core文件夹名一致;
- dataDir:确认Add Core时,会在new_core目录下生成名为data的文件夹
- config:new_core下的conf下的config配置文件(solrconfig.xml)
- schema: new_core下的conf下的schema文件(schema.xml)
确认Add Core时,会在new_core下生成data文件夹,与core.properties文件。
2.3.5 Core新增索引库的方法
- windows下:
- 打开dos命令窗口,切换目录到${solr.home}\bin,然后输入:solr create -c corename之后回车;
- 打开solr安装文件,在/server/solr下就会出现新的文件夹corename(就是新创建的core);
- 打开浏览器,输入solr访问路径:http://localhost:8983/solr,就会看到新建的core
- Linux下:
- 直接在/server/solr下创建新文件夹,名字自定义,此处命名为newcore,作为新建的core;
- 找到/server/solr/configsets/_default目录下的conf文件夹,然后拷贝一份到/server/solr/newcore目录节点下。
- 然后按照下图操作
2.3.6 Java Properties(Java 属性)
- 顾名思义,它展示了一些Java的配置:
2.3.7 Thread Dump(相关线程)
2.4 Core Selector
- Core Selector 是对Solr中的数据进行操作、分析等。此章节将为大家讲解此模块的各个菜单:
- 点击具体的Core后,出现如下界面:
- 选择 core
如图所示,house_core是我们自己定义的一个core。在Solr中每一个Core代表一个索引库,里面包含索引数据及其配置信息。 Solr中可以拥有多个Core,也就是同时管理多个索引库,就像mysql中可以有多个数据库一样。我们使用solr的语法就可以访问core中的数据了。solr core内的数据支持增删查改。下一章节会举例说明;
- Overview(概览): 能够查看我们的运行实例信息
- Analysis(分析): 通过使用Analysis,我们能够实时的分析我们的语句解析情况。如使用ik分词器,检测内容:“我是中国人”的分词结果如图:
- Dataimport(从数据库导入数据):前提是已经配置好了相关的配置,则可以使用此功能进行数据库导入。有兴趣的可以百度了解。此处步骤暂略。
- Comman选项:full_import:全导入;delta_import:增量导入。 所谓delta-import主要是对于数据库(也可能是文件等等)中增加或者被修改的字段进行导入。主要原理是利用率每次我们进行import的时候在solr.home\conf下面生成的dataimport.properties文件,此文件里面有最近一次导入的相关信息。这个文件如下: #Tue Jul 19 10:15:50 CST 2016 advertiser.last_index_time=2016-07-19 10:15:49 last_index_time=2016-07-19 10:15:49 其实last_index_time是最近一次索引(full-import或者delta-import)的时间。 通过比较这个时间和我们数据库表中的timestamp列即可得出哪些是之后修改或者添加的。
- Verbose: Clean: 在索引开始构建之前是否删除之前的索引,默认为true Commit: 在索引完成之后是否提交。默认为true Optimize: 是否在索引完成之后对索引进行优化。默认为true Debug: 是否以调试模式运行,适用于交互式开发(interactive development mode)之中。请注意,如果以调试模式运行,那么默认不会自动提交,请加参数“commit=true” Entity: entity是document下面的标签(data-config.xml)。使用这个参数可以有选择的执行一个或多个entity 。使用多个entity 参数可以使得多个entity同时运行。如果不选择此参数那么所有的都会被运行。 Start,Rows:分页 Custom Parameters:导入条件 Excute:执行导入。 Refresh Status:刷新后才能看到数据发生了变化,如果刷新后数据还是0,说明未导入。
- Document(文档操作):(索引文档)索引的相关操作,如:增加,修改,删除等,例如我们要增加一个索引(companyName)的办法:
- 先要在solr 的D:\solr_home\mycore1\conf 的 schema.xml配置文件下,增加相关的字段field
<field name="companyName" type="text_ik" indexed="false" stored="true" multiValued="false" />
新版本solr可能没有schema.xml的话,可在要配置的core的相应conf 内寻找managed-schema 文件
- 使用 Core Admin -> Reload命令进行重载或者对solr服务器重启,刷新索引:
- 在如下页面,选择/update ,文档格式选择json ,然后submit 提交。这样 索引就增加上了。修改与增加一样,都是/update ,删除为/delete 。成功之后,我们去query里查询数据就能查到我们刚添加的数据.
如果此处的advertiserId与advertiserType没有配置field 的话,可根据后面的教程配置,或者去掉这两个再进行尝试新增、修改、删除功能;
- File: 能够查看服务器中solr的文件信息,并可进行配置:
Request-Handler(qt): 要进行的操作(update\delete) Document Type:类型,有JSON、XML等格式 Document(s): 内容,手动写的内容。 Commit Within: 在内部提交 Overwrite: 为true,说明如果id重复则覆盖以前的值;为false说明如果id重复不覆盖以前的值. Boost: 好像是什么版本,没用过
- Ping: 网络延迟,按一下可以看这个Core还是不是活着的以及响应时间。
点击Ping按钮相当于发送一个请求到对应的Core并获得答复。对应的http API是 /admin/ping (http://localhost:8983/solr//admin/ping),后端的处理逻辑是PingRequestHandler类
- Plugins/Stats: Solr自带的一些插件以及我们安装的插件的信息以及统计。Plugins页面展示的是在Core下运行的各种插件的统计数据和状态信息。比如,可以找到Solr Cache的性能统计数据,Solr Searcher的状态,Request Handler和Search Component的配置信息等等。
- Query:一个简单的结构化查询工具,下一节详细介绍
- Replication: 显示你当前Core的副本,并提供disable/enable功能。在单机环境下可进行实例与实例之间的配置。在实例之间复制数据,保证数据的可用性,从而提高系统的服务能力。
为Master-slave的副本工作方式提供了一个管理和查看副本的页面,SolrCloud中已经取代了这个页面上的大部分功能,单机版也无需关注该功能。(如果使用的是SolrCloud,不要使用该页面上的DisableReplication功能。)
- schema:展示该Core的shema数据,如果是用ManagedSchema模式,还可以通过该页面修改,增加,删除schema的字段。(对比Solr4,是一个比较实用的提升。)
- Segments info: 展示底层Lucence的分段信息。展示底层Lucence索引段,包括每个段的大小(字节大小和数据条数)以及其他的一些基本元数据信息,最显眼的是deleted documents数量,把鼠标移动到段上可以看到更多的数据信息。这些信息可以帮管理员做性能优化,优化该数据集的合并段的设置。
2.4 Query的查询详解
q: 查询字符串(必须的)。*:*表示查询所有;keyword:东看 表示按关键字“东看”查询 fq: filter query 过滤查询。使用Filter Query可以充分利用Filter Query Cache,提高检索性能。作用:在q查询符合结果中同时是fq查询符合的(类似求交集),例如:q=mm&fq=date_time:[20081001 TO 20091031],找关键字mm,并且date_time是20081001到20091031之间的。 sort: 排序。格式如下:字段名 排序方式;如advertiserId desc 表示按id字段降序排列查询结果。 start,rows:表示查回结果从第几条数据开始显示,共显示多少条。 fl: field list。指定查询结果返回哪些字段。多个时以空格“ ”或逗号“,”分隔。不指定时,默认全返回。 df: default field默认的查询字段,一般默认指定。 Raw Query Parameters: wt: write type。指定查询输出结果格式,我们常用的有json格式与xml格式。在solrconfig.xml中定义了查询输出格式:xml、json、python、ruby、php、phps、custom。 indent: 返回的结果是否缩进,默认关闭,用 indent=true | on 开启,一般调试json,php,phps,ruby输出才有必要用这个参数。 debugQuery: 设置返回结果是否显示Debug信息。 dismax: edismax: hl: high light 高亮。hl=true表示启用高亮 hl.fl : 用空格或逗号隔开的字段列表(指定高亮的字段)。要启用某个字段的highlight功能,就得保证该字段在schema中是stored。如果该参数未被给出,那么就会高 亮默认字段 standard handler会用df参数,dismax字段用qf参数。你可以使用星号去方便的高亮所有字段。如果你使用了通配符,那么要考虑启用 hl.requiredFieldMatch选项。 hl.simple.pre: hl.requireFieldMatch: 如果置为true,除非该字段的查询结果不为空才会被高亮。它的默认值是false,意味 着它可能匹配某个字段却高亮一个不同的字段。如果hl.fl使用了通配符,那么就要启用该参数。尽管如此,如果你的查询是all字段(可能是使用 copy-field 指令),那么还是把它设为false,这样搜索结果能表明哪个字段的查询文本未被找到 hl.usePhraseHighlighter:如果一个查询中含有短语(引号框起来的)那么会保证一定要完全匹配短语的才会被高亮。 hl.highlightMultiTerm:如果使用通配符和模糊搜索,那么会确保与通配符匹配的term会高亮。默认为false,同时hl.usePhraseHighlighter要为true。 facet:分组统计,在搜索关键字的同时,能够按照Facet的字段进行分组并统计。 facet.query:Facet Query利用类似于filter query的语法提供了更为灵活的Facet.通过facet.query参数,可以对任意字段进行筛选。 facet.field:需要分组统计的字段,可以多个。 facet.prefix: 表示Facet字段值的前缀。比如facet.field=cpu&facet.prefix=Intel,那么对cpu字段进行Facet查询,返回的cpu都是以Intel开头的, AMD开头的cpu型号将不会被统计在内。 spatial: spellcheck: 拼写检查。
2.5 Solr的配置文件的使用
schema.xml
这个配置文件的根本目的是为了通过配置告诉Solr如何建立索引。在老版本叫做schema.xml,在较新的solr版本中叫做:managed-schema,注意区分。
2.6 Solr的分词配置
2.7 Solr的定位配置
三. Solr后端引入及基本使用
3.1 引入SpringBoot项目教程
- 在项目工程中
pom.xml (Maven)
配置依赖:<dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-solrj</artifactId> <version>8.5.2</version> </dependency>
solr 支持Restful风格的Http请求,所以当部署成功后,我们可以直接以普通请求访问。而引入solrj则能够使用到第三方封装的一些方法,使用面向对象的方式去方便的操作solr。 solrj的版本可能会有差异,会导致查询方式有变化,这点请注意。
- 在
application.yml
中加入配置:# Solr相关配置 solr: url: http://ip地址:8983/solr # solr连接地址 core_house: xxxx # 库,你创建的core叫什么,就传什么 initialSize: 5 # 初始化连接数 maxIdleSize: 5 # 最大空闲连接数 maxActiveSize: 10 # 最大活动连接数 connTimeOut: 60 # 等待时间
- 引入配置类:
- Kotlin 代码:
import gw.commonkotlin.exception.BaseException import org.apache.solr.client.solrj.impl.HttpSolrClient import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import java.util.* import javax.annotation.PostConstruct /** * @author zhanglei * @data: 2020/11/2 */ @Service class SolrConfig { // 空闲连接集合 private val freeConnection: MutableList<HttpSolrClient> = Vector() // 活动连接集合 private val activeConnection: MutableList<HttpSolrClient?> = Vector() // solr连接地址 @Value("\${solr.url:}") private val url: String? = null // 初始化连接数 @Value("\${solr.initialSize:1}") private val initialSize = 0 // 最大空闲连接数 @Value("\${solr.maxIdleSize:1}") private val maxIdleSize = 0 // 最大活动连接数 @Value("\${solr.maxActiveSize:1}") private val maxActiveSize = 0 // 等待时间 @Value("\${solr.connTimeOut:30}") private val connTimeOut = 0 // 库 @Value("\${solr.core_house:}") private val coreHouse: String? = null private val lock = Object() // 初始化 @PostConstruct fun init() { try { for (i in 0 until initialSize) { val newConnection = newConnection() freeConnection.add(newConnection) } } catch (e: Exception) { logger.error("solr初始化失败", e) throw BaseException("初始化Solr失败,请检查配置参数!") } } // 创建新的Connection private fun newConnection(): HttpSolrClient { val client: HttpSolrClient client = try { HttpSolrClient.Builder("$url/$coreHouse").build() } catch (e: Exception) { logger.error("创建Solr客户端失败!", e) throw BaseException("创建Solr客户端失败!") } connCount++ return client }// 大于最大活动连接,进行等待,重新获取连接 // 递归调用getConnection方法 // 创建新的连接 // 还有活动连接可以使用 fun connection(): HttpSolrClient { var connection: HttpSolrClient? =null try { if (connCount < maxActiveSize) { // 还有活动连接可以使用 connection = if (freeConnection.size > 0) { freeConnection.removeAt(0) } else { // 创建新的连接 newConnection() } if (availableFlag(connection)) { activeConnection.add(connection) } else { connCount-- connection = connection() } } else { synchronized(lock) { // 大于最大活动连接,进行等待,重新获取连接 lock.wait(connTimeOut.toLong()) } // 递归调用getConnection方法 connection = connection() } } catch (e: Exception) { e.printStackTrace() } return connection!! } // 判断连接是否可用 fun availableFlag(connection: HttpSolrClient?): Boolean { return connection != null } // 关闭连接 fun close(connection: HttpSolrClient) { try { if (availableFlag(connection)) { // 判断空闲连接集合是否大于最大空闲连接数 if (freeConnection.size < maxIdleSize) { freeConnection.add(connection) } else { // 空闲连接数已经满了 connection.close() connCount-- } activeConnection.remove(connection) synchronized(lock) { lock.notifyAll() } } } catch (e: Exception) { logger.error("删除连接出错", e) throw BaseException("删除连接出错!") } } companion object { private val logger = LoggerFactory.getLogger(SolrConfig::class.java) // 记录连接总数 private var connCount = 0 } }
- Java版本代码:
import java.util.List; import java.util.Vector; import org.apache.solr.client.solrj.impl.HttpSolrClient; /** * solr连接池 * * @author a_bo * @version 创建时间:2019年4月1日 上午10:24:05 */ public class SolrConnectionPool { // 空闲连接集合 private List<HttpSolrClient> freeConnection = new Vector<HttpSolrClient>(); // 活动连接集合 private List<HttpSolrClient> activeConnection = new Vector<HttpSolrClient>(); // 记录连接总数 private static int connCount; // solr连接地址 private String url; // 初始化连接数 private int initialSize; // 最大空闲连接数 private int maxIdleSize; // 最大活动连接数 private int maxActiveSize; // 等待时间 private int connTimeOut; public static int getConnCount() { return connCount; } public static void setConnCount(int connCount) { SolrConnectionPool.connCount = connCount; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public int getInitialSize() { return initialSize; } public void setInitialSize(int initialSize) { this.initialSize = initialSize; } public int getConnTimeOut() { return connTimeOut; } public void setConnTimeOut(int connTimeOut) { this.connTimeOut = connTimeOut; } public int getMaxIdleSize() { return maxIdleSize; } public void setMaxIdleSize(int maxIdleSize) { this.maxIdleSize = maxIdleSize; } public int getMaxActiveSize() { return maxActiveSize; } public void setMaxActiveSize(int maxActiveSize) { this.maxActiveSize = maxActiveSize; } // 初始化 public void init() { try { for (int i = 0; i < initialSize; i++) { HttpSolrClient newConnection = newConnection(); if (newConnection != null) { // 添加到空闲连接中... freeConnection.add(newConnection); } } } catch (Exception e) { e.getStackTrace(); throw new RuntimeException("初始化Solr失败,请检查配置参数!"); } } // 创建新的Connection private HttpSolrClient newConnection() { HttpSolrClient client = null; try { HttpSolrClient.Builder builder = new HttpSolrClient.Builder(url); client = builder.build(); } catch (Exception e) { e.getStackTrace(); throw new RuntimeException("创建Solr客户端失败!"); } connCount++; return client; } public HttpSolrClient getConnection() { HttpSolrClient connection = null; try { if (connCount < maxActiveSize) { // 还有活动连接可以使用 if (freeConnection.size() > 0) { connection = freeConnection.remove(0); } else { // 创建新的连接 connection = newConnection(); } if (isAvailable(connection)) { activeConnection.add(connection); } else { connCount--; connection = getConnection(); } } else { synchronized (this) { // 大于最大活动连接,进行等待,重新获取连接 wait(connTimeOut); } connection = getConnection();// 递归调用getConnection方法 } } catch (Exception e) { e.printStackTrace(); } return connection; } // 判断连接是否可用 public boolean isAvailable(HttpSolrClient connection) { // 此处没想好怎么判断连接是否可用 if (connection == null) { return false; } return true; } // 关闭连接 public void close(HttpSolrClient connection) { try { if (isAvailable(connection)) { // 判断空闲连接集合是否大于最大空闲连接数 if (freeConnection.size() < maxIdleSize) { freeConnection.add(connection); } else { // 空闲连接数已经满了 connection.close(); connCount--; } activeConnection.remove(connection); synchronized (this) { notifyAll(); } } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("删除连接出错!"); } } }
> 如果使用的是java,则使用下面的连接池即可。2个代码可能少部分不同,因语言差异导致,可以忽略。
- Kotlin 代码:
3.2 基本查询方法:增、删、查、改
- 测试代码如下:
import com.yzz.house.config.SolrConfig; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.beans.Field; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.client.solrj.response.UpdateResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.io.Serializable; /** * @author CSDN-暗余 */ public class Student implements Serializable { @Field("name") private String name; @Field("age") private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } } @Service class Test { @Autowired private SolrConfig solrConfig; /** * 测试新增/修改 * 若无此id的数据,则新增,若有此id的数据则修改。 * * @return boolean 是否成功的状态 * @throws IOException * @throws SolrServerException */ public Boolean testInsert() throws IOException, SolrServerException { Student student = new Student(); student.setName("张三"); student.setAge(15); HttpSolrClient connection = solrConfig.connection(); connection.addBean(student); connection.close(); solrConfig.close(connection); return true; } /** * 测试删除 * @return boolean 是否删除成功的状态值 * @throws IOException * @throws SolrServerException */ public Boolean testDelete() throws IOException, SolrServerException { HttpSolrClient connection = solrConfig.connection(); UpdateResponse updateResponse = connection.deleteById("1"); return true; } /** * 查询 */ public SolrDocument testGetById() throws IOException, SolrServerException { HttpSolrClient connection = solrConfig.connection(); SolrDocument solrDocument = connection.getById("1"); return solrDocument; } /** * 查询 * @return */ public SolrDocumentList testQuery() throws IOException, SolrServerException { HttpSolrClient connection = solrConfig.connection(); SolrQuery solrQuery= new SolrQuery(); solrQuery.set("q", "*:*"); QueryResponse query = connection.query(solrQuery); SolrDocumentList results = query.getResults(); return results; } }
首先需要在Solr的配置文件中配置好索引index。 然后定义一个合规的Field对象。对此对象进行赋值,然后保存并更新至Solr,最后关闭solrj的连接流和我们配置的流。我们自身的solrConfig可以不用配置连接池也可以,可自行选择。 这里的代码只是一些示例作用,如果你能够看懂它,那么你已经基本能够使用Solr了,当在你的项目中,你可以基于demo去按自己的风格实现或者封装;后面会分享博主自己的封装solrj代码示例;
3.3 注意事项
- 使用Solrj进行新增、修改时,需要对传输的对象的每个变量加上
@Field
注解,具体的使用方式可以百度。 - 加上
@Field
的变量,必须与Solr中的索引对应上,即字段名一样,类型是对应关系的。 - 主键id必须是字符串,在调用Solrj的
getById()
方法以及deleteById()
都是按照id来进行删除的。 - Solrj的每个版本可能连接方式以及方法可能会有变化,以实际为准。
- Solrj中的
addBean()
方法,可新增和修改。如果存在此id的对象则新增,若不存在则修改。
四. Solr高级语法
4.1 分词
- 用solr自带的text分词它会把一句话分成单个的字。如果是英文环境就可以适配。但是在中文环境下无法适配词汇。所以我们可以使用分词器。Solr有自带的分词器,但是博主建议使用IK分词器进行分词。首先它的分词效果更好、支持敏感词过滤、自定义词汇等扩展性功能。
- IK分词器介绍
- IK分词器安装步骤:
- 下载Ik分词器资源 github.com/magese/ik-a…
- 将ik-analyzer-solr7-7.x.jar包上传到 $SOLR_INSTALL_HOME/server/solr-webapp/webapp/WEB-INF/lib目录下
- 在$SOLR_INSTALL_HOME/server/solr-webapp/webapp/WEB-INF目录下新建目录classes目录,并将IK分词器中源码的资源文件上传到该目录下,如图所示:
ext.dic自定义词 如沙雕 在汉语里面不是一个词 ,它只是一个网络用语,可以配置到这里面让它成为一个词
stopword.dic 停止字典, 如:啊 吧 唉 不作分词 IKAnalyzer.cfg.xml配置ik的配置文件 不用改 Jar:如果要使用ik分词要导入的jar包 - 修改managed-sahma:
<!--添加一个中文分词器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>
useSmart 和分词的粒度相关:false: 分词的粒度大,一句话里面分的词语少; true:分词的粒度细,一句话里面分的词语多.那我们在导入时需要的关键字多吗?让索引的数据量少一点。我们的粒度大:false;我们在搜索时需要的关键字多吗?我们想尽可能的覆盖所有的范围,我们的粒度要细:true
- 重启solr,在界面测试,如图所示:
- 使用自带分词器步骤:
- 分词器的jar包到solr-webapp目录下:
cp $SOLR_INSTALL_HOME/contrib/analysis-extras/lucene-libs/lucene-analyzers-smartcn-7.7.2.jar $SOLR_INSTALL_HOME//server/solr-webapp/webapp/WEB-INF/lib/
- 编辑$SOLR_INSTALL_HOME/server/solr/my_core/conf/managed-schema文件,引入自带分词器,在文件底部加入以下内容(PS:在schema标签内):
<!-- solr自带分词器 --> <fieldType name="text_smartcn" class="solr.TextField" positionIncrementGap="0"> <analyzer type="index"> <tokenizer class="org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory"/> </analyzer> <analyzer type="query"> <tokenizer class="org.apache.lucene.analysis.cn.smart.HMMChineseTokenizerFactory"/> </analyzer> </fieldType>
- 重启solr,在界面测试,如图所示:
4.2 高亮
4.3 分组
4.4 统计
4.5 其他语法
五. Solr优化
5.1 Solr查询连接池
- 参照本篇文章3.1即可
5.2 Solr查询封装-Kotlin
- SolrApi :
import com.yzz.house.solr.service.SolrService
import com.yzz.house.solr.service.SolrWrapper
import gw.commonkotlin.respond.PageResult
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
@Controller
class SolrApi {
@Autowired
private lateinit var solrService: SolrService
/**
* 多条件查询
*/
fun <T> list(solrWrapper: SolrWrapper, clazz: Class<T>): PageResult<T> {
return solrService.list(solrWrapper, clazz)
}
/**
* 查询单个
*/
fun <T> getById(id: String, clazz: Class<T>): T? {
return solrService.getById(id, clazz)
}
/**
* 新增或修改
*/
fun saveOrUpdate(field: Any): Boolean {
return solrService.saveOrUpdate(field)
}
/**
* 批量新增或修改
*/
fun saveOrUpdateBatch(fields: List<Any>): Boolean {
return solrService.saveOrUpdateBatch(fields)
}
/**
* 删除
*/
fun removeById(id: String): Boolean {
return solrService.removeByIds(listOf(id))
}
/**
* 批量删除
*/
fun removeByIds(ids: List<String>): Boolean {
return solrService.removeByIds(ids)
}
/**
* 根据搜索条件删除
*/
fun remove(solrWrapper: SolrWrapper): Boolean {
return solrService.remove(solrWrapper)
}
/**
* 根据搜索条件删除
*/
fun remove(query: String): Boolean {
return solrService.remove(query)
}
}
- SolrService :
import com.xxx.house.config.SolrConfig
import com.xxx.house.solr.SolrApi
import gw.commonkotlin.respond.PageResult
import gw.commonkotlin.utils.issEmpty
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
/**
* @author:Anyu-csdn
* @date: 2020/11/19|
*/
@Service
class SolrService : AbstractSolrService() {
private final val logger: Logger = LoggerFactory.getLogger(SolrApi::class.java)
@Autowired
private lateinit var solrConfig: SolrConfig
fun <T> list(solrWrapper: SolrWrapper, clazz: Class<T>): PageResult<T> {
// 1. 建立连接 使用之前的连接池连接
val connection = solrConfig.connection()
// 2. 建立查询请求参数并查询
val query = connection.query(solrWrapper.buildSolrQuery())
// 3.获取结果
val results = query.results
// 4.关闭或归还连接至线程池
solrConfig.close(connection)
return PageResult(query.getBeans(clazz), results.numFound)
}
fun <T> getById(id: String, clazz: Class<T>): T? {
return try {
// 1. 建立连接
val connection = solrConfig.connection()
// 2. 进行查询
val solrDocument = connection.getById(id)
solrConfig.close(connection)
// 3. 进行封装
getBean(solrDocument, clazz)
} catch (e: Exception) {
logger.error("查询失败", e)
null
}
}
fun saveOrUpdate(field: Any): Boolean {
return try {
// 0. 校验参数
if (field.issEmpty()) return false
// 1. 建立连接
val connection = solrConfig.connection()
// 2. 进行新增或修改
connection.addBean(field)
// 3. 提交
connection.commit()
// 4. 关闭连接或将连接返回连接池
solrConfig.close(connection)
true
} catch (e: Exception) {
logger.error("solr新增或修改失败", e)
false
}
}
fun saveOrUpdateBatch(fields: List<Any>): Boolean {
return try {
// 0. 校验参数
if (fields.issEmpty()) return false
// 1. 建立连接
val connection = solrConfig.connection()
// 2. 进行新增或修改
connection.addBeans(fields)
// 3. 提交
connection.commit()
// 4. 关闭连接或将连接返回连接池
solrConfig.close(connection)
true
} catch (e: Exception) {
logger.error("solr新增或修改失败", e)
false
}
}
fun removeByIds(ids: List<String>): Boolean {
return try {
// 0. 校验参数
if (ids.issEmpty()) return false
// 1. 建立连接
val connection = solrConfig.connection()
// 2. 删除数据
connection.deleteById(ids)
// 3. 确认提交
connection.commit()
// 4. 关闭连接或将连接返回连接池
solrConfig.close(connection)
true
} catch (e: Exception) {
logger.error("删除失败", e)
false
}
}
/**
* 根据搜索条件删除
*/
fun remove(solrWrapper: SolrWrapper): Boolean {
return try {
// 0. 校验参数
if (solrWrapper.issEmpty()) return false
// 1. 建立连接
val connection = solrConfig.connection()
// 2. 删除数据
connection.deleteByQuery(solrWrapper.buildSolrQuery().query)
// 3. 确认提交
connection.commit()
// 4. 关闭连接或将连接返回连接池
solrConfig.close(connection)
true
} catch (e: Exception) {
logger.error("删除失败", e)
false
}
}
/**
* 根据搜索条件删除
*/
fun remove(query: String): Boolean {
return try {
// 0. 校验参数
if (query.issEmpty()) return false
// 1. 建立连接
val connection = solrConfig.connection()
// 2. 删除数据
connection.deleteByQuery(query)
// 3. 确认提交
connection.commit()
// 4. 关闭连接或将连接返回连接池
solrConfig.close(connection)
true
} catch (e: Exception) {
logger.error("删除失败", e)
false
}
}
}
- SolrWrapper :
import com.yzz.house.dto.OrderBy
import gw.commonkotlin.utils.gtZero
import gw.commonkotlin.utils.issEmpty
import gw.commonkotlin.utils.issNotEmpty
import org.apache.commons.lang3.StringUtils
import org.apache.solr.client.solrj.SolrQuery
import org.slf4j.LoggerFactory.getLogger
import java.math.BigDecimal
import java.util.*
import java.util.stream.Collectors
/**
* @author:anyu-csdn
* @date: 2020/11/19
* @desc: solr的查询拼接类,类似于Mybatis-plus的简单版本,基于Solrj对其的进一步封装。
*/
class SolrWrapper : AbstractSolrService() {
/**
* 目前封装支持的查询: and且 or或 range范围 sort排序 limit分页
*/
private val logger = getLogger(SolrWrapper::class.java)
// eq 存储的数据集
private var eqMap = IdentityHashMap<String, Any>()
// range范围查询存储的数据集
private var rgMap = IdentityHashMap<String, String>()
// or 存储的数据集
private var orMap = IdentityHashMap<String, Any>()
// order排序
private var orderMap = IdentityHashMap<String, SolrQuery.ORDER>()
// 返回指定字段的参数
private var selectSet = hashSetOf<String>()
// geo数据 查询字段|经度,纬度
private var solrGeo: SolrGeo? = null
private var pageIndex: Int = 0
private var pageSize: Int = 10
private var q: String = ""
/**
* 默认自定义查询q
*/
fun q(query: String): SolrWrapper {
q = query
return this
}
/**
* 默认自定义查询q
*/
fun q(boolean: Boolean, query: String): SolrWrapper {
if (boolean) {
q = query
}
return this
}
/**
* 匹配查询:equals
*/
fun eq(key: String, value: Any = "*"): SolrWrapper {
eqMap[key] = value
return this
}
/**
* or查询
*/
fun or(key: String, value: Any = "*"): SolrWrapper {
orMap[key] = value
return this
}
/**
* or查询: 或
*/
fun or(boolean: Boolean, key: String, value: Any = "*"): SolrWrapper {
if (boolean) {
orMap[key] = value
}
return this
}
fun orList(boolean: Boolean, key: String = "*", values: List<Any> = listOf("*")): SolrWrapper {
if (boolean) {
values.forEach {
orMap[key] = it
}
}
return this
}
/**
* 匹配查询:equals
*/
fun eq(boolean: Boolean, key: String, value: Any = "*"): SolrWrapper {
if (boolean) {
eqMap[key] = value
}
return this
}
/**
* 匹配查询: 传入列表
*/
fun eqList(boolean: Boolean, key: String = "*", values: List<Any> = listOf("*")): SolrWrapper {
if (boolean) {
val stringBuffer = StringBuffer()
values.forEach {
val value = if (StringUtils.isEmpty(it.toString())) "*" else it.toString()
if (stringBuffer.isEmpty()) stringBuffer.append(value) else stringBuffer.append(" OR $value")
}
if (StringUtils.isNotEmpty(stringBuffer.toString())) eqMap[key] = "($stringBuffer)"
}
return this
}
/**
* 分页查询: page
*/
fun pg(boolean: Boolean, pageIndex: Int, pageSize: Int): SolrWrapper {
if (boolean) {
this.pageIndex = pageIndex
this.pageSize = pageSize
}
return this
}
/**
* 分页查询: page
*/
fun pg(pageIndex: Int, pageSize: Int): SolrWrapper {
this.pageIndex = pageIndex
this.pageSize = pageSize
return this
}
/**
* 范围查询:range
*/
fun rg(boolean: Boolean = true, key: String, start: Any = "*", end: Any = "*"): SolrWrapper {
if (boolean) {
rgMap[key] = "$start TO $end"
}
return this
}
/**
* 范围查询:range
*/
fun rg(key: String, start: Any = "*", end: Any = "*"): SolrWrapper {
rgMap[key] = "[$start TO $end]"
return this
}
/**
* 范围查询:range
*/
fun rgList(boolean: Boolean, key: String, values: List<String> = listOf("* TO *")): SolrWrapper {
if (boolean) {
val stringBuffer = StringBuffer()
values.forEach {
val value = if (StringUtils.isEmpty(it)) "[* TO *]" else it
if (stringBuffer.isEmpty()) stringBuffer.append(value) else stringBuffer.append(" OR $value")
}
if (StringUtils.isNotEmpty(stringBuffer.toString())) rgMap[key] = "($stringBuffer)"
}
return this
}
/**
* 排序:排序顺排
*/
fun orderByAsc(boolean: Boolean, key: String): SolrWrapper {
if (boolean) {
orderMap[key] = SolrQuery.ORDER.asc
}
return this
}
/**
* 排序: 顺序排序
*/
fun orderByAsc(key: String): SolrWrapper {
orderMap[key] = SolrQuery.ORDER.asc
return this
}
/**
* 排序:倒叙排序
*/
fun orderByDesc(key: String): SolrWrapper {
orderMap[key] = SolrQuery.ORDER.desc
return this
}
/**
* 排序:倒叙排序
*/
fun orderByDesc(boolean: Boolean, key: String): SolrWrapper {
if (boolean) {
orderMap[key] = SolrQuery.ORDER.desc
}
return this
}
/**
* 排序: 根据对象排序
*/
fun orderByList(orderBy: List<OrderBy>): SolrWrapper {
orderBy.forEach {
if (StringUtils.isNotEmpty(it.field) && (StringUtils.equals(it.orderBy, "asc") || StringUtils.equals(it.orderBy, "desc"))) {
orderMap[it.field] = if (SolrQuery.ORDER.asc.name == it.orderBy) SolrQuery.ORDER.asc else SolrQuery.ORDER.desc
}
}
return this
}
/**
* 设置定位搜索
*/
fun geo(boolean: Boolean, key: String, latitude: BigDecimal, longtude: BigDecimal, range: Int): SolrWrapper {
if (boolean) {
// 当key不为空、经纬度大于0、范围大于0则加入查询条件
if (StringUtils.isNotEmpty(key) && latitude > BigDecimal.ZERO && longtude > BigDecimal.ZERO && range > 0) {
val sg = SolrGeo()
sg.fieldName = key
sg.latitude = latitude.toString()
sg.longtude = longtude.toString()
sg.range = range
solrGeo = sg
}
}
return this
}
/**
* 设置定位搜索
*/
fun geo(key: String, latitude: BigDecimal, longtude: BigDecimal, range: Int): SolrWrapper {
// 当key不为空、经纬度大于0、范围大于0则加入查询条件
if (StringUtils.isNotEmpty(key) && latitude > BigDecimal.ZERO && longtude > BigDecimal.ZERO && range > 0) {
val sg = SolrGeo()
sg.fieldName = key
sg.latitude = latitude.toString()
sg.longtude = longtude.toString()
sg.range = range
solrGeo = sg
}
return this
}
/**
* 获取排序集合
*/
private fun orderMap(): Map<String, SolrQuery.ORDER> {
return orderMap
}
/**
* 返回指定字段的参数
*/
fun select(vararg key: String): SolrWrapper {
key.filter { StringUtils.isNotEmpty(it) }.forEach { selectSet.add(it) }
return this
}
/**
* 返回指定字段的参数
*/
fun select(boolean: Boolean, list: List<String>): SolrWrapper {
if (boolean) {
selectSet.addAll(list.filter { StringUtils.isNotEmpty(it) })
}
return this
}
private fun rgMapSQL(): String {
val rgBuffer = StringBuffer()
//拼装其他条件语句
rgMap.forEach { (key, value) ->
run {
val range = "$key:$value"
if (rgBuffer.isEmpty()) {
rgBuffer.append(range)
} else {
rgBuffer.append(" AND $range")
}
}
}
return rgBuffer.toString()
}
private fun eqMapSQL(): String {
val eqBuffer = StringBuffer()
//拼装其他条件语句
eqMap.forEach { (key, value) ->
run {
if (eqBuffer.isEmpty()) {
eqBuffer.append("($key:$value)")
} else {
eqBuffer.append(" AND ($key:$value)")
}
}
}
return eqBuffer.toString()
}
private fun orMapSQL(): String {
val orBuffer = StringBuffer()
//拼装其他条件语句
orMap.forEach { (key, value) ->
run {
if (orBuffer.issEmpty()) {
orBuffer.append("$key:$value")
} else {
orBuffer.append(" OR $key:$value")
}
}
}
return orBuffer.toString()
}
private fun allSQL(): String {
// 封装sql
val mergeSolrSQL = mergeSolrSQL(eqMapSQL(), rgMapSQL(), orMapSQL())
return if (StringUtils.isEmpty(q)) {
// 如果封装查询为空,则默认查询全部
if (StringUtils.isEmpty(mergeSolrSQL)) "*:*" else mergeSolrSQL
} else {
// 如果封装查询不为空,则判断封装查询是否为空。
if (StringUtils.isEmpty(mergeSolrSQL)) q else "($mergeSolrSQL) AND ($q)"
}
}
fun buildSolrQuery(): SolrQuery {
val solrQuery = SolrQuery()
// 1. 拼接查询参数
val allSQL = this.allSQL()
solrQuery.set("q", allSQL)
// 2. 拼接经纬度参数
if (solrGeo.issNotEmpty()) {
solrQuery.set("d", solrGeo!!.range)
.set("pt", "${solrGeo!!.latitude},${solrGeo!!.longtude}")
.set("geodist()", "geodist() asc")
.set("sfield", "houseLocation")
.set("fq", "{!geofilt}")
.set("fl", "*,dist:geodist()")
}
// 3. 拼接分页参数
if (this.pageIndex.gtZero() && this.pageSize.gtZero()) {
solrQuery.start = (this.pageIndex * this.pageSize) - (this.pageSize)
solrQuery.rows = this.pageSize
}
// 4.拼接排序参数
if (orderMap.issNotEmpty()) {
this.orderMap().forEach { (key, value) -> solrQuery.setSort(key, value) }
}
// 确定返回值
if (selectSet.issNotEmpty()) {
solrQuery.set("fl", selectSet.stream().collect(Collectors.joining(",")))
}
logger.info("\r\nSolr查询语句为:\r\n$allSQL\r\n")
return solrQuery
}
}
- 其他附带引用类: AbstractSolrService:
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONArray
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils
import org.apache.commons.lang3.StringUtils
/**
* @author:anyu
* @date: 2020/11/19
*/
abstract class AbstractSolrService{
/**
* 将一个对象转为指定的另外一个对象
*/
fun <T> getBean(any: Any, clazz: Class<T>): T? {
return if (!ObjectUtils.isEmpty(any)) {
val solrResult = JSON.parseObject(JSON.toJSONString(any), clazz)
solrResult
} else {
null
}
}
/**
* 将一个集合转为指定的集合
*/
fun <T> getArrayBean(any: Any, clazz: Class<T>): List<T>? {
return if (!ObjectUtils.isEmpty(any)) {
val solrResult = JSONArray.parseArray(JSON.toJSONString(any), clazz)
solrResult
} else {
return null
}
}
/**
* 添加内容到List 自动排除为空的部分
*/
fun mergeSolrSQL(vararg objs: String?): String {
val mergeBuffer = StringBuffer()
for (obj in objs) {
if (StringUtils.isNotEmpty(obj)) {
if (mergeBuffer.isEmpty()) {
mergeBuffer.append("($obj) ")
} else {
mergeBuffer.append(" AND ($obj)")
}
}
}
return mergeBuffer.toString()
}
}
- 其他附带引用类: SolrGeo:
/**
* @author:anyu
* @date: 2020/11/19
* @desc: GPS封装对象
*/
class SolrGeo {
/**
* 要搜索定位的字段名
*/
var fieldName: String=""
/**
* 纬度
*/
var latitude: String=""
/**
* 经度
*/
var longtude: String=""
/**
* 查询公里范围,为方便,取整数
*/
var range: Int=0
}