Changeset 66

Show
Ignore:
Timestamp:
12/08/05 03:15:42 (3 years ago)
Author:
ged
Message:

- Test and fix for new compound pointer types (fixes #3)
- Added mode-specification and path specification to Lexicon constructor. This

is to support modifying WordNet? databases as well as being able to open them
read-only. This addresses the "writable data files" bug (fixes #2).

- Moved default location for data files to CONFIGdatadir?
- Added quite a bit more documentation which is visible if you use the included

docs/makedocs.rb script or manually specify an 'accessors' option to rdoc.

Location:
trunk
Files:
8 modified

Legend:

Unmodified
Added
Removed
  • trunk/convertdb.rb

    r60 r66  
    3232    $LOAD_PATH.unshift "#{base}/lib" unless $LOAD_PATH.include?( "#{base}/lib" ) 
    3333    $LOAD_PATH.unshift base 
     34 
     35    unless defined?( UtilityFunctions ) 
     36        require "#{base}/utils.rb" 
     37        include UtilityFunctions 
     38    end 
    3439end 
    3540 
    3641require 'strscan' 
    37 require 'utils' 
    3842require 'wordnet' 
    3943require 'optparse' 
    4044require 'fileutils' 
    4145 
    42 include UtilityFunctions 
    4346 
    4447# Globals: Index of words => senses, StringScanner for parsing. 
     
    6972CommitThreshold = 2000 
    7073 
     74# Temporary location for the lexicon data files 
     75BuildDir = File::join( File::dirname(__FILE__), File::basename(WordNet::Lexicon::DefaultDbEnv) ) 
     76 
     77 
    7178 
    7279##################################################################### 
    7380### M A I N   P R O G R A M 
    7481##################################################################### 
    75 def main 
     82def convertdb( errorLimit=0 ) 
    7683    $stderr.sync = $stdout.sync = true 
    7784    header "WordNet Lexicon Converter" 
    78     errorLimit = 0 
    79  
    80     ARGV.options {|oparser| 
    81         oparser.banner = "Usage: #{File::basename($0)} -dv\n" 
    82  
    83         # Debugging on/off 
    84         oparser.on( "--debug", "-d", TrueClass, "Turn debugging on" ) { 
    85             $DEBUG = true 
    86             debugMsg "Turned debugging on." 
    87         } 
    88  
    89         # Verbose 
    90         oparser.on( "--verbose", "-v", TrueClass, "Verbose progress messages" ) { 
    91             $VERBOSE = true 
    92             debugMsg "Turned verbose on." 
    93         } 
    94  
    95         # Error-limit 
    96         oparser.on( "--error-limit=COUNT", "-eCOUNT", Integer, 
    97             "Error limit -- quit after COUNT errors" ) {|arg| 
    98             errorLimit = arg.to_i 
    99             debugMsg "Set error limit to #{errorLimit}" 
    100         } 
    101  
    102         # Handle the 'help' option 
    103         oparser.on( "--help", "-h", "Display this text." ) { 
    104             $stderr.puts oparser 
    105             exit!(0) 
    106         } 
    107  
    108         oparser.parse! 
    109     } 
    11085 
    11186    # Make sure the user knows what they're in for 
     
    11792    # Open the database and check to be sure it's empty. Confirm overwrite if 
    11893    # not. Checkpoint and set up logging proc if debugging. 
    119     if File::exists?( WordNet::Lexicon::DbFile ) 
     94    if File::exists?( BuildDir ) 
    12095        message ">>> Warning: Existing data in the Ruby-WordNet databases\n"\ 
    12196            "will be overwritten.\n" 
    12297        abort( "user cancelled." ) unless  
    12398            /^y/i =~ promptWithDefault( "Continue?", "n" ) 
    124         FileUtils::rm_rf( WordNet::Lexicon::DbFile ) 
     99        FileUtils::rm_rf( BuildDir ) 
    125100    end 
    126101 
    127102    # Find the source data files 
    128103    if ARGV.empty? 
     104 
     105        # :TODO: Do some more intelligent searching here 
    129106        message "Where can I find the WordNet data files?\n" 
    130         datadir = promptWithDefault( "Data directory", "/usr/local/WordNet-2.0/dict" ) 
     107        datadir = promptWithDefault( "Data directory", "/usr/local/WordNet-2.1/dict" ) 
    131108    else 
    132109        datadir = ARGV.shift 
     
    139116        File::exists?( testfile ) 
    140117 
    141     # Open the lexicon, which creates a new database under lib/wordnet/lexicon. 
    142     lexicon = WordNet::Lexicon::new 
     118    # Open the lexicon readwrite into the temporary datadir 
     119    FileUtils::mkdir( BuildDir ) 
     120    lexicon = WordNet::Lexicon::new( BuildDir, 0666 ) 
    143121 
    144122    # Process each fileset 
     
    373351 
    374352 
    375 # Start the program 
    376 main 
    377  
     353# Start the program if it's run directly 
     354if $0 == __FILE__ 
     355    errorLimit = 0 
     356     
     357    ARGV.options {|oparser| 
     358        oparser.banner = "Usage: #{File::basename($0)} -dv\n" 
     359 
     360        # Debugging on/off 
     361        oparser.on( "--debug", "-d", TrueClass, "Turn debugging on" ) { 
     362            $DEBUG = true 
     363            debugMsg "Turned debugging on." 
     364        } 
     365 
     366        # Verbose 
     367        oparser.on( "--verbose", "-v", TrueClass, "Verbose progress messages" ) { 
     368            $VERBOSE = true 
     369            debugMsg "Turned verbose on." 
     370        } 
     371 
     372        # Error-limit 
     373        oparser.on( "--error-limit=COUNT", "-eCOUNT", Integer, 
     374            "Error limit -- quit after COUNT errors" ) {|arg| 
     375            errorLimit = arg.to_i 
     376            debugMsg "Set error limit to #{errorLimit}" 
     377        } 
     378 
     379        # Handle the 'help' option 
     380        oparser.on( "--help", "-h", "Display this text." ) { 
     381            $stderr.puts oparser 
     382            exit!(0) 
     383        } 
     384 
     385        oparser.parse! 
     386    } 
     387 
     388    convertdb( errorLimit ) 
     389end 
     390 
  • trunk/lib/wordnet/constants.rb

    r58 r66  
    3030# == Copyright 
    3131# 
    32 # Copyright (c) 2003 The FaerieMUD Consortium. All rights reserved. 
     32# Copyright (c) 2003, 2005 The FaerieMUD Consortium. All rights reserved. 
    3333#  
    3434# This module is free software. You may use, modify, and/or redistribute this 
     
    6363            const_set( cname, val ) 
    6464        } 
     65 
     66        # Information about pointer types is contained in the wninput(5WN) 
     67        # manpage. 
    6568 
    6669        # Synset pointer typenames -> indicators 
     
    9497        } 
    9598 
     99        # Hypernym synset pointer types 
     100        HypernymTypes = { 
     101            nil             => '@', # Install non-subtype methods, too 
     102            :instance       => '@i', 
     103        } 
     104         
     105        # Hypernym indicator -> type map 
     106        HypernymSymbols = HypernymTypes.invert 
     107 
     108        # Hyponym synset pointer types 
     109        HyponymTypes = { 
     110            nil             => '~', # Install non-subtype methods, too 
     111            :instance       => '~i', 
     112        } 
     113         
     114        # Hyponym indicator -> type map 
     115        HyponymSymbols = HyponymTypes.invert 
     116 
    96117        # Meronym synset pointer types 
    97118        MeronymTypes = { 
     
    168189        # Map of primary types to maps of their subtypes  
    169190        PointerSubTypes = { 
     191            :hyponym    => HyponymTypes, 
     192            :hypernym   => HypernymTypes, 
    170193            :meronym    => MeronymTypes, 
    171194            :holonym    => HolonymTypes, 
  • trunk/lib/wordnet/lexicon.rb

    r58 r66  
    1616# Michael Granger <ged@FaerieMUD.org> 
    1717#  
    18 # Copyright (c) 2002, 2003 The FaerieMUD Consortium. All rights reserved. 
     18# Copyright (c) 2002, 2003, 2005 The FaerieMUD Consortium. All rights reserved. 
    1919#  
    2020# This module is free software. You may use, modify, and/or redistribute this 
     
    3030#  
    3131 
     32require 'rbconfig' 
    3233require 'bdb' 
    3334require 'sync' 
     
    3637require 'wordnet/synset' 
    3738 
    38 module WordNet 
    39  
    40     ### Lexicon exception - something has gone wrong in the internals of the 
    41     ### lexicon. 
    42     class LexiconError < StandardError ; end 
    43  
    44     ### Lookup error - the object being looked up either doesn't exist or is 
    45     ### malformed 
    46     class LookupError < StandardError ; end 
    47  
    48     ### WordNet lexicon class - abstracts access to the WordNet lexical 
    49     ### databases, and provides factory methods for looking up and creating new 
    50     ### WordNet::Synset objects. 
    51     class Lexicon 
    52         include WordNet::Constants 
    53         include CrossCase if defined?( CrossCase ) 
    54  
    55         # Class constants 
    56         Version = /([\d\.]+)/.match( %q{$Revision: 1.4 $} )[1] 
    57         Rcsid = %q$Id$ 
    58  
    59         ############################################################# 
    60         ### B E R K E L E Y D B   C O N F I G U R A T I O N 
    61         ############################################################# 
    62  
    63         # The path to the WordNet BerkeleyDB Env. It lives in the directory that 
    64         # this module is in. 
    65         DbFile = File::join( File::dirname(__FILE__), "lexicon" ) 
    66  
    67         # Options for the creation of the Env object 
    68         EnvOptions = { 
    69             :set_timeout    => 50, 
    70             :set_lk_detect  => 1, 
    71             :set_verbose    => false, 
     39### Lexicon exception - something has gone wrong in the internals of the 
     40### lexicon. 
     41class WordNet::LexiconError < StandardError ; end 
     42 
     43### Lookup error - the object being looked up either doesn't exist or is 
     44### malformed 
     45class WordNet::LookupError < StandardError ; end 
     46 
     47### WordNet lexicon class - abstracts access to the WordNet lexical 
     48### databases, and provides factory methods for looking up and creating new 
     49### WordNet::Synset objects. 
     50class WordNet::Lexicon 
     51    include WordNet::Constants 
     52    include CrossCase if defined?( CrossCase ) 
     53 
     54    # Subversion Id 
     55    SvnId = %q$Id$ 
     56 
     57    # Subversion revision 
     58    SvnRev = %q$Rev$ 
     59 
     60 
     61    ############################################################# 
     62    ### B E R K E L E Y D B   C O N F I G U R A T I O N 
     63    ############################################################# 
     64 
     65    # The path to the WordNet BerkeleyDB Env. It lives in the directory that 
     66    # this module is in. 
     67    DefaultDbEnv = File::join( Config::CONFIG['datadir'], "ruby-wordnet" ) 
     68 
     69    # Options for the creation of the Env object 
     70    EnvOptions = { 
     71        :set_timeout    => 50, 
     72        :set_lk_detect  => 1, 
     73        :set_verbose    => false, 
     74    } 
     75 
     76    # Flags for the creation of the Env object 
     77    EnvFlags = BDB::CREATE|BDB::INIT_TRANSACTION|BDB::RECOVER 
     78 
     79    # Table names (actually database names in BerkeleyDB) 
     80    TableNames = { 
     81        :index => "index", 
     82        :data => "data", 
     83        :morph => "morph", 
     84    } 
     85 
     86 
     87 
     88    ############################################################# 
     89    ### I N S T A N C E   M E T H O D S 
     90    ############################################################# 
     91 
     92    ### Create a new WordNet::Lexicon object that will read its data from 
     93    ### the given +dbenv+ (a BerkeleyDB env directory). The database will be 
     94    ### opened with the specified +mode+, which can either be a numeric  
     95    ### octal mode (e.g., 0444) or one of (:readonly, :readwrite). 
     96    def initialize( dbenv=DefaultDbEnv, mode=:readonly ) 
     97        raise ArgumentError, "Cannot find data directory '#{dbenv}'" unless 
     98            File::directory?( dbenv ) 
     99 
     100        mode = normalize_mode( mode ) 
     101         
     102        begin 
     103            @env = BDB::Env::new( dbenv, EnvFlags, EnvOptions ) 
     104            @indexDb = @env.open_db( BDB::BTREE, "index", nil, BDB::CREATE, mode ) 
     105            @dataDb = @env.open_db( BDB::BTREE, "data", nil, BDB::CREATE, mode ) 
     106            @morphDb = @env.open_db( BDB::BTREE, "morph", nil, BDB::CREATE, mode ) 
     107        rescue StandardError => err 
     108            msg = "Error while opening Ruby-WordNet data files: %s" % err.message 
     109            raise err.exception( msg ) 
     110        end 
     111    end 
     112 
     113 
     114 
     115    ###### 
     116    public 
     117    ###### 
     118 
     119    # The BDB::Env object which contains the wordnet lexicon's databases. 
     120    attr_reader :env 
     121 
     122    # The handle to the index table 
     123    attr_reader :indexDb 
     124 
     125    # The handle to the synset data table 
     126    attr_reader :dataDb 
     127 
     128    # The handle to the morph table 
     129    attr_reader :morphDb 
     130 
     131 
     132    ### Close the lexicon's database environment 
     133    def close 
     134        @env.close 
     135    end 
     136 
     137 
     138    ### Checkpoint the database. (BerkeleyDB-specific) 
     139    def checkpoint( bytes=0, minutes=0 ) 
     140        @env.checkpoint 
     141    end 
     142 
     143 
     144    ### Return a list of archival logfiles that can be removed 
     145    ### safely. (BerkeleyDB-specific). 
     146    def archlogs 
     147        return @env.log_archive( BDB::ARCH_ABS ) 
     148    end 
     149 
     150 
     151    ### Remove any archival logfiles for the lexicon's database 
     152    ### environment. (BerkeleyDB-specific). 
     153    def cleanLogs 
     154        self.archlogs.each {|logfile| 
     155            File::chmod( 0777, logfile ) 
     156            File::delete( logfile ) 
    72157        } 
    73  
    74         # Flags for the creation of the Env object 
    75         EnvFlags = BDB::CREATE|BDB::INIT_TRANSACTION|BDB::RECOVER 
    76  
    77         # Table names (actually database names in BerkeleyDB) 
    78         TableNames = { 
    79             :index => "index", 
    80             :data => "data", 
    81             :morph => "morph", 
     158    end 
     159 
     160 
     161    ### Returns an integer of the familiarity/polysemy count for +word+ as a 
     162    ### +partOfSpeech+. Note that polysemy can be identified for a given 
     163    ### word by counting the synsets returned by #lookupSynsets. 
     164    def familiarity( word, partOfSpeech, polyCount=nil ) 
     165        wordkey = self.makeWordKey( word, partOfSpeech ) 
     166        return nil unless @indexDb.key?( wordkey ) 
     167        @indexDb[ wordkey ].split( WordNet::SubDelimRe ).length 
     168    end 
     169 
     170 
     171    ### Look up sysets (Wordnet::Synset objects) matching +text+ as a 
     172    ### +partOfSpeech+, where +partOfSpeech+ is one of +WordNet::Noun+, 
     173    ### +WordNet::Verb+, +WordNet::Adjective+, or +WordNet::Adverb+. Without 
     174    ### +sense+, #lookupSynsets will return all matches that are a 
     175    ### +partOfSpeech+. If +sense+ is specified, only the synset object that 
     176    ### matches that particular +partOfSpeech+ and +sense+ is returned. 
     177    def lookupSynsets( word, partOfSpeech, sense=nil ) 
     178        wordkey = self.makeWordKey( word, partOfSpeech ) 
     179        pos = self.makePos( partOfSpeech ) 
     180        synsets = [] 
     181 
     182        # Look up the index entry, trying first the word as given, and if 
     183        # that fails, trying morphological conversion. 
     184        entry = @indexDb[ wordkey ] 
     185        if entry.nil? && (word = self.morph( word, partOfSpeech )) 
     186            entry = @indexDb[ wordkey ] 
     187        end 
     188 
     189        # If the lookup failed both ways, just abort 
     190        return nil unless entry 
     191 
     192        # Make synset keys from the entry, narrowing it to just the sense 
     193        # requested if one was specified. 
     194        synkeys = entry.split( SubDelimRe ).collect {|off| "#{off}%#{pos}" } 
     195        if sense 
     196            return lookupSynsetsByKey( synkeys[sense - 1] ) 
     197        else 
     198            return [ lookupSynsetsByKey(*synkeys) ].flatten 
     199        end 
     200    end 
     201 
     202 
     203    ### Returns the WordNet::Synset objects corresponding to the +keys+ 
     204    ### specified. The +keys+ are made up of the target synset's "offset" 
     205    ### and syntactic category catenated together with a '%' character. 
     206    def lookupSynsetsByKey( *keys ) 
     207        synsets = [] 
     208 
     209        keys.each {|key| 
     210            raise LookupError, "Failed lookup of synset '#{key}':"\ 
     211                "No such synset" unless @dataDb.key?( key ) 
     212 
     213            data = @dataDb[ key ] 
     214            offset, partOfSpeech = key.split( /%/, 2 ) 
     215            synsets << WordNet::Synset::new( self, offset, partOfSpeech, nil, data ) 
    82216        } 
    83217 
    84  
    85  
    86         ############################################################# 
    87         ### I N S T A N C E   M E T H O D S 
    88         ############################################################# 
    89  
    90         ### Create a new WordNet::Lexicon object. 
    91         def initialize 
    92             Dir::mkdir( DbFile ) unless File::directory?( DbFile ) 
    93  
    94             @env = BDB::Env::new( DbFile, EnvFlags, EnvOptions ) 
    95             @indexDb = @env.open_db( BDB::BTREE, "index", nil, BDB::CREATE, 0666 ) 
    96             @dataDb = @env.open_db( BDB::BTREE, "data", nil, BDB::CREATE, 0666 ) 
    97             @morphDb = @env.open_db( BDB::BTREE, "morph", nil, BDB::CREATE, 0666 ) 
     218        return *synsets 
     219    end 
     220    alias_method :lookupSynsetsByOffset, :lookupSynsetsByKey 
     221 
     222 
     223    ### Returns a form of +word+ as a part of speech +partOfSpeech+, as 
     224    ### found in the WordNet morph files. The #lookupSynsets method perfoms 
     225    ### morphological conversion automatically, so a call to #morph is not 
     226    ### required. 
     227    def morph( word, partOfSpeech ) 
     228        return @morphDb[ self.makeWordKey(word, partOfSpeech) ] 
     229    end 
     230 
     231 
     232    ### Returns the result of looking up +word+ in the inverse of the WordNet 
     233    ### morph files. _(This is undocumented in Lingua::Wordnet)_ 
     234    def reverseMorph( word ) 
     235        @morphDb.invert[ word ] 
     236    end 
     237 
     238 
     239    ### Returns an array of compound words matching +text+. 
     240    def grep( text ) 
     241        return [] if text.empty? 
     242         
     243        words = [] 
     244         
     245        # Grab a cursor into the database and fetch while the key matches 
     246        # the target text 
     247        cursor = @indexDb.cursor 
     248        rec = cursor.set_range( text ) 
     249        while /^#{text}/ =~ rec[0] 
     250            words.push rec[0] 
     251            rec = cursor.next 
    98252        end 
    99  
    100  
    101         ###### 
    102         public 
    103         ###### 
    104  
    105         # The BDB::Env object which contains the wordnet lexicon's databases. 
    106         attr_reader :env 
    107  
    108         # The handle to the index table 
    109         attr_reader :indexDb 
    110  
    111         # The handle to the synset data table 
    112         attr_reader :dataDb 
    113  
    114         # The handle to the morph table 
    115         attr_reader :morphDb 
    116  
    117  
    118         ### Checkpoint the database. (BerkeleyDB-specific) 
    119         def checkpoint( bytes=0, minutes=0 ) 
    120             @env.checkpoint 
     253        cursor.close 
     254 
     255        return *words 
     256    end 
     257 
     258 
     259    ### Factory method: Creates and returns a new WordNet::Synset object in 
     260    ### this lexicon for the specified +word+ and +partOfSpeech+. 
     261    def createSynset( word, partOfSpeech ) 
     262        return WordNet::Synset::new( self, '', partOfSpeech, word ) 
     263    end 
     264    alias_method :newSynset, :createSynset 
     265 
     266 
     267    ### Store the specified +synset+ (a WordNet::Synset object) in the 
     268    ### lexicon. Returns the key of the stored synset. 
     269    def storeSynset( synset ) 
     270        strippedOffset = nil 
     271        pos = nil 
     272 
     273        # Start a transaction 
     274        @env.begin( BDB::TXN_COMMIT, @dataDb ) do |txn,datadb| 
     275 
     276            # If this is a new synset, generate an offset for it 
     277            if synset.offset == 1 
     278                synset.offset = 
     279                    (datadb['offsetcount'] = datadb['offsetcount'].to_i + 1) 
     280            end 
     281             
     282            # Write the data entry 
     283            datadb[ synset.key ] = synset.serialize 
     284                 
     285            # Write the index entries 
     286            txn.begin( BDB::TXN_COMMIT, @indexDb ) do |txn,indexdb| 
     287 
     288                # Make word/part-of-speech pairs from the words in the synset 
     289                synset.words.collect {|word| word + "%" + pos }.each {|word| 
     290 
     291                    # If the index already has this word, but not this 
     292                    # synset, add it 
     293                    if indexdb.key?( word ) 
     294                        indexdb[ word ] << SubDelim << synset.offset unless 
     295                            indexdb[ word ].include?( synset.offset ) 
     296                    else 
     297                        indexdb[ word ] = synset.offset 
     298                    end 
     299                } 
     300            end # transaction on @indexDb 
     301        end # transaction on @dataDB 
     302 
     303        return synset.offset 
     304    end 
     305 
     306 
     307    ### Remove the specified +synset+ (a WordNet::Synset object) in the 
     308    ### lexicon. Returns the offset of the stored synset. 
     309    def removeSynset( synset ) 
     310        # If it's not in the database (ie., doesn't have a real offset), 
     311        # just return. 
     312        return nil if synset.offset == 1 
     313 
     314        # Start a transaction on the data table 
     315        @env.begin( BDB::TXN_COMMIT, @dataDb ) do |txn,datadb| 
     316 
     317            # First remove the index entries for this synset by iterating 
     318            # over each of its words 
     319            txn.begin( BDB::TXN_COMMIT, @indexDb ) do |txn,indexdb| 
     320                synset.words.collect {|word| word + "%" + pos }.each {|word| 
     321 
     322                    # If the index contains an entry for this word, either 
     323                    # splice out the offset for the synset being deleted if 
     324                    # there are more than one, or just delete the whole 
     325                    # entry if it's the only one. 
     326                    if indexdb.key?( word ) 
     327                        offsets = indexdb[ word ]. 
     328                            split( SubDelimRe ). 
     329                            reject {|offset| offset == synset.offset} 
     330 
     331                        unless offsets.empty? 
     332                            indexDb[ word ] = newoffsets.join( SubDelim ) 
     333                        else 
     334                            indexDb.delete( word ) 
     335                        end 
     336                    end 
     337                } 
     338            end 
     339 
     340            # :TODO: Delete synset from pointers of related synsets 
     341 
     342            # Delete the synset from the main db 
     343            datadb.delete( synset.offset ) 
    121344        end 
    122345 
    123  
    124         ### Return a list of archival logfiles that can be removed 
    125         ### safely. (BerkeleyDB-specific). 
    126         def archlogs 
    127             return @env.log_archive( BDB::ARCH_ABS ) 
    128         end 
    129  
    130  
    131         ### Remove any archival logfiles for the lexicon's database 
    132         ### environment. (BerkeleyDB-specific). 
    133         def cleanLogs 
    134             self.archlogs.each {|logfile| 
    135                 File::chmod( 0777, logfile ) 
    136                 File::delete( logfile ) 
    137             } 
    138         end 
    139  
    140  
    141         ### Returns an integer of the familiarity/polysemy count for +word+ as a 
    142         ### +partOfSpeech+. Note that polysemy can be identified for a given 
    143         ### word by counting the synsets returned by #lookupSynsets. 
    144         def familiarity( word, partOfSpeech, polyCount=nil ) 
    145             wordkey = self.makeWordKey( word, partOfSpeech ) 
    146             return nil unless @indexDb.key?( wordkey ) 
    147             @indexDb[ wordkey ].split( WordNet::SubDelimRe ).length 
    148         end 
    149  
    150  
    151         ### Look up sysets (Wordnet::Synset objects) matching +text+ as a 
    152         ### +partOfSpeech+, where +partOfSpeech+ is one of +WordNet::Noun+, 
    153         ### +WordNet::Verb+, +WordNet::Adjective+, or +WordNet::Adverb+. Without 
    154         ### +sense+, #lookupSynsets will return all matches that are a 
    155         ### +partOfSpeech+. If +sense+ is specified, only the synset object that 
    156         ### matches that particular +partOfSpeech+ and +sense+ is returned. 
    157         def lookupSynsets( word, partOfSpeech, sense=nil ) 
    158             wordkey = self.makeWordKey( word, partOfSpeech ) 
    159             pos = self.makePos( partOfSpeech ) 
    160             synsets = [] 
    161  
    162             # Look up the index entry, trying first the word as given, and if 
    163             # that fails, trying morphological conversion. 
    164             entry = @indexDb[ wordkey ] 
    165             if entry.nil? && (word = self.morph( word, partOfSpeech )) 
    166                 entry = @indexDb[ wordkey ] 
    167             end 
    168  
    169             # If the lookup failed both ways, just abort 
    170             return nil unless entry 
    171  
    172             # Make synset keys from the entry, narrowing it to just the sense 
    173             # requested if one was specified. 
    174             synkeys = entry.split( SubDelimRe ).collect {|off| "#{off}%#{pos}" } 
    175             if sense 
    176                 return lookupSynsetsByKey( synkeys[sense - 1] ) 
    177             else 
    178                 return [ lookupSynsetsByKey(*synkeys) ].flatten 
    179             end 
    180         end 
    181  
    182  
    183         ### Returns the WordNet::Synset objects corresponding to the +keys+ 
    184         ### specified. The +keys+ are made up of the target synset's "offset" 
    185         ### and syntactic category catenated together with a '%' character. 
    186         def lookupSynsetsByKey( *keys ) 
    187             synsets = [] 
    188  
    189             keys.each {|key| 
    190                 raise LookupError, "Failed lookup of synset '#{key}':"\ 
    191                     "No such synset" unless @dataDb.key?( key ) 
    192  
    193                 data = @dataDb[ key ] 
    194                 offset, partOfSpeech = key.split( /%/, 2 ) 
    195                 synsets << Synset::new( self, offset, partOfSpeech, nil, data ) 
    196             } 
    197  
    198             return *synsets 
    199         end 
    200         alias_method :lookupSynsetsByOffset, :lookupSynsetsByKey 
    201  
    202  
    203         ### Returns a form of +word+ as a part of speech +partOfSpeech+, as 
    204         ### found in the WordNet morph files. The #lookupSynsets method perfoms 
    205         ### morphological conversion automatically, so a call to #morph is not 
    206         ### required. 
    207         def morph( word, partOfSpeech ) 
    208             return @morphDb[ self.makeWordKey(word, partOfSpeech) ] 
    209         end 
    210</