~barry/ubuntu/raring/python-whoosh/hg1423

« back to all changes in this revision

Viewing changes to src/whoosh/analysis/analyzers.py

  • Committer: Barry Warsaw
  • Date: 2013-01-23 16:36:20 UTC
  • mfrom: (1.2.20)
  • Revision ID: barry@python.org-20130123163620-wmrpb5uhvx68bo4x
* Pull from upstream Mercurial r1423 for Python 3.3 support.
* d/control:
  - Add B-D and B-D-I on python3-* packages.
  - Added X-Python3-Version: >= 3.2
  - Added python3-whoosh binary package.
* d/patches, d/patches/fix-setup.patch: Fix typo in setup.py and remove
  --pep8 flag from [pytest] section of setup.cfg since it doesn't work.
* d/*.install: Added python3-whoosh.install and updated paths.
* d/rules:
  - Add appropriate targets for Python 3 build.
  - Add get-{packaged-}orig-source for grabbing from upstream Mercurial.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2007 Matt Chaput. All rights reserved.
 
2
#
 
3
# Redistribution and use in source and binary forms, with or without
 
4
# modification, are permitted provided that the following conditions are met:
 
5
#
 
6
#    1. Redistributions of source code must retain the above copyright notice,
 
7
#       this list of conditions and the following disclaimer.
 
8
#
 
9
#    2. Redistributions in binary form must reproduce the above copyright
 
10
#       notice, this list of conditions and the following disclaimer in the
 
11
#       documentation and/or other materials provided with the distribution.
 
12
#
 
13
# THIS SOFTWARE IS PROVIDED BY MATT CHAPUT ``AS IS'' AND ANY EXPRESS OR
 
14
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 
15
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 
16
# EVENT SHALL MATT CHAPUT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 
17
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 
18
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 
19
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 
20
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 
21
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 
22
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
23
#
 
24
# The views and conclusions contained in the software and documentation are
 
25
# those of the authors and should not be interpreted as representing official
 
26
# policies, either expressed or implied, of Matt Chaput.
 
27
 
 
28
from whoosh.analysis.acore import Composable
 
29
from whoosh.analysis.filters import LowercaseFilter
 
30
from whoosh.analysis.filters import StopFilter, STOP_WORDS
 
31
from whoosh.analysis.morph import StemFilter
 
32
from whoosh.analysis.intraword import IntraWordFilter
 
33
from whoosh.analysis.tokenizers import default_pattern
 
34
from whoosh.analysis.tokenizers import CommaSeparatedTokenizer
 
35
from whoosh.analysis.tokenizers import IDTokenizer
 
36
from whoosh.analysis.tokenizers import RegexTokenizer
 
37
from whoosh.analysis.tokenizers import SpaceSeparatedTokenizer
 
38
from whoosh.lang.porter import stem
 
39
 
 
40
 
 
41
# Analyzers
 
42
 
 
43
class Analyzer(Composable):
 
44
    """ Abstract base class for analyzers.
 
45
    """
 
46
 
 
47
    def __repr__(self):
 
48
        return "%s()" % self.__class__.__name__
 
49
 
 
50
    def __eq__(self, other):
 
51
        return (other
 
52
                and self.__class__ is other.__class__
 
53
                and self.__dict__ == other.__dict__)
 
54
 
 
55
    def __call__(self, value, **kwargs):
 
56
        raise NotImplementedError
 
57
 
 
58
    def clean(self):
 
59
        pass
 
60
 
 
61
 
 
62
class CompositeAnalyzer(Analyzer):
 
63
    def __init__(self, *composables):
 
64
        self.items = []
 
65
        for comp in composables:
 
66
            if isinstance(comp, CompositeAnalyzer):
 
67
                self.items.extend(comp.items)
 
68
            else:
 
69
                self.items.append(comp)
 
70
 
 
71
    def __repr__(self):
 
72
        return "%s(%s)" % (self.__class__.__name__,
 
73
                           ", ".join(repr(item) for item in self.items))
 
74
 
 
75
    def __call__(self, value, no_morph=False, **kwargs):
 
76
        items = self.items
 
77
        # Start with tokenizer
 
78
        gen = items[0](value, **kwargs)
 
79
        # Run filters
 
80
        for item in items[1:]:
 
81
            if not (no_morph and hasattr(item, "is_morph") and item.is_morph):
 
82
                gen = item(gen)
 
83
        return gen
 
84
 
 
85
    def __getitem__(self, item):
 
86
        return self.items.__getitem__(item)
 
87
 
 
88
    def __len__(self):
 
89
        return len(self.items)
 
90
 
 
91
    def __eq__(self, other):
 
92
        return (other
 
93
                and self.__class__ is other.__class__
 
94
                and self.items == other.items)
 
95
 
 
96
    def clean(self):
 
97
        for item in self.items:
 
98
            if hasattr(item, "clean"):
 
99
                item.clean()
 
100
 
 
101
    def has_morph(self):
 
102
        return any(item.is_morph for item in self.items)
 
103
 
 
104
 
 
105
# Functions that return composed analyzers
 
106
 
 
107
def IDAnalyzer(lowercase=False):
 
108
    """Deprecated, just use an IDTokenizer directly, with a LowercaseFilter if
 
109
    desired.
 
110
    """
 
111
 
 
112
    tokenizer = IDTokenizer()
 
113
    if lowercase:
 
114
        tokenizer = tokenizer | LowercaseFilter()
 
115
    return tokenizer
 
116
 
 
117
 
 
118
def KeywordAnalyzer(lowercase=False, commas=False):
 
119
    """Parses whitespace- or comma-separated tokens.
 
120
 
 
121
    >>> ana = KeywordAnalyzer()
 
122
    >>> [token.text for token in ana("Hello there, this is a TEST")]
 
123
    ["Hello", "there,", "this", "is", "a", "TEST"]
 
124
 
 
125
    :param lowercase: whether to lowercase the tokens.
 
126
    :param commas: if True, items are separated by commas rather than
 
127
        whitespace.
 
128
    """
 
129
 
 
130
    if commas:
 
131
        tokenizer = CommaSeparatedTokenizer()
 
132
    else:
 
133
        tokenizer = SpaceSeparatedTokenizer()
 
134
    if lowercase:
 
135
        tokenizer = tokenizer | LowercaseFilter()
 
136
    return tokenizer
 
137
 
 
138
 
 
139
def RegexAnalyzer(expression=r"\w+(\.?\w+)*", gaps=False):
 
140
    """Deprecated, just use a RegexTokenizer directly.
 
141
    """
 
142
 
 
143
    return RegexTokenizer(expression=expression, gaps=gaps)
 
144
 
 
145
 
 
146
def SimpleAnalyzer(expression=default_pattern, gaps=False):
 
147
    """Composes a RegexTokenizer with a LowercaseFilter.
 
148
 
 
149
    >>> ana = SimpleAnalyzer()
 
150
    >>> [token.text for token in ana("Hello there, this is a TEST")]
 
151
    ["hello", "there", "this", "is", "a", "test"]
 
152
 
 
153
    :param expression: The regular expression pattern to use to extract tokens.
 
154
    :param gaps: If True, the tokenizer *splits* on the expression, rather
 
155
        than matching on the expression.
 
156
    """
 
157
 
 
158
    return RegexTokenizer(expression=expression, gaps=gaps) | LowercaseFilter()
 
159
 
 
160
 
 
161
def StandardAnalyzer(expression=default_pattern, stoplist=STOP_WORDS,
 
162
                     minsize=2, maxsize=None, gaps=False):
 
163
    """Composes a RegexTokenizer with a LowercaseFilter and optional
 
164
    StopFilter.
 
165
 
 
166
    >>> ana = StandardAnalyzer()
 
167
    >>> [token.text for token in ana("Testing is testing and testing")]
 
168
    ["testing", "testing", "testing"]
 
169
 
 
170
    :param expression: The regular expression pattern to use to extract tokens.
 
171
    :param stoplist: A list of stop words. Set this to None to disable
 
172
        the stop word filter.
 
173
    :param minsize: Words smaller than this are removed from the stream.
 
174
    :param maxsize: Words longer that this are removed from the stream.
 
175
    :param gaps: If True, the tokenizer *splits* on the expression, rather
 
176
        than matching on the expression.
 
177
    """
 
178
 
 
179
    ret = RegexTokenizer(expression=expression, gaps=gaps)
 
180
    chain = ret | LowercaseFilter()
 
181
    if stoplist is not None:
 
182
        chain = chain | StopFilter(stoplist=stoplist, minsize=minsize,
 
183
                                   maxsize=maxsize)
 
184
    return chain
 
185
 
 
186
 
 
187
def StemmingAnalyzer(expression=default_pattern, stoplist=STOP_WORDS,
 
188
                     minsize=2, maxsize=None, gaps=False, stemfn=stem,
 
189
                     ignore=None, cachesize=50000):
 
190
    """Composes a RegexTokenizer with a lower case filter, an optional stop
 
191
    filter, and a stemming filter.
 
192
 
 
193
    >>> ana = StemmingAnalyzer()
 
194
    >>> [token.text for token in ana("Testing is testing and testing")]
 
195
    ["test", "test", "test"]
 
196
 
 
197
    :param expression: The regular expression pattern to use to extract tokens.
 
198
    :param stoplist: A list of stop words. Set this to None to disable
 
199
        the stop word filter.
 
200
    :param minsize: Words smaller than this are removed from the stream.
 
201
    :param maxsize: Words longer that this are removed from the stream.
 
202
    :param gaps: If True, the tokenizer *splits* on the expression, rather
 
203
        than matching on the expression.
 
204
    :param ignore: a set of words to not stem.
 
205
    :param cachesize: the maximum number of stemmed words to cache. The larger
 
206
        this number, the faster stemming will be but the more memory it will
 
207
        use. Use None for no cache, or -1 for an unbounded cache.
 
208
    """
 
209
 
 
210
    ret = RegexTokenizer(expression=expression, gaps=gaps)
 
211
    chain = ret | LowercaseFilter()
 
212
    if stoplist is not None:
 
213
        chain = chain | StopFilter(stoplist=stoplist, minsize=minsize,
 
214
                                   maxsize=maxsize)
 
215
    return chain | StemFilter(stemfn=stemfn, ignore=ignore,
 
216
                              cachesize=cachesize)
 
217
 
 
218
 
 
219
def FancyAnalyzer(expression=r"\s+", stoplist=STOP_WORDS, minsize=2,
 
220
                  maxsize=None, gaps=True, splitwords=True, splitnums=True,
 
221
                  mergewords=False, mergenums=False):
 
222
    """Composes a RegexTokenizer with an IntraWordFilter, LowercaseFilter, and
 
223
    StopFilter.
 
224
 
 
225
    >>> ana = FancyAnalyzer()
 
226
    >>> [token.text for token in ana("Should I call getInt or get_real?")]
 
227
    ["should", "call", "getInt", "get", "int", "get_real", "get", "real"]
 
228
 
 
229
    :param expression: The regular expression pattern to use to extract tokens.
 
230
    :param stoplist: A list of stop words. Set this to None to disable
 
231
        the stop word filter.
 
232
    :param minsize: Words smaller than this are removed from the stream.
 
233
    :param maxsize: Words longer that this are removed from the stream.
 
234
    :param gaps: If True, the tokenizer *splits* on the expression, rather
 
235
        than matching on the expression.
 
236
    """
 
237
 
 
238
    return (RegexTokenizer(expression=expression, gaps=gaps)
 
239
            | IntraWordFilter(splitwords=splitwords, splitnums=splitnums,
 
240
                              mergewords=mergewords, mergenums=mergenums)
 
241
            | LowercaseFilter()
 
242
            | StopFilter(stoplist=stoplist, minsize=minsize)
 
243
            )
 
244
 
 
245
 
 
246
def LanguageAnalyzer(lang, expression=default_pattern, gaps=False,
 
247
                     cachesize=50000):
 
248
    """Configures a simple analyzer for the given language, with a
 
249
    LowercaseFilter, StopFilter, and StemFilter.
 
250
 
 
251
    >>> ana = LanguageAnalyzer("es")
 
252
    >>> [token.text for token in ana("Por el mar corren las liebres")]
 
253
    ['mar', 'corr', 'liebr']
 
254
 
 
255
    :param expression: The regular expression pattern to use to extract tokens.
 
256
    :param gaps: If True, the tokenizer *splits* on the expression, rather
 
257
        than matching on the expression.
 
258
    :param cachesize: the maximum number of stemmed words to cache. The larger
 
259
        this number, the faster stemming will be but the more memory it will
 
260
        use.
 
261
    """
 
262
 
 
263
    from whoosh.lang import NoStemmer, NoStopWords
 
264
    from whoosh.lang import stopwords_for_language
 
265
 
 
266
    # Make the start of the chain
 
267
    chain = (RegexTokenizer(expression=expression, gaps=gaps)
 
268
             | LowercaseFilter())
 
269
 
 
270
    # Add a stop word filter
 
271
    try:
 
272
        stoplist = stopwords_for_language(lang)
 
273
        chain = chain | StopFilter(stoplist=stoplist)
 
274
    except NoStopWords:
 
275
        pass
 
276
 
 
277
    # Add a stemming filter
 
278
    try:
 
279
        chain = chain | StemFilter(lang=lang, cachesize=cachesize)
 
280
    except NoStemmer:
 
281
        pass
 
282
 
 
283
    return chain