Implemented Lucene search result paging
2 files added
7 files modified
| | |
| | | * 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;
|
| | | }
|
| | |
|
| | |
| | | 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);
|
| | |
| | | *
|
| | | * @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]));
|
| | | }
|
| | |
|
| | | /**
|
| | |
| | | *
|
| | | * @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;
|
| | | }
|
| | |
| | | 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];
|
| | |
| | | public class SearchResult implements Serializable {
|
| | |
|
| | | private static final long serialVersionUID = 1L;
|
| | | |
| | | public int hitId;
|
| | | |
| | | public int totalHits;
|
| | |
|
| | | public float score;
|
| | |
|
| | |
| | | </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">
|
| | |
| | | <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>
|
| | |
| | | */
|
| | | package com.gitblit.wicket.pages;
|
| | |
|
| | | import java.text.MessageFormat;
|
| | | import java.util.ArrayList;
|
| | | import java.util.List;
|
| | |
|
| | |
| | |
|
| | | 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;
|
| | |
| | | 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 {
|
| | |
|
| | |
| | | // 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", "");
|
| | |
| | | // 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
|
| | |
| | | }
|
| | | 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", "#", "←"));
|
| | | // } 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, "&"), "←"));
|
| | | // }
|
| | | //
|
| | | // // 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", "#", "→"));
|
| | | // } 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, "&"), "→"));
|
| | | // }
|
| | | // return sb.toString();
|
| | | // }
|
| | |
|
| | | }
|
New file |
| | |
| | | <!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> |
New file |
| | |
| | | /*
|
| | | * 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;
|
| | | }
|
| | | }
|
| | | }
|
| | |
| | | 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
|
| | |
| | | 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
|
| | |
| | | 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);
|
| | |
|
| | |
| | | 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
|
| | |
| | | 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());
|
| | |
|
| | |
| | | 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());
|
| | | }
|