jjzjj

ElasticSearch——手写一个ElasticSearch分词器(附源码)

止步前行 2023-09-02 原文

1. 分词器插件

ElasticSearch提供了对文本内容进行分词的插件系统,对于不同的语言的文字分词器,规则一般是不一样的,而ElasticSearch提供的插件机制可以很好的集成各语种的分词器。

Elasticsearch 本身并不支持中文分词,但好在它支持编写和安装额外的分词管理插件,而开源的中文分词器 ik 就非常强大,具有20万以上的常用词库,可以满足一般的常用分词功能。

1.1 分词器插件作用

分词器的主要作用是把文本拆分成一个个最小粒度的单词,然后给ElasticSearch作为索引系统的词条使用。不同语种拆分单词规则也是不一样的,最常见的就是中文分词和英文分词。

对于同一个文本,使用不同分词器,拆分的效果也是不同的。如:"中国人民共和国"使用ik_max_word分词器会被拆分成:中国人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、共和、国,而使用standard分词器则会拆分成:中、国、人、民、共、和、国。被拆分后的词就可以作为ElasticSearch的索引词条来构建索引系统,这样就可以使用部分内容进行搜索了

2. 常用分词器

2.1 分词器介绍

  • standard

    标准分词器。处理英文能力强, 会将词汇单元转换成小写形式,并去除停用词和标点符号,对于非英文按单字切分

  • whitespace

    空格分词器。 针对英文,仅去除空格,没有其他任何处理, 不支持非英文

  • simple

    针对英文,通过非字母字符分割文本信息,然后将词汇单元统一为小写形式,数字类型的字符会被去除

  • stop

    stop 的功能超越了simplestopsimple的基础上增加了去除英文中的常用单词(如 thea 等),也可以更加自己的需要设置常用单词,不支持中文

  • keyword

    Keyword把整个输入作为一个单独词汇单元,不会对文本进行任何拆分,通常是用在邮政编码、电话号码等需要全匹配的字段上

  • pattern

    查询文本会被自动当做正则表达式处理,生成一组terms关键字,然后在对Elasticsearch进行查询

  • snowball

    雪球分析器,在 standard 的基础上添加了 snowball filterLucene官方不推荐使用

  • language

    一个用于解析特殊语言文本的analyzer集合,但不包含中文

  • ik

    IK分词器是一个开源的基于java语言开发的轻量级的中文分词工具包。 采用了特有的“正向迭代最细粒度切分算法”,支持细粒度和最大词长两种切分模式。支持:英文字母、数字、中文词汇等分词处理,兼容韩文、日文字符。 同时支持用户自定义词库。 它带有两个分词器:

    • ik_max_word : 将文本做最细粒度的拆分,尽可能多的拆分出词语
    • ik_smart:做最粗粒度的拆分,已被分出的词语将不会再次被其它词语占有
  • pinyin

    通过用户输入的拼音匹配 Elasticsearch 中的中文

2.2 分词器示例

对于同一个输入,使用不同分词器的结果。

输入:栖霞站长江线14w6号断路器

2.2.1 standard

{
    "tokens": [
        {
            "token": "栖",
            "start_offset": 0,
            "end_offset": 1,
            "type": "<IDEOGRAPHIC>",
            "position": 0
        },
        {
            "token": "霞",
            "start_offset": 1,
            "end_offset": 2,
            "type": "<IDEOGRAPHIC>",
            "position": 1
        },
        {
            "token": "站",
            "start_offset": 2,
            "end_offset": 3,
            "type": "<IDEOGRAPHIC>",
            "position": 2
        },
        {
            "token": "长",
            "start_offset": 3,
            "end_offset": 4,
            "type": "<IDEOGRAPHIC>",
            "position": 3
        },
        {
            "token": "江",
            "start_offset": 4,
            "end_offset": 5,
            "type": "<IDEOGRAPHIC>",
            "position": 4
        },
        {
            "token": "线",
            "start_offset": 5,
            "end_offset": 6,
            "type": "<IDEOGRAPHIC>",
            "position": 5
        },
        {
            "token": "14w6",
            "start_offset": 6,
            "end_offset": 10,
            "type": "<ALPHANUM>",
            "position": 6
        },
        {
            "token": "号",
            "start_offset": 10,
            "end_offset": 11,
            "type": "<IDEOGRAPHIC>",
            "position": 7
        },
        {
            "token": "断",
            "start_offset": 11,
            "end_offset": 12,
            "type": "<IDEOGRAPHIC>",
            "position": 8
        },
        {
            "token": "路",
            "start_offset": 12,
            "end_offset": 13,
            "type": "<IDEOGRAPHIC>",
            "position": 9
        },
        {
            "token": "器",
            "start_offset": 13,
            "end_offset": 14,
            "type": "<IDEOGRAPHIC>",
            "position": 10
        }
    ]
}

2.2.2 ik

  • ik_smart

{
    "tokens": [
        {
            "token": "栖霞",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "站",
            "start_offset": 2,
            "end_offset": 3,
            "type": "CN_CHAR",
            "position": 1
        },
        {
            "token": "长江",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 2
        },
        {
            "token": "线",
            "start_offset": 5,
            "end_offset": 6,
            "type": "CN_CHAR",
            "position": 3
        },
        {
            "token": "14w6",
            "start_offset": 6,
            "end_offset": 10,
            "type": "LETTER",
            "position": 4
        },
        {
            "token": "号",
            "start_offset": 10,
            "end_offset": 11,
            "type": "COUNT",
            "position": 5
        },
        {
            "token": "断路器",
            "start_offset": 11,
            "end_offset": 14,
            "type": "CN_WORD",
            "position": 6
        }
    ]
}
  • ik_max_word

{
    "tokens": [
        {
            "token": "栖霞",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "站长",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 1
        },
        {
            "token": "长江",
            "start_offset": 3,
            "end_offset": 5,
            "type": "CN_WORD",
            "position": 2
        },
        {
            "token": "线",
            "start_offset": 5,
            "end_offset": 6,
            "type": "CN_CHAR",
            "position": 3
        },
        {
            "token": "14w6",
            "start_offset": 6,
            "end_offset": 10,
            "type": "LETTER",
            "position": 4
        },
        {
            "token": "14",
            "start_offset": 6,
            "end_offset": 8,
            "type": "ARABIC",
            "position": 5
        },
        {
            "token": "w",
            "start_offset": 8,
            "end_offset": 9,
            "type": "ENGLISH",
            "position": 6
        },
        {
            "token": "6",
            "start_offset": 9,
            "end_offset": 10,
            "type": "ARABIC",
            "position": 7
        },
        {
            "token": "号",
            "start_offset": 10,
            "end_offset": 11,
            "type": "COUNT",
            "position": 8
        },
        {
            "token": "断路器",
            "start_offset": 11,
            "end_offset": 14,
            "type": "CN_WORD",
            "position": 9
        },
        {
            "token": "断路",
            "start_offset": 11,
            "end_offset": 13,
            "type": "CN_WORD",
            "position": 10
        },
        {
            "token": "器",
            "start_offset": 13,
            "end_offset": 14,
            "type": "CN_CHAR",
            "position": 11
        }
    ]
}

2.2.3 pinyin

{
    "tokens": [
        {
            "token": "q",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
            "token": "qi",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
            "token": "栖霞站长江线14w6号断路器",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
            "token": "qixiazhanzhangjiangxian14w6haoduanluqi",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
            "token": "qxzzjx14w6hdlq",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
            "token": "x",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 1
        },
        {
            "token": "xia",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 1
        },
        {
            "token": "z",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 2
        },
        {
            "token": "zhan",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 2
        },
        {
            "token": "zhang",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 3
        },
        {
            "token": "j",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 4
        },
        {
            "token": "jiang",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 4
        },
        {
            "token": "xian",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 5
        },
        {
            "token": "14",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 6
        },
        {
            "token": "w",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 7
        },
        {
            "token": "6",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 8
        },
        {
            "token": "h",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 9
        },
        {
            "token": "hao",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 9
        },
        {
            "token": "d",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 10
        },
        {
            "token": "duan",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 10
        },
        {
            "token": "l",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 11
        },
        {
            "token": "lu",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 11
        }
    ]
}

3. 自定义分词器

对于上面三种分词器的效果,在某些场景下可能都符合要求,下面来看看为什么需要自定义分词器。

众所周知,在推荐系统中,对于拼音搜索是很有必要的,比如输入:"ls",希望返回与"ls"相关的索引词条,“零食(ls)”、“雷蛇(ls)”、“林书豪(lsh)”……,上面都是对的情况,但如果此处仅仅使用拼音分词器,可会"l"相关的索引词条也会被命中,比如"李宁(l)"、“兰蔻(l)”……,这种情况下的推荐就是不合理的。

如果使用拼音分词器,对于上面的输入"栖霞站长江线14w6号断路器",会产生出很多单个字母的索引词条,比如:"h""d""l"等。如果用户输入的查询条件"ql",根本不想看到这条”栖霞站长江线14w6号断路器“数据,但由于"l"词条被命中,所有该条数据也会被返回。

那如果避免这些单字母词条的索引生成呢?下面就自己手写一个ElasticSearch的分词器,定制化!!!

3.1 分词器原理

3.1.1 分词器插件工作流程

  • ElasticSearch在启动过程中会读取plugins/分词器/plugin-descriptor.properties文件
  • 读取该配置文件获取分词器插件启动类信息并进行初始化,属性classname指向的启动类
  • 分词器插件启动类必须继承AnalysisPlugin,确保ElasticSearch可以调用我们自定义的类来获取分词器对象
  • ElasticSearch调用分词进行分词时,会实例化AnalyzerProvider对象,该对象中有get()方法可以获取到我们自定义的Analyzer对象,同时内部tokenStream()方法会调用用createComponents()方法实例化我们自定义的Tokenizer对象
  • Tokenizer是自定义分词器的核心组件,核心方法有4个,如下:
    • incrementToken():用来判断分词集合列表中是否还存在没读取的词条信息,以及设置term的基础属性如:长度,起始偏移量,结束偏移量,词条等
    • reset():重置默认数据和加载自定义模型来处理用户输入的字符串数据并进行分词处理,加入到分词集合列表
    • end():设置当分词结束的偏移量信息
    • close():销毁输入流对象和自定义的数据
  • Tokenizer对象每次完成一次用户输入文本的分词过程都会进行上述4步方法调用

3.2 分词器验证

  • 安装分词器

    将打包完的分词器zip文件,拷贝放到ElasticSearch安装目录的plugins目录下,可用elasticsearch-plugin list命令查看

  • 启动ElasticSearch

  • 验证分词器

{
    "tokens": [
        {
            "token": "栖霞",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "qixia",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 1
        },
        {
            "token": "qx",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 2
        },
        {
            "token": "栖霞站长江线14w6号断路器",
            "start_offset": 1,
            "end_offset": 14,
            "type": "word",
            "position": 3
        },
        {
            "token": "qixiazhanzhangjiangxian14w6haoduanluqi",
            "start_offset": 1,
            "end_offset": 14,
            "type": "word",
            "position": 4
        },
        {
            "token": "qxzzjx14w6hdlq",
            "start_offset": 1,
            "end_offset": 14,
            "type": "word",
            "position": 5
        },
        {
            "token": "站",
            "start_offset": 2,
            "end_offset": 3,
            "type": "word",
            "position": 6
        },
        {
            "token": "长江",
            "start_offset": 3,
            "end_offset": 5,
            "type": "word",
            "position": 7
        },
        {
            "token": "zhangjiang",
            "start_offset": 3,
            "end_offset": 5,
            "type": "word",
            "position": 8
        },
        {
            "token": "zj",
            "start_offset": 3,
            "end_offset": 5,
            "type": "word",
            "position": 9
        },
        {
            "token": "线",
            "start_offset": 5,
            "end_offset": 6,
            "type": "word",
            "position": 10
        },
        {
            "token": "14w6",
            "start_offset": 6,
            "end_offset": 10,
            "type": "word",
            "position": 11
        },
        {
            "token": "号",
            "start_offset": 10,
            "end_offset": 11,
            "type": "word",
            "position": 12
        },
        {
            "token": "断路",
            "start_offset": 11,
            "end_offset": 13,
            "type": "word",
            "position": 13
        },
        {
            "token": "duanlu",
            "start_offset": 11,
            "end_offset": 13,
            "type": "word",
            "position": 14
        },
        {
            "token": "dl",
            "start_offset": 11,
            "end_offset": 13,
            "type": "word",
            "position": 15
        },
        {
            "token": "断路器",
            "start_offset": 11,
            "end_offset": 14,
            "type": "word",
            "position": 16
        },
        {
            "token": "duanluqi",
            "start_offset": 11,
            "end_offset": 14,
            "type": "word",
            "position": 17
        },
        {
            "token": "dlq",
            "start_offset": 11,
            "end_offset": 14,
            "type": "word",
            "position": 18
        }
    ]
}

3.3 源码编译

  • JDK17idea支持JDK17

  • luncene版本与ElasticSearch版本要求一致

  • ElasticSearch打包后的分词器与ElasticSearch使用版本一致

源码地址:https://gitee.com/frank_zxd/elasticsearch-search-analyzer

有关ElasticSearch——手写一个ElasticSearch分词器(附源码)的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  4. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  5. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  6. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  7. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  8. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  9. ruby - Rails 关联 - 同一个类的多个 has_one 关系 - 2

    我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下

  10. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

随机推荐