解决jive搜索结果中的中文搜索字符串高亮度显示的方法

03-07-25 zhuojun
1.问题:在搜索结果中,命中的中文字符串不能正确地高亮度显示。
2.工作过程:Jive用搜索字符串生成一个正则表达式,然后在搜索得到的结果文本中用该表达式做替换操作,
如果匹配成功,就进行替换操作。不能匹配,则不进行替换操作。替换操作的结果是将Html格式语句和搜索
字符串插入到结果文本中,显示时候就可以看见高亮度的字符串。
jive中的jakarta-oro-XXX包中含有兼容Perl5正则表达式的类,jive使用其substitute()方法实现替换操作。
3.原因:jive生成如下的Perl5锚模式的正则表达式:
  s/\b(XXX)\b/<font style='background-color:ffff00'><b>$1<\/b><\/font>/igm
  XXX表示匹配字符串。上式含意看看正则表达式的资料就明白了。
  问题出在模式部分"\b(XXX)\b"。这样的匹配是要求首尾做边界匹配(单词匹配),中文书写格式中,边界一
  般是开始的空格和句中及结尾的标点符号,所以完成匹配很困难。如:
  月落乌啼霜满天,江枫渔火对愁眠。
  只有”\b(月落乌啼霜满天)\b”和“\b(江枫渔火对愁眠)\b”两个匹配是正确的,诸如”\b(月落)\b”、”\b(乌啼)\b”、
  ”\b(满天)\b”等都无法匹配。因此也就不能完成替换操作。只有搜索字符串正好被分隔符包围,才能完成替换操作。
  所以实际使用中经常是有时中文可以高亮度显示,而大部分则不行。就是因为一般的查询很难满足上述条件。
3.修改方法:
  (1)分析此处的匹配要求是:中文任意位置匹配,英文单词匹配。
     要求写出这样的正则表达式挺难(不能?),如有熟悉正则表达式的请指正。
  (2)最简单的方法是去掉匹配的参数b,做任意匹配处理。即使用这样的正则表达式:
     s/(XXX)/<font style='background-color:ffff00'><b>$1<\/b><\/font>/igm
     找到com.jivesoftware.util.StringUtils.java文件中的highlightWords()方法的下列语句,修改为注释中的语句。
     regexp.insert(0, "s/\\b(");  // 修改此句为:regexp.insert(0, "s/(");
     // The word list is here already, so just append the rest.
     regexp.append(")\\b/");      // 修改此句为regexp.append(")/");
     regexp.append(startHighlight);
     regexp.append("$1");
     regexp.append(endHighlight);
     regexp.append("/igm");
     这样修改最方便,对中文效果不错,但是对英文会出现单词中匹配,如:
     give me a pen or pencil.
     搜索“pen”时,pen和pencil中的前三个字母都会作为高亮度显示出来,后一种情况看起来很别扭。
  (2)如果做的考究一点,可以将中文和英文分开处理。使用这样的正则表达式:
     s/(XXX|\bYYY\b)/<font style='background-color:ffff00'><b>$1$2<\/b><\/font>/igm
     XXX表示中文匹配字符串,YYY表示英文匹配字符串。修改的程序如下:
  /**
   * Highlights words in a string. Words matching ignores case. The actual
   * higlighting method is specified with the start and end higlight tags.
   * Those might be beginning and ending HTML bold tags, or anything else.<p>
   *
   * This method is under the Jive Open Source Software License and was
   * written by Mark Imbriaco.
   *
   * @param string the String to highlight words in.
   * @param words an array of words that should be highlighted in the string.
   * @param startHighlight the tag that should be inserted to start highlighting.
   * @param endHighlight the tag that should be inserted to end highlighting.
   * @return a new String with the specified words highlighted.
   */
  /**
   * 一个匹配"中文" 和 “english"的正则表达式:
   * 英文做单词匹配(单词首尾边界匹配),中文任意匹配。
   * s/(中文|\benglish\b)/<font style='background-color:ffff00'><b>$1$2<\/b><\/font>/igm
   * 选项说明
   * i 忽略模式中的大小写
   * g 改变模式中的所有匹配
   * m 将待匹配串视为多行
   *
   * 2003-6-22
   */

  public static final String highlightWords(String string, String[] words,
					    String startHighlight,
					    String endHighlight) {
    if (string == null || words == null ||
	startHighlight == null || endHighlight == null) {
      return null;
    }

    StringBuffer regexp = new StringBuffer();

    // Iterate through each word and generate a word list for the regexp.
    for (int x = 0; x < words.length; x++) {
      // Excape "|" and "/"  to keep us out of trouble in our regexp.
      words[x] = perl5Util.substitute("s#([\\|\\/\\.])#\\\\$1$2g", words[x]);
      if (regexp.length() > 0) {
	regexp.append("|");
      }
      if (words[x].charAt(0) > 127) {
// 中文不做边界匹配
	regexp.append(words[x]);
      }
      else {
// 英文单词做边界匹配检查
	try {
	  PatternCompiler compiler = new Perl5Compiler();
	  String regex = "\\b" + words[x].toString() + "\\b";
	  Pattern pattern = compiler.compile(regex);
	  PatternMatcher matcher = new Perl5Matcher();
	  if (matcher.contains(string, pattern)) {
// 如果有边界匹配的单词,正则表达式使用边界匹配
	    regexp.append(regex.toString());
	  }
	  else {
// 使用任意匹配
	    regexp.append(words[x]);
	  }
	}
	catch (Exception e) {
	  System.out.println(e);
	}
      }
    }

    // Escape the regular expression delimiter ("/").
    startHighlight = perl5Util.substitute("s#\\/#\\\\/g", startHighlight);
    endHighlight = perl5Util.substitute("s#\\/#\\\\/g", endHighlight);

    // Build the regular expression. insert() the first part.
    regexp.insert(0, "s/(");
    // The word list is here already, so just append the rest.
    regexp.append(")/");
    regexp.append(startHighlight);
    regexp.append("$1$2");
    regexp.append(endHighlight);
    regexp.append("/igm");

    // Do the actual substitution via a simple regular expression.
    return perl5Util.substitute(regexp.toString(), string);
  }
  
4.给搜索结果的标题(subject)部分加上高亮度显示
  在search.jsp文件中找到有highlightWords()方法的一行,在前面加入两句
  String subject = StringUtils.escapeHTMLTags(message.getUnfilteredSubject());
  subject = StringUtils.highlightWords(subject, queryWords, "<font style='background-color:ffff00'><b>", "</b></font>");
  后面将subject给浏览器就行了。参见下面代码的注释。
  ......
  while (results.hasNext()) {
    ForumMessage message = (ForumMessage)results.next();
// 添加的语句。subject中命中字符串的高亮显示
    String subject = StringUtils.escapeHTMLTags(message.getUnfilteredSubject());
    subject = StringUtils.highlightWords(subject, queryWords, "<font style='background-color:ffff00'><b>", "</b></font>");
//
    String body = StringUtils.escapeHTMLTags(message.getUnfilteredBody());
    body = StringUtils.chopAtWord(body, 150) + " ...";
    body = StringUtils.highlightWords(body, queryWords, "<font style='background-color:ffff00'><b>", "</b></font>");
    long mID = message.getID();
......
   <td>
   <font class=p2 face="<%= JiveGlobals.getJiveProperty("skin.default.fontFace") %>">
   <a href="thread.jsp?forum=<%= fID %>&thread=<%= tID %>&message=<%= mID %>&redirect=true&hilite=true&q=<%= StringUtils.ISOtoGBK(queryText) %>"
<%-- 将message.getSubject()改为subject。subject高亮度显示  --%>
<%-- ><%= message.getSubject() %></a> --%>
   ><%= subject %></a>
......
   <%  } // end while loop
   %>


参见正则表达式的有关资料。
<p>

allain
2003-08-01 22:07
看来zhuojun兄对jive做了深入研究了,我也遇到过这个问题

可我不懂正则表达式,只好又写了方法,专门处理中文关键字

高亮问题,用的是最笨的方法:search-replace

呵呵,看来要向zhuojun多学习一下

zhuojun
2003-08-04 08:18
上面的仁兄过奖了。不瞒你说,原本为学Java来看Jive,结果是看了Lucene、正则表达式、JavaCC、JTree,Java倒没有看多少。我要是能写个search-replace,感觉一定很好,毕竟是要学习Java。

allain
2003-08-08 12:54
你说的方法试验过,好用!

luost
2003-08-08 14:43
//subject = StringUtils.highlightWords(subject, queryWords, "<font style='background-color:ffff00'><b>", "</b></font>");

//body = StringUtils.highlightWords(body, queryWords, "<font style='background-color:ffff00'><b>", "</b></font>");

我将涉及highlightWords方法的上面两个句子注释掉,就可以正常搜索,只是不能高亮显示,但不注释掉,就出现下面的错误,我看过代码,是Perl5Util.substitute方法有问题,但我不可能去改这个方法吧,我查询的中文英文都会出现下面的错误,不知是什么原因,有没有人碰到和我一样的问题??请zhuojun和斑竹帮忙解决一下!!!谢了!!

org.apache.oro.text.perl.MalformedPerl5PatternException: Invalid option: b

at org.apache.oro.text.perl.Perl5Util.substitute(Perl5Util.java:665)

at com.jivesoftware.util.StringUtils.highlightWords(StringUtils.java:753)

at jsp_servlet.__search._jspService(__search.java:467)

at weblogic.servlet.jsp.JspBase.service(JspBase.java:27)

at weblogic.servlet.internal.ServletStubImpl$ServletInvocationAction.run(ServletStubImpl.java:1058)

at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:401)

at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:306)

at weblogic.servlet.internal.WebAppServletContext$ServletInvocationAction.run(WebAppServletContext.java:5412)

at weblogic.security.service.SecurityServiceManager.runAs(SecurityServiceManager.java:744)

at weblogic.servlet.internal.WebAppServletContext.invokeServlet(WebAppServletContext.java:3086)

at weblogic.servlet.internal.ServletRequestImpl.execute(ServletRequestImpl.java:2544)

at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:153)

at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:134)

zhuojun
2003-08-08 16:36
>org.apache.oro.text.perl.MalformedPerl5PatternException: Invalid option: b

错误是正则表达式中b选项不对,但一般不会是Perl5的方法问题.最有可能的是中文问题,JSP中文参数进了Java中没有转换成正确的Unicode码.你要保证在调用Perl5方法时的参数为:

s/\b(XXX)\b/<font style='background-color:ffff00'><b>$1<\/b><\/font>/igm
<p>

如果中文是乱码,会将正则表达式的格式破坏了,就会碰到非法的参数.

我原来好像碰到过,并没有修改过高亮度的代码就好了.

如果格式是对的,再怀疑Perl5的方法.可以当个jakarta-oro-XXX包换一下试试.

供参考.

xiaohu
2004-01-02 11:56
首先指出原文的正则表达式的写法存在的问题,原文写道:

『(3)如果做的考究一点,可以将中文和英文分开处理。使用这样的正则表达式:

s/(XXX|\bYYY\b)/<font color=red>$1$2</font>/igm        』

实际上这里的“$2”是多余的,因为它前面只有1个引用(即一对括号)。只要写成

s/(XXX|\bYYY\b)/<font color=red>$1</font>/igm

就可以了。

按照原文的方法,对于英文单词搜索使用表达式“s/(\bYYY\b)/$1/igm”,这样可以解决大部分问题,可是遇到英文夹在汉字中间的情况就会出现问题,如:

“我的enemy出现了。He is also your enemy. ”

如果使用边界匹配的话,这句话中前一个“enemy”是不会被匹配到的;但是不使用边界匹配确实又可能会有误匹配,怎么办?其实可以把边界的条件放宽一些,就能解决这个问题了,使用下面的匹配句式:

s/([^A-Za-z0-9_]|\b)<font color=blue>(XXX)</font>([^A-Za-z0-9_]|\b)/$1<mark><font color=blue>$2</font></mark>$3/igm

这里使用的边界不单是“\b”了,而是“[^A-Za-z0-9_]|\b”,也就是说,传统的“\b”边界或者是其它非“字母数字”符号作为边界(“字母数字”符号特指英文字母A-Z或a-z,阿拉伯数字0-9以及下划线'_')。在“s/ .. /”中间有3对括号,即有3个匹配结果――前边界、单词、后边界――分别对应$1、$2、$3;替换时在$1和$2之间加入tag(这里的例子是“<mark>”),在$2和$3之间加入tag的结束符(这里的例子是“</mark>”),这样就完成了特定英文单词的高亮显示。

下面接着原作者的思路解决中英文混排时高亮显示的问题,程序如下:

StringBuffer regexp = new StringBuffer();
StringBuffer regexp_cn = new StringBuffer();	 // for Chinese character
// Iterate through each word and generate a word list for the regexp or regexp_cn 
for (int x = 0; x < words.length; x++) {
	// Excape "|" and "/"  to keep us out of trouble in our regexp.  
	words[x] =
		perl5Util.substitute("s#([\\|\\/\\.])#\\\\$1$2g", words[x]);
	if (words[x].charAt(0) > 127) {
		// 中文不做边界匹配	
		if (regexp_cn.length() > 0) {
			regexp_cn.append("|");
		}
		regexp_cn.append(words[x]);
	} else {
		// 英文单词做放宽条件的边界匹配检查 --- 边界为:[^A-Za-z0-9_]|\b	
		if (regexp.length() > 0) {
			regexp.append("|");
		}
		regexp.append(words[x]);
	}
}
// Escape the regular expression delimiter ("/").    
startHighlight = perl5Util.substitute("s#\\/#\\\\/g", startHighlight);
endHighlight = perl5Util.substitute("s#\\/#\\\\/g", endHighlight);

String rs = string;
// if keywords include English (or other western language words):
if (regexp.length() > 0) { 
	// Build the regular expression. insert() the first part.    
	regexp.insert(0, "s/([^A-Za-z0-9_]|\\b)(");
	// The word list is here already, so just append the rest.    
	regexp.append(")([^A-Za-z0-9_]|\\b)/$1");
	regexp.append(startHighlight);
	regexp.append("$2");
	regexp.append(endHighlight);
	regexp.append("$3/igm");
	// Do the actual substitution via a simple regular expression.
	rs = perl5Util.substitute(regexp.toString(), string);
}

// if keywords include Chinese (or Japanese, Korean words):
if (regexp_cn.length() > 0) { 
	// Build the regular expression. insert() the first part.    
	regexp_cn.insert(0, "s/(");
	// The word list is here already, so just append the rest.    
	regexp_cn.append(")/");
	regexp_cn.append(startHighlight);
	regexp_cn.append("$1");
	regexp_cn.append(endHighlight);
	regexp_cn.append("/igm");
	// Do the actual substitution via a simple regular expression.
	rs = perl5Util.substitute(regexp_cn.toString(), rs);
}

return rs;
<p>

问题到这里似乎已经解决了,可是Jive搜索功能还有一项“Supports Stemming”(例如搜索单词"talk"可以得到"talk"、"talking"以及"talked"), 如果此功能为开启的话,问题就没有这么简单了――幸好设为中文(韩日)搜索时不是“stemming”的。

zhuojun
2004-01-04 09:24
xiaohu兄对正则表达式很熟悉呀,多谢指正。

xiaohu
2004-01-05 18:42
哪里,zhuojun兄的思路给了我很大的启发,小弟现学现卖,献丑了。

不过还有一点疑问,判断汉字时用

if (words[x].charAt(0) > 127) { ... }
<p>

好像不太好吧。话说回来,反正现在这样也可以用了,总之要多谢zhunjun兄的慷慨分享,bow

猜你喜欢