Sotaro KARASAWA 10 years ago
parent
commit
c69591e7a0
5 changed files with 238 additions and 149 deletions
  1. 148 103
      lib/util/search.js
  2. 7 0
      lib/util/swigFunctions.js
  3. 55 46
      lib/views/layout/2column.html
  4. 2 0
      lib/views/page_list.html
  5. 26 0
      resource/css/_search.scss

+ 148 - 103
lib/util/search.js

@@ -41,6 +41,38 @@ SearchClient.prototype.buildIndex = function(uri) {
   });
 };
 
+SearchClient.prototype.deleteIndex = function(uri) {
+  return this.client.indices.delete({
+    index: this.index_name,
+  });
+};
+
+SearchClient.prototype.prepareBodyForUpdate = function(body, page) {
+  if (!Array.isArray(body)) {
+    throw new Error('Body must be an array.');
+  }
+
+  var command = {
+    update: {
+      _index: this.index_name,
+      _type: 'pages',
+      _id: page._id.toString(),
+    }
+  };
+
+  var document = {
+    path: page.path,
+    body: page.revision.body,
+    username: page.creator.username,
+    comment_count: page.commentCount,
+    like_count: page.liker.length || 0,
+    updated_at: page.updatedAt,
+  };
+
+  body.push(command);
+  body.push(document);
+};
+
 SearchClient.prototype.prepareBodyForCreate = function(body, page) {
   if (!Array.isArray(body)) {
     throw new Error('Body must be an array.');
@@ -82,6 +114,20 @@ SearchClient.prototype.addPages = function(pages)
   });
 };
 
+SearchClient.prototype.updatePages = function(pages)
+{
+  var self = this;
+  var body = [];
+
+  pages.map(function(page) {
+    self.prepareBodyForUpdate(body, page);
+  });
+
+  return this.client.bulk({
+    body: body,
+  });
+};
+
 SearchClient.prototype.addAllPages = function()
 {
   var self = this;
@@ -99,15 +145,13 @@ SearchClient.prototype.addAllPages = function()
 
       debug('Prepare', doc);
       self.prepareBodyForCreate(body, doc);
-      //debug('Data received: ', doc.path, doc.liker.length, doc.revision.body);
     }).on('error', function (err) {
+      // TODO: handle err
       debug('Error stream:', err);
-      // handle err
     }).on('close', function () {
       // all done
-      debug('Close');
 
-      debug('SEnd', body);
+      debug('Send', body);
       // 最後に送信
       self.client.bulk({ body: body, })
       .then(function(res) {
@@ -121,112 +165,119 @@ SearchClient.prototype.addAllPages = function()
   });
 };
 
-module.exports = SearchClient;
-
-/*
-
-
-SearchClient.prototype.deleteIndex = function() {
+SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function()
+{
+  // default is only id field, sorted by updated_at
+  return {
+    index: this.index_name,
+    type: 'pages',
+    body: {
+      fields: ['_id'],
+      sort: [{ updated_at: { order: 'desc'}}],
+      query: {}, // query
+    }
+  };
 };
-*/
 
-/*
-module.exports = function(crowi) {
-  var elasticsearch = require('elasticsearch'),
-    debug = require('debug')('crowi:lib:search'),
-    Page = crowi.model('Page'),
-    Config = crowi.model('Config'),
-    config = crowi.getConfig(),
-    TYPE_PAGE = 'page',
-    SLOW_INTERVAL = 200, // 200ms interval.
-    lib = {};
-
-  // TODO: configurable
-  var host = '127.0.0.1:9200';
-  var index_name = 'crowi';
-  var default_mapping_file = crowi.resourceDir + 'search/mappings.json';
-
-  var client = new elasticsearch.Client({
-    host: host,
-  });
-
-
-  lib.deleteIndex = function() {
-    return client.indices.delete({
-      index: index_name
-    });
+SearchClient.prototype.createSearchQuerySortedByScore = function()
+{
+  // sort by score
+  return {
+    index: this.index_name,
+    type: 'pages',
+    body: {
+      fields: ['_id'],
+      sort: [ {_score: { order: 'desc'} }],
+      query: {}, // query
+    }
   };
+};
 
-  lib.buildIndex = function() {
-    return client.indices.create({
-      index: index_name,
-      body: require(default_mapping_file)
-    });
+SearchClient.prototype.searchSuggest = function(keyword)
+{
+  var query = this.createSearchQuerySortedByScore();
+
+  query.body.query = {
+    bool: {
+      must: [
+        {
+          bool: {
+            should: [
+              {
+                match: {
+                  'path.raw': {
+                    query: sprintf('*%s*', keyword),
+                    operator: 'or'
+                  }
+                }
+              },
+              {
+                match: {
+                  'body.ja': {
+                    query: keyword,
+                    operator: 'or'
+                  }
+                }
+              }
+            ]
+          }
+        }
+      ]
+    }
   };
 
-  lib.rebuildIndex = function() {
-    var self = this;
+  return this.client.search(query);
+};
 
-    return self.deleteIndex()
-    .then(function(data) {
-      return self.buildIndex();
-    });
+SearchClient.prototype.searchKeyword = function(keyword)
+{
+  var query = this.createSearchQuerySortedByUpdatedAt();
+
+  query.body.query = {
+    bool: {
+      must: [
+        {
+          bool: {
+            should: [
+              {
+                match: {
+                  'path.raw': {
+                    query: sprintf('*%s*', keyword),
+                    operator: 'or'
+                  }
+                }
+              },
+              {
+                match: {
+                  'body.ja': {
+                    query: keyword,
+                    operator: 'or'
+                  }
+                }
+              }
+            ]
+          }
+        }
+      ]
+    }
   };
 
-  lib.addAllPages = function() {
-    var offset = 0;
-    var stream = Page.getStreamOfFindAll();
-    var self = this;
-
-    stream.on('data', function (doc) {
-      if (!doc.creator || !doc.revision) {
-        debug('Skipped', doc.path);
-        return ;
-      }
-
-      var likeCount = doc.liker.length;
-      var bookmarkCount = 0; // TODO
-      var updated = doc.updatedAt; // TODO
-
-      self.addPage(doc._id.toString(), doc.path, doc.revision.body, doc.creator.username, likeCount, bookmarkCount, updated, true)
-      .then(function(data) {
-        debug('Page Added', data);
-      }).catch(function (err) {
-        debug('Error addPage:', err);
-      });
-
-      //debug('Data received: ', doc.path, doc.liker.length, doc.revision.body);
-    }).on('error', function (err) {
-      debug('Error stream:', err);
-      // handle err
-    }).on('close', function () {
-      debug('Close');
-      // all done
-    });
-  };
+  return this.client.search(query);
+};
 
-  lib.addPage = function(id, path, body, creator, likeCount, bookmarkCount, updated, is_public) {
-    var self = this;
+SearchClient.prototype.searchByPath = function(keyword, prefix)
+{
+  var query = this.createSearchQuerySortedByUpdatedAt();
+};
 
-    return client.create({
-      index: index_name,
-      type: 'page',
-      id: id,
-      body: {
-        path: path,
-        body: body,
-        creator: creator,
-        likeCount: likeCount,
-        bookmarkCount: bookmarkCount,
-        is_public: is_public,
-        updated: updated,
-      }
-    });
+SearchClient.prototype.searchKeywordUnderPath = function(keyword, path)
+{
+  var query = this.createSearchQuerySortedByUpdatedAt();
+};
 
-  };
+module.exports = SearchClient;
 
-  lib.updatePage = function(id, path, body, creator, likeCount, bookmarkCount, updated, is_public) {
-  };
+/*
 
   lib.searchPageByKeyword = function(keyword) {
     var queryBody = {
@@ -248,10 +299,4 @@ module.exports = function(crowi) {
 
   };
 
-  lib.searchPageByLikeCount = function() {
-  };
-
-  return lib;
-};
-
 */

+ 7 - 0
lib/util/swigFunctions.js

@@ -15,6 +15,13 @@ module.exports = function(crowi, app, locals) {
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
 
+  locals.searchConfigured = function() {
+    if (crowi.getSearcher()) {
+      return true;
+    }
+    return false;
+  };
+
   locals.slackConfigured = function() {
     var config = crowi.getConfig()
     if (Config.hasSlackToken(config)) {

+ 55 - 46
lib/views/layout/2column.html

@@ -8,7 +8,20 @@
       <img alt="Crowi" src="/logo/32x32.png" width="16">
       {% block title %}{{ config.crowi['app:title'] }}{% endblock %}
     </a>
+  {% if searchConfigured() %}
+  <form class="navbar-form navbar-left search-top" role="search">
+    <div class="form-group input-group search-top-input-group">
+      <input type="text" id="search-top-input" class="search-top-input form-control" placeholder="Search ...">
+      <span class="input-group-btn">
+        <button class="btn btn-default" type="button"><i class="search-top-icon fa fa-search"></i></button>
+      </span>
+    </div>
+  </form>
+  <div class="search-suggest" id="search-suggest">
   </div>
+  {% endif %}
+  </div>
+
 
   <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbarCollapse">
     <span class="sr-only">Toggle navigation</span>
@@ -18,10 +31,6 @@
   </button>
   <!-- Collect the nav links, forms, and other content for toggling -->
   <div class="collapse navbar-collapse" id="navbarCollapse">
-    <form id="headerSearch" class="navbar-form navbar-left form-inline" role="search" action="/_search">
-      <input id="searchQuery" name="q" type="text" class="form-control" placeholder="検索文字...">
-      <button type="submit" class="btn btn-default">検索</button>
-    </form>
 
     <ul class="nav navbar-nav navbar-right">
 
@@ -46,18 +55,18 @@
           <i class="fa fa-globe"></i> <span class="badge badge-danger">6</span>
         </a>
         <script>
-          $('#notif-opener').popover({
-            placement: 'bottom',
-            trigger: 'manual',
-            html: 'true',
-            content: function () {
-              return '<div></div>';
-            }
-          });
-          $('#notif-opener').click(function(e) {
-            $('#notif-opener').popover('show');
-            return false;
-          });
+$('#notif-opener').popover({
+  placement: 'bottom',
+  trigger: 'manual',
+  html: 'true',
+  content: function () {
+    return '<div></div>';
+  }
+});
+$('#notif-opener').click(function(e) {
+  $('#notif-opener').popover('show');
+  return false;
+});
         </script>
       </li>
       #}
@@ -97,19 +106,19 @@
 
 <a href="" class=" hidden-xs hidden-sm layout-control" id="toggle-sidebar"><i class="fa fa-chevron-right"></i> <span class="hide-on-affix-top"></span></a>
 <script>
-  $(function() {
-    $('#toggle-sidebar').click(function(e) {
-      var $mainContainer = $('.main-container');
-      if ($mainContainer.hasClass('aside-hidden')) {
-        $('.main-container').removeClass('aside-hidden');
-        $.cookie('aside-hidden', 0, { expires: 30, path: '/' });
-      } else {
-        $mainContainer.addClass('aside-hidden');
-        $.cookie('aside-hidden', 1, { expires: 30, path: '/' });
-      }
-      return false;
-    });
+$(function() {
+  $('#toggle-sidebar').click(function(e) {
+    var $mainContainer = $('.main-container');
+    if ($mainContainer.hasClass('aside-hidden')) {
+      $('.main-container').removeClass('aside-hidden');
+      $.cookie('aside-hidden', 0, { expires: 30, path: '/' });
+    } else {
+      $mainContainer.addClass('aside-hidden');
+      $.cookie('aside-hidden', 1, { expires: 30, path: '/' });
+    }
+    return false;
   });
+});
 </script>
 <aside class="sidebar col-md-3 hidden-xs hidden-sm hidden-print">
 
@@ -117,8 +126,8 @@
   {% endblock %}
 
   <div class="side-content">
-  {% block side_content %}
-  {% endblock %}
+    {% block side_content %}
+    {% endblock %}
   </div>
 
   {% block side_footer %}
@@ -126,20 +135,20 @@
 
   <div id="footer-container" class="footer">
     <footer class="">
-    <p>
-    <a href="" data-target="#help-modal" data-toggle="modal"><i class="fa fa-question-circle"> ヘルプ</i></a>
-    &copy; {{ now|date('Y') }} {{ config.crowi['app:title'] }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
+      <p>
+      <a href="" data-target="#help-modal" data-toggle="modal"><i class="fa fa-question-circle"> ヘルプ</i></a>
+      &copy; {{ now|date('Y') }} {{ config.crowi['app:title'] }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
     </footer>
   </div>
 </aside>
 {% include '../modal/widget_help.html' %}
 
 <script>
-  $(function() {
-    if ($.cookie('aside-hidden') == 1) {
-      $('.main-container').addClass('aside-hidden');
-    }
-  });
+$(function() {
+  if ($.cookie('aside-hidden') == 1) {
+    $('.main-container').addClass('aside-hidden');
+  }
+});
 </script>
 {% endblock %} {# layout_sidebar #}
 
@@ -147,19 +156,19 @@
 <div id="main" class="main col-md-9 {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
   {% if page && page.grant != 1 %}
   <p class="page-grant">
-    <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} (このページの閲覧は制限されています)
+  <i class="fa fa-lock"></i> {{ consts.pageGrants[page.grant] }} (このページの閲覧は制限されています)
   </p>
   {% endif %}
   <article>
-  {% block content_head %}
-  {% endblock %}
+    {% block content_head %}
+    {% endblock %}
 
-  {% block content_main %}
-  //
-  {% endblock content_main %}
+    {% block content_main %}
+    //
+    {% endblock content_main %}
 
-  {% block content_footer %}
-  {% endblock %}
+    {% block content_footer %}
+    {% endblock %}
   </article>
 </div>
 

+ 2 - 0
lib/views/page_list.html

@@ -15,6 +15,7 @@
     {% endif %}
     <h1 class="title">
       <span class="" id="revision-path">{{ path|insertSpaceToEachSlashes }}</span>
+      {% if searchConfigured() && path != '/' %}
       <div class="form-group form-group-sm has-feedback search-input-group" data-toggle="tooltip" data-placement="bottom" title="{{ path }} 以下から検索">
         <label class="control-label sr-only" for="inputSuccess5">Search</label>
         <input
@@ -22,6 +23,7 @@
         >
         <i class="form-control-feedback search-listpage-icon fa fa-search"></i>
       </div>
+      {% endif %}
     </h1>
   </header>
 </div>

+ 26 - 0
resource/css/_search.scss

@@ -13,3 +13,29 @@
 
 .search-listpage-input {
 }
+
+.search-top {
+  .search-top-input-group {
+    .search-top-input {
+    }
+    &:focus {
+      width: 400px;
+      transition: .3s ease;
+    }
+  }
+}
+
+.search-suggest {
+  // => dicided by JS
+  // top: 43px;
+  // left: 125px;
+  display: none;
+  position: fixed;
+  width: 500px;
+  background: #fff;
+  border: solid 1px #ccc;
+  box-shadow: 0 0 2px #ccc;
+  padding: 16px;
+
+}
+