~slub.team/goobi-indexserver/3.x

« back to all changes in this revision

Viewing changes to lucene/contrib/analyzers/common/src/java/org/apache/lucene/analysis/pt/RSLPStemmerBase.java

  • Committer: Sebastian Meyer
  • Date: 2012-08-03 09:12:40 UTC
  • Revision ID: sebastian.meyer@slub-dresden.de-20120803091240-x6861b0vabq1xror
Remove Lucene and Solr source code and add patches instead
Fix Bug #985487: Auto-suggestion for the search interface

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
package org.apache.lucene.analysis.pt;
2
 
 
3
 
/**
4
 
 * Licensed to the Apache Software Foundation (ASF) under one or more
5
 
 * contributor license agreements.  See the NOTICE file distributed with
6
 
 * this work for additional information regarding copyright ownership.
7
 
 * The ASF licenses this file to You under the Apache License, Version 2.0
8
 
 * (the "License"); you may not use this file except in compliance with
9
 
 * the License.  You may obtain a copy of the License at
10
 
 *
11
 
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 
 *
13
 
 * Unless required by applicable law or agreed to in writing, software
14
 
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 
 * See the License for the specific language governing permissions and
17
 
 * limitations under the License.
18
 
 */
19
 
 
20
 
import java.io.IOException;
21
 
import java.io.InputStream;
22
 
import java.io.InputStreamReader;
23
 
import java.io.LineNumberReader;
24
 
import java.util.ArrayList;
25
 
import java.util.Arrays;
26
 
import java.util.HashMap;
27
 
import java.util.List;
28
 
import java.util.Map;
29
 
import java.util.regex.Matcher;
30
 
import java.util.regex.Pattern;
31
 
 
32
 
import org.apache.lucene.analysis.CharArraySet;
33
 
import org.apache.lucene.util.Version;
34
 
 
35
 
import static org.apache.lucene.analysis.util.StemmerUtil.*;
36
 
 
37
 
/**
38
 
 * Base class for stemmers that use a set of RSLP-like stemming steps.
39
 
 * <p>
40
 
 * RSLP (Removedor de Sufixos da Lingua Portuguesa) is an algorithm designed
41
 
 * originally for stemming the Portuguese language, described in the paper
42
 
 * <i>A Stemming Algorithm for the Portuguese Language</i>, Orengo et. al.
43
 
 * <p>
44
 
 * Since this time a plural-only modification (RSLP-S) as well as a modification
45
 
 * for the Galician language have been implemented. This class parses a configuration
46
 
 * file that describes {@link Step}s, where each Step contains a set of {@link Rule}s.
47
 
 * <p>
48
 
 * The general rule format is: 
49
 
 * <blockquote>{ "suffix", N, "replacement", { "exception1", "exception2", ...}}</blockquote>
50
 
 * where:
51
 
 * <ul>
52
 
 *   <li><code>suffix</code> is the suffix to be removed (such as "inho").
53
 
 *   <li><code>N</code> is the min stem size, where stem is defined as the candidate stem 
54
 
 *       after removing the suffix (but before appending the replacement!)
55
 
 *   <li><code>replacement</code> is an optimal string to append after removing the suffix.
56
 
 *       This can be the empty string.
57
 
 *   <li><code>exceptions</code> is an optional list of exceptions, patterns that should 
58
 
 *       not be stemmed. These patterns can be specified as whole word or suffix (ends-with) 
59
 
 *       patterns, depending upon the exceptions format flag in the step header.
60
 
 * </ul>
61
 
 * <p>
62
 
 * A step is an ordered list of rules, with a structure in this format:
63
 
 * <blockquote>{ "name", N, B, { "cond1", "cond2", ... }
64
 
 *               ... rules ... };
65
 
 * </blockquote>
66
 
 * where:
67
 
 * <ul>
68
 
 *   <li><code>name</code> is a name for the step (such as "Plural").
69
 
 *   <li><code>N</code> is the min word size. Words that are less than this length bypass
70
 
 *       the step completely, as an optimization. Note: N can be zero, in this case this 
71
 
 *       implementation will automatically calculate the appropriate value from the underlying 
72
 
 *       rules.
73
 
 *   <li><code>B</code> is a "boolean" flag specifying how exceptions in the rules are matched.
74
 
 *       A value of 1 indicates whole-word pattern matching, a value of 0 indicates that 
75
 
 *       exceptions are actually suffixes and should be matched with ends-with.
76
 
 *   <li><code>conds</code> are an optional list of conditions to enter the step at all. If
77
 
 *       the list is non-empty, then a word must end with one of these conditions or it will
78
 
 *       bypass the step completely as an optimization.
79
 
 * </ul>
80
 
 * <p>
81
 
 * @see <a href="http://www.inf.ufrgs.br/~viviane/rslp/index.htm">RSLP description</a>
82
 
 * @lucene.internal
83
 
 */
84
 
public abstract class RSLPStemmerBase {
85
 
  
86
 
  /**
87
 
   * A basic rule, with no exceptions.
88
 
   */
89
 
  protected static class Rule {
90
 
    protected final char suffix[];
91
 
    protected final char replacement[];
92
 
    protected final int min;
93
 
    
94
 
    /**
95
 
     * Create a rule.
96
 
     * @param suffix suffix to remove
97
 
     * @param min minimum stem length
98
 
     * @param replacement replacement string
99
 
     */
100
 
    public Rule(String suffix, int min, String replacement) {
101
 
      this.suffix = suffix.toCharArray();
102
 
      this.replacement = replacement.toCharArray();
103
 
      this.min = min;
104
 
    }
105
 
    
106
 
    /**
107
 
     * @return true if the word matches this rule.
108
 
     */
109
 
    public boolean matches(char s[], int len) {
110
 
      return (len - suffix.length >= min && endsWith(s, len, suffix));
111
 
    }
112
 
    
113
 
    /**
114
 
     * @return new valid length of the string after firing this rule.
115
 
     */
116
 
    public int replace(char s[], int len) {
117
 
      if (replacement.length > 0) {
118
 
        System.arraycopy(replacement, 0, s, len - suffix.length, replacement.length);
119
 
      }
120
 
      return len - suffix.length + replacement.length;
121
 
    }
122
 
  }
123
 
  
124
 
  /**
125
 
   * A rule with a set of whole-word exceptions.
126
 
   */
127
 
  protected static class RuleWithSetExceptions extends Rule {
128
 
    protected final CharArraySet exceptions;
129
 
    
130
 
    public RuleWithSetExceptions(String suffix, int min, String replacement,
131
 
        String[] exceptions) {
132
 
      super(suffix, min, replacement);
133
 
      for (int i = 0; i < exceptions.length; i++) {
134
 
        if (!exceptions[i].endsWith(suffix))
135
 
          System.err.println("warning: useless exception '" + exceptions[i] + "' does not end with '" + suffix + "'");
136
 
      }
137
 
      this.exceptions = new CharArraySet(Version.LUCENE_31,
138
 
           Arrays.asList(exceptions), false);
139
 
    }
140
 
 
141
 
    @Override
142
 
    public boolean matches(char s[], int len) {
143
 
      return super.matches(s, len) && !exceptions.contains(s, 0, len);
144
 
    }
145
 
  }
146
 
  
147
 
  /**
148
 
   * A rule with a set of exceptional suffixes.
149
 
   */
150
 
  protected static class RuleWithSuffixExceptions extends Rule {
151
 
    // TODO: use a more efficient datastructure: automaton?
152
 
    protected final char[][] exceptions;
153
 
    
154
 
    public RuleWithSuffixExceptions(String suffix, int min, String replacement,
155
 
        String[] exceptions) {
156
 
      super(suffix, min, replacement);
157
 
      for (int i = 0; i < exceptions.length; i++) {
158
 
        if (!exceptions[i].endsWith(suffix))
159
 
          System.err.println("warning: useless exception '" + exceptions[i] + "' does not end with '" + suffix + "'");
160
 
      }
161
 
      this.exceptions = new char[exceptions.length][];
162
 
      for (int i = 0; i < exceptions.length; i++)
163
 
        this.exceptions[i] = exceptions[i].toCharArray();
164
 
    }
165
 
    
166
 
    @Override
167
 
    public boolean matches(char s[], int len) {
168
 
      if (!super.matches(s, len))
169
 
        return false;
170
 
      
171
 
      for (int i = 0; i < exceptions.length; i++)
172
 
        if (endsWith(s, len, exceptions[i]))
173
 
          return false;
174
 
 
175
 
      return true;
176
 
    }
177
 
  }
178
 
  
179
 
  /**
180
 
   * A step containing a list of rules.
181
 
   */
182
 
  protected static class Step {
183
 
    protected final String name;
184
 
    protected final Rule rules[];
185
 
    protected final int min;
186
 
    protected final char[][] suffixes;
187
 
    
188
 
    /**
189
 
     * Create a new step
190
 
     * @param name Step's name.
191
 
     * @param rules an ordered list of rules.
192
 
     * @param min minimum word size. if this is 0 it is automatically calculated.
193
 
     * @param suffixes optional list of conditional suffixes. may be null.
194
 
     */
195
 
    public Step(String name, Rule rules[], int min, String suffixes[]) {
196
 
      this.name = name;
197
 
      this.rules = rules;
198
 
      if (min == 0) {
199
 
        min = Integer.MAX_VALUE;
200
 
        for (Rule r : rules)
201
 
          min = Math.min(min, r.min + r.suffix.length);
202
 
      }
203
 
      this.min = min;
204
 
      
205
 
      if (suffixes == null || suffixes.length == 0) {
206
 
        this.suffixes = null;
207
 
      } else {
208
 
        this.suffixes = new char[suffixes.length][];
209
 
        for (int i = 0; i < suffixes.length; i++)
210
 
          this.suffixes[i] = suffixes[i].toCharArray();
211
 
      }
212
 
    }
213
 
    
214
 
    /**
215
 
     * @return new valid length of the string after applying the entire step.
216
 
     */
217
 
    public int apply(char s[], int len) {
218
 
      if (len < min)
219
 
        return len;
220
 
      
221
 
      if (suffixes != null) {
222
 
        boolean found = false;
223
 
        
224
 
        for (int i = 0; i < suffixes.length; i++)
225
 
          if (endsWith(s, len, suffixes[i])) {
226
 
            found = true;
227
 
            break;
228
 
          }
229
 
        
230
 
        if (!found) return len;
231
 
      }
232
 
      
233
 
      for (int i = 0; i < rules.length; i++) {
234
 
        if (rules[i].matches(s, len))
235
 
          return rules[i].replace(s, len);
236
 
      }
237
 
      
238
 
      return len;
239
 
    }
240
 
  }
241
 
  
242
 
  /**
243
 
   * Parse a resource file into an RSLP stemmer description.
244
 
   * @return a Map containing the named Steps in this description.
245
 
   */
246
 
  protected static Map<String,Step> parse(Class<? extends RSLPStemmerBase> clazz, String resource) {
247
 
    // TODO: this parser is ugly, but works. use a jflex grammar instead.
248
 
    try {
249
 
      InputStream is = clazz.getResourceAsStream(resource);
250
 
      LineNumberReader r = new LineNumberReader(new InputStreamReader(is, "UTF-8"));
251
 
      Map<String,Step> steps = new HashMap<String,Step>();
252
 
      String step;
253
 
      while ((step = readLine(r)) != null) {
254
 
        Step s = parseStep(r, step);
255
 
        steps.put(s.name, s);
256
 
      }
257
 
      r.close();
258
 
      return steps;
259
 
    } catch (IOException e) {
260
 
      throw new RuntimeException(e);
261
 
    }
262
 
  }
263
 
  
264
 
  private static final Pattern headerPattern = 
265
 
    Pattern.compile("^\\{\\s*\"([^\"]*)\",\\s*([0-9]+),\\s*(0|1),\\s*\\{(.*)\\},\\s*$");
266
 
  private static final Pattern stripPattern = 
267
 
    Pattern.compile("^\\{\\s*\"([^\"]*)\",\\s*([0-9]+)\\s*\\}\\s*(,|(\\}\\s*;))$");
268
 
  private static final Pattern repPattern = 
269
 
    Pattern.compile("^\\{\\s*\"([^\"]*)\",\\s*([0-9]+),\\s*\"([^\"]*)\"\\}\\s*(,|(\\}\\s*;))$");
270
 
  private static final Pattern excPattern = 
271
 
    Pattern.compile("^\\{\\s*\"([^\"]*)\",\\s*([0-9]+),\\s*\"([^\"]*)\",\\s*\\{(.*)\\}\\s*\\}\\s*(,|(\\}\\s*;))$");
272
 
  
273
 
  private static Step parseStep(LineNumberReader r, String header) throws IOException {
274
 
    Matcher matcher = headerPattern.matcher(header);
275
 
    if (!matcher.find()) {
276
 
      throw new RuntimeException("Illegal Step header specified at line " + r.getLineNumber());
277
 
    }
278
 
    assert matcher.groupCount() == 4;
279
 
    String name = matcher.group(1);
280
 
    int min = Integer.parseInt(matcher.group(2));
281
 
    int type = Integer.parseInt(matcher.group(3));
282
 
    String suffixes[] = parseList(matcher.group(4));
283
 
    Rule rules[] = parseRules(r, type);
284
 
    return new Step(name, rules, min, suffixes);
285
 
  }
286
 
  
287
 
  private static Rule[] parseRules(LineNumberReader r, int type) throws IOException {
288
 
    List<Rule> rules = new ArrayList<Rule>();
289
 
    String line;
290
 
    while ((line = readLine(r)) != null) {
291
 
      Matcher matcher = stripPattern.matcher(line);
292
 
      if (matcher.matches()) {
293
 
        rules.add(new Rule(matcher.group(1), Integer.parseInt(matcher.group(2)), ""));
294
 
      } else {
295
 
        matcher = repPattern.matcher(line);
296
 
        if (matcher.matches()) {
297
 
          rules.add(new Rule(matcher.group(1), Integer.parseInt(matcher.group(2)), matcher.group(3)));
298
 
        } else {
299
 
          matcher = excPattern.matcher(line);
300
 
          if (matcher.matches()) {
301
 
            if (type == 0) {
302
 
              rules.add(new RuleWithSuffixExceptions(matcher.group(1), 
303
 
                        Integer.parseInt(matcher.group(2)), 
304
 
                        matcher.group(3), 
305
 
                        parseList(matcher.group(4))));
306
 
            } else {
307
 
              rules.add(new RuleWithSetExceptions(matcher.group(1), 
308
 
                        Integer.parseInt(matcher.group(2)), 
309
 
                        matcher.group(3), 
310
 
                        parseList(matcher.group(4))));
311
 
            }
312
 
          } else {
313
 
            throw new RuntimeException("Illegal Step rule specified at line " + r.getLineNumber());
314
 
          }
315
 
        }
316
 
      }
317
 
      if (line.endsWith(";"))
318
 
        return rules.toArray(new Rule[rules.size()]);
319
 
    }
320
 
    return null;
321
 
  }
322
 
  
323
 
  private static String[] parseList(String s) {
324
 
    if (s.length() == 0)
325
 
      return null;
326
 
    String list[] = s.split(",");
327
 
    for (int i = 0; i < list.length; i++)
328
 
      list[i] = parseString(list[i].trim());
329
 
    return list;
330
 
  }
331
 
  
332
 
  private static String parseString(String s) {
333
 
    return s.substring(1, s.length()-1);
334
 
  }
335
 
  
336
 
  private static String readLine(LineNumberReader r) throws IOException {
337
 
    String line = null;
338
 
    while ((line = r.readLine()) != null) {
339
 
      line = line.trim();
340
 
      if (line.length() > 0 && line.charAt(0) != '#')
341
 
        return line;
342
 
    }
343
 
    return line;
344
 
  }
345
 
}