Changed object and alias search methods to be more effective. Made aliases a separate model to avoid overhead with large searches. The change of aliases meand you need to resync your database.

This commit is contained in:
Griatch 2010-10-30 18:42:37 +00:00
parent 268328d36a
commit 19dd476115
3 changed files with 174 additions and 196 deletions

View file

@ -3,115 +3,16 @@ Custom manager for Objects.
"""
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models.fields import exceptions
from src.typeclasses.managers import TypedObjectManager
from src.typeclasses.managers import returns_typeclass, returns_typeclass_list
from src.utils import create
# Try to use a custom way to parse id-tagged multimatches.
try:
IDPARSER = __import__(
settings.ALTERNATE_OBJECT_SEARCH_MULTIMATCH_PARSER).object_multimatch_parser
except Exception:
from src.objects.object_search_funcs import object_multimatch_parser as IDPARSER
#
# Helper functions for the ObjectManger's search methods
#
def match_list(searchlist, ostring, exact_match=True,
attribute_name=None):
"""
Helper function.
does name/attribute matching through a list of objects.
"""
if not ostring:
return []
if not attribute_name:
attribute_name = "key"
if isinstance(ostring, basestring):
# strings are case-insensitive
ostring = ostring.lower()
if exact_match:
return [prospect for prospect in searchlist
if (hasattr(prospect, attribute_name) and
ostring == str(getattr(prospect, attribute_name)).lower())
or (prospect.has_attribute(attribute_name) and
ostring == str(prospect.get_attribute(attribute_name)).lower())]
else:
return [prospect for prospect in searchlist
if (hasattr(prospect, attribute_name) and
ostring in str(getattr(prospect, attribute_name)).lower())
or (prospect.has_attribute(attribute_name) and
ostring in str(prospect.get_attribute(attribute_name)).lower())]
else:
# If it's not a string, we don't convert to lowercase. This is also
# always treated as an exact match.
return [prospect for prospect in searchlist
if (hasattr(prospect, attribute_name) and
ostring == getattr(prospect, attribute_name))
or (prospect.has_attribute(attribute_name)
and ostring == prospect.get_attribute(attribute_name))]
def separable_search(ostring, searchlist,
attribute_name='db_key', exact_match=False):
"""
Searches a list for a object match to ostring or separator+keywords.
This version handles search criteria defined by IDPARSER. By default this
is of the type N-keyword, used to differentiate several objects of the
exact same name, e.g. 1-box, 2-box etc.
ostring: (string) The string to match against.
searchlist: (List of Objects) The objects to perform attribute comparisons on.
attribute_name: (string) attribute name to search.
exact_match: (bool) 'exact' or 'fuzzy' matching.
Note that the fuzzy matching gives precedence to exact matches; so if your
search query matches an object in the list exactly, it will be the only result.
This means that if the list contains [box,box11,box12], the search string 'box'
will only match the first entry since it is exact. The search 'box1' will however
match both box11 and box12 since neither is an exact match.
This method always returns a list, also for a single result.
"""
# Full search - this may return multiple matches.
results = match_list(searchlist, ostring, exact_match, attribute_name)
# Deal with results of search
match_number = None
if not results:
# if we have no match, check if we are dealing
# with a "N-keyword" query, if so, strip it out.
match_number, ostring = IDPARSER(ostring)
if match_number != None and ostring:
# Run the search again, without the match number
results = match_list(searchlist, ostring, exact_match, attribute_name)
elif not exact_match:
# we have results, but are using fuzzy matching; run
# second sweep in results to catch eventual exact matches
# (these are given precedence, so a search for 'ball' in
# ['ball', 'ball2'] will correctly return the first ball
# only).
exact_results = match_list(results, ostring, True, attribute_name)
if exact_results:
results = exact_results
if len(results) > 1 and match_number != None:
# We have multiple matches, but a N-type match number
# is available to separate them.
try:
results = [results[match_number]]
except IndexError:
pass
# this is always a list.
return results
IDPARSER_PATH = getattr(settings, 'ALTERNATE_OBJECT_SEARCH_MULTIMATCH_PARSER', 'src.objects.object_search_funcs')
if not IDPARSER_PATH:
# can happen if variable is set to "" in settings
IDPARSER_PATH = 'src.objects.object_search_funcs'
exec("from %s import object_multimatch_parser as IDPARSER" % IDPARSER_PATH)
class ObjectManager(TypedObjectManager):
"""
@ -127,7 +28,6 @@ class ObjectManager(TypedObjectManager):
# ObjectManager Get methods
#
# user/player related
@returns_typeclass
@ -166,48 +66,88 @@ class ObjectManager(TypedObjectManager):
dbref = player_matches[0].id
# use the id to find the player
return self.get_object_with_user(dbref)
# attr/property related
@returns_typeclass_list
def get_objs_with_attr(self, attribute_name):
def get_objs_with_attr(self, attribute_name, location=None):
"""
Returns all objects having the given attribute_name defined at all.
"""
from src.objects.models import ObjAttribute
return [attr.obj for attr in ObjAttribute.objects.filter(db_key=attribute_name)]
lstring = ""
if location:
lstring = ", db_obj__db_location=location"
attrs = eval("ObjAttribute.objects.filter(db_key=attribute_name%s)" % lstring)
return [attr.obj for attr in attrs]
@returns_typeclass_list
def get_objs_with_attr_match(self, attribute_name, attribute_value):
def get_objs_with_attr_match(self, attribute_name, attribute_value, location=None, exact=False):
"""
Returns all objects having the valid
attrname set to the given value. Note that no conversion is made
to attribute_value, and so it can accept also non-strings.
"""
from src.objects.models import ObjAttribute
return [attr.obj for attr in ObjAttribute.objects.filter(db_key=attribute_name)
if attribute_value == attr.value]
lstring = ""
if location:
lstring = ", db_obj__db_location=location"
attrs = eval("ObjAttribute.objects.filter(db_key=attribute_name%s)" % lstring)
if exact:
return [attr.obj for attr in attrs if attribute_value == attr.value]
else:
return [attr.obj for attr in attrs if str(attribute_value) in str(attr.value)]
@returns_typeclass_list
def get_objs_with_db_property(self, property_name):
def get_objs_with_db_property(self, property_name, location=None):
"""
Returns all objects having a given db field property
Returns all objects having a given db field property.
property_name = search string
location - actual location object to restrict to
"""
return [prospect for prospect in self.all()
if hasattr(prospect, 'db_%s' % property_name)
or hasattr(prospect, property_name)]
lstring = ""
if location:
lstring = ".filter(db_location=location)"
try:
return eval("self.exclude(db_%s=None)%s" % (property_name, lstring))
except exceptions.FieldError:
return []
@returns_typeclass_list
def get_objs_with_db_property_match(self, property_name, property_value):
def get_objs_with_db_property_match(self, property_name, property_value, location, exact=False):
"""
Returns all objects having a given db field property
"""
lstring = ""
if location:
lstring = ", db_location=location"
try:
return eval("self.filter(db_%s=%s)" % (property_name, property_value))
except Exception:
if exact:
return eval("self.filter(db_%s__iexact=property_value%s)" % (property_name, lstring))
else:
return eval("self.filter(db_%s__icontains=property_value%s)" % (property_name, lstring))
except exceptions.FieldError:
return []
@returns_typeclass_list
def get_objs_with_key_or_alias(self, ostring, location, exact=False):
"""
Returns objects based on key or alias match
"""
lstring_key, lstring_alias, estring = "", "", "icontains"
if location:
lstring_key = ", db_location=location"
lstring_alias = ", db_obj__db_location=location"
if exact:
estring = "iexact"
matches = eval("self.filter(db_key__%s=ostring%s)" % (estring, lstring_key))
if not matches:
alias_matches = eval("self.model.alias_set.related.model.objects.filter(db_key__%s=ostring%s)" % (estring, lstring_alias))
matches = [alias.db_obj for alias in alias_matches]
return matches
# main search methods and helper functions
@returns_typeclass_list
@ -220,18 +160,6 @@ class ObjectManager(TypedObjectManager):
if excludeobj:
oquery = oquery.exclude(db_key=excludeobj)
return oquery
@returns_typeclass_list
def alias_list_search(self, ostring, objlist):
"""
Search a list of objects by trying to match their aliases.
"""
matches = []
for obj in (obj for obj in objlist
if hasattr(obj, 'aliases') and
ostring in obj.aliases):
matches.append(obj)
return matches
@returns_typeclass_list
def object_search(self, character, ostring,
@ -254,87 +182,116 @@ class ObjectManager(TypedObjectManager):
location = character.location
# Easiest case - dbref matching (always exact)
dbref = self.dbref(ostring)
if dbref:
dbref_match = self.dbref_search(dbref)
if dbref_match:
return [dbref_match]
# not a dbref. Search by attribute/property.
if not attribute_name:
# If the search string is one of the following, return immediately with
# the appropriate result.
if location and ostring == 'here':
return [location]
if character and ostring in ['me', 'self']:
return [character]
if character and ostring in ['*me', '*self']:
return [character.player]
attribute_name = 'key'
# Test some common self-references
if location and ostring == 'here':
return [location]
if character and ostring in ['me', 'self']:
return [character]
if character and ostring in ['*me', '*self']:
return [character.player]
# Test if we are looking for a player object
if str(ostring).startswith("*"):
# Player search - try to find obj by its player's name
player_string = ostring.lstrip("*")
player_match = self.get_object_with_player(player_string)
player_match = self.get_object_with_player(ostring)
if player_match is not None:
return [player_match.player]
# find suitable objects
if global_search or not location:
# search all objects in database
objlist = self.get_objs_with_db_property(attribute_name)
if not objlist:
objlist = self.get_objs_with_attr(attribute_name)
else:
# local search
objlist = character.contents
objlist.extend(location.contents)
objlist.append(location) #easy to forget!
if not objlist:
return []
# Search for keys, aliases or other attributes
search_locations = [None] # this means a global search
if not global_search and location:
# Test if we are referring to the current room
if location and (ostring.lower() == location.key.lower()
or ostring.lower() in [alias.lower() for alias in location.aliases]):
return [location]
# otherwise, setup the locations to search in
search_locations = [character, location]
# do the search on the found objects
matches = separable_search(ostring, objlist,
attribute_name, exact_match=False)
def local_and_global_search(ostring, exact=False):
"Helper method for searching objects"
matches = []
for location in search_locations:
if attribute_name:
# Attribute/property search. First, search for db_<attrname> matches on the model
matches.extend(self.get_objs_with_db_property_match(attribute_name, ostring, location, exact))
if not matches:
# Next, try Attribute matches
matches.extend(self.get_objs_with_attr_match(attribute_name, ostring, location, exact))
else:
# No attribute/property named. Do a normal key/alias-search
matches = self.get_objs_with_key_or_alias(ostring, location, exact)
return matches
if not matches and attribute_name in ('key', 'name'):
# No matches. If we tried to match a key/name field, we also try to
# see if an alias works better.
matches = self.alias_list_search(ostring, objlist)
return matches
# Search through all possibilities.
match_number = None
matches = local_and_global_search(ostring, exact=True)
if not matches:
# if we have no match, check if we are dealing with an "N-keyword" query - if so, strip it.
match_number, ostring = IDPARSER(ostring)
if match_number != None and ostring:
# Run search again, without match number:
matches = local_and_global_search(ostring, exact=True)
if ostring and (len(matches) > 1 or not matches):
# Already multimatch or no matches. Run a fuzzy matching.
matches = local_and_global_search(ostring, exact=False)
elif len(matches) > 1:
# multiple matches already. Run a fuzzy search. This catches partial matches (suggestions)
matches = local_and_global_search(ostring, exact=False)
# deal with the result
if len(matches) > 1 and match_number != None:
# We have multiple matches, but a N-type match number is available to separate them.
try:
matches = [matches[match_number]]
except IndexError:
pass
# This is always a list.
return matches
#
# ObjectManager Copy method
#
def copy_object(self, original_object, new_name=None,
new_location=None, new_home=None, aliases=None):
new_location=None, new_home=None, new_aliases=None):
"""
Create and return a new object as a copy of the source object. All will
be identical to the original except for the dbref and the 'user' field
which will be set to None.
be identical to the original except for the arguments given specifically
to this method.
original_object (obj) - the object to make a copy from
new_name (str) - name the copy differently from the original.
new_location (obj) - if None, we create the new object in the same place as the old one.
new_location (obj) - if not None, change the location
new_home (obj) - if not None, change the Home
new_aliases (list of strings) - if not None, change object aliases.
"""
# get all the object's stats
name = original_object.key
if new_name:
name = new_name
typeclass_path = original_object.typeclass_path
if not new_name:
new_name = original_object.key
if not new_location:
new_location = original_object.location
if not new_home:
new_home = original_object.new_home
if not new_aliases:
new_aliases = original_object.aliases
# create new object
from src import create
new_object = create.create_object(name, typeclass_path, new_location,
new_home, user=None, aliases=None)
new_object = create.create_object(new_name, typeclass_path, new_location,
new_home, user=None, aliases=new_aliases)
if not new_object:
return None

View file

@ -20,6 +20,7 @@ except ImportError:
from django.db import models
from django.conf import settings
from src.utils.idmapper.models import SharedMemoryModel
from src.typeclasses.models import Attribute, TypedObject
from src.typeclasses.typeclass import TypeClass
from src.objects.manager import ObjectManager
@ -52,7 +53,27 @@ class ObjAttribute(Attribute):
verbose_name = "Object Attribute"
verbose_name_plural = "Object Attributes"
#------------------------------------------------------------
#
# Alias
#
#------------------------------------------------------------
class Alias(SharedMemoryModel):
"""
This model holds a range of alternate names for an object.
These are intrinsic properties of the object. The split
is so as to allow for effective global searches also by
alias.
"""
db_key = models.CharField(max_length=255)
db_obj = models.ForeignKey("ObjectDB")
class Meta:
"Define Django meta options"
verbose_name = "Object alias"
verbose_name_plural = "Object aliases"
#------------------------------------------------------------
#
# ObjectDB
@ -108,7 +129,7 @@ class ObjectDB(TypedObject):
# comma-separated list of alias-names of this object. Note that default
# searches only search aliases in the same location as caller.
db_aliases = models.CharField(max_length=255, blank=True)
db_aliases = models.ForeignKey(Alias, db_index=True, blank=True, null=True)
# If this is a character object, the player is connected here.
db_player = models.ForeignKey("players.PlayerDB", blank=True, null=True)
# The location in the game world. Since this one is likely
@ -138,20 +159,20 @@ class ObjectDB(TypedObject):
#@property
def aliases_get(self):
"Getter. Allows for value = self.aliases"
if self.db_aliases:
return [alias for alias in self.db_aliases.split(',')]
return []
return [alias.db_key for alias in Alias.objects.filter(db_obj=self)]
#@aliases.setter
def aliases_set(self, aliases):
"Setter. Allows for self.aliases = value"
"Setter. Allows for self.aliases = value"
if not is_iter(aliases):
aliases = str(aliases).split(',')
self.db_aliases = ",".join([alias.strip() for alias in aliases])
self.save()
aliases = [aliases]
for alias in aliases:
new_alias = Alias(db_key=alias, db_obj=self)
new_alias.save()
#@aliases.deleter
def aliases_del(self):
"Deleter. Allows for del self.aliases"
self.db_aliases = ""
for alias in Alias.objects.filter(db_obj=self):
alias.delete()
aliases = property(aliases_get, aliases_set, aliases_del)
# player property (wraps db_player)
@ -450,8 +471,8 @@ class ObjectDB(TypedObject):
break
results = ObjectDB.objects.object_search(self, ostring,
global_search,
attribute_name)
global_search=global_search,
attribute_name=attribute_name)
if ignore_errors:
return results