SpringBoot环境的ik应用
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├── README.md
3├── ik-demo-config
4│ ├── resources
5│ │ └── config
6│ │ ├── analysis-ik
7│ │ │ ├── IKAnalyzer.cfg.xml
8│ │ │ ├── extra_main.dic
9│ │ │ ├── extra_single_word.dic
10│ │ │ ├── extra_single_word_full.dic
11│ │ │ ├── extra_single_word_low_freq.dic
12│ │ │ ├── extra_stopword.dic
13│ │ │ ├── main.dic
14│ │ │ ├── preposition.dic
15│ │ │ ├── quantifier.dic
16│ │ │ ├── stopword.dic
17│ │ │ ├── suffix.dic
18│ │ │ └── surname.dic
19│ │ ├── application-dev.yml
20│ │ ├── application-prod.yml
21│ │ ├── application-test.yml
22│ │ └── application.yml
23│ └── target
24│ └── generated-sources
25│ └── annotations
26├── ik-demo-parent.iml
27├── ik-demo-server
28│ ├── pom.xml
29│ └── src
30│ ├── main
31│ │ ├── java
32│ │ │ └── abc
33│ │ │ ├── App.java
34│ │ │ └── kit
35│ │ │ └── Kit.java
36│ │ └── resources
37│ └── test
38│ └── java
39├── ik-demo-services
40│ ├── pom.xml
41│ └── src
42│ ├── main
43│ │ ├── java
44│ │ │ └── abc
45│ │ │ ├── config
46│ │ │ │ └── IKConfig.java
47│ │ │ └── services
48│ │ │ └── IkJob.java
49│ │ └── resources
50│ └── test
51│ └── java
52└── pom.xml
详细配置如下
以下配置仅为了描述过程,部分内容实际使用中需要调整,如版本的管理。
-
parent pom.xml
1<properties> 2 <elasticsearch.version>7.4.0</elasticsearch.version> 3 <java.version>1.8</java.version> 4 <maven.compiler.target>1.8</maven.compiler.target> 5</properties> 6 7<dependencyManagement> 8 <dependencies> 9 <dependency> 10 <groupId>org.elasticsearch</groupId> 11 <artifactId>elasticsearch-analysis-ik</artifactId> 12 <version>${elasticsearch.version}</version> 13 </dependency> 14 <dependency> 15 <groupId>org.example</groupId> 16 <artifactId>ik-demo-services</artifactId> 17 <version>${project.version}</version> 18 </dependency> 19 </dependencies> 20</dependencyManagement>
-
services module pom.xml
1<dependencies> 2 <dependency> 3 <groupId>org.elasticsearch</groupId> 4 <artifactId>elasticsearch-analysis-ik</artifactId> 5 </dependency> 6 <dependency> 7 <groupId>org.springframework.boot</groupId> 8 <artifactId>spring-boot-starter-web</artifactId> 9 <version>2.4.1</version> 10 </dependency> 11</dependencies>
-
示例部分源码(点击这里是完整内容)
1static String text = "123abc;;.。。。\\l a吃 IK Analyzer是一个结合词典分词和文法分词的中文分词开源工具包。它使用了全新的正向迭代最细粒度切分算法。"; 2 3... 4 5IKSegmenter segmenter = new IKSegmenter(new StringReader(text), configuration); 6Lexeme next; 7System.out.print("非智能分词结果(ik_max_world):"); 8StringJoiner stringJoiner = new StringJoiner(","); 9while((next=segmenter.next())!=null){ 10 String lexemeText = next.getLexemeText(); 11 stringJoiner.add(lexemeText); 12} 13System.out.println(stringJoiner); 14System.out.println(); 15System.out.println("----------------------------分割线------------------------------"); 16 17this.configuration.setUseSmart(true); 18IKSegmenter smartSegmenter = new IKSegmenter(new StringReader(text), configuration); 19System.out.print("智能分词结果(ik_smart):"); 20stringJoiner = new StringJoiner(","); 21while((next=smartSegmenter.next())!=null) { 22 String lexemeText = next.getLexemeText(); 23 stringJoiner.add(lexemeText); 24} 25System.out.println(stringJoiner);
遇到的问题
1ERROR 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)
2
3ERROR 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的源码写的有点随意,风格各种都有…)
1public Configuration(Environment env,Settings settings) {
2 this.environment = env;
3 this.settings=settings;
4
5 this.useSmart = settings.get("use_smart", "false").equals("true");
6 this.enableLowercase = settings.get("enable_lowercase", "true").equals("true");
7 this.enableRemoteDict = settings.get("enable_remote_dict", "true").equals("true");
8
9 Dictionary.initial(this);
10}
可以看到,构造Configuration
需要Environment
及Settings
。从字面理解,这二者应该就是与config
有关的。先看看Environment
的构造
1public Environment(Settings settings, Path configPath) {
2 this(settings, configPath, PathUtils.get(System.getProperty("java.io.tmpdir"), new String[0]));
3 }
4
5 Environment(Settings settings, Path configPath, Path tmpPath) {
6 if (PATH_HOME_SETTING.exists(settings)) {
7 Path homeFile = PathUtils.get((String)PATH_HOME_SETTING.get(settings), new String[0]).toAbsolutePath().normalize();
8 if (configPath != null) {
9 this.configFile = configPath.toAbsolutePath().normalize();
10 } else {
11 this.configFile = homeFile.resolve("config");
12 }
13//... 其他已省略
可以看到,configFile
源自Settings
或configPath
,于是有了下面的构造一个Configuration
的代码
1Environment environment = new Environment(Settings.builder().put("path.home", path).build(), null);
2Settings settings = Settings.builder()
3 .put("use_smart", false)
4 .put("enable_lowercase", false)
5 .put("enable_remote_dict", false)
6 .build();
7return 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<build>
2 <plugins>
3 <plugin>
4 <groupId>org.springframework.boot</groupId>
5 <artifactId>spring-boot-maven-plugin</artifactId>
6 <configuration>
7 <mainClass>abc.App</mainClass>
8 </configuration>
9 <executions>
10 <execution>
11 <goals>
12 <goal>repackage</goal>
13 </goals>
14 </execution>
15 </executions>
16 </plugin>
17 <plugin> <!-- 拷贝ik配置到${project.build.directory}/config下 -->
18 <artifactId>maven-resources-plugin</artifactId>
19 <version>2.6</version>
20 <executions>
21 <execution>
22 <id>copy-resources</id> <!-- here the phase you need -->
23 <phase>validate</phase>
24 <goals>
25 <goal>copy-resources</goal>
26 </goals>
27 <configuration> <!--copyTo的目录-->
28 <outputDirectory>${project.build.directory}/config</outputDirectory>
29 <resources>
30 <resource> <!--被copy的目录-->
31 <directory>../ik-demo-config/config</directory>
32 <filtering>true</filtering>
33 </resource>
34 </resources>
35 </configuration>
36 </execution>
37 </executions>
38 </plugin>
39 </plugins>
40 <resources> <!-- 指定SpringBoot资源配置位置 -->
41 <resource>
42 <directory>../ik-demo-config/resources</directory>
43 </resource>
44 <resource>
45 <directory>src/main/resources</directory>
46 </resource>
47 </resources>
48</build>
进行上面的配置之后,当进行mvn clean package
时则会将ik-demo-config/config
下面的内容配置拷贝到jar
同级的config
目录中,这样就可以了。那么对应的path
也就知道了
1String 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里实时调试就有问题了,于是有了下面的兼容处理
1String path = System.getProperty("user.dir");
2// 仅在idea中实时调试需要,与config所在的目录必须一致,此处为ik-demo-config
3if (!Kit.runningAsJar) {
4 path += "/ik-demo-config";
5}
有了上面的前提就得到了完整的Configuration
构造(同时将其注入ioc,此处inIdea
的处理与jar-cp
有略微差异)
1@Bean
2public Configuration ikConfiguration() {
3 String path = System.getProperty("user.dir");
4 // 仅在idea中实时调试需要,与config所在的目录必须一致,此处为ik-demo-config
5 if (!Kit.runningAsJar) {
6 path += "/ik-demo-config";
7 }
8 Environment environment = new Environment(Settings.builder().put("path.home", path).build(), null);
9 Settings settings = Settings.builder()
10 .put("use_smart", false)
11 .put("enable_lowercase", false)
12 .put("enable_remote_dict", false)
13 .build();
14 return new Configuration(environment, settings).setUseSmart(false);
15}
经测试上面的配置可以实现分词。
那么除了maven
插件以外,是不是可以使用Java
来直接拷贝呢,会不会更便捷呢?于是有了jar-cp
的方式
使用jar-cp方式(推荐)
ik-springboot-demo的
jar-cp
分支有完整源码
使用jar-cp
,实质就是Java
进行相关资源拷贝的逻辑处理,于是有了下面的代码(拷贝jar
中的config
目录及其子目录、其他properties
、yaml
、yml
、xml
配置文件到当前jar
所在的config
目录中)
1/**
2 * Kit
3 *
4 * @author Qicz
5 */
6public final class Kit {
7
8 public static boolean runningAsJar = false;
9
10 /**
11 * 拷贝SpringBoot配置&ik配置
12 * @param appClass spring boot 启动类
13 * @throws IOException io exception
14 */
15 public static void copyConfigInJar(Class<?> appClass) throws IOException {
16 ApplicationHome applicationHome = new ApplicationHome(appClass);
17 File source = applicationHome.getSource();
18 if (source != null) {
19 String absolutePath = source.getAbsolutePath();
20 Kit.runningAsJar = absolutePath.endsWith("jar");
21 if (!Kit.runningAsJar) {
22 return;
23 }
24
25 final String configPath = "config/";
26 File config = new File(System.getProperty("user.dir") + "/" + configPath);
27 if (config.exists() || !config.mkdir()) {
28 return;
29 }
30
31 JarFile jarFile = new JarFile(source);
32 final Set<String> configFiles = new HashSet<String>(){{
33 add(".properties");
34 add(".yaml");
35 add(".yml");
36 add(".xml");
37 }};
38 for (Enumeration<? extends ZipEntry> entries = jarFile.entries(); entries.hasMoreElements(); ) {
39 ZipEntry entry = entries.nextElement();
40 String entryName = entry.getName();
41 // 仅拷贝config目录或properties,yaml。
42 boolean isConfig = false;
43 int lastIndexOf = entryName.indexOf(configPath);
44 String outPath = "";
45 // has config dir
46 if (lastIndexOf != -1) {
47 outPath = entryName.substring(lastIndexOf);
48 isConfig = true;
49 } else {
50 lastIndexOf = entryName.lastIndexOf(".");
51 if (lastIndexOf != -1) {
52 String file = entryName.substring(entryName.lastIndexOf("/") + 1);
53 boolean pomFile = "pom.properties".equals(file) || "pom.xml".equals(file);
54 isConfig = configFiles.contains(entryName.substring(lastIndexOf)) && !pomFile;
55 if (isConfig) {
56 outPath = String.join("", configPath, file);
57 }
58 }
59 }
60
61 if (!isConfig) {
62 continue;
63 }
64 InputStream jarFileInputStream = jarFile.getInputStream(entry);
65 File currentFile = new File(outPath.substring(0, outPath.lastIndexOf('/')));;
66 if (!currentFile.exists() && !currentFile.mkdirs()) {
67 continue;
68 }
69 // 判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压
70 if (new File(outPath).isDirectory()) {
71 continue;
72 }
73
74 FileOutputStream out = new FileOutputStream(outPath);
75 byte[] bytes = new byte[1024];
76 int len;
77 while ((len = jarFileInputStream.read(bytes)) > 0) {
78 out.write(bytes, 0, len);
79 }
80 jarFileInputStream.close();
81 out.close();
82 }
83 }
84 }
85}
需要注意的时,这个处理必须要在SpringBoot启用起来之前进行,即就是
1/**
2 * App
3 *
4 * @author Qicz
5 */
6@SpringBootApplication
7public class App {
8
9 public static void main(String[] args) throws IOException {
10 Kit.copyConfigInJar(App.class);
11 SpringApplication.run(App.class, args);
12 }
13}
借用了SpringBoot
的ApplicationHome
来获取当前应用的启动类所在的路径absolutePath
1ApplicationHome applicationHome = new ApplicationHome(App.class);
2File source = applicationHome.getSource();
3if (source != null) {
4 String absolutePath = source.getAbsolutePath();
5 ...
通过检查这个absolutePath
是否以jar
结尾确定是否以jar
方式运行。如果是则开始进行jar
文件的解析,将其资源拷贝到jar
的同级目录的config
中。
采用jar-cp
对应的ik-demo-server的pom.xml配置也有相应的差异,无需maven插件的处理。
1<build>
2 <plugins>
3 <plugin>
4 <groupId>org.springframework.boot</groupId>
5 <artifactId>spring-boot-maven-plugin</artifactId>
6 <configuration>
7 <mainClass>abc.App</mainClass>
8 </configuration>
9 <executions>
10 <execution>
11 <goals>
12 <goal>repackage</goal>
13 </goals>
14 </execution>
15 </executions>
16 </plugin>
17 </plugins>
18 <resources>
19 <resource>
20 <directory>../ik-demo-config/resources</directory>
21 </resource>
22 <resource>
23 <directory>src/main/resources</directory>
24 </resource>
25 </resources>
26</build>
对应的Configuration
构造,差异在于inIdea
中的路径配置
1@Bean
2public Configuration ikConfiguration() {
3 String path = System.getProperty("user.dir");
4 // 仅在idea中实时调试需要,与config所在的目录必须一致,此处为ik-demo-config/resources
5 if (!Kit.runningAsJar) {
6 path += "/ik-demo-config/resources";
7 }
8 Environment environment = new Environment(Settings.builder().put("path.home", path).build(), null);
9 Settings settings = Settings.builder()
10 .put("use_smart", false)
11 .put("enable_lowercase", false)
12 .put("enable_remote_dict", false)
13 .build();
14 return new Configuration(environment, settings).setUseSmart(false);
15}
需要注意的是,在
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├── ik-demo-config
2│ ├── resources
3│ │ └── config
4│ │ ├── analysis-ik
5│ │ │ ├── IKAnalyzer.cfg.xml
6│ │ │ ├── extra_main.dic
7│ │ │ ├── extra_single_word.dic
8│ │ │ ├── extra_single_word_full.dic
9│ │ │ ├── extra_single_word_low_freq.dic
10│ │ │ ├── extra_stopword.dic
11│ │ │ ├── main.dic
12│ │ │ ├── preposition.dic
13│ │ │ ├── quantifier.dic
14│ │ │ ├── stopword.dic
15│ │ │ ├── suffix.dic
16│ │ │ └── surname.dic
17│ │ ├── application-dev.yml
18│ │ ├── application-prod.yml
19│ │ ├── application-test.yml
20│ │ └── 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.ymlik-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 * App
3 *
4 * @author Qicz
5 */
6@SpringBootApplication
7@EnableExtension
8public class App {
9
10 public static void main(String[] args) throws InterruptedException {
11 SpringApplicationX.run(App.class, args);
12 }
13}