11.5.3 封装聊天服务类

聊天服务类的主要作用有3个:①获取问答知识表的所有记录,并对其创建索引;②从索引文件中检索匹配指定问题的问答知识;③封装一个提供给外部使用的聊天方法。聊天服务类的实现如下:

  1. 1 package org.liufeng.course.service;
  2. 2 
  3. 3 import java.io.File;
  4. 4 import java.util.List;
  5. 5 import java.util.Random;
  6. 6 import org.apache.lucene.document.Document;
  7. 7 import org.apache.lucene.document.IntField;
  8. 8 import org.apache.lucene.document.StringField;
  9. 9 import org.apache.lucene.document.TextField;
  10. 10 import org.apache.lucene.document.Field.Store;
  11. 11 import org.apache.lucene.index.IndexReader;
  12. 12 import org.apache.lucene.index.IndexWriter;
  13. 13 import org.apache.lucene.index.IndexWriterConfig;
  14. 14 import org.apache.lucene.queryparser.classic.QueryParser;
  15. 15 import org.apache.lucene.search.IndexSearcher;
  16. 16 import org.apache.lucene.search.Query;
  17. 17 import org.apache.lucene.search.ScoreDoc;
  18. 18 import org.apache.lucene.search.TopDocs;
  19. 19 import org.apache.lucene.store.Directory;
  20. 20 import org.apache.lucene.store.FSDirectory;
  21. 21 import org.apache.lucene.util.Version;
  22. 22 import org.liufeng.course.pojo.Knowledge;
  23. 23 import org.liufeng.course.util.MySQLUtil;
  24. 24 import org.wltea.analyzer.lucene.IKAnalyzer;
  25. 25 
  26. 26 /**
  27. 27  * 聊天服务类
  28. 28  *
  29. 29  * @author liufeng
  30. 30  * @date 2013-12-01
  31. 31  */
  32. 32 public class ChatService {
  33. 33  /**
  34. 34  * 得到索引存储目录
  35. 35  *
  36. 36  * @return WEB-INF/classes/index/
  37. 37  */
  38. 38  public static String getIndexDir() {
  39. 39  // 得到.class文件所在路径(WEB-INF/classes/)
  40. 40  String classpath = ChatService.class.getResource("/").getPath();
  41. 41  // 将classpath中的%20替换为空格
  42. 42  classpath = classpath.replaceAll("%20", " ");
  43. 43  // 索引存储位置:WEB-INF/classes/index/
  44. 44  return classpath + "index/";
  45. 45  }
  46. 46 
  47. 47  /**
  48. 48  * 创建索引
  49. 49  */
  50. 50  public static void createIndex() {
  51. 51  // 取得问答知识库中的所有记录
  52. 52  List<Knowledge> knowledgeList = MySQLUtil.findAllKnowledge();
  53. 53  Directory directory = null;
  54. 54  IndexWriter indexWriter = null;
  55. 55  try {
  56. 56  directory = FSDirectory.open(new File(getIndexDir()));
  57. 57  IndexWriterConfig iwConfig = new IndexWriterConfig(Version.LUCENE_46,
  58. 58  new IKAnalyzer(true));
  59. 59  indexWriter = new IndexWriter(directory, iwConfig);
  60. 60  Document doc = null;
  61. 61  // 遍历问答知识库创建索引
  62. 62  for (Knowledge knowledge : knowledgeList) {
  63. 63  doc = new Document();
  64. 64  // 对question进行分词存储
  65. 65  doc.add(new TextField("question", knowledge.getQuestion(),
  66.   Store.YES));
  67. 66  // 对id、answer和category不分词存储
  68. 67  doc.add(new IntField("id", knowledge.getId(), Store.YES));
  69. 68  doc.add(new StringField("answer", knowledge.getAnswer(), Store.YES));
  70. 69  doc.add(new IntField("category", knowledge.getCategory(), Store.YES));
  71. 70  indexWriter.addDocument(doc);
  72. 71  }
  73. 72  indexWriter.close();
  74. 73  directory.close();
  75. 74  } catch (Exception e) {
  76. 75  e.printStackTrace();
  77. 76  }
  78. 77  }
  79. 78 
  80. 79  /**
  81. 80  * 从索引文件中根据问题检索答案
  82. 81  *
  83. 82  * @param content
  84. 83  * @return Knowledge
  85. 84  */
  86. 85  @SuppressWarnings("deprecation")
  87. 86  private static Knowledge searchIndex(String content) {
  88. 87  Knowledge knowledge = null;
  89. 88  try {
  90. 89  Directory directory = FSDirectory.open(new File(getIndexDir()));
  91. 90  IndexReader reader = IndexReader.open(directory);
  92. 91  IndexSearcher searcher = new IndexSearcher(reader);
  93. 92  // 使用查询解析器创建Query
  94. 93  QueryParser questParser = new QueryParser(Version.LUCENE_46,
  95. 94  "question", new IKAnalyzer(true));
  96. 95  Query query = questParser.parse(QueryParser.escape(content));
  97. 96  // 检索得分最高的文档
  98. 97  TopDocs topDocs = searcher.search(query, 1);
  99. 98  if (topDocs.totalHits > 0) {
  100. 99  knowledge = new Knowledge();
  101. 100  ScoreDoc[] scoreDoc = topDocs.scoreDocs;
  102. 101  for (ScoreDoc sd : scoreDoc) {
  103. 102  Document doc = searcher.doc(sd.doc);
  104. 103  knowledge.setId(doc.getField("id").numericValue().intValue());
  105. 104  knowledge.setQuestion(doc.get("question"));
  106. 105  knowledge.setAnswer(doc.get("answer"));
  107. 106 
  108. 107  knowledge.setCategory(doc.getField("category")
  109.   .numericValue().intValue());
  110. 108  }
  111. 109  }
  112. 110  reader.close();
  113. 111  directory.close();
  114. 112  } catch (Exception e) {
  115. 113  knowledge = null;
  116. 114  e.printStackTrace();
  117. 115  }
  118. 116  return knowledge;
  119. 117  }
  120. 118 
  121. 119  /**
  122. 120  * 聊天方法(根据question返回answer)
  123. 121  *
  124. 122  * @param openId 用户的OpenID
  125. 123  * @param createTime 消息创建时间
  126. 124  * @param question 用户上行的问题
  127. 125  * @return answer
  128. 126  */
  129. 127  public static String chat(String openId, String createTime, String question) {
  130. 128  String answer = null;
  131. 129  int chatCategory = 0;
  132. 130  Knowledge knowledge = searchIndex(question);
  133. 131  // 找到匹配项
  134. 132  if (null != knowledge) {
  135. 133  // 笑话
  136. 134  if (2 == knowledge.getCategory()) {
  137. 135  answer = MySQLUtil.getJoke();
  138. 136  chatCategory = 2;
  139. 137  }
  140. 138  // 上下文
  141. 139  else if (3 == knowledge.getCategory()) {
  142. 140  // 判断上一次的聊天类别
  143. 141  int category = MySQLUtil.getLastCategory(openId);
  144. 142  // 如果是笑话,本次继续回复笑话给用户
  145. 143  if (2 == category) {
  146. 144  answer = MySQLUtil.getJoke();
  147. 145  chatCategory = 2;
  148. 146  } else {
  149. 147  answer = knowledge.getAnswer();
  150. 148  chatCategory = knowledge.getCategory();
  151. 149  }
  152. 150  }
  153. 151  // 普通对话
  154. 152  else {
  155. 153  answer = knowledge.getAnswer();
  156. 154  // 如果答案为空,根据知识id从问答知识分表中随机获取一条
  157. 155  if ("".equals(answer))
  158. 156  answer = MySQLUtil.getKnowledSub(knowledge.getId());
  159. 157  chatCategory = 1;
  160. 158  }
  161. 159  }
  162. 160  // 未找到匹配项
  163. 161  else {
  164. 162  answer = getDefaultAnswer();
  165. 163  chatCategory = 0;
  166. 164  }
  167. 165  // 保存聊天记录
  168. 166  MySQLUtil.saveChatLog(openId, createTime, question, answer, chatCategory);
  169. 167  return answer;
  170. 168  }
  171. 169 
  172. 170  /**
  173. 171  * 随机获取一个默认的答案
  174. 172  *
  175. 173  * @return
  176. 174  */
  177. 175  private static String getDefaultAnswer() {
  178. 176  String []answer = {
  179. 177  "要不我们聊点别的?",
  180. 178  "恩?你到底在说什么呢?",
  181. 179  "没有听懂你说的,能否换个说法?",
  182. 180  "虽然不明白你的意思,但我却能用心去感受",
  183. 181  "听得我一头雾水,阁下的知识真是渊博呀,膜拜~",
  184. 182  "真心听不懂你在说什么,要不你换种表达方式如何?",
  185. 183  "哎,我小学语文是体育老师教的,理解起来有点困难哦",
  186. 184  "是世界变化太快,还是我不够有才?为何你说话我不明白?"
  187. 185  };
  188. 186  return answer[getRandomNumber(answer.length)];
  189. 187  }
  190. 188 
  191. 189  /**
  192. 190  * 随机生成 0~length-1 之间的某个值
  193. 191  *
  194. 192  * @return int
  195. 193  */
  196. 194  private static int getRandomNumber(int length) {
  197. 195  Random random = new Random();
  198. 196  return random.nextInt(length);
  199. 197  }
  200. 198 }

上述代码的主要说明如下。

第65~69行:将knowledge表的4个字段id、question、answer和category都存储到索引中,其中,id、answer和category这3个字段原样(不分词)存储在索引中,而question字段是分词存储,因此聊天是根据question进行检索。

第127~168行:这是聊天机器人的核心业务逻辑。接收到用户发送的消息时,首先从索引中检索是否有匹配的问答知识,如果没有,随机返回一条默认的回复;如果有,则进一步判断问答知识的类型。如果类型为2(笑话),就从joke表中随机查询一条笑话;如果类型为3(上下文),则判断上一条聊天是否与笑话有关,如果是,继续返回笑话;如果类型为1(普通对话),还需要判断当前问答知识是否对应多个答案。

第175~187行:getDefaultAnswer()方法定义了8条默认答案,当机器人不能应答时,随机返回其中一条。