+from pokedex.roomaji import romanize
+
+__all__ = ['open_index', 'lookup', 'random_lookup']
+
+INTERMEDIATE_LOOKUP_RESULTS = 25
+MAX_LOOKUP_RESULTS = 10
+
+# Dictionary of table name => table class.
+# Need the table name so we can get the class from the table name after we
+# retrieve something from the index
+indexed_tables = {}
+for cls in [
+ tables.Ability,
+ tables.Item,
+ tables.Move,
+ tables.Pokemon,
+ tables.Type,
+ ]:
+ indexed_tables[cls.__tablename__] = cls
+
+def open_index(directory=None, session=None, recreate=False):
+ """Opens the whoosh index stored in the named directory and returns (index,
+ speller). If the index doesn't already exist, it will be created.
+
+ `directory`
+ Directory containing the index. Defaults to a location within the
+ `pokedex` egg directory.
+
+ `session`
+ If the index needs to be created, this database session will be used.
+ Defaults to an attempt to connect to the default SQLite database
+ installed by `pokedex setup`.
+
+ `recreate`
+ If set to True, the whoosh index will be created even if it already
+ exists.
+ """
+
+ # Defaults
+ if not directory:
+ directory = pkg_resources.resource_filename('pokedex',
+ 'data/whoosh-index')
+
+ if not session:
+ session = connect()
+
+ # Attempt to open or create the index
+ directory_exists = os.path.exists(directory)
+ if directory_exists and not recreate:
+ # Already exists; should be an index!
+ try:
+ index = whoosh.index.open_dir(directory, indexname='MAIN')
+ spell_store = whoosh.filedb.filestore.FileStorage(directory)
+ speller = whoosh.spelling.SpellChecker(spell_store)
+ return index, speller
+ except whoosh.index.EmptyIndexError as e:
+ # Apparently not a real index. Fall out of the if and create it
+ pass
+
+ # Delete and start over if we're going to bail anyway.
+ if directory_exists and recreate:
+ # Be safe and only delete if it looks like a whoosh index, i.e.,
+ # everything starts with _
+ if all(f[0] == '_' for f in os.listdir(directory)):
+ shutil.rmtree(directory)
+ directory_exists = False
+
+ if not directory_exists:
+ os.mkdir(directory)
+
+
+ ### Create index
+ schema = whoosh.fields.Schema(
+ name=whoosh.fields.ID(stored=True),
+ table=whoosh.fields.ID(stored=True),
+ row_id=whoosh.fields.ID(stored=True),
+ language=whoosh.fields.STORED,
+ display_name=whoosh.fields.STORED, # non-lowercased name
+ forme_name=whoosh.fields.ID,
+ )
+
+ index = whoosh.index.create_in(directory, schema=schema, indexname='MAIN')
+ writer = index.writer()
+
+ # Index every name in all our tables of interest
+ # speller_entries becomes a list of (word, score) tuples; the score is 2
+ # for English names, 1.5 for Roomaji, and 1 for everything else. I think
+ # this biases the results in the direction most people expect, especially
+ # when e.g. German names are very similar to English names
+ speller_entries = []
+ for cls in indexed_tables.values():
+ q = session.query(cls)
+
+ for row in q.yield_per(5):
+ # XXX need to give forme_name a dummy value because I can't search
+ # for explicitly empty fields. boo.
+ row_key = dict(table=unicode(cls.__tablename__),
+ row_id=unicode(row.id),
+ forme_name=u'XXX')
+
+ def add(name, language, score):
+ writer.add_document(name=name.lower(), display_name=name,
+ language=language,
+ **row_key)
+ speller_entries.append((name.lower(), score))
+
+ # If this is a form, mark it as such
+ if getattr(row, 'forme_base_pokemon_id', None):
+ row_key['forme_name'] = row.forme_name
+
+ name = row.name
+ add(name, None, 1)
+
+ # Pokemon also get other languages
+ for foreign_name in getattr(row, 'foreign_names', []):
+ moonspeak = foreign_name.name
+ if name == moonspeak:
+ # Don't add the English name again as a different language;
+ # no point and it makes spell results confusing
+ continue
+
+ add(moonspeak, foreign_name.language.name, 3)
+
+ # Add Roomaji too
+ if foreign_name.language.name == 'Japanese':
+ roomaji = romanize(foreign_name.name)
+ add(roomaji, u'Roomaji', 8)
+
+ writer.commit()
+
+ # Construct and populate a spell-checker index. Quicker to do it all
+ # at once, as every call to add_* does a commit(), and those seem to be
+ # expensive
+ speller = whoosh.spelling.SpellChecker(index.storage)
+ speller.add_scored_words(speller_entries)
+
+ return index, speller
+
+
+class LanguageWeighting(whoosh.scoring.Weighting):
+ """A scoring class that forces otherwise-equal English results to come
+ before foreign results.
+ """
+
+ def score(self, searcher, fieldnum, text, docnum, weight, QTF=1):
+ doc = searcher.stored_fields(docnum)
+ if doc['language'] == None:
+ # English (well, "default"); leave it at 1
+ return weight
+ elif doc['language'] == u'Roomaji':
+ # Give Roomaji a bit of a boost, as it's most likely to be searched
+ return weight * 0.95
+ else:
+ # Everything else can drop down the totem pole
+ return weight * 0.9
+
+rx_is_number = re.compile('^\d+$')
+
+LookupResult = namedtuple('LookupResult',
+ ['object', 'name', 'language', 'exact'])
+
+def _parse_table_name(name):
+ """Takes a singular table name, table name, or table object and returns the
+ table name.
+
+ Returns None for a bogus name.
+ """
+ if hasattr(name, '__tablename__'):
+ return getattr(name, '__tablename__')
+ elif name in indexed_tables:
+ return name
+ elif name + 's' in indexed_tables:
+ return name + 's'
+ else:
+ # Bogus. Be nice and return dummy
+ return None
+
+def _whoosh_records_to_results(records, session, exact=True):
+ """Converts a list of whoosh's indexed records to LookupResult tuples
+ containing database objects.
+ """
+ # XXX this 'exact' thing is getting kinda leaky. would like a better way
+ # to handle it, since only lookup() cares about fuzzy results
+ seen = {}
+ results = []
+ for record in records:
+ # Skip dupes
+ seen_key = record['table'], record['row_id']
+ if seen_key in seen:
+ continue
+ seen[seen_key] = True
+
+ cls = indexed_tables[record['table']]
+ obj = session.query(cls).get(record['row_id'])
+
+ results.append(LookupResult(object=obj,
+ name=record['display_name'],
+ language=record['language'],
+ exact=exact))