Initial check-in, after turning into a standalone.
This commit is contained in:
commit
59f61fcbab
|
@ -0,0 +1,10 @@
|
|||
*.pyc
|
||||
*.rej
|
||||
*.orig
|
||||
*.pyo
|
||||
*#
|
||||
.#*
|
||||
.DS_Store
|
||||
*~
|
||||
*.xcf
|
||||
build/
|
|
@ -0,0 +1,19 @@
|
|||
The MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE AND DATA IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,103 @@
|
|||
django-mysqlfulltextsearch
|
||||
==============================
|
||||
|
||||
django-mysqlfulltextsearch is a simple plug-in for Django that
|
||||
provides access to MySQL's FULLTEXT INDEX feature available in MySQL
|
||||
5.0 and up.
|
||||
|
||||
Although Django supports the "search" lookup in QuerySet filters
|
||||
[http://docs.djangoproject.com/en/dev/ref/models/querysets/#search],
|
||||
the docs specify that you must create the fulltext index yourself.
|
||||
This variant, inspired by code from a blog entry by Andrew Durdin
|
||||
[http://www.mercurytide.co.uk/news/article/django-full-text-search/],
|
||||
includes a return value "relevance," which is the score MySQL awards
|
||||
to each row returned. This is a win for small sites that do not need
|
||||
a heavyweight search solution such as Lucene or Xapian. ("relevance"
|
||||
is supposed to be a configurable dynamic field name, but I haven't
|
||||
provided a reliable path to change it yet.)
|
||||
|
||||
Along with the updated API, this code provides for index discovery.
|
||||
If a table has exactly one fulltext index, you can create a
|
||||
SearchManager without declaring any fields at all, and it will
|
||||
auto-discover the index on its own. If you specify a tuple of search
|
||||
fields for which no corresponding index exists, the returned exception
|
||||
will include a list of valid indices.
|
||||
|
||||
|
||||
|
||||
Standard Usage:
|
||||
---------------
|
||||
|
||||
Create the index. For the model "book" in the app "Books":
|
||||
|
||||
./manage dbshell
|
||||
> CREATE FULLTEXT INDEX book_title on books_book (title, summary)
|
||||
|
||||
Or via South:
|
||||
|
||||
def forwards(self, orm):
|
||||
db.execute('CREATE FULLTEXT INDEX book_text_index ON books_book (title, summary)')
|
||||
|
||||
Using the index:
|
||||
|
||||
from mysqlfulltextsearch import SearchManager
|
||||
class Books:
|
||||
...
|
||||
objects = SearchManager()
|
||||
|
||||
books = Book.objects.search('The Metamorphosis', ('title', 'summary')).order_by('-relevance')
|
||||
|
||||
> books[0].title
|
||||
"The Metamorphosis"
|
||||
> books[0].author
|
||||
"Franz Kafka"
|
||||
> books[0].relevance
|
||||
9.4
|
||||
|
||||
If there is only one index for the table, the fields do not need to be
|
||||
specified, the SearchQuerySet object can find it automatically:
|
||||
|
||||
from mysqlfulltextsearch import SearchManager
|
||||
class Books:
|
||||
...
|
||||
objects = SearchManager()
|
||||
|
||||
books = Book.objects.search('The Metamorphosis').order_by('-relevance')
|
||||
|
||||
|
||||
|
||||
Tips:
|
||||
-----
|
||||
Generating the index is a relatively heavyweight process. When you
|
||||
have a few thousand documents, it might be best to load them first,
|
||||
then generate the index afterward.
|
||||
|
||||
|
||||
|
||||
To Do:
|
||||
-----
|
||||
|
||||
-- Easy
|
||||
|
||||
Make the "relevance" dynamic field name configurable.
|
||||
|
||||
|
||||
-- Moderate
|
||||
|
||||
Provide means for matching against BOOLEAN, NATURAL LANGUAGE, and
|
||||
QUERY EXPANSION modes. (Preliminary experiments with this revealed
|
||||
some... interesting... problems with parameter quotation.)
|
||||
|
||||
|
||||
-- Difficult
|
||||
|
||||
Provide means for using a SearchManager to access indices on joined
|
||||
tables, for example:
|
||||
|
||||
Author.objects.search("The Metamorphosis", "book__title")
|
||||
|
||||
-- Insane
|
||||
|
||||
Provide for a way to have FULLTEXT search indices specified in a
|
||||
model's Meta class, and have syncdb or south pick up that information
|
||||
and do the right thing with it.
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2010 by Elf M. Sternberg. All rights not expressly granted
|
||||
# herein are reserved.
|
||||
#
|
||||
# Created in the United States of America.
|
||||
#
|
||||
# This digital media is protected by U.S. and international copyright
|
||||
# and intellectual property laws. Unless otherwise specified, all
|
||||
# information and screens appearing as part of this digital medium,
|
||||
# including software, services, documents, text, images, icons, and
|
||||
# logos design; the selection, assembly, arrangement, and design
|
||||
# thereof; and the code that enables its presentation, are the sole
|
||||
# property of Elf M. Sternberg.
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE AND DATA IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
|
@ -0,0 +1 @@
|
|||
# Create your models here.
|
|
@ -0,0 +1,133 @@
|
|||
from functools import wraps
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db import models, backends
|
||||
from django.db import connection
|
||||
from MySQLdb import OperationalError
|
||||
from MySQLdb.constants.ER import FT_MATCHING_KEY_NOT_FOUND
|
||||
|
||||
|
||||
def _get_indices(model):
|
||||
""" Return all of the FULLTEXT indices available for a given
|
||||
Django model."""
|
||||
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('show index from %s where index_type = "FULLTEXT"' %
|
||||
connection.ops.quote_name(model._meta.db_table))
|
||||
found = {}
|
||||
item = cursor.fetchone()
|
||||
while item:
|
||||
if not item:
|
||||
break
|
||||
(model_name, key_name, column_name) = (item[0], item[2], item[4])
|
||||
if not found.has_key(key_name):
|
||||
found[key_name] = []
|
||||
found[key_name].append(column_name)
|
||||
item = cursor.fetchone()
|
||||
|
||||
return found.values()
|
||||
|
||||
|
||||
def _handle_oper(f):
|
||||
""" Specialized wrapper for methods of SearchQuerySet that will
|
||||
inform the user of what indices are available, should the user
|
||||
specify a list of fields on which to search."""
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return f(self, *args, **kwargs)
|
||||
except OperationalError, e:
|
||||
if e.args[0] != FT_MATCHING_KEY_NOT_FOUND:
|
||||
raise
|
||||
|
||||
idc = _get_indices(self.model)
|
||||
message = "No FULLTEXT indices found for this table."
|
||||
if len(idc) > 0:
|
||||
message = ("Index not found. Indices available include: %s" %
|
||||
str(tuple(idc)))
|
||||
raise FieldError, message
|
||||
|
||||
return wraps(f)(wrapper)
|
||||
|
||||
|
||||
class SearchQuerySet(models.query.QuerySet):
|
||||
""" A QuerySet with a new method, search, and wrappers around the
|
||||
most common operations performed on a query set."""
|
||||
|
||||
def __init__(self, model = None, query = None, using = None,
|
||||
aggregate_field_name = 'relevance'):
|
||||
|
||||
super(SearchQuerySet, self).__init__(model, query, using)
|
||||
self._aggregate_field_name = aggregate_field_name
|
||||
|
||||
|
||||
def search(self, query, fields):
|
||||
meta = self.model._meta
|
||||
|
||||
if not fields:
|
||||
found = _get_indices(self.model)
|
||||
if len(found) != 1:
|
||||
raise FieldError, "More than one index found for this table."
|
||||
fields = found[0]
|
||||
|
||||
columns = [meta.get_field(name, many_to_many=False).column
|
||||
for name in fields]
|
||||
full_names = ["%s.%s" % (connection.ops.quote_name(meta.db_table),
|
||||
connection.ops.quote_name(column))
|
||||
for column in columns]
|
||||
match_expr = "MATCH(%s) AGAINST (%%s)" % (", ".join(full_names))
|
||||
|
||||
return self.extra(select={self._aggregate_field_name: match_expr},
|
||||
where=[match_expr],
|
||||
params=[query],
|
||||
select_params = [query])
|
||||
|
||||
|
||||
# Python Magic Methods wrapped to provide useful information on exception.
|
||||
|
||||
def __repr__(self):
|
||||
return super(SearchQuerySet, self).__repr__()
|
||||
__repr__ = _handle_oper(__repr__)
|
||||
|
||||
|
||||
def __len__(self):
|
||||
return super(SearchQuerySet, self).__len__()
|
||||
__len__ = _handle_oper(__len__)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return super(SearchQuerySet, self).__iter__()
|
||||
__iter__ = _handle_oper(__iter__)
|
||||
|
||||
|
||||
def _result_iter(self):
|
||||
return super(SearchQuerySet, self)._result_iter()
|
||||
_result_iter = _handle_oper(_result_iter)
|
||||
|
||||
|
||||
def __nonzero__(self):
|
||||
return super(SearchQuerySet, self).__nonzero__()
|
||||
__nonzero__ = _handle_oper(__nonzero__)
|
||||
|
||||
|
||||
def __getitem__(self, k):
|
||||
return super(SearchQuerySet, self).__getitem__(k)
|
||||
__getitem__ = _handle_oper(__getitem__)
|
||||
|
||||
|
||||
# This is a private method of QuerySet. It's not guaranteed to even exist
|
||||
# after Django 1.2
|
||||
|
||||
def _fill_cache(self, *args, **kwargs):
|
||||
return super(SearchQuerySet, self)._fill_cache(*args, **kwargs)
|
||||
_fill_cache = _handle_oper(_fill_cache)
|
||||
|
||||
|
||||
|
||||
class SearchManager(models.Manager):
|
||||
|
||||
def get_query_set(self, fields = []):
|
||||
return SearchQuerySet(self.model)
|
||||
|
||||
|
||||
def search(self, query, fields = []):
|
||||
return self.get_query_set().search(query, fields)
|
|
@ -0,0 +1,23 @@
|
|||
"""
|
||||
This file demonstrates two different styles of tests (one doctest and one
|
||||
unittest). These will both pass when you run "manage.py test".
|
||||
|
||||
Replace these with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.failUnlessEqual(1 + 1, 2)
|
||||
|
||||
__test__ = {"doctest": """
|
||||
Another way to test that 1 + 1 is equal to 2.
|
||||
|
||||
>>> 1 + 1 == 2
|
||||
True
|
||||
"""}
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Create your views here.
|
|
@ -0,0 +1,2 @@
|
|||
Django==1.2
|
||||
MySQL>=5.0
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup (
|
||||
name='django-mysqlfulltextsearch',
|
||||
version='0.1',
|
||||
description='A full-text search app for Django and MySQL',
|
||||
author='Elf M. Sternberg',
|
||||
author_email='elf.sternberg@gmail.com',
|
||||
url='http://github.com/elfsternberg/django-mysqlfulltextsearch/',
|
||||
license='MIT License',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Plugins',
|
||||
'Framework :: Django',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
],
|
||||
packages=find_packages(),
|
||||
)
|
Loading…
Reference in New Issue