1 Solr检索原理
1.1 引言
场景:小时候我们都使用过新华字典,老师叫你翻开第38页,找到“学习”所在的位置,此时你会怎么查呢?毫无疑问,你的眼睛会从38页的第一个字开始从头至尾地扫描,直到找到“学习”二字为止。这种搜索方法叫做顺序扫描法
。对于少量的数据,使用顺序扫描是够用的。但是老师叫你查出学习的“学”字在哪一页时,你要是从第一页的第一个字逐个的扫描下去,那就是问题了。此时就需要用到索引。索引记录了“学”字在哪一页,只需在索引中找到“学”字,然后找到对应的页码,答案就出来了。因为在索引中查找“学”字是非常快的,因为你知道它的偏旁,因此也就可迅速定位到这个字。
那么新华字典的目录(索引表)是怎么编写而成的呢?首先对于新华字典这本书来说,除去目录后,这本书就是一堆没有结构的数据集。但是每个字都会对应到一个页码,比如“学”字就在第38页,“习”字在第90页。于是他们就从中提取这些信息,构造成一个有结构的数据。
类似数据库中的表结构:
word | page_no |
---|---|
学 | 38 |
习 | 90 |
... | ... |
这样就形成了一个完整的目录(索引库),查找的时候就非常方便了。对于全文检索也是类似的原理,它可以归结为两个过程:
- 索引创建(Indexing)
- 搜索索引(Search)
1.2 索引
Solr/Lucene
采用的是一种反向索引,所谓反向索引
:就是从关键字到文档的映射过程,保存这种映射这种信息的索引称为反向索引
左边保存的是字符串序列
右边是字符串的文档(Document)编号链表,称为倒排表(Posting List)
字段串列表和文档编号链表两者构成了一个字典。现在想搜索”lucene”,那么索引直接告诉我们,包含有”lucene”的文档有:2,3,10,35,92,而无需在整个文档库中逐个查找。如果是想搜既包含”lucene”又包含”solr”的文档,那么与之对应的两个倒排表去交集即可获得:3、10、35、92。
1.3 索引创建
假设有如下两个原始文档:
文档一:Students should be allowed to go out with their friends, but not allowed to drink beer.
文档二:My friend Jerry went to school to see his students but found them drunk which is not allowed.
创建过程大概分为如下步骤:
image.png
1.3.1 把原始文档交给分词组件(Tokenizer)
分词组件(Tokenizer
)会做以下几件事情(这个过程称为:Tokenize
),处理得到的结果是词汇单元(Token)
- 将文档分成一个一个单独的单词
- 去除标点符号
- 去除停词(stop word)
所谓停词(Stop word
)就是一种语言中没有具体含义,因而大多数情况下不会作为搜索的关键词,这样一来创建索引时能减少索引的大小。英语中停词(Stop word)如:”the”、”a”、”this”,中文有:”的,得”等。不同语种的分词组件(Tokenizer),都有自己的停词(stop word)集合。经过分词(Tokenizer)后得到的结果称为词汇单元(Token)。
上例子中,便得到以下词汇单元(Token):
"Students","allowed","go","their","friends","allowed","drink","beer","My","friend","Jerry","went","school","see","his","students","found","them","drunk","allowed"
1.3.2 词汇单元(Token)传给语言处理组件(Linguistic Processor)
语言处理组件(linguistic processor
)主要是对得到的词元(Token
)做一些语言相关的处理。对于英语,语言处理组件(Linguistic Processor
)一般做以下几点:
- 变为小写(
Lowercase
)。 - 将单词缩减为词根形式,如”cars”到”car”等。这种操作称为:stemming。
- 将单词转变为词根形式,如”drove”到”drive”等。这种操作称为:lemmatization。
语言处理组件(linguistic processor)处理得到的结果称为词(Term),例子中经过语言处理后得到的词(Term)如下:
"student","allow","go","their","friend","allow","drink","beer","my","friend","jerry","go","school","see","his","student","find","them","drink","allow"。
经过语言处理后,搜索drive时drove也能被搜索出来。
Stemming
和 lemmatization
的异同:
- 相同之处:
Stemming
和lemmatization
都要使词汇成为词根形式。 - 两者的方式不同:
Stemming
采用的是”缩减”的方式:”cars”到”car”,”driving”到”drive”。
Lemmatization
采用的是”转变”的方式:”drove”到”drove”,”driving”到”drive”。 - 两者的算法不同:
Stemming
主要是采取某种固定的算法来做这种缩减,如去除”s”,去除”ing”加”e”,将”ational”变为”ate”,将”tional”变为”tion”。
Lemmatization
主要是采用事先约定的格式保存某种字典中。比如字典中有”driving”到”drive”,”drove”到”drive”,”am, is, are”到”be”的映射,做转变时,按照字典中约定的方式转换就可以了。 -
Stemming
和lemmatization
不是互斥关系,是有交集的,有的词利用这两种方式都能达到相同的转换。
1.3.3 得到的词(Term)传递给索引组件(Indexer)
利用得到的词(Term)创建一个字典
Term | Document ID |
---|---|
student | 1 |
allow | 1 |
go | 1 |
their | 1 |
friend | 1 |
allow | 1 |
drink | 1 |
beer | 1 |
my | 2 |
friend | 2 |
jerry | 2 |
go | 2 |
school | 2 |
see | 2 |
his | 2 |
student | 2 |
find | 2 |
them | 2 |
drink | 2 |
allow | 2 |
对字典按字母顺序排序:
Term | Document ID |
---|---|
allow | 1 |
allow | 1 |
allow | 2 |
beer | 1 |
drink | 1 |
drink | 2 |
find | 2 |
friend | 1 |
friend | 2 |
go | 1 |
go | 2 |
his | 2 |
jerry | 2 |
my | 2 |
school | 2 |
see | 2 |
student | 1 |
student | 2 |
their | 1 |
them | 2 |
合并相同的词(Term)成为文档倒排(Posting List)链表
image.png
-
Document Frequency
:文档频次,表示多少文档出现过此词(Term) -
Frequency
:词频,表示某个文档中该词(Term)出现过几次
对词(Term) “allow”来讲,总共有两篇文档包含此词(Term),词(Term)后面的文档链表总共有两个,第一个表示包含”allow”的第一篇文档,即1号文档,此文档中,”allow”出现了2次,第二个表示包含”allow”的第二个文档,是2号文档,此文档中,”allow”出现了1次
至此索引创建完成,搜索”drive”时,”driving”,”drove”,”driven”也能够被搜到。因为在索引中,”driving”,”drove”,”driven”都会经过语言处理而变成”drive”,在搜索时,如果输入”driving”,输入的查询语句同样经过分词组件和语言处理组件处理的步骤,变为查询”drive”,从而可以搜索到想要的文档。
1.4 搜索步骤
搜索”microsoft job”,用户的目的是希望在微软找一份工作,如果搜出来的结果是:”Microsoft does a good job at software industry…”,这就与用户的期望偏离太远了。如何进行合理有效的搜索,搜索出用户最想要得结果呢?搜索主要有如下步骤:
1.4.1 对查询内容进行词法分析、语法分析、语言处理
-
词法分析:区分查询内容中单词和关键字,比如:english and janpan,”and”就是关键字,”english”和”janpan”是普通单词。
-
根据查询语法的语法规则形成一棵树
image.png -
语言处理,和创建索引时处理方式是一样的。比如:leaned–>lean,driven–>drive
1.4.2 搜索索引
搜索索引,得到符合语法树的文档集合
1.4.3 根据查询语句与文档的相关性,对结果进行排序
我们把查询语句也看作是一个文档,对文档与文档之间的相关性(relevance
)进行打分(scoring
),分数高比较越相关,排名就越靠前。当然还可以人工影响打分,比如百度搜索,就不一定完全按照相关性来排名的。
如何评判文档之间的相关性?一个文档由多个(或者一个)词(Term
)组成,比如:”solr”, “toturial”,不同的词可能重要性不一样,比如solr就比toturial重要,如果一个文档出现了10次toturial,但只出现了一次solr,而另一文档solr出现了4次,toturial出现一次,那么后者很有可能就是我们想要的搜的结果。这就引申出权重(Term weight
)的概念。
权重
表示该词在文档中的重要程度,越重要的词当然权重越高,因此在计算文档相关性时影响力就更大。通过词之间的权重得到文档相关性的过程叫做空间向量模型算法(Vector Space Model
)
影响一个词在文档中的重要性主要有两个方面:
- Term Frequencey(tf),Term在此文档中出现的频率,tf越大表示越重要
- Document Frequency(df),表示有多少文档中出现过这个Trem,df越大表示越不重要
权重的公式:
image.png
空间向量模型
文档中词的权重看作一个向量
Document = {term1, term2, …… ,term N}
Document Vector = {weight1, weight2, …… ,weight N}
把欲要查询的语句看作一个简单的文档,也用向量表示:
Query = {term1, term 2, …… , term N}
Query Vector = {weight1, weight2, …… , weight N}
把搜索出的文档向量及查询向量放入N维度的空间中,每个词表示一维:
image.png
夹角越小,表示越相似,相关性越大
2 整合MySQL
2.1 MySQL
2.1.1 solrconfig.xml
拷贝mysql-connector-java-5.1.25-bin.jar
到server\solr-webapp\webapp\WEB-INF\lib
目录下面
假如核心实例是test_core
,配置server\solr\test_core\conf\solrconfig.xml
在里边加入如下内容,放置的位置你可以放到其他requestHandler
旁边
<requestHandler name="/dataimport"
class="solr.DataImportHandler">
<lst name="defaults">
<str name="config">data-config.xml</str>
</lst>
</requestHandler>
2.1.2 data-config.xml
创建server\solr\test_core\conf\data-config.xml
,指定MySQL
数据库地址,用户名、密码以及建立索引的数据表
<?xml version="1.0" encoding="UTF-8" ?>
<dataConfig>
<dataSource type="JdbcDataSource"
driver="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/django_blog"
user="root"
password=""/>
<document name="blog">
<entity name="blog_blog" pk="id"
query="select id,title,content from blog_blog"
deltaImportQuery="select id,title,content from blog_blog where ID='${dataimporter.delta.id}'"
deltaQuery="select id from blog_blog where add_time > '${dataimporter.last_index_time}'"
deletedPkQuery="select id from blog_blog where id=0">
<!-- 数据库字段映射solr字段 也可以不写,写出来是为了结构清晰,数据库字段和core的属性一一对应-->
<field column="id" name="id" />
<field column="title" name="title" />
<field column="content" name="content"/>
</entity>
</document>
</dataConfig>
-
query
: 用于初次导入到索引的sql
语句。
考虑到数据表中的数据量非常大,比如千万级,不可能一次索引完,因此需要分批次完成,那么查询语句query
要设置两个参数:${dataimporter.request.length}
和${dataimporter.request.offset}
即 :query=”select id,title,content from blog_blog limit ${dataimporter.request.length} offset ${dataimporter.request.offset}”
请求:http://localhost:8983/solr/collection2/dataimport?command=full-import&commit=true&clean=false&offset=0&length=10000
-
deltaImportQuery
: 根据ID
取得需要进入的索引的单条数据。 -
deltaQuery
: 用于增量索引的sql语句,用于取得需要增量索引的ID。 -
deletedPkQuery
: 用于取出需要从索引中删除文档的的ID
2.1.3 managed-schema
在solr 6.6
之前是schema.xml
文件,之后则是managed-schema
<!-- mysql -->
<field name="id" type="string" indexed="true" stored="true" required="true" />
<field name="title" type="text_cn" indexed="true" stored="true" termVectors="true" termPositions="true" termOffsets="true"/>
<field name="content" type="text_cn" indexed="true" stored="true" termVectors="true" termPositions="true" termOffsets="true"/>
<!-- mysql -->
网友评论