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