Changeset 379

Show
Ignore:
Timestamp:
02/08/07 00:20:45 (2 years ago)
Author:
athomas
Message:

pyndexter:

  • Builtin indexer can now optionally use different dbm implementations.
  • Added a Reducer class that Indexers can optionally use to stem and uniquify documents and Query objects.
  • Added Query.reduce(reducer) which will reduce each term in the Query tree..
  • Xapian and builtin indexers are using the Reducer.
  • Stop-gap FileSource URI quoting added.
  • Started to add port support to URI.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • pyndexter/trunk/pyndexter/indexers/builtin.py

    r378 r379  
    2323 
    2424class KeyedSet(object): 
    25     def __init__(self, file, mode='c'): 
    26         self.db = anydbm.open(file, mode) 
     25    def __init__(self, db): 
     26        self.db = db 
    2727 
    2828    def update(self, key, values): 
    2929        key = key.encode('utf-8') 
    30         if key in self.db
     30        try
    3131            v = pickle.loads(self.db[key]) 
    32         else
     32        except KeyError
    3333            v = set() 
    3434        v.update(values) 
     
    3737    def remove(self, key, values=None): 
    3838        key = key.encode('utf-8') 
    39         if key in self.db
    40             if values is None
     39        if values is None
     40            try
    4141                del self.db[key] 
    42             else: 
     42            except KeyError: 
     43                pass 
     44        else: 
     45            try: 
    4346                v = pickle.loads(self.db[key]) 
    4447                v.remove(values) 
    4548                self.db[key] = pickle.dumps(v, 2) 
     49            except KeyError: 
     50                pass 
    4651 
    4752    def replace(self, key, values): 
     
    5156    def get(self, key): 
    5257        key = key.encode('utf-8') 
    53         if key in self.db
     58        try
    5459            return pickle.loads(self.db[key]) 
    55         else
     60        except KeyError
    5661            return set() 
    5762 
     
    6267class PickleDict(DictMixin): 
    6368    """A dictionary wrapper that automatically pickles keys and values.""" 
    64     def __init__(self, file, mode='c'): 
    65         self.db = anydbm.open(file, mode) 
     69    def __init__(self, db): 
     70        self.db = db 
    6671 
    6772    def __getitem__(self, key): 
     
    8186    """Constructor URI is: 
    8287 
    83         builtin://<path>/?words=<regex>&max_word_length=<int>&backend=<uri
     88        builtin://<path>/?words=<regex>&dbm=<dbm
    8489 
    8590    eg. 
    8691 
    87         builtin:///tmp/builtin.idx?backend=mysql://localhost/database 
     92        builtin:///tmp/builtin.idx?dbm=gdbm 
     93 
     94    Supported dbm's are `anydbm`, `dbhash`, `gdbm` and `dbm` (Python 2.5). 
     95    `anydbm` is the default. 
    8896 
    8997    """ 
    90     def __init__(self, framework, path, words=r'\w+', max_word_length=32): 
     98    def __init__(self, framework, path, dbm='anydbm'): 
    9199        Indexer.__init__(self, framework) 
    92  
    93         self.words_re = re.compile(words, re.UNICODE) 
    94         self.max_word_length = max_word_length 
    95100 
    96101        self.path = path 
    97102        self.state_path = os.path.join(path, 'store.db') 
    98103        self.db_path = os.path.join(path, 'builtin.db') 
     104 
     105        framework.reduce.split = True 
     106        framework.reduce.unique = True 
     107 
     108        dbm = __import__(dbm, {}, {}, ['']) 
    99109 
    100110        if framework.mode == READWRITE: 
     
    106116 
    107117        # word:set(uri) 
    108         self.words = KeyedSet(os.path.join(self.db_path, 'words'), mode
     118        self.words = KeyedSet(dbm.open(os.path.join(self.db_path, 'words'), mode)
    109119        # uri:set(word) 
    110         self.uris = KeyedSet(os.path.join(self.db_path, 'uris'), mode
     120        self.uris = KeyedSet(dbm.open(os.path.join(self.db_path, 'uris'), mode)
    111121        # attribute:dict(attributes) 
    112         self.attributes = PickleDict(os.path.join(self.db_path, 'attributes'), 
    113                                      mode) 
    114  
     122        self.attributes = PickleDict(dbm.open(os.path.join(self.db_path, 
     123                                                           'attributes'), mode)) 
    115124 
    116125    def index(self, document): 
    117126        self.attributes[document.uri] = document.attributes 
    118127 
    119         words = set([self.framework.stemmer(w.lower()) for w in 
    120                     set(self.words_re.findall(document.content)) 
    121                     if len(w) < self.max_word_length]) 
    122  
     128        words = self.framework.reduce(document.content) 
    123129        doc_set = set([document.uri]) 
    124130 
     
    160166    def search(self, query): 
    161167        # FIXME currently simply finding the intersection of all documents (AND) 
    162         words = [self.framework.stemmer(w.lower()) for w in 
    163                  query.as_string(and_=' ', or_=' ', not_=' ').split()] 
    164  
     168        query.reduce(self.framework.reduce) 
     169        words = query.as_string(and_=' ', or_=' ', not_=' ').split() 
    165170        uris = None 
    166171        for word in words: 
     
    173178 
    174179 
    175 indexer_factory = PluginFactory(BuiltinIndexer, max_word_length=int
     180indexer_factory = PluginFactory(BuiltinIndexer
    176181 
    177182 
  • pyndexter/trunk/pyndexter/indexers/xapian.py

    r378 r379  
    2121 
    2222class XapianIndexer(Indexer): 
    23     def __init__(self, framework, path, words=r'\w+', max_word_length=240): 
     23    def __init__(self, framework, path): 
    2424        Indexer.__init__(self, framework) 
    25         self.words = re.compile(words) 
    26         self.max_word_length = max_word_length 
     25 
     26        framework.reduce.split = True 
    2727 
    2828        path = path.encode('utf-8') 
     
    4242        doc = xapian.Document() 
    4343 
    44         # Xapian doesn't support UTF-8 yet. Coming soon. 
     44        # FIXME Xapian doesn't support UTF-8 yet. "Coming soon." 
    4545        content = document.content.encode('utf-8') 
    4646        uri = document.uri.encode('utf-8') 
     
    5050        doc.add_term('Q' + uri) 
    5151 
    52         words = [self.framework.stemmer(w.lower()) 
    53                  for w in set(self.words.findall(content))] 
    54         for word in self.words.finditer(content): 
    55             term = self.framework.stemmer(word.group().lower()) 
    56             if len(term) > self.max_word_length: 
    57                continue 
    58             doc.add_posting(term, word.start()) 
     52        words = self.framework.reduce(content) 
     53        for word in words: 
     54            doc.add_posting(word, 0) 
    5955 
    6056        self.db.replace_document('Q' + uri, doc) 
     
    7975        class StemmerWrapper(xapian.Stem): 
    8076            def stem_word(self, word): 
    81                 return framework.stemmer(word) 
     77                return framework.reduce.stemmer(word) 
    8278 
    8379        query_parser = xapian.QueryParser() 
    84         query_parser.set_stemmer(StemmerWrapper('english')) 
    8580        query = query_parser.parse_query(query.as_string().encode('utf-8').lower()) 
    8681        enquire = xapian.Enquire(self.db) 
  • pyndexter/trunk/pyndexter/__init__.py

    r378 r379  
    4141from StringIO import StringIO 
    4242from urlparse import urlsplit, urlunsplit 
    43 from pyndexter.util import set 
     43from pyndexter.util import set, URI 
    4444 
    4545 
     
    5858READONLY READWRITE 
    5959 
    60 Query Framework Document Source Indexer Result StateStore Hit PluginFactory 
     60Query Framework Document Source Indexer Result StateStore Hit PluginFactory URI 
    6161""".split() 
    6262 
     
    177177    `difference()` assume that `_state` will contain a dictionary of 
    178178    uri:modification-time mappings. 
     179 
     180    All URI's passed to and from Source objects must be `URI` objects. 
    179181 
    180182    (All attributes, including document contents and URI's must be in unicode) 
     
    446448        return _convert(self) 
    447449 
     450    def reduce(self, reduce): 
     451        """Pass each TERM node through `Reducer`.""" 
     452        def _reduce(node): 
     453            if not node: 
     454                return 
     455            if node.type == node.TERM: 
     456                node.value = reduce(node.value) 
     457            _reduce(node.left) 
     458            _reduce(node.right) 
     459        _reduce(self) 
     460 
    448461    # Internal methods 
    449462    def _tokenise(self, phrase): 
     
    469482 
    470483 
     484class Reducer(object): 
     485    """Compact all words in a block of text.""" 
     486 
     487    def __init__(self, words_re=re.compile(r'\w+'), stemmer=lambda w: w, 
     488                 min_word_length=3, max_word_length=64, unique=False, 
     489                 split=False, lower=True): 
     490        """`words_re` is a regular expression object or string. 
     491 
     492        `stemmer` is a callable that stems a single word. 
     493 
     494        If `unique` is true, return a string of **unordered** words with 
     495        duplicates removed. 
     496 
     497        If `split` is true, return words in a collection rather than joining 
     498        them into a single string. 
     499 
     500        If `lower` is true, lowercase text.""" 
     501 
     502        if isinstance(words_re, basestring): 
     503            words_re = re.compile(words_re, re.UNICODE) 
     504        self.words_re = words_re 
     505        self.stemmer = stemmer 
     506        self.min_word_length = min_word_length 
     507        self.max_word_length = max_word_length 
     508        self.unique = unique 
     509        self.split = split 
     510        self.lower = lower 
     511 
     512    def __call__(self, text, unique=None, split=None): 
     513        if unique is None: 
     514            unique = self.unique 
     515 
     516        if unique: 
     517            out = set() 
     518            def append(word): 
     519                out.add(word) 
     520        else: 
     521            out = [] 
     522            def append(word): 
     523                out.append(word) 
     524 
     525        min = self.min_word_length 
     526        max = self.max_word_length 
     527        stemmer = self.stemmer 
     528 
     529        if self.lower: 
     530            text = text.lower() 
     531 
     532        words = self.words_re.findall(text) 
     533        if unique: 
     534            words = set(words) 
     535 
     536        for word in words: 
     537            if min > len(word) > max: 
     538                continue 
     539            append(stemmer(word)) 
     540 
     541        if split is None: 
     542            split = self.split 
     543        if split: 
     544            return out 
     545        return u' '.join(out) 
     546 
     547 
    471548class StateStore(object): 
    472549    """A class providing file-like objects for storage and retrieval of 
     
    541618        return None 
    542619 
     620 
    543621class PluginFactory(object): 
    544622    """Factory for translating URL-style query parameters into a standard 
    545     module constructor call. 
     623    plugin constructor call. 
    546624 
    547625    >>> class C: 
     
    645723 
    646724    If the `Indexer` is not capable of storing state and automatic updates are 
    647     desired, a `StateStore` object should be passed to the `Framework`. 
    648  
    649     `indexer` is a URI used to construct an indexer, or an `Indexer` object. 
    650  
    651     `stemmer` is a callable that stems individual words. Indexers can 
    652     optionally use this, though some may have their own stemming mechanisms, 
    653     typically passed as a URI parameter.""" 
    654  
    655     def __init__(self, indexer, sources=[], mode=READWRITE, state_store=None, 
     725    desired, a `StateStore` object should be passed to the `Framework`.""" 
     726 
     727    def __init__(self, indexer, mode=READWRITE, state_store=None, reduce=None, 
    656728                 stemmer=None): 
     729        """`indexer` is a URI used to construct an indexer, or an `Indexer` 
     730        object. 
     731 
     732        `reduce` is a `Reducer` object.If `reduce` is not specified, a default 
     733        `Reduce` object will be instantiated using `stemmer` (URI or callable) 
     734        as defaults. '''NOTE:''' Use of the reducer is optional - some 
     735        indexers may implement stemming and reduction internally.""" 
    657736        self.mode = mode 
     737 
     738        if reduce is None: 
     739            if stemmer is None: 
     740                stemmer = lambda word: word 
     741            elif isinstance(stemmer, basestring): 
     742                Stemmer = self._load_plugin('stemmer', stemmer) 
     743                stemmer = Stemmer(uri=stemmer) 
     744            self.reduce = Reducer(stemmer=stemmer) 
     745        else: 
     746            self.reduce = reduce 
    658747 
    659748        if isinstance(indexer, basestring): 
     
    663752            self.indexer = indexer 
    664753 
    665         if stemmer is None: 
    666             self.stemmer = lambda word: word 
    667         elif isinstance(stemmer, basestring): 
    668             self.stemmer = self._load_plugin('stemmer', stemmer) 
    669             self.stemmer = self.stemmer(uri=stemmer) 
    670         else: 
    671             self.stemmer = stemmer 
    672  
    673754        if state_store is None: 
    674755            self.state_store = self.indexer.state_store() 
     
    676757            self.state_store = state_store 
    677758 
    678         sources = [self._load_plugin('source', source)(framework=self, uri=source) 
    679                    for source in sources] 
    680  
    681759        from pyndexter.sources.metasource import MetaSource 
    682760        self.source = MetaSource(self) 
    683         for source in sources: 
    684             self.add_source(source) 
    685761 
    686762    def add_source(self, source): 
     
    847923        self._document = document 
    848924        self.attributes = attributes 
     925        if isinstance(uri, basestring): 
     926            from pyndexter.util import URI 
     927            uri = URI(uri) 
    849928        self.attributes['uri'] = uri 
    850929 
  • pyndexter/trunk/pyndexter/sources/file.py

    r376 r379  
    2323from stat import * 
    2424from urlparse import urlsplit, urlunsplit 
     25from urllib import quote, unquote 
    2526from pyndexter import * 
    2627 
     
    5758    def matches(self, uri): 
    5859        scheme, netloc, path, query, fragment = urlsplit(uri, 'file') 
    59         path = os.path.normpath(path
     60        path = os.path.normpath(unquote(path)
    6061        return scheme == 'file' and \ 
    6162               path.startswith(self.path) and \ 
     
    8687 
    8788    def _file2uri(self, file): 
    88         return urlunsplit(('file', '', file, '', '')) 
     89        return urlunsplit(('file', '', quote(file), '', '')) 
    8990 
    9091    def _uri2file(self, uri): 
     
    9394            raise InvalidURI("URI scheme in '%s' not supported by FileSource" 
    9495                             % scheme) 
    95         path = os.path.normpath(path
     96        path = os.path.normpath(unquote(path)
    9697        if not path.startswith(self.path): 
    9798            raise InvalidURI("Requested URI '%s' is not from this FileSource" 
  • pyndexter/trunk/pyndexter/util.py

    r378 r379  
    88 
    99import re 
     10import posixpath 
     11from StringIO import StringIO 
     12from urllib import quote, unquote 
    1013try: 
    1114    set = set 
     15    frozenset = frozenset 
    1216except: 
    1317    from sets import Set as set 
    1418    from sets import ImmutableSet as frozenset 
    1519 
     20 
     21__all__ = """ 
     22set frozenset 
     23quote unquote 
     24URI 
     25""".split() 
    1626 
    1727class URI(object): 
     
    2838    PS. `urlparse` is not useful. """ 
    2939 
    30     _pattern = re.compile(r'(?P<scheme>[^:]+)://(?:(?P<username>[^:@]*)(?::(?P<password>[^@]*))?@)?(?P<host>[^?/#]*)(?P<path>/[^#?]*)?(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?') 
     40    _pattern = re.compile(r'(?:(?P<scheme>[^:]+)://)?(?:(?P<username>[^:@]*)(?::(?P<password>[^@]*))?@)?(?P<host>[^?/#:]*)(?::(P<port>[\d+]+))?(?P<path>/[^#?]*)?(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?') 
    3141 
    32     __slots__ = ('scheme', 'username', 'password', 'host', 'path', 'query', 
    33                  'fragment') 
     42    __slots__ = ('scheme', 'username', 'password', 'host', 'port', '_path', 
     43                 'query', 'fragment') 
    3444 
    35     def __init__(self, uri=None): 
    36         if uri is not None: 
     45    def __init__(self, uri=None, scheme='', username='', password='', host='', 
     46                 port='', path='', query={}, fragment=''): 
     47        self._path = '' 
     48        # Copy attributes of a URI object 
     49        if isinstance(uri, URI): 
     50            from copy import copy 
     51            self.scheme, self.username, self.password, self.host, self.port, \ 
     52                self.path, self.query, self.fragment = \ 
     53                    uri.scheme, uri.username, uri.password, uri.host, \ 
     54                    uri.port, uri.path, copy(uri.query), uri.fragment 
     55        elif uri is not None: 
     56            # Parse URI string 
    3757            from cgi import parse_qs 
    3858 
     
    4060            if match is None: 
    4161                raise ValueError('Invalid URI') 
    42             groups = match.groups() 
    43             groups = groups[0:5] + (parse_qs(groups[5] or ''),) + groups[6:] 
    44             groups = [group or '' for group in groups] 
     62            groups = [g or '' for g in match.groups()] 
     63            groups = map(unquote, groups[0:6]) + \ 
     64                     [parse_qs(groups[6] or '')] + \ 
     65                     map(unquote, groups[7:]) 
     66            self.scheme, self.username, self.password, self.host, self.port, \ 
     67                self.path, self.query, self.fragment = groups 
    4568        else: 
    46             groups = [''] * 7 
     69            # Explicitly provide URI components 
     70            self.scheme, self.username, self.password, self.host, self.port, \ 
     71                self.path, self.query, self.fragment = scheme, username, \ 
     72                    password, host, port, path, query, fragment 
    4773 
    48         if not groups[5]: 
    49             groups[5] = {} 
    50         self.scheme, self.username, self.password, self.host, self.path, \ 
    51             self.query, self.fragment = groups 
     74    def _set_path(self, path): 
     75        if path: 
     76            self._path = '/' + posixpath.normpath(path).lstrip('/') 
     77        else: 
     78            self._path = '' 
     79 
     80    def _get_path(self): 
     81        return self._path 
     82 
     83    path = property(_get_path, _set_path) 
    5284 
    5385    def __ne__(self, other): 
     
    5587 
    5688    def __repr__(self): 
    57         uri = self.scheme + '://
     89        uri = self.scheme and (quote(self.scheme) + '://') or '
    5890        if self.username or self.password: 
    5991            if self.username: 
    60                 uri += self.username 
     92                uri += quote(self.username) 
    6193            if self.password: 
    62                 uri += ':' + self.password 
     94                uri += ':' + quote(self.password) 
    6395            uri += '@' 
    64         uri += self.host + self.path 
     96        uri += quote(self.host) 
     97        if self.port: 
     98            uri += ':%s' % port 
     99        uri += quote(self.path) 
    65100        if self.query: 
    66             uri += '?' + '&'.join(['&'.join(['%s=%s' % (k, v) for v in l]) 
     101            uri += '?' + '&'.join(['&'.join(['%s=%s' % (k, quote(v)) for v in l]) 
    67102                                   for k, l in sorted(self.query.items())]) 
    68103        if self.fragment: 
    69             uri += '#' + self.fragment 
     104            uri += '#' + quote(self.fragment) 
    70105        return uri 
    71  
    72 def reduce_text(text, words_re, stemmer=lambda w: w, min_word_length=3, 
    73                 max_word_length=64, unique=False): 
    74     """Compact all words in a block of text. 
    75  
    76     `words_re` is a compiled re object, `stemmer` is a callable returning a 
    77     stemmed word. 
    78  
    79     If `unique` is true, return a string of **unordered** words with duplicates 
    80     removed.""" 
    81     from StringIO import StringIO 
    82     if unique: 
    83         out = set() 
    84         def append(word): 
    85             out.add(word) 
    86     else: 
    87         out = [] 
    88         def append(word): 
    89             out.append(word) 
    90     for word in words_re.findall(text): 
    91         # Cull short and long words 
    92         if min_word_length > len(word) > max_word_length: 
    93             continue 
    94         append(stemmer(word)) 
    95     return u' '.join(out) 
    96  
  • pyndexter/trunk/.todo

    r378 r379  
    114114        Add utility function for converting attribute dictionary keys to plain strings (common pattern). 
    115115    </note> 
     116    <note priority="medium" time="1170829158"> 
     117        Normalise URI usage everything. 
     118    </note> 
     119    <note priority="veryhigh" time="1170915596"> 
     120        Fix port parsing in util.URI. 
     121    </note> 
    116122</todo>