Edgewall Software

AdvancedSearch: trac_whoosh_integration_20090321c.diff

File trac_whoosh_integration_20090321c.diff, 30.5 KB (added by Chris Mulligan <chris.mulligan@…>, 3 years ago)

Whoosh Integration patch 20090321c

  • setup.py

     
    7070 
    7171    install_requires = [ 
    7272        'setuptools>=0.6b1', 
    73         'Genshi>=0.6dev-r960' 
     73        'Genshi>=0.6dev-r960', 
     74        'whoosh>=0.1.11', 
    7475    ], 
    7576    extras_require = { 
    7677        'Babel': ['Babel>=0.9.4'], 
     
    102103        trac.mimeview.txtl = trac.mimeview.txtl[Textile] 
    103104        trac.prefs = trac.prefs.web_ui 
    104105        trac.search = trac.search.web_ui 
     106        trac.search.admin = trac.search.admin 
    105107        trac.ticket.admin = trac.ticket.admin 
    106108        trac.ticket.query = trac.ticket.query 
    107109        trac.ticket.report = trac.ticket.report 
  • trac/attachment.py

     
    3535from trac.mimeview import * 
    3636from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy 
    3737from trac.resource import * 
    38 from trac.search import search_to_sql, shorten_result 
    3938from trac.util import get_reporter_id, create_unique_file, content_disposition 
    4039from trac.util.datefmt import format_datetime, to_timestamp, utc 
    4140from trac.util.text import exception_to_unicode, pretty_size, print_table, \ 
     
    481480                       _(" attached to "), tag.em(name, title=title)) 
    482481        elif field == 'description': 
    483482            return format_to(self.env, None, context(attachment.parent), descr) 
    484     
    485     def get_search_results(self, req, resource_realm, terms): 
    486         """Return a search result generator suitable for ISearchSource. 
    487          
    488         Search results are attachments on resources of the given  
    489         `resource_realm.realm` whose filename, description or author match  
    490         the given terms. 
    491         """ 
    492         db = self.env.get_db_cnx() 
    493         sql_query, args = search_to_sql(db, ['filename', 'description',  
    494                                         'author'], terms) 
    495         cursor = db.cursor() 
    496         cursor.execute("SELECT id,time,filename,description,author " 
    497                        "FROM attachment " 
    498                        "WHERE type = %s " 
    499                        "AND " + sql_query, (resource_realm.realm, ) + args) 
    500          
    501         for id, time, filename, desc, author in cursor: 
    502             attachment = resource_realm(id=id).child('attachment', filename) 
    503             if 'ATTACHMENT_VIEW' in req.perm(attachment): 
    504                 yield (get_resource_url(self.env, attachment, req.href), 
    505                        get_resource_shortname(self.env, attachment), 
    506                        datetime.fromtimestamp(time, utc), author, 
    507                        shorten_result(desc, terms)) 
    508483     
    509484    # IResourceManager methods 
    510485     
  • trac/ticket/api.py

     
    166166        self._fields_lock = threading.RLock() 
    167167 
    168168    # Public API 
     169     
     170    def get_tickets(self): 
     171        """Returns a list of all tickets.""" 
     172        db = self.env.get_db_cnx() 
     173        cursor = db.cursor() 
     174        cursor.execute("SELECT DISTINCT id FROM ticket") 
     175        for (ticket,) in cursor: 
     176            yield ticket 
     177             
    169178 
    170179    def get_available_actions(self, req, ticket): 
    171180        """Returns a sorted list of available actions""" 
  • trac/ticket/web_ui.py

     
    3131from trac.mimeview.api import Mimeview, IContentConverter, Context 
    3232from trac.resource import Resource, get_resource_url, \ 
    3333                         render_resource_link, get_resource_shortname 
    34 from trac.search import ISearchSource, search_to_sql, shorten_result 
    3534from trac.ticket.api import TicketSystem, ITicketManipulator, \ 
    3635                            ITicketActionController 
    3736from trac.ticket.model import Milestone, Ticket, group_milestones 
     
    5958class TicketModule(Component): 
    6059 
    6160    implements(IContentConverter, INavigationContributor, IRequestHandler, 
    62                ISearchSource, ITemplateProvider, ITimelineEventProvider) 
     61               ITemplateProvider, ITimelineEventProvider) 
    6362 
    6463    ticket_manipulators = ExtensionPoint(ITicketManipulator) 
    6564 
     
    198197    def get_templates_dirs(self): 
    199198        return [pkg_resources.resource_filename('trac.ticket', 'templates')] 
    200199 
    201     # ISearchSource methods 
    202200 
    203     def get_search_filters(self, req): 
    204         if 'TICKET_VIEW' in req.perm: 
    205             yield ('ticket', 'Tickets') 
    206  
    207     def get_search_results(self, req, terms, filters): 
    208         if not 'ticket' in filters: 
    209             return 
    210         ticket_realm = Resource('ticket') 
    211         db = self.env.get_db_cnx() 
    212         sql, args = search_to_sql(db, ['b.newvalue'], terms) 
    213         sql2, args2 = search_to_sql(db, ['summary', 'keywords', 'description', 
    214                                          'reporter', 'cc',  
    215                                          db.cast('id', 'text')], terms) 
    216         sql3, args3 = search_to_sql(db, ['c.value'], terms) 
    217         cursor = db.cursor() 
    218         cursor.execute("SELECT DISTINCT a.summary,a.description,a.reporter, " 
    219                        "a.type,a.id,a.time,a.status,a.resolution " 
    220                        "FROM ticket a " 
    221                        "LEFT JOIN ticket_change b ON a.id = b.ticket " 
    222                        "LEFT OUTER JOIN ticket_custom c ON (a.id = c.ticket) " 
    223                        "WHERE (b.field='comment' AND %s) OR %s OR %s" %  
    224                        (sql, sql2, sql3), args + args2 + args3) 
    225         ticketsystem = TicketSystem(self.env) 
    226         for summary, desc, author, type, tid, ts, status, resolution in cursor: 
    227             t = ticket_realm(id=tid) 
    228             if 'TICKET_VIEW' in req.perm(t): 
    229                 yield (req.href.ticket(tid), 
    230                        tag(tag.span(get_resource_shortname(self.env, t), 
    231                                     class_=status), 
    232                            ': ', 
    233                            ticketsystem.format_summary(summary, status, 
    234                                                        resolution, type)), 
    235                        datetime.fromtimestamp(ts, utc), author, 
    236                        shorten_result(desc, terms)) 
    237          
    238         # Attachments 
    239         for result in AttachmentModule(self.env).get_search_results( 
    240             req, ticket_realm, terms): 
    241             yield result         
    242  
    243201    # ITimelineEventProvider methods 
    244202 
    245203    def get_timeline_filters(self, req): 
  • trac/ticket/__init__.py

     
    11from trac.ticket.api import * 
    22from trac.ticket.default_workflow import * 
    33from trac.ticket.model import * 
     4from trac.ticket.search import * 
     5 No newline at end of file 
  • trac/ticket/roadmap.py

     
    2323 
    2424from trac import __version__ 
    2525from trac.attachment import AttachmentModule 
    26 from trac.config import ExtensionOption 
     26from trac.config import ExtensionOption, ExtensionPoint 
    2727from trac.core import * 
    2828from trac.mimeview import Context 
    2929from trac.perm import IPermissionRequestor 
    3030from trac.resource import * 
    31 from trac.search import ISearchSource, search_to_sql, shorten_result 
     31from trac.search import ISearchParticipant, SearchSystem 
    3232from trac.util.datefmt import parse_date, utc, to_timestamp, to_datetime, \ 
    3333                              get_date_format_hint, get_datetime_format_hint, \ 
    3434                              format_date, format_datetime 
     
    5050        This method returns a valid TicketGroupStats object. 
    5151        """ 
    5252 
     53class IMilestoneChangeListener(Interface): 
     54    """Extension point interface for components that require notification 
     55    when tickets are created, modified, or deleted.""" 
     56 
     57    def milestone_created(milestone): 
     58        """Called when a ticket is created.""" 
     59 
     60    def milestone_changed(milestone, comment, author, old_values): 
     61        """Called when a milestone is modified. 
     62 
     63        `old_values` is a dictionary containing the previous values of the 
     64        fields that have changed. 
     65        """ 
     66 
     67    def milestone_deleted(milestone): 
     68        """Called when a milestone is deleted.""" 
     69 
     70 
    5371class TicketGroupStats(object): 
    5472    """Encapsulates statistics on a group of tickets.""" 
    5573 
     
    286304                               for interval in stat.intervals]} 
    287305 
    288306 
    289  
    290307class RoadmapModule(Component): 
    291308 
    292     implements(INavigationContributor, IPermissionRequestor, IRequestHandler) 
     309    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
     310        ISearchParticipant) 
    293311    stats_provider = ExtensionOption('roadmap', 'stats_provider', 
    294312                                     ITicketGroupStatsProvider, 
    295313                                     'DefaultTicketGroupStatsProvider', 
     
    468486        write_prop('END', 'VCALENDAR') 
    469487 
    470488 
     489    # ISearchParticipant methods 
     490    def get_search_filters(self, req=None): 
     491        if not req or 'MILESTONE_VIEW' in req.perm: 
     492            return ('milestone', 'Milestones') 
    471493 
     494    def build_search_index(self, s): 
     495        db = self.env.get_db_cnx() 
     496        milestones = Milestone.select(self.env, True, db) 
     497        for milestone in milestones: 
     498            _index_milestone(self.env, milestone) 
     499 
     500    def format_search_results(self, res): 
     501        return u'Milestone: %s' % res['id'] 
     502 
     503 
     504 
     505 
    472506class MilestoneModule(Component): 
    473507 
    474508    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
    475509               ITimelineEventProvider, IWikiSyntaxProvider, IResourceManager, 
    476                ISearchSource) 
     510               IMilestoneChangeListener) 
    477511  
     512    change_listeners = ExtensionPoint(IMilestoneChangeListener) 
    478513    stats_provider = ExtensionOption('milestone', 'stats_provider', 
    479514                                     ITicketGroupStatsProvider, 
    480515                                     'DefaultTicketGroupStatsProvider', 
     
    590625            retarget_to = req.args.get('target') or None 
    591626        milestone.delete(retarget_to, req.authname) 
    592627        db.commit() 
     628        for listener in self.change_listeners: 
     629            listener.milestone_deleted(milestone) 
    593630        req.redirect(req.href.roadmap()) 
    594631 
    595632    def _do_save(self, req, db, milestone): 
     
    660697            milestone.insert() 
    661698        db.commit() 
    662699 
     700        for listener in self.change_listeners: 
     701            listener.milestone_changed(milestone) 
    663702        req.redirect(req.href.milestone(milestone.name)) 
    664703 
    665704    def _render_confirm(self, req, db, milestone): 
     
    820859        else: 
    821860            return desc 
    822861 
    823     # ISearchSource methods 
     862    # IMilestoneChangeListener methods 
    824863 
    825     def get_search_filters(self, req): 
    826         if 'MILESTONE_VIEW' in req.perm: 
    827             yield ('milestone', _('Milestones')) 
     864    def milestone_changed(self, milestone): 
     865        _index_milestone(self.env, milestone) 
    828866 
    829     def get_search_results(self, req, terms, filters): 
    830         if not 'milestone' in filters: 
    831             return 
    832         db = self.env.get_db_cnx() 
    833         sql_query, args = search_to_sql(db, ['name', 'description'], terms) 
    834         cursor = db.cursor() 
    835         cursor.execute("SELECT name,due,completed,description " 
    836                        "FROM milestone " 
    837                        "WHERE " + sql_query, args) 
    838  
    839         milestone_realm = Resource('milestone') 
    840         for name, due, completed, description in cursor: 
    841             milestone = milestone_realm(id=name) 
    842             if 'MILESTONE_VIEW' in req.perm(milestone): 
    843                 yield (get_resource_url(self.env, milestone, req.href), 
    844                        get_resource_name(self.env, milestone), 
    845                        datetime.fromtimestamp( 
    846                            completed or due or time(), utc), 
    847                        '', shorten_result(description, terms)) 
     867    def milestone_deleted(self, milestone): 
     868        s = SearchSystem(self.env) 
     869        s.delete_doc(u'milestone', milestone.name) 
    848870         
    849         # Attachments 
    850         for result in AttachmentModule(self.env).get_search_results( 
    851             req, milestone_realm, terms): 
    852             yield result 
     871def _index_milestone(env, milestone, search_system=None): 
     872    if not search_system: 
     873        search_system = SearchSystem(env) 
     874    if milestone.is_completed: 
     875        status = u'completed' 
     876    else: 
     877        status = u'open' 
     878    contents = {'id' : milestone.name, 
     879        'content' : milestone.description, 
     880        'status' : status, 
     881        } 
     882    search_system.index_doc(u'milestone', contents) 
     883 No newline at end of file 
  • trac/versioncontrol/web_ui/changeset.py

     
    3333from trac.mimeview import Mimeview, is_binary, Context 
    3434from trac.perm import IPermissionRequestor 
    3535from trac.resource import Resource, ResourceNotFound 
    36 from trac.search import ISearchSource, search_to_sql, shorten_result 
    3736from trac.timeline.api import ITimelineEventProvider 
    3837from trac.util import embedded_numbers, content_disposition 
    3938from trac.util.compat import any 
     
    122121    """ 
    123122 
    124123    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
    125                ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource) 
     124               ITimelineEventProvider, IWikiSyntaxProvider) 
    126125 
    127126    property_diff_renderers = ExtensionPoint(IPropertyDiffRenderer) 
    128127     
     
    955954 
    956955    # ISearchSource methods 
    957956 
    958     def get_search_filters(self, req): 
    959         if 'CHANGESET_VIEW' in req.perm: 
    960             yield ('changeset', _('Changesets')) 
    961957 
    962     def get_search_results(self, req, terms, filters): 
    963         if not 'changeset' in filters: 
    964             return 
    965         repos = self.env.get_repository(req.authname) 
    966         db = self.env.get_db_cnx() 
    967         sql, args = search_to_sql(db, ['rev', 'message', 'author'], terms) 
    968         cursor = db.cursor() 
    969         cursor.execute("SELECT rev,time,author,message " 
    970                        "FROM revision WHERE " + sql, args) 
    971         for rev, ts, author, log in cursor: 
    972             if not repos.authz.has_permission_for_changeset(rev): 
    973                 continue 
    974             yield (req.href.changeset(rev), 
    975                    '[%s]: %s' % (rev, shorten_line(log)), 
    976                    datetime.fromtimestamp(ts, utc), author, 
    977                    shorten_result(log, terms)) 
    978  
    979  
    980958class AnyDiffModule(Component): 
    981959 
    982960    implements(IRequestHandler) 
  • trac/wiki/web_ui.py

     
    2929from trac.mimeview.api import Mimeview, IContentConverter, Context 
    3030from trac.perm import IPermissionRequestor 
    3131from trac.resource import * 
    32 from trac.search import ISearchSource, search_to_sql, shorten_result 
    3332from trac.timeline.api import ITimelineEventProvider 
    3433from trac.util import get_reporter_id 
    3534from trac.util.datefmt import to_timestamp, utc 
     
    5453class WikiModule(Component): 
    5554 
    5655    implements(IContentConverter, INavigationContributor, IPermissionRequestor, 
    57                IRequestHandler, ITimelineEventProvider, ISearchSource, 
     56               IRequestHandler, ITimelineEventProvider, 
    5857               ITemplateProvider) 
    5958 
    6059    page_manipulators = ExtensionPoint(IWikiPageManipulator) 
     
    608607                    wiki_page.id, version=wiki_page.version, action='diff') 
    609608                markup = tag(markup, ' ', tag.a('(diff)', href=diff_href)) 
    610609            return markup 
    611  
    612     # ISearchSource methods 
    613  
    614     def get_search_filters(self, req): 
    615         if 'WIKI_VIEW' in req.perm: 
    616             yield ('wiki', _('Wiki')) 
    617  
    618     def get_search_results(self, req, terms, filters): 
    619         if not 'wiki' in filters: 
    620             return 
    621         db = self.env.get_db_cnx() 
    622         sql_query, args = search_to_sql(db, ['w1.name', 'w1.author', 'w1.text'], 
    623                                         terms) 
    624         cursor = db.cursor() 
    625         cursor.execute("SELECT w1.name,w1.time,w1.author,w1.text " 
    626                        "FROM wiki w1," 
    627                        "(SELECT name,max(version) AS ver " 
    628                        "FROM wiki GROUP BY name) w2 " 
    629                        "WHERE w1.version = w2.ver AND w1.name = w2.name " 
    630                        "AND " + sql_query, args) 
    631  
    632         wiki_realm = Resource('wiki') 
    633         for name, ts, author, text in cursor: 
    634             page = wiki_realm(id=name) 
    635             if 'WIKI_VIEW' in req.perm(page): 
    636                 yield (get_resource_url(self.env, page, req.href), 
    637                        '%s: %s' % (name, shorten_line(text)), 
    638                        datetime.fromtimestamp(ts, utc), author, 
    639                        shorten_result(text, terms)) 
    640          
    641         # Attachments 
    642         for result in AttachmentModule(self.env).get_search_results( 
    643             req, wiki_realm, terms): 
    644             yield result 
  • trac/wiki/__init__.py

     
    33from trac.wiki.intertrac import * 
    44from trac.wiki.model import * 
    55from trac.wiki.parser import * 
     6from trac.wiki.search import * 
     7 No newline at end of file 
  • trac/search/admin.py

     
     1# -*- coding: utf-8 -*- 
     2# 
     3# Copyright (C) 2008 Edgewall Software 
     4# All rights reserved. 
     5# 
     6# This software is licensed as described in the file COPYING, which 
     7# you should have received as part of this distribution. The terms 
     8# are also available at http://trac.edgewall.com/license.html. 
     9# 
     10# This software consists of voluntary contributions made by many 
     11# individuals. For the exact contribution history, see the revision 
     12# history and logs, available at http://trac.edgewall.org/. 
     13 
     14from datetime import datetime 
     15import os.path 
     16import pkg_resources 
     17import sys 
     18import time 
     19 
     20from trac.admin import * 
     21from trac.core import * 
     22from trac.search.api import * 
     23 
     24 
     25class SearchAdmin(Component): 
     26    """Search administration component. Only rebuilds the index.""" 
     27 
     28    implements(IAdminCommandProvider) 
     29 
     30    # IAdminCommandProvider methods 
     31     
     32    def get_admin_commands(self): 
     33        yield ('search rebuild', '', 
     34            'Rebuild the search index', 
     35            None, SearchSystem(self.env).rebuild_index) 
     36        yield ('search optimize', '', 
     37            'Optimize the search index', 
     38            None, SearchSystem(self.env).optimize) 
     39 No newline at end of file 
  • trac/search/api.py

     
    1212# history and logs, available at http://trac.edgewall.org/log/. 
    1313 
    1414from trac.core import * 
     15from trac.env import IEnvironmentSetupParticipant 
    1516 
     17from whoosh.fields import * 
     18from whoosh import index 
     19import os 
    1620 
    17 class ISearchSource(Interface): 
    18     """Extension point interface for adding search sources to the search 
    19     system. 
     21class ISearchParticipant(Interface): 
     22    """Extension point interface for components that should be searched. 
    2023    """ 
    2124 
    2225    def get_search_filters(req): 
    23         """Return a list of filters that this search source supports. 
    24          
    25         Each filter must be a `(name, label[, default])` tuple, where `name` is 
    26         the internal name, `label` is a human-readable name for display and 
    27         `default` is an optional boolean for determining whether this filter 
    28         is searchable by default. 
    29         """ 
     26        """Called when we want to build the list of components with search. 
     27        Passes the request object to do permission checking.""" 
    3028 
    31     def get_search_results(req, terms, filters): 
    32         """Return a list of search results matching each search term in `terms`. 
     29    def build_search_index(search_system): 
     30        """Called when we want to rebuild the entire index.""" 
    3331         
    34         The `filters` parameters is a list of the enabled filters, each item 
    35         being the name of the tuples returned by `get_search_events`. 
     32    def format_search_results(contents): 
     33        """Called to see if the module wants to format the search results.""" 
    3634 
    37         The events returned by this function must be tuples of the form 
    38         `(href, title, date, author, excerpt).` 
    39         """ 
    4035 
     36class SearchSystem(Component): 
     37    """Implements the search system, provides methods for adding and  
     38    Deleting documents for indexing. 
     39    """ 
    4140 
    42 def search_to_sql(db, columns, terms): 
    43     """Convert a search query into an SQL WHERE clause and corresponding 
    44     parameters. 
     41    implements(IEnvironmentSetupParticipant)     
     42    search_participants = ExtensionPoint(ISearchParticipant) 
    4543     
    46     The result is returned as an `(sql, params)` tuple. 
    47     """ 
    48     assert columns and terms 
     44    def __init__(self): 
     45        if not self.env.path: 
     46            raise TracError(_("Environment.path is very much required...")) 
     47        self.searchpath = os.path.join(self.env.path, 'search_index') 
    4948 
    50     likes = ['%s %s' % (i, db.like()) for i in columns] 
    51     c = ' OR '.join(likes) 
    52     sql = '(' + ') AND ('.join([c] * len(terms)) + ')' 
    53     args = [] 
    54     for t in terms: 
    55         args.extend(['%' + db.like_escape(t) + '%'] * len(columns)) 
    56     return sql, tuple(args) 
     49    SCHEMA = Schema( 
     50        unique_id=ID(stored=True, unique=True), 
     51        id=ID(stored=True), 
     52        type=ID(stored=True), 
     53        time=ID(stored=True), 
     54        author=ID(stored=True), 
     55        component=KEYWORD,  
     56        status=KEYWORD(stored=True),  
     57        resolution=KEYWORD(stored=True), 
     58        keywords=KEYWORD(scorable=True), 
     59        milestone=TEXT, 
     60        summary=TEXT(stored=True), 
     61        content=TEXT(stored=True), 
     62        changes=TEXT, 
     63        ) 
    5764 
     65    def rebuild_index(self): 
     66       """Delete the index if it exists. Then create a new full index.""" 
     67       self.log.info('Rebuilding the search index.') 
     68        
     69       if not os.path.exists(self.searchpath): 
     70           os.mkdir(self.searchpath) 
     71       self.index = index.create_in(self.searchpath, schema=self.SCHEMA) 
     72       for participant in self.search_participants: 
     73           self.log.debug('Now building search index for: %s.' 
     74                % participant.get_search_filters()[0]) 
     75           participant.build_search_index(self) 
     76       self.index.commit() 
     77       self.index.optimize() 
     78 
     79    # IEnvironmentSetupParticipant methods 
     80 
     81    def environment_created(self): 
     82       """Create the initial search index.""" 
     83       self.rebuild_index() 
     84 
     85    def environment_needs_upgrade(self, db): 
     86       """Check if we need to make the index based on if the path is there""" 
     87       if self.env.path: 
     88           try: 
     89               ix = index.open_dir(self.searchpath) 
     90               #TODO: figure out schema equality testing and re-enable this 
     91               #return ix.schema != self.SCHEMA 
     92               return False 
     93           except: 
     94               return True 
     95       else: 
     96           return True 
     97 
     98    def upgrade_environment(self, db): 
     99       """Create the initial search index.""" 
     100       self.rebuild_index() 
     101 
     102    # Public APIs 
     103     
     104    def optimize(self): 
     105        """Optimize the index to improve search performance.""" 
     106        if 'index' not in self.__dict__: 
     107            try: 
     108                self.index = index.open_dir(self.searchpath) 
     109            except(IOError, index.EmptyIndexError): 
     110                 raise TracError(_("""Search index missing. \ 
     111Rebuild by running trac-admin %s search rebuild.""" % self.env.path)) 
     112        self.index.optimize() 
     113 
     114    def index_doc(self, type, contents): 
     115        """Add a document (of any type) to the search index. 
     116 
     117        The contents should be a dict with fields matching the search schema. 
     118        The only required fields are type and id, everything else is optional. 
     119        """ 
     120        if 'index' not in self.__dict__: 
     121            try: 
     122                self.index = index.open_dir(self.searchpath) 
     123            except(IOError, index.EmptyIndexError): 
     124                 raise TracError(_("""Search index missing. \ 
     125Rebuild by running trac-admin %s search rebuild.""" % self.env.path)) 
     126        if 'writer' not in self.__dict__: 
     127            self.writer = self.index.writer() 
     128        contents['type'] = type 
     129        contents['unique_id'] = type+contents['id'] 
     130        self.log.debug('Adding doc to the search index: %s' % contents['unique_id']) 
     131        self.writer.update_document(**contents) 
     132        self.writer.commit() 
     133 
     134    def delete_doc(self, type, id): 
     135        """Delete a document from the search index. 
     136        """ 
     137        if 'index' not in self.__dict__: 
     138            try: 
     139                self.index = index.open_dir(self.searchpath) 
     140            except(IOError, index.EmptyIndexError): 
     141                 raise TracError(_("""Search index missing. \ 
     142Rebuild by running trac-admin %s search rebuild.""" % self.env.path)) 
     143        self.log.debug('Removing doc from the search index: %s' % type+unicode(id)) 
     144        self.index.delete_by_term('unique_id', type+unicode(id)) 
     145        self.index.commit() 
     146     
     147     
     148 
    58149def shorten_result(text='', keywords=[], maxlen=240, fuzz=60): 
    59150    if not text: 
    60151        text = '' 
  • trac/search/web_ui.py

     
    1717import pkg_resources 
    1818import re 
    1919import time 
     20import os 
    2021 
    2122from genshi.builder import tag, Element 
    2223 
     
    2425from trac.core import * 
    2526from trac.mimeview import Context 
    2627from trac.perm import IPermissionRequestor 
    27 from trac.search.api import ISearchSource 
     28from trac.resource import Resource 
     29from trac.search.api import * 
    2830from trac.util.datefmt import format_datetime 
    2931from trac.util.presentation import Paginator 
    3032from trac.util.translation import _ 
    3133from trac.web import IRequestHandler 
    3234from trac.web.chrome import add_link, add_stylesheet, INavigationContributor, \ 
    3335                            ITemplateProvider 
     36from trac.wiki import WikiSystem, WikiPage 
    3437from trac.wiki.api import IWikiSyntaxProvider 
    3538from trac.wiki.formatter import extract_link 
    3639 
     40from whoosh.fields import * 
     41from whoosh import index 
     42from whoosh.qparser import MultifieldParser 
    3743 
     44 
    3845class SearchModule(Component): 
    3946 
    4047    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
    41                ITemplateProvider, IWikiSyntaxProvider) 
    42  
    43     search_sources = ExtensionPoint(ISearchSource) 
     48        ITemplateProvider, IWikiSyntaxProvider) 
    4449     
     50    search_participants = ExtensionPoint(ISearchParticipant) 
     51     
    4552    RESULTS_PER_PAGE = 10 
    4653 
    47     min_query_length = IntOption('search', 'min_query_length', 3, 
    48         """Minimum length of query string allowed when performing a search.""") 
    49  
    5054    default_disabled_filters = ListOption('search', 'default_disabled_filters', 
    5155        doc="""Specifies which search filters should be disabled by default 
    5256               on the search page. This will also restrict the filters for the 
     
    8084            return ('opensearch.xml', {}, 
    8185                    'application/opensearchdescription+xml') 
    8286 
    83         available_filters = [] 
    84         for source in self.search_sources: 
    85             available_filters += source.get_search_filters(req) 
     87        available_filters = filter(None, [p.get_search_filters(req) for p 
     88            in self.search_participants]) 
    8689        filters = [f[0] for f in available_filters if req.args.has_key(f[0])] 
    8790        if not filters: 
    8891            filters = [f[0] for f in available_filters 
     
    100103            data['quickjump'] = self._check_quickjump(req, query) 
    101104            if query.startswith('!'): 
    102105                query = query[1:] 
    103             terms = self._get_search_terms(query) 
     106             
     107            titlers = dict([(x.get_search_filters(req)[0], x.format_search_results) 
     108                for x in self.search_participants if x.get_search_filters(req)]) 
     109                 
     110            ix = index.open_dir(os.path.join(self.env.path, 'search_index')) 
     111            searcher = ix.searcher() 
     112            parser = MultifieldParser(["id", 'summary', "content", 'changes', 
     113                'keywords', 'component', 'milestone',], schema = ix.schema) 
     114            whoosh_results = searcher.search(parser.parse(query)) 
     115            whoosh_results = [r for r in whoosh_results if r['type'] in filters]  
    104116 
    105             # Refuse queries that obviously would result in a huge result set 
    106             if len(terms) == 1 and len(terms[0]) < self.min_query_length: 
    107                 raise TracError(_('Search query too short. Query must be at ' 
    108                                   'least %(num)s characters long.', 
    109                                   num=self.min_query_length), _('Search Error')) 
    110  
    111             results = [] 
    112             for source in self.search_sources: 
    113                 results += list(source.get_search_results(req, terms, filters)) 
    114             results.sort(lambda x,y: cmp(y[2], x[2])) 
    115  
    116117            page = int(req.args.get('page', '1')) 
    117             results = Paginator(results, page - 1, self.RESULTS_PER_PAGE) 
     118            results = Paginator(whoosh_results, page - 1, self.RESULTS_PER_PAGE) 
    118119            for idx, result in enumerate(results): 
    119                 results[idx] = {'href': result[0], 'title': result[1], 
    120                                 'date': format_datetime(result[2]), 
    121                                 'author': result[3], 'excerpt': result[4]} 
     120                resdata = {'href': req.href(result['type'], result['id']), 
     121                                'id' : result['id'],} 
     122                for k in ['summary', 'author', 'content', 'status',]: 
     123                    if result.has_key(k): 
     124                        resdata[k] = result[k] 
     125                if result.has_key('content'): 
     126                    resdata['excerpt'] = shorten_result(result['content']) 
     127                if result.has_key('time'): 
     128                    resdata['date'] = format_datetime(int(result['time'])) 
     129                         
     130                resdata['title'] = titlers[result['type']](result) 
     131                results[idx] = resdata 
    122132             
    123133            pagedata = []     
    124134            data['results'] = results 
     
    156166        add_stylesheet(req, 'common/css/search.css') 
    157167        return 'search.html', data, None 
    158168 
     169 
    159170    # ITemplateProvider methods 
    160171 
    161172    def get_htdocs_dirs(self):