Plusaber's Blog

  • Home

  • Tags

  • Categories

  • Archives

Maven(1)_Maven基础

Posted on 2014-11-05 | In java | Comments:

参考Maven入门指南,Maven Getting Started Guide,Maven Tutorial

Maven主要为开发者提供了一个可复用、可维护的工程模型,以及与基于这个模型的构建工具和一系列插件。

构建工具是将软件项目构建相关的过程自动化的工具。构建一个软件项目通常包含以下一个或多个过程:

  • 生成源码(如果项目使用自动生成源码);
  • 从源码生成项目文档;
  • 编译源码;
  • 将编译后的代码打包成JAR文件或者ZIP文件;
  • 将打包好的代码安装到服务器、仓库或者其它的地方;

有些项目可能需要更多的过程才能完成构建,这些过程一般也可以整合到构建工具中,因此它们也可以实现自动化。

自动化构建过程的好处是将手动构建过程中犯错的风险降到最低。而且,自动构建工具通常要比手动执行同样的构建过程要快。

核心概念

pom.xml

Maven工程结构和内容被定义在一个xml文件中 - pom.xml,也就是project object model,此文件是整个Maven系统的基础组件,描述了项目的资源,源码,测试代码,依赖等。pom.xml位于项目的根目录下。

在执行一条maven命令时,maven会读取项目的pom.xml文件,并根据pom.xml的相关内容执行命令。

构建生命周期、阶段和目标

Maven的构建过程从大到小被分为构建生命周期、阶段和目标,其关系为:

  • 一个生命周期包含若干个阶段
  • 一个阶段包含若干个目标

一般maven使用的命令形式为:mvn command,其中command命令就是构建生命周期、阶段或者目标的名字。如果command为一个生命周期,该生命周期的所有构建阶段都会被执行。如果command为一个生命周期具体的一个构建阶段,那么这个生命周期中,所有处于目标执行阶段之前的阶段以及该目标阶段都会被执行。

目标操作(goal)表示某个有助于项目构建和管理的特定的任务。

  • 一个目标操作可以绑定0到多个构建阶段。
  • 不绑定任何构建阶段的目标操作可以通过直接调用的方式执行。

执行的顺序取决于目标操作和构建阶段调用的次序。以下面的命令为例,参数clean和package是构建阶段而dependency:copy-dependencies是一个目标操作。

mvn clean dependency:copy-dependencies package

在这里,clean阶段会首先执行,然后是执行dependency:copy-dependencies目标操作,最后是执行package阶段。

依赖和仓库

一个大型项目一般都会需要依赖许多外部包,也就是这个项目的依赖。Maven设置了一个本地仓库,所有依赖都会被放在本地仓库(本地的一个目录)中。如果在本地仓库中不存在该依赖,则Maven会从中央仓库下载并放到本地仓库。

插件

Maven提供了一系列标准的构建生命周期和构建阶段、目标,如果仍无法满足项目构建的要求,可以使用查找并使用插件来满足需要。

Maven实际上是一个执行插件的框架,其所有的任务其实都是由插件完成。Maven插件通常用于:

  • 生成jar包文件
  • 生成war包文件
  • 编译源码文件
  • 代码单元测试
  • 生成项目文档
  • 生成项目报告

一个插件通常提供一系列的目标操作,并且目标操作可以通过以下格式的命令执行:
mvn [插件名]:[目标操作名]
例如,一个Java项目可以通过运行下面的命令使用maven-compiler-plugin的compile目标操作编译。
mvn compiler:compile

另外插件目标也可以与绑定到构建阶段上(可以是现有的构建阶段)。这样在执行该构建阶段时,所绑定的目标也会一起被执行。

例如在执行构建阶段时如果需要输出信息,可以使用maven-antrun-plugin插件来打印数据到控制台。

创建下面的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
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.companyname.projectgroup</groupId>
<artifactId>project</artifactId>
<version>1.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<id>id.clean</id>
<phase>clean</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>clean phase</echo>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

执行mvn clean.

Maven将会开始处理并且输出clean生命周期的clean阶段的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------
[INFO] Building Unnamed - com.companyname.projectgroup:project:jar:1.0
[INFO] task-segment: [post-clean]
[INFO] ------------------------------------------------------------------
[INFO] [clean:clean {execution: default-clean}]
[INFO] [antrun:run {execution: id.clean}]
[INFO] Executing tasks
[echo] clean phase
[INFO] Executed tasks
[INFO] ------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------
[INFO] Total time: < 1 second
[INFO] Finished at: Sat Jul 07 13:38:59 IST 2012
[INFO] Final Memory: 4M/44M
[INFO] ------------------------------------------------------------------

上面的例子中阐明了下面几个关键概念:

  • 插件在pom.xml文件中是通过plugins节点明确指定的。
  • 每个插件可以有多个目标操作。
  • 你可以使用phase节点来定义插件从哪个节点开始处理。我们已使用的是clean阶段。
  • 你可以通过绑定任务到插件的目标操作来配置要执行的任务。我们已经绑定了echo任务到maven-antrun-plugin插件的run目标操作。
  • 以上,Maven会处理剩下的事情。若本地仓库中找不到,Maven会下载插件,并且开始处理。

Maven提供了下面两类插件:

类型 描述
构建插件(Build plugins) 这类插件在构建过程中执行,并且应该配置在pom.xml文件的节点中。
报告插件(Reporting plugins) 这类插件在生成站点过程中执行,并且应该配置在pom.xml文件的节点中。

下面是一些常用的插件的列表:

插件 描述
clean 构建完成后清理目标,删除目标目录。
compiler 编译Java源文件。
surefile 运行JUnit单元测试,生成测试报告。
jar 从当前项目生成JAR文件。
war 从当前项目生成WAR文件。
javadoc 生成项目的Javadoc。
antrun 运行任意指定构建阶段的一系列ant任务。

配置文件

配置文件用于以不同的方式构建项目。比如可能需要在本地环境构建,用于开发和测试,也可能需要构建后用于开发环境。具体可以配置的一些内容包括:

  • 本地仓库的路径
  • 当前编译配置选项等

maven设置了两个配置文件,分别在:

  • maven安装目录下: $M2_HOME/conf/settings.xml
  • 用户主目录中: ${user.home}/.m2/settings.xml

如果两个文件都存在,则用户目录的配置会覆盖安装目下的配置。

在POM文件中增加不同的构建配置,可以启用不同的构建过程。当运行Maven时,可以指定要使用的配置。

POM

POM意为项目对象模型(Project Object Model),是Maven中基本单元。它是一个名为pom.xml的XML文件,总是存在于项目的更目录下。

POM包含了项目相关的信息和Maven用来构建一个或多个项目的各种配置详情。POM文件描述的是构建“什么”,而不是“如何”构建。如何构建是取决于Maven的构建阶段和目标。当然,如果需要,你也可以向Maven构建阶段中添加自定义的目标。

每一个项目都有一个POM文件。POM文件即pom.xml,应该放在项目的根目录下。一个项目如果分为多个子项目,一般来讲,父项目有一个POM文件,每一个子项目都有一个POM文件。在这种结构下,既可以一步构建整个项目,也可以各个子项目分开构建。

POM也包含了各种目标操作(goal)和插件。当执行一个任务或者目标操作时,Maven会查找当前目录下的POM。Maven读取POM,从中获得需要的配置信息,然后执行目标操作。部分Maven可以从POM中明确的配置列出如下:

  • 项目依赖(project dependencies)
  • 插件(plugins)
  • 目标操作(goals)
  • 构建(build profiles)
  • 项目版本(project version)
  • 开发者(developers)
  • 邮件列表(mailing list)

一个最基本的POM文件如下:

1
2
3
4
5
6
7
8
9
10
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>
</project>

groupID表示开发者或者开发公司的唯一ID,一般使用java包的根名作为groupID。artifactId一般是项目名。version指项目版本。

注意,项目在maven仓库的结构化目中,该结构化目录会与groupID匹配,每一个.会成为目录分隔符,每个词都表示一个目录。如groupID为com.plusaber项目将位于MAVEN_REPO/com/plusaber。类似的,artifactId也会成为MAVEN_REPO/com/plusaber的子目录,另外artifactId也是构建完项目后生成的jar包的文件名的一部分。

  • 父pom

所有的Maven pom文件都继承自一个父pom。如果没有指定父pom,则该pom文件继承自根pom。

可以在pom文件指定父pom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<project xmlns=”http://maven.apache.org/POM/4.0.0″
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd”>
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.codehaus.mojo</groupId>
<artifactId>my-parent</artifactId>
<version>2.0</version>
<relativePath>../my-parent</relativePath>
</parent>

<artifactId>my-project</artifactId>
...

子pom的配置会覆盖父pom的配置,由于继承和覆盖的原因,无法直接通过查看子pom和父pom了解最后的有效pom。这时可以使用mvn help:effective-pom打印出当前的有效pom文件。

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<?xml version="1.0" encoding="UTF-8"?>
<!-- ================================================================= -->
<!-- -->
<!-- Generated by Maven Help Plugin on 2012-07-05T11:41:51 -->
<!-- See: http://maven.apache.org/plugins/maven-help-plugin/ -->
<!-- -->
<!-- ================================================================= -->

<!-- ================================================================= -->
<!-- -->
<!-- Effective POM for project -->
<!-- 'com.companyname.project-group:project-name:jar:1.0' -->
<!-- -->
<!-- ================================================================= -->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/
2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 h
ttp://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.companyname.project-group</groupId>
<artifactId>project</artifactId>
<version>1.0</version>
<build>
<sourceDirectory>C:\MVN\project\src\main\java</sourceDirectory>
<scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>C:\MVN\project\src\test\java</testSourceDirectory>
<outputDirectory>C:\MVN\project\target\classes</outputDirectory>
<testOutputDirectory>C:\MVN\project\target\test-classes</testOutputDirectory>
<resources>
<resource>
<mergeId>resource-0</mergeId>
<directory>C:\MVN\project\src\main\resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<mergeId>resource-1</mergeId>
<directory>C:\MVN\project\src\test\resources</directory>
</testResource>
</testResources>
<directory>C:\MVN\project\target</directory>
<finalName>project-1.0</finalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-2</version>
</plugin>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.0</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.4</version>
</plugin>
<plugin>
<artifactId>maven-ear-plugin</artifactId>
<version>2.3.1</version>
</plugin>
<plugin>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.1</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.5</version>
</plugin>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>2.4.3</version>
</plugin>
<plugin>
<artifactId>maven-rar-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>2.0-beta-8</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.3</version>
</plugin>
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>2.0-beta-7</version>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>2.0.4</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4.3</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.1-alpha-2</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<artifactId>maven-help-plugin</artifactId>
<version>2.1.1</version>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>Maven Repository Switchboard</name>
<url>http://repo1.maven.org/maven2</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>Maven Plugin Repository</name>
<url>http://repo1.maven.org/maven2</url>
</pluginRepository>
</pluginRepositories>
<reporting>
<outputDirectory>C:\MVN\project\target/site</outputDirectory>
</reporting>
</project>

在上面的完整的effective pom.xml中,你可以看到项目构建的所有信息,包括默认的源文件目录结构,输出路径,需要的插件,报告目录,这些信息Maven在执行期望的目标操作时都会用到。

Maven的pom.xml在大部分情况要求手动写。Maven提供了很多archetype插件用来创建项目以及按规则创建项目结构和pom.xml文件。

Maven命令

mvn command,其中command命令就是构建生命周期、阶段或者目标的名字。如果command为一个生命周期,该生命周期的所有构建阶段都会被执行。如果command为一个生命周期具体的一个构建阶段,那么这个生命周期中,所有处于目标执行阶段之前的阶段以及该目标阶段都会被执行。
mvn install
install是默认生命周期的一个阶段,具体任务包括编译项目,将打包的jar文件复制到本地仓库。事实上,在执行install之前,默认生命周期位于install的所有阶段都被执行。

我们可以向mvn命令传入多个参数,执行多个构建周期或阶段,如:
mvn clean install
clean是一个生命周期(简称周期),具体任务是删除maven输入目录中已编译的类文件,然后执行install构建阶段。一个一个顺序执行,互相没有影响。

也可以执行一个maven目标,这时参数需要将构建阶段与目标名以冒号相连。
mvn dependency:copy-dependencies,执行dependency构建阶段中的copy-dependencies目标。

Maven目录结构

maven的一个主要原则就是约定优先,maven设置了一个标准的目录结构,如果在项目中遵循Maven的目录结构,就无需在pom文件中指定源码测试代码等目录。

dir/file usage
src/main/java Application/Library sources
src/main/resources Application/Library resources
src/main/filters Resource filter files
src/main/webapp Web application sources
src/test/java Test sources
src/test/resources Test resources
src/test/filters Test resource filter files
src/it Integration Tests (primarily for plugins)
src/assembly Assembly descriptors
src/site Site
LICENSE.txt Project’s license
NOTICE.txt Notices and attributions required by libraries that the project depends on
README.txt Project’s readme

依赖

项目依赖

Maven提供了依赖管理的功能。你只需要在pom文件里指定依赖jar包的名称、版本号,Maven会自动下载并放到你的Maven本地仓库中。如果这些外部jar包依赖了其它的库,它们也会被下载到你的Maven本地仓库。

可以通过在pom文件添加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
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>
</project>
1
2
MAVEN_REPOSITORY_ROOT/junit/junit/4.11
MAVEN_REPOSITORY_ROOT/org/jsoup/jsoup/1.7.1

可以看到,每一个依赖也是由groupID,artifactId和version来描述,和pom文件开头用来标识项目的方式是一样的。

执行这个pom文件时,如果本地仓库已经有了这两个依赖,maven就不会去中央仓库下载(本地仓库的依赖可以被多个项目使用)。如果没有,maven则会去中央仓库寻找这些依赖并下载到本地仓库。

需要注意的是,有时候指定的依赖在maven的中央仓库里没有,这是我们需要手动下载这些依赖并放到mavne的本地仓库。这些依赖必须放到与groupId, artifactId和version匹配的字目录中,用/替换.并为每个字段创建相应目录。

外部依赖

Maven的外部依赖指的是不在Maven的仓库(包括本地仓库、中央仓库和远程仓库)中的依赖(jar包)。它可能位于你本地硬盘的某个地方,比如web应用的lib目录下。

可以配置外部依赖如下:

1
2
3
4
5
6
7
<dependency>
<groupId>mydependency</groupId>
<artifactId>mydependency</artifactId>
<scope>system</scope>
<version>1.0</version>
<systemPath>${basedir}\war\WEB-INF\lib\mydependency.jar</systemPath>
</dependency>

groupId和artifactId为依赖的名称,即API的名称。scope属性为system。systemPath属性为jar文件的路径。${basedir}为pom文件所在的目录,路径中的其它部分是相对于该目录而言的。

快照依赖

快照依赖指的是那些还在开发中的依赖(jar包)。与其经常地更新版本号来获取最新版本,不如你直接依赖项目的快照版本。快照版本的每一个build版本都会被下载到本地仓库,即使该快照版本已经在本地仓库了。总是下载快照依赖可以确保本地仓库中的每一个build版本都是最新的。

在pom文件的最开头(设置groupId和artifactId的地方),在版本号后追加-SNAPSHOT,则告诉Maven你的项目是一个快照版本。如:

<version>1.0-SNAPSHOT</version>

在配置依赖时,在版本号后追加-SNAPSHOT表明依赖的是一个快照版本。如:

1
2
3
4
5
<dependency>
<groupId>com.jenkov</groupId>
<artifactId>java-web-crawler</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

追加在version后的-SNAPSHOT告诉Maven这是一个快照版本。

Maven仓库

Maven仓库就是存储jar包和一些元数据信息的目录,一般是用于根目录下的.m2文件夹,比如/Users/admin/.m2。其中的元数据即pom文件,描述了该jar包属于哪个项目,以及jar包所需的外部依赖。该元数据信息使得Maven可以递归地下载所有的依赖,直到整个依赖树都下载完毕并放到你的本地仓库中。参考Introduction to Repositories。

Maven有三种类型的仓库:

  • 本地仓库
  • 中央仓库
  • 远程仓库

Maven根据以上的顺序去仓库中搜索依赖。首先是本地仓库,然后是中央仓库,最后,如果pom文件中配置了远程仓库,则会去远程仓库中查找。

本地仓库

本地仓库就是开发者电脑上的一个目录。该仓库包含了Maven下载的所有依赖。一般来讲,一个本地仓库为多个不同的项目服务。因此,Maven只需下载一次,即使有多个项目都依赖它(如junit)。

通过mvn install命令可以将你自己的项目构建并安装到本地仓库中。这样,你的其它项目就可以通过在pom文件将该jar包作为外部依赖来使用。

Maven的本地仓库默认在你本机的用户目录下。不过,你可以在Maven的配置文件中修改本地仓库的路径。Maven的配置文件也在用户目录下(user-home/.m2),文件名为settings.xml。以下示例为本地仓库指定其它的路径:

1
2
3
4
5
<settings>
<localRepository>
d:\data\java\products\maven\repository
</localRepository>
</settings>

中央仓库

Maven的中央仓库由Maven社区提供。默认情况下,所有不在本地仓库中的依赖都会去这个中央仓库查找。然后Maven会将这些依赖下载到你的本地仓库。访问中央仓库不需要做额外的配置。

远程仓库

远程仓库是位于web服务器上的一个仓库,Maven可以从该仓库下载依赖,就像从中央仓库下载依赖一样。远程仓库可以位于Internet上的任何地方,也可以是位于本地网络中。

远程仓库一般用于放置组织内部的项目,该项目由多个项目共享。比如,由多个内部项目共用的安全项目。该安全项目不能被外部访问,因此不能放在公开的中央仓库下,而应该放到内部的远程仓库中。

远程仓库中的依赖也会被Maven下载到本地仓库中。可以在pom文件里配置远程仓库。将以下的xml片段放到属性之后:

1
2
3
4
5
6
<repositories>
<repository>
<id>jenkov.code</id>
<url>http://maven.jenkov.com/maven2/lib</url>
</repository>
</repositories>

Maven的构建生命周期、阶段和目标

当使用Maven构建项目时,会遵循一个构建生命周期。该生命周期分为多个构建阶段,而构建阶段又分为多个构建目标。参考Introduction to the Build Lifecycle

构建生命周期

Maven有三个内嵌的构建生命周期:clean, default, site.

每一个构建生命期关注项目构建的不同方面。因此,它们是独立地执行的。Maven可以执行多个生命期,但是它们是串行执行的,相互独立,就像你执行了多条独立的Maven命令。

Clean生命周期

当我们执行mvn的post-clean命令时,Maven调用clean生命周期,它包含以下阶段:

  • pre-clean
  • clean
  • post-clean

Maven的clean目标操作(clean:clean)在clean生命周期中被绑定到clean阶段。clean:clean 目标操作通过删除构建目录来删除一个项目构建的输出文件。这样,当mvn clean命令执行时,Maven会删除构建目录。

default生命周期

构建生命周期是Maven的最主要的生命周期,用来构建应用。default生命期关注的是项目的编译和打包。clean生命期关注的是从输出目录中删掉临时文件,包括自动生成的源文件、编译后的类文件,之前版本的jar文件等。site生命期关注的是为项目生成文档。实际上,site可以使用文档为项目生成一个完整的网站。

构建阶段

每一个构建生命期被分为一系列的构建阶段,构建阶段又被分为构建目标。因此,整个构建过程由一系列的构建生命期、构建阶段和构建目标组成。

你可以执行一个构建生命期,如clean或site,一个构建阶段,如default生命期的install,或者一个构建目标,如dependency:copy-dependencies。注意:你不能直接执行default生命期,你需要指定default生命期中的一个构建阶段或者构建目标。

当你执行一个构建阶段时,所有在该构建阶段之前的构建阶段(根据标准构建顺序)都会被执行。因此,执行install阶段,意味着所有位于install阶段前的构建阶段都会被执行,然后才执行install阶段。

default生命期更多的关注于构建代码。由于你不能直接执行default生命期,你需要执行其中一个构建阶段或者构建目标。default生命期包含了相当多的构建阶段和目标,这里不会所有都介绍。最常用的构建阶段有:

  • validate - validate the project is correct and all necessary information is available
  • compile - compile the source code of the project
  • test - test the compiled source code using a suitable unit * testing framework. These tests should not require the code be packaged or deployed
  • package - take the compiled code and package it in its distributable format, such as a JAR.
  • integration-test - process and deploy the package if necessary into an environment where integration tests can be run
  • verify - run any checks to verify the package is valid and meets quality criteria
  • install - install the package into the local repository, for use as a dependency in other projects locally
  • deploy - done in an integration or release environment, copies the final package to the remote repository for sharing with other developers and projects.

完整的default周期的阶段:

生命周期阶段 描述
validate(校验) 校验项目是否正确并且所有必要的信息可以完成项目的构建过程。
initialize(初始化) 初始化构建状态,比如设置属性值。
generate-sources(生成源代码) 生成包含在编译阶段中的任何源代码。
process-sources(处理源代码) 处理源代码,比如说,过滤任意值。
generate-resources(生成资源文件) 生成将会包含在项目包中的资源文件。
process-resources (处理资源文件) 复制和处理资源到目标目录,为打包阶段最好准备。
compile(编译) 编译项目的源代码。
process-classes(处理类文件) 处理编译生成的文件,比如说对Java class文件做字节码改善优化。
generate-test-sources(生成测试源代码) 生成包含在编译阶段中的任何测试源代码。
process-test-sources(处理测试源代码) 处理测试源代码,比如说,过滤任意值。
test-compile(编译测试源码) 编译测试源代码到测试目标目录.
process-test-classes(处理测试类文件) 处理测试源码编译生成的文件。
test(测试) 使用合适的单元测试框架运行测试(Juint是其中之一)。
prepare-package(准备打包) 在实际打包之前,执行任何的必要的操作为打包做准备。
package(打包) 将编译后的代码打包成可分发格式的文件,比如JAR、WAR或者EAR文件。
pre-integration-test(集成测试前) 在执行集成测试前进行必要的动作。比如说,搭建需要的环境。
integration-test(集成测试) 处理和部署项目到可以运行集成测试环境中。
post-integration-test(集成测试后) 在执行集成测试完成后进行必要的动作。比如说,清理集成测试环境。
verify (验证) 运行任意的检查来验证项目包有效且达到质量标准。
install(安装) 安装项目包到本地仓库,这样项目包可以用作其他本地项目的依赖。
deploy(部署) 将最终的项目包复制到远程仓库中与其他开发者和项目共享。

构建目标

构建目标是Maven构建过程中最细化的步骤。一个目标可以与一个或多个构建阶段绑定,也可以不绑定。如果一个目标没有与任何构建阶段绑定,你只能将该目标的名称作为参数传递给mvn命令来执行它。如果一个目标绑定到多个构建阶段,该目标在绑定的构建阶段执行的同时被执行。

Maven构建配置

Maven构建配置使你能使用不同的配置来构建项目。不用创建两个独立的pom文件。你只需使用不同的构建配置指定不同的配置文件,然后使用该配置文件构建项目即可。

关于构建配置的详细信息可以参考Maven POM参考的Profile部分。这里是一个快速的介绍。

Maven的构建配置在pom文件的profiles属性中指定,例如:

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
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.jenkov.crawler</groupId>
<artifactId>java-web-crawler</artifactId>
<version>1.0.0</version>

<profiles>
<profile>
<id>test</id>
<activation>...</activation>
<build>...</build>
<modules>...</modules>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<dependencies>...</dependencies>
<reporting>...</reporting>
<dependencyManagement>...</dependencyManagement>
<distributionManagement>...</distributionManagement>
</profile>
</profiles>

</project>

构建配置描述的是当使用该配置构建项目时,对pom文件所做的修改,比如修改应用使用的配置文件等。profile属性中的值将会覆盖其上层的、位于pom文件中的配置。

在profile属性中,有一个activation子属性。该属性指定了启用该构建配置的条件。选择构建配置的一种方式是在settings.xml文件中指定;另一种方式是在Maven命令行使用-P profile-name指定。更多细节参考构建配置的文档。

Maven插件

使用Maven插件,可以向构建过程添加自定义的动作。创建一个简单的Java类,该类继承一个特殊的Maven类,然后为项目创建一个pom文件。该插件应该位于其项目下。可以参考Plugin Developers Centre。

Observer pattern

Posted on 2014-11-02 | In Design Pattern | Comments:

观察者模式定义了对象之间的一对多依赖,观察者依赖于此主体,只要主体状态有变化,观察者就会被通知。

Design_pattern_observer_1

一种基于Subject和Observer接口的实现:

Design_pattern_observer_2

观察者模式提供了一种对象设计,让主题和观察者之间实现松耦合。

气象站的观察者模式设计:

Design_pattern_observer_3

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}

public interface Observer {
public void update(float temp, float humidity, float pressure);
}

public interface DisplayElement {
public void display();
}
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
public class WeatherData implements Subject {
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;

public WeatherData() {
observers = new ArrayList();
}

public void registerObserver(Observer o) {
observers.add(o);
}

public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}

public void notifyObservers() {
for (int i = 0; i < observers.size(); i++) {
Observer observer = (Observer)observers.get(i);
observer.update(temperature, humidity, pressure);
}
}

public void measurementsChanged() {
notifyObservers();
}

public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}

// other WeatherData methods here

public float getTemperature() {
return temperature;
}

public float getHumidity() {
return humidity;
}

public float getPressure() {
return pressure;
}
}

同样,观察者模式也有缺点,观察者无法主动获取所需要的数据,而是由主题对象一次推送所有数据,在某些情况并不合适,后面介绍的模式将解决这个问题。

另外Java内置有观察者模式,但是是基于抽象类,而不是接口的实现,在很多时候并不适用。在JDK中,Swing的JButton等组件加入Listener即是使用了观察者模式,当按扭事件被触发时,所有listener将会得到通知并做出相应行为。

Strategy Pattern

Posted on 2014-10-10 | In Design Pattern | Comments:

假设我们要实现鸭子对象及其行为,假设鸭子行为有鸣叫,颜色,飞行等。

Several method

With Extending

很自然的做法是定义一个鸭子父类,然后定义特定鸭子的子类。

由于不同的鸭子有不同的鸣叫,颜色,飞行等行为,如果把这些行为都放在鸭子父类中,则会导致部分不具有这些行为的鸭子也变得具有这些行为,一种弥补方法是对于这些行为什么都不做,但是会导致代码僵化,如果需要改动则需要修改所有鸭子的代码。

With Interface

  • 另一种实现方法是使用接口,把会飞,会叫这些行为各自独立抽象出接口,对于需要具有这些行为的鸭子,使这些鸭子实现相应的接口即可。看起来很不错,但是一个问题是,接口不具有实现代码,也就是无法达到代码的复用,如果某种功能需要更改,比如飞行行为。在这个问题下,如果许多鸭子其实是具有相同的飞行行为的,如果需要进行调整,则同样需要修改所有这些鸭子的飞行行为。

Strategy mode

这里问题的关键是:没有分离出变化部分和稳定部分。一个设计原则是:

找出应用中可能需要变化之处,把它们独立出来,不要和哪些不需要变化的代码混在一起。

  • 根据上面的分析,我们应该把变化的部分和不变的不变的部分区分开来。

假设在这个问题下fly()和quack()行为是会随着鸭子的不同而改变的,但并不是每种鸭子的行为都是不同功能的,部分鸭子会共享某种飞行行为或quack行为。这里我们不使用接口来抽象这些行为,因为前面提到,部分鸭子会共享某种飞行行为或quack行为,使用抽象则会导致无法复用这些代码。这里我们分别建立一组抽象来代表每个行为(可以为接口)。

然后对于几种不同的行为,分别实现具体类继承定义的抽象behavior。然后在鸭子类引用这些类的实例,通过调用这些实例的实现方法来实现鸭子的相应行为。也就是具体行为的实现是在这些具体行为类中实现的,鸭子只是调用这些实现方法来实现相应行为。

上面解释的过于抽象,简单说来关键在于:
鸭子现在将飞行和鸣叫的动作『委托』别人处理(独立的behavior接口),而不是定义在鸭子类内部的飞行方法或鸣叫方法(仍然会有相关方法,但是其内容就是简单的引用behavior类的具体实现)。

1
2
3
4
5
6
7
public class Duck {
FlyBehaviour flyb;

public void performFly() {
flyb.fly(); // 委托给行为类
}
}
1
2
3
4
5
public class MallarDuck extends Duck {
public MalllardDuck() {
flyb = new HighFlyBehavior();
}
}
1
2
3
4
5
public class NanoDuck extends Duck {
public MalllardDuck() {
flyb = new SpaceFlyBehavior();
}
}

另外,在鸭子类中应用behavior类时,需要注意:
针对接口编程,而不是针对具体实现编程。

也就是我们在鸭子类中的依赖应该是抽象behavior类引用,而不是behavior的具体实现类,否则会造成鸭子类behavior限定于某个实现类,不利于后面的改变扩展。

  • 进一步增强,实现动态设定行为。

前面我们通过在鸭子类内部引用behaviour类来完成具体行为,具体是在鸭子类构造函数内完成behaviour的实例化。由于具体行为是由behaviour类的实例完成,这也就是给我们提供了动态改变鸭子实例的行为,比如通过setFlyBehaviour(FlyBehavior fb)方法设定新的行为实现类,我们便可以在运行时改变鸭子的行为。

1
2
3
4
5
6
7
8
9
10
11
public class Duck {
FlyBehaviour flyb;

public void performFly() {
flyb.fly();
}

public void setFlyBehaviour(FlyBehavior fb) {
flyb = fb;
}
}

Summary

很多时候”有一个”可能比”是一个”更好,这也是一个重要的设计原则:
多用组合,少用继承。
前面提到的还有:
找出应用中可能需要变化之处,把它们独立出来,不要和哪些不需要变化的代码混在一起。
针对接口编程,而不是针对具体实现编程。

使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以在运行时动态的改变行为,只要组合的行为对象符合正确的接口标准即可。

Strategy Pattern

前面使用的模式就是策略模式。策略模式定义了算法族,并分别封装起来,让它们之间可以互相替换。此模式让算法的变化独立于使用算法的类。

Lucene(4)_Document analysis

Posted on 2014-10-01 | In Developing | Comments:

Analysis是指将field的内容转换为最基本的索引表示单元—项(Term),也就是token+field名。分析器对分析操作进行了封装,提供一系列操作将文本转换为tokens,可能包括:提取单词、取出标点符号、转为小写、去除停用词、词干还原等。这个过程称为tokenizaiton,从文本提取token,与其域名(field)结合后,就形成了项(term)。

使用Lucene时,选择一个合适的分析器是非常关键的,决定于语言、行业等。Lucene提供了许多内置分析器可以满足一般需要,同时如果我们需要自定义分析器,Lucene的构建模块也可以使得这一过程变得简单。

使用分析器(analyzers)

分析操作将出现在任何需要将文本转换为Term的时候,对于Lucene核心来说,主要包括两个过程:建立索引期间和使用QueryParser对象进行搜索时。

下面是使用4个内置分析器分别分析两个短语,让我们先对分析操作有一个直观的认识。

1
2
3
4
5
6
7
8
9
Analyzing "The quick brown fox jumped over the lazy dog"
WhitespaceAnalyzer:
[The] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dog]
SimpleAnalyzer:
[the] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dog]
StopAnalyzer:
[quick] [brown] [fox] [jumped] [over] [lazy] [dog]
StandardAnalyzer:
[quick] [brown] [fox] [jumped] [over] [lazy] [dog]
1
2
3
4
5
6
7
8
9
Analyzing "XY&Z Corporation - xyz@example.com"
WhitespaceAnalyzer:
[XY&Z] [Corporation] [-] [xyz@example.com]
SimpleAnalyzer:
[xy] [z] [corporation] [xyz] [example] [com]
StopAnalyzer:
[xy] [z] [corporation] [xyz] [example] [com]
StandardAnalyzer:
[xy&z] [corporation] [xyz@example.com]

可以看出分析结果中的词汇单元取决于对应的分析器。

  • WhitespaceAnalyzer,该分析器仅仅功过空格来分割文本信息,并不对生成的token进行其他处理。
  • SimpleAnalyzer,通过非字母字符来分割文本信息,然后统一为小写形式。
  • StopAnalyzer,在上面的基础上去除停用词。
  • StandardAnalyzer,这是Lucene最复杂的核心分析器,包含大量的逻辑操作来之别某些种类的token,比如公司名称,实体,e-mail等。同样会将token转换为小写形式并去除停用词和标点符号。

索引过程的analysis

在索引期间,文档field的内容需要被转换为token,用于indexing。

1
2
3
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
IndexWriter writer = new IndexWriter(directory, analyzer,
IndexWriter.MaxFieldLength.UNLIMITED);

Lucene_5

通常需要实例化一个Analyzer对象,然后将其传递给IndexWriter对象。如果没有指定则会使用默认的分析器。另外如果某个文档需要特殊的分析器处理的话,在addDocument和updateDocument时也可以指定分析器。

为了确保文本信息被分析器处理,可以在创建field时指定Field.Index.ANALYZED或Field.Index.ANLYZED_NO_NORMS参数。如果需要将整个field内容作为一个token处理(也就是不需要tokenlization),可以设置为Field.Index.NOT_ANALYZED或Field.Index.NOT_NO_ANALYZED。

1
2
3
4
5
6
7
8
9
10
11
12
new Field(String, String, Field.Store.YES, Field.Index.ANALYZED) 
//creates a tokenized and stored field. Rest assured the original
//String value is stored. But the output of the designated
//Analyzer dictates what’s indexed and available for searching.

//The following code demonstrates indexing of a document where
//one field is analyzed and stored, and the second field is
//analyzed but not stored:

Document doc = new Document();
doc.add(new Field("title", "This is the title",
Field.Store.YES, Field.Index.ANALYZED));

QueryParser分析

QueryParser同样需要使用analyzer将query分解为各个term。分析器会接受queyr expression的连续的独立文本片段,但不会接收整个表达式。如:
"president obama" +harvard +professor
QueryParser会调用三次分析器,分别处理"president obama",harvard,professor。

What’s inside an analyzer?

Analyzer是一个抽象类,是所有分析器的基类。只需要实现一个抽象方法,将text转换为TokenStream实例。

1
public TokenStream tokenStream(String fieldName, Reader reader)

TokenStream用于循环遍历所有terms。

下面是简单的SimpleAnalyzer类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class SimpleAnalyzer extends Analyzer {
@Override
public TokenStream tokenStream(String fieldName, Reader reader) {
return new LowerCaseTokenizer(reader);
}
@Override
public TokenStream reusableTokenStream(String fieldName, Reader reader
throws IOException {
Tokenizer tokenizer = (Tokenizer) getPreviousTokenStream();
if (tokenizer == null) {
tokenizer = new LowerCaseTokenizer(reader);
setPreviousTokenStream(tokenizer);
} else
tokenizer.reset(reader);
return tokenizer;
} }

LowerCasetokenizer对象根据文本中的非字母字符来分割文本,并将所有字母换成小写形式。resusableTokenStream()方法是一个可选方法,分析器可以实现这个方法实现更好的效率,因为这个方法实现了重复利用前面线程创建的TokenStream。

Token

token是分析过程中产生的基本单元。一个token带有其text值已经其他一些元数据,例如起始位置终止位置偏移量、类型等。文本被tokenlize之后,每个token的位置信息都是相对于前面一个token的位置的增量值进行保存,表示所有token都是连续的。

在tokenlize之后,每个token连接field形成term被传递给索引。位置增量(position increment)、起点(start)和终点偏移量(end offset)和有效负载(payload)是token一起附带到索引的元数据。

位置增量使得当前token和前一个token在位置上关联起来。一般来说位置增量为1,表示每个token存在于field中唯一且连续的位置上。位置增量因子会直接影响短语查询(phrase queries)和跨度查询(span queries),因为这些查询需要知道field中各个term之间的距离。

如果位置增量大于1,则允许token之间有空隙,可以用这个空隙来表示被删除的单词。
位置增量为0的token表示该token放置与与前一个token相同的位置上。同义词分析器可以通过0增量来表示插入的同义词。

分析TokenStream

TokenStream是一个能在被调用后产生token序列的类,TokenStream类有两种不同类型:Tokenizer类和TokenFilter类。注意TokenFilter类可以封装另一个TokenStream抽象类对象。

Tokenizer对象通过java.io.Reaser对象读取字符并创建token,而TokenFilter则负责处理输入的token,然后通过新增、删除或修改属性的方式来产生新的token。

当分析器从它的tokenStream方法或者reusableTokenStream方法返回tokenStream对象后,就可以用一个tokenizer对象创建tokens序列,然后再链接任意数量的tokenFilter对象来对这些tokens进行修改。这被称为分析器链(analyzer chain)。

下面是Lucene的核心Tokenizer类和TokenFilter类。

Class name Description
TokenStream Abstract Tokenizer base class.
Tokenizer TokenStream whose input is a Reader.
CharTokenizer Parent class of character-based tokenizers, with abstract isTokenChar() method. Emits tokens for contiguous blocks when isTokenChar() returns true. Also provides the capability to normalize (for example, lowercase) characters. Tokens are limited to a maximum size of 255 characters.
WhitespaceTokenizer CharTokenizer with isTokenChar() true for all nonwhitespace characters.
KeywordTokenizer Tokenizes the entire input string as a single token.
LetterTokenizer Tokenizes the entire input string as a single token. CharTokenizer with isTokenChar() true when Character.isLetter is true.
LowerCaseTokenizer LetterTokenizer that normalizes all characters to lowercase.
SinkTokenizer A Tokenizer that absorbs tokens, caches them in a private list, and can later iterate over the tokens it had previously cached. This is used in conjunction with TeeTokenizer to “split” a TokenStream.
StandardTokenizer Sophisticated grammar-based tokenizer, emitting tokens for high-level types like email addresses (see section 4.3.2 for more details). Each emitted token is tagged with a special type, some of which are handled specially by StandardFilter.
TokenFilter TokenStream whose input is another TokenStream.
LowerCaseFilter Lowercases token text.
StopFilter Removes words that exist in a provided set of words.
PorterStemFilter Stems each token using the Porter stemming algorithm. For example, country and countries both stem to countri.
TeeTokenFilter Splits a TokenStream by passing each token it iterates through into a SinkTokenizer. It also returns the token unnmodified to its caller.
ASCIIFoldingFilter Maps accented characters to their unaccented counterparts.
CachingTokenFilter Saves all tokens from the input stream and can replay the stream once reset is called.
LengthFilter Accepts tokens whose text length falls within a specified range.
StandardFilter Designed to be fed by a StandardTokenizer. Removes dots from acronyms and ’s (apostrophe followed by s) from words with apostrophes.

下面是生成一个分析链的代码。

1
2
3
4
5
public TokenStream tokenStream(String fieldName, Reader reader) {
return new StopFilter(true,
new LowerCaseTokenizer(reader),
stopWords);
}

在这个分析器中,LowerCaseTokenizer对象会通过Reader对象输出原始tokens,然后这些token将会被StopFilter处理。

观察分析器分析过程

通常情况下,分析过程所产生的token会在没有展示的情况下用于索引操作或者检索。下面我们具体观察一些具体的分析过程。

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
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.util.Version;
import java.io.IOException;

public class AnalyzerDemo {
private static final String[] examples = {
"The quick brown fox jumped over the lazy dog",
"XY&Z Corporation - xyz@example.com"
};

private static final Analyzer[] analyzers = new Analyzer[] {
new WhitespaceAnalyzer(),
new SimpleAnalyzer(),
new StopAnalyzer(Version.LUCENE_30),
new StandardAnalyzer(Version.LUCENE_30)
};

public static void main(String[] args) throws IOException {

String[] strings = examples;
if (args.length > 0) { // A
strings = args;
}

for (String text : strings) {
analyze(text);
}
}

private static void analyze(String text) throws IOException {
System.out.println("Analyzing \"" + text + "\"");
for (Analyzer analyzer : analyzers) {
String name = analyzer.getClass().getSimpleName();
System.out.println(" " + name + ":");
System.out.print(" ");
AnalyzerUtils.displayTokens(analyzer, text); // B
System.out.println("\n");
}
}
}

// #A Analyze command-line strings, if specified
// #B Real work done in here

下面的AnalyzerUtils调用了analyzer对text进行分析,并将得到的token直接输出,而不是用于索引。

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import junit.framework.Assert;
import org.apache.lucene.util.AttributeSource;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.TermAttribute;
import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.util.Version;

import java.io.IOException;
import java.io.StringReader;

// From chapter 4
public class AnalyzerUtils {
public static void displayTokens(Analyzer analyzer,
String text) throws IOException {
displayTokens(analyzer.tokenStream("contents", new StringReader(text))); //A
}

public static void displayTokens(TokenStream stream)
throws IOException {

TermAttribute term = stream.addAttribute(TermAttribute.class);
while(stream.incrementToken()) {
System.out.print("[" + term.term() + "] "); //B
}
}
/*
#A Invoke analysis process
#B Print token text surrounded by brackets
*/

public static int getPositionIncrement(AttributeSource source) {
PositionIncrementAttribute attr = source.addAttribute(PositionIncrementAttribute.class);
return attr.getPositionIncrement();
}

public static String getTerm(AttributeSource source) {
TermAttribute attr = source.addAttribute(TermAttribute.class);
return attr.term();
}

public static String getType(AttributeSource source) {
TypeAttribute attr = source.addAttribute(TypeAttribute.class);
return attr.type();
}

public static void setPositionIncrement(AttributeSource source, int posIncr) {
PositionIncrementAttribute attr = source.addAttribute(PositionIncrementAttribute.class);
attr.setPositionIncrement(posIncr);
}

public static void setTerm(AttributeSource source, String term) {
TermAttribute attr = source.addAttribute(TermAttribute.class);
attr.setTermBuffer(term);
}

public static void setType(AttributeSource source, String type) {
TypeAttribute attr = source.addAttribute(TypeAttribute.class);
attr.setType(type);
}

public static void displayTokensWithPositions
(Analyzer analyzer, String text) throws IOException {

TokenStream stream = analyzer.tokenStream("contents",
new StringReader(text));
TermAttribute term = stream.addAttribute(TermAttribute.class);
PositionIncrementAttribute posIncr = stream.addAttribute(PositionIncrementAttribute.class);

int position = 0;
while(stream.incrementToken()) {
int increment = posIncr.getPositionIncrement();
if (increment > 0) {
position = position + increment;
System.out.println();
System.out.print(position + ": ");
}

System.out.print("[" + term.term() + "] ");
}
System.out.println();
}

public static void displayTokensWithFullDetails(Analyzer analyzer,
String text) throws IOException {

TokenStream stream = analyzer.tokenStream("contents", // #A
new StringReader(text));

TermAttribute term = stream.addAttribute(TermAttribute.class); // #B
PositionIncrementAttribute posIncr = // #B
stream.addAttribute(PositionIncrementAttribute.class); // #B
OffsetAttribute offset = stream.addAttribute(OffsetAttribute.class); // #B
TypeAttribute type = stream.addAttribute(TypeAttribute.class); // #B

int position = 0;
while(stream.incrementToken()) { // #C

int increment = posIncr.getPositionIncrement(); // #D
if (increment > 0) { // #D
position = position + increment; // #D
System.out.println(); // #D
System.out.print(position + ": "); // #D
}

System.out.print("[" + // #E
term.term() + ":" + // #E
offset.startOffset() + "->" + // #E
offset.endOffset() + ":" + // #E
type.type() + "] "); // #E
}
System.out.println();
}
/*
#A Perform analysis
#B Obtain attributes of interest
#C Iterate through all tokens
#D Compute position and print
#E Print all token details
*/

public static void assertAnalyzesTo(Analyzer analyzer, String input,
String[] output) throws Exception {
TokenStream stream = analyzer.tokenStream("field", new StringReader(input));

TermAttribute termAttr = stream.addAttribute(TermAttribute.class);
for (String expected : output) {
Assert.assertTrue(stream.incrementToken());
Assert.assertEquals(expected, termAttr.term());
}
Assert.assertFalse(stream.incrementToken());
stream.close();
}

public static void displayPositionIncrements(Analyzer analyzer, String text)
throws IOException {
TokenStream stream = analyzer.tokenStream("contents", new StringReader(text));
PositionIncrementAttribute posIncr = stream.addAttribute(PositionIncrementAttribute.class);
while (stream.incrementToken()) {
System.out.println("posIncr=" + posIncr.getPositionIncrement());
}
}

public static void main(String[] args) throws IOException {
System.out.println("SimpleAnalyzer");
displayTokensWithFullDetails(new SimpleAnalyzer(),
"The quick brown fox....");

System.out.println("\n----");
System.out.println("StandardAnalyzer");
displayTokensWithFullDetails(new StandardAnalyzer(Version.LUCENE_30),
"I'll email you at xyz@example.com");
}
}

/*
#1 Invoke analysis process
#2 Output token text surrounded by brackets
*/
1
2
3
4
public static void main(String[] args) throws IOException {
AnalyzerUtils.displayTokensWithFullDetails(new SimpleAnalyzer(),
"The quick brown fox....");
}
1
2
3
4
1: [the:0->3:word]
2: [quick:4->9:word]
3: [brown:10->15:word]
4: [fox:16->19:word]

可以看到,每个token都被置于与前一token邻接的位置上,这里所有的token都是单词类型的。

属性
需要注意的TokenStream不会显式生成包含所有token属性的token对象,而是是必须与token对应的可重用的属性结构进行交互获得这些属性,这么做主要是考虑扩展性和效率。

TokenStream继承类AttributeSource。AttributeSouce是一种有效并通用的类,用于提供可扩展的属性并且不需要运行时类型转换。Lucene在分析期间可以使用预定义的属性,同样我们也可以加入预定义的属性,需要实现Attribute接口。

Lucene内置的token属性:

Token attribute interface Description
TermAttribute Token’s text
PositionIncrementAttribute Position increment (defaults to 1)
OffsetAttribute Start and end character offset
TypeAttribute Token’s type (defaults to word)
FlagsAttribute Bits to encode custom flags
PayloadAttribute Per-token byte[] payload (see section 6.5)

通过这个可重用的API,我们可以首先通过调用addAttribute方法来获取所需要的属性,该方法会返回一个实现对应接口的具体类的实例。然后我们可以调用TokenStream的incrementToken()方法来顺序访问所有的token。如果该方法成功移动到下一个token则会返回true,这时之前获得的属性实例都会将内部状态修改为下一个token的属性,我们可以通过这些属性实例进行交互获得token的属性值。

1
2
3
4
5
6
7
TokenStream stream = analyzer.tokenStream("contents",
new StringReader(text));

PositionIncrementAttribute posIncr = stream.addAttribute(PositionIncrementAttribute.class);
while (stream.incrementToken()) {
System.out.println("posIncr=" + posIncr.getPositionIncrement());
}

注意通过上面的属性实例是双向的,我们也可以为token设置属性,

另外,我们也可以控制PositionIncrement,也就是控制是否移到下一个词,也就是通过保持位置不变,我们可以在当前位置添加很多词,而不是原来当前位置的一个词,通过TokenPositionIncrementAttribute.setPositionIncrement(0)可以实现,同样也可以跳过一些词,一般token与token之间的增量是1.
通过这种方法我们可以在同一位置添加同义词,从而实现同义词的去querying。

另外有时我们需要对当前处理的token进行一个完整的备份,用于之后回到当前这个状态。我们可以通过调用captureState来实现记录当前状态,然会一个State对象(保存了所有的状态,属性)。之后我们可以调用restoreStore进行恢复。注意这个方法的代价很高,一般应该尽量避免。

起始和结束位置偏移量可以用于TermVector类进行索引,通常可用于高亮现实搜索结果。

我们还可以设置token类型,如StandardAnalyzer对I’ll email you at xyz@exam- ple.com的处理结果。

1
2
3
4
1: [i'll:0->4:<APOSTROPHE>]
2: [email:5->10:<ALPHANUM>]
3: [you:11->14:<ALPHANUM>]
5: [xyz@example.com:18->33:<EMAIL>]

token类型还可以用于metaphone和同义词分析器。但是默认情况下。Lucene并不将token类型编入索引,而只是在分析时使用,因此如果有这个需求时,我们需要使用TypeAsPayload的token filter将类型作为有效负载记录下来。

TokenFilter的顺序很重要

TokenFilter链在处理token时顺序很重要,如果过滤停用词时,我们需要首先使用LowerCaseFilter,然后再使用StopFilter。如果使用错误的顺序StopFilter,LowerCaseFilter,那么The有可能不会被过滤掉,因为StopFilter默认所有字符已经是小写所以只会匹配the。

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
// correct version
public class StopAnalyzer2 extends Analyzer {

private Set stopWords;

public StopAnalyzer2() {
stopWords = StopAnalyzer.ENGLISH_STOP_WORDS_SET;
}

public StopAnalyzer2(String[] stopWords) {
this.stopWords = StopFilter.makeStopSet(stopWords);
}

public TokenStream tokenStream(String fieldName, Reader reader) {
return new StopFilter(true, new LowerCaseFilter(new LetterTokenizer(reader)),
stopWords);
}
}

// wrong version
public class StopAnalyzerFlawed extends Analyzer {
private Set stopWords;

public StopAnalyzerFlawed() {
stopWords = StopAnalyzer.ENGLISH_STOP_WORDS_SET;
}

public StopAnalyzerFlawed(String[] stopWords) {
this.stopWords = StopFilter.makeStopSet(stopWords);
}

/**
* Ordering mistake here
*/
public TokenStream tokenStream(String fieldName, Reader reader) {
return new LowerCaseFilter(
new StopFilter(true, new LetterTokenizer(reader),
stopWords));
}
}

使用内置分析器

Lucene的一些常用分析器:

Analyzer Steps taken
WhitespaceAnalyzer Splits tokens at whitespace.
SimpleAnalyzer Divides text at nonletter characters and lowercases.
StopAnalyzer Divides text at nonletter characters, lowercases, and removes stop words.
KeywordAnalyzer Treats entire text as a single token.
StandardAnalyzer Tokenizes based on a sophisticated grammar that recognizes email addresses, acronyms, Chinese-Japanese-Korean characters, alphanumer- ics, and more. It also lowercases and removes stop words.

StopAnalyzer
StopAnalyzer分析器除了完成基本的token拆分和小写化功能之外,还负责移除停用词(stop words)。StopAnalyzer类内置了如下一个常用常用英文停用词集合,该集合有ENGLISH_STOP_WORDS_SET定义,默认包括:

1
2
3
4
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it", "no", "not", "of", "on",
"or", "such","that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"

StopAnalyzer有一个可重载的构造方法,允许通过这个方法传入子集的停用词集合。

StandardAnalyzer
StandardAnalyzer是最复杂强大也是最使用的Lucene内置分析器。

分析器的选择

大部分应用程序都不使用任意一种内置分析器,而是选择创建自己的分析器链,因为很多情况下都有特殊的需求,如自定义停用词列表,特殊的tokenlization操作等。

后面的部分将介绍如何创建自己的实用分析器,包括两种常用功能:近音词查询和同义词扩展。

近音词查询(Sounds-like querying)

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
public class MetaphoneAnalyzerTest extends TestCase {
public void testKoolKat() throws Exception {
RAMDirectory directory = new RAMDirectory();
Analyzer analyzer = new MetaphoneReplacementAnalyzer();

IndexWriter writer = new IndexWriter(directory, analyzer, true,
IndexWriter.MaxFieldLength.UNLIMITED);

Document doc = new Document();
doc.add(new Field("contents", //#A
"cool cat",
Field.Store.YES,
Field.Index.ANALYZED));
writer.addDocument(doc);
writer.close();

IndexSearcher searcher = new IndexSearcher(directory);

Query query = new QueryParser(Version.LUCENE_30, //#B
"contents", analyzer) //#B
.parse("kool kat"); //#B

TopDocs hits = searcher.search(query, 1);
assertEquals(1, hits.totalHits); //#C
int docID = hits.scoreDocs[0].doc;
doc = searcher.doc(docID);
assertEquals("cool cat", doc.get("contents")); //#D

searcher.close();
}

/*
#A Index document
#B Parse query text
#C Verify match
#D Retrieve original value
*/

public static void main(String[] args) throws IOException {
MetaphoneReplacementAnalyzer analyzer =
new MetaphoneReplacementAnalyzer();
AnalyzerUtils.displayTokens(analyzer,
"The quick brown fox jumped over the lazy dog");

System.out.println("");
AnalyzerUtils.displayTokens(analyzer,
"Tha quik brown phox jumpd ovvar tha lazi dag");
}
}

关键是MetaphoneReplacementAnalyzer。

1
2
3
4
5
6
public class MetaphoneReplacementAnalyzer extends Analyzer {
public TokenStream tokenStream(String fieldName, Reader reader) {
return new MetaphoneReplacementFilter(
new LetterTokenizer(reader));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MetaphoneReplacementFilter extends TokenFilter {
public static final String METAPHONE = "metaphone";

private Metaphone metaphoner = new Metaphone();
private TermAttribute termAttr;
private TypeAttribute typeAttr;

public MetaphoneReplacementFilter(TokenStream input) {
super(input);
termAttr = addAttribute(TermAttribute.class);
typeAttr = addAttribute(TypeAttribute.class);
}

public boolean incrementToken() throws IOException {
if (!input.incrementToken()) //#A
return false; //#A

String encoded;
encoded = metaphoner.encode(termAttr.term()); //#B
termAttr.setTermBuffer(encoded); //#C
typeAttr.setType(METAPHONE); //#D
return true;
}
}

这里的核心是将一个词转化为它的语音词根(phonetic root),也就是Metaphne algorithm,这里使用了Apache Commons Codec project的实现。

具体实现是将每个incoming的token的text在同一位置替换为该token的phonetic root,并设置为类型为METAPHONE。

1
2
3
4
String encoded;
encoded = metaphoner.encode(termAttr.term());
termAttr.setTermBuffer(encoded);
typeAttr.setType(METAPHONE);
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws IOException {
MetaphoneReplacementAnalyzer analyzer =
new MetaphoneReplacementAnalyzer();
AnalyzerUtils.displayTokens(analyzer,
"The quick brown fox jumped over the lazy dog");

System.out.println("");
AnalyzerUtils.displayTokens(analyzer,
"Tha quik brown phox jumpd ovvar tha lazi dag");
}

可以发现samples经过metaphone encoder处理后是完全一样的:

1
2
[0] [KK] [BRN] [FKS] [JMPT] [OFR] [0] [LS] [TKS]
[0] [KK] [BRN] [FKS] [JMPT] [OFR] [0] [LS] [TKS]

在实际情况下,我们只在特殊情况下使用sound-like querying,因为sound-like querying通常也会匹配许多完全不相关的结果。Google的策略是只有在用户现有的query存在拼写错误或者几乎没有匹配结果的情况下,才会使用sounld-like querying为用户提供suggestion。

同义词查询

一种处理同义词的方法是使得analyzer将token的同义词插入到现在正在处理的token stream中。

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
public void testJumps() throws Exception {
TokenStream stream =
synonymAnalyzer.tokenStream("contents", // #A
new StringReader("jumps")); // #A
TermAttribute term = stream.addAttribute(TermAttribute.class);
PositionIncrementAttribute posIncr = stream.addAttribute(PositionIncrementAttribute.class);

int i = 0;
String[] expected = new String[]{"jumps", // #B
"hops", // #B
"leaps"}; // #B
while(stream.incrementToken()) {
assertEquals(expected[i], term.term());

int expectedPos; // #C
if (i == 0) { // #C
expectedPos = 1; // #C
} else { // #C
expectedPos = 0; // #C
} // #C
assertEquals(expectedPos, // #C
posIncr.getPositionIncrement()); // #C
i++;
}
assertEquals(3, i);
}

SynonymAnalyzer首先需要detect具有同义词的token的出现,然后将这个token的synonyms插入到相同位置。

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
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.LowerCaseFilter;
import org.apache.lucene.analysis.StopFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.analysis.standard.StandardTokenizer;
import org.apache.lucene.analysis.standard.StandardFilter;
import org.apache.lucene.util.Version;
import java.io.Reader;

// From chapter 4
public class SynonymAnalyzer extends Analyzer {
private SynonymEngine engine;

public SynonymAnalyzer(SynonymEngine engine) {
this.engine = engine;
}

public TokenStream tokenStream(String fieldName, Reader reader) {
TokenStream result = new SynonymFilter(
new StopFilter(true,
new LowerCaseFilter(
new StandardFilter(
new StandardTokenizer(
Version.LUCENE_30, reader))),
StopAnalyzer.ENGLISH_STOP_WORDS_SET),
engine
);
return result;
}
}
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
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.TokenFilter;
import org.apache.lucene.analysis.tokenattributes.TermAttribute;
import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
import org.apache.lucene.util.AttributeSource;
import java.io.IOException;
import java.util.Stack;
import lia.analysis.AnalyzerUtils;

// From chapter 4
public class SynonymFilter extends TokenFilter {
public static final String TOKEN_TYPE_SYNONYM = "SYNONYM";

private Stack<String> synonymStack;
private SynonymEngine engine;
private AttributeSource.State current;

private final TermAttribute termAtt;
private final PositionIncrementAttribute posIncrAtt;

public SynonymFilter(TokenStream in, SynonymEngine engine) {
super(in);
synonymStack = new Stack<String>(); //#1
this.engine = engine;

this.termAtt = addAttribute(TermAttribute.class);
this.posIncrAtt = addAttribute(PositionIncrementAttribute.class);
}

public boolean incrementToken() throws IOException {
if (synonymStack.size() > 0) { //#2
String syn = synonymStack.pop(); //#2
restoreState(current); //#2
termAtt.setTermBuffer(syn);
posIncrAtt.setPositionIncrement(0); //#3
return true;
}

if (!input.incrementToken()) //#4
return false;

if (addAliasesToStack()) { //#5
current = captureState(); //#6
}

return true; //#7
}

private boolean addAliasesToStack() throws IOException {
String[] synonyms = engine.getSynonyms(termAtt.term()); //#8
if (synonyms == null) {
return false;
}
for (String synonym : synonyms) { //#9
synonymStack.push(synonym);
}
return true;
}
}

/*
#1 Define synonym buffer
#2 Pop buffered synonyms
#3 Set position increment to 0
#4 Read next token
#5 Push synonyms onto stack
#6 Save current token
#7 Return current token
#8 Retrieve synonyms
#9 Push synonyms onto stack
*/

考虑到SynonymEngine的可扩展性,因此SynoymEngine类被设计成只有一个方法的接口:

1
2
3
public interface SynonymEngine {
String[] getSynonyms(String s) throws IOException;
}

下面是一个简单的测试实现方法,实际上Lucene提供了一个基于WordNet的强大的SynonymEngine类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestSynonymEngine implements SynonymEngine {
private static HashMap<String, String[]> map = new HashMap<String, String[]>();

static {
map.put("quick", new String[] {"fast", "speedy"});
map.put("jumps", new String[] {"leaps", "hops"});
map.put("over", new String[] {"above"});
map.put("lazy", new String[] {"apathetic", "sluggish"});
map.put("dog", new String[] {"canine", "pooch"});
}

public String[] getSynonyms(String s) {
return map.get(s);
}
}

完整测试:

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
86
87
88
89
90
91
92
93
94
95
96
97
public class SynonymAnalyzerTest extends TestCase {
private IndexSearcher searcher;
private static SynonymAnalyzer synonymAnalyzer =
new SynonymAnalyzer(new TestSynonymEngine());

public void setUp() throws Exception {
RAMDirectory directory = new RAMDirectory();

IndexWriter writer = new IndexWriter(directory,
synonymAnalyzer, //#1
IndexWriter.MaxFieldLength.UNLIMITED);
Document doc = new Document();
doc.add(new Field("content",
"The quick brown fox jumps over the lazy dog",
Field.Store.YES,
Field.Index.ANALYZED)); //#2
writer.addDocument(doc);

writer.close();

searcher = new IndexSearcher(directory, true);
}

public void tearDown() throws Exception {
searcher.close();
}

public void testJumps() throws Exception {
TokenStream stream =
synonymAnalyzer.tokenStream("contents", // #A
new StringReader("jumps")); // #A
TermAttribute term = stream.addAttribute(TermAttribute.class);
PositionIncrementAttribute posIncr = stream.addAttribute(PositionIncrementAttribute.class);

int i = 0;
String[] expected = new String[]{"jumps", // #B
"hops", // #B
"leaps"}; // #B
while(stream.incrementToken()) {
assertEquals(expected[i], term.term());

int expectedPos; // #C
if (i == 0) { // #C
expectedPos = 1; // #C
} else { // #C
expectedPos = 0; // #C
} // #C
assertEquals(expectedPos, // #C
posIncr.getPositionIncrement()); // #C
i++;
}
assertEquals(3, i);
}

/*
#A Analyze with SynonymAnalyzer
#B Check for correct synonyms
#C Verify synonyms positions
*/

public void testSearchByAPI() throws Exception {

TermQuery tq = new TermQuery(new Term("content", "hops")); //#1
assertEquals(1, TestUtil.hitCount(searcher, tq));

PhraseQuery pq = new PhraseQuery(); //#2
pq.add(new Term("content", "fox")); //#2
pq.add(new Term("content", "hops")); //#2
assertEquals(1, TestUtil.hitCount(searcher, pq));
}

/*
#1 Search for "hops"
#2 Search for "fox hops"
*/

public void testWithQueryParser() throws Exception {
Query query = new QueryParser(Version.LUCENE_30, // 1
"content", // 1
synonymAnalyzer).parse("\"fox jumps\""); // 1
assertEquals(1, TestUtil.hitCount(searcher, query)); // 1
System.out.println("With SynonymAnalyzer, \"fox jumps\" parses to " +
query.toString("content"));

query = new QueryParser(Version.LUCENE_30, // 2
"content", // 2
new StandardAnalyzer(Version.LUCENE_30)).parse("\"fox jumps\""); // B
assertEquals(1, TestUtil.hitCount(searcher, query)); // 2
System.out.println("With StandardAnalyzer, \"fox jumps\" parses to " +
query.toString("content"));
}

/*
#1 SynonymAnalyzer finds the document
#2 StandardAnalyzer also finds document
*/
}

词干分析(Stemming analysis)

域分析(Field variations)

语言分析(Stemming analysis)

Lucene(3)_Search

Posted on 2014-09-21 | In Developing | Comments:

实现简单的搜索功能

使用Lucene进行搜索时,我们可以自己构建查询语句,也可以是使用Lucene的QueryParser类将用户输入的文本转换为Query对象。

对于term的检索

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
import junit.framework.TestCase;

import lia.common.TestUtil;

import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.store.Directory;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.Version;

// From chapter 3
public class BasicSearchingTest extends TestCase {

public void testTerm() throws Exception {
Directory dir = TestUtil.getBookIndexDirectory(); //A
IndexSearcher searcher = new IndexSearcher(dir); //B

Term t = new Term("subject", "ant");
Query query = new TermQuery(t);
TopDocs docs = searcher.search(query, 10);
assertEquals("Ant in Action", //C
1, docs.totalHits); //C

t = new Term("subject", "junit");
docs = searcher.search(new TermQuery(t), 10);
assertEquals("Ant in Action, " + //D
"JUnit in Action, Second Edition", //D
2, docs.totalHits); //D

searcher.close();
dir.close();
}

/*
#A Obtain directory from TestUtil
#B Create IndexSearcher
#C Confirm one hit for "ant"
#D Confirm two hits for "junit"
*/

解析用户输入的查询表达式:QueryParser

Lucene的搜索方法需要一个Query对象作为参数。对查询表达式的解析就是将用户的query text,例如mock OR junit,转换为对应的Query对象。

Lucene_4

QueryParser为查询解析提供了强大的支持,可以处理复杂的条件表达式,最后生成的Query 会非常庞大而复杂。

QueryParser类需要使用一个分析器将查询语句分割为多个项。

1
2
3
QueryParser parser = new QueryParser(Version matchVersion,
String field,
Analyzer analyzer)

Version matchVersion用于告诉Lucene使用哪个版本,以实现向后兼容。
field用于指示默认搜索的field,除了query里明确指示了应该搜索哪个域,例如title:lucene。
analyzer指定了用于分解query的分析器。

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
  public void testQueryParser() throws Exception {
Directory dir = TestUtil.getBookIndexDirectory();
IndexSearcher searcher = new IndexSearcher(dir);

QueryParser parser = new QueryParser(Version.LUCENE_30, //A
"contents", //A
new SimpleAnalyzer()); //A

Query query = parser.parse("+JUNIT +ANT -MOCK"); //B
TopDocs docs = searcher.search(query, 10);
assertEquals(1, docs.totalHits);
Document d = searcher.doc(docs.scoreDocs[0].doc);
assertEquals("Ant in Action", d.get("title"));

query = parser.parse("mock OR junit"); //B
docs = searcher.search(query, 10);
assertEquals("Ant in Action, " +
"JUnit in Action, Second Edition",
2, docs.totalHits);

searcher.close();
dir.close();
}
/*
#A Create QueryParser
#B Parse user's text
*/

query expressions以及其QueryParser分解

Query expression Matches documents that
java Contain the term java in the default field
java junit Contain the term java or junit, or both, in the default field
java OR junit Contain the term java or junit, or both, in the default field
+java +junit Contain both java and junit in the default field
java AND junit Contain both java and junit in the default field
title:ant Contain the term ant in the title field
title:extreme –subject:sports Contain extreme in the title field and don’t have sports in the subject field
title:extreme AND NOT subject:sports Contain extreme in the title field and don’t have sports in the subject field
(agile OR extreme) AND methodology Contain methodology and must also contain agile and/or extreme, all in the default field
title:”junit in action” Contain the exact phrase “junit in action” in the title field
title:”junit action”~5 Contain the terms junit and action within five positions of one another, in the title field
java* Contain terms that begin with java, like javaspaces, javaserver, java.net, and the exact tem java itself.
java~ Contain terms that are close to the word java, such as lava
lastmodified: [1/1/09 TO 12/31/09] Have lastmodified field values between the dates January 1, 2009 and December 31, 2009

使用IndexSearcher

创建IndexSearcher

1
2
3
Directory dir = FSDirectory.open(new File("/path/to/index"));
IndexReader reader = IndexReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);

IndexReaser完成了诸如打开索引系统文件和提供底层reader API等繁重的工作,而IndexSearcher则要简单很多。打开一个IndexReader需要较大的系统开销,所有在搜索期间最好重复使用同一个或有限的几个IndexReader实例,只在有必要的时候才创建新的IndexReader。

另外还可以从Directory直接创建IndexSearcher,这时会创建这个searcher自己的IndexReader,searcher关闭时这个reader也会关闭,消耗比较大。

IndexReader的读操作总是基于它创建时的快照,如果index发生了改变而且我们需要IndexReader同步这些修改,我们需要重新创建一个IndexReader。如果已经有建立的reader,这时可以调用IndexReaser.reopen,会conver所有的change,同时资源消耗会远小于直接创建一个新的reader。

1
2
3
4
5
6
IndexReader newReader = reader.reopen();
if (reader != newReader) {
reader.close();
reader = newReader;
searcher = new IndexSearcher(reader);
}

如果索引有所变更,reopen方法返回一个新的reader,这是程序必须关闭reader并创建新的searcher。在实际的应用程序中,可能还有多个线程在使用旧的reader,我们要注意保证线程安全。

实现搜索功能

在建立IndexSearcher实例后,使用search(Query, int)方法进行检索。另外更好高级的操作还有过滤和排序。

search method when to use
TopDocs search(Query query, int n) Straightforward searches. The int n parameter specifies how many top-scoring documents to return.
TopDocs search(Query query, Filter filter, int n) Searches constrained to a subset of available documents, based on filter criteria.
TopFieldDocs search(Query query, Filter filter, int n, Sort sort) Searches constrained to a subset of available documents based on filter criteria, and sorted by a custom Sort object
void search(Query query, Collector results) Used when you have custom logic to implement for each document visited, or you’d like to collect a different subset of documents than the top N by the sort criteria.
void search(Query query, Filter filter, Collector results) Same as previous, except documents are only accepted if they pass the filter criteria.

Working with TopDocs

TopDocs method or attribute Return value
totalHits Number of documents that matched the search
scoreDocs Array of ScoreDoc instances that contains the results
getMaxScore() Returns best score of all matches, if scoring was done while searching (when sorting by field, you separately control whether scores are computed)

搜索结果分页

大部分情况下用户只在首页结果进行查找,但是分页仍是需要的,主要有下面两种解决方案:

  • 返回多页的结果并保存在ScoreDocs和IndexSearcher中。
  • 重新发送查询请求(requerying)。

Requerying一般是更好的解决方案,可以减少保存用户state,在很多用户的情况下,保存很多用户的state代价是非常大的。Lucene通常查询速度非常快,而且操作系统通常会cache最近查询的数据,使得很多情况下需要的数据还在RAM中。

Near-real-time search

Lucene 2.9开始引入近实时检索,使得我们可以快速检索最近index新添加的内容,即是IndexWriter还没有把内容写入到磁盘。许多应用程序在提供搜索功能的同时,也在不断索引新添加的内容,需要维持一个不能段时间关闭的writter,而且需要在搜索时反映出这些新添加的内容。如果writer和reader处于同一个JVM中,我们就可以使用近实时检索(Near-real-time search)。

近实时检索可以是我们能够检索新创建但还未完成提交的段进行检索。

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
import org.apache.lucene.util.Version;
import org.apache.lucene.store.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.document.*;
import org.apache.lucene.analysis.standard.StandardAnalyzer;

import junit.framework.TestCase;

// From chapter 3
public class NearRealTimeTest extends TestCase {
public void testNearRealTime() throws Exception {
Directory dir = new RAMDirectory();
IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(Version.LUCENE_30), IndexWriter.MaxFieldLength.UNLIMITED);
for(int i=0;i<10;i++) {
Document doc = new Document();
doc.add(new Field("id", ""+i, Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
doc.add(new Field("text", "aaa", Field.Store.NO, Field.Index.ANALYZED));
writer.addDocument(doc);
}
IndexReader reader = writer.getReader(); // #1
IndexSearcher searcher = new IndexSearcher(reader); // #A

Query query = new TermQuery(new Term("text", "aaa"));
TopDocs docs = searcher.search(query, 1);
assertEquals(10, docs.totalHits); // #B

writer.deleteDocuments(new Term("id", "7")); // #2

Document doc = new Document(); // #3
doc.add(new Field("id", // #3
"11", // #3
Field.Store.NO, // #3
Field.Index.NOT_ANALYZED_NO_NORMS)); // #3
doc.add(new Field("text", // #3
"bbb", // #3
Field.Store.NO, // #3
Field.Index.ANALYZED)); // #3
writer.addDocument(doc); // #3

IndexReader newReader = reader.reopen(); // #4
assertFalse(reader == newReader); // #5
reader.close(); // #6
searcher = new IndexSearcher(newReader);

TopDocs hits = searcher.search(query, 10); // #7
assertEquals(9, hits.totalHits); // #7

query = new TermQuery(new Term("text", "bbb")); // #8
hits = searcher.search(query, 1); // #8
assertEquals(1, hits.totalHits); // #8

newReader.close();
writer.close();
}
}

/*
#1 Create near-real-time reader
#A Wrap reader in IndexSearcher
#B Search returns 10 hits
#2 Delete 1 document
#3 Add 1 document
#4 Reopen reader
#5 Confirm reader is new
#6 Close old reader
#7 Verify 9 hits now
#8 Confirm new document matched
*/

/*
#1 IndexWriter returns a reader that's able to search all previously committed changes to the index, plus any uncommitted changes. The returned reader is always readOnly.
#2,#3 We make changes to the index, but do not commit them.
#4,#5,#6 Ask the reader to reopen. Note that this simply re-calls writer.getReader again under the hood. Because we made changes, the newReader will be different from the old one so we must close the old one.
#7, #8 The changes made with the writer are reflected in new searches.
*/

Lucene的评分机制(scoring)

How Lucene scores

Lucene similarity scoring formula:

其中d指document,t指term,q指query。
通过这个方程我们可以计算文档对于query的评分,通常还需要将评分进行归一化处理,也就是除以最大评分。评分越大说明文档和query的匹配程度越高。Lucene根据匹配文档的得分进行排序,然后将结果返回。

This score is the raw score, which is a floating-point number >= 0.0. Typically, if an application presents the score to the end user, it’s best to first normalize the scores by dividing all scores by the maximum score for the query. The larger the similarity score, the better the match of the document to the query. By default Lucene returns documents reverse-sorted by this score, meaning the top documents are the best matching ones. Table 3.5 describes each of the factors in the scoring formula.
Boost factors are built into the equation to let you affect a query or field’s influence on score. Field boosts come in explicitly in the equation as the boost(t.field in d) factor, set at indexing time. The default value of field boosts, logically, is 1.0. During indexing, a document can be assigned a boost, too. A document boost factor implicitly sets the starting field boost of all fields to the specified value. Field-specific boosts are multiplied by the starting value, giving the final value of the field boost factor. It’s pos- sible to add the same named field to a document multiple times, and in such situations the field boost is computed as all the boosts specified for that field and document mul- tiplied together. Section 2.5 discusses index-time boosting in more detail.
In addition to the explicit factors in this equation, other factors can be computed on a per-query basis as part of the queryNorm factor. Queries themselves can have an impact on the document score. Boosting a Query instance is sensible only in a multi- ple-clause query; if only a single term is used for searching, changing its boost would impact all matched documents equally. In a multiple-clause Boolean query, some doc- uments may match one clause but not another, enabling the boost factor to discrimi- nate between matching documents. Queries also default to a 1.0 boost factor.
Most of these scoring formula factors are controlled and implemented as a sub- class of the abstract Similarity class. DefaultSimilarity is the implementation used unless otherwise specified. More computations are performed under the covers of DefaultSimilarity; for example, the term frequency factor is the square root of the actual frequency. Because this is an “in action” book, it’s beyond the book’s scope to delve into the inner workings of these calculations. In practice, it’s extremely rare to need a change in these factors. Should you need to change them, please refer to Similarity’s Javadocs, and be prepared with a solid understanding of these factors and the effect your changes will have.
It’s important to note that a change in index-time boosts or the Similarity meth- ods used during indexing, such as lengthNorm, require that the index be rebuilt for all factors to be in sync.
Let’s say you’re baffled as to why a certain document got a good score to your Query. Lucene offers a nice feature to help provide the answer.

Factor Description
tf(t in d) Term frequency factor for the term (t) in the document (d)—how many times the term t occurs in the document.
idf(t) Inverse document frequency of the term: a measure of how “unique” the term is. Very common terms have a low idf; very rare terms have a high idf.
boost(t.field in d) Field and document boost, as set during indexing (see section 2.5). You may use this to statically boost certain fields and certain docu- ments over others.
lengthNorm(t.field in d) Normalization value of a field, given the number of terms within the field. This value is computed during indexing and stored in the index norms. Shorter fields (fewer tokens) get a bigger boost from this factor.
coord(q, d) Coordination factor, based on the number of query terms the document contains. The coordination factor gives an AND-like boost to documents that contain more of the search terms than other documents.
queryNorm(q) Normalization value for a query, given the sum of the squared weights of each of the query terms.

多样化查询(Lucene’s diverse queries)

QueryParser

1…121314…17
Plusaber

Plusaber

Plusaber's Blog
82 posts
12 categories
22 tags
Links
  • LinkedIn
  • Indeed
  • Baito
  • Kaggle
© 2014 – 2019 Plusaber
Powered by Hexo v3.8.0
|
Theme – NexT.Mist v7.1.1