Skip to content

Commit 6d2d70f

Browse files
kin122金端
andauthored
Fix: ik_smart complex crossPath fallback #1119 #1155 (#1157)
Co-authored-by: 金端 <duan.jin01@weimob.com>
1 parent 6e84689 commit 6d2d70f

3 files changed

Lines changed: 340 additions & 166 deletions

File tree

core/src/main/java/org/wltea/analyzer/core/IKArbitrator.java

Lines changed: 180 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,16 @@
3030
/**
3131
* IK分词歧义裁决器
3232
*/
33-
class IKArbitrator {
34-
35-
IKArbitrator(){
36-
37-
}
33+
class IKArbitrator {
34+
private static final int LONG_CN_WORD_LENGTH_THRESHOLD = 10;
35+
private static final int LONG_CN_WORD_COUNT_THRESHOLD = 5;
36+
private static final int TOTAL_LEXEME_COUNT_THRESHOLD = 50;
37+
private static final int DENSE_CROSS_PATH_SIZE_THRESHOLD = 20;
38+
private static final int DENSE_OVERLAP_RATIO_THRESHOLD = 4;
39+
40+
IKArbitrator(){
41+
42+
}
3843

3944
/**
4045
* 分词歧义处理
@@ -53,13 +58,12 @@ void process(AnalyzeContext context , boolean useSmart){
5358
//crossPath没有歧义 或者 不做歧义处理
5459
//直接输出当前crossPath
5560
context.addLexemePath(crossPath);
56-
}else{
57-
//对当前的crossPath进行歧义处理
58-
QuickSortSet.Cell headCell = crossPath.getHead();
59-
LexemePath judgeResult = this.judge(headCell, crossPath.getPathLength());
60-
//输出歧义处理结果judgeResult
61-
context.addLexemePath(judgeResult);
62-
}
61+
}else{
62+
//对当前的crossPath进行歧义处理
63+
LexemePath judgeResult = this.judge(crossPath);
64+
//输出歧义处理结果judgeResult
65+
context.addLexemePath(judgeResult);
66+
}
6367

6468
//把orgLexeme加入新的crossPath中
6569
crossPath = new LexemePath();
@@ -74,157 +78,167 @@ void process(AnalyzeContext context , boolean useSmart){
7478
//crossPath没有歧义 或者 不做歧义处理
7579
//直接输出当前crossPath
7680
context.addLexemePath(crossPath);
77-
}else{
78-
//对当前的crossPath进行歧义处理
79-
QuickSortSet.Cell headCell = crossPath.getHead();
80-
LexemePath judgeResult = this.judge(headCell, crossPath.getPathLength());
81-
//输出歧义处理结果judgeResult
82-
context.addLexemePath(judgeResult);
83-
}
84-
}
85-
86-
/**
87-
* 检测是否为叠词模式
88-
* @param lexemeCell 词元链表头
89-
* @return 如果检测到叠词模式返回简化的路径,否则返回null
90-
*/
91-
private LexemePath detectRepeatedWords(QuickSortSet.Cell lexemeCell) {
92-
if (lexemeCell == null || lexemeCell.getLexeme() == null) {
93-
return null;
94-
}
95-
96-
// 检查是否有连续的长词元(长度>10)或大量重复词元
97-
QuickSortSet.Cell current = lexemeCell;
98-
int longLexemeCount = 0;
99-
int totalCount = 0;
100-
Lexeme firstLexeme = null;
101-
Lexeme lastLexeme = null;
102-
103-
while (current != null && current.getLexeme() != null) {
104-
Lexeme lexeme = current.getLexeme();
105-
if (firstLexeme == null) {
106-
firstLexeme = lexeme;
107-
}
108-
lastLexeme = lexeme;
109-
110-
if (lexeme.getLength() > 10) {
111-
longLexemeCount++;
112-
}
113-
totalCount++;
114-
115-
// 如果发现多个长词元或词元总数过多,认为是叠词
116-
if (longLexemeCount > 5 || totalCount > 50) {
117-
// 构造简化路径:第一个词元 + 剩余部分合并为一个词元
118-
LexemePath simplifiedPath = new LexemePath();
119-
120-
// 添加第一个词元
121-
simplifiedPath.addNotCrossLexeme(firstLexeme);
122-
123-
// 如果有剩余部分,创建一个合并的词元
124-
if (totalCount > 1 && lastLexeme != null) {
125-
// 计算剩余部分的起始位置和长度
126-
int remainStart = firstLexeme.getBegin() + firstLexeme.getLength();
127-
int remainEnd = lastLexeme.getBegin() + lastLexeme.getLength();
128-
int remainLength = remainEnd - remainStart;
129-
130-
if (remainLength > 0) {
131-
// 创建一个表示剩余部分的词元
132-
// offset 应该是第一个词元的 offset + 第一个词元的长度
133-
int remainOffset = firstLexeme.getOffset() + firstLexeme.getLength();
134-
Lexeme remainLexeme = new Lexeme(remainOffset, remainStart, remainLength, Lexeme.TYPE_CNCHAR);
135-
simplifiedPath.addNotCrossLexeme(remainLexeme);
136-
}
137-
}
138-
139-
return simplifiedPath;
140-
}
141-
142-
current = current.getNext();
143-
}
144-
145-
return null; // 没有检测到叠词模式
146-
}
147-
148-
/**
149-
* 歧义识别
150-
* @param lexemeCell 歧义路径链表头
151-
* @param fullTextLength 歧义路径文本长度
152-
* @return
153-
*/
154-
private LexemePath judge(QuickSortSet.Cell lexemeCell , int fullTextLength){
155-
// 首先检测是否为叠词模式,如果是则直接返回简化路径
156-
LexemePath simplifiedPath = this.detectRepeatedWords(lexemeCell);
157-
if (simplifiedPath != null) {
158-
//System.out.println("Detected repeated words pattern, using simplified path");
159-
return simplifiedPath;
160-
}
161-
162-
//候选路径集合
163-
TreeSet<LexemePath> pathOptions = new TreeSet<LexemePath>();
164-
//候选结果路径
165-
LexemePath option = new LexemePath();
166-
167-
//对crossPath进行一次遍历,同时返回本次遍历中有冲突的Lexeme栈
168-
Stack<QuickSortSet.Cell> lexemeStack = this.forwardPath(lexemeCell , option);
169-
170-
//当前词元链并非最理想的,加入候选路径集合
171-
pathOptions.add(option.copy());
172-
173-
//存在歧义词,处理
174-
QuickSortSet.Cell c = null;
175-
while(!lexemeStack.isEmpty()){
176-
c = lexemeStack.pop();
177-
//回滚词元链
178-
this.backPath(c.getLexeme() , option);
179-
//从歧义词位置开始,递归,生成可选方案
180-
this.forwardPath(c , option);
181-
pathOptions.add(option.copy());
182-
}
183-
184-
//返回集合中的最优方案
185-
return pathOptions.first();
186-
187-
}
188-
189-
/**
190-
* 向前遍历,添加词元,构造一个无歧义词元组合
191-
// * @param LexemePath path
192-
* @return
193-
*/
194-
private Stack<QuickSortSet.Cell> forwardPath(QuickSortSet.Cell lexemeCell , LexemePath option){
195-
//发生冲突的Lexeme栈
196-
Stack<QuickSortSet.Cell> conflictStack = new Stack<QuickSortSet.Cell>();
197-
QuickSortSet.Cell c = lexemeCell;
198-
//迭代遍历Lexeme链表
199-
while(c != null && c.getLexeme() != null){
200-
//限制大长度叠词,避免性能问题和整数溢出
201-
//只对中文词元应用长度限制,因为只有中文才可能有叠词问题
202-
//对于数字、字母、字母数字混合等其他类型的词元,不限制长度
203-
if(c.getLexeme().getLexemeType() == Lexeme.TYPE_CNWORD && c.getLexeme().getLength() > 10){
204-
//跳过过长的中文叠词
205-
c = c.getNext();
206-
continue;
207-
}
208-
209-
if(!option.addNotCrossLexeme(c.getLexeme())){
210-
//词元交叉,添加失败则加入lexemeStack栈
211-
conflictStack.push(c);
212-
}
213-
c = c.getNext();
214-
}
215-
return conflictStack;
216-
}
217-
218-
/**
219-
* 回滚词元链,直到它能够接受指定的词元
220-
// * @param lexeme
221-
* @param l
222-
*/
223-
private void backPath(Lexeme l , LexemePath option){
224-
while(option.checkCross(l)){
225-
option.removeTail();
226-
}
227-
228-
}
229-
230-
}
81+
}else{
82+
//对当前的crossPath进行歧义处理
83+
LexemePath judgeResult = this.judge(crossPath);
84+
//输出歧义处理结果judgeResult
85+
context.addLexemePath(judgeResult);
86+
}
87+
}
88+
89+
/**
90+
* 为过于复杂的交叉路径构造低成本兜底路径。
91+
* 该逻辑是ik_smart歧义裁决的性能护栏,避免在高密度crossPath上进行昂贵的回溯裁决。
92+
* @param crossPath 当前待裁决的交叉词元路径
93+
* @return 如果路径复杂度超过阈值则返回兜底路径,否则返回null
94+
*/
95+
private LexemePath tryBuildFallbackPathForComplexCrossPath(LexemePath crossPath) {
96+
if (crossPath == null || crossPath.isEmpty()) {
97+
return null;
98+
}
99+
100+
if (!this.shouldFallbackForComplexCrossPath(crossPath)) {
101+
return null;
102+
}
103+
104+
return this.buildFallbackPath(crossPath);
105+
}
106+
107+
/**
108+
* 判断当前交叉路径是否已经复杂到需要跳过正常回溯裁决。
109+
*/
110+
private boolean shouldFallbackForComplexCrossPath(LexemePath crossPath) {
111+
if (crossPath.size() > TOTAL_LEXEME_COUNT_THRESHOLD) {
112+
return true;
113+
}
114+
115+
int longCnWordCount = 0;
116+
long totalLexemeLength = 0;
117+
QuickSortSet.Cell current = crossPath.getHead();
118+
while (current != null && current.getLexeme() != null) {
119+
Lexeme lexeme = current.getLexeme();
120+
121+
totalLexemeLength += lexeme.getLength();
122+
123+
if (lexeme.getLexemeType() == Lexeme.TYPE_CNWORD
124+
&& lexeme.getLength() > LONG_CN_WORD_LENGTH_THRESHOLD) {
125+
longCnWordCount++;
126+
}
127+
current = current.getNext();
128+
}
129+
130+
int pathLength = crossPath.getPathEnd() - crossPath.getPathBegin();
131+
if (pathLength <= 0) {
132+
return false;
133+
}
134+
135+
return longCnWordCount > LONG_CN_WORD_COUNT_THRESHOLD
136+
&& crossPath.size() >= DENSE_CROSS_PATH_SIZE_THRESHOLD
137+
&& totalLexemeLength >= (long) pathLength * DENSE_OVERLAP_RATIO_THRESHOLD;
138+
}
139+
140+
/**
141+
* 构造低成本兜底路径。保留首词,并将其后的覆盖区间合成为一个词元。
142+
*/
143+
private LexemePath buildFallbackPath(LexemePath crossPath) {
144+
Lexeme firstLexeme = crossPath.peekFirst();
145+
if (firstLexeme == null) {
146+
return null;
147+
}
148+
149+
LexemePath fallbackPath = new LexemePath();
150+
if (!fallbackPath.addNotCrossLexeme(firstLexeme)) {
151+
return null;
152+
}
153+
154+
int remainStart = firstLexeme.getBegin() + firstLexeme.getLength();
155+
int remainLength = crossPath.getPathEnd() - remainStart;
156+
157+
if (remainLength > 0) {
158+
Lexeme remainLexeme = new Lexeme(
159+
firstLexeme.getOffset(),
160+
remainStart,
161+
remainLength,
162+
Lexeme.TYPE_CNWORD);
163+
if (!fallbackPath.addNotCrossLexeme(remainLexeme)) {
164+
return null;
165+
}
166+
}
167+
168+
return fallbackPath;
169+
}
170+
171+
/**
172+
* 歧义识别
173+
* @param crossPath 歧义路径
174+
* @return
175+
*/
176+
private LexemePath judge(LexemePath crossPath){
177+
// 首先判断当前crossPath是否过于复杂,如果是则直接返回低成本兜底路径
178+
LexemePath fallbackPath = this.tryBuildFallbackPathForComplexCrossPath(crossPath);
179+
if (fallbackPath != null) {
180+
return fallbackPath;
181+
}
182+
183+
QuickSortSet.Cell lexemeCell = crossPath.getHead();
184+
185+
//候选路径集合
186+
TreeSet<LexemePath> pathOptions = new TreeSet<LexemePath>();
187+
//候选结果路径
188+
LexemePath option = new LexemePath();
189+
190+
//对crossPath进行一次遍历,同时返回本次遍历中有冲突的Lexeme栈
191+
Stack<QuickSortSet.Cell> lexemeStack = this.forwardPath(lexemeCell , option);
192+
193+
//当前词元链并非最理想的,加入候选路径集合
194+
pathOptions.add(option.copy());
195+
196+
//存在歧义词,处理
197+
QuickSortSet.Cell c = null;
198+
while(!lexemeStack.isEmpty()){
199+
c = lexemeStack.pop();
200+
//回滚词元链
201+
this.backPath(c.getLexeme() , option);
202+
//从歧义词位置开始,递归,生成可选方案
203+
this.forwardPath(c , option);
204+
pathOptions.add(option.copy());
205+
}
206+
207+
//返回集合中的最优方案
208+
return pathOptions.first();
209+
210+
}
211+
212+
/**
213+
* 向前遍历,添加词元,构造一个无歧义词元组合
214+
// * @param LexemePath path
215+
* @return
216+
*/
217+
private Stack<QuickSortSet.Cell> forwardPath(QuickSortSet.Cell lexemeCell , LexemePath option){
218+
//发生冲突的Lexeme栈
219+
Stack<QuickSortSet.Cell> conflictStack = new Stack<QuickSortSet.Cell>();
220+
QuickSortSet.Cell c = lexemeCell;
221+
//迭代遍历Lexeme链表
222+
while(c != null && c.getLexeme() != null){
223+
if(!option.addNotCrossLexeme(c.getLexeme())){
224+
//词元交叉,添加失败则加入lexemeStack栈
225+
conflictStack.push(c);
226+
}
227+
c = c.getNext();
228+
}
229+
return conflictStack;
230+
}
231+
232+
/**
233+
* 回滚词元链,直到它能够接受指定的词元
234+
// * @param lexeme
235+
* @param l
236+
*/
237+
private void backPath(Lexeme l , LexemePath option){
238+
while(option.checkCross(l)){
239+
option.removeTail();
240+
}
241+
242+
}
243+
244+
}

0 commit comments

Comments
 (0)