]> source.dussan.org Git - gitblit.git/commitdiff
Implemented Lucene search result paging
authorJames Moger <james.moger@gitblit.com>
Sat, 17 Mar 2012 15:02:27 +0000 (11:02 -0400)
committerJames Moger <james.moger@gitblit.com>
Sat, 17 Mar 2012 15:02:27 +0000 (11:02 -0400)
src/com/gitblit/GitBlit.java
src/com/gitblit/LuceneExecutor.java
src/com/gitblit/models/SearchResult.java
src/com/gitblit/wicket/pages/LucenePage.html
src/com/gitblit/wicket/pages/LucenePage.java
src/com/gitblit/wicket/panels/PagerPanel.html [new file with mode: 0644]
src/com/gitblit/wicket/panels/PagerPanel.java [new file with mode: 0644]
tests/com/gitblit/tests/IssuesTest.java
tests/com/gitblit/tests/LuceneExecutorTest.java

index 272630c1897a1f38461cc07fb69d5908ad5ac4d5..b2e53d67eb85962a815ca046f03250b17d8f93ab 100644 (file)
@@ -1667,12 +1667,13 @@ public class GitBlit implements ServletContextListener {
         * Search the specified repositories using the Lucene query.\r
         * \r
         * @param query\r
-        * @param maximumHits\r
+        * @param page\r
+        * @param pageSize\r
         * @param repositories\r
         * @return\r
         */\r
-       public List<SearchResult> search(String query, int maximumHits, List<String> repositories) {\r
-               List<SearchResult> srs = luceneExecutor.search(query, maximumHits, repositories);\r
+       public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) {             \r
+               List<SearchResult> srs = luceneExecutor.search(query, page, pageSize, repositories);\r
                return srs;\r
        }\r
 \r
index 489d308dd0f195dbf68a3d1c59fb430af9ce959d..220967befcd455e6998c807e7d86d191e7829f1f 100644 (file)
@@ -940,8 +940,10 @@ public class LuceneExecutor implements Runnable {
                return false;\r
        }\r
 \r
-       private SearchResult createSearchResult(Document doc, float score) throws ParseException {\r
+       private SearchResult createSearchResult(Document doc, float score, int hitId, int totalHits) throws ParseException {\r
                SearchResult result = new SearchResult();\r
+               result.hitId = hitId;\r
+               result.totalHits = totalHits;\r
                result.score = score;\r
                result.date = DateTools.stringToDate(doc.get(FIELD_DATE));\r
                result.summary = doc.get(FIELD_SUMMARY);                \r
@@ -1017,19 +1019,21 @@ public class LuceneExecutor implements Runnable {
         * \r
         * @param text\r
         *            if the text is null or empty, null is returned\r
-        * @param maximumHits\r
-        *            the maximum number of hits to collect\r
+        * @param page\r
+        *            the page number to retrieve. page is 1-indexed.\r
+        * @param pageSize\r
+        *            the number of elements to return for this page\r
         * @param repositories\r
         *            a list of repositories to search. if no repositories are\r
         *            specified null is returned.\r
         * @return a list of SearchResults in order from highest to the lowest score\r
         * \r
         */\r
-       public List<SearchResult> search(String text, int maximumHits, List<String> repositories) {\r
+       public List<SearchResult> search(String text, int page, int pageSize, List<String> repositories) {\r
                if (ArrayUtils.isEmpty(repositories)) {\r
                        return null;\r
                }\r
-               return search(text, maximumHits, repositories.toArray(new String[0]));\r
+               return search(text, page, pageSize, repositories.toArray(new String[0]));\r
        }\r
        \r
        /**\r
@@ -1037,15 +1041,17 @@ public class LuceneExecutor implements Runnable {
         * \r
         * @param text\r
         *            if the text is null or empty, null is returned\r
-        * @param maximumHits\r
-        *            the maximum number of hits to collect\r
+        * @param page\r
+        *            the page number to retrieve. page is 1-indexed.\r
+        * @param pageSize\r
+        *            the number of elements to return for this page\r
         * @param repositories\r
         *            a list of repositories to search. if no repositories are\r
         *            specified null is returned.\r
         * @return a list of SearchResults in order from highest to the lowest score\r
         * \r
-        */     \r
-       public List<SearchResult> search(String text, int maximumHits, String... repositories) {\r
+        */\r
+       public List<SearchResult> search(String text, int page, int pageSize, String... repositories) {\r
                if (StringUtils.isEmpty(text)) {\r
                        return null;\r
                }\r
@@ -1082,14 +1088,15 @@ public class LuceneExecutor implements Runnable {
                                searcher = new IndexSearcher(reader);\r
                        }\r
                        Query rewrittenQuery = searcher.rewrite(query);\r
-                       TopScoreDocCollector collector = TopScoreDocCollector.create(maximumHits, true);\r
+                       TopScoreDocCollector collector = TopScoreDocCollector.create(5000, true);\r
                        searcher.search(rewrittenQuery, collector);\r
-                       ScoreDoc[] hits = collector.topDocs().scoreDocs;\r
+                       int offset = Math.max(0, (page - 1) * pageSize);\r
+                       ScoreDoc[] hits = collector.topDocs(offset, pageSize).scoreDocs;\r
+                       int totalHits = collector.getTotalHits();\r
                        for (int i = 0; i < hits.length; i++) {\r
                                int docId = hits[i].doc;\r
                                Document doc = searcher.doc(docId);\r
-                               // TODO identify the source index for the doc, then eliminate FIELD_REPOSITORY                          \r
-                               SearchResult result = createSearchResult(doc, hits[i].score);\r
+                               SearchResult result = createSearchResult(doc, hits[i].score, offset + i + 1, totalHits);\r
                                if (repositories.length == 1) {\r
                                        // single repository search\r
                                        result.repository = repositories[0];\r
index 566230750d9cdb57682be5209384744bedaf5d21..efd1b075cba52dcb488a8696f9b953bcff007691 100644 (file)
@@ -15,6 +15,10 @@ import com.gitblit.Constants.SearchObjectType;
 public class SearchResult implements Serializable {\r
 \r
        private static final long serialVersionUID = 1L;\r
+       \r
+       public int hitId;\r
+       \r
+       public int totalHits;\r
 \r
        public float score;\r
 \r
index e6a9b932dbddc1411feca5d7610f75a05a4cc5b2..75f633648b7449d60ef2304d68d1f76217984f60 100644 (file)
                        </div>\r
                </div>\r
        </form>\r
-       <hr/>\r
-       <div class="row-fluid">\r
+\r
+       <div class="row-fluid"> \r
+       <!-- results header -->\r
+       <div class="span8">\r
+               <h3><span wicket:id="resultsHeader"></span> <small><span wicket:id="resultsCount"></span></small></h3>\r
+       </div>\r
+       <!-- pager links -->\r
+       <div class="span4" wicket:id="topPager"></div>\r
+       </div>\r
+       \r
+       <div class="row-fluid"> \r
+       <!--  search result repeater -->\r
        <div class="searchResult" wicket:id="searchResults">\r
                <div><i wicket:id="type"></i><span class="summary" wicket:id="summary"></span></div>\r
                <div class="body">\r
                        <span class="repository" wicket:id="repository"></span>:<span class="branch" wicket:id="branch"></span>                 \r
                </div>\r
        </div>\r
-       </div>\r
+\r
+       <!-- pager links -->\r
+       <div wicket:id="bottomPager"></div>\r
+\r
+       </div>  \r
 </body>\r
 </wicket:extend>\r
 </html>
\ No newline at end of file
index 099471a82a196f106b9e4735e13b56981bd88c72..10de0bf5fdb524c98249576a979917f09d106077 100644 (file)
@@ -15,6 +15,7 @@
  */\r
 package com.gitblit.wicket.pages;\r
 \r
+import java.text.MessageFormat;\r
 import java.util.ArrayList;\r
 import java.util.List;\r
 \r
@@ -31,6 +32,7 @@ import org.eclipse.jgit.lib.Constants;
 \r
 import com.gitblit.Constants.SearchType;\r
 import com.gitblit.GitBlit;\r
+import com.gitblit.Keys;\r
 import com.gitblit.models.RepositoryModel;\r
 import com.gitblit.models.SearchResult;\r
 import com.gitblit.models.UserModel;\r
@@ -40,6 +42,7 @@ import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.StringChoiceRenderer;\r
 import com.gitblit.wicket.WicketUtils;\r
 import com.gitblit.wicket.panels.LinkPanel;\r
+import com.gitblit.wicket.panels.PagerPanel;\r
 \r
 public class LucenePage extends RootPage {\r
 \r
@@ -59,12 +62,16 @@ public class LucenePage extends RootPage {
                // default values\r
                ArrayList<String> repositories = new ArrayList<String>();                               \r
                String query = "";\r
+               int page = 1;\r
+               int pageSize = GitBlit.getInteger(Keys.web.itemsPerPage, 50);\r
 \r
                if (params != null) {\r
                        String repository = WicketUtils.getRepositoryName(params);\r
                        if (!StringUtils.isEmpty(repository)) {\r
                                repositories.add(repository);\r
                        }\r
+\r
+                       page = WicketUtils.getPage(params);     \r
                        \r
                        if (params.containsKey("repositories")) {\r
                                String value = params.getString("repositories", "");\r
@@ -144,7 +151,18 @@ public class LucenePage extends RootPage {
                // execute search\r
                final List<SearchResult> results = new ArrayList<SearchResult>();\r
                if (!ArrayUtils.isEmpty(searchRepositories) && !StringUtils.isEmpty(query)) {\r
-                       results.addAll(GitBlit.self().search(query, 100, searchRepositories));\r
+                       results.addAll(GitBlit.self().search(query, page, pageSize, searchRepositories));\r
+               }\r
+               \r
+               // results header\r
+               if (results.size() == 0) {\r
+                       add(new Label("resultsHeader").setVisible(false));\r
+                       add(new Label("resultsCount").setVisible(false));\r
+               } else {\r
+                       add(new Label("resultsHeader", query).setRenderBodyOnly(true));\r
+                       add(new Label("resultsCount", MessageFormat.format("results {0} - {1} ({2} hits)",\r
+                                       results.get(0).hitId, results.get(results.size() - 1).hitId, results.get(0).totalHits)).\r
+                                       setRenderBodyOnly(true));\r
                }\r
                \r
                // search results view\r
@@ -178,11 +196,74 @@ public class LucenePage extends RootPage {
                                }\r
                                item.add(new Label("fragment", sr.fragment).setEscapeModelStrings(false).setVisible(!StringUtils.isEmpty(sr.fragment)));\r
                                item.add(new LinkPanel("repository", null, sr.repository, SummaryPage.class, WicketUtils.newRepositoryParameter(sr.repository)));\r
-                               item.add(new LinkPanel("branch", "branch", StringUtils.getRelativePath(Constants.R_HEADS, sr.branch), LogPage.class, WicketUtils.newObjectParameter(sr.repository, sr.branch)));\r
+                               if (StringUtils.isEmpty(sr.branch)) {\r
+                                       item.add(new Label("branch", "null"));\r
+                               } else {\r
+                                       item.add(new LinkPanel("branch", "branch", StringUtils.getRelativePath(Constants.R_HEADS, sr.branch), LogPage.class, WicketUtils.newObjectParameter(sr.repository, sr.branch)));\r
+                               }\r
                                item.add(new Label("author", sr.author));\r
                                item.add(WicketUtils.createDatestampLabel("date", sr.date, getTimeZone()));\r
                        }\r
                };\r
                add(resultsView.setVisible(results.size() > 0));\r
-       }       \r
+               \r
+               PageParameters pagerParams = new PageParameters();\r
+               pagerParams.put("repositories", StringUtils.flattenStrings(repositoriesModel.getObject()));\r
+               pagerParams.put("query", queryModel.getObject());\r
+               \r
+               int totalPages = 0;\r
+               if (results.size() > 0) {\r
+                       totalPages = (results.get(0).totalHits / pageSize) + (results.get(0).totalHits % pageSize > 0 ? 1 : 0);\r
+               }\r
+               \r
+               add(new PagerPanel("topPager", page, totalPages, LucenePage.class, pagerParams));\r
+               add(new PagerPanel("bottomPager", page, totalPages, LucenePage.class, pagerParams));\r
+       }\r
+       \r
+//     private String buildPager(int currentPage, int count, int total) {\r
+//             int pages = (total / count) + (total % count == 0 ? 0 : 1);\r
+//             \r
+//             // pages are 1-indexed\r
+//             // previous page link\r
+//             if (currentPage <= 1) {\r
+//                     sb.append(MessageFormat.format(li, "disabled", "#", "&larr;"));\r
+//             } else {\r
+//                     List<String> parameters = new ArrayList<String>();\r
+//                     if (!StringUtils.isEmpty(penString)) {\r
+//                             parameters.add(penString);\r
+//                     }\r
+//                     parameters.add(MessageFormat.format(pg, currentPage - 1));\r
+//                     sb.append(MessageFormat.format(li, "", StringUtils.flattenStrings(parameters, "&"), "&larr;"));\r
+//             }\r
+//\r
+//             // page links in middle\r
+//             int minpage = Math.max(1, currentPage - Math.min(2, 2));\r
+//             int maxpage = Math.min(pages, minpage + 4);\r
+//             for (int i = minpage; i <= maxpage; i++) {\r
+//                     String cssClass = "";\r
+//                     if (i == currentPage) {\r
+//                             cssClass = "active";\r
+//                     }\r
+//                     List<String> parameters = new ArrayList<String>();\r
+//                     if (!StringUtils.isEmpty(penString)) {\r
+//                             parameters.add(penString);\r
+//                     }\r
+//                     parameters.add(MessageFormat.format(pg, i));\r
+//                     sb.append(MessageFormat.format(li, cssClass, StringUtils.flattenStrings(parameters, "&"), i));\r
+//             }\r
+//\r
+//             // next page link\r
+//             if (currentPage == pages) {\r
+//                     sb.append(MessageFormat.format(li, "disabled", "#", "&rarr;"));\r
+//             } else {\r
+//                     List<String> parameters = new ArrayList<String>();\r
+//                     if (!StringUtils.isEmpty(penString)) {\r
+//                             parameters.add(penString);\r
+//                     }\r
+//                     parameters.add(MessageFormat.format(pg, currentPage + 1));\r
+//                     sb.append(MessageFormat.format(li, "", StringUtils.flattenStrings(parameters, "&"), "&rarr;"));\r
+//             }\r
+//             return sb.toString();\r
+//     }\r
+\r
 }\r
diff --git a/src/com/gitblit/wicket/panels/PagerPanel.html b/src/com/gitblit/wicket/panels/PagerPanel.html
new file mode 100644 (file)
index 0000000..099001d
--- /dev/null
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\r
+<html xmlns="http://www.w3.org/1999/xhtml"  \r
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  \r
+      xml:lang="en"  \r
+      lang="en">\r
+<wicket:panel>\r
+       <div class="pagination pagination-right" style="margin: 0px;">\r
+               <ul>\r
+                       <li wicket:id="page"><span wicket:id="pageLink"></span></li>\r
+               </ul> \r
+       </div>\r
+</wicket:panel>\r
+</html>
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/panels/PagerPanel.java b/src/com/gitblit/wicket/panels/PagerPanel.java
new file mode 100644 (file)
index 0000000..a5dbb9e
--- /dev/null
@@ -0,0 +1,95 @@
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.wicket.panels;\r
+\r
+import java.io.Serializable;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+\r
+import org.apache.wicket.PageParameters;\r
+import org.apache.wicket.markup.html.panel.Panel;\r
+import org.apache.wicket.markup.repeater.Item;\r
+import org.apache.wicket.markup.repeater.data.DataView;\r
+import org.apache.wicket.markup.repeater.data.ListDataProvider;\r
+\r
+import com.gitblit.wicket.WicketUtils;\r
+import com.gitblit.wicket.pages.BasePage;\r
+\r
+public class PagerPanel extends Panel {\r
+\r
+       private static final long serialVersionUID = 1L;\r
+\r
+       public PagerPanel(String wicketId, final int currentPage, final int totalPages,\r
+                       final Class<? extends BasePage> pageClass, final PageParameters baseParams) {\r
+               super(wicketId);\r
+               List<PageObject> pages = new ArrayList<PageObject>();\r
+               int[] deltas;\r
+               if (currentPage == 1) {\r
+                       // [1], 2, 3, 4, 5\r
+                       deltas = new int[] { 0, 1, 2, 3, 4 };                   \r
+               } else if (currentPage == 2) {\r
+                       // 1, [2], 3, 4, 5\r
+                       deltas = new int[] { -1, 0, 1, 2, 3 };                  \r
+               } else {\r
+                       // 1, 2, [3], 4, 5\r
+                       deltas = new int[] { -2, -1, 0, 1, 2 };\r
+               }\r
+\r
+               if (totalPages > 0) {\r
+                       pages.add(new PageObject("\u2190", currentPage - 1));\r
+               }\r
+               for (int delta : deltas) {\r
+                       int page = currentPage + delta;\r
+                       if (page > 0 && page <= totalPages) {\r
+                               pages.add(new PageObject("" + page, page));\r
+                       }\r
+               }\r
+               if (totalPages > 0) {\r
+                       pages.add(new PageObject("\u2192", currentPage + 1));\r
+               }\r
+\r
+               ListDataProvider<PageObject> pagesProvider = new ListDataProvider<PageObject>(pages);\r
+               final DataView<PageObject> pagesView = new DataView<PageObject>("page", pagesProvider) {\r
+                       private static final long serialVersionUID = 1L;\r
+\r
+                       public void populateItem(final Item<PageObject> item) {\r
+                               PageObject pageItem = item.getModelObject();\r
+                               PageParameters pageParams = new PageParameters(baseParams);\r
+                               pageParams.put("pg", pageItem.page);\r
+                               LinkPanel link = new LinkPanel("pageLink", null, pageItem.text, pageClass, pageParams);\r
+                               link.setRenderBodyOnly(true);\r
+                               item.add(link);\r
+                               if (pageItem.page == currentPage || pageItem.page < 1 || pageItem.page > totalPages) {\r
+                                       WicketUtils.setCssClass(item, "disabled");\r
+                               }\r
+                       }\r
+               };\r
+               add(pagesView);\r
+       }\r
+\r
+       private class PageObject implements Serializable {\r
+\r
+               private static final long serialVersionUID = 1L;\r
+               \r
+               String text;\r
+               int page;\r
+\r
+               PageObject(String text, int page) {\r
+                       this.text = text;\r
+                       this.page = page;\r
+               }\r
+       }\r
+}\r
index 9133f9b133e1b6339e322aa392e60417a0f37ac4..e329f6674560394671738bebd3c25dda621099ee 100644 (file)
@@ -134,7 +134,7 @@ public class IssuesTest {
                for (IssueModel anIssue : allIssues) {\r
                        lucene.index(name, anIssue);\r
                }\r
-               List<SearchResult> hits = lucene.search("working", 10, name);\r
+               List<SearchResult> hits = lucene.search("working", 1, 10, name);\r
                assertTrue(hits.size() > 0);\r
                \r
                // reindex an issue\r
index 7a171dbc3cac1bd5f482b8594b403e388c342b0e..d221744c5c1f0024eb92ef21cab59751833bb706 100644 (file)
@@ -66,9 +66,9 @@ public class LuceneExecutorTest {
                lucene.reindex(model, repository);\r
                repository.close();\r
                \r
-               SearchResult result = lucene.search("type:blob AND path:bit.bit", 1, model.name).get(0);                \r
+               SearchResult result = lucene.search("type:blob AND path:bit.bit", 1, 1, model.name).get(0);             \r
                assertEquals("Mike Donaghy", result.author);\r
-               result = lucene.search("type:blob AND path:clipper.prg", 1, model.name).get(0);         \r
+               result = lucene.search("type:blob AND path:clipper.prg", 1, 1, model.name).get(0);              \r
                assertEquals("tinogomes", result.author);               \r
 \r
                // reindex theoretical physics\r
@@ -95,18 +95,18 @@ public class LuceneExecutorTest {
                RepositoryModel model = newRepositoryModel(repository);\r
                repository.close();\r
                \r
-               List<SearchResult> results = lucene.search("ada", 10, model.name);\r
+               List<SearchResult> results = lucene.search("ada", 1, 10, model.name);\r
                assertEquals(2, results.size());\r
                for (SearchResult res : results) {\r
                        assertEquals("refs/heads/master", res.branch);\r
                }\r
 \r
                // author test\r
-               results = lucene.search("author: tinogomes AND type:commit", 10, model.name);\r
+               results = lucene.search("author: tinogomes AND type:commit", 1, 10, model.name);\r
                assertEquals(2, results.size());\r
                \r
                // blob test\r
-               results = lucene.search("type: blob AND \"import std.stdio\"", 10, model.name);\r
+               results = lucene.search("type: blob AND \"import std.stdio\"", 1, 10, model.name);\r
                assertEquals(1, results.size());\r
                assertEquals("d.D", results.get(0).path);\r
                \r
@@ -115,20 +115,20 @@ public class LuceneExecutorTest {
                model = newRepositoryModel(repository);\r
                repository.close();\r
                \r
-               results = lucene.search("\"add the .nojekyll file\"", 10, model.name);\r
+               results = lucene.search("\"add the .nojekyll file\"", 1, 10, model.name);\r
                assertEquals(1, results.size());\r
                assertEquals("Ondrej Certik", results.get(0).author);\r
                assertEquals("2648c0c98f2101180715b4d432fc58d0e21a51d7", results.get(0).commitId);\r
                assertEquals("refs/heads/gh-pages", results.get(0).branch);\r
                \r
-               results = lucene.search("type:blob AND \"src/intro.rst\"", 10, model.name);\r
+               results = lucene.search("type:blob AND \"src/intro.rst\"", 1, 10, model.name);\r
                assertEquals(4, results.size());\r
                \r
                // hash id tests\r
-               results = lucene.search("commit:57c4f26f157ece24b02f4f10f5f68db1d2ce7ff5", 10, model.name);\r
+               results = lucene.search("commit:57c4f26f157ece24b02f4f10f5f68db1d2ce7ff5", 1, 10, model.name);\r
                assertEquals(1, results.size());\r
 \r
-               results = lucene.search("commit:57c4f26f157*", 10, model.name);\r
+               results = lucene.search("commit:57c4f26f157*", 1, 10, model.name);\r
                assertEquals(1, results.size());                \r
                \r
                // annotated tag test\r
@@ -136,7 +136,7 @@ public class LuceneExecutorTest {
                model = newRepositoryModel(repository);\r
                repository.close();\r
                \r
-               results = lucene.search("I663208919f297836a9c16bf458e4a43ffaca4c12", 10, model.name);\r
+               results = lucene.search("I663208919f297836a9c16bf458e4a43ffaca4c12", 1, 10, model.name);\r
                assertEquals(1, results.size());\r
                assertEquals("[v1.3.0.201202151440-r]", results.get(0).tags.toString());                \r
                \r
@@ -155,7 +155,7 @@ public class LuceneExecutorTest {
                list.add(newRepositoryModel(repository).name);\r
                repository.close();\r
 \r
-               List<SearchResult> results = lucene.search("test", 10, list);\r
+               List<SearchResult> results = lucene.search("test", 1, 10, list);\r
                lucene.close();\r
                assertEquals(10, results.size());\r
        }\r