search.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * Search
  3. */
  4. var elasticsearch = require('elasticsearch'),
  5. debug = require('debug')('crowi:lib:search');
  6. function SearchClient(crowi, esUri) {
  7. this.esUri = esUri;
  8. this.crowi = crowi;
  9. var uri = this.parseUri(this.esUri);
  10. this.host = uri.host;
  11. this.index_name = uri.index_name;
  12. this.client = new elasticsearch.Client({
  13. host: this.host,
  14. });
  15. this.mappingFile = crowi.resourceDir + 'search/mappings.json';
  16. //this.Page = crowi.model('Page');
  17. //this.Config = crowi.model('Config');
  18. //this.config = crowi.getConfig();
  19. }
  20. SearchClient.prototype.parseUri = function(uri) {
  21. if (!(m = uri.match(/^elasticsearch:\/\/([^:]+):([^\/]+)\/(.+)$/))) {
  22. throw new Error('Invalid ELASTICSEARCH_URI format. Should be elasticsearch://host:port/index_name');
  23. }
  24. return {
  25. host: m[1] + ':' + m[2],
  26. index_name: m[3],
  27. };
  28. };
  29. SearchClient.prototype.buildIndex = function(uri) {
  30. return this.client.indices.create({
  31. index: this.index_name,
  32. body: require(this.mappingFile)
  33. });
  34. };
  35. SearchClient.prototype.deleteIndex = function(uri) {
  36. return this.client.indices.delete({
  37. index: this.index_name,
  38. });
  39. };
  40. SearchClient.prototype.prepareBodyForUpdate = function(body, page) {
  41. if (!Array.isArray(body)) {
  42. throw new Error('Body must be an array.');
  43. }
  44. var command = {
  45. update: {
  46. _index: this.index_name,
  47. _type: 'pages',
  48. _id: page._id.toString(),
  49. }
  50. };
  51. var document = {
  52. path: page.path,
  53. body: page.revision.body,
  54. username: page.creator.username,
  55. comment_count: page.commentCount,
  56. like_count: page.liker.length || 0,
  57. updated_at: page.updatedAt,
  58. };
  59. body.push(command);
  60. body.push(document);
  61. };
  62. SearchClient.prototype.prepareBodyForCreate = function(body, page) {
  63. if (!Array.isArray(body)) {
  64. throw new Error('Body must be an array.');
  65. }
  66. var command = {
  67. index: {
  68. _index: this.index_name,
  69. _type: 'pages',
  70. _id: page._id.toString(),
  71. }
  72. };
  73. var document = {
  74. path: page.path,
  75. body: page.revision.body,
  76. username: page.creator.username,
  77. comment_count: page.commentCount,
  78. like_count: page.liker.length || 0,
  79. created_at: page.createdAt,
  80. updated_at: page.updatedAt,
  81. };
  82. body.push(command);
  83. body.push(document);
  84. };
  85. SearchClient.prototype.addPages = function(pages)
  86. {
  87. var self = this;
  88. var body = [];
  89. pages.map(function(page) {
  90. self.prepareBodyForCreate(body, page);
  91. });
  92. return this.client.bulk({
  93. body: body,
  94. });
  95. };
  96. SearchClient.prototype.updatePages = function(pages)
  97. {
  98. var self = this;
  99. var body = [];
  100. pages.map(function(page) {
  101. self.prepareBodyForUpdate(body, page);
  102. });
  103. return this.client.bulk({
  104. body: body,
  105. });
  106. };
  107. SearchClient.prototype.addAllPages = function()
  108. {
  109. var self = this;
  110. var offset = 0;
  111. var Page = this.crowi.model('Page');
  112. var stream = Page.getStreamOfFindAll();
  113. var body = [];
  114. return new Promise(function(resolve, reject) {
  115. stream.on('data', function (doc) {
  116. if (!doc.creator || !doc.revision) {
  117. debug('Skipped', doc.path);
  118. return ;
  119. }
  120. debug('Prepare', doc);
  121. self.prepareBodyForCreate(body, doc);
  122. }).on('error', function (err) {
  123. // TODO: handle err
  124. debug('Error stream:', err);
  125. }).on('close', function () {
  126. // all done
  127. debug('Send', body);
  128. // 最後に送信
  129. self.client.bulk({ body: body, })
  130. .then(function(res) {
  131. debug('Reponse from es:', res);
  132. return resolve(res);
  133. }).catch(function(err) {
  134. debug('Err from es:', err);
  135. return reject(err);
  136. });
  137. });
  138. });
  139. };
  140. SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function()
  141. {
  142. // default is only id field, sorted by updated_at
  143. return {
  144. index: this.index_name,
  145. type: 'pages',
  146. body: {
  147. fields: ['_id'],
  148. sort: [{ updated_at: { order: 'desc'}}],
  149. query: {}, // query
  150. }
  151. };
  152. };
  153. SearchClient.prototype.createSearchQuerySortedByScore = function()
  154. {
  155. // sort by score
  156. return {
  157. index: this.index_name,
  158. type: 'pages',
  159. body: {
  160. fields: ['_id'],
  161. sort: [ {_score: { order: 'desc'} }],
  162. query: {}, // query
  163. }
  164. };
  165. };
  166. SearchClient.prototype.searchSuggest = function(keyword)
  167. {
  168. var query = this.createSearchQuerySortedByScore();
  169. query.body.query = {
  170. bool: {
  171. must: [
  172. {
  173. bool: {
  174. should: [
  175. {
  176. match: {
  177. 'path.raw': {
  178. query: sprintf('*%s*', keyword),
  179. operator: 'or'
  180. }
  181. }
  182. },
  183. {
  184. match: {
  185. 'body.ja': {
  186. query: keyword,
  187. operator: 'or'
  188. }
  189. }
  190. }
  191. ]
  192. }
  193. }
  194. ]
  195. }
  196. };
  197. return this.client.search(query);
  198. };
  199. SearchClient.prototype.searchKeyword = function(keyword)
  200. {
  201. var query = this.createSearchQuerySortedByUpdatedAt();
  202. query.body.query = {
  203. bool: {
  204. must: [
  205. {
  206. bool: {
  207. should: [
  208. {
  209. match: {
  210. 'path.raw': {
  211. query: sprintf('*%s*', keyword),
  212. operator: 'or'
  213. }
  214. }
  215. },
  216. {
  217. match: {
  218. 'body.ja': {
  219. query: keyword,
  220. operator: 'or'
  221. }
  222. }
  223. }
  224. ]
  225. }
  226. }
  227. ]
  228. }
  229. };
  230. return this.client.search(query);
  231. };
  232. SearchClient.prototype.searchByPath = function(keyword, prefix)
  233. {
  234. var query = this.createSearchQuerySortedByUpdatedAt();
  235. };
  236. SearchClient.prototype.searchKeywordUnderPath = function(keyword, path)
  237. {
  238. var query = this.createSearchQuerySortedByUpdatedAt();
  239. };
  240. module.exports = SearchClient;
  241. /*
  242. lib.searchPageByKeyword = function(keyword) {
  243. var queryBody = {
  244. query: {
  245. bool: {
  246. should: [
  247. {term: { path: { term: keyword, boost: 2.0 } }},
  248. {term: { body: { term: keyword } }}
  249. ]
  250. }
  251. },
  252. highlight : { fields : { body : {} } },
  253. //sort: [{ updated: { order: "desc" } } ]
  254. };
  255. return client.search({
  256. index: index_name,
  257. body: queryBody
  258. });
  259. };
  260. */