Index: setup.py
===================================================================
--- setup.py	(revision 7941)
+++ setup.py	(working copy)
@@ -70,7 +70,8 @@
 
     install_requires = [
         'setuptools>=0.6b1',
-        'Genshi>=0.6dev-r960'
+        'Genshi>=0.6dev-r960',
+        'whoosh>=0.1.11',
     ],
     extras_require = {
         'Babel': ['Babel>=0.9.4'],
@@ -102,6 +103,7 @@
         trac.mimeview.txtl = trac.mimeview.txtl[Textile]
         trac.prefs = trac.prefs.web_ui
         trac.search = trac.search.web_ui
+        trac.search.admin = trac.search.admin
         trac.ticket.admin = trac.ticket.admin
         trac.ticket.query = trac.ticket.query
         trac.ticket.report = trac.ticket.report
Index: trac/attachment.py
===================================================================
--- trac/attachment.py	(revision 7941)
+++ trac/attachment.py	(working copy)
@@ -35,7 +35,6 @@
 from trac.mimeview import *
 from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy
 from trac.resource import *
-from trac.search import search_to_sql, shorten_result
 from trac.util import get_reporter_id, create_unique_file, content_disposition
 from trac.util.datefmt import format_datetime, to_timestamp, utc
 from trac.util.text import exception_to_unicode, pretty_size, print_table, \
@@ -481,30 +480,6 @@
                        _(" attached to "), tag.em(name, title=title))
         elif field == 'description':
             return format_to(self.env, None, context(attachment.parent), descr)
-   
-    def get_search_results(self, req, resource_realm, terms):
-        """Return a search result generator suitable for ISearchSource.
-        
-        Search results are attachments on resources of the given 
-        `resource_realm.realm` whose filename, description or author match 
-        the given terms.
-        """
-        db = self.env.get_db_cnx()
-        sql_query, args = search_to_sql(db, ['filename', 'description', 
-                                        'author'], terms)
-        cursor = db.cursor()
-        cursor.execute("SELECT id,time,filename,description,author "
-                       "FROM attachment "
-                       "WHERE type = %s "
-                       "AND " + sql_query, (resource_realm.realm, ) + args)
-        
-        for id, time, filename, desc, author in cursor:
-            attachment = resource_realm(id=id).child('attachment', filename)
-            if 'ATTACHMENT_VIEW' in req.perm(attachment):
-                yield (get_resource_url(self.env, attachment, req.href),
-                       get_resource_shortname(self.env, attachment),
-                       datetime.fromtimestamp(time, utc), author,
-                       shorten_result(desc, terms))
     
     # IResourceManager methods
     
Index: trac/ticket/api.py
===================================================================
--- trac/ticket/api.py	(revision 7941)
+++ trac/ticket/api.py	(working copy)
@@ -166,6 +166,15 @@
         self._fields_lock = threading.RLock()
 
     # Public API
+    
+    def get_tickets(self):
+        """Returns a list of all tickets."""
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT DISTINCT id FROM ticket")
+        for (ticket,) in cursor:
+            yield ticket
+            
 
     def get_available_actions(self, req, ticket):
         """Returns a sorted list of available actions"""
Index: trac/ticket/web_ui.py
===================================================================
--- trac/ticket/web_ui.py	(revision 7941)
+++ trac/ticket/web_ui.py	(working copy)
@@ -31,7 +31,6 @@
 from trac.mimeview.api import Mimeview, IContentConverter, Context
 from trac.resource import Resource, get_resource_url, \
                          render_resource_link, get_resource_shortname
-from trac.search import ISearchSource, search_to_sql, shorten_result
 from trac.ticket.api import TicketSystem, ITicketManipulator, \
                             ITicketActionController
 from trac.ticket.model import Milestone, Ticket, group_milestones
@@ -59,7 +58,7 @@
 class TicketModule(Component):
 
     implements(IContentConverter, INavigationContributor, IRequestHandler,
-               ISearchSource, ITemplateProvider, ITimelineEventProvider)
+               ITemplateProvider, ITimelineEventProvider)
 
     ticket_manipulators = ExtensionPoint(ITicketManipulator)
 
@@ -198,48 +197,7 @@
     def get_templates_dirs(self):
         return [pkg_resources.resource_filename('trac.ticket', 'templates')]
 
-    # ISearchSource methods
 
-    def get_search_filters(self, req):
-        if 'TICKET_VIEW' in req.perm:
-            yield ('ticket', 'Tickets')
-
-    def get_search_results(self, req, terms, filters):
-        if not 'ticket' in filters:
-            return
-        ticket_realm = Resource('ticket')
-        db = self.env.get_db_cnx()
-        sql, args = search_to_sql(db, ['b.newvalue'], terms)
-        sql2, args2 = search_to_sql(db, ['summary', 'keywords', 'description',
-                                         'reporter', 'cc', 
-                                         db.cast('id', 'text')], terms)
-        sql3, args3 = search_to_sql(db, ['c.value'], terms)
-        cursor = db.cursor()
-        cursor.execute("SELECT DISTINCT a.summary,a.description,a.reporter, "
-                       "a.type,a.id,a.time,a.status,a.resolution "
-                       "FROM ticket a "
-                       "LEFT JOIN ticket_change b ON a.id = b.ticket "
-                       "LEFT OUTER JOIN ticket_custom c ON (a.id = c.ticket) "
-                       "WHERE (b.field='comment' AND %s) OR %s OR %s" % 
-                       (sql, sql2, sql3), args + args2 + args3)
-        ticketsystem = TicketSystem(self.env)
-        for summary, desc, author, type, tid, ts, status, resolution in cursor:
-            t = ticket_realm(id=tid)
-            if 'TICKET_VIEW' in req.perm(t):
-                yield (req.href.ticket(tid),
-                       tag(tag.span(get_resource_shortname(self.env, t),
-                                    class_=status),
-                           ': ',
-                           ticketsystem.format_summary(summary, status,
-                                                       resolution, type)),
-                       datetime.fromtimestamp(ts, utc), author,
-                       shorten_result(desc, terms))
-        
-        # Attachments
-        for result in AttachmentModule(self.env).get_search_results(
-            req, ticket_realm, terms):
-            yield result        
-
     # ITimelineEventProvider methods
 
     def get_timeline_filters(self, req):
Index: trac/ticket/__init__.py
===================================================================
--- trac/ticket/__init__.py	(revision 7941)
+++ trac/ticket/__init__.py	(working copy)
@@ -1,3 +1,4 @@
 from trac.ticket.api import *
 from trac.ticket.default_workflow import *
 from trac.ticket.model import *
+from trac.ticket.search import *
\ No newline at end of file
Index: trac/ticket/roadmap.py
===================================================================
--- trac/ticket/roadmap.py	(revision 7941)
+++ trac/ticket/roadmap.py	(working copy)
@@ -23,12 +23,12 @@
 
 from trac import __version__
 from trac.attachment import AttachmentModule
-from trac.config import ExtensionOption
+from trac.config import ExtensionOption, ExtensionPoint
 from trac.core import *
 from trac.mimeview import Context
 from trac.perm import IPermissionRequestor
 from trac.resource import *
-from trac.search import ISearchSource, search_to_sql, shorten_result
+from trac.search import ISearchParticipant, SearchSystem
 from trac.util.datefmt import parse_date, utc, to_timestamp, to_datetime, \
                               get_date_format_hint, get_datetime_format_hint, \
                               format_date, format_datetime
@@ -50,6 +50,24 @@
         This method returns a valid TicketGroupStats object.
         """
 
+class IMilestoneChangeListener(Interface):
+    """Extension point interface for components that require notification
+    when tickets are created, modified, or deleted."""
+
+    def milestone_created(milestone):
+        """Called when a ticket is created."""
+
+    def milestone_changed(milestone, comment, author, old_values):
+        """Called when a milestone is modified.
+
+        `old_values` is a dictionary containing the previous values of the
+        fields that have changed.
+        """
+
+    def milestone_deleted(milestone):
+        """Called when a milestone is deleted."""
+
+
 class TicketGroupStats(object):
     """Encapsulates statistics on a group of tickets."""
 
@@ -286,10 +304,10 @@
                                for interval in stat.intervals]}
 
 
-
 class RoadmapModule(Component):
 
-    implements(INavigationContributor, IPermissionRequestor, IRequestHandler)
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+        ISearchParticipant)
     stats_provider = ExtensionOption('roadmap', 'stats_provider',
                                      ITicketGroupStatsProvider,
                                      'DefaultTicketGroupStatsProvider',
@@ -468,13 +486,30 @@
         write_prop('END', 'VCALENDAR')
 
 
+    # ISearchParticipant methods
+    def get_search_filters(self, req=None):
+        if not req or 'MILESTONE_VIEW' in req.perm:
+            return ('milestone', 'Milestones')
 
+    def build_search_index(self, s):
+        db = self.env.get_db_cnx()
+        milestones = Milestone.select(self.env, True, db)
+        for milestone in milestones:
+            _index_milestone(self.env, milestone)
+
+    def format_search_results(self, res):
+        return u'Milestone: %s' % res['id']
+
+
+
+
 class MilestoneModule(Component):
 
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
                ITimelineEventProvider, IWikiSyntaxProvider, IResourceManager,
-               ISearchSource)
+               IMilestoneChangeListener)
  
+    change_listeners = ExtensionPoint(IMilestoneChangeListener)
     stats_provider = ExtensionOption('milestone', 'stats_provider',
                                      ITicketGroupStatsProvider,
                                      'DefaultTicketGroupStatsProvider',
@@ -590,6 +625,8 @@
             retarget_to = req.args.get('target') or None
         milestone.delete(retarget_to, req.authname)
         db.commit()
+        for listener in self.change_listeners:
+            listener.milestone_deleted(milestone)
         req.redirect(req.href.roadmap())
 
     def _do_save(self, req, db, milestone):
@@ -660,6 +697,8 @@
             milestone.insert()
         db.commit()
 
+        for listener in self.change_listeners:
+            listener.milestone_changed(milestone)
         req.redirect(req.href.milestone(milestone.name))
 
     def _render_confirm(self, req, db, milestone):
@@ -820,33 +859,24 @@
         else:
             return desc
 
-    # ISearchSource methods
+    # IMilestoneChangeListener methods
 
-    def get_search_filters(self, req):
-        if 'MILESTONE_VIEW' in req.perm:
-            yield ('milestone', _('Milestones'))
+    def milestone_changed(self, milestone):
+        _index_milestone(self.env, milestone)
 
-    def get_search_results(self, req, terms, filters):
-        if not 'milestone' in filters:
-            return
-        db = self.env.get_db_cnx()
-        sql_query, args = search_to_sql(db, ['name', 'description'], terms)
-        cursor = db.cursor()
-        cursor.execute("SELECT name,due,completed,description "
-                       "FROM milestone "
-                       "WHERE " + sql_query, args)
-
-        milestone_realm = Resource('milestone')
-        for name, due, completed, description in cursor:
-            milestone = milestone_realm(id=name)
-            if 'MILESTONE_VIEW' in req.perm(milestone):
-                yield (get_resource_url(self.env, milestone, req.href),
-                       get_resource_name(self.env, milestone),
-                       datetime.fromtimestamp(
-                           completed or due or time(), utc),
-                       '', shorten_result(description, terms))
+    def milestone_deleted(self, milestone):
+        s = SearchSystem(self.env)
+        s.delete_doc(u'milestone', milestone.name)
         
-        # Attachments
-        for result in AttachmentModule(self.env).get_search_results(
-            req, milestone_realm, terms):
-            yield result
+def _index_milestone(env, milestone, search_system=None):
+    if not search_system:
+        search_system = SearchSystem(env)
+    if milestone.is_completed:
+        status = u'completed'
+    else:
+        status = u'open'
+    contents = {'id' : milestone.name,
+        'content' : milestone.description,
+        'status' : status,
+        }
+    search_system.index_doc(u'milestone', contents)
\ No newline at end of file
Index: trac/versioncontrol/web_ui/changeset.py
===================================================================
--- trac/versioncontrol/web_ui/changeset.py	(revision 7941)
+++ trac/versioncontrol/web_ui/changeset.py	(working copy)
@@ -33,7 +33,6 @@
 from trac.mimeview import Mimeview, is_binary, Context
 from trac.perm import IPermissionRequestor
 from trac.resource import Resource, ResourceNotFound
-from trac.search import ISearchSource, search_to_sql, shorten_result
 from trac.timeline.api import ITimelineEventProvider
 from trac.util import embedded_numbers, content_disposition
 from trac.util.compat import any
@@ -122,7 +121,7 @@
     """
 
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
-               ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource)
+               ITimelineEventProvider, IWikiSyntaxProvider)
 
     property_diff_renderers = ExtensionPoint(IPropertyDiffRenderer)
     
@@ -955,28 +954,7 @@
 
     # ISearchSource methods
 
-    def get_search_filters(self, req):
-        if 'CHANGESET_VIEW' in req.perm:
-            yield ('changeset', _('Changesets'))
 
-    def get_search_results(self, req, terms, filters):
-        if not 'changeset' in filters:
-            return
-        repos = self.env.get_repository(req.authname)
-        db = self.env.get_db_cnx()
-        sql, args = search_to_sql(db, ['rev', 'message', 'author'], terms)
-        cursor = db.cursor()
-        cursor.execute("SELECT rev,time,author,message "
-                       "FROM revision WHERE " + sql, args)
-        for rev, ts, author, log in cursor:
-            if not repos.authz.has_permission_for_changeset(rev):
-                continue
-            yield (req.href.changeset(rev),
-                   '[%s]: %s' % (rev, shorten_line(log)),
-                   datetime.fromtimestamp(ts, utc), author,
-                   shorten_result(log, terms))
-
-
 class AnyDiffModule(Component):
 
     implements(IRequestHandler)
Index: trac/wiki/web_ui.py
===================================================================
--- trac/wiki/web_ui.py	(revision 7941)
+++ trac/wiki/web_ui.py	(working copy)
@@ -29,7 +29,6 @@
 from trac.mimeview.api import Mimeview, IContentConverter, Context
 from trac.perm import IPermissionRequestor
 from trac.resource import *
-from trac.search import ISearchSource, search_to_sql, shorten_result
 from trac.timeline.api import ITimelineEventProvider
 from trac.util import get_reporter_id
 from trac.util.datefmt import to_timestamp, utc
@@ -54,7 +53,7 @@
 class WikiModule(Component):
 
     implements(IContentConverter, INavigationContributor, IPermissionRequestor,
-               IRequestHandler, ITimelineEventProvider, ISearchSource,
+               IRequestHandler, ITimelineEventProvider,
                ITemplateProvider)
 
     page_manipulators = ExtensionPoint(IWikiPageManipulator)
@@ -608,37 +607,3 @@
                     wiki_page.id, version=wiki_page.version, action='diff')
                 markup = tag(markup, ' ', tag.a('(diff)', href=diff_href))
             return markup
-
-    # ISearchSource methods
-
-    def get_search_filters(self, req):
-        if 'WIKI_VIEW' in req.perm:
-            yield ('wiki', _('Wiki'))
-
-    def get_search_results(self, req, terms, filters):
-        if not 'wiki' in filters:
-            return
-        db = self.env.get_db_cnx()
-        sql_query, args = search_to_sql(db, ['w1.name', 'w1.author', 'w1.text'],
-                                        terms)
-        cursor = db.cursor()
-        cursor.execute("SELECT w1.name,w1.time,w1.author,w1.text "
-                       "FROM wiki w1,"
-                       "(SELECT name,max(version) AS ver "
-                       "FROM wiki GROUP BY name) w2 "
-                       "WHERE w1.version = w2.ver AND w1.name = w2.name "
-                       "AND " + sql_query, args)
-
-        wiki_realm = Resource('wiki')
-        for name, ts, author, text in cursor:
-            page = wiki_realm(id=name)
-            if 'WIKI_VIEW' in req.perm(page):
-                yield (get_resource_url(self.env, page, req.href),
-                       '%s: %s' % (name, shorten_line(text)),
-                       datetime.fromtimestamp(ts, utc), author,
-                       shorten_result(text, terms))
-        
-        # Attachments
-        for result in AttachmentModule(self.env).get_search_results(
-            req, wiki_realm, terms):
-            yield result
Index: trac/wiki/__init__.py
===================================================================
--- trac/wiki/__init__.py	(revision 7941)
+++ trac/wiki/__init__.py	(working copy)
@@ -3,3 +3,4 @@
 from trac.wiki.intertrac import *
 from trac.wiki.model import *
 from trac.wiki.parser import *
+from trac.wiki.search import *
\ No newline at end of file
Index: trac/search/admin.py
===================================================================
--- trac/search/admin.py	(revision 0)
+++ trac/search/admin.py	(revision 0)
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/.
+
+from datetime import datetime
+import os.path
+import pkg_resources
+import sys
+import time
+
+from trac.admin import *
+from trac.core import *
+from trac.search.api import *
+
+
+class SearchAdmin(Component):
+    """Search administration component. Only rebuilds the index."""
+
+    implements(IAdminCommandProvider)
+
+    # IAdminCommandProvider methods
+    
+    def get_admin_commands(self):
+        yield ('search rebuild', '',
+            'Rebuild the search index',
+            None, SearchSystem(self.env).rebuild_index)
+        yield ('search optimize', '',
+            'Optimize the search index',
+            None, SearchSystem(self.env).optimize)
\ No newline at end of file
Index: trac/search/api.py
===================================================================
--- trac/search/api.py	(revision 7941)
+++ trac/search/api.py	(working copy)
@@ -12,49 +12,140 @@
 # history and logs, available at http://trac.edgewall.org/log/.
 
 from trac.core import *
+from trac.env import IEnvironmentSetupParticipant
 
+from whoosh.fields import *
+from whoosh import index
+import os
 
-class ISearchSource(Interface):
-    """Extension point interface for adding search sources to the search
-    system.
+class ISearchParticipant(Interface):
+    """Extension point interface for components that should be searched.
     """
 
     def get_search_filters(req):
-        """Return a list of filters that this search source supports.
-        
-        Each filter must be a `(name, label[, default])` tuple, where `name` is
-        the internal name, `label` is a human-readable name for display and
-        `default` is an optional boolean for determining whether this filter
-        is searchable by default.
-        """
+        """Called when we want to build the list of components with search.
+        Passes the request object to do permission checking."""
 
-    def get_search_results(req, terms, filters):
-        """Return a list of search results matching each search term in `terms`.
+    def build_search_index(search_system):
+        """Called when we want to rebuild the entire index."""
         
-        The `filters` parameters is a list of the enabled filters, each item
-        being the name of the tuples returned by `get_search_events`.
+    def format_search_results(contents):
+        """Called to see if the module wants to format the search results."""
 
-        The events returned by this function must be tuples of the form
-        `(href, title, date, author, excerpt).`
-        """
 
+class SearchSystem(Component):
+    """Implements the search system, provides methods for adding and 
+    Deleting documents for indexing.
+    """
 
-def search_to_sql(db, columns, terms):
-    """Convert a search query into an SQL WHERE clause and corresponding
-    parameters.
+    implements(IEnvironmentSetupParticipant)    
+    search_participants = ExtensionPoint(ISearchParticipant)
     
-    The result is returned as an `(sql, params)` tuple.
-    """
-    assert columns and terms
+    def __init__(self):
+        if not self.env.path:
+            raise TracError(_("Environment.path is very much required..."))
+        self.searchpath = os.path.join(self.env.path, 'search_index')
 
-    likes = ['%s %s' % (i, db.like()) for i in columns]
-    c = ' OR '.join(likes)
-    sql = '(' + ') AND ('.join([c] * len(terms)) + ')'
-    args = []
-    for t in terms:
-        args.extend(['%' + db.like_escape(t) + '%'] * len(columns))
-    return sql, tuple(args)
+    SCHEMA = Schema(
+        unique_id=ID(stored=True, unique=True),
+        id=ID(stored=True),
+        type=ID(stored=True),
+        time=ID(stored=True),
+        author=ID(stored=True),
+        component=KEYWORD, 
+        status=KEYWORD(stored=True), 
+        resolution=KEYWORD(stored=True),
+        keywords=KEYWORD(scorable=True),
+        milestone=TEXT,
+        summary=TEXT(stored=True),
+        content=TEXT(stored=True),
+        changes=TEXT,
+        )
 
+    def rebuild_index(self):
+       """Delete the index if it exists. Then create a new full index."""
+       self.log.info('Rebuilding the search index.')
+       
+       if not os.path.exists(self.searchpath):
+           os.mkdir(self.searchpath)
+       self.index = index.create_in(self.searchpath, schema=self.SCHEMA)
+       for participant in self.search_participants:
+           self.log.debug('Now building search index for: %s.'
+                % participant.get_search_filters()[0])
+           participant.build_search_index(self)
+       self.index.commit()
+       self.index.optimize()
+
+    # IEnvironmentSetupParticipant methods
+
+    def environment_created(self):
+       """Create the initial search index."""
+       self.rebuild_index()
+
+    def environment_needs_upgrade(self, db):
+       """Check if we need to make the index based on if the path is there"""
+       if self.env.path:
+           try:
+               ix = index.open_dir(self.searchpath)
+               #TODO: figure out schema equality testing and re-enable this
+               #return ix.schema != self.SCHEMA
+               return False
+           except:
+               return True
+       else:
+           return True
+
+    def upgrade_environment(self, db):
+       """Create the initial search index."""
+       self.rebuild_index()
+
+    # Public APIs
+    
+    def optimize(self):
+        """Optimize the index to improve search performance."""
+        if 'index' not in self.__dict__:
+            try:
+                self.index = index.open_dir(self.searchpath)
+            except(IOError, index.EmptyIndexError):
+                 raise TracError(_("""Search index missing. \
+Rebuild by running trac-admin %s search rebuild.""" % self.env.path))
+        self.index.optimize()
+
+    def index_doc(self, type, contents):
+        """Add a document (of any type) to the search 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.
+        """
+        if 'index' not in self.__dict__:
+            try:
+                self.index = index.open_dir(self.searchpath)
+            except(IOError, index.EmptyIndexError):
+                 raise TracError(_("""Search index missing. \
+Rebuild by running trac-admin %s search rebuild.""" % self.env.path))
+        if 'writer' not in self.__dict__:
+            self.writer = self.index.writer()
+        contents['type'] = type
+        contents['unique_id'] = type+contents['id']
+        self.log.debug('Adding doc to the search index: %s' % contents['unique_id'])
+        self.writer.update_document(**contents)
+        self.writer.commit()
+
+    def delete_doc(self, type, id):
+        """Delete a document from the search index.
+        """
+        if 'index' not in self.__dict__:
+            try:
+                self.index = index.open_dir(self.searchpath)
+            except(IOError, index.EmptyIndexError):
+                 raise TracError(_("""Search index missing. \
+Rebuild by running trac-admin %s search rebuild.""" % self.env.path))
+        self.log.debug('Removing doc from the search index: %s' % type+unicode(id))
+        self.index.delete_by_term('unique_id', type+unicode(id))
+        self.index.commit()
+    
+    
+
 def shorten_result(text='', keywords=[], maxlen=240, fuzz=60):
     if not text:
         text = ''
Index: trac/search/web_ui.py
===================================================================
--- trac/search/web_ui.py	(revision 7941)
+++ trac/search/web_ui.py	(working copy)
@@ -17,6 +17,7 @@
 import pkg_resources
 import re
 import time
+import os
 
 from genshi.builder import tag, Element
 
@@ -24,29 +25,32 @@
 from trac.core import *
 from trac.mimeview import Context
 from trac.perm import IPermissionRequestor
-from trac.search.api import ISearchSource
+from trac.resource import Resource
+from trac.search.api import *
 from trac.util.datefmt import format_datetime
 from trac.util.presentation import Paginator
 from trac.util.translation import _
 from trac.web import IRequestHandler
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor, \
                             ITemplateProvider
+from trac.wiki import WikiSystem, WikiPage
 from trac.wiki.api import IWikiSyntaxProvider
 from trac.wiki.formatter import extract_link
 
+from whoosh.fields import *
+from whoosh import index
+from whoosh.qparser import MultifieldParser
 
+
 class SearchModule(Component):
 
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
-               ITemplateProvider, IWikiSyntaxProvider)
-
-    search_sources = ExtensionPoint(ISearchSource)
+        ITemplateProvider, IWikiSyntaxProvider)
     
+    search_participants = ExtensionPoint(ISearchParticipant)
+    
     RESULTS_PER_PAGE = 10
 
-    min_query_length = IntOption('search', 'min_query_length', 3,
-        """Minimum length of query string allowed when performing a search.""")
-
     default_disabled_filters = ListOption('search', 'default_disabled_filters',
         doc="""Specifies which search filters should be disabled by default
                on the search page. This will also restrict the filters for the
@@ -80,9 +84,8 @@
             return ('opensearch.xml', {},
                     'application/opensearchdescription+xml')
 
-        available_filters = []
-        for source in self.search_sources:
-            available_filters += source.get_search_filters(req)
+        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
@@ -100,25 +103,32 @@
             data['quickjump'] = self._check_quickjump(req, query)
             if query.startswith('!'):
                 query = query[1:]
-            terms = self._get_search_terms(query)
+            
+            titlers = dict([(x.get_search_filters(req)[0], x.format_search_results)
+                for x in self.search_participants if x.get_search_filters(req)])
+                
+            ix = index.open_dir(os.path.join(self.env.path, 'search_index'))
+            searcher = ix.searcher()
+            parser = MultifieldParser(["id", 'summary', "content", 'changes',
+                'keywords', 'component', 'milestone',], schema = ix.schema)
+            whoosh_results = searcher.search(parser.parse(query))
+            whoosh_results = [r for r in whoosh_results if r['type'] in filters] 
 
-            # Refuse queries that obviously would result in a huge result set
-            if len(terms) == 1 and len(terms[0]) < self.min_query_length:
-                raise TracError(_('Search query too short. Query must be at '
-                                  'least %(num)s characters long.',
-                                  num=self.min_query_length), _('Search Error'))
-
-            results = []
-            for source in self.search_sources:
-                results += list(source.get_search_results(req, terms, filters))
-            results.sort(lambda x,y: cmp(y[2], x[2]))
-
             page = int(req.args.get('page', '1'))
-            results = Paginator(results, page - 1, self.RESULTS_PER_PAGE)
+            results = Paginator(whoosh_results, page - 1, self.RESULTS_PER_PAGE)
             for idx, result in enumerate(results):
-                results[idx] = {'href': result[0], 'title': result[1],
-                                'date': format_datetime(result[2]),
-                                'author': result[3], 'excerpt': result[4]}
+                resdata = {'href': req.href(result['type'], result['id']),
+                                'id' : result['id'],}
+                for k in ['summary', 'author', 'content', 'status',]:
+                    if result.has_key(k):
+                        resdata[k] = result[k]
+                if result.has_key('content'):
+                    resdata['excerpt'] = shorten_result(result['content'])
+                if result.has_key('time'):
+                    resdata['date'] = format_datetime(int(result['time']))
+                        
+                resdata['title'] = titlers[result['type']](result)
+                results[idx] = resdata
             
             pagedata = []    
             data['results'] = results
@@ -156,6 +166,7 @@
         add_stylesheet(req, 'common/css/search.css')
         return 'search.html', data, None
 
+
     # ITemplateProvider methods
 
     def get_htdocs_dirs(self):

