自定义分词器

前言

es能够实现快速的全文搜索,除了依赖其本身倒排索引的思想,还依赖其分词器

分析器

  • es本身内置了一些常用的分析器(analyzer),分析器由三种构建组成:
    • character filter: 字符过滤器(在一段文本进行分词之前,先进行预处理,比如过滤html标签等)
    • tokenizer: 分词器(对字段进行切分)
    • token filter: token过滤器(对切分的单词进行加工,如大小写转换等)
  • 三者顺序: character filter -> tokenizer -> token filter
  • 三者个数: character filter(0个或多个)+tokenizer(恰好一个)+token filter(0个或多个)

es内置的分析器

  • es内置了一些常用的分析器,如下:
Standard Analyzer - 默认分词器,按词切分,小写处理
Simple Analyzer - 按照非字母切分(符号被过滤), 小写处理
Stop Analyzer - 小写处理,停用词过滤(the,a,is)
Whitespace Analyzer - 按照空格切分,不转小写
Keyword Analyzer - 不分词,直接将输入当作输出
Patter Analyzer - 正则表达式,默认\W+(非字符分割)
Language - 提供了30多种常见语言的分词器
Customer Analyzer 自定义分词器
  • 根据这些分词器我们可以进行自定义一些简单的分词器,如 以逗号分隔的分词器
{
 "settings":{
  "analysis":{
    "analyzer":{
      "comma":{
        "type":"pattern",
        "pattern":","
      }
    }
  }
 }
}
  • 或者自定义选择分词器及过滤器,组装一个新的分析器
{
    "settings": {
        "analysis": {
            "analyzer": {
                "std_folded": {
                    "type": "custom",
                    "tokenizer": "standard",
                    "filter": [
                        "lowercase",
                        "asciifolding"
                    ]
                }
            }
        }
    }
}

自定义分析器

  • 并不是所有的需求都可以以内置的组件进行组装得到,当有一些特殊的需求时,内置的分词器可能很难实现,这时我们可以尝试自定义分析器。 以下以连续字符串分词为例: 给定一个字符串,要求分词出来的结果涵盖: 所有的连续3个字母、4个字母、5个字母...
    嗯... 其实elasticsearch内置的分词器,也可以实现,如下:
{
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "my_tokenizer"
        }
      },
      "tokenizer": {
        "my_tokenizer": {
          "type": "ngram",
          "min_gram": 4,
          "max_gram": 10,
          "token_chars": [
            "letter",
            "digit"
          ]
        }
      }
    }
  }}

自定义插件实现

这里我们以一个空格分词器为例

pom文件
  <properties>
    <elasticsearch.version>6.5.4</elasticsearch.version>
    <lucene.version>7.5.0</lucene.version>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.elasticsearch</groupId>
      <artifactId>elasticsearch</artifactId>
      <version>${elasticsearch.version}</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>false</filtering>
        <excludes>
          <exclude>*.properties</exclude>
        </excludes>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>2.6</version>
        <configuration>
          <appendAssemblyId>false</appendAssemblyId>
          <outputDirectory>${project.build.directory}/releases/</outputDirectory>
          <descriptors>
            <descriptor>${basedir}/src/main/assemblies/plugin.xml</descriptor>
          </descriptors>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.5.1</version>
        <configuration>
          <source>${maven.compiler.target}</source>
          <target>${maven.compiler.target}</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  • 注意这里指定了 plugin.xml并设置了静态资源文件
plugin.xml 注意文件位置
<?xml version="1.0"?>
<assembly>
  <id>my-analysis</id>
  <formats>
    <format>zip</format>
  </formats>
  <includeBaseDirectory>false</includeBaseDirectory>
  <files>
    <file>
      <source>${project.basedir}/src/main/resources/my.properties</source>
      <outputDirectory/>
      <filtered>true</filtered>
    </file>
  </files>
  <dependencySets>
    <dependencySet>
      <outputDirectory/>
      <useProjectArtifact>true</useProjectArtifact>
      <useTransitiveFiltering>true</useTransitiveFiltering>
      <excludes>
        <exclude>org.elasticsearch:elasticsearch</exclude>
      </excludes>
    </dependencySet>
  </dependencySets>
</assembly>
  • 这里指定了my.properties
my.properties
description=${project.description}
version=${project.version}
name=${project.name}
classname=com.test.plugin.MyPlugin
java.version=${maven.compiler.target}
elasticsearch.version=${elasticsearch.version}
  • 这里指定了classname就是我们的插件类
代码
  • 分析器
package com.test.index.analysis;

import org.apache.lucene.analysis.Analyzer;

/**
 * @author phil.zhang
 * @date 2021/2/21
 */
public class MyAnalyzer extends Analyzer {
  @Override
  protected TokenStreamComponents createComponents(String fieldName) {
    MyTokenizer myTokenizer = new MyTokenizer();
    return new TokenStreamComponents(myTokenizer);
  }
}
  • 分析器provider
package com.test.index.analysis;

import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.analysis.AbstractIndexAnalyzerProvider;

/**
 * @author phil.zhang
 * @date 2021/2/21
 */
public class MyAnalyzerProvider extends AbstractIndexAnalyzerProvider<MyAnalyzer> {
  private MyAnalyzer myAnalyzer;
  public MyAnalyzerProvider(IndexSettings indexSettings,Environment environment, String name, Settings settings) {
    super(indexSettings,name,settings);
    myAnalyzer = new MyAnalyzer();
  }
  @Override
  public MyAnalyzer get() {
    return myAnalyzer;
  }
}
  • 分词器--核心逻辑
package com.test.index.analysis;

import java.io.IOException;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;

/**
 * @author phil.zhang
 * @date 2021/2/21
 */
public class MyTokenizer extends Tokenizer {
  private final StringBuilder buffer = new StringBuilder();
  private int suffixOffset;
  /** 分词开始的位置 **/
  private int tokenStart = 0;
  /** 分词结束的位置 **/
  private int tokenEnd = 0;
  /** 将attribute加入map, 这里分出来的词语 需要包含字符串 和 offset两种属性 **/
  private final CharTermAttribute termAttribute = addAttribute(CharTermAttribute.class);
  private final OffsetAttribute offsetAttribute = addAttribute(OffsetAttribute.class);

  @Override
  public boolean incrementToken() throws IOException {
    clearAttributes();
    buffer.setLength(0); // 清空数据
    int ci;
    char ch;
    tokenStart = tokenEnd;
    // 读取一个字符
    ci = input.read();
    ch = (char)ci;
    while (true) {
      if (ci == -1) {
        // 没有数据了
        if (buffer.length() == 0) {
          // 分词结束
          return false;
        }else {
          // 返回一个分词结果
          termAttribute.setEmpty().append(buffer);
          offsetAttribute.setOffset(correctOffset(tokenStart),correctOffset(tokenEnd));
          return true;
        }
      }else if (ch == ' ') {
        // 遇到空格
        tokenEnd ++;
        if (buffer.length()>0) {
          termAttribute.setEmpty().append(buffer);
          offsetAttribute.setOffset(correctOffset(tokenStart),correctOffset(tokenEnd));
          return true;
        }else {
          ci = input.read();
          ch = (char) ci;
        }
      }else { // 没有遇到空格,继续追加
        buffer.append(ch);
        tokenEnd++;
        ci = input.read();
        ch = (char) ci;

      }
    }
  }

  @Override
  public void end() throws IOException {
    int finalOffset = correctOffset(suffixOffset);
    offsetAttribute.setOffset(finalOffset,finalOffset);
  }

  @Override
  public void reset() throws IOException {
    super.reset();
    tokenStart = tokenEnd = 0;
  }
}
  • 分词器工厂
package com.test.index.analysis;

import org.apache.lucene.analysis.Tokenizer;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.analysis.AbstractTokenizerFactory;

/**
 * @author phil.zhang
 * @date 2021/2/21
 */
public class MyTokenizerFactory extends AbstractTokenizerFactory {

  public MyTokenizerFactory(IndexSettings indexSettings,Environment environment,String ignored, Settings settings) {
    super(indexSettings,ignored,settings);
  }

  @Override
  public Tokenizer create() {
    return new MyTokenizer();
  }
}
  • 插件类
package com.test.plugin;

import com.test.index.analysis.MyAnalyzerProvider;
import com.test.index.analysis.MyTokenizerFactory;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.analysis.Analyzer;
import org.elasticsearch.index.analysis.AnalyzerProvider;
import org.elasticsearch.index.analysis.TokenizerFactory;
import org.elasticsearch.indices.analysis.AnalysisModule;
import org.elasticsearch.plugins.AnalysisPlugin;
import org.elasticsearch.plugins.Plugin;

/**
 * @author phil.zhang
 * @date 2021/2/21
 */
public class MyPlugin extends Plugin implements AnalysisPlugin {

  @Override
  public Map<String, AnalysisModule.AnalysisProvider<TokenizerFactory>> getTokenizers() {
    Map<String, AnalysisModule.AnalysisProvider<TokenizerFactory>> extra = new HashMap<>();
    extra.put("my-word", MyTokenizerFactory::new);
    return extra;
  }
  @Override
  public Map<String, AnalysisModule.AnalysisProvider<AnalyzerProvider<? extends Analyzer>>> getAnalyzers() {

    Map<String, AnalysisModule.AnalysisProvider<AnalyzerProvider<? extends Analyzer>>> extra = new HashMap<>();
    extra.put("my-word", MyAnalyzerProvider::new);
    return extra;
  }
}
后续

到这里代码就开发完成了,可以进行简单的自测看下效果,然后就可以使用maven命令进行打包,之后就是分词器插件的安装流程,这里不再进一步说明

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容