You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by ju...@apache.org on 2012/12/24 14:47:07 UTC
svn commit: r1425649 - in /incubator/bloodhound/trunk/bloodhound_search: ./
bhsearch/ bhsearch/default-pages/ bhsearch/templates/ bhsearch/tests/
Author: jure
Date: Mon Dec 24 13:47:06 2012
New Revision: 1425649
URL: http://svn.apache.org/viewvc?rev=1425649&view=rev
Log:
bloodhound search plugin update, towards #285 (from andrej)
Added:
incubator/bloodhound/trunk/bloodhound_search/bhsearch/admin.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/default-pages/
incubator/bloodhound/trunk/bloodhound_search/bhsearch/default-pages/BloodhoundSearch
incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/index_with_whoosh.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/ticket_search.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/ticket_search.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
Modified:
incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py
incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py
incubator/bloodhound/trunk/bloodhound_search/setup.py
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/admin.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/admin.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/admin.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/admin.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+r"""Administration commands for Bloodhound Search."""
+from trac.admin import *
+from bhsearch.api import BloodhoundSearchApi
+
+class BloodhoundSearchAdmin(Component):
+ """Bloodhound Search administration component."""
+
+ implements(IAdminCommandProvider)
+
+ # IAdminCommandProvider methods
+ def get_admin_commands(self):
+ yield ('bhsearch rebuild', '',
+ 'Rebuild Bloodhound Search index',
+ None, BloodhoundSearchApi(self.env).rebuild_index)
+ yield ('bhsearch optimize', '',
+ 'Optimize Bloodhound search index',
+ None, BloodhoundSearchApi(self.env).optimize)
+
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py?rev=1425649&r1=1425648&r2=1425649&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/api.py Mon Dec 24 13:47:06 2012
@@ -18,66 +18,177 @@
# specific language governing permissions and limitations
# under the License.
-"""Core Bloodhound Search components"""
+r"""Core Bloodhound Search components."""
from trac.core import *
+from trac.config import ExtensionOption
-class BloodhoundQuerySystem(Component):
- """Implements core query functionality.
+ASC = "asc"
+DESC = "desc"
+SCORE = "score"
+
+class QueryResult(object):
+ def __init__(self):
+ self.hits = 0
+ self.page_count = 0
+ self.page_number = 0
+ self.offset = 0
+ self.docs = []
+ self.facets = None
+
+
+class ISearchBackend(Interface):
+ """Extension point interface for search backend systems.
"""
- def query(self, query, sort = None, boost = None, filters = None,
- facets = None, start = 0, rows = None):
- """Return query result from on underlying backend.
+ def add_doc(self, doc, commit=True):
+ """
+ Called when new document instance must be added
+
+ :param doc: document to add
+ :param commit: flag if commit should be automatically called
+ """
+
+ def delete_doc(self, doc, commit=True):
+ """
+ Delete document from index
+
+ :param doc: document to delete
+ :param commit: flag if commit should be automatically called
+ """
+
+ def commit(self):
+ """
+ Commits changes
+ """
+
+ def optimize(self):
+ """
+ Optimize index if needed
+ """
+
+ def recreate_index(self):
+ """
+ Create a new index, if index exists, it will be deleted
+ """
+
+ def open_or_create_index_if_missing(self):
+ """
+ Open existing index, if index does not exist, create new one
+ """
+ def query(self, query, sort = None, fields = None, boost = None, filters = None,
+ facets = None, pagenum = 1, pagelen = 20):
+ """
+ Perform query implementation
+
+ :param query:
+ :param sort:
+ :param fields:
+ :param boost:
+ :param filters:
+ :param facets:
+ :param pagenum:
+ :param pagelen:
+ :return: TBD!!!
+ """
+ pass
+
+class ISearchParticipant(Interface):
+ """Extension point interface for components that should be searched.
+ """
+
+ def get_search_filters(req):
+ """Called when we want to build the list of components with search.
+ Passes the request object to do permission checking."""
+ pass
+
+ def build_search_index(backend):
+ """Called when we want to rebuild the entire index.
+ :type backend: ISearchBackend
+ """
+ pass
+
+ def format_search_results(contents):
+ """Called to see if the module wants to format the search results."""
+
+class IQueryParser(Interface):
+ """Extension point for Bloodhound Search query parser.
+ """
+
+ def parse(query_string, req = None):
+ pass
+
+class BloodhoundSearchApi(Component):
+ """Implements core indexing functionality, provides methods for
+ searching, adding and deleting documents from index.
+ """
+ backend = ExtensionOption('bhsearch', 'search_backend',
+ ISearchBackend, 'WhooshBackend',
+ 'Name of the component implementing Bloodhound Search backend \
+ interface: ISearchBackend.')
+
+ parser = ExtensionOption('bhsearch', 'query_parser',
+ IQueryParser, 'DefaultQueryParser',
+ 'Name of the component implementing Bloodhound Search query \
+ parser.')
+
+ search_participants = ExtensionPoint(ISearchParticipant)
+
+ def query(self, query, req = None, sort = None, fields = None, boost = None, filters = None,
+ facets = None, pagenum = 1, pagelen = 20):
+ """Return query result from an underlying search backend.
Arguments:
- query -- query string e.g. âbla status:closedâ or a parsed
+ :param query: query string e.g. âbla status:closedâ or a parsed
representation of the query.
- sort -- optional sorting
- boost -- optional list of fields with boost values e.g.
+ :param sort: optional sorting
+ :param boost: optional list of fields with boost values e.g.
{âidâ: 1000, âsubjectâ :100, âdescriptionâ:10}.
- filters -- optional list of terms. Usually can be cached by underlying
+ :param filters: optional list of terms. Usually can be cached by underlying
search framework. For example {âtypeâ: âwikiâ}
- facets - optional list of facet terms, can be field or expression.
- start, rows -- paging support
+ :param facets: optional list of facet terms, can be field or expression.
+ :param page: paging support
+ :param pagelen: paging support
- The result is returned as the following dictionary: {
- "docs": [
- {
- "id": "ticket:123",
- "resource_id": "123",
- "type": "ticket",
- ...
- }
- ],
- "numFound":3,"
- "start":0,
- "facet_counts":{
- "facet_fields":{
- "cat":[ "electronics",3, "card",2, "graphics",2, "music",1]
- }
- },
- }
+ :return: result QueryResult
"""
self.env.log.debug("Receive query request: %s", locals())
- #TODO: add implementation here
- dummy_result = dict(docs = [
- dict(
- resource_id = "123",
- summary = "Dummy result for query: " + (query or ''),
- )
- ])
- return dummy_result
+ # TODO: add query parsers and meta keywords post-parsing
+
+ # TODO: apply security filters
+
+ parsed_query = self.parser.parse(query, req)
+
+ #some backend-independent logic will come here...
+ query_result = self.backend.query(
+ query = parsed_query,
+ sort = sort,
+ fields = fields,
+ filters = filters,
+ facets = facets,
+ pagenum = pagenum,
+ pagelen = pagelen,
+ )
+
+ return query_result
-class BloodhoundIndexSystem(Component):
- """Implements core indexing functionality, provides methods for
- adding and deleting documents form index.
- """
def rebuild_index(self):
- """Erase the index if it exists. Then create a new index from scratch.
- """
+ """Delete the index if it exists. Then create a new full index."""
+ self.log.info('Rebuilding the search index.')
+ self.backend.recreate_index()
+
+ for participant in self.search_participants:
+ participant.build_search_index(self.backend)
+ self.backend.commit()
+ self.backend.optimize()
+
+ #Erase the index if it exists. Then create a new index from scratch.
+
+ #erase ticket
+ #call reindex for each resource
+ #commit
pass
def optimize(self):
@@ -89,12 +200,14 @@ class BloodhoundIndexSystem(Component):
The doc must be dictionary with obligatory "type" field
"""
- pass
+ self.backend.add_doc(doc)
- def delete_doc(self, doc):
+ def delete_doc(self, type, id):
"""Add a document from underlying search backend.
The doc must be dictionary with obligatory "type" field
"""
pass
+
+
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/default-pages/BloodhoundSearch
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/default-pages/BloodhoundSearch?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/default-pages/BloodhoundSearch (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/default-pages/BloodhoundSearch Mon Dec 24 13:47:06 2012
@@ -0,0 +1,3 @@
+= Bloodhound Search =
+
+TODO: add docs here
\ No newline at end of file
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+r"""Provides Bloodhound Search query parsing functionality"""
+
+from bhsearch.api import IQueryParser
+from bhsearch.whoosh_backend import WhooshBackend
+from trac.core import Component, implements
+from whoosh.qparser import QueryParser, DisMaxParser, MultifieldParser
+
+class DefaultQueryParser(Component):
+ implements(IQueryParser)
+
+ def parse(self, query_string, req = None):
+ #todo: make field boost configurable e.g. read from config setting
+ #this is prototype implementation ,the fields boost must be tuned later
+ field_boosts = dict(
+ id = 6,
+ type = 2,
+ summary = 5,
+ author = 3,
+ milestone = 2,
+ keywords = 2,
+ component = 2,
+ status = 2,
+ content = 1,
+ changes = 1,
+ )
+ parser = MultifieldParser(
+ field_boosts.keys(),
+ WhooshBackend.SCHEMA,
+ fieldboosts=field_boosts
+ )
+ query_string = unicode(query_string)
+ parsed_query = parser.parse(query_string)
+
+ #todo: impelement pluggable mechanizem for query post processing
+ #e.g. meta keyword replacement etc.
+ return parsed_query
+
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html?rev=1425649&r1=1425648&r2=1425649&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html Mon Dec 24 13:47:06 2012
@@ -27,9 +27,15 @@
<xi:include href="layout.html" />
<head>
<title py:choose="">
- <py:when test="query">Search Results</py:when>
- <py:otherwise>Search</py:otherwise>
+ <py:when test="query">Bloodhound Search Results</py:when>
+ <py:otherwise>Bloodhound Search</py:otherwise>
</title>
+ <py:if test="results">
+ <meta name="startIndex" content="${results.span[0] + 1}"/>
+ <meta name="totalResults" content="$results.num_items"/>
+ <meta name="itemsPerPage" content="$results.max_per_page"/>
+ </py:if>
+
<script type="text/javascript">
jQuery(document).ready(function($) {$("#q").get(0).focus()});
</script>
@@ -37,22 +43,35 @@
<body>
<div id="content" class="search">
- <h1>This is dummy page. Implementation is coming...</h1>
+ <h1>This page provides prototype functionality. Implementation is coming...</h1>
<h1><label for="q">Search</label></h1>
- <form id="bhsearch" action="${href.bhsearch()}" method="get">
+ <form id="fullsearch" action="${href.bhsearch()}" method="get">
<p>
<input type="text" id="q" name="q" size="40" value="${query}" />
+ <input type="hidden" name="noquickjump" value="1" />
<input type="submit" value="${_('Search')}" />
</p>
</form>
<py:if test="results"><hr />
<h2 py:if="results">
- Results <span class="numresults">(${results.displayed_items()})</span>
+ Results <small>(${results.displayed_items()})</small>
</h2>
- <xi:include py:with="paginator = results" href="page_index.html" />
<div>
<dl id="results">
+
+ <!--This just a prototype stub. Should be replaced by proper ui mocks-->
+ <div>
+ <ul class="nav nav-tabs" id="mainnav">
+ <!--<li py:if="chrome.nav.mainnav"-->
+ <!--py:for="idx, item in enumerate(i for i in chrome.nav.mainnav if i.name in mainnav_show)" -->
+ <!--class="${classes(first_last(idx, chrome.nav.mainnav), active=item.active)}">${item.label}</li>-->
+ <li class="$active}"><a href="${page_href}">All (XXX)</a></li>
+ <li class=""><a href="${page_href}">Wiki (XXX)</a></li>
+ <li class=""><a href="${page_href}">Tickets (XXX)</a></li>
+ </ul>
+ </div>
+
<py:for each="result in results">
<dt><a href="${result.href}" class="searchable">${result.title}</a></dt>
<dd class="searchable">${result.excerpt}</dd>
@@ -63,12 +82,21 @@
</py:for>
</dl>
</div>
- <xi:include py:with="paginator = results" href="page_index.html" />
+ <xi:include py:with="paginator = results" href="bh_page_index.html" />
</py:if>
- <div id="notfound" py:if="query and not (results)">
- No matches found.
+ <div class="span12"
+ py:if="query and not (results or quickjump)">
+ <p id="notfound" class="alert">
+ No matches found.
+ </p>
</div>
+
+ <div id="help" class="help-block pull-right" i18n:msg="">
+ <strong>Note:</strong> See <a href="${href.wiki('BloodhoundSearch')}">BloodhoundSearch</a>
+ for help on searching.
+ </div>
+
</div>
</body>
</html>
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py?rev=1425649&r1=1425648&r2=1425649&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py Mon Dec 24 13:47:06 2012
@@ -17,4 +17,18 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+import doctest
+import unittest
+from bhsearch.tests import whoosh_backend, index_with_whoosh, web_ui, ticket_search, api
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(index_with_whoosh.suite())
+ suite.addTest(whoosh_backend.suite())
+ suite.addTest(web_ui.suite())
+ suite.addTest(ticket_search.suite())
+ suite.addTest(api.suite())
+ return suite
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')
\ No newline at end of file
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/api.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+from datetime import datetime, timedelta
+from pprint import pprint
+
+import unittest
+import tempfile
+import shutil
+from bhsearch.api import BloodhoundSearchApi
+from bhsearch.query_parser import DefaultQueryParser
+from bhsearch.tests.utils import BaseBloodhoundSearchTest
+from bhsearch.ticket_search import TicketSearchParticipant
+
+from bhsearch.whoosh_backend import WhooshBackend
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.ticket.api import TicketSystem
+
+
+class ApiQueryWithWhooshTestCase(BaseBloodhoundSearchTest):
+ def setUp(self):
+ self.env = EnvironmentStub(enable=['bhsearch.*'])
+ self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+ self.ticket_system = TicketSystem(self.env)
+ self.whoosh_backend = WhooshBackend(self.env)
+ self.whoosh_backend.recreate_index()
+ self.search_api = BloodhoundSearchApi(self.env)
+ self.ticket_participant = TicketSearchParticipant(self.env)
+ self.ticket_system = TicketSystem(self.env)
+ self.query_parser = DefaultQueryParser(self.env)
+
+ def tearDown(self):
+ shutil.rmtree(self.env.path)
+ self.env.reset_db()
+
+ def test_can_search_free_description(self):
+ #arrange
+ self.insert_ticket("dummy summary", description="aaa keyword bla")
+ #act
+ results = self.search_api.query("keyword")
+ #assert
+ self.print_result(results)
+ self.assertEqual(1, results.hits)
+
+ def test_can_query_free_summary(self):
+ #arrange
+ self.insert_ticket("summary1 keyword")
+ #act
+ results = self.search_api.query("keyword")
+ #assert
+ self.print_result(results)
+ self.assertEqual(1, results.hits)
+
+ def test_can_query_strict_summary(self):
+ #arrange
+ self.insert_ticket("summary1 keyword")
+ self.insert_ticket("summary2", description = "bla keyword")
+ #act
+ results = self.search_api.query("summary:keyword")
+ #assert
+ self.print_result(results)
+ self.assertEqual(1, results.hits)
+
+ def test_that_summary_hit_is_higher_than_description(self):
+ #arrange
+ self.insert_ticket("summary1 keyword")
+ self.insert_ticket("summary2", description = "bla keyword")
+ #act
+ results = self.search_api.query("keyword")
+ self.print_result(results)
+ #assert
+ self.assertEqual(2, results.hits)
+ docs = results.docs
+ self.assertEqual("summary1 keyword", docs[0]["summary"])
+ self.assertEqual("summary2", docs[1]["summary"])
+
+ def test_other_conditions_applied(self):
+ #arrange
+ self.insert_ticket("summary1 keyword", status="closed")
+ self.insert_ticket("summary2", description = "bla keyword")
+ self.insert_ticket("summary3", status="closed")
+ #act
+ results = self.search_api.query("keyword status:closed")
+ self.print_result(results)
+ #assert
+ self.assertEqual(1, results.hits)
+ docs = results.docs
+ self.assertEqual("summary1 keyword", docs[0]["summary"])
+
+ def test_can_search_id_and_summary(self):
+ #arrange
+ self.insert_ticket("summary1")
+ self.insert_ticket("summary2 1")
+ #act
+ results = self.search_api.query("1")
+ self.print_result(results)
+ #assert
+ self.assertEqual(2, results.hits)
+ docs = results.docs
+ self.assertEqual("summary1", docs[0]["summary"])
+
+def suite():
+ return unittest.makeSuite(ApiQueryWithWhooshTestCase, 'test')
+
+if __name__ == '__main__':
+ unittest.main()
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/index_with_whoosh.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/index_with_whoosh.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/index_with_whoosh.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/index_with_whoosh.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+from datetime import datetime
+
+import unittest
+import tempfile
+import shutil
+from bhsearch.api import BloodhoundSearchApi
+from bhsearch.tests.utils import BaseBloodhoundSearchTest
+from bhsearch.ticket_search import TicketSearchParticipant
+
+from bhsearch.whoosh_backend import WhooshBackend
+from trac.test import EnvironmentStub
+from trac.ticket.api import TicketSystem
+
+
+class IndexWhooshTestCase(BaseBloodhoundSearchTest):
+ def setUp(self):
+ self.env = EnvironmentStub(enable=['bhsearch.*'])
+ self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+# self.perm = PermissionSystem(self.env)
+ self.whoosh_backend = WhooshBackend(self.env)
+ self.whoosh_backend.recreate_index()
+ self.search_api = BloodhoundSearchApi(self.env)
+ self.ticket_participant = TicketSearchParticipant(self.env)
+ self.ticket_system = TicketSystem(self.env)
+
+ def tearDown(self):
+ shutil.rmtree(self.env.path)
+ self.env.reset_db()
+
+ def test_can_index_ticket(self):
+ ticket = self.create_dummy_ticket()
+ ticket.id = "1"
+ self.ticket_participant.ticket_created(ticket)
+
+ results = self.search_api.query("*:*")
+ self.print_result(results)
+ self.assertEqual(1, results.hits)
+
+ def test_that_ticket_indexed_when_inserted_in_db(self):
+ ticket = self.create_dummy_ticket()
+ ticket_id = ticket.insert()
+ print "Created ticket #%s" % ticket_id
+
+ results = self.search_api.query("*:*")
+ self.print_result(results)
+ self.assertEqual(1, results.hits)
+
+ def test_can_reindex(self):
+ self.insert_ticket("t1")
+ self.insert_ticket("t2")
+ self.insert_ticket("t3")
+ self.whoosh_backend.recreate_index()
+ #act
+ self.search_api.rebuild_index()
+ #assert
+ results = self.search_api.query("*:*")
+ self.print_result(results)
+ self.assertEqual(3, results.hits)
+
+
+def suite():
+ return unittest.makeSuite(IndexWhooshTestCase, 'test')
+
+if __name__ == '__main__':
+ unittest.main()
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/ticket_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/ticket_search.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/ticket_search.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/ticket_search.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+from datetime import datetime
+
+import unittest
+import tempfile
+import shutil
+from bhsearch.api import BloodhoundSearchApi
+from bhsearch.tests.utils import BaseBloodhoundSearchTest
+from bhsearch.ticket_search import TicketSearchParticipant
+
+from bhsearch.whoosh_backend import WhooshBackend
+from trac.test import EnvironmentStub
+from trac.ticket.api import TicketSystem
+
+
+class TicketSearchSilenceOnExceptionTestCase(BaseBloodhoundSearchTest):
+ def setUp(self):
+ self.env = EnvironmentStub(
+ enable=['bhsearch.*'],
+ path=tempfile.mkdtemp('bhsearch-tempenv'),
+ )
+ self.ticket_participant = TicketSearchParticipant(self.env)
+
+ def tearDown(self):
+ pass
+
+ def test_does_not_raise_exception_by_default(self):
+ self.ticket_participant.ticket_created(None)
+
+ def test_raise_exception_if_configured(self):
+ self.env.config.set('bhsearch', 'silence_on_error', "False")
+ self.assertRaises(
+ Exception,
+ self.ticket_participant.ticket_created,
+ None)
+
+
+def suite():
+ return unittest.makeSuite(TicketSearchSilenceOnExceptionTestCase, 'test')
+
+if __name__ == '__main__':
+ unittest.main()
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/utils.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+r"""
+Test utils methods
+"""
+import pprint
+import unittest
+from trac.ticket import Ticket
+
+class BaseBloodhoundSearchTest(unittest.TestCase):
+ def print_result(self, result):
+ print "Received result:"
+ pprint.pprint(result.__dict__)
+
+ def create_dummy_ticket(self, summary = None):
+ if not summary:
+ summary = 'Hello World'
+ data = {'component': 'foo', 'milestone': 'bar'}
+ return self.create_ticket(summary, reporter='john', **data)
+
+ def create_ticket(self, summary, **kw):
+ ticket = Ticket(self.env)
+ ticket["summary"] = summary
+ for k, v in kw.items():
+ ticket[k] = v
+ return ticket
+
+ def insert_ticket(self, summary, **kw):
+ """Helper for inserting a ticket into the database"""
+ ticket = self.create_ticket(summary, **kw)
+ return ticket.insert()
+
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/web_ui.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,169 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+from datetime import datetime, timedelta
+from pprint import pprint
+
+import unittest
+import tempfile
+import shutil
+from bhsearch.api import BloodhoundSearchApi
+from bhsearch.tests.utils import BaseBloodhoundSearchTest
+from bhsearch.ticket_search import TicketSearchParticipant
+from bhsearch.web_ui import BloodhoundSearchModule, SEARCH_PERMISSION
+
+from bhsearch.whoosh_backend import WhooshBackend
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.ticket.api import TicketSystem
+from trac.ticket import Ticket
+from trac.util.datefmt import FixedOffset
+from trac.util import format_datetime
+from trac.web import Href
+
+BHSEARCH_URL = "/bhsearch"
+DEFAULT_DOCS_PER_PAGE = 10
+
+class WebUiTestCaseWithWhoosh(BaseBloodhoundSearchTest):
+ def setUp(self):
+ self.env = EnvironmentStub(enable=['bhsearch.*'])
+ self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+
+# self.perm = PermissionSystem(self.env)
+ self.ticket_system = TicketSystem(self.env)
+ self.whoosh_backend = WhooshBackend(self.env)
+ self.whoosh_backend.recreate_index()
+ self.search_api = BloodhoundSearchApi(self.env)
+ self.ticket_participant = TicketSearchParticipant(self.env)
+ self.ticket_system = TicketSystem(self.env)
+ self.web_ui = BloodhoundSearchModule(self.env)
+
+ self.req = Mock(
+ perm=MockPerm(),
+ chrome={'logo': {}},
+ href=Href("/bhsearch"),
+ args={},
+ )
+# self.req = Mock(href=self.env.href, authname='anonymous', tz=utc)
+# self.req = Mock(base_path='/trac.cgi', path_info='',
+# href=Href('/trac.cgi'), chrome={'logo': {}},
+# abs_href=Href('http://example.org/trac.cgi'),
+# environ={}, perm=[], authname='-', args={}, tz=None,
+# locale='', session=None, form_token=None)
+
+# self.req = Mock(href=self.env.href, abs_href=self.env.abs_href, tz=utc,
+# perm=MockPerm())
+#
+
+ def tearDown(self):
+ shutil.rmtree(self.env.path)
+ self.env.reset_db()
+
+ def _process_request(self):
+ response = self.web_ui.process_request(self.req)
+ url, data, x = response
+ print "Received url: %s data:" % url
+ pprint(data)
+ if data.has_key("results"):
+ print "results :"
+ pprint(data["results"].__dict__)
+ return data
+
+ def test_can_process_empty_request(self):
+ data = self._process_request()
+ self.assertEqual("", data["query"])
+
+ def test_can_process_query_empty_data(self):
+ self.req.args["q"] = "bla"
+ data = self._process_request()
+ self.assertEqual("bla", data["query"])
+ self.assertEqual([], data["results"].items)
+
+ def test_can_process_first_page(self):
+ self._insert_docs(5)
+ self.req.args["q"] = "summary:test"
+ data = self._process_request()
+ self.assertEqual("summary:test", data["query"])
+ self.assertEqual(5, len(data["results"].items))
+
+ def test_can_return_utc_time(self):
+ #arrange
+ ticket_id = self.insert_ticket("bla")
+ ticket = Ticket(self.env, ticket_id)
+ ticket_time = ticket.time_changed
+ #act
+ self.req.args["q"] = "*:*"
+ data = self._process_request()
+ result_items = data["results"].items
+ #assert
+ self.assertEqual(1, len(result_items))
+ result_datetime = result_items[0]["date"]
+ print "Ticket time: %s, Returned time: %s" % (ticket_time, result_datetime)
+ self.assertEqual(format_datetime(ticket_time), result_items[0]["date"])
+
+ def test_can_return_user_time(self):
+ #arrange
+ ticket_id = self.insert_ticket("bla")
+ ticket = Ticket(self.env, ticket_id)
+ ticket_time = ticket.time_changed
+ #act
+ self.req.tz = FixedOffset(60, 'GMT +1:00')
+ self.req.args["q"] = "*:*"
+ data = self._process_request()
+ result_items = data["results"].items
+ #asset
+ self.assertEqual(1, len(result_items))
+ expected_datetime = format_datetime(ticket_time)
+ result_datetime = result_items[0]["date"]
+ print "Ticket time: %s, Formatted time: %s ,Returned time: %s" % (
+ ticket_time, expected_datetime,result_datetime)
+ self.assertEqual(expected_datetime, result_datetime)
+
+ def test_ticket_href(self):
+ self._insert_docs(1)
+ self.req.args["q"] = "*:*"
+ data = self._process_request()
+ docs = data["results"].items
+ self.assertEqual(1, len(docs))
+ self.assertEqual(BHSEARCH_URL + "/ticket/1", docs[0]["href"])
+
+ def test_page_href(self):
+ self._insert_docs(DEFAULT_DOCS_PER_PAGE+1)
+ self.req.args["q"] = "*:*"
+ data = self._process_request()
+ shown_pages = data["results"].shown_pages
+ self.assertEqual(BHSEARCH_URL + "/bhsearch?q=*%3A*&page=2&noquickjump=1", shown_pages[1]["href"])
+
+ def test_facets(self):
+ self.insert_ticket("summary1 keyword", status="closed")
+ self.insert_ticket("summary2 keyword", status="new")
+ self.req.args["q"] = "*:*"
+ data = self._process_request()
+ facets = data["facets"]
+ pprint(facets)
+ self.assertEqual({u'ticket': 2}, facets["type"])
+
+
+ def _insert_docs(self, n):
+ for i in range(1, n+1):
+ self.insert_ticket("test %s" % i)
+def suite():
+ return unittest.makeSuite(WebUiTestCaseWithWhoosh, 'test')
+
+if __name__ == '__main__':
+ unittest.main()
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/tests/whoosh_backend.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,292 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+from datetime import datetime, timedelta
+
+import unittest
+import tempfile
+import shutil
+from bhsearch.api import ASC, DESC, SCORE
+from bhsearch.tests.utils import BaseBloodhoundSearchTest
+from bhsearch.whoosh_backend import WhooshBackend
+from trac.test import EnvironmentStub
+from trac.util.datefmt import FixedOffset, utc
+from whoosh.qparser import MultifieldParser, MultifieldPlugin, syntax, QueryParser, WhitespacePlugin, PhrasePlugin, PlusMinusPlugin
+
+
+class WhooshBackendTestCase(BaseBloodhoundSearchTest):
+ def setUp(self):
+ self.env = EnvironmentStub(enable=['bhsearch.*'])
+ self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
+# self.perm = PermissionSystem(self.env)
+ self.whoosh_backend = WhooshBackend(self.env)
+ self.whoosh_backend.recreate_index()
+
+ def tearDown(self):
+ shutil.rmtree(self.env.path)
+ self.env.reset_db()
+
+ def test_can_retrieve_docs(self):
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+ self.whoosh_backend.add_doc(dict(id="2", type="ticket"))
+ result = self.whoosh_backend.query(
+# result = self.search_api.query(
+ "*:*",
+ sort = [("id", ASC)],
+ )
+ self.print_result(result)
+ self.assertEqual(2, result.hits)
+ docs = result.docs
+ self.assertEqual(
+ {'id': '1', 'type': 'ticket', 'unique_id': 'ticket:1',
+ 'score': u'1'},
+ docs[0])
+ self.assertEqual(
+ {'id': '2', 'type': 'ticket', 'unique_id': 'ticket:2',
+ 'score': u'2'},
+ docs[1])
+
+ def test_can_return_all_fields(self):
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+ result = self.whoosh_backend.query("*:*")
+ self.print_result(result)
+ docs = result.docs
+ self.assertEqual(
+ {'id': '1', 'type': 'ticket', 'unique_id': 'ticket:1',
+ "score": 1.0},
+ docs[0])
+
+ def test_can_select_fields(self):
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+ result = self.whoosh_backend.query("*:*",
+ fields=("id", "type"))
+ self.print_result(result)
+ docs = result.docs
+ self.assertEqual(
+ {'id': '1', 'type': 'ticket'},
+ docs[0])
+
+
+ def test_can_survive_after_restart(self):
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+ whoosh_backend2 = WhooshBackend(self.env)
+ whoosh_backend2.add_doc(dict(id="2", type="ticket"))
+ result = whoosh_backend2.query("*:*")
+ self.assertEqual(2, result.hits)
+
+ def test_can_multi_sort_asc(self):
+ self.whoosh_backend.add_doc(dict(id="2", type="ticket2"))
+ self.whoosh_backend.add_doc(dict(id="3", type="ticket1"))
+ self.whoosh_backend.add_doc(dict(id="4", type="ticket3"))
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket1"))
+ result = self.whoosh_backend.query(
+ "*:*",
+ sort = [("type", ASC), ("id", ASC)],
+ fields=("id", "type"),
+ )
+ self.print_result(result)
+ self.assertEqual([{'type': 'ticket1', 'id': '1'},
+ {'type': 'ticket1', 'id': '3'},
+ {'type': 'ticket2', 'id': '2'},
+ {'type': 'ticket3', 'id': '4'}],
+ result.docs)
+
+ def test_can_multi_sort_desc(self):
+ self.whoosh_backend.add_doc(dict(id="2", type="ticket2"))
+ self.whoosh_backend.add_doc(dict(id="3", type="ticket1"))
+ self.whoosh_backend.add_doc(dict(id="4", type="ticket3"))
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket1"))
+ result = self.whoosh_backend.query(
+ "*:*",
+ sort = [("type", ASC), ("id", DESC)],
+ fields=("id", "type"),
+ )
+ self.print_result(result)
+ self.assertEqual([{'type': 'ticket1', 'id': '3'},
+ {'type': 'ticket1', 'id': '1'},
+ {'type': 'ticket2', 'id': '2'},
+ {'type': 'ticket3', 'id': '4'}],
+ result.docs)
+
+ def test_can_sort_by_score_and_date(self):
+ the_first_date = datetime(2012, 12, 1)
+ the_second_date = datetime(2012, 12, 2)
+ the_third_date = datetime(2012, 12, 3)
+
+ exact_match_string = "texttofind"
+ not_exact_match_string = "texttofind bla"
+
+ self.whoosh_backend.add_doc(dict(
+ id="1",
+ type="ticket",
+ summary=not_exact_match_string,
+ time=the_first_date,
+ ))
+ self.whoosh_backend.add_doc(dict(
+ id="2",
+ type="ticket",
+ summary=exact_match_string,
+ time=the_second_date,
+ ))
+ self.whoosh_backend.add_doc(dict(
+ id="3",
+ type="ticket",
+ summary=not_exact_match_string,
+ time=the_third_date,
+ ))
+ self.whoosh_backend.add_doc(dict(
+ id="4",
+ type="ticket",
+ summary="some text out of search scope",
+ time=the_third_date,
+ ))
+ result = self.whoosh_backend.query(
+ "summary:texttofind",
+ sort = [(SCORE, ASC), ("time", DESC)],
+# fields=("id", "type"),
+ )
+ self.print_result(result)
+ self.assertEqual(3, result.hits)
+ docs = result.docs
+ #must be found first, because the highest score (of exact match)
+ self.assertEqual("2", docs[0]["id"])
+ #must be found second, because the time order DESC
+ self.assertEqual("3", docs[1]["id"])
+ #must be found third, because the time order DESC
+ self.assertEqual("1", docs[2]["id"])
+
+ def test_can_do_facet_count(self):
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket", product="A"))
+ self.whoosh_backend.add_doc(dict(id="2", type="ticket", product="B"))
+ self.whoosh_backend.add_doc(dict(id="3", type="wiki", product="A"))
+ result = self.whoosh_backend.query(
+ "*:*",
+ sort = [("type", ASC), ("id", DESC)],
+ fields=("id", "type"),
+ facets= ("type", "product")
+ )
+ self.print_result(result)
+ self.assertEqual(3, result.hits)
+ facets = result.facets
+ self.assertEqual({"ticket":2, "wiki":1}, facets["type"])
+ self.assertEqual({"A":2, "B":1}, facets["product"])
+
+ @unittest.skip(
+ "Fix this, check why exception is raise on Whoosh mailing list")
+ #TODO: fix this!!!!
+ def test_can_do_facet_if_filed_missing_TODO(self):
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket"))
+ self.whoosh_backend.add_doc(dict(id="2", type="ticket", status="New"))
+ result = self.whoosh_backend.query(
+ "*:*",
+ facets= ("type", "status")
+ )
+ self.print_result(result)
+ self.assertEqual(2, result.hits)
+ facets = result.facets
+ self.assertEqual({"ticket":2}, facets["type"])
+ self.assertEqual({"new":1}, facets["status"])
+
+ def test_can_return_empty_result(self):
+ result = self.whoosh_backend.query(
+ "*:*",
+ sort = [("type", ASC), ("id", DESC)],
+ fields=("id", "type"),
+ facets= ("type", "product")
+ )
+ self.print_result(result)
+ self.assertEqual(0, result.hits)
+
+ def test_can_search_time_with_utc_tzinfo(self):
+ time = datetime(2012, 12, 13, 11, 8, 34, 711957, tzinfo=FixedOffset(0, 'UTC'))
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket", time=time))
+ result = self.whoosh_backend.query("*:*")
+ self.print_result(result)
+ self.assertEqual(time, result.docs[0]["time"])
+
+ def test_can_search_time_without_tzinfo(self):
+ time = datetime(2012, 12, 13, 11, 8, 34, 711957, tzinfo=None)
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket", time=time))
+ result = self.whoosh_backend.query("*:*")
+ self.print_result(result)
+ self.assertEqual(time.replace(tzinfo=utc), result.docs[0]["time"])
+
+ def test_can_search_time_with_non_utc_tzinfo(self):
+ hours = 8
+ tz_diff = 1
+ time = datetime(2012, 12, 13, 11, hours, 34, 711957,
+ tzinfo=FixedOffset(tz_diff, "just_one_timezone"))
+ self.whoosh_backend.add_doc(dict(id="1", type="ticket", time=time))
+ result = self.whoosh_backend.query("*:*")
+ self.print_result(result)
+ self.assertEqual(datetime(2012, 12, 13, 11, hours-tz_diff, 34, 711957,
+ tzinfo=utc), result.docs[0]["time"])
+
+ @unittest.skip("TODO clarify behavior on Whoosh mail list")
+ def test_can_search_id_and_summary(self):
+ #arrange
+ self.insert_ticket("test x")
+ self.insert_ticket("test 1")
+
+# field_boosts = dict(
+# id = 6,
+# type = 2,
+# summary = 5,
+# author = 3,
+# milestone = 2,
+# keywords = 2,
+# component = 2,
+# status = 2,
+# content = 1,
+# changes = 0.8,
+# )
+ fieldboosts = dict(
+ id = 1,
+ summary = 1,
+ )
+
+# parser = MultifieldParser(
+# fieldboosts.keys(),
+# WhooshBackend.SCHEMA,
+## fieldboosts=field_boosts
+# )
+
+ mfp = MultifieldPlugin(list(fieldboosts.keys()),
+# fieldboosts=fieldboosts,
+# group=syntax.DisMaxGroup
+ )
+ pins = [WhitespacePlugin,
+# PlusMinusPlugin,
+ PhrasePlugin,
+ mfp]
+ parser = QueryParser(None, WhooshBackend.SCHEMA, plugins=pins)
+
+
+ parsed_query = parser.parse(u"1")
+# parsed_query = parser.parse(u"test")
+ result = self.whoosh_backend.query(parsed_query)
+ self.print_result(result)
+ self.assertEqual(2, result.hits)
+
+
+def suite():
+ return unittest.makeSuite(WhooshBackendTestCase, 'test')
+
+if __name__ == '__main__':
+ unittest.main()
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/ticket_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/ticket_search.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/ticket_search.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/ticket_search.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+r"""Ticket specifics for Bloodhound Search plugin."""
+from bhsearch.api import ISearchParticipant, BloodhoundSearchApi
+from genshi.builder import tag
+from trac.core import *
+from trac.ticket.api import ITicketChangeListener, TicketSystem
+from trac.ticket import Ticket
+from trac.ticket.query import Query
+from trac.config import Option
+from trac.web.chrome import add_warning
+from trac.util.datefmt import to_datetime
+
+TICKET = "ticket"
+TICKET_STATUS = 'status'
+
+class TicketSearchParticipant(Component):
+ implements(ITicketChangeListener, ISearchParticipant)
+ silence_on_error = Option('bhsearch', 'silence_on_error', "True",
+ """If true, do not throw an exception during indexing a resource""")
+
+ def _index_ticket(self, ticket, search_api=None, raise_exception = False):
+ """Internal method for actually indexing a ticket.
+ This reduces duplicating code."""
+ try:
+ if not search_api:
+ search_api = BloodhoundSearchApi(self.env)
+
+ #This is very naive prototype implementation
+ #TODO: a lot of improvements must be added here!!!
+ contents = {
+ 'id': unicode(ticket.id),
+ 'time': ticket.time_changed,
+ 'type': TICKET,
+ }
+ fields = [('component',), ('description','content'), ('component',),
+ ('keywords',), ('milestone',), ('summary',),
+ ('status',), ('resolution',), ('reporter','author')]
+ for f in fields:
+ if f[0] in ticket.values:
+ if len(f) == 1:
+ contents[f[0]] = ticket.values[f[0]]
+ elif len(f) == 2:
+ contents[f[1]] = ticket.values[f[0]]
+ contents['changes'] = u'\n\n'.join([x[4] for x in ticket.get_changelog()
+ if x[2] == u'comment'])
+ search_api.add_doc(contents)
+ except Exception, e:
+ if (not raise_exception) and self.silence_on_error.lower() == "true":
+ #Is there any way to get request object to add warning?
+ # add_warning(req, _('Exception during ticket indexing: %s' % e))
+ self.log.error("Error occurs during ticke indexing. \
+ The error will not be propagated. Exception: %s", e)
+ else:
+ raise
+
+
+ #ITicketChangeListener methods
+ def ticket_created(self, ticket):
+ """Index a recently created ticket."""
+ self._index_ticket(ticket)
+
+ def ticket_changed(self, ticket, comment, author, old_values):
+ """Reindex a recently modified ticket."""
+ self._index_ticket(ticket)
+
+ def ticket_deleted(self, ticket):
+ """Called when a ticket is deleted."""
+ s = BloodhoundSearchApi(self.env)
+ s.delete_doc(u'ticket', unicode(ticket.id))
+
+ # ISearchParticipant methods
+ def get_search_filters(self, req=None):
+ if not req or 'TICKET_VIEW' in req.perm:
+ return ('ticket', 'Tickets')
+
+ def build_search_index(self, backend):
+ """
+ :type backend: ISearchBackend
+ """
+ #TODO: some king of paging/batch size should be introduced in order to
+ # avoid loading of all ticket ids in memory
+ query_records = self.load_tickets_ids()
+ for record in query_records:
+ ticket_id = record["id"]
+ ticket = Ticket(self.env, ticket_id)
+ self._index_ticket(ticket, backend, raise_exception=True)
+
+ def load_tickets_ids(self):
+ #is there better way to get all tickets?
+ query = Query(self.env, cols=['id'], order='id')
+ return query.execute()
+
+ def format_search_results(self, res):
+ if not TICKET_STATUS in res:
+ stat = 'undefined_status'
+ class_ = 'undefined_status'
+ else:
+ class_= res[TICKET_STATUS]
+ if res[TICKET_STATUS] == 'closed':
+ resolution = ""
+ if 'resolution' in res:
+ resolution = res['resolution']
+ stat = '%s: %s' % (res['status'], resolution)
+ else:
+ stat = res[TICKET_STATUS]
+
+ id = tag(tag.span('#'+res['id'], class_=class_))
+ return id + ': %s (%s)' % (res['summary'], stat)
+
Modified: incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py?rev=1425649&r1=1425648&r2=1425649&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py Mon Dec 24 13:47:06 2012
@@ -18,7 +18,7 @@
# specific language governing permissions and limitations
# under the License.
-"""Bloodhound Search user interface"""
+r"""Bloodhound Search user interface."""
import pkg_resources
import re
@@ -26,12 +26,15 @@ import re
from trac.core import *
from genshi.builder import tag
from trac.perm import IPermissionRequestor
+from trac.search import shorten_result
+from trac.util.presentation import Paginator
+from trac.util.datefmt import format_datetime, user_time
from trac.web import IRequestHandler
from trac.util.translation import _
from trac.web.chrome import (INavigationContributor, ITemplateProvider,
add_link, add_stylesheet, add_warning,
web_context)
-from bhsearch.api import BloodhoundQuerySystem
+from bhsearch.api import BloodhoundSearchApi, ISearchParticipant, SCORE, ASC, DESC
SEARCH_PERMISSION = 'SEARCH_VIEW'
@@ -43,6 +46,12 @@ class BloodhoundSearchModule(Component):
# IWikiSyntaxProvider #todo: implement later
)
+ search_participants = ExtensionPoint(ISearchParticipant)
+
+ RESULTS_PER_PAGE = 10
+ DEFAULT_SORT = [(SCORE, ASC), ("time", DESC)]
+
+
# INavigationContributor methods
def get_active_navigation_item(self, req):
return 'bhsearch'
@@ -65,18 +74,130 @@ class BloodhoundSearchModule(Component):
req.perm.assert_permission(SEARCH_PERMISSION)
query = req.args.get('q')
+ if query == None:
+ query = ""
+
+ #TODO add quick jump support
- data = {}
- if query:
- data["query"] = query
-
- #TODO: add implementation here
- querySystem = BloodhoundQuerySystem(self.env)
- result = querySystem.query(query)
+ #TODO: refactor filters or replace with facets
+ filters = []
+# available_filters = filter(None, [p.get_search_filters(req) for p
+# in self.search_participants])
+# filters = [f[0] for f in available_filters if req.args.has_key(f[0])]
+# if not filters:
+# filters = [f[0] for f in available_filters
+# if f[0] not in self.default_disabled_filters and
+# (len(f) < 3 or len(f) > 2 and f[2])]
+# data = {'filters': [{'name': f[0], 'label': f[1],
+# 'active': f[0] in filters}
+# for f in available_filters],
+# 'quickjump': None,
+# 'results': []}
+
+ data = {
+ 'query': query,
+ }
+
+ # Initial page request
+ #todo: filters check, tickets etc
+ if not any((query, )):
+ return self._return_data(req, data)
+
+ page = int(req.args.get('page', '1'))
+
+ #todo: retrieve sort from query string
+ sort = self.DEFAULT_SORT
+
+ #todo: add proper facets functionality
+# facets = ("type", "status")
+ facets = ("type",)
+
+
+ querySystem = BloodhoundSearchApi(self.env)
+ query_result = querySystem.query(
+ query,
+ pagenum = page,
+ pagelen = self.RESULTS_PER_PAGE,
+ sort = sort,
+ facets = facets,
+ )
+ ui_docs = [self._process_doc(doc, req)
+ for doc in query_result.docs]
+
+
+ results = Paginator(
+ ui_docs,
+ page - 1,
+ self.RESULTS_PER_PAGE,
+ query_result.hits,
+ )
+
+ results.shown_pages = self._prepare_shown_pages(
+ filters,
+ query,
+ req,
+ shown_pages = results.get_shown_pages(self.RESULTS_PER_PAGE))
+
+ results.current_page = {'href': None, 'class': 'current',
+ 'string': str(results.page + 1),
+ 'title':None}
+
+ if results.has_next_page:
+ next_href = req.href.bhsearch(zip(filters, ['on'] * len(filters)),
+ q=req.args.get('q'), page=page + 1,
+ noquickjump=1)
+ add_link(req, 'next', next_href, _('Next Page'))
+
+ if results.has_previous_page:
+ prev_href = req.href.bhsearch(zip(filters, ['on'] * len(filters)),
+ q=req.args.get('q'), page=page - 1,
+ noquickjump=1)
+ add_link(req, 'prev', prev_href, _('Previous Page'))
+
+ data['results'] = results
+
+ #add proper facet links
+ data['facets'] = query_result.facets
+
+ data['page_href'] = req.href.bhsearch(
+ zip(filters, ['on'] * len(filters)), q=req.args.get('q'),
+ noquickjump=1)
+ return self._return_data(req, data)
+ def _return_data(self, req, data):
add_stylesheet(req, 'common/css/search.css')
return 'bhsearch.html', data, None
+ def _process_doc(self, doc,req):
+ titlers = dict([(x.get_search_filters(req)[0], x.format_search_results)
+ for x in self.search_participants if x.get_search_filters(req)])
+
+ #todo: introduce copy by predefined value
+ ui_doc = dict(doc)
+
+ ui_doc["href"] = req.href(doc['type'], doc['id'])
+ #todo: perform content adaptation here
+ if doc.has_key('content'):
+ ui_doc['excerpt'] = shorten_result(doc['content'])
+ if doc.has_key('time'):
+ ui_doc['date'] = user_time(req, format_datetime, doc['time'])
+
+ ui_doc['title'] = titlers[doc['type']](doc)
+ return ui_doc
+
+ def _prepare_shown_pages(self, filters, query, req, shown_pages):
+ pagedata = []
+ for shown_page in shown_pages:
+ page_href = req.href.bhsearch([(f, 'on') for f in filters],
+ q=query,
+ page=shown_page, noquickjump=1)
+ pagedata.append([page_href, None, str(shown_page),
+ 'page ' + str(shown_page)])
+ fields = ['href', 'class', 'string', 'title']
+ result_shown_pages = [dict(zip(fields, p)) for p in pagedata]
+ return result_shown_pages
+
+
# ITemplateProvider methods
def get_htdocs_dirs(self):
# return [('bhsearch', pkg_resources.resource_filename(__name__, 'htdocs'))]
@@ -85,3 +206,4 @@ class BloodhoundSearchModule(Component):
def get_templates_dirs(self):
return [pkg_resources.resource_filename(__name__, 'templates')]
+
Added: incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py?rev=1425649&view=auto
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py (added)
+++ incubator/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py Mon Dec 24 13:47:06 2012
@@ -0,0 +1,261 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+r"""Whoosh specific backend for Bloodhound Search plugin."""
+from bhsearch.api import ISearchBackend, DESC, QueryResult, SCORE
+import os
+from trac.core import *
+from trac.config import Option, PathOption
+from trac.util.datefmt import utc
+from whoosh.fields import *
+from whoosh import index, sorting
+from whoosh.qparser import QueryParser
+from whoosh.searching import ResultsPage
+from whoosh.sorting import FieldFacet
+from whoosh.writing import AsyncWriter
+from datetime import datetime, date
+
+class WhooshBackend(Component):
+ """
+ Implements Whoosh SearchBackend interface
+ """
+ implements(ISearchBackend)
+
+ index_dir_setting = Option('bhsearch', 'whoosh_index_dir', 'whoosh_index',
+ """Relative path is resolved relatively to the
+ directory of the environment.""")
+
+ #This is schema prototype. It will be changed later
+ #TODO: add other fields support, add dynamic field support
+ SCHEMA = Schema(
+ unique_id=ID(stored=True, unique=True),
+ id=ID(stored=True),
+ type=ID(stored=True),
+ product=ID(stored=True),
+ time=DATETIME(stored=True),
+ author=ID(stored=True),
+ component=KEYWORD(stored=True),
+ status=KEYWORD(stored=True),
+ resolution=KEYWORD(stored=True),
+ keywords=KEYWORD(scorable=True),
+ milestone=TEXT(spelling=True),
+ summary=TEXT(stored=True),
+ content=TEXT(stored=True),
+ changes=TEXT(),
+ )
+
+ def __init__(self):
+ self.index_dir = self.index_dir_setting
+ if not os.path.isabs(self.index_dir):
+ self.index_dir = os.path.join(self.env.path, self.index_dir)
+ self.open_or_create_index_if_missing()
+
+ #ISearchBackend methods
+ def add_doc(self, doc, commit=True):
+ """Add any type of document index.
+
+ The contents should be a dict with fields matching the search schema.
+ The only required fields are type and id, everything else is optional.
+ """
+ # Really make sure it's unicode, because Whoosh won't have it any
+ # other way.
+ for key in doc:
+ doc[key] = self._to_whoosh_format(doc[key])
+
+ doc["unique_id"] = u"%s:%s" % (doc["type"], doc["id"])
+
+ writer = AsyncWriter(self.index)
+ committed = False
+ try:
+ #todo: remove it!!!
+ self.log.debug("Doc to index: %s", doc)
+ writer.update_document(**doc)
+ writer.commit()
+ committed = True
+ finally:
+ if not committed:
+ writer.cancel()
+
+
+ def query(self, query, sort = None, fields = None, boost = None, filters = None,
+ facets = None, pagenum = 1, pagelen = 20):
+
+ with self.index.searcher() as searcher:
+ parser = QueryParser("content", self.index.schema)
+ if isinstance(query, basestring):
+ query = unicode(query)
+ parsed_query = parser.parse(unicode(query))
+ else:
+ parsed_query = query
+
+ sortedby = self._prepare_sortedby(sort)
+ groupedby = self._prepare_groupedby(facets)
+ self.env.log.debug("Whoosh query to execute: %s, sortedby = %s, \
+ pagenum=%s, pagelen=%s, facets=%s",
+ parsed_query,
+ sortedby,
+ pagenum,
+ pagelen,
+ groupedby,
+ )
+ raw_page = searcher.search_page(
+ parsed_query,
+ pagenum = pagenum,
+ pagelen = pagelen,
+ sortedby = sortedby,
+ groupedby = groupedby,
+ )
+# raw_page = ResultsPage(whoosh_results, pagenum, pagelen)
+ results = self._process_results(raw_page, fields)
+ return results
+
+ def delete_doc(self, doc, commit=True):
+ pass
+
+ def commit(self):
+ pass
+
+ def optimize(self):
+ pass
+
+ def recreate_index(self):
+ self.index = self._create_index()
+
+ def open_or_create_index_if_missing(self):
+ if index.exists_in(self.index_dir):
+ self.index = index.open_dir(self.index_dir)
+ else:
+ self.index = self._create_index()
+
+
+ def _to_whoosh_format(self, value):
+ if isinstance(value, basestring):
+ value = unicode(value)
+ elif isinstance(value, datetime):
+ value = self._convert_date_to_tz_naive_utc(value)
+ return value
+
+ def _convert_date_to_tz_naive_utc(self, value):
+ """Convert datetime to naive utc datetime
+ Whoosh can not read from index datetime value with
+ tzinfo=trac.util.datefmt.FixedOffset because of non-empty
+ constructor"""
+ if value.tzinfo:
+ utc_time = value.astimezone(utc)
+ value = utc_time.replace(tzinfo=None)
+ return value
+
+ def _from_whoosh_format(self, value):
+ if isinstance(value, datetime):
+ value = utc.localize(value)
+ return value
+
+ def _prepare_groupedby(self, facets):
+ if not facets:
+ return None
+ groupedby = sorting.Facets()
+ for facet_name in facets:
+ groupedby.add_field(facet_name, allow_overlap=True, maptype=sorting.Count)
+ return groupedby
+
+ def _prepare_sortedby(self, sort):
+ if not sort:
+ return None
+ sortedby = []
+ for (field, order) in sort:
+ if field.lower() == SCORE:
+ if self._is_desc(order):
+ #We can implement later our own ScoreFacet with
+ # "score DESC" support
+ raise TracError("Whoosh does not support DESC score ordering.")
+ sort_condition = sorting.ScoreFacet()
+ else:
+ sort_condition = sorting.FieldFacet(field, reverse=self._is_desc(order))
+ sortedby.append(sort_condition)
+ return sortedby
+
+ def _is_desc(self, order):
+ return (order.lower()==DESC)
+
+ def _process_results(self, page, fields):
+ # It's important to grab the hits first before slicing. Otherwise, this
+ # can cause pagination failures.
+ """
+ :type fields: iterator
+ :type page: ResultsPage
+ """
+ results = QueryResult()
+ results.hits = page.total
+ results.total_page_count = page.pagecount
+ results.page_number = page.pagenum
+ results.offset = page.offset
+ results.facets = self._load_facets(page)
+
+ docs = []
+ for doc_offset, retrieved_record in enumerate(page):
+ result_doc = self._process_record(fields, retrieved_record)
+ docs.append(result_doc)
+ results.docs = docs
+ return results
+
+ def _process_record(self, fields, retrieved_record):
+ result_doc = dict()
+ #add score field by default
+ if not fields or SCORE in fields:
+ score = retrieved_record.score
+ result_doc[SCORE] = score
+
+ if fields:
+ for field in fields:
+ if field in retrieved_record:
+ result_doc[field] = retrieved_record[field]
+ else:
+ for key, value in retrieved_record.items():
+ result_doc[key] = value
+
+ for key, value in result_doc.iteritems():
+ result_doc[key] = self._from_whoosh_format(value)
+ return result_doc
+
+ def _load_facets(self, page):
+ non_paged_results = page.results
+ facet_names = non_paged_results.facet_names()
+ if not facet_names:
+ return None
+ facets_result = dict()
+ for name in facet_names:
+ facets_result[name] = non_paged_results.groups(name)
+ return facets_result
+
+ def _create_index(self):
+ self.log.info('Creating Whoosh index in %s' % self.index_dir)
+ self._mkdir_if_not_exists()
+ return index.create_in(self.index_dir, schema=self.SCHEMA)
+
+ def _mkdir_if_not_exists(self):
+ if not os.path.exists(self.index_dir):
+ os.mkdir(self.index_dir)
+
+ if not os.access(self.index_dir, os.W_OK):
+ raise TracError(
+ "The path to Whoosh index '%s' is not writable for the\
+ current user."
+ % self.index_dir)
+
Modified: incubator/bloodhound/trunk/bloodhound_search/setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/bloodhound_search/setup.py?rev=1425649&r1=1425648&r2=1425649&view=diff
==============================================================================
--- incubator/bloodhound/trunk/bloodhound_search/setup.py (original)
+++ incubator/bloodhound/trunk/bloodhound_search/setup.py Mon Dec 24 13:47:06 2012
@@ -29,7 +29,7 @@ Add free text search and query functiona
"""
versions = [
- (0, 4, 0),
+ (0, 4, 1),
]
latest = '.'.join(str(x) for x in versions[-1])
@@ -95,11 +95,11 @@ PKG_INFO = {'bhsearch' : ('bhsearch',
'htdocs/img/*.*', 'htdocs/js/*.js',
'templates/*', 'default-pages/*'],
),
-# 'search.widgets' : ('search/widgets', # Package dir
+# 'search.widgets' : ('bhsearch/widgets', # Package dir
# # Package data
# ['templates/*', 'htdocs/*.css'],
# ),
-# 'search.layouts' : ('search/layouts', # Package dir
+# 'search.layouts' : ('bhsearch/layouts', # Package dir
# # Package data
# ['templates/*'],
# ),
@@ -109,21 +109,38 @@ PKG_INFO = {'bhsearch' : ('bhsearch',
),
}
-ENTRY_POINTS = r"""
- [trac.plugins]
- bhsearch.web_ui = bhsearch.web_ui
- bhsearch.api = bhsearch.api
- """
+#ENTRY_POINTS = r"""
+# [trac.plugins]
+# bhsearch.web_ui = bhsearch.web_ui
+# bhsearch.api = bhsearch.api
+# bhsearch.admin = bhsearch.admin
+# bhsearch.ticket_search = bhsearch.ticket_search
+# bhsearch.query_parser = bhsearch.query_parser
+# bhsearch.whoosh_backend = bhsearch.whoosh_backend
+# """
+ENTRY_POINTS = {
+ 'trac.plugins': [
+ 'bhsearch.web_ui = bhsearch.web_ui',
+ 'bhsearch.api = bhsearch.api',
+ 'bhsearch.admin = bhsearch.admin',
+ 'bhsearch.ticket_search = bhsearch.ticket_search',
+ 'bhsearch.query_parser = bhsearch.query_parser',
+ 'bhsearch.whoosh_backend = bhsearch.whoosh_backend',
+ ],
+ }
+#bhsearch.whoosh_backend = bhsearch.whoosh_backend
+#bhsearch.ticket_search = bhsearch.ticket_search
setup(
name=DIST_NM,
version=latest,
description=DESC.split('\n', 1)[0],
requires = ['trac'],
- tests_require = ['dutest>=0.2.4', 'TracXMLRPC'],
+# tests_require = ['dutest>=0.2.4', 'TracXMLRPC'],
install_requires = [
'setuptools>=0.6b1',
'Trac>=0.11',
+ 'whoosh>=2.4.1',
],
package_dir = dict([p, i[0]] for p, i in PKG_INFO.iteritems()),
packages = PKG_INFO.keys(),