以前只知道有lucene全文搜索这个开源软件,但一直没用过。这次一个项目正好用到,于是了解一下,发现网上介绍的文章大多比较旧,不适用,要么介绍比较浅。这里记录我从全不会到通的过程,以供借鉴。
迷茫期
首先打开lucene的官网,一堆英文,我的英文很一般,看了半天没有头绪,于是搜中文使用教程,先得有一个全面的了解。
看到的很多先介绍了一些名词:索引结构Document(文档)、分词器、Field域。虽然有图有介绍,还是有点懵。不过还是知道lucene是一个jar包,只要引到自己的项目中,通过调用相关类就可以使用了。
对于编程,看得再多,不如动手一试。建一个测试环境,用一下就会有很直观的认识。对于现在这项目使用的是node.js开发,但lucene是java,这是没法直接调用的。在网上搜了一下,node-lucenes是大拿封装好的,可以直接使用。但仔细一看,它只支持到node11,现在都一般都使用node12,node14也都出来了,而且此项目好多年没有更新了。不能建新项目使用过时的内容,以后维护会非常麻烦。肯定还有其它方法,也考虑过用node调用系统cmd,由系统cmd调lucene包,但总感觉不顺。如果能直接调用最好,不死心,再接着搜。
终于到找一篇文章说solr是lucene的封装,提供接口供其它程序使用,其中有http协议传输json格式内容。这下思路就清晰了,nodejs通过http协议调用solr,由solr完成lucene全文搜索工作。
于是到官网下载solr,发现首页原来就是和lucene并排放着,那么显眼,我竟然没留意,结果绕了一大圈,还是英文不行,能力限制了思考。
入门期
solr下载安装一路顺利,使用方法和tomcat类似。打开后台看了看,不知道干什么。
于是搜了一篇入门文章(solr7.4 安装与使用 - Tony_ding - 博客园),照着做了一遍,才明白了索引结构core、Document(文档)、分词器、Field域这些概念。把新知纳入现有知识体系中是最高效的掌握,我对数据库了解很多,一比对就掌握了大概。
- core相当于数据库中的表空间; 通过命令行创建core: $ solr create -c CORE_NAME
- Document相当于表;
- field相当于字段;
- 分词器定义了怎么提取关键字。
solr启动命令: $ solr start -p 8984
由于lucene是国外的,怎么提取中文关键字是个问题,按照文章方法使用了ikanalyzer,当尝试分词"博客园"后,对solr有了直观的认识。
进阶期
到这个阶段,中文介绍就不多了。在网上找到一文章 搜索引擎技术系列教材 (八)- solr - Solr 入门教程 讲到了设置字段、创建索引、分页查询,其它的就需要去看官网了,不过看完后对solr有了更进一步的认识。
目前有的疑问是solr具体和node怎么对接?node创建一条数据每次都需要调用solr吗?上传文件需要使用tika获得文本后再使用?
solr7.7.0搜索引擎使用(三)(添加文件索引) 提到solr可以对pdf,txt,doc等文件生成索引,solr直接引入了tika,不需要考虑tika的问题了。按此文章提示操作,建立一个文件索引。
- 步骤1.添加core,取名暂且为 coreFile 在bin下执行命令 ./solr create -c coreFile
- 步骤2.准备要搜索的文件
- 步骤3.添加搜索的数据源 注意,此时使用的class是solr.DataimportHandler
- 步骤4.添加数据源文件,注意更换 baseDir为你自己的文件路径
- 步骤5.添加字段索引
- 步骤6.添加中文分词
文章中没说清的地方:
- 将解压后的solr-8.5.1contribanalysis-extraslucene-libs下的lucene-analyzers-smartcn-8.5.1.jar放到Tomcat8webappssolrWEB-INFlib下。
- 在solrconfig.xml中加入DIH jar包依赖。 内容是:
<lib dir="${solr.install.dir:../../..}/dist/" regex="solr-dataimporthandler-.*.jar" />
<lib dir="${solr.install.dir:../../..}/contrib/extraction/lib/" regex=".*.jar"/>
extraction/lib目录下主要引用的是tika包,它可以把word等文档转成文字。我刚开始因为没有加这个目录,导入建索引一直没有成功。
测试:将本地的50个数据文件导入到solr并创建index
测试:查找文件名
补充:如果是txt文件需要保证内容是UTF-8编码,默认txt文件是的编码是GBK,上传之后最好进行转码。
建好测试,开始还想着怎么导文件怎么提取word文本,原来一切如此简单。现在要测试问题如下:
- 如果新增一个文件会不会自动增加索引?
答案 :不会,需要用到增量更新 - 搜索找到文件后,如何和原平台对接,如何找到数据库对应此文件备注等内容?
答案:不能直接对接文件,需要对接数据库 - 如何增加权限,每人只可搜索自己上传的文件,这需要怎么处理?
针对第一个问题解决方法有:
- DataImportHandler下使用ContentStreamDataSource数据源接收POST参数实现导入数据,可以实时,但请求时间会变长;
- delta-import实现增量导入,定时执行任务,不能做到实时;或使用apache-solr-dataimportscheduler;
- ExtractingRequestHandler方法,需要上传文件,但本项目中文件在同一服务器上,不需要上传。
不知道应该使用哪种方法好,最终看到 携程酒店订单Elasticsearch实战 (搜索引擎怎么选?携程酒店订单Elasticsearch实战 - 51CTO.COM)上面的分析:
实时扫描数据库
初看这是一种很低效的方案,但是在结合以下实际场景后,它却是一种简单、稳定、高效的方案:
1、零耦合。相关应用无需做任何改动,不会影响业务处理效率和稳定性。
2、批量写 Elastic Search。由于扫描出来的都是成批的数据,可以批量写入 Elastic Search,
避免 Elastic Search 由于过多单个请求,频繁刷新缓存。
3、存在大量毫秒级并发的写。扫描数据库时无返回数据意味着额外的数据库性能消耗,
我们的场景写的并发和量都非常大,所以这种额外消耗可以接受。
4、不删除数据。扫描数据库无法扫描出删除的记录,但是订单相关的记录都需要保留,
所以不存在删除数据的场景。
为了零耦合,了终决定使用第二种方法。第一种方案每次增删改都要调用会很麻烦,而且随着业务逻辑增加会更加明显。
个人感觉ElasticSearch在增量这块比solr做的好,大项目还是要用ElasticSearch,目前本项目不大,单机运行,使用solr应该足够了 。
高级期 :增量更新
原来content表中增加“LAST_INDEX_TIME”、“IS_DELETE”字段
增量更新配置data-config.xml文件:
<!--
transformer 格式转化:HTMLStripTransformer 索引中忽略HTML标签
query:查询所有未删除记录数据,主要用在full-import全量导入时候
deltaQuery:根据dataimporter.properties每次刷新的last_index_time,实现刷新从上次last_index_time至今的数据,从而增量索引主键ID查询处理,注意这个只能返回ID字段
deletedPkQuery:增量索引删除access=-1主键ID
deltaImportQuery:增量查询从上次刷新时间到现在数据且未删除的access>=0,进行增量更新发布索引文件
-->
<dataConfig>
<dataSource name="fileDataSource" type="BinFileDataSource"/>
<dataSource name="mysqlDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/weihuidb?characterEncoding=UTF-8" user="weihui" password="weihui" />
<document>
<entity name="t_file" pk="id" dataSource="mysqlDataSource"
query="select * from File where access>=0 and isDirectory=false"
deltaQuery="select id from File where access>=0 and isDirectory=false and createTime> '${dataimporter.last_index_time}'"
deletedPkQuery="select id from File where access=-1"
deltaImportQuery="select * from File where access>=0 and id = '${dih.delta.id}'"
>
<field column="id" name="id" />
<field column="name" name="name" />
<field column="ownerName" name="ownerName" />
<field column="path" name="path" />
<field column="taskName" name="taskName" />
<field column="department" name="department" />
<field column="file_id" name="file_id" />
<field column="access" name="access" />
<entity name="pdf" processor="TikaEntityProcessor" url="${t_file.path}" format="text" dataSource="fileDataSource">
<field column="text" name="text"/>
</entity>
</entity>
</document>
</dataConfig>
下载jar包 apache-solr-dataimportscheduler.jar到 solr 项目的WEB-INFlib 目录下
修改web.xml文件配置监听,在servlet节点前增加:
<listener>
<listener-class>
org.apache.solr.handler.dataimport.scheduler.ApplicationListener
</listener-class>
</listener>
从solr-data-importscheduler.jar
中提取出dataimport.properties
放入在server/solr/conf目录下。(conf 目录需要自己新建),并根据自己的需要进行修改;比如我的配置如下:
#Tue Jul 21 12:10:50 CEST 2010
metadataObject.last_index_time=2010-09-20 11:12:47
last_index_time=2010-09-20 11:12:47
#################################################
# #
# dataimport scheduler properties #
# #
#################################################
# to sync or not to sync
# 1 - active; anything else - inactive
syncEnabled=1
# which cores to schedule
# in a multi-core environment you can decide which cores you want syncronized
# leave empty or comment it out if using single-core deployment
syncCores=weihui
# solr server name or IP address
# [defaults to localhost if empty]
server=localhost
# solr server port
# [defaults to 80 if empty]
port=9039
# application name/context
# [defaults to current ServletContextListener's context (app) name]
webapp=solr
# URL params [mandatory]
# remainder of URL
params=/dataimport?command=delta-import&clean=false&commit=true
# schedule interval
# number of minutes between two runs
# [defaults to 30 if empty]
interval=10
# schedule interval
# number of minutes between two runs
# [defaults to 30 if empty]
# 自动增量更新时间间隔,单位为 min,默认为 30 min
interval=5
# 重做索引时间间隔,单位 min,默认 7200,即 5 天
reBuildIndexInterval = 7200
# 重做索引的参数
reBuildIndexParams=/dataimport?command=full-import&clean=true&commit=true
# 重做索引时间间隔的开始时间
reBuildIndexBeginTime=1:30:00
到此,我们就可以实现数据库自动增量导入了;
补充:使用这方法更新时一直报错,所以先使用系统的定时任务更新,定时调用下面连接即可:
http://127.0.0.1:8983/solr/new_core/dataimport?command=delta-import&clean=false&commit=true
注:127.0.0.1:8983是solr服务地址,new_core是创建core的名称。
高级期:高级查询/权限/过滤
每人只可搜索自己上传的文件,这个问题怎么处理呢?
目前可以想到的就是在select的参数中加以限定。
1. 查询所有 http:// localhost:8080/solr/pri mary/select?q=*:*
多字段或关系AND
TITLE:("中国人" AND "美国人" AND "英国人")
多字段不包含的关系 NOT
这个语法就是我吃苦的地方,之前已经当多值or那样去查,结果不是,要写成
TITLE:(* NOT "上网费用高" NOT "宽带收费不合理" )
查询一个范围 BETWEEN
NUM:[-90 TO 360 ] OR CREATED_AT:[" + date1 + " TO " + date2 + "]
2. 限定返回字段
http://localhost:8080/solr/primary/select?q=*:*&fl=productId
表示:查询所有记录,只返回productId字段
3. 分页
http://localhost:8080/solr/primary/select?q=*:*&fl=productId&rows=6&start=0
表示:查询前六条记录,只返回productId字段
4. 增加限定条件
http://localhost:8080/solr/primary/select?q=*:*&fl=productId&rows=6&start=0&fq=category:2002&fq=namespace:d&fl=productId+category&fq=en_US_city_i:1101
表示:查询category=2002、en_US_city_i=110
以及namespace=d的前六条记录,只返回productId和category字段
5. 添加排序
http://localhost:8080/solr/primary/select?q=*:*&fl=productId&rows=6&start=0&fq=category:2002&fq=namespace:d&sort=category_2002_sort_i+asc
表示:查询category=2002以及namespace=d并按category_2002_sort_i
升序排序的前六条记录,只返回productId字段
6. facet查询
现实分组统计结果
http://localhost:8080/solr/primary/select?q=*:*&fl=productId&fq=category:2002&facet=true&facet.field=en_US_county_i&facet.field=en_US_hotelType_s&facet.field=price_p&facet.field=heatRange_i
http://localhost:8080/solr/primary/select?q=*:*&fl=productId&fq=category:2002&facet=true&facet.field=en_US_county_i&facet.field=en_US_hotelType_s&facet.field=price_p&facet.field=heatRange_i&facet.query=price_p:[300.00000+TO+*]
这里就要使用到facet查询
上面是比较直接的Faceted Search例子,品牌、产品特征、卖家,均是 Facet 。而Apple、Lenovo等品牌,就是 Facet values 或者说 Constraints ,而Facet values所带的统计值就是 Facet count/Constraint count 。
建议将访问角色(是,其复数)存储为文档元数据.这里所需的字段access_roles是一个多面值的多值字符串字段.
Doc1: access_roles:[user_jane, manager_vienna] // Jane and the Vienna branch manager may see it
Doc2: access_roles:[user_john, manager_vienna, special_team] // Jane, the Vienna branch manager and a member of special team may see it
拥有文档的用户是该文档的默认访问角色.
要更改文档的访问角色,请编辑access_roles.
当Jane搜索时,她所属的访问角色将成为查询的一部分. Solr将仅检索与用户访问角色匹配的文档.
当维也纳办事处(manager_vienna)的经理Jane(user_jane)搜索时,她的搜索结果如下:
q=mainquery
&fq=access_roles:user_jane
&fq=access_roles:manager_vienna
&facet=on
&facet.field=access_roles
它在access_roles中获取包含user_jane OR manager_vienna的所有文档; Doc1和Doc2.
当Bob,(user_bob),特殊团队(specia_team)的成员搜索时,
q=mainquery
&fq=access_roles:user_bob
&fq=access_roles:special_team
&facet=on
&facet.field=access_roles
它为他取得了Doc2.
高级期:优化中文选词
solr自带的中文选词不好用,有些词如地名选不上,而且不能加自定义词,所以改为用ikanalyzer选词。
下载地址:https://pan.baidu.com/s/1Dbma2vAepBSsCag_EztTTw
下载解压后 把两个jar文件复制到solr-8.5.1serversolr-webappwebappWEB-INFlib中
在solr-8.5.1serversolr-webappwebappWEB-INFclasses目录下新建一个classes目录,把下面三个文件复制进去
managed-schema.xml 添加如下代码:
<fieldType name="text_cn" class="solr.TextField">
<analyzer type="index" useSmart="false"
class="org.wltea.analyzer.lucene.IKAnalyzer" />
<analyzer type="query" useSmart="true"
class="org.wltea.analyzer.lucene.IKAnalyzer" />
</fieldType>
高级期:windows环境下部署
因项目需要,只能在windows环境下部署,·采用solr 做成windows服务 文章中说的方法,记录如下。
- 下载NSSM这个工具,地址是http://www.nssm.cc/download,复制NSSM.exe到solr的bin目录下,按shift键右键bin文件夹,选择菜单“从此处打开命令窗口”启动cmd命令窗口;
- 输入 nssm install solr
- 好solr的启动文件 solr.cmd,启动参数Arguments 里面填写 start -f -p 8983 (-f是必须填写的)
- 要删除该服务可以用windows自带的命令,sc delete <服务名> ,注意要用超级管理员启动cmd
参考:
- 增量导入数据——DeltaImport
- 通过配置apache solr的last_index_time实现dataimport导入功能支持增量更新delta-import索引功能
- solr 的全量更新与增量更新_数据库_weixin_30481087的博客-CSDN博客
- Solr的学习使用之(七)Solr高级查询facet、facet.pivot简介 - OnTheRoad_Lee
- http://www.voidcn.com/article/p-kpkgpxdg-btr.html
附:ContentStreamDataSource使用方法
<dataConfig>
<dataSource name="streamsrc" type="ContentStreamDataSource" loggerLevel="TRACE" />
<document>
<entity
stream="true"
name="streamxml"
dataSource="streamsrc1"
processor="XPathEntityProcessor"
rootEntity="true"
forEach="/books/book"
transformer="TemplateTransformer" >
<field column="load" template="some static payload"/>
<field column="b_title" xpath="/books/book/name"/>
</entity>
</document>
</dataConfig>
curl -X POST
http://xxx.yyy.zzz/xmlimport
-H 'content-type: multipart/form-data; boundary=---- WebKitFormBoundary7MA4YWxkTrZu0gW'
-F 'stream.body=<?xml version="1.0" encoding="utf-8"?>
<books>
<book>
<name>NAME1</name>
</book>
<book>
<name>NAME2</name>
</book>
</books>'
-F commit=true
-F debug=true
-F clean=false
-F command=full-import