ElasticSearch 倒排索引、分词
- - 行业应用 - ITeye博客es使用称为倒排索引的结构达到快速全文搜索的目的. 一个倒排索引包含一系列不同的单词,这些单词出现在任何一个文档,. 对于每个单词,对应着所有它出现的文档. 比如说,我们有2个文档,每个文档有一个conteng字段. 我们首先对每个字段进行分词,我们称之为terms或者tokens,创建了一些列有序列表,.
es使用称为倒排索引的结构达到快速全文搜索的目的。
一个倒排索引包含一系列不同的单词,这些单词出现在任何一个文档,
对于每个单词,对应着所有它出现的文档。
比如说,我们有2个文档,每个文档有一个conteng字段。
内容如下:
“ The quick brown fox jumped over the lazy dog”
“ Quick brown foxes leap over lazy dogs in summer”
为了创建倒排索引,
我们首先对每个字段进行分词,我们称之为terms或者tokens,创建了一些列有序列表,
然后列举了每个单词所出现的文档,结果如下:
Term Doc_1 Doc_2
-------------------------
Quick | | X
The | X |
brown | X | X
dog | X |
dogs | | X
fox | X |
foxes | | X
in | | X
jumped | X |
lazy | X | X
leap | | X
over | X | X
quick | X |
summer | | X
the | X |
------------------------
现在,如果我们想搜索
"quick brown"
,我们只需要找到每个单词出现的文档。
Term Doc_1 Doc_2
-------------------------
brown | X | X
quick | X |
------------------------
Total | 2 | 1
两个文档都匹配,但是第一个文档有更高的匹配度,
如果我们采用一个简单的相似算法,我们可以说,第一个文档比第2个文档有更高的匹配度。
也更相关。
但是,仍然有一些问题。
"Quick"
和
"quick"
看起来是不同的单词,但是用户通常认为是一样的。
"fox"
和
"foxes"
更相似,还有
"dog"
和
"dogs"
,具有共同的词根。
"jumped"
和
"leap"
,尽管不具备相同的词根,意思上是一样的。它们是同义词。
对于以上的索引,一个搜索
"+Quick + fox"
不可能匹配任何文档。
单词
"Quick"
和单词
"fox"
必须在同一个文档里以满足查询要求,
但是第一个文档包括
"quick fox"
而第2个文档包含
"Quick foxes"
.
我们的用户有理由希望两个文档都匹配,我们可以做的更好。
如果我们把单词归一化到标准格式,我们就可以达到上面的目标。
这种情况下,虽然单词不是完全一致,但是也足够相似保证相关性。比如:
"Quick"
可以小写为
"quick"
.
"foxes"
可以提取词根成为
"fox"
.
类似的
"dogs"
可以成为
"dog"
.
"jumped"
和
"leap"
是同义词,可以索引为一个单词
"jump"
.
那么,现在的索引就是:
Term Doc_1 Doc_2
-------------------------
brown | X | X
dog | X | X
fox | X | X
in | | X
jump | X | X
lazy | X | X
over | X | X
quick | X | X
summer | | X
the | X | X
------------------------
我们的搜索
"+Quick +fox"
仍然失败,因为我们不再有Quick在索引里,
尽管如此,如果我们采用同样的归一化规则,
我们可以用在查询字符串上,它就变为
"+quick +fox"
,
这样就可以匹配到文档。
这一点非常重要,你只能找到那些在你的索引里出现的单词,所以,索引过的文本和查询字符串都需要遵循同一种归一化规则。
分词和归一化叫做分析http://my.oschina.net/qiangzigege/blog/265360
什么叫分析过程?
1
)将文本块分词,以倒排索引的方式
2
)归一化到标准形式来提高可搜索性。
这个工作的执行者叫做分析器,一个分析器包含以下
3
个功能:
1
)字母过滤
首先,字符串依次通过任何一个字符过滤器,
过滤器的工作是清洗字符串,也就是说先清洗再分词。
一个字符过滤器可以去除HTML字符,也可以转换
"&"
变为
"and"
.
2
)分词
下一步,字符串被分词为很多个单词,一个简单的分词器也许依靠空格或者标点符号来分词。
3
)单词过滤
最后,每个单词传给单词过滤器,它们可以将单词小写或者删除单词比如a,and,the,etc.
或者增加单词比如jump和leap.
es提供了很多字符串过滤器,分词器和单词过滤器。
这些单元可以组合起来使用,后面再说。
内置的分析器
尽管如此,es也包含了很多内置的分词器,你可以直接使用。
我们列举了最重要的一些,演示区别。
所用的文本基于:
"Set the shape to semi-transparent by calling set_trans(5)"
标准分析器
标准分析器是es默认的分析器,如果文本可能是各种语言,总的来说选择标准分析器是没错的。
它把文本分成单词(由Unicode决定),移除大部分标点符号,
最终,将所有单词小写化,这样,结果如下:
set
, the, shape, to, semi, transparent, by, calling, set_trans,
5
PS:先清洗,再分词,再归一化。
简单analyzer
这个分析器将文本分词的规则是每个字符是不是一个letter,
然后将所有单词小写化,结果如下:
set
, the, shape, to, semi, transparent, by, calling,
set
, trans
空格analyzer
依靠空格来分词,不将单词小写化,
结果如下:
Set, the, shape, to, semi-transparent, by, calling, set_trans(
5
)
语言分析器
特定语言分析器是可用的,可以处理特殊字符。
比如,英语分析器附带很多英语过滤单词集合,这些集合包括没啥意义的单词。
那分析器就会去掉这些单词,可以对英语单词进行提取词干,
这是因为英语分析器知道英语的语法规则。
结果如下:
set
, shape, semi, transpar, call, set_tran,
5
注意:
"transparent"
,
"calling"
, and
"set_trans"
已经归一化到词根形式。
什么时候用分析器
当我们索引一个文档,它的整个文档被分析成单词,单词用来创建倒排索引。
尽管如下,当我们搜索全文字段,我们也需要将查询字符串进行同样的分析过程。
实际上也就是说,你之前如何对全文做索引的,这个时候也要对查询字符串做同样的归一化处理。这样才可以保证可以查出来数据。
后面说全文搜索,
当你查询一个全文字段,查询将对查询字符串应用同样的分析器来产生一系列分词后的单词。
当你查询一个具体的字段,查询就不会对查询字符串进行分析,仅仅搜索具体的值。
现在你就理解了之前的:
date字段包含了一个具体的值:一个单词
"2014-09-15"
.
_all字段是一个全文字段,所以分析器已经把日期分成
3
个单词:
"2014"
,
"09"
and
"15"
.
当我们在_all字段里搜索
2014
,有
12
个结果,因为都包含
2014
这个单词。
GET /_search?q=
2014
#
12
results
当我们在_all字段里搜索
2014
-
09
-
15
, 首先会把查询字符串分词为
"2014"
,
"09"
and
"15"
.
仍然包含了
12
个tweets,因为都包含
2014
.
GET /_search?q=
2014
-
09
-
15
#
12
results !
当我们查询date字段,值为
2014
-
09
-
15
, 搜索会寻找具体的日期,结果只有
1
个:
GET /_search?q=date:
2014
-
09
-
15
#
1
result
当我们查询date字段,值为
2014
,就找不到文档。
GET /_search?q=date:
2014
#
0
results !
测试分析器
如果你是一个新手,有时会很难理解分词的具体原理和存储索引的过程。
(不看源码你永远不可能知道,just read the fuc*ing source code please!!!)
为了更好的理解怎样运行的,你可以使用分析API来看文本如何分析的。
在查询字符串参数里指定你想用哪个分析器,body里指定分析的文本。
GET /_analyze?analyzer=standard
Text to analyze
结果如下:
{
"tokens"
: [
{
"token"
:
"text"
,
"start_offset"
:
0
,
"end_offset"
:
4
,
"type"
:
"<ALPHANUM>"
,
"position"
:
1
},
{
"token"
:
"to"
,
"start_offset"
:
5
,
"end_offset"
:
7
,
"type"
:
"<ALPHANUM>"
,
"position"
:
2
},
{
"token"
:
"analyze"
,
"start_offset"
:
8
,
"end_offset"
:
15
,
"type"
:
"<ALPHANUM>"
,
"position"
:
3
}
]
}
这些单词就是真实的被存储在索引里的单词。
position表明单词出现的顺序,
start_offset 和 end_offset 表明 字符在原始文本里的位置。
分析API对于理解es的索引很有用。
指定分析器
当es发现有需要索引的
String
字段,自动认为是全文字符串字段,用标准分析器来分析。
有的时候你可能不想要这个分析器,纳尼?
也许你想采用一个不同的分析器,因为你觉得它更合适。
也许你还希望一个string字段就是一个字段,不需要认为是全文字段,比如说字符串类型的userid和内部身份。
索引里的每个文档有一个type,
每一个type有它自己的映射模式,一个映射指定了type里的字段,每个字段的数据类型,并且字段如何被es处理。
一个映射也被用来配置元数据。
我们会后续详细讨论映射。
核心简单字段类型
es支持下列简单的字段类型:
String: string
Whole number: byte, short, integer, long
Floating point: float,
double
Boolean: boolean
Date: date
当你索引一个文档,而这个文档包含了一个新的字段,es会动态映射此字段,规则如下:
JSON type: Field type:
Boolean: true or false
"boolean"
Whole number:
123
"long"
Floating point:
123.45
"double"
String, valid date:
"2014-09-15"
"date"
String:
"foo bar"
"string"
这也就意味着,如果你索引 “
123
”,就映射成字符串,而不是long.
尽管如此,如果这个字段已经映射成long,es将尝试按此转换,失败则抛出异常。
查看映射
使用/_mapping来查看已有的映射关系。
比如查看index:gb type:tweet的映射
GET /gb/_mapping/tweet
结果:
{
"gb"
: {
"mappings"
: {
"tweet"
: {
"properties"
: {
"date"
: {
"type"
:
"date"
,
"format"
:
"dateOptionalTime"
},
"name"
: {
"type"
:
"string"
},
"tweet"
: {
"type"
:
"string"
},
"user_id"
: {
"type"
:
"long"
}
}
}
}
}
}
定制字段的映射
字段最重要的属性就是type,
{
"number_of_clicks"
:
{
"type"
:
"integer"
}
}
string类型的字段,默认,认为是全文搜索,
也就是说,值会传递给分析器,搜索之前也会将查询字符串进行全文分词再搜索。
字符串字段最重要的
2
个映射关系是index和analyser.
index
index属性决定字符串如何被索引,有
3
个值:
1
analyzed
先分词,再索引,全文搜索
2
not_analyzed
索引这个字段,这样可以被搜索,但是直接索引,不分词。
3
no
不要索引这个字段,这个字段也不会被搜出来。
string字段默认是analyzed.
下面阐述了不分词的做法
{
"tag"
: {
"type"
:
"string"
,
"index"
:
"not_analyzed"
}
}
其它简单的字段类型-long,
double
, date etc ,
也可以接收index参数,不过只可以取值no ,not_analyzed.
analyzer
对于需要分词的string字段,analyzer属性决定使用哪个分词器,默认,使用standard分词器。
但是你可以设置为一个内置的分词器,比如whitespace,simple,english.
{
"tweet"
: {
"type"
:
"string"
,
"analyzer"
:
"english"
}
}
更新映射
你可以在创建索引时指定映射,另外,你可以使用/_mapping来修改映射或者增加映射。
如果一个字段已经在映射里存在,这也许意味着这个字段的数据已经被索引了,如果你想这个字段的映射,
已经索引过的数据就会出错。
我们可以通过增加一个新的字段来修改映射,但是我们不能修改已经存在的字段从分词到不分词。
删除gb索引
DELETE /gb
然后创建一个索引,指定tweet字段使用english分词器。
PUT /gb
{
"mappings"
: {
"tweet"
: {
"properties"
: {
"tweet"
: {
"type"
:
"string"
,
"analyzer"
:
"english"
},
"date"
: {
"type"
:
"date"
},
"name"
: {
"type"
:
"string"
},
"user_id"
: {
"type"
:
"long"
}
}
}
}
}
这就创建了一个带映射关系的索引。
下面修改映射,增加一个新的不分词的字段tag到tweet映射中。
PUT /gb/_mapping/tweet
{
"properties"
: {
"tag"
: {
"type"
:
"string"
,
"index"
:
"not_analyzed"
}
}
}
测试映射
可以使用分词API来测试映射,
GET /gb/_analyze?field=tweet
Black-cats
GET /gb/_analyze?field=tag
Black-cats
前者
2
个分词,后者一个分词。
除了简单的数据类型,JSON还有null值,数组和对象,es都支持。
多值字段:
有可能我们想我们的标签字段包含好几个标签,可以用数组:
{ "tag": [ "search", "nosql" ]}
这没有什么特别的,任何字段可以包含0,1或者多个值,这跟一个全文字段产生多个分词道理是一样的。
这意味着,一个数组的所有值必须是同样的数据类型,你不能混杂两种数据类型,
如果你通过索引一个数组而创建了一个新的字段,es将使用第一个值的数据类型来决定整个字段的数据类型。
数组的元素没有顺序,你不能说第一个元素和最后一个元素,就是一个集合。
空字段
数组,可以为空,事实上,Lucene是没有办法存储空值的,所以,一个没有值的字段被认为是一个空的字段。
以下四种字段可以认为是空的,不会被索引:
"empty_string": "",
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
多层对象
最后一个JSON数据类型是对象object,比如哈希,字典和数组。
数据对象嵌套是常见的,比如:
{
"tweet": "Elasticsearch is very flexible",
"user": {
"id": "@johnsmith",
"gender": "male",
"age": 26,
"name": {
"full": "John Smith",
"first": "John",
"last": "Smith"
}
}
}
内部对象的映射
es会探测到新的对象字段并且映射为object类型,
映射如下:
{
"gb": {
"tweet": {
"properties": {
"tweet": { "type": "string" },
"user": {
"type": "object",
"properties": {
"id": { "type": "string" },
"gender": { "type": "string" },
"age": { "type": "long" },
"name": {
"type": "object",
"properties": {
"full": { "type": "string" },
"first": { "type": "string" },
"last": { "type": "string" }
}
}
}
}
}
}
}
}
内部对象如何被索引?
Lucene不知道内部对象,一个Lucene文档包含一个平级的k/v结构。
为了让es索引内部对象,我们的文档会转化如下:
{
"tweet": [elasticsearch, flexible, very],
"user.id": [@johnsmith],
"user.gender": [male],
"user.age": [26],
"user.name.full": [john, smith],
"user.name.first": [john],
"user.name.last": [smith]
}
Lucene仅仅索引简单的值,不是复杂的结构。
内部对象数组
最后,思考下,一个内部对象数组如何被索引,
比如说如下:
{
"followers": [
{ "age": 35, "name": "Mary White"},
{ "age": 26, "name": "Alex Jones"},
{ "age": 19, "name": "Lisa Smith"}
]
}
这个文档将被转化如下的结构
{
"followers.age": [19, 26, 35],
"followers.name": [alex, jones, lisa, smith, mary, white]
}
那么{age: 35} 和 {name: Mary White} 已经丢失了
Is there a follower who is 26 years old?
这个可以回答
Is there a follower who is 26 years old and who is called Alex Jones?
这个问题就无法回答。
后续再讨论这个问题。http://my.oschina.net/qiangzigege/blog/270948