2021第一篇黑科技
背景
最近有个项目接入了es,对于检索关键字需要扩大范围检索结果的匹配范围,所以考虑将检索关键字进行分词处理,再交给es处理。(当然这里可以用es的highlevelclient的analyze接口,此处不考虑这种情况)。于是研究ik-analysis,然后有了ik-springboot-demo这个项目,接下来都将以此项目展开描述。
解决的问题
在SpringBoot项目中直接且简单便捷的使用ik。
收获
- 得到了SpringBoot项目中使用ik的两种方式;
- 得到了SpringBoot项目的资源管理的更优方案。
重头戏
从ik的项目情况开始
(以下摘录自ik的github README)
IKAnalyzer.cfg.xml can be located at {conf}/analysis-ik/config/IKAnalyzer.cfg.xml or {plugins}/elasticsearch-analysis-ik-*/config/IKAnalyzer.cfg.xml
ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合,适合 Term Query;
ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”,适合 Phrase 查询。
项目引入ik
ik似乎已经很久没有将更新push到maven Central了,致使maven Central上的不是最新的。需要最新的版本,需要自行编译。这个是很入门的操作了(下载源码,本地mvn install
),就不展开了。此处以最新的7.4.0展开。
项目结构
注意:mvn-cp
及jar-cp
方式的项目,在ik-demo-config
目录有差异,以下示例是jar-cp
的结构。具体从ik-springboot-demo的mvn-cp
和jar-cp
两个分支查看。(master
分支使用的jar-cp
方式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
.
├── README.md
├── ik-demo-config
│ ├── resources
│ │ └── config
│ │ ├── analysis-ik
│ │ │ ├── IKAnalyzer.cfg.xml
│ │ │ ├── extra_main.dic
│ │ │ ├── extra_single_word.dic
│ │ │ ├── extra_single_word_full.dic
│ │ │ ├── extra_single_word_low_freq.dic
│ │ │ ├── extra_stopword.dic
│ │ │ ├── main.dic
│ │ │ ├── preposition.dic
│ │ │ ├── quantifier.dic
│ │ │ ├── stopword.dic
│ │ │ ├── suffix.dic
│ │ │ └── surname.dic
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ ├── application-test.yml
│ │ └── application.yml
│ └── target
│ └── generated-sources
│ └── annotations
├── ik-demo-parent.iml
├── ik-demo-server
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── abc
│ │ │ ├── App.java
│ │ │ └── kit
│ │ │ └── Kit.java
│ │ └── resources
│ └── test
│ └── java
├── ik-demo-services
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── abc
│ │ │ ├── config
│ │ │ │ └── IKConfig.java
│ │ │ └── services
│ │ │ └── IkJob.java
│ │ └── resources
│ └── test
│ └── java
└── pom.xml
|
详细配置如下
以下配置仅为了描述过程,部分内容实际使用中需要调整,如版本的管理。
-
parent pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<properties>
<elasticsearch.version>7.4.0</elasticsearch.version>
<java.version>1.8</java.version>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-analysis-ik</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>ik-demo-services</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
|
-
services module pom.xml
1
2
3
4
5
6
7
8
9
10
11
|
<dependencies>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-analysis-ik</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.1</version>
</dependency>
</dependencies>
|
-
示例部分源码(点击这里是完整内容)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
static String text = "123abc;;.。。。\\l a吃 IK Analyzer是一个结合词典分词和文法分词的中文分词开源工具包。它使用了全新的正向迭代最细粒度切分算法。";
...
IKSegmenter segmenter = new IKSegmenter(new StringReader(text), configuration);
Lexeme next;
System.out.print("非智能分词结果(ik_max_world):");
StringJoiner stringJoiner = new StringJoiner(",");
while((next=segmenter.next())!=null){
String lexemeText = next.getLexemeText();
stringJoiner.add(lexemeText);
}
System.out.println(stringJoiner);
System.out.println();
System.out.println("----------------------------分割线------------------------------");
this.configuration.setUseSmart(true);
IKSegmenter smartSegmenter = new IKSegmenter(new StringReader(text), configuration);
System.out.print("智能分词结果(ik_smart):");
stringJoiner = new StringJoiner(",");
while((next=smartSegmenter.next())!=null) {
String lexemeText = next.getLexemeText();
stringJoiner.add(lexemeText);
}
System.out.println(stringJoiner);
|
遇到的问题
1
2
3
|
ERROR Dictionary ik-analyzer: Main Dict not found java.io.FileNotFoundException: /Users/Qicz/.m2/repository/org/elasticsearch/elasticsearch-analysis-ik/7.4.0/config/main.dic (No such file or directory)
ERROR Dictionary ik-analyzer: Surname not found java.io.FileNotFoundException: /Users/Qicz/.m2/repository/org/elasticsearch/elasticsearch-analysis-ik/7.4.0/config/surname.dic (No such file or directory)
|
找不到对应的ik的词典。其他博友的解决方案是改写ik。我的考虑是先分析源码,再确定是不是要改写。从ik源码Configuration
入手(不得不说ik的源码写的有点随意,风格各种都有…)
1
2
3
4
5
6
7
8
9
10
|
public Configuration(Environment env,Settings settings) {
this.environment = env;
this.settings=settings;
this.useSmart = settings.get("use_smart", "false").equals("true");
this.enableLowercase = settings.get("enable_lowercase", "true").equals("true");
this.enableRemoteDict = settings.get("enable_remote_dict", "true").equals("true");
Dictionary.initial(this);
}
|
可以看到,构造Configuration
需要Environment
及Settings
。从字面理解,这二者应该就是与config
有关的。先看看Environment
的构造
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public Environment(Settings settings, Path configPath) {
this(settings, configPath, PathUtils.get(System.getProperty("java.io.tmpdir"), new String[0]));
}
Environment(Settings settings, Path configPath, Path tmpPath) {
if (PATH_HOME_SETTING.exists(settings)) {
Path homeFile = PathUtils.get((String)PATH_HOME_SETTING.get(settings), new String[0]).toAbsolutePath().normalize();
if (configPath != null) {
this.configFile = configPath.toAbsolutePath().normalize();
} else {
this.configFile = homeFile.resolve("config");
}
//... 其他已省略
|
可以看到,configFile
源自Settings
或configPath
,于是有了下面的构造一个Configuration
的代码
1
2
3
4
5
6
7
|
Environment environment = new Environment(Settings.builder().put("path.home", path).build(), null);
Settings settings = Settings.builder()
.put("use_smart", false)
.put("enable_lowercase", false)
.put("enable_remote_dict", false)
.build();
return new Configuration(environment, settings).setUseSmart(false);
|
那么接下里的问题就是如果确定这path
了。从ik的源码来看,借助了Path
需要一个绝对路径,提供一个流
(InputStream
)都不好使,如果真那样就只能改源码了,不是我的初衷,于是从构造path
继续深入。
一个绝对的path
需要考虑在IDE(如idea)中调试及实际应用运行(jar方式)两个情况,而为了便于应用的部署,配置也必然会与jar
一起打包,那么这种情况下就无法拿到绝对的path
了(?!)。所以需要把相关的配置拿到jar
之外,那么这就自然想到了使用maven
的插件,将关联的配置文件拷贝到jar
可以读取的地方。
使用maven插件
ik-springboot-demo的mvn-cp
分支有完整源码
使用maven插件,有了ik-demo-server
的下面pom.xml配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>abc.App</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <!-- 拷贝ik配置到${project.build.directory}/config下 -->
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-resources</id> <!-- here the phase you need -->
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration> <!--copyTo的目录-->
<outputDirectory>${project.build.directory}/config</outputDirectory>
<resources>
<resource> <!--被copy的目录-->
<directory>../ik-demo-config/config</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources> <!-- 指定SpringBoot资源配置位置 -->
<resource>
<directory>../ik-demo-config/resources</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
|
进行上面的配置之后,当进行mvn clean package
时则会将ik-demo-config/config
下面的内容配置拷贝到jar
同级的config
目录中,这样就可以了。那么对应的path
也就知道了
1
|
String path = System.getProperty("user.dir")/* + "/config" */;
|
因为ik做了config目录的配置处理,所以+ "/config"
不必添加。也就是String path = System.getProperty("user.dir");
但在IDE(如idea)中,实时编译的class都在target下(并未进行mvn clean package
操作),那么在target下就没有config
目录,那么在ide里实时调试就有问题了,于是有了下面的兼容处理
1
2
3
4
5
|
String path = System.getProperty("user.dir");
// 仅在idea中实时调试需要,与config所在的目录必须一致,此处为ik-demo-config
if (!Kit.runningAsJar) {
path += "/ik-demo-config";
}
|
有了上面的前提就得到了完整的Configuration
构造(同时将其注入ioc,此处inIdea
的处理与jar-cp
有略微差异)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Bean
public Configuration ikConfiguration() {
String path = System.getProperty("user.dir");
// 仅在idea中实时调试需要,与config所在的目录必须一致,此处为ik-demo-config
if (!Kit.runningAsJar) {
path += "/ik-demo-config";
}
Environment environment = new Environment(Settings.builder().put("path.home", path).build(), null);
Settings settings = Settings.builder()
.put("use_smart", false)
.put("enable_lowercase", false)
.put("enable_remote_dict", false)
.build();
return new Configuration(environment, settings).setUseSmart(false);
}
|
经测试上面的配置可以实现分词。
那么除了maven
插件以外,是不是可以使用Java
来直接拷贝呢,会不会更便捷呢?于是有了jar-cp
的方式
使用jar-cp方式(推荐)
ik-springboot-demo的jar-cp
分支有完整源码
使用jar-cp
,实质就是Java
进行相关资源拷贝的逻辑处理,于是有了下面的代码(拷贝jar
中的config
目录及其子目录、其他properties
、yaml
、yml
、xml
配置文件到当前jar
所在的config
目录中)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
/**
* Kit
*
* @author Qicz
*/
public final class Kit {
public static boolean runningAsJar = false;
/**
* 拷贝SpringBoot配置&ik配置
* @param appClass spring boot 启动类
* @throws IOException io exception
*/
public static void copyConfigInJar(Class<?> appClass) throws IOException {
ApplicationHome applicationHome = new ApplicationHome(appClass);
File source = applicationHome.getSource();
if (source != null) {
String absolutePath = source.getAbsolutePath();
Kit.runningAsJar = absolutePath.endsWith("jar");
if (!Kit.runningAsJar) {
return;
}
final String configPath = "config/";
File config = new File(System.getProperty("user.dir") + "/" + configPath);
if (config.exists() || !config.mkdir()) {
return;
}
JarFile jarFile = new JarFile(source);
final Set<String> configFiles = new HashSet<String>(){{
add(".properties");
add(".yaml");
add(".yml");
add(".xml");
}};
for (Enumeration<? extends ZipEntry> entries = jarFile.entries(); entries.hasMoreElements(); ) {
ZipEntry entry = entries.nextElement();
String entryName = entry.getName();
// 仅拷贝config目录或properties,yaml。
boolean isConfig = false;
int lastIndexOf = entryName.indexOf(configPath);
String outPath = "";
// has config dir
if (lastIndexOf != -1) {
outPath = entryName.substring(lastIndexOf);
isConfig = true;
} else {
lastIndexOf = entryName.lastIndexOf(".");
if (lastIndexOf != -1) {
String file = entryName.substring(entryName.lastIndexOf("/") + 1);
boolean pomFile = "pom.properties".equals(file) || "pom.xml".equals(file);
isConfig = configFiles.contains(entryName.substring(lastIndexOf)) && !pomFile;
if (isConfig) {
outPath = String.join("", configPath, file);
}
}
}
if (!isConfig) {
continue;
}
InputStream jarFileInputStream = jarFile.getInputStream(entry);
File currentFile = new File(outPath.substring(0, outPath.lastIndexOf('/')));;
if (!currentFile.exists() && !currentFile.mkdirs()) {
continue;
}
// 判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压
if (new File(outPath).isDirectory()) {
continue;
}
FileOutputStream out = new FileOutputStream(outPath);
byte[] bytes = new byte[1024];
int len;
while ((len = jarFileInputStream.read(bytes)) > 0) {
out.write(bytes, 0, len);
}
jarFileInputStream.close();
out.close();
}
}
}
}
|
需要注意的时,这个处理必须要在SpringBoot启用起来之前进行,即就是
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/**
* App
*
* @author Qicz
*/
@SpringBootApplication
public class App {
public static void main(String[] args) throws IOException {
Kit.copyConfigInJar(App.class);
SpringApplication.run(App.class, args);
}
}
|
借用了SpringBoot
的ApplicationHome
来获取当前应用的启动类所在的路径absolutePath
1
2
3
4
5
|
ApplicationHome applicationHome = new ApplicationHome(App.class);
File source = applicationHome.getSource();
if (source != null) {
String absolutePath = source.getAbsolutePath();
...
|
通过检查这个absolutePath
是否以jar
结尾确定是否以jar
方式运行。如果是则开始进行jar
文件的解析,将其资源拷贝到jar
的同级目录的config
中。
采用jar-cp
对应的ik-demo-server的pom.xml配置也有相应的差异,无需maven插件的处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>abc.App</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>../ik-demo-config/resources</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
|
对应的Configuration
构造,差异在于inIdea
中的路径配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Bean
public Configuration ikConfiguration() {
String path = System.getProperty("user.dir");
// 仅在idea中实时调试需要,与config所在的目录必须一致,此处为ik-demo-config/resources
if (!Kit.runningAsJar) {
path += "/ik-demo-config/resources";
}
Environment environment = new Environment(Settings.builder().put("path.home", path).build(), null);
Settings settings = Settings.builder()
.put("use_smart", false)
.put("enable_lowercase", false)
.put("enable_remote_dict", false)
.build();
return new Configuration(environment, settings).setUseSmart(false);
}
|
需要注意的是,在jar-cp
方式中,对应的ik-demo-config中的资源的存放方式是将SpringBoot的配置及ik的配置都放到ik-demo-config/resources/config
中,并将ik-demo-config/resources使用maven配置成了项目的resources
,也就是package
时会将resources
下面的SpringBoot配置及ik配置都打包进jar
,这也是必然的,这是进行jar-cp
的基础,使用Java拷贝问题,总得有东西给你拷吧。😝,这样的好处就是:
你拿到一个jar
,直接java -jar xx.jar
(项目部署)对应的配置(SpringBoot各类profile及ik的配置)都有了。不需要在手动拷贝,也可以避免不必要的失误 。
目录结构是这样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
├── ik-demo-config
│ ├── resources
│ │ └── config
│ │ ├── analysis-ik
│ │ │ ├── IKAnalyzer.cfg.xml
│ │ │ ├── extra_main.dic
│ │ │ ├── extra_single_word.dic
│ │ │ ├── extra_single_word_full.dic
│ │ │ ├── extra_single_word_low_freq.dic
│ │ │ ├── extra_stopword.dic
│ │ │ ├── main.dic
│ │ │ ├── preposition.dic
│ │ │ ├── quantifier.dic
│ │ │ ├── stopword.dic
│ │ │ ├── suffix.dic
│ │ │ └── surname.dic
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ ├── application-test.yml
│ │ └── application.yml
|
总结
-
使用了两种方式来处理ik的配置,mvn-cp
及jar-cp
,推荐jar-cp
,便于应用的部署;
-
两种方式中对应的ik-demo-config
的目录结构有差异
-
mvn-cp
方式
mvn-cp
,mvn package
仅拷贝ik的配置(当然可以配置将SpringBoot配置也拷贝,但是实际项目部署,当你是用jar
直接部署时,SpringBoot的配置和ik配置都是没有的,所以没有什么区别)
ik-demo-config/resources
/application.yml
ik-demo-config/config
/analysis-ik
-
jar-cp
方式
jar-cp
,mvn package
时会将SpringBoot配置及ik配置(它们都在ik-demo-config/resources/config
下)都打包进jar,为后面jar运行执行jar-cp
提供基础`
ik-demo-config/resources/config
/analysis-ik…application.yml
-
jar-cp
【推荐方式】便于在项目现场的部署,不需要手动拷贝相关的配置,避免不不要的手误;且与SpringBoot的配置机制一致,只需针对性微调关联配置即可(SpringBoot本就会优先扫描当前应用jar所在目录下的config
目录的application-{profile}.yml/prperties)参考这里。
jar-cp
核心的copy
处理已合并到spring-boot-x
的SpringApplicationX中,如下使用即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/**
* App
*
* @author Qicz
*/
@SpringBootApplication
@EnableExtension
public class App {
public static void main(String[] args) throws InterruptedException {
SpringApplicationX.run(App.class, args);
}
}
|