├── .bumpversion.cfg ├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── algorithms.rst ├── autodoc.sh ├── changelog.rst ├── conf.py ├── index.rst ├── large_datasets.rst ├── models.rst ├── modules.rst ├── providers.rst ├── quickstart.rst ├── recommends.algorithms.rst ├── recommends.management.commands.rst ├── recommends.management.rst ├── recommends.providers.rst ├── recommends.rst ├── recommends.storages.djangoorm.rst ├── recommends.storages.mongodb.rst ├── recommends.storages.redis.rst ├── recommends.storages.rst ├── recommends.templatetags.rst ├── recommends.tests.rst ├── settings.rst ├── signals.rst ├── storages.rst └── templatetags.rst ├── recommends ├── __init__.py ├── algorithms │ ├── __init__.py │ ├── base.py │ ├── ghetto.py │ ├── naive.py │ └── pyrecsys.py ├── apps.py ├── converters.py ├── fixtures │ └── products.json ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── recommends_precompute.py ├── managers.py ├── models.py ├── providers │ └── __init__.py ├── settings.py ├── similarities.py ├── storages │ ├── __init__.py │ ├── base.py │ ├── djangoorm │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── managers.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20141013_2311.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── routers.py │ │ ├── settings.py │ │ ├── south_migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto__chg_field_similarity_related_object_ctype__add_index_similarity_.py │ │ │ └── __init__.py │ │ └── storage.py │ ├── mongodb │ │ ├── __init__.py │ │ ├── managers.py │ │ ├── settings.py │ │ └── storage.py │ └── redis │ │ ├── __init__.py │ │ ├── managers.py │ │ ├── settings.py │ │ └── storage.py ├── tasks.py ├── templatetags │ ├── __init__.py │ └── recommends.py ├── tests │ ├── __init__.py │ ├── admin.py │ ├── models.py │ ├── recommendations.py │ ├── runtests.py │ ├── settings.py │ ├── templates │ │ ├── base.html │ │ ├── home.html │ │ └── recommends │ │ │ └── recproduct_detail.html │ ├── tests.py │ ├── urls.py │ └── views.py └── utils.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements-py2.7.txt ├── test-requirements-py3.3.txt ├── test-requirements-py3.4.txt ├── test-requirements-py3.5.txt └── test-requirements.txt /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.4.0 3 | files = setup.py recommends/__init__.py 4 | commit = True 5 | tag = True 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | *.swp 4 | *.pyc 5 | *.patch 6 | ._* 7 | .DS_Store 8 | .project 9 | build 10 | dist 11 | docs/_build 12 | *.egg-info 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | services: 3 | - redis-server 4 | - mongodb 5 | - rabbitmq 6 | before_install: 7 | - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh 8 | - chmod +x miniconda.sh 9 | - ./miniconda.sh -b 10 | - export PATH=/home/travis/miniconda2/bin:$PATH 11 | - conda update --yes conda 12 | # The next couple lines fix a crash with multiprocessing on Travis and are not specific to using Miniconda 13 | - sudo rm -rf /dev/shm 14 | - sudo ln -s /run/shm /dev/shm 15 | install: 16 | - conda update --yes conda 17 | - conda create --yes -n condaenv python=$TRAVIS_PYTHON_VERSION 18 | - conda install --yes -n condaenv pip 19 | - source activate condaenv 20 | - conda install --yes python=$TRAVIS_PYTHON_VERSION numpy scipy 21 | - pip install --quiet --upgrade pip 22 | - pip install -r test-requirements-py$TRAVIS_PYTHON_VERSION.txt 23 | - pip install . 24 | - pip install -q $DJANGO 25 | before_script: 26 | - "export PYTHONPATH=$TRAVIS_BUILD_DIR:$PYTHONPATH;" 27 | - "django-admin.py version;" 28 | - "flake8 --exclude=docs,south_migrations,tests,tasks --ignore=E501,E123,E124,E126,E127,E128 --statistics .;" 29 | python: 30 | - "2.7" 31 | - "3.3" 32 | - "3.4" 33 | - "3.5" 34 | env: 35 | - DJANGO="Django>=1.8,<1.9" 36 | - DJANGO="Django>=1.9,<1.10" 37 | - DJANGO="Django>=1.10,<1.11" 38 | matrix: 39 | exclude: 40 | - python: "3.3" 41 | env: DJANGO="Django>=1.10,<1.11" 42 | - python: "3.3" 43 | env: DJANGO="Django>=1.9,<1.10" 44 | 45 | script: python recommends/tests/runtests.py 46 | after_failure: 47 | - env 48 | - cat $PYTHONPATH 49 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | django-recommends is written and maintained by Flavio Curella and 2 | various contributors: 3 | 4 | Development Lead 5 | ```````````````` 6 | 7 | - Flavio Curella 8 | 9 | 10 | Patches and Suggestions 11 | ``````````````````````` 12 | 13 | - Andrii Kostenko 14 | - Ilya Baryshev 15 | - Maxim Gurets 16 | - Kirill Zaitsev 17 | - Wang GaoXiang 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Flavio Curella 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | recursive-include tests *.py -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-recommends 2 | ====================================== 3 | 4 | .. image:: https://travis-ci.org/fcurella/django-recommends.png?branch=master 5 | :target: https://travis-ci.org/fcurella/django-recommends 6 | 7 | A django app that builds item-based suggestions for users. 8 | 9 | Documentation on `readthedocs `_. 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-recommends.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-recommends.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-recommends" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-recommends" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/algorithms.rst: -------------------------------------------------------------------------------- 1 | .. ref-algorithms: 2 | 3 | Recommendation Algorithms 4 | ========================= 5 | 6 | A Recommendation Algorithm is a subclass of ``recommends.algorithms.base.BaseAlgorithm`` that implements methods for calculating similarities and recommendations. 7 | 8 | Subclasses must implement this methods: 9 | 10 | * ``calculate_similarities(self, vote_list)`` 11 | 12 | Must return an dict of similarities for every object: 13 | 14 | Accepts a list of votes with the following schema: 15 | 16 | :: 17 | 18 | [ 19 | ("", "", ), 20 | ("", "", ), 21 | ] 22 | 23 | Output must be a dictionary with the following schema: 24 | 25 | :: 26 | 27 | [ 28 | ("", [ 29 | (, ), 30 | (, ), 31 | ]), 32 | ("", [ 33 | (, ), 34 | (, ), 35 | ]), 36 | ] 37 | 38 | 39 | 40 | * ``calculate_recommendations(self, vote_list, itemMatch)`` 41 | 42 | Returns a list of recommendations: 43 | 44 | :: 45 | 46 | [ 47 | (, [ 48 | ("", ), 49 | ("", ), 50 | ]), 51 | (, [ 52 | ("", ), 53 | ("", ), 54 | ]), 55 | ] 56 | 57 | NaiveAlgorithm 58 | -------------- 59 | 60 | This class implement a basic algorithm (adapted from: Segaran, T: Programming Collective Intelligence) that doesn't require any dependency at the expenses of performances. 61 | 62 | Properties 63 | ~~~~~~~~~~ 64 | 65 | * ``similarity`` 66 | 67 | A callable that determines the similiarity between two elements. 68 | 69 | Functions for Euclidean Distance and Pearson Correlation are provided for convenience at ``recommends.similarities.sim_distance`` and ``recommends.similarities.sim_pearson``. 70 | 71 | Defaults to ``recommends.similarities.sim_distance`` 72 | 73 | RecSysAlgorithm 74 | ---------------- 75 | 76 | This class implement a SVD algorithm. Requires ``python-recsys`` (available at https://github.com/ocelma/python-recsys). 77 | 78 | ``python-recsys`` in turn requires ``SciPy``, ``NumPy``, and other python libraries. 79 | -------------------------------------------------------------------------------- /docs/autodoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sphinx-apidoc -f -o . ../recommends 4 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. ref-changelog: 2 | 3 | Changelog 4 | ========= 5 | * v0.4.0 6 | * Drop support for Django 1.7. 7 | * Add support for Django 1.10. 8 | * v0.3.11 9 | * Start deprecating ``GhettoAlgorithm`` in favor of ``NaiveAlgorithm``. 10 | * v0.3.1 11 | * Fix wrong import 12 | * v0.3.0 13 | * Added support for Django 1.9. 14 | * v0.2.2 15 | * Added Python 3.3 Trove classifier to `setup.py`. 16 | * v0.2.1 17 | * Added Python 3.4 Trove classifier to `setup.py`. 18 | * v0.2.0 19 | * Added support for Python 3.4 20 | * Dropped support for Celery 2.x 21 | * v0.1.0 22 | * Django 1.8 compatibility. Removed support for Django 1.6. 23 | * Added Providers autodiscovery. 24 | * v0.0.22 25 | * Django 1.7 compatibility. Thanks Ilya Baryshev. 26 | * v0.0.21 27 | * Release lock even if an exception is raised. 28 | * v0.0.20 29 | * Removed lock expiration in Redis Storage. 30 | * v0.0.19 31 | * added storages locking. Thanks Kirill Zaitsev. 32 | * v0.0.16 33 | * renamed ``--verbose`` option to ``--verbosity``. 34 | * The ``recommends_precompute`` method is available even with ``RECOMMENDS_TASK_RUN = False``. 35 | * v0.0.15 36 | * added ``--verbose`` option to ``recommends_precompute`` command. 37 | * v0.0.14 38 | * more verbose ``recommends_precompute`` command. Thanks WANG GAOXIANG. 39 | * Introduced ``raw_id` parameter for lighter queries. WANG GAOXIANG. 40 | * Introduced ``RECOMMENDS_STORAGE_MONGODB_FSYNC`` setting. 41 | * v0.0.13 42 | * Use ``{}`` instead of ``dict()`` for better performance. 43 | * v0.0.12 44 | * python 3.3 and Django 1.5 compatibility 45 | * v0.0.11 46 | * ``get_rating_site`` provider method now defaults to ``settings.SITE_ID`` instead of ``None``. 47 | * ``similarities`` templatetag result is now cached per object 48 | * fixed tests if ``recommends_precompute`` is None. 49 | * explicitly named celery tasks. 50 | * v0.0.10 51 | * Added ``RecSysAlgorithm``. 52 | * v0.0.9 53 | * Now tests can run in app's ./manage.py test. Thanks Andrii Kostenko. 54 | * Added support for ignored user recommendation. Thanks Maxim Gurets. 55 | * v0.0.8 56 | * Added ``threshold_similarities`` and ``threshold_recommnedations`` to the storage backends. 57 | * v0.0.7 58 | * added Mongodb storage 59 | * added Redis storage 60 | * added ``unregister`` method to the registry 61 | * v0.0.6 62 | * added logging 63 | * DjangoOrmStorage now saves Similarities and Suggestions in batches, according to the new ``RECOMMENDS_STORAGE_COMMIT_THRESHOLD`` setting. 64 | * Decoupled Algorithms from Providers 65 | * v0.0.5 66 | * Refactored providers registry 67 | * Renamed recommends.storages.django to recommends.storages.djangoorm to avoid name conflicts 68 | * Refactored DjangoOrmStorage and moved it to recommends.storages.djangoorm.storage 69 | * Added optional database router 70 | * v0.0.4 71 | * Refactored providers to use lists of votes instead of dictionaries 72 | * fixed a critical bug where we ere calling the wrong method with the wrong signature. 73 | * v0.0.3 74 | * Added filelocking to the pre-shipped precomputing task 75 | * Refactored signal handling, and added a task to remove similarities on pre_delete 76 | * Added optional hooks for storing and retrieving the vote matrix 77 | * v0.0.2 78 | * Added the ``RECOMMENDS_TASK_RUN`` setting 79 | * v0.0.1 80 | * Initial Release 81 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | # -*- coding: utf-8 -*- 3 | # 4 | # django-recommends documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Nov 21 12:16:20 2011. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.dirname(os.path.abspath('.'))) 22 | sys.path.insert( 23 | 0, 24 | os.path.join(os.path.dirname(os.path.abspath('.')), 25 | 'example_project')) 26 | os.environ['DJANGO_SETTINGS_MODULE'] = 'example_project.settings' 27 | 28 | # -- General configuration ----------------------------------------------- 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be extensions 34 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.todo', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.viewcode'] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix of source filenames. 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | # source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = 'django-recommends' 55 | copyright = '2011, Flavio Curella' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | from recommends import VERSION 63 | 64 | version = '.'.join([str(n) for n in VERSION[:2]]) 65 | # The full version, including alpha/beta/rc tags. 66 | release = '.'.join([str(n) for n in VERSION]) 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | # today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | # today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = ['_build'] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all documents. 83 | # default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | # add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | # add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | # show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | # modindex_common_prefix = [] 101 | 102 | 103 | # -- Options for HTML output --------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | html_theme = 'default' 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | # html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | # html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | # html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | # html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | # html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | # html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | # html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | # html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | # html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | # html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | # html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | # html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | # html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | # html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | # html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | # html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | # html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | # html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = 'django-recommendsdoc' 181 | 182 | 183 | # -- Options for LaTeX output -------------------------------------------- 184 | 185 | latex_elements = { 186 | # The paper size ('letterpaper' or 'a4paper'). 187 | #'papersize': 'letterpaper', 188 | 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | #'pointsize': '10pt', 191 | 192 | # Additional stuff for the LaTeX preamble. 193 | #'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, author, documentclass [howto/manual]). 198 | latex_documents = [ 199 | ('index', 'django-recommends.tex', 'django-recommends Documentation', 200 | 'Flavio Curella', 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | # latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | # latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | # latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | # latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | # latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | # latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output -------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'django-recommends', u'django-recommends Documentation', 230 | ['Flavio Curella'], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | # man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ------------------------------------------ 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'django-recommends', 'django-recommends Documentation', 244 | 'Flavio Curella', 'django-recommends', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | # texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | # texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | # texinfo_show_urls = 'footnote' 256 | 257 | 258 | # -- Options for Epub output --------------------------------------------- 259 | 260 | # Bibliographic Dublin Core info. 261 | epub_title = 'django-recommends' 262 | epub_author = 'Flavio Curella' 263 | epub_publisher = 'Flavio Curella' 264 | epub_copyright = '2011, Flavio Curella' 265 | 266 | # The language of the text. It defaults to the language option 267 | # or en if the language is not set. 268 | # epub_language = '' 269 | 270 | # The scheme of the identifier. Typical schemes are ISBN or URL. 271 | # epub_scheme = '' 272 | 273 | # The unique identifier of the text. This can be a ISBN number 274 | # or the project homepage. 275 | # epub_identifier = '' 276 | 277 | # A unique identification for the text. 278 | # epub_uid = '' 279 | 280 | # A tuple containing the cover image and cover page html template filenames. 281 | # epub_cover = () 282 | 283 | # HTML files that should be inserted before the pages created by sphinx. 284 | # The format is a list of tuples containing the path and title. 285 | # epub_pre_files = [] 286 | 287 | # HTML files shat should be inserted after the pages created by sphinx. 288 | # The format is a list of tuples containing the path and title. 289 | # epub_post_files = [] 290 | 291 | # A list of files that should not be packed into the epub file. 292 | # epub_exclude_files = [] 293 | 294 | # The depth of the table of contents in toc.ncx. 295 | # epub_tocdepth = 3 296 | 297 | # Allow duplicate toc entries. 298 | # epub_tocdup = True 299 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-recommends documentation master file, created by 2 | sphinx-quickstart on Mon Nov 21 12:16:20 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-recommends's documentation! 7 | ============================================= 8 | 9 | A django app that builds item-based suggestions for users. 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | quickstart 17 | providers 18 | algorithms 19 | models 20 | storages 21 | signals 22 | templatetags 23 | settings 24 | large_datasets 25 | changelog 26 | 27 | 28 | Requirements 29 | ============ 30 | 31 | * Python 2.7, Python 3.3+ 32 | * Django>=1.8 33 | * celery>=3 34 | * django-celery>=2.3.3 35 | 36 | Optional 37 | -------- 38 | * redis 39 | * pymongo 40 | * python-recsys (Python 2.x only) 41 | 42 | Indices and tables 43 | ================== 44 | 45 | * :ref:`genindex` 46 | * :ref:`modindex` 47 | * :ref:`search` 48 | 49 | -------------------------------------------------------------------------------- /docs/large_datasets.rst: -------------------------------------------------------------------------------- 1 | .. ref-large_datasets: 2 | 3 | Large Datasets 4 | ============== 5 | 6 | Calculating item similarities is computationally heavy, in terms of cpu cycles, amount of RAM and database load. 7 | 8 | Some strategy you can use to mitigate it includes: 9 | 10 | * Parallelize the precomputing task. This could be achieved by disabling the default task (via ``RECOMMENDS_TASK_RUN = False``) and breaking it down to smaller tasks (one per app, or one per model), which will be distributed to different machines using dedicated celery queues. 11 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | .. ref-models: 2 | 3 | Models 4 | ====== 5 | 6 | ``Recommends`` uses these classes to represent similarities and recommendations. 7 | These classes don't have be Django Models (ie: tied to a table in a database). All they have to do is implementing the properties descripted below. 8 | 9 | .. class:: Similarity() 10 | 11 | A ``Similarity`` is an object with the fellowing properties: 12 | 13 | .. attribute:: object 14 | 15 | The source object. 16 | 17 | .. attribute:: related_object 18 | 19 | The target object 20 | 21 | .. attribute:: score 22 | 23 | How much the ``related_object`` is similar to ``object``. 24 | 25 | .. class:: Recommendation() 26 | 27 | A ``Recommendation`` is an object with the fellowing properties: 28 | 29 | .. attribute:: object 30 | 31 | The object being suggested to the user. 32 | 33 | .. attribute:: user 34 | 35 | The user we are suggesting ``object`` to. 36 | 37 | .. attribute:: score 38 | 39 | How much the ``user`` is supposed to like ``object``. 40 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | recommends 2 | ========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | recommends 8 | -------------------------------------------------------------------------------- /docs/providers.rst: -------------------------------------------------------------------------------- 1 | .. ref-providers: 2 | 3 | Recommendation Providers 4 | ======================== 5 | 6 | In order to compute and retrieve similarities and recommendations, you must create a ``RecommendationProvider`` and register it with the model that represents the rating and a list of the models that will receive the votes. 7 | 8 | A ``RecommendationProvider`` is a class that specifies how to retrieve various informations (items, users, votes) necessary for computing recommendation and similarities for a set of objects. 9 | 10 | Subclasses override properties amd methods in order to determine what constitutes rated items, a rating, its score, and user. 11 | 12 | The algorithm to use for computing is specified by the ``algorithm`` property. 13 | 14 | A basic algorithm class is provided for convenience at ``recommends.algorithms.naive.NaiveAlgorithm``, but users can implement their own solutions. See :doc:`algorithms`. 15 | 16 | Example:: 17 | 18 | # models.py 19 | from __future__ import unicode_literals 20 | from django.db import models 21 | from django.contrib.auth.models import User 22 | from django.contrib.sites.models import Site 23 | from django.utils.encoding import python_2_unicode_compatible 24 | 25 | 26 | @python_2_unicode_compatible 27 | class Product(models.Model): 28 | """A generic Product""" 29 | name = models.CharField(blank=True, max_length=100) 30 | sites = models.ManyToManyField(Site) 31 | 32 | def __str__(self): 33 | return self.name 34 | 35 | @models.permalink 36 | def get_absolute_url(self): 37 | return ('product_detail', [self.id]) 38 | 39 | def sites_str(self): 40 | return ', '.join([s.name for s in self.sites.all()]) 41 | sites_str.short_description = 'sites' 42 | 43 | 44 | @python_2_unicode_compatible 45 | class Vote(models.Model): 46 | """A Vote on a Product""" 47 | user = models.ForeignKey(User, related_name='votes') 48 | product = models.ForeignKey(Product) 49 | site = models.ForeignKey(Site) 50 | score = models.FloatField() 51 | 52 | def __str__(self): 53 | return "Vote" 54 | 55 | 56 | Create a file called ``recommendations.py`` inside your app:: 57 | 58 | # recommendations.py 59 | 60 | from django.contrib.auth.models import User 61 | from recommends.providers import RecommendationProvider 62 | from recommends.providers import recommendation_registry 63 | 64 | from .models import Product, Vote 65 | 66 | class ProductRecommendationProvider(RecommendationProvider): 67 | def get_users(self): 68 | return User.objects.filter(is_active=True, votes__isnull=False).distinct() 69 | 70 | def get_items(self): 71 | return Product.objects.all() 72 | 73 | def get_ratings(self, obj): 74 | return Vote.objects.filter(product=obj) 75 | 76 | def get_rating_score(self, rating): 77 | return rating.score 78 | 79 | def get_rating_site(self, rating): 80 | return rating.site 81 | 82 | def get_rating_user(self, rating): 83 | return rating.user 84 | 85 | def get_rating_item(self, rating): 86 | return rating.product 87 | 88 | recommendation_registry.register(Vote, [Product], ProductRecommendationProvider) 89 | 90 | All files called ``recommendations.py`` will be autodiscovered and loaded by 91 | ``django-recommends``. You can change the default module name, or disable 92 | autodiscovery by tweaking the ``RECOMMENDS_AUTODISCOVER_MODULE`` setting (see 93 | :doc:`settings`), or you could manually import your module in your app's 94 | ``AppConfig.ready``:: 95 | 96 | # apps.py 97 | 98 | from django.apps import AppConfig 99 | 100 | 101 | class MyAppConfig(AppConfig): 102 | name = 'my_app' 103 | 104 | def ready(self): 105 | from .myrecs import * 106 | 107 | Properties 108 | ---------- 109 | * ``signals`` 110 | 111 | This property define to which signals the provider should listen to. 112 | A method of the same name will be called on the provider when the 113 | corresponding signal is fired from one of the rated model. 114 | 115 | See :doc:`signals`. 116 | 117 | Defaults to ``['django.db.models.pre_delete']`` 118 | 119 | * ``algorithm`` 120 | 121 | Defaults to ``recommends.algorithms.naive.NaiveAlgorithm`` 122 | 123 | Methods 124 | ------- 125 | 126 | * ``get_items(self)`` 127 | 128 | This method must return items that have been voted. 129 | 130 | * ``items_ignored(self)`` 131 | 132 | Returns user ignored items. 133 | User can delete items from the list of recommended. 134 | 135 | See recommends.converters.IdentifierManager.get_identifier for help. 136 | 137 | * ``get_ratings(self, obj)`` 138 | 139 | Returns all ratings for given item. 140 | 141 | * ``get_rating_user(self, rating)`` 142 | 143 | Returns the user who performed the rating. 144 | 145 | * ``get_rating_score(self, rating)`` 146 | 147 | Returns the score of the rating. 148 | 149 | * ``get_rating_item(self, rating)`` 150 | 151 | Returns the rated object. 152 | 153 | * ``get_rating_site(self, rating)`` 154 | 155 | Returns the site of the rating. Can be a ``Site`` object or its ID. 156 | 157 | Defaults to ``settings.SITE_ID``. 158 | 159 | * ``is_rating_active(self, rating)`` 160 | 161 | Returns if the rating is active. 162 | 163 | * ``pre_store_similarities(self, itemMatch)`` 164 | 165 | Optional. This method will get called right before passing the similarities to the storage. 166 | 167 | For example, you can override this method to do some stats or visualize the data. 168 | 169 | * ``pre_delete(self, sender, instance, **kwargs)`` 170 | 171 | This function gets called when a signal in ``self.rate_signals`` is 172 | fired from one of the rated models. 173 | 174 | Overriding this method is optional. The default method removes the 175 | suggestions for the deleted objected. 176 | 177 | See :doc:`signals`. 178 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. ref-quickstart: 2 | 3 | Quickstart 4 | ----------- 5 | 1. Install ``django-recommends`` with:: 6 | 7 | $ pip install django-recommends 8 | 9 | 2. Create a RecommendationProvider for your models, and register it in your ``AppConfig`` (see :doc:`providers`) 10 | 11 | 3. Add ``'recommends'`` and ``'recommends.storages.djangoorm'`` to ``INSTALLED_APPS`` 12 | 13 | 4. Run ``syncdb`` 14 | 15 | -------------------------------------------------------------------------------- /docs/recommends.algorithms.rst: -------------------------------------------------------------------------------- 1 | algorithms Package 2 | ================== 3 | 4 | :mod:`base` Module 5 | ------------------ 6 | 7 | .. automodule:: recommends.algorithms.base 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`ghetto` Module 13 | -------------------- 14 | 15 | .. automodule:: recommends.algorithms.ghetto 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`pyrecsys` Module 21 | ---------------------- 22 | 23 | .. automodule:: recommends.algorithms.pyrecsys 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | -------------------------------------------------------------------------------- /docs/recommends.management.commands.rst: -------------------------------------------------------------------------------- 1 | commands Package 2 | ================ 3 | 4 | :mod:`recommends_precompute` Module 5 | ----------------------------------- 6 | 7 | .. automodule:: recommends.management.commands.recommends_precompute 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | -------------------------------------------------------------------------------- /docs/recommends.management.rst: -------------------------------------------------------------------------------- 1 | management Package 2 | ================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | recommends.management.commands 10 | 11 | -------------------------------------------------------------------------------- /docs/recommends.providers.rst: -------------------------------------------------------------------------------- 1 | providers Package 2 | ================= 3 | 4 | :mod:`providers` Package 5 | ------------------------ 6 | 7 | .. automodule:: recommends.providers 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | -------------------------------------------------------------------------------- /docs/recommends.rst: -------------------------------------------------------------------------------- 1 | recommends Package 2 | ================== 3 | 4 | :mod:`recommends` Package 5 | ------------------------- 6 | 7 | .. automodule:: recommends.__init__ 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`converters` Module 13 | ------------------------ 14 | 15 | .. automodule:: recommends.converters 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`managers` Module 21 | ---------------------- 22 | 23 | .. automodule:: recommends.managers 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`models` Module 29 | -------------------- 30 | 31 | .. automodule:: recommends.models 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`settings` Module 37 | ---------------------- 38 | 39 | .. automodule:: recommends.settings 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`similarities` Module 45 | -------------------------- 46 | 47 | .. automodule:: recommends.similarities 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | :mod:`tasks` Module 53 | ------------------- 54 | 55 | .. automodule:: recommends.tasks 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | :mod:`utils` Module 61 | ------------------- 62 | 63 | .. automodule:: recommends.utils 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | Subpackages 69 | ----------- 70 | 71 | .. toctree:: 72 | 73 | recommends.algorithms 74 | recommends.management 75 | recommends.providers 76 | recommends.storages 77 | recommends.templatetags 78 | recommends.tests 79 | 80 | -------------------------------------------------------------------------------- /docs/recommends.storages.djangoorm.rst: -------------------------------------------------------------------------------- 1 | djangoorm Package 2 | ================= 3 | 4 | :mod:`admin` Module 5 | ------------------- 6 | 7 | .. automodule:: recommends.storages.djangoorm.admin 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`managers` Module 13 | ---------------------- 14 | 15 | .. automodule:: recommends.storages.djangoorm.managers 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`models` Module 21 | -------------------- 22 | 23 | .. automodule:: recommends.storages.djangoorm.models 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`routers` Module 29 | --------------------- 30 | 31 | .. automodule:: recommends.storages.djangoorm.routers 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`settings` Module 37 | ---------------------- 38 | 39 | .. automodule:: recommends.storages.djangoorm.settings 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`storage` Module 45 | --------------------- 46 | 47 | .. automodule:: recommends.storages.djangoorm.storage 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | -------------------------------------------------------------------------------- /docs/recommends.storages.mongodb.rst: -------------------------------------------------------------------------------- 1 | mongodb Package 2 | =============== 3 | 4 | :mod:`managers` Module 5 | ---------------------- 6 | 7 | .. automodule:: recommends.storages.mongodb.managers 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`settings` Module 13 | ---------------------- 14 | 15 | .. automodule:: recommends.storages.mongodb.settings 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`storage` Module 21 | --------------------- 22 | 23 | .. automodule:: recommends.storages.mongodb.storage 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | -------------------------------------------------------------------------------- /docs/recommends.storages.redis.rst: -------------------------------------------------------------------------------- 1 | redis Package 2 | ============= 3 | 4 | :mod:`managers` Module 5 | ---------------------- 6 | 7 | .. automodule:: recommends.storages.redis.managers 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`settings` Module 13 | ---------------------- 14 | 15 | .. automodule:: recommends.storages.redis.settings 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`storage` Module 21 | --------------------- 22 | 23 | .. automodule:: recommends.storages.redis.storage 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | -------------------------------------------------------------------------------- /docs/recommends.storages.rst: -------------------------------------------------------------------------------- 1 | storages Package 2 | ================ 3 | 4 | :mod:`base` Module 5 | ------------------ 6 | 7 | .. automodule:: recommends.storages.base 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | Subpackages 13 | ----------- 14 | 15 | .. toctree:: 16 | 17 | recommends.storages.djangoorm 18 | recommends.storages.mongodb 19 | recommends.storages.redis 20 | 21 | -------------------------------------------------------------------------------- /docs/recommends.templatetags.rst: -------------------------------------------------------------------------------- 1 | templatetags Package 2 | ==================== 3 | 4 | :mod:`recommends` Module 5 | ------------------------ 6 | 7 | .. automodule:: recommends.templatetags.recommends 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | -------------------------------------------------------------------------------- /docs/recommends.tests.rst: -------------------------------------------------------------------------------- 1 | tests Package 2 | ============= 3 | 4 | :mod:`tests` Package 5 | -------------------- 6 | 7 | .. automodule:: recommends.tests 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`admin` Module 13 | ------------------- 14 | 15 | .. automodule:: recommends.tests.admin 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`models` Module 21 | -------------------- 22 | 23 | .. automodule:: recommends.tests.models 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`providers` Module 29 | ----------------------- 30 | 31 | .. automodule:: recommends.tests.providers 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`runtests` Module 37 | ---------------------- 38 | 39 | .. automodule:: recommends.tests.runtests 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`settings` Module 45 | ---------------------- 46 | 47 | .. automodule:: recommends.tests.settings 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | :mod:`tests` Module 53 | ------------------- 54 | 55 | .. automodule:: recommends.tests.tests 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | :mod:`urls` Module 61 | ------------------ 62 | 63 | .. automodule:: recommends.tests.urls 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | :mod:`views` Module 69 | ------------------- 70 | 71 | .. automodule:: recommends.tests.views 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | Autodiscovery 5 | ------------- 6 | 7 | By default, ``django-recommends`` will import and load any modules called 8 | ``recommendations`` within your apps. 9 | 10 | You can change the default module name by setting ``RECOMMENDS_AUTODISCOVER_MODULE`` 11 | to the name that you want, or you can disable this behavior by setting it to ``False``. 12 | 13 | Celery Task 14 | ----------- 15 | 16 | Computations are done by a scheduled celery task. 17 | 18 | The task is run every 24 hours by default, but can be overridden by the ``RECOMMENDS_TASK_CRONTAB`` setting:: 19 | 20 | RECOMMENDS_TASK_CRONTAB = {'hour': '*/24'} 21 | 22 | ``RECOMMENDS_TASK_CRONTAB`` must be a dictionary of kwargs acceptable by celery.schedulers.crontab. 23 | 24 | If you don’t want to run this task (maybe because you want to write your own), set ``RECOMMENDS_TASK_RUN = False`` 25 | 26 | Additionally, you can specify an expiration time for the task by using the ``RECOMMENDS_TASK_EXPIRES`` settings, which defaults to ``None``. 27 | 28 | Template tags and filters cache timeout 29 | --------------------------------------- 30 | 31 | RECOMMENDS_CACHE_TEMPLATETAGS_TIMEOUT controls how long template tags and fitlers cache their results. Default is 60 seconds. 32 | 33 | 34 | Storage backend 35 | --------------- 36 | 37 | ``RECOMMENDS_STORAGE_BACKEND`` specifies which :doc:`storages` class to use for storing similarity and recommendations. Defaults to ``'recommends.storages.djangoorm.DjangoOrmStorage'``. Providers can override this settings using the ``storage`` property (see :doc:`providers`). 38 | 39 | Logging 40 | ------- 41 | 42 | ``RECOMMENDS_LOGGER_NAME`` specifies which logger to use. Defaults to ``'recommends'``. 43 | -------------------------------------------------------------------------------- /docs/signals.rst: -------------------------------------------------------------------------------- 1 | Signals 2 | ======= 3 | 4 | When a signal specified in the provider is fired up by the one of the rated models, Django-recommends automaticaly calls a function with the same name. 5 | 6 | You can override this function or connect to a different set of signals on the provider using the `signals` property:: 7 | 8 | from django.db.models.signals import post_save, post_delete 9 | 10 | class MyProvider(DjangoRecommendationProvider): 11 | signals = ['django.db.models.post_save', 'django.db.models.pre_delete'] 12 | 13 | def post_save(self, sender, instance, **kwargs): 14 | # Code that handles what should happen… 15 | 16 | def pre_delete(self, sender, instance, **kwargs): 17 | # Code that handles what should happen… 18 | 19 | 20 | By default, a ``RecommendationProvider`` registers a function with the ``pre_delete`` signal that removes the suggestion for the deleted rated object (via its storage's ``remove_recommendation`` and ``remove_similarity`` methods). 21 | 22 | -------------------------------------------------------------------------------- /docs/storages.rst: -------------------------------------------------------------------------------- 1 | .. ref-storages: 2 | 3 | Storage backend 4 | ================ 5 | 6 | Results of the computation are stored according to the storage backend defined in ``RECOMMENDS_STORAGE_BACKEND`` (default to ``'recommends.storages.djangoorm.storage.DjangoOrmStorage'``). A storage backend defines how de/serialize and store/retrieve objects and results. 7 | 8 | A storage backend can be any class extending ``recommends.storages.base.RecommendationStorage`` that implements the following methods and properties: 9 | 10 | .. method:: get_identifier(self, obj, *args, **kwargs) 11 | 12 | Given an object and optional parameters, returns a string identifying the object uniquely. 13 | 14 | .. method:: resolve_identifier(self, identifier) 15 | 16 | This method is the opposite of ``get_identifier``. It resolve the object's identifier to an actual model. 17 | 18 | .. method:: get_similarities_for_object(self, obj, limit, raw_id=False) 19 | 20 | if raw_id = False: 21 | Returns a list of ``Similarity`` objects for given ``obj``, ordered by score. 22 | else: 23 | Returns a list of similar ``model`` ids[pk] for given ``obj``, ordered by score. 24 | 25 | Example: 26 | 27 | :: 28 | 29 | [ 30 | { 31 | "related_object_id": XX, "content_type_id": XX 32 | }, 33 | .. 34 | ] 35 | 36 | 37 | .. method:: get_recommendations_for_user(self, user, limit, raw_id=False) 38 | 39 | if raw_id = False: 40 | Returns a list of :doc:`Recommendation ` objects for given ``user``, order by score. 41 | else: 42 | Returns a list of recommended ``model`` ids[pk] for given ``user``, ordered by score. 43 | 44 | Example: 45 | 46 | :: 47 | 48 | [ 49 | { 50 | "object_id": XX, "content_type_id": XX 51 | }, 52 | .. 53 | ] 54 | 55 | .. method:: get_votes(self) 56 | 57 | Optional. 58 | 59 | Retrieves the vote matrix saved by ``store_votes``. 60 | 61 | You won't usually need to implement this method, because you want to use fresh data. 62 | But it might be useful if you want some kind of heavy caching, maybe for testing purposes. 63 | 64 | .. method:: store_similarities(self, itemMatch) 65 | 66 | .. method:: store_recommendations(self, user, recommendations) 67 | 68 | Stores all the recommendations. 69 | 70 | ``recommendations`` is an iterable with the following schema: 71 | 72 | :: 73 | 74 | ( 75 | ( 76 | , 77 | ( 78 | (, ), 79 | (, ) 80 | ), 81 | ) 82 | ) 83 | 84 | .. method:: store_votes(self, iterable) 85 | 86 | Optional. 87 | 88 | Saves the vote matrix. 89 | 90 | You won't usually need to implement this method, because you want to use fresh data. 91 | But it might be useful if you want to dump the votes on somewhere, maybe for testing purposes. 92 | 93 | ``iterable`` is the vote matrix, expressed as a list of tuples with the following schema: 94 | 95 | :: 96 | 97 | [ 98 | ("", "", ), 99 | ("", "", ), 100 | ("", "", ), 101 | ("", "", ), 102 | ] 103 | 104 | .. method:: remove_recommendations(self, obj) 105 | 106 | Deletes all recommendations for object ``obj``. 107 | 108 | .. method:: remove_similarities(self, obj) 109 | 110 | Deletes all similarities that have object ``obj`` as source or target. 111 | 112 | .. method:: get_lock(self) 113 | 114 | Optional. Acquires an exclusive lock on the storage is acquired. Returns ``True`` if the lock is aquired, or ``False`` if the lock is already acquired by a previous process. 115 | 116 | .. method:: release_lock(self) 117 | 118 | Optional. Releases the lock acquired with the ``get_lock`` method. 119 | 120 | .. property:: can_lock 121 | 122 | 123 | Optional. Determines if the storage provides its own locking mechanism. Defaults to ``False``, meaning the default file-base locking will be used. 124 | 125 | RedisStorage 126 | ------------ 127 | 128 | This storage allows you to store results in Redis. This is the recommended storage backend, but it is not the default because it requires you to install redis-server. 129 | 130 | Options 131 | ~~~~~~~ 132 | 133 | ``threshold_similarities`` Defaults to ``0``. Only similarities with score greater than ``threshold similarities`` will be persisted. 134 | 135 | ``threshold_recommendations`` Defaults to ``0``. Only recommendations with score greater than ``threshold similarities`` will be persisted. 136 | 137 | Settings 138 | ~~~~~~~~ 139 | 140 | ``RECOMMENDS_STORAGE_REDIS_DATABASE``: A dictionary representing how to connect to the redis server. Defaults to: 141 | 142 | :: 143 | 144 | { 145 | 'HOST': 'localhost', 146 | 'PORT': 6379, 147 | 'NAME': 0 148 | } 149 | 150 | DjangoOrmStorage 151 | ---------------- 152 | 153 | This is the default storage. It requires minimal installation, but it's also the less performant. 154 | 155 | This storage allows you to store results in a database specified by your ``DATABASES`` setting. 156 | 157 | In order to use this storage, you'll also need to add ``'recommends.storages.djangoorm'`` to your ``INSTALLED_APPS``. 158 | 159 | Options 160 | ~~~~~~~ 161 | 162 | ``threshold_similarities`` Defaults to ``0``. Only similarities with score greater than ``threshold similarities`` will be persisted. 163 | 164 | ``threshold_recommendations`` Defaults to ``0``. Only recommendations with score greater than ``threshold similarities`` will be persisted. 165 | 166 | Settings 167 | ~~~~~~~~ 168 | 169 | To minimize disk I/O from the database, Similiarities and Suggestions will be committed in batches. The ``RECOMMENDS_STORAGE_COMMIT_THRESHOLD`` setting set how many record should be committed in each batch. Defaults to ``1000``. 170 | 171 | ``RECOMMENDS_STORAGE_DATABASE_ALIAS`` is used as the database where similarities and suggestions will be stored. Note that you will have to add ``recommends.storages.djangoorm.routers.RecommendsRouter`` to your settings' ``DATABASE_ROUTERS`` if you want to use something else than the default database. Default value is set to ``'recommends'``. 172 | 173 | 174 | MongoStorage 175 | ------------ 176 | 177 | Options 178 | ~~~~~~~ 179 | 180 | ``threshold_similarities`` Defaults to ``0``. Only similarities with score greater than ``threshold similarities`` will be persisted. 181 | 182 | ``threshold_recommendations`` Defaults to ``0``. Only recommendations with score greater than ``threshold similarities`` will be persisted. 183 | 184 | Settings 185 | ~~~~~~~~ 186 | 187 | ``RECOMMENDS_STORAGE_MONGODB_DATABASE``: A dictionary representing how to connect to the mongodb server. Defaults to: 188 | 189 | :: 190 | 191 | { 192 | 'HOST': 'localhost', 193 | 'PORT': 27017, 194 | 'NAME': 'recommends' 195 | } 196 | 197 | ``RECOMMENDS_STORAGE_MONGODB_FSYNC``: Boolean specifying if MongoDB should force writes to the disk. Default to ``False``. 198 | -------------------------------------------------------------------------------- /docs/templatetags.rst: -------------------------------------------------------------------------------- 1 | .. ref-templatetags: 2 | 3 | Template Tags & Filters 4 | ======================= 5 | 6 | To use the included template tags and filters, load the library in your templates by using ``{% load recommends %}``. 7 | 8 | Filters 9 | ------- 10 | 11 | The available filters are: 12 | 13 | ``similar:``: returns a list of Similarity objects, representing how much an object is similar to the given one. The ``limit`` argument is optional and defaults to ``5``:: 14 | 15 | {% for similarity in myobj|similar:5 %} 16 | {{ similarity.related_object }} 17 | {% endfor %} 18 | 19 | Tags 20 | ---- 21 | 22 | The available tags are: 23 | 24 | ``{% suggested as [limit ] %}``: Returns a list of Recommendation (suggestions of objects) for the current user. ``limit`` is optional and defaults to ``5``:: 25 | 26 | {% suggested as suggestions [limit 5] %} 27 | {% for suggested in suggestions %} 28 | {{ suggested.object }} 29 | {% endfor %} 30 | 31 | Templatetags Cache 32 | ------------------ 33 | 34 | By default, the templatetags provided by django-recommends will cache their result for 60 seconds. 35 | This time can be overridden via the ``RECOMMENDS_CACHE_TEMPLATETAGS_TIMEOUT``. 36 | -------------------------------------------------------------------------------- /recommends/__init__.py: -------------------------------------------------------------------------------- 1 | _version = "0.4.0" 2 | __version__ = VERSION = tuple(map(int, _version.split('.'))) 3 | 4 | default_app_config = 'recommends.apps.RecommendsConfig' 5 | -------------------------------------------------------------------------------- /recommends/algorithms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/algorithms/__init__.py -------------------------------------------------------------------------------- /recommends/algorithms/base.py: -------------------------------------------------------------------------------- 1 | class BaseAlgorithm(object): 2 | """ 3 | """ 4 | 5 | _cache = {} 6 | 7 | def clear_cache(self): 8 | self._cache = {} 9 | 10 | @property 11 | def cache(self): 12 | return self._cache 13 | 14 | def calculate_similarities(self, vote_list, verbose=0): 15 | """ 16 | Must return an dict of similarities for every object: 17 | 18 | Accepts a vote matrix representing votes with the following schema: 19 | 20 | :: 21 | 22 | [ 23 | (, "", ), 24 | (, "", ), 25 | ] 26 | 27 | Output must be a dictionary with the following schema: 28 | 29 | :: 30 | 31 | [ 32 | ("", [ 33 | ("", ), 34 | ("", ), 35 | ]), 36 | ("", [ 37 | ("", ), 38 | ("", ), 39 | ]), 40 | ] 41 | 42 | """ 43 | raise NotImplemented 44 | 45 | def calculate_recommendations(self, vote_list, itemMatch): 46 | """ 47 | ``itemMatch`` is supposed to be the result of ``calculate_similarities()`` 48 | 49 | Returns a list of recommendations: 50 | 51 | :: 52 | 53 | [ 54 | (, [ 55 | ("", ), 56 | ("", ), 57 | ]), 58 | (, [ 59 | ("", ), 60 | ("", ), 61 | ]), 62 | ] 63 | 64 | """ 65 | raise NotImplemented 66 | -------------------------------------------------------------------------------- /recommends/algorithms/ghetto.py: -------------------------------------------------------------------------------- 1 | 2 | import warnings 3 | 4 | from .naive import NaiveAlgorithm 5 | 6 | 7 | class GhettoAlgorithm(NaiveAlgorithm): 8 | def __init__(self, *args, **kwargs): 9 | warnings.warn( 10 | "`GhettoAlgorithm` is pending deprecation and it will be removed in " 11 | "future versions. Use `recommends.algorithms.naive.NaiveAlgorithm instead.", 12 | PendingDeprecationWarning, 13 | ) 14 | super(GhettoAlgorithm, self).__init__(*args, **kwargs) 15 | -------------------------------------------------------------------------------- /recommends/algorithms/naive.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import math 3 | from recommends.similarities import sim_distance 4 | from recommends.converters import convert_vote_list_to_userprefs, convert_vote_list_to_itemprefs 5 | from .base import BaseAlgorithm 6 | 7 | 8 | class NaiveAlgorithm(BaseAlgorithm): 9 | """ 10 | """ 11 | similarity = sim_distance 12 | 13 | def top_matches(self, prefs, p1): 14 | """ 15 | Returns the best matches for p1 from the prefs dictionary. 16 | """ 17 | return [(p2, self.similarity(prefs[p1], prefs[p2])) for p2 in prefs if p2 != p1] 18 | 19 | def calculate_similarities(self, vote_list, verbose=0): 20 | # Invert the preference matrix to be item-centric 21 | itemPrefs = convert_vote_list_to_itemprefs(vote_list) 22 | itemMatch = {} 23 | for item in itemPrefs: 24 | # Find the most similar items to this one 25 | itemMatch[item] = self.top_matches(itemPrefs, item) 26 | iteritems = itemMatch.items() 27 | return iteritems 28 | 29 | def get_recommended_items(self, vote_list, itemMatch, itemIgnored, user): 30 | prefs = convert_vote_list_to_userprefs(vote_list) 31 | itemMatch = dict(itemMatch) 32 | 33 | if user in prefs: 34 | userRatings = prefs[user] 35 | scores = defaultdict(int) 36 | totalSim = defaultdict(int) 37 | 38 | # Loop over items rated by this user 39 | for (item, rating) in userRatings.items(): 40 | # Loop over items similar to this one 41 | for (item2, similarity) in itemMatch[item]: 42 | # Skip ignored items 43 | if user.pk in itemIgnored and item2 in itemIgnored[user.pk]: 44 | continue 45 | # Ignore if this user has already rated this item 46 | if not math.isnan(similarity) and item2 not in userRatings: 47 | # Weighted sum of rating times similarity 48 | scores[item2] += similarity * rating 49 | 50 | # Sum of all the similarities 51 | totalSim[item2] += similarity 52 | 53 | # Divide each total score by total weighting to get an average 54 | rankings = ((item, (score / totalSim[item])) for item, score in scores.items() if totalSim[item] != 0) 55 | return rankings 56 | return [] 57 | 58 | def calculate_recommendations(self, vote_list, itemMatch, itemIgnored): 59 | """ 60 | ``itemMatch`` is supposed to be the result of ``calculate_similarities()`` 61 | 62 | Returns a list of recommendations: 63 | 64 | :: 65 | 66 | [ 67 | (, [ 68 | ("", ), 69 | ("", ), 70 | ]), 71 | (, [ 72 | ("", ), 73 | ("", ), 74 | ]), 75 | ] 76 | 77 | """ 78 | recommendations = [] 79 | users = set(map(lambda x: x[0], vote_list)) 80 | for user in users: 81 | rankings = self.get_recommended_items(vote_list, itemMatch, itemIgnored, user) 82 | recommendations.append((user, rankings)) 83 | return recommendations 84 | -------------------------------------------------------------------------------- /recommends/algorithms/pyrecsys.py: -------------------------------------------------------------------------------- 1 | from recsys.datamodel.data import Data 2 | from recsys.algorithm.factorize import SVD 3 | from .base import BaseAlgorithm 4 | from recommends.converters import convert_vote_list_to_itemprefs 5 | 6 | 7 | class RecSysAlgorithm(BaseAlgorithm): 8 | def __init__(self, k=100, *args, **kwargs): 9 | self.k = k 10 | super(RecSysAlgorithm, self).__init__(*args, **kwargs) 11 | 12 | @property 13 | def svd(self): 14 | return self.cache.get('svd', None) 15 | 16 | def setup_svd(self, vote_list): 17 | if self.svd is None: 18 | self.cache['svd'] = SVD() 19 | data = Data() 20 | 21 | for vote in vote_list: 22 | user_id = vote[0].id 23 | item_id = vote[1] 24 | value = float(vote[2]) 25 | data.add_tuple((value, item_id, user_id)) # Tuple format is: 26 | self.cache['svd'].set_data(data) 27 | self.cache['svd'].compute(k=self.k, min_values=1) 28 | return self.svd 29 | 30 | def calculate_similarities(self, vote_list, verbose=0): 31 | svd = self.setup_svd(vote_list) 32 | 33 | itemPrefs = convert_vote_list_to_itemprefs(vote_list) 34 | itemMatch = {} 35 | for item in itemPrefs: 36 | itemMatch[item] = svd.similar(item) 37 | iteritems = itemMatch.items() 38 | return iteritems 39 | 40 | def calculate_recommendations(self, vote_list, itemMatch, itemIgnored): 41 | svd = self.setup_svd(vote_list) 42 | 43 | recommendations = [] 44 | users = set(map(lambda x: x[0], vote_list)) 45 | for user in users: 46 | try: 47 | rankings = svd.recommend(user.id, only_unknowns=True, is_row=False) 48 | recommendations.append((user, rankings)) 49 | except KeyError: 50 | pass 51 | return recommendations 52 | -------------------------------------------------------------------------------- /recommends/apps.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from django.apps import AppConfig, apps 4 | from .settings import RECOMMENDS_AUTODISCOVER_MODULE 5 | 6 | 7 | class RecommendsConfig(AppConfig): 8 | name = 'recommends' 9 | 10 | def ready(self): 11 | if not RECOMMENDS_AUTODISCOVER_MODULE: 12 | return 13 | 14 | for appconfig in apps.get_app_configs(): 15 | try: 16 | importlib.import_module('.' + RECOMMENDS_AUTODISCOVER_MODULE, appconfig.name) 17 | except ImportError: 18 | pass 19 | -------------------------------------------------------------------------------- /recommends/converters.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from django.apps import apps 3 | 4 | 5 | def model_path(obj): 6 | return '%s.%s' % (obj._meta.app_label, obj._meta.object_name.lower()) 7 | 8 | 9 | class IdentifierManager(object): 10 | _sites = None 11 | _ctypes = None 12 | 13 | @property 14 | def sites(self): 15 | if self._sites is None: 16 | from django.contrib.sites.models import Site 17 | 18 | self._sites = dict([(s.id, s) for s in Site.objects.all()]) 19 | return self._sites 20 | 21 | @property 22 | def ctypes(self): 23 | if self._ctypes is None: 24 | from django.contrib.contenttypes.models import ContentType 25 | 26 | self._ctypes = dict([("%s.%s" % (c.app_label, c.model), c) for c in ContentType.objects.all()]) 27 | return self._ctypes 28 | 29 | def resolve_identifier(self, identifier): 30 | """ 31 | The opposite of ``get_identifier()`` 32 | """ 33 | app_module, site_id, object_id = identifier.split(':') 34 | app_label, model = app_module.split('.') 35 | site = self.sites[int(site_id)] 36 | ModelClass = apps.get_model(app_label, model) 37 | model = ModelClass.objects.get(pk=object_id) 38 | return model, site 39 | 40 | def identifier_to_dict(self, identifier, score=None, related=False): 41 | """ 42 | The opposite of ``get_identifier()`` 43 | """ 44 | app_module, site_id, object_id = identifier.split(':') 45 | ctype = self.ctypes[app_module] 46 | 47 | if related: 48 | spec = { 49 | 'related_object_ctype': ctype.id, 50 | 'related_object_id': int(object_id), 51 | 'related_object_site': int(site_id) 52 | } 53 | else: 54 | spec = { 55 | 'object_ctype': ctype.id, 56 | 'object_id': int(object_id), 57 | 'object_site': int(site_id) 58 | } 59 | if score is not None: 60 | spec['score'] = score 61 | 62 | return spec 63 | 64 | def get_identifier(self, obj, site_id): 65 | """ 66 | Given a Django Model, returns a string identifier in the format 67 | .::. 68 | """ 69 | return "%s:%s:%s" % (model_path(obj), site_id, obj.id) 70 | 71 | 72 | def convert_vote_list_to_userprefs(vote_list): 73 | """ 74 | Return a user-centerd prefernce matrix. 75 | 76 | ``vote_list must be`` composed of (user_id, object_identifier, rating) 77 | 78 | ``object_identifier`` is any string that uniquely identifies the object ie: 79 | .:. 80 | 81 | The ``utils.get_identifier`` method is provided as convenience for creating such identifiers. 82 | """ 83 | prefs = defaultdict(dict) 84 | for pref in vote_list: 85 | prefs[pref[0]][pref[1]] = pref[2] 86 | return prefs 87 | 88 | 89 | def convert_vote_list_to_itemprefs(vote_list): 90 | """ 91 | Return a item-centerd prefernce matrix. 92 | 93 | ``vote_list must be`` composed of (user_id, object_identifier, rating) 94 | 95 | ``object_identifier`` is any string that uniquely identifies the object ie: 96 | .:. 97 | 98 | The ``utils.get_identifier`` method is provided as convenience for creating such identifiers. 99 | """ 100 | prefs = defaultdict(dict) 101 | for pref in vote_list: 102 | prefs[pref[1]][pref[0]] = pref[2] 103 | return prefs 104 | 105 | 106 | def similary_results_to_itemMatch(qs, provider): 107 | itemMatch = defaultdict(list) 108 | for i in qs: 109 | site = i.related_object_site 110 | item = provider.get_identifier(i.get_object(), site) 111 | similarity = i.score 112 | item2 = provider.get_identifier(i.get_related_object(), site) 113 | 114 | itemMatch[item].append((similarity, item2)) 115 | 116 | return itemMatch 117 | -------------------------------------------------------------------------------- /recommends/fixtures/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 3, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "user2", 7 | "first_name": "", 8 | "last_name": "", 9 | "is_active": true, 10 | "is_superuser": false, 11 | "is_staff": false, 12 | "last_login": "2011-11-15 10:17:18", 13 | "groups": [], 14 | "user_permissions": [], 15 | "password": "", 16 | "email": "", 17 | "date_joined": "2011-11-15 10:17:18" 18 | } 19 | }, 20 | { 21 | "pk": 4, 22 | "model": "auth.user", 23 | "fields": { 24 | "username": "user3", 25 | "first_name": "", 26 | "last_name": "", 27 | "is_active": true, 28 | "is_superuser": false, 29 | "is_staff": false, 30 | "last_login": "2011-11-15 10:17:25", 31 | "groups": [], 32 | "user_permissions": [], 33 | "password": "", 34 | "email": "", 35 | "date_joined": "2011-11-15 10:17:25" 36 | } 37 | }, 38 | { 39 | "pk": 2, 40 | "model": "auth.user", 41 | "fields": { 42 | "username": "user1", 43 | "first_name": "", 44 | "last_name": "", 45 | "is_active": true, 46 | "is_superuser": false, 47 | "is_staff": false, 48 | "last_login": "2011-11-16 12:27:08", 49 | "groups": [], 50 | "user_permissions": [], 51 | "password": "", 52 | "email": "", 53 | "date_joined": "2011-11-15 10:17:10" 54 | } 55 | }, 56 | { 57 | "pk": 1, 58 | "model": "recommends.recproduct", 59 | "fields": { 60 | "name": "Dozen Red Roses", 61 | "sites": [ 62 | 1 63 | ] 64 | } 65 | }, 66 | { 67 | "pk": 2, 68 | "model": "recommends.recproduct", 69 | "fields": { 70 | "name": "Orange Juice", 71 | "sites": [ 72 | 1 73 | ] 74 | } 75 | }, 76 | { 77 | "pk": 3, 78 | "model": "recommends.recproduct", 79 | "fields": { 80 | "name": "Coffee Mug", 81 | "sites": [ 82 | 1 83 | ] 84 | } 85 | }, 86 | { 87 | "pk": 4, 88 | "model": "recommends.recproduct", 89 | "fields": { 90 | "name": "Batteries", 91 | "sites": [ 92 | 1 93 | ] 94 | } 95 | }, 96 | { 97 | "pk": 5, 98 | "model": "recommends.recproduct", 99 | "fields": { 100 | "name": "Bottle of Red Wine", 101 | "sites": [ 102 | 1 103 | ] 104 | } 105 | }, 106 | { 107 | "pk": 6, 108 | "model": "recommends.recproduct", 109 | "fields": { 110 | "name": "Red Sofa", 111 | "sites": [ 112 | 1 113 | ] 114 | } 115 | }, 116 | { 117 | "pk": 7, 118 | "model": "recommends.recproduct", 119 | "fields": { 120 | "name": "30\" Cinema Display", 121 | "sites": [ 122 | 1 123 | ] 124 | } 125 | }, 126 | { 127 | "pk": 8, 128 | "model": "recommends.recproduct", 129 | "fields": { 130 | "name": "Standing Desk", 131 | "sites": [ 132 | 1 133 | ] 134 | } 135 | }, 136 | { 137 | "pk": 9, 138 | "model": "recommends.recproduct", 139 | "fields": { 140 | "name": "Pots & Pans", 141 | "sites": [ 142 | 1 143 | ] 144 | } 145 | }, 146 | { 147 | "pk": 10, 148 | "model": "recommends.recproduct", 149 | "fields": { 150 | "name": "Sunflower Seeds", 151 | "sites": [ 152 | 1 153 | ] 154 | } 155 | }, 156 | { 157 | "pk": 11, 158 | "model": "recommends.recproduct", 159 | "fields": { 160 | "name": "1lb Tenderloin Steak", 161 | "sites": [ 162 | 1 163 | ] 164 | } 165 | }, 166 | { 167 | "pk": 1, 168 | "model": "recommends.recvote", 169 | "fields": { 170 | "product": 1, 171 | "score": 1.0, 172 | "user": 2, 173 | "site": 1 174 | } 175 | }, 176 | { 177 | "pk": 2, 178 | "model": "recommends.recvote", 179 | "fields": { 180 | "product": 2, 181 | "score": 1.0, 182 | "user": 2, 183 | "site": 1 184 | } 185 | }, 186 | { 187 | "pk": 3, 188 | "model": "recommends.recvote", 189 | "fields": { 190 | "product": 2, 191 | "score": 1.0, 192 | "user": 3, 193 | "site": 1 194 | } 195 | }, 196 | { 197 | "pk": 4, 198 | "model": "recommends.recvote", 199 | "fields": { 200 | "product": 3, 201 | "score": 1.0, 202 | "user": 3, 203 | "site": 1 204 | } 205 | }, 206 | { 207 | "pk": 5, 208 | "model": "recommends.recvote", 209 | "fields": { 210 | "product": 4, 211 | "score": 1.0, 212 | "user": 4, 213 | "site": 1 214 | } 215 | }, 216 | { 217 | "pk": 6, 218 | "model": "recommends.recvote", 219 | "fields": { 220 | "product": 9, 221 | "score": 1.0, 222 | "user": 4, 223 | "site": 1 224 | } 225 | }, 226 | { 227 | "pk": 7, 228 | "model": "recommends.recvote", 229 | "fields": { 230 | "product": 10, 231 | "score": 1.0, 232 | "user": 4, 233 | "site": 1 234 | } 235 | }, 236 | { 237 | "pk": 8, 238 | "model": "recommends.recvote", 239 | "fields": { 240 | "product": 5, 241 | "score": 1.0, 242 | "user": 3, 243 | "site": 1 244 | } 245 | }, 246 | { 247 | "pk": 9, 248 | "model": "recommends.recvote", 249 | "fields": { 250 | "product": 5, 251 | "score": 1.0, 252 | "user": 4, 253 | "site": 1 254 | } 255 | }, 256 | { 257 | "pk": 10, 258 | "model": "recommends.recvote", 259 | "fields": { 260 | "product": 7, 261 | "score": 1.0, 262 | "user": 2, 263 | "site": 1 264 | } 265 | }, 266 | { 267 | "pk": 11, 268 | "model": "recommends.recvote", 269 | "fields": { 270 | "product": 11, 271 | "score": 1.0, 272 | "user": 3, 273 | "site": 1 274 | } 275 | } 276 | ] -------------------------------------------------------------------------------- /recommends/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/management/__init__.py -------------------------------------------------------------------------------- /recommends/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/management/commands/__init__.py -------------------------------------------------------------------------------- /recommends/management/commands/recommends_precompute.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from recommends.tasks import recommends_precompute 3 | 4 | from datetime import datetime 5 | import dateutil.relativedelta 6 | from optparse import make_option 7 | 8 | import warnings 9 | 10 | 11 | class Command(BaseCommand): 12 | help = 'Calculate recommendations and similarities based on ratings' 13 | option_list = BaseCommand.option_list + ( 14 | make_option('--verbose', 15 | action='store_true', 16 | dest='verbose', 17 | default=False, 18 | help='verbose mode' 19 | ), 20 | ) 21 | 22 | def handle(self, *args, **options): 23 | verbosity = int(options.get('verbosity', 0)) 24 | if options['verbose']: 25 | warnings.warn('The `--verbose` option is being deprecated and it will be removed in the next release. Use `--verbosity` instead.', PendingDeprecationWarning) 26 | verbosity = 1 27 | 28 | if verbosity == 0: 29 | # avoids allocating the results 30 | recommends_precompute() 31 | else: 32 | self.stdout.write("\nCalculation Started.\n") 33 | start_time = datetime.now() 34 | results = recommends_precompute() 35 | end_time = datetime.now() 36 | if verbosity > 1: 37 | for r in results: 38 | self.stdout.write( 39 | "%d similarities and %d recommendations saved.\n" 40 | % (r['similar_count'], r['recommend_count'])) 41 | rd = dateutil.relativedelta.relativedelta(end_time, start_time) 42 | self.stdout.write( 43 | "Calculation finished in %d years, %d months, %d days, %d hours, %d minutes and %d seconds\n" 44 | % (rd.years, rd.months, rd.days, rd.hours, rd.minutes, rd.seconds)) 45 | -------------------------------------------------------------------------------- /recommends/managers.py: -------------------------------------------------------------------------------- 1 | from recommends.utils import ctypes_dict 2 | 3 | 4 | class CachedContentTypesMixin(object): 5 | _ctypes = None 6 | 7 | @property 8 | def ctypes(self): 9 | if self._ctypes is None: 10 | self._ctypes = ctypes_dict() 11 | return self._ctypes 12 | 13 | def get_ctype_id_for_obj(self, obj): 14 | app_label = obj._meta.app_label 15 | module_name = obj._meta.model_name 16 | return self.ctypes["%s.%s" % (app_label, module_name)] 17 | 18 | 19 | class DictStorageManager(CachedContentTypesMixin): 20 | def similarity_for_objects(self, object_target, object_target_site, object_related, object_related_site): 21 | object_ctype_id = self.get_ctype_id_for_obj(object_target) 22 | object_id = object_target.id 23 | 24 | related_object_ctype_id = self.get_ctype_id_for_obj(object_related) 25 | related_object_id = object_related.id 26 | 27 | return dict( 28 | object_ctype=object_ctype_id, 29 | object_id=object_id, 30 | object_site=object_target_site.id, 31 | related_object_ctype=related_object_ctype_id, 32 | related_object_id=related_object_id, 33 | related_object_site=object_related_site.id, 34 | ) 35 | 36 | def suggestion_for_object(self, user, object_recommended, object_site): 37 | object_ctype_id = self.get_ctype_id_for_obj(object_recommended) 38 | object_id = object_recommended.id 39 | 40 | return dict( 41 | object_ctype=object_ctype_id, 42 | object_id=object_id, 43 | object_site=object_site.id, 44 | user=user.id, 45 | ) 46 | -------------------------------------------------------------------------------- /recommends/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.utils.encoding import python_2_unicode_compatible 4 | 5 | 6 | @python_2_unicode_compatible 7 | class MockModel(object): 8 | _object = None 9 | 10 | def __init__(self, **kwargs): 11 | self.__dict__ = kwargs 12 | 13 | def __str__(self): 14 | return "Mock for content object %s" % self.object 15 | 16 | def __repr__(self): 17 | return "<%s>" % self.__str__() 18 | 19 | @property 20 | def object(self): 21 | if self._object is None: 22 | ModelClass = ContentType.objects.get(pk=self.object_ctype).model_class() 23 | self._object = ModelClass.objects.get(pk=self.object_id) 24 | return self._object 25 | 26 | 27 | @python_2_unicode_compatible 28 | class MockSimilarity(MockModel): 29 | _related_object = None 30 | 31 | @property 32 | def related_object(self): 33 | if self._related_object is None: 34 | ModelClass = ContentType.objects.get(pk=self.related_object_ctype).model_class() 35 | self._related_object = ModelClass.objects.get(pk=self.related_object_id) 36 | return self._related_object 37 | 38 | def __str__(self): 39 | return "Similarity between %s and %s" % (self.object, self.related_object) 40 | -------------------------------------------------------------------------------- /recommends/providers/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.contrib.auth.models import User 3 | from django.contrib.sites.models import Site 4 | from django.conf import settings 5 | from ..converters import model_path 6 | from ..settings import RECOMMENDS_STORAGE_BACKEND, RECOMMENDS_LOGGER_NAME 7 | from ..tasks import remove_suggestions, remove_similarities 8 | from ..utils import import_from_classname 9 | from ..algorithms.naive import NaiveAlgorithm 10 | 11 | 12 | logger = logging.getLogger(RECOMMENDS_LOGGER_NAME) 13 | 14 | 15 | class RecommendationProviderRegistry(object): 16 | _vote_providers = {} 17 | _content_providers = {} 18 | providers = set() 19 | 20 | def __init__(self): 21 | StorageClass = import_from_classname(RECOMMENDS_STORAGE_BACKEND) 22 | self.storage = StorageClass(settings) 23 | 24 | def register(self, vote_model, content_models, Provider): 25 | provider_instance = Provider() 26 | self._vote_providers[model_path(vote_model)] = provider_instance 27 | for model in content_models: 28 | self._content_providers[model_path(model)] = provider_instance 29 | 30 | self.providers.add(provider_instance) 31 | 32 | for signal in provider_instance.rate_signals: 33 | if isinstance(signal, str): 34 | sig_class_name = signal.split('.')[-1] 35 | sig_instance = import_from_classname(signal) 36 | listener = getattr(provider_instance, sig_class_name, False) 37 | if listener: 38 | for model in content_models: 39 | sig_instance.connect(listener, sender=model) 40 | 41 | def unregister(self, vote_model, content_models, Provider): 42 | provider_instance = Provider() 43 | 44 | for signal in provider_instance.rate_signals: 45 | if isinstance(signal, str): 46 | sig_class_name = signal.split('.')[-1] 47 | sig_instance = import_from_classname(signal) 48 | listener = getattr(provider_instance, sig_class_name, False) 49 | if listener: 50 | for model in content_models: 51 | sig_instance.disconnect(listener, sender=model) 52 | 53 | new_set = [i for i in self.providers if not isinstance(i, Provider)] 54 | self.providers = set(new_set) 55 | 56 | for model in content_models: 57 | del self._content_providers[model_path(model)] 58 | del self._vote_providers[model_path(vote_model)] 59 | 60 | def get_provider_for_vote(self, model): 61 | return self._vote_providers[model_path(model)] 62 | 63 | def get_provider_for_content(self, model): 64 | return self._content_providers[model_path(model)] 65 | 66 | def get_vote_providers(self): 67 | return self._vote_providers.values() 68 | 69 | 70 | recommendation_registry = RecommendationProviderRegistry() 71 | 72 | 73 | class Rating(object): 74 | 75 | def __init__(self, user, rated_object, rating): 76 | self.user = user 77 | self.rated_object = rated_object 78 | self.rating = rating 79 | 80 | 81 | class RecommendationProvider(object): 82 | """ 83 | A ``RecommendationProvider`` specifies how to retrieve various informations (items, users, votes) 84 | necessary for computing recommendation and similarities for a set of objects. 85 | 86 | Subclasses override methods in order to determine what constitutes voted items, a vote, 87 | its score, and user. 88 | """ 89 | rate_signals = ['django.db.models.signals.pre_delete'] 90 | algorithm = NaiveAlgorithm() 91 | 92 | def __init__(self): 93 | if not getattr(self, 'storage', False): 94 | self.storage = recommendation_registry.storage 95 | 96 | def get_items(self): 97 | """Return items that have been voted.""" 98 | raise NotImplementedError 99 | 100 | def get_ratings(self, obj): 101 | """Returns all ratings for given item.""" 102 | raise NotImplementedError 103 | 104 | def get_rating_user(self, rating): 105 | """Returns the user who performed the rating.""" 106 | raise NotImplementedError 107 | 108 | def get_rating_score(self, rating): 109 | """Returns the score of the rating.""" 110 | raise NotImplementedError 111 | 112 | def get_rating_item(self, rating): 113 | """Returns the rated object.""" 114 | raise NotImplementedError 115 | 116 | def get_rating_site(self, rating): 117 | """Returns the site of the rating. Can be a ``Site`` object or its ID. 118 | 119 | Defaults to ``settings.SITE_ID``.""" 120 | return settings.SITE_ID 121 | 122 | def is_rating_active(self, rating): 123 | """Returns if the rating is active.""" 124 | return True 125 | 126 | def pre_delete(self, sender, instance, **kwargs): 127 | """ 128 | This function gets called when a signal in ``self.rate_signals`` is 129 | fired from one of the rated model. 130 | 131 | Overriding this method is optional. The default method removes the 132 | suggestions for the deleted objected. 133 | 134 | See :doc:`signals`. 135 | """ 136 | remove_similarities.delay(rated_model=model_path(sender), object_id=instance.id) 137 | remove_suggestions.delay(rated_model=model_path(sender), object_id=instance.id) 138 | 139 | def vote_list(self): 140 | vote_list = self.storage.get_votes() 141 | if vote_list is None: 142 | vote_list = [] 143 | for item in self.get_items(): 144 | for rating in self.get_ratings(item): 145 | user = self.get_rating_user(rating) 146 | score = self.get_rating_score(rating) 147 | site = self.get_rating_site(rating) 148 | if isinstance(site, Site): 149 | site_id = site.id 150 | else: 151 | site_id = site 152 | identifier = self.storage.get_identifier(item, site_id) 153 | vote_list.append((user, identifier, score)) 154 | self.storage.store_votes(vote_list) 155 | return vote_list 156 | 157 | def items_ignored(self): 158 | """ 159 | Returns user ignored items. 160 | User can delete items from the list of recommended. 161 | 162 | See recommends.converters.IdentifierManager.get_identifier for help. 163 | :: 164 | 165 | {: ["object_identifier1",..., "object_identifierN"], ..} 166 | """ 167 | return {} 168 | 169 | def precompute(self, vote_list=None): 170 | """ 171 | This function will be called by the task manager in order 172 | to compile and store the results. 173 | 174 | Returns a dictionary contains count of recommended and similar items 175 | :: 176 | {: XX, : XX} 177 | """ 178 | if vote_list is None: 179 | logger.info('fetching votes from the provider...') 180 | vote_list = self.vote_list() 181 | logger.info('calculating similarities...') 182 | self.algorithm.clear_cache() 183 | itemMatch = self.algorithm.calculate_similarities(vote_list) 184 | 185 | logger.info('saving similarities...') 186 | self.pre_store_similarities(itemMatch) 187 | self.storage.store_similarities(itemMatch) 188 | 189 | logger.info('fetching ignored items...') 190 | itemIgnored = self.items_ignored() 191 | 192 | logger.info('saving recommendations...') 193 | recommendItems = self.algorithm.calculate_recommendations( 194 | vote_list, itemMatch, itemIgnored) 195 | self.storage.store_recommendations(recommendItems) 196 | return { 197 | 'similar_count': len(itemMatch), 198 | 'recommend_count': len(recommendItems)} 199 | 200 | def get_users(self): 201 | """Returns all users who have voted something.""" 202 | return User.objects.filter(is_active=True) 203 | 204 | def pre_store_similarities(self, itemMatch): 205 | """ 206 | Optional. This method will get called right before passing the 207 | similarities to the storage. For example, you can override this method 208 | to do some stats or visualize the data. 209 | """ 210 | pass 211 | -------------------------------------------------------------------------------- /recommends/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | RECOMMENDS_TASK_RUN = getattr(settings, 'RECOMMENDS_TASK_RUN', True) 5 | RECOMMENDS_TASK_CRONTAB = getattr(settings, 'RECOMMENDS_TASK_CRONTAB', {'hour': '*/24'}) 6 | RECOMMENDS_CACHE_TEMPLATETAGS_TIMEOUT = getattr(settings, 'RECOMMENDS_CACHE_TEMPLATETAGS_TIMEOUT', 60) 7 | RECOMMENDS_STORAGE_BACKEND = getattr(settings, 'RECOMMENDS_STORAGE_BACKEND', 'recommends.storages.djangoorm.storage.DjangoOrmStorage') 8 | RECOMMENDS_STORAGE_LOGGING_THRESHOLD = getattr(settings, 'RECOMMENDS_STORAGE_LOGGING_THRESHOLD', 1000) 9 | RECOMMENDS_LOGGER_NAME = getattr(settings, 'RECOMMENDS_LOGGER_NAME', 'recommends') 10 | RECOMMENDS_TASK_EXPIRES = getattr(settings, 'RECOMMENDS_TASK_EXPIRES', None) 11 | RECOMMENDS_AUTODISCOVER_MODULE = getattr(settings, 'RECOMMENDS_AUTODISCOVER_MODULE', 'recommendations') 12 | -------------------------------------------------------------------------------- /recommends/similarities.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | 4 | @staticmethod 5 | def sim_distance(p1, p2): 6 | """Returns a distance-based similarity score for p1 and p2""" 7 | # Get the list of shared_items 8 | si = [item for item in p1 if item in p2] 9 | 10 | if len(si) != 0: 11 | squares = [pow(p1[item] - p2[item], 2) for item in si] 12 | # Add up the squares of all the differences 13 | sum_of_squares = sum(squares) 14 | return 1 / (1 + sqrt(sum_of_squares)) 15 | return 0 16 | 17 | 18 | @staticmethod 19 | def sim_pearson(p1, p2): 20 | """ 21 | Returns the Pearson correlation coefficient for p1 and p2 22 | """ 23 | # Get the list of mutually rated items 24 | si = [item for item in p1 if item in p2] 25 | 26 | # Find the number of elements 27 | n = len(si) 28 | 29 | # if they have no ratings in common, return 0 30 | if n != 0: 31 | # Add up all the preferences 32 | sum1 = sum([p1[it] for it in si]) 33 | sum2 = sum([p2[it] for it in si]) 34 | 35 | # Sum up the squares 36 | sum1Sq = sum([pow(p1[it], 2) for it in si]) 37 | sum2Sq = sum([pow(p2[it], 2) for it in si]) 38 | 39 | # Sum up the products 40 | pSum = sum([p1[it] * p2[it] for it in si]) 41 | 42 | # Calculate Pearson score 43 | num = pSum - (sum1 * sum2 / n) 44 | den = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n)) 45 | if den == 0: 46 | return 0 47 | r = num / den 48 | return r 49 | return 0 50 | -------------------------------------------------------------------------------- /recommends/storages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/storages/__init__.py -------------------------------------------------------------------------------- /recommends/storages/base.py: -------------------------------------------------------------------------------- 1 | from recommends.converters import IdentifierManager 2 | 3 | 4 | class BaseRecommendationStorage(object): 5 | threshold_similarities = 0 6 | threshold_recommendations = 0 7 | 8 | can_lock = False 9 | 10 | def __init__(self, settings=None): 11 | self.identifier_manager = IdentifierManager() 12 | self.settings = settings 13 | 14 | def get_identifier(self, obj, site_id=None, rating=None, *args, **kwargs): 15 | """ 16 | Given an object and optional parameters, returns a string identifying the object uniquely. 17 | """ 18 | if rating is not None: 19 | site_id = self.get_rating_site(rating).id 20 | if site_id is None: 21 | site_id = self.settings.SITE_ID 22 | return self.identifier_manager.get_identifier(obj, site_id) 23 | 24 | def resolve_identifier(self, identifier): 25 | """ 26 | This method is the opposite of ``get_identifier``. 27 | It resolve the object's identifier to an actual model. 28 | """ 29 | return self.identifier_manager.resolve_identifier(identifier) 30 | 31 | def get_similarities_for_object(self, obj, limit, raw_id=False): 32 | """ 33 | if raw_id = False: 34 | Returns a list of ``Similarity`` objects for given ``obj``, ordered by score. 35 | else: 36 | Returns a list of similar ``model`` ids[pk] for given ``obj``, ordered by score. 37 | 38 | Example: 39 | 40 | :: 41 | 42 | [ 43 | { 44 | "related_object_id": XX, "contect_type_id": XX 45 | }, 46 | .. 47 | ] 48 | """ 49 | raise NotImplementedError 50 | 51 | def get_recommendations_for_user(self, user, limit, raw_id=False): 52 | """ 53 | if raw_id = False: 54 | Returns a list of ``Recommendation`` objects for given ``user``, ordered by score. 55 | else: 56 | Returns a list of recommended ``model`` ids[pk] for given ``user``, ordered by score. 57 | 58 | Example: 59 | 60 | :: 61 | [ 62 | { 63 | "object_id": XX, "contect_type_id": XX 64 | }, 65 | .. 66 | ] 67 | """ 68 | raise NotImplementedError 69 | 70 | def store_similarities(self, itemMatch): 71 | raise NotImplementedError 72 | 73 | def store_recommendations(self, recommendations): 74 | """ 75 | Stores all the recommendations. 76 | 77 | ``recommendations`` is an iterable with the following schema: 78 | 79 | :: 80 | 81 | ( 82 | ( 83 | , 84 | ( 85 | (, ), 86 | (, ) 87 | ), 88 | ) 89 | ) 90 | """ 91 | raise NotImplementedError 92 | 93 | def get_votes(self): 94 | """ 95 | Optional. 96 | 97 | Retrieves the vote matrix saved by ``store_votes``. 98 | 99 | You won't usually need to implement this method, because you want to use fresh data. 100 | But it might be useful if you want some kind of heavy caching, maybe for testing purposes. 101 | """ 102 | raise NotImplementedError 103 | 104 | def store_votes(self, iterable): 105 | """ 106 | Optional. 107 | 108 | Saves the vote matrix. 109 | 110 | You won't usually need to implement this method, because you want to use fresh data. 111 | But it might be useful if you want to dump the votes on somewhere, maybe for testing purposes. 112 | 113 | ``iterable`` is the vote matrix, expressed as a list of tuples with the following schema: 114 | 115 | :: 116 | 117 | [ 118 | ("", "", ), 119 | ("", "", ), 120 | ("", "", ), 121 | ("", "", ), 122 | ] 123 | """ 124 | raise NotImplementedError 125 | 126 | def remove_recommendation(self, obj): 127 | """ 128 | Deletes all recommendations for object ``obj``. 129 | """ 130 | raise NotImplementedError 131 | 132 | def remove_similarity(self, obj): 133 | """ 134 | Deletes all similarities that have object ``obj`` as source or target. 135 | """ 136 | raise NotImplementedError 137 | 138 | def get_lock(self): 139 | """ 140 | Acquire a storage-specific lock 141 | """ 142 | raise NotImplementedError 143 | 144 | def release_lock(self): 145 | """ 146 | Release a storage-specific lock 147 | """ 148 | raise NotImplementedError 149 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/storages/djangoorm/__init__.py -------------------------------------------------------------------------------- /recommends/storages/djangoorm/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Similarity, Recommendation 3 | 4 | 5 | class SimilarityAdmin(admin.ModelAdmin): 6 | list_display = ('object', 'object_site', 'related_object', 'related_object_site', 'score') 7 | list_filter = ('object_site',) 8 | 9 | 10 | admin.site.register(Similarity, SimilarityAdmin) 11 | 12 | 13 | class RecommendationAdmin(admin.ModelAdmin): 14 | list_display = ('user', 'score', 'object', 'object_site') 15 | list_filter = ('object_site',) 16 | 17 | 18 | admin.site.register(Recommendation, RecommendationAdmin) 19 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/managers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from recommends.managers import CachedContentTypesMixin 4 | 5 | 6 | class RecommendsManager(models.Manager, CachedContentTypesMixin): 7 | def filter_for_model(self, model): 8 | ctype_id = self.get_ctype_id_for_obj(model) 9 | return self.filter(object_ctype=ctype_id) 10 | 11 | def filter_for_object(self, obj): 12 | return self.filter_for_model(obj).filter(object_id=obj.id) 13 | 14 | 15 | class SimilarityManager(RecommendsManager): 16 | def filter_for_related_model(self, related_model): 17 | ctype_id = self.get_ctype_id_for_obj(related_model) 18 | return self.filter(related_object_ctype=ctype_id) 19 | 20 | def filter_for_related_object(self, related_obj): 21 | return self.filter_for_related_model(related_obj).filter(related_object_id=related_obj.id) 22 | 23 | def filter_by_couple(self, target_object, related_obj): 24 | related_ctype_id = self.get_ctype_id_for_obj(related_obj) 25 | 26 | return self.filter_for_object(target_object).filter( 27 | related_object_ctype=related_ctype_id, 28 | related_object_id=related_obj.id 29 | ) 30 | 31 | def get_queryset(self): 32 | return super(SimilarityManager, self).get_queryset().filter(score__isnull=False) 33 | 34 | def get_or_create_for_objects(self, object_target, object_target_site, object_related, object_related_site): 35 | object_ctype_id = self.get_ctype_id_for_obj(object_target) 36 | object_id = object_target.id 37 | 38 | related_object_ctype_id = self.get_ctype_id_for_obj(object_related) 39 | related_object_id = object_related.id 40 | 41 | return self.get_or_create( 42 | object_ctype_id=object_ctype_id, 43 | object_id=object_id, 44 | object_site=object_target_site.id, 45 | related_object_ctype_id=related_object_ctype_id, 46 | related_object_id=related_object_id, 47 | related_object_site=object_related_site.id 48 | ) 49 | 50 | def set_score_for_objects(self, object_target, object_target_site, object_related, object_related_site, score): 51 | if score == 0: 52 | self.filter_by_couple(object_target, object_related).filter( 53 | object_site=object_target_site.id, 54 | related_object_site=object_related_site.id 55 | ).delete() 56 | return None 57 | 58 | result, created = self.get_or_create_for_objects(object_target, object_target_site, object_related, object_related_site) 59 | result.score = score 60 | result.save() 61 | return result 62 | 63 | def similar_to(self, obj, site=None, **kwargs): 64 | if site is None and 'related_object_site' not in kwargs: 65 | kwargs['related_object_site'] = settings.SITE_ID 66 | return self.filter_for_object(obj).filter(**kwargs) 67 | 68 | 69 | class RecommendationManager(RecommendsManager): 70 | def get_queryset(self): 71 | return super(RecommendationManager, self).get_queryset().filter(score__isnull=False) 72 | 73 | def get_or_create_for_object(self, user, object_recommended, object_site): 74 | object_ctype_id = self.get_ctype_id_for_obj(object_recommended) 75 | object_id = object_recommended.id 76 | 77 | return self.get_or_create( 78 | object_ctype_id=object_ctype_id, 79 | object_id=object_id, 80 | object_site=object_site.id, 81 | user=user.id 82 | ) 83 | 84 | def set_score_for_object(self, user, object_recommended, object_site, score): 85 | if score == 0: 86 | self.filter_for_object(object_recommended).filter(user=user.id).delete() 87 | return None 88 | 89 | result, created = self.get_or_create_for_object(user, object_recommended, object_site) 90 | result.score = score 91 | result.save() 92 | return result 93 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Recommendation', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('object_ctype', models.PositiveIntegerField()), 18 | ('object_id', models.PositiveIntegerField()), 19 | ('object_site', models.PositiveIntegerField()), 20 | ('user', models.PositiveIntegerField()), 21 | ('score', models.FloatField(default=None, null=True, blank=True)), 22 | ], 23 | options={ 24 | 'ordering': ['-score'], 25 | }, 26 | bases=(models.Model,), 27 | ), 28 | migrations.CreateModel( 29 | name='Similarity', 30 | fields=[ 31 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 32 | ('object_ctype', models.PositiveIntegerField()), 33 | ('object_id', models.PositiveIntegerField()), 34 | ('object_site', models.PositiveIntegerField()), 35 | ('score', models.FloatField(default=None, null=True, blank=True)), 36 | ('related_object_ctype', models.PositiveIntegerField()), 37 | ('related_object_id', models.PositiveIntegerField()), 38 | ('related_object_site', models.PositiveIntegerField()), 39 | ], 40 | options={ 41 | 'ordering': ['-score'], 42 | 'verbose_name_plural': 'similarities', 43 | }, 44 | bases=(models.Model,), 45 | ), 46 | migrations.AlterUniqueTogether( 47 | name='similarity', 48 | unique_together=set([('object_ctype', 'object_id', 'object_site', 'related_object_ctype', 'related_object_id', 'related_object_site')]), 49 | ), 50 | migrations.AlterUniqueTogether( 51 | name='recommendation', 52 | unique_together=set([('object_ctype', 'object_id', 'user')]), 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/migrations/0002_auto_20141013_2311.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangoorm', '0001_initial'), 11 | ('contenttypes', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='recommendation', 17 | name='object_ctype', 18 | field=models.ForeignKey(to='contenttypes.ContentType'), 19 | ), 20 | migrations.AlterField( 21 | model_name='similarity', 22 | name='object_ctype', 23 | field=models.ForeignKey(to='contenttypes.ContentType'), 24 | ), 25 | migrations.AlterField( 26 | model_name='similarity', 27 | name='related_object_ctype', 28 | field=models.ForeignKey(related_name='similar', to='contenttypes.ContentType'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/storages/djangoorm/migrations/__init__.py -------------------------------------------------------------------------------- /recommends/storages/djangoorm/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.db import models 4 | from django.contrib.contenttypes.fields import GenericForeignKey 5 | from .managers import RecommendsManager, SimilarityManager, RecommendationManager 6 | from django.utils.encoding import python_2_unicode_compatible 7 | 8 | 9 | @python_2_unicode_compatible 10 | class RecommendsBaseModel(models.Model): 11 | """(RecommendsBaseModel description)""" 12 | object_ctype = models.ForeignKey(ContentType) 13 | object_id = models.PositiveIntegerField() 14 | object_site = models.PositiveIntegerField() 15 | object = GenericForeignKey('object_ctype', 'object_id') 16 | 17 | objects = RecommendsManager() 18 | 19 | class Meta: 20 | abstract = True 21 | unique_together = ('object_ctype', 'object_id', 'object_site') 22 | 23 | def __str__(self): 24 | return "RecommendsBaseModel" 25 | 26 | 27 | @python_2_unicode_compatible 28 | class Similarity(RecommendsBaseModel): 29 | """How much an object is similar to another""" 30 | 31 | score = models.FloatField(null=True, blank=True, default=None) 32 | 33 | related_object_ctype = models.ForeignKey(ContentType, related_name='similar') 34 | related_object_id = models.PositiveIntegerField() 35 | related_object_site = models.PositiveIntegerField() 36 | related_object = GenericForeignKey('related_object_ctype', 'related_object_id') 37 | 38 | objects = SimilarityManager() 39 | 40 | class Meta: 41 | verbose_name_plural = 'similarities' 42 | unique_together = ('object_ctype', 'object_id', 'object_site', 'related_object_ctype', 'related_object_id', 'related_object_site') 43 | ordering = ['-score'] 44 | 45 | def __str__(self): 46 | return "Similarity between %s and %s" % (self.object, self.related_object) 47 | 48 | 49 | @python_2_unicode_compatible 50 | class Recommendation(RecommendsBaseModel): 51 | """Recommended an object for a particular user""" 52 | user = models.PositiveIntegerField() 53 | score = models.FloatField(null=True, blank=True, default=None) 54 | 55 | objects = RecommendationManager() 56 | 57 | class Meta: 58 | unique_together = ('object_ctype', 'object_id', 'user') 59 | ordering = ['-score'] 60 | 61 | def __str__(self): 62 | return "Recommendation for user %s" % (self.user) 63 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/routers.py: -------------------------------------------------------------------------------- 1 | from .settings import RECOMMENDS_STORAGE_DATABASE_ALIAS 2 | 3 | 4 | class RecommendsRouter(object): 5 | def db_for_read(self, model, **hints): 6 | "Point all operations on recommends models to RECOMMENDS_STORAGE_DATABASE_NAME" 7 | if model._meta.app_label == 'recommends': 8 | return RECOMMENDS_STORAGE_DATABASE_ALIAS 9 | return None 10 | 11 | def db_for_write(self, model, **hints): 12 | "Point all operations on recommends models to RECOMMENDS_STORAGE_DATABASE_NAME" 13 | if model._meta.app_label == 'recommends': 14 | return RECOMMENDS_STORAGE_DATABASE_ALIAS 15 | return None 16 | 17 | def allow_relation(self, obj1, obj2, **hints): 18 | "Allow any relation if a model in recommends is involved" 19 | if obj1._meta.app_label == 'recommends' or obj2._meta.app_label == 'recommends': 20 | return True 21 | return None 22 | 23 | def allow_syncdb(self, db, model): 24 | "Make sure the recommends app only appears on the RECOMMENDS_STORAGE_DATABASE_NAME db" 25 | if db == RECOMMENDS_STORAGE_DATABASE_ALIAS: 26 | return model._meta.app_label == 'recommends' 27 | elif model._meta.app_label == 'recommends': 28 | return False 29 | return None 30 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | RECOMMENDS_STORAGE_DATABASE_ALIAS = getattr(settings, 'RECOMMENDS_STORAGE_DATABASE_ALIAS', 'recommends') 4 | RECOMMENDS_STORAGE_COMMIT_THRESHOLD = getattr(settings, 'RECOMMENDS_STORAGE_COMMIT_THRESHOLD', 1000) 5 | -------------------------------------------------------------------------------- /recommends/storages/djangoorm/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'Similarity' 12 | db.create_table(u'djangoorm_similarity', ( 13 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('object_ctype', self.gf('django.db.models.fields.PositiveIntegerField')()), 15 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 16 | ('object_site', self.gf('django.db.models.fields.PositiveIntegerField')()), 17 | ('score', self.gf('django.db.models.fields.FloatField')(default=None, null=True, blank=True)), 18 | ('related_object_ctype', self.gf('django.db.models.fields.PositiveIntegerField')()), 19 | ('related_object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 20 | ('related_object_site', self.gf('django.db.models.fields.PositiveIntegerField')()), 21 | )) 22 | db.send_create_signal(u'djangoorm', ['Similarity']) 23 | 24 | # Adding unique constraint on 'Similarity', fields ['object_ctype', 'object_id', 'object_site', 'related_object_ctype', 'related_object_id', 'related_object_site'] 25 | db.create_unique(u'djangoorm_similarity', ['object_ctype', 'object_id', 'object_site', 'related_object_ctype', 'related_object_id', 'related_object_site']) 26 | 27 | # Adding model 'Recommendation' 28 | db.create_table(u'djangoorm_recommendation', ( 29 | (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 30 | ('object_ctype', self.gf('django.db.models.fields.PositiveIntegerField')()), 31 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), 32 | ('object_site', self.gf('django.db.models.fields.PositiveIntegerField')()), 33 | ('user', self.gf('django.db.models.fields.PositiveIntegerField')()), 34 | ('score', self.gf('django.db.models.fields.FloatField')(default=None, null=True, blank=True)), 35 | )) 36 | db.send_create_signal(u'djangoorm', ['Recommendation']) 37 | 38 | # Adding unique constraint on 'Recommendation', fields ['object_ctype', 'object_id', 'user'] 39 | db.create_unique(u'djangoorm_recommendation', ['object_ctype', 'object_id', 'user']) 40 | 41 | 42 | def backwards(self, orm): 43 | # Removing unique constraint on 'Recommendation', fields ['object_ctype', 'object_id', 'user'] 44 | db.delete_unique(u'djangoorm_recommendation', ['object_ctype', 'object_id', 'user']) 45 | 46 | # Removing unique constraint on 'Similarity', fields ['object_ctype', 'object_id', 'object_site', 'related_object_ctype', 'related_object_id', 'related_object_site'] 47 | db.delete_unique(u'djangoorm_similarity', ['object_ctype', 'object_id', 'object_site', 'related_object_ctype', 'related_object_id', 'related_object_site']) 48 | 49 | # Deleting model 'Similarity' 50 | db.delete_table(u'djangoorm_similarity') 51 | 52 | # Deleting model 'Recommendation' 53 | db.delete_table(u'djangoorm_recommendation') 54 | 55 | 56 | models = { 57 | u'djangoorm.recommendation': { 58 | 'Meta': {'ordering': "[u'-score']", 'unique_together': "((u'object_ctype', u'object_id', u'user'),)", 'object_name': 'Recommendation'}, 59 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'object_ctype': ('django.db.models.fields.PositiveIntegerField', [], {}), 61 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 62 | 'object_site': ('django.db.models.fields.PositiveIntegerField', [], {}), 63 | 'score': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), 64 | 'user': ('django.db.models.fields.PositiveIntegerField', [], {}) 65 | }, 66 | u'djangoorm.similarity': { 67 | 'Meta': {'ordering': "[u'-score']", 'unique_together': "((u'object_ctype', u'object_id', u'object_site', u'related_object_ctype', u'related_object_id', u'related_object_site'),)", 'object_name': 'Similarity'}, 68 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 69 | 'object_ctype': ('django.db.models.fields.PositiveIntegerField', [], {}), 70 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 71 | 'object_site': ('django.db.models.fields.PositiveIntegerField', [], {}), 72 | 'related_object_ctype': ('django.db.models.fields.PositiveIntegerField', [], {}), 73 | 'related_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 74 | 'related_object_site': ('django.db.models.fields.PositiveIntegerField', [], {}), 75 | 'score': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) 76 | } 77 | } 78 | 79 | complete_apps = ['djangoorm'] -------------------------------------------------------------------------------- /recommends/storages/djangoorm/south_migrations/0002_auto__chg_field_similarity_related_object_ctype__add_index_similarity_.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from south.utils import datetime_utils as datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | 12 | # Renaming column for 'Similarity.related_object_ctype' to match new field type. 13 | db.rename_column(u'djangoorm_similarity', 'related_object_ctype', 'related_object_ctype_id') 14 | # Changing field 'Similarity.related_object_ctype' 15 | db.alter_column(u'djangoorm_similarity', 'related_object_ctype_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])) 16 | # Adding index on 'Similarity', fields ['related_object_ctype'] 17 | db.create_index(u'djangoorm_similarity', ['related_object_ctype_id']) 18 | 19 | 20 | # Renaming column for 'Similarity.object_ctype' to match new field type. 21 | db.rename_column(u'djangoorm_similarity', 'object_ctype', 'object_ctype_id') 22 | # Changing field 'Similarity.object_ctype' 23 | db.alter_column(u'djangoorm_similarity', 'object_ctype_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])) 24 | # Adding index on 'Similarity', fields ['object_ctype'] 25 | db.create_index(u'djangoorm_similarity', ['object_ctype_id']) 26 | 27 | 28 | # Renaming column for 'Recommendation.object_ctype' to match new field type. 29 | db.rename_column(u'djangoorm_recommendation', 'object_ctype', 'object_ctype_id') 30 | # Changing field 'Recommendation.object_ctype' 31 | db.alter_column(u'djangoorm_recommendation', 'object_ctype_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])) 32 | # Adding index on 'Recommendation', fields ['object_ctype'] 33 | db.create_index(u'djangoorm_recommendation', ['object_ctype_id']) 34 | 35 | 36 | def backwards(self, orm): 37 | # Removing index on 'Recommendation', fields ['object_ctype'] 38 | db.delete_index(u'djangoorm_recommendation', ['object_ctype_id']) 39 | 40 | # Removing index on 'Similarity', fields ['object_ctype'] 41 | db.delete_index(u'djangoorm_similarity', ['object_ctype_id']) 42 | 43 | # Removing index on 'Similarity', fields ['related_object_ctype'] 44 | db.delete_index(u'djangoorm_similarity', ['related_object_ctype_id']) 45 | 46 | 47 | # Renaming column for 'Similarity.related_object_ctype' to match new field type. 48 | db.rename_column(u'djangoorm_similarity', 'related_object_ctype_id', 'related_object_ctype') 49 | # Changing field 'Similarity.related_object_ctype' 50 | db.alter_column(u'djangoorm_similarity', 'related_object_ctype', self.gf('django.db.models.fields.PositiveIntegerField')()) 51 | 52 | # Renaming column for 'Similarity.object_ctype' to match new field type. 53 | db.rename_column(u'djangoorm_similarity', 'object_ctype_id', 'object_ctype') 54 | # Changing field 'Similarity.object_ctype' 55 | db.alter_column(u'djangoorm_similarity', 'object_ctype', self.gf('django.db.models.fields.PositiveIntegerField')()) 56 | 57 | # Renaming column for 'Recommendation.object_ctype' to match new field type. 58 | db.rename_column(u'djangoorm_recommendation', 'object_ctype_id', 'object_ctype') 59 | # Changing field 'Recommendation.object_ctype' 60 | db.alter_column(u'djangoorm_recommendation', 'object_ctype', self.gf('django.db.models.fields.PositiveIntegerField')()) 61 | 62 | models = { 63 | u'contenttypes.contenttype': { 64 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 65 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 68 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 69 | }, 70 | u'djangoorm.recommendation': { 71 | 'Meta': {'ordering': "[u'-score']", 'unique_together': "((u'object_ctype', u'object_id', u'user'),)", 'object_name': 'Recommendation'}, 72 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 73 | 'object_ctype': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 74 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 75 | 'object_site': ('django.db.models.fields.PositiveIntegerField', [], {}), 76 | 'score': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), 77 | 'user': ('django.db.models.fields.PositiveIntegerField', [], {}) 78 | }, 79 | u'djangoorm.similarity': { 80 | 'Meta': {'ordering': "[u'-score']", 'unique_together': "((u'object_ctype', u'object_id', u'object_site', u'related_object_ctype', u'related_object_id', u'related_object_site'),)", 'object_name': 'Similarity'}, 81 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 82 | 'object_ctype': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 83 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 84 | 'object_site': ('django.db.models.fields.PositiveIntegerField', [], {}), 85 | 'related_object_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'similar'", 'to': u"orm['contenttypes.ContentType']"}), 86 | 'related_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), 87 | 'related_object_site': ('django.db.models.fields.PositiveIntegerField', [], {}), 88 | 'score': ('django.db.models.fields.FloatField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) 89 | } 90 | } 91 | 92 | complete_apps = ['djangoorm'] -------------------------------------------------------------------------------- /recommends/storages/djangoorm/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/storages/djangoorm/south_migrations/__init__.py -------------------------------------------------------------------------------- /recommends/storages/djangoorm/storage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | from recommends.storages.base import BaseRecommendationStorage 4 | from recommends.settings import RECOMMENDS_LOGGER_NAME 5 | from .settings import RECOMMENDS_STORAGE_COMMIT_THRESHOLD 6 | from .models import Similarity, Recommendation 7 | 8 | 9 | logger = logging.getLogger(RECOMMENDS_LOGGER_NAME) 10 | 11 | 12 | class DjangoOrmStorage(BaseRecommendationStorage): 13 | def get_similarities_for_object(self, obj, limit=10, raw_id=False): 14 | object_site_id = self.settings.SITE_ID 15 | qs = Similarity.objects.similar_to( 16 | obj, 17 | related_object_site=object_site_id, 18 | score__gt=0).order_by('-score') 19 | if raw_id: 20 | qs = qs.extra( 21 | select={'contect_type_id': 'object_ctype_id'}).values( 22 | 'related_object_id', 'contect_type_id' 23 | ) 24 | return qs[:limit] 25 | 26 | def get_recommendations_for_user(self, user, limit=10, raw_id=False): 27 | object_site_id = self.settings.SITE_ID 28 | qs = Recommendation.objects.filter( 29 | user=user.id, 30 | object_site=object_site_id).order_by('-score') 31 | if raw_id: 32 | qs = qs.extra( 33 | select={'contect_type_id': 'object_ctype_id'}).values( 34 | 'object_id', 'contect_type_id' 35 | ) 36 | return qs[:limit] 37 | 38 | def get_votes(self): 39 | pass 40 | 41 | def store_votes(self, iterable): 42 | pass 43 | 44 | def store_similarities(self, itemMatch): 45 | try: 46 | logger.info('saving similarities') 47 | count = 0 48 | for object_id, scores in itemMatch: 49 | object_target, object_target_site = self.resolve_identifier( 50 | object_id) 51 | 52 | for related_object_id, score in scores: 53 | if not math.isnan(score) and score > self.threshold_similarities: 54 | object_related, object_related_site = self.resolve_identifier( 55 | related_object_id) 56 | if object_target != object_related: 57 | count = count + 1 58 | Similarity.objects.set_score_for_objects( 59 | object_target=object_target, 60 | object_target_site=object_target_site, 61 | object_related=object_related, 62 | object_related_site=object_related_site, 63 | score=score 64 | ) 65 | if count % RECOMMENDS_STORAGE_COMMIT_THRESHOLD == 0: 66 | logger.debug( 67 | 'saved %s similarities...' % 68 | count) 69 | finally: 70 | logger.info('saved %s similarities...' % count) 71 | 72 | def store_recommendations(self, recommendations): 73 | try: 74 | logger.info('saving recommendations') 75 | count = 0 76 | for (user, rankings) in recommendations: 77 | for object_id, score in rankings: 78 | if not math.isnan(score) and score > self.threshold_recommendations: 79 | count = count + 1 80 | object_recommended, site = self.resolve_identifier( 81 | object_id) 82 | Recommendation.objects.set_score_for_object( 83 | user=user, 84 | object_recommended=object_recommended, 85 | object_site=site, 86 | score=score 87 | ) 88 | if count % RECOMMENDS_STORAGE_COMMIT_THRESHOLD == 0: 89 | logger.debug('saved %s recommendations...' % count) 90 | finally: 91 | logger.info('saved %s recommendations...' % count) 92 | 93 | def remove_recommendations(self, obj): 94 | Recommendation.objects.filter_for_object(obj=obj).delete() 95 | 96 | def remove_similarities(self, obj): 97 | Similarity.objects.filter_for_object(obj=obj).delete() 98 | Similarity.objects.filter_for_related_object(related_obj=obj).delete() 99 | -------------------------------------------------------------------------------- /recommends/storages/mongodb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/storages/mongodb/__init__.py -------------------------------------------------------------------------------- /recommends/storages/mongodb/managers.py: -------------------------------------------------------------------------------- 1 | from recommends.managers import DictStorageManager 2 | 3 | 4 | class MongoStorageManager(DictStorageManager): 5 | def filter_for_object(self, obj): 6 | ctype_id = self.get_ctype_id_for_obj(obj) 7 | return {'object_ctype': ctype_id, 'object_id': obj.id} 8 | 9 | def filter_for_related_object(self, related_obj): 10 | ctype_id = self.get_ctype_id_for_obj(related_obj) 11 | return {'related_object_ctype': ctype_id, 'related_object_id': related_obj.id} 12 | -------------------------------------------------------------------------------- /recommends/storages/mongodb/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | RECOMMENDS_STORAGE_MONGODB_DATABASE_DEFAULT = { 4 | 'HOST': 'localhost', 5 | 'PORT': 27017, 6 | 'NAME': 'recommends', 7 | } 8 | RECOMMENDS_STORAGE_MONGODB_DATABASE = getattr(settings, 'RECOMMENDS_STORAGE_MONGODB_DATABASE', RECOMMENDS_STORAGE_MONGODB_DATABASE_DEFAULT) 9 | RECOMMENDS_STORAGE_MONGODB_SIMILARITY_COLLECTION = getattr(settings, 'RECOMMENDS_STORAGE_MONGODB_SIMILARITY_COLLECTION', 'similarity') 10 | RECOMMENDS_STORAGE_MONGODB_RECOMMENDATION_COLLECTION = getattr(settings, 'RECOMMENDS_STORAGE_MONGODB_RECOMMENDATION_COLLECTION', 'recommendation') 11 | RECOMMENDS_STORAGE_MONGODB_FSYNC = getattr(settings, 'RECOMMENDS_STORAGE_MONGODB_FSYNC', False) 12 | -------------------------------------------------------------------------------- /recommends/storages/mongodb/storage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | import pymongo 4 | from recommends.models import MockModel, MockSimilarity 5 | from recommends.storages.base import BaseRecommendationStorage 6 | from recommends.settings import RECOMMENDS_LOGGER_NAME, RECOMMENDS_STORAGE_LOGGING_THRESHOLD 7 | from .settings import ( 8 | RECOMMENDS_STORAGE_MONGODB_DATABASE, 9 | RECOMMENDS_STORAGE_MONGODB_SIMILARITY_COLLECTION, 10 | RECOMMENDS_STORAGE_MONGODB_RECOMMENDATION_COLLECTION, 11 | RECOMMENDS_STORAGE_MONGODB_FSYNC 12 | ) 13 | from .managers import MongoStorageManager 14 | 15 | 16 | logger = logging.getLogger(RECOMMENDS_LOGGER_NAME) 17 | 18 | 19 | class MongoStorage(BaseRecommendationStorage): 20 | manager = MongoStorageManager() 21 | 22 | def _get_mock_models(self, spec, collection_name, limit, raw_id, mock_class=MockModel): 23 | connection = pymongo.Connection(RECOMMENDS_STORAGE_MONGODB_DATABASE['HOST'], RECOMMENDS_STORAGE_MONGODB_DATABASE['PORT']) 24 | db = connection[RECOMMENDS_STORAGE_MONGODB_DATABASE['NAME']] 25 | collection = db[collection_name] 26 | 27 | documents = collection.find(spec, limit=limit, sort=[('score', pymongo.DESCENDING)]) 28 | if raw_id: 29 | if mock_class is MockModel: 30 | return [{ 31 | 'object_id': item['object_id'], 32 | 'contect_type_id': item['object_ctype']} 33 | for item in documents] 34 | elif mock_class is MockSimilarity: 35 | return [{ 36 | 'related_object_id': item['related_object_id'], 37 | 'contect_type_id': item['object_ctype']} 38 | for item in documents] 39 | return map(lambda x: mock_class(**x), documents) 40 | 41 | def get_similarities_for_object(self, obj, limit=10, raw_id=False): 42 | object_site_id = self.settings.SITE_ID 43 | spec = dict(related_object_site=object_site_id, **self.manager.filter_for_object(obj)) 44 | collection_name = RECOMMENDS_STORAGE_MONGODB_SIMILARITY_COLLECTION 45 | 46 | return self._get_mock_models(spec, collection_name, limit, raw_id, mock_class=MockSimilarity) 47 | 48 | def get_recommendations_for_user(self, user, limit=10, raw_id=False): 49 | spec = {'user': user.id, 'object_site': self.settings.SITE_ID} 50 | collection_name = RECOMMENDS_STORAGE_MONGODB_RECOMMENDATION_COLLECTION 51 | 52 | return self._get_mock_models(spec, collection_name, limit, raw_id) 53 | 54 | def get_votes(self): 55 | pass 56 | 57 | def store_votes(self, iterable): 58 | pass 59 | 60 | def store_similarities(self, itemMatch): 61 | connection = pymongo.Connection(RECOMMENDS_STORAGE_MONGODB_DATABASE['HOST'], RECOMMENDS_STORAGE_MONGODB_DATABASE['PORT']) 62 | db = connection[RECOMMENDS_STORAGE_MONGODB_DATABASE['NAME']] 63 | collection = db[RECOMMENDS_STORAGE_MONGODB_SIMILARITY_COLLECTION] 64 | 65 | logger.info('saving similarities') 66 | count = 0 67 | 68 | for object_id, scores in itemMatch: 69 | object_target, object_target_site = self.resolve_identifier(object_id) 70 | 71 | for related_object_id, score in scores: 72 | if not math.isnan(score) and score > self.threshold_similarities: 73 | object_related, object_related_site = self.resolve_identifier(related_object_id) 74 | if object_target != object_related: 75 | spec = self.manager.similarity_for_objects(object_target=object_target, object_target_site=object_target_site, object_related=object_related, object_related_site=object_related_site) 76 | collection.update(spec, {'$set': {'score': score}}, upsert=True, fsync=RECOMMENDS_STORAGE_MONGODB_FSYNC) 77 | count = count + 1 78 | 79 | if count % RECOMMENDS_STORAGE_LOGGING_THRESHOLD == 0: 80 | logger.debug('saved %s similarities...' % count) 81 | 82 | logger.info('saved %s similarities...' % count) 83 | 84 | def store_recommendations(self, recommendations): 85 | connection = pymongo.Connection(RECOMMENDS_STORAGE_MONGODB_DATABASE['HOST'], RECOMMENDS_STORAGE_MONGODB_DATABASE['PORT']) 86 | db = connection[RECOMMENDS_STORAGE_MONGODB_DATABASE['NAME']] 87 | collection = db[RECOMMENDS_STORAGE_MONGODB_RECOMMENDATION_COLLECTION] 88 | 89 | logger.info('saving recommendation') 90 | count = 0 91 | 92 | for (user, rankings) in recommendations: 93 | for object_id, score in rankings: 94 | if not math.isnan(score) and score > self.threshold_recommendations: 95 | count = count + 1 96 | object_recommended, site = self.resolve_identifier(object_id) 97 | spec = self.manager.suggestion_for_object( 98 | user=user, 99 | object_recommended=object_recommended, 100 | object_site=site 101 | ) 102 | collection.update(spec, {'$set': {'score': score}}, upsert=True, fsync=RECOMMENDS_STORAGE_MONGODB_FSYNC) 103 | 104 | if count % RECOMMENDS_STORAGE_LOGGING_THRESHOLD == 0: 105 | logger.debug('saved %s recommendations...' % count) 106 | logger.info('saved %s recommendation...' % count) 107 | 108 | def remove_recommendations(self, obj): 109 | connection = pymongo.Connection(RECOMMENDS_STORAGE_MONGODB_DATABASE['HOST'], RECOMMENDS_STORAGE_MONGODB_DATABASE['PORT']) 110 | db = connection[RECOMMENDS_STORAGE_MONGODB_DATABASE['NAME']] 111 | collection = db[RECOMMENDS_STORAGE_MONGODB_RECOMMENDATION_COLLECTION] 112 | collection.remove(self.manager.filter_for_object(obj), fsync=RECOMMENDS_STORAGE_MONGODB_FSYNC) 113 | 114 | def remove_similarities(self, obj): 115 | connection = pymongo.Connection(RECOMMENDS_STORAGE_MONGODB_DATABASE['HOST'], RECOMMENDS_STORAGE_MONGODB_DATABASE['PORT']) 116 | db = connection[RECOMMENDS_STORAGE_MONGODB_DATABASE['NAME']] 117 | 118 | collection = db[RECOMMENDS_STORAGE_MONGODB_SIMILARITY_COLLECTION] 119 | 120 | collection.remove(self.manager.filter_for_object(obj), fsync=RECOMMENDS_STORAGE_MONGODB_FSYNC) 121 | collection.remove(self.manager.filter_for_related_object(obj), fsync=RECOMMENDS_STORAGE_MONGODB_FSYNC) 122 | -------------------------------------------------------------------------------- /recommends/storages/redis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/storages/redis/__init__.py -------------------------------------------------------------------------------- /recommends/storages/redis/managers.py: -------------------------------------------------------------------------------- 1 | from recommends.managers import DictStorageManager 2 | 3 | 4 | class RedisStorageManager(DictStorageManager): 5 | def similarity_for_objects(self, score, *args, **kwargs): 6 | spec = super(RedisStorageManager, self).similarity_for_objects(*args, **kwargs) 7 | spec['score'] = score 8 | return spec 9 | 10 | def filter_for_object(self, obj): 11 | ctype_id = self.get_ctype_id_for_obj(obj) 12 | return {'object_ctype': ctype_id, 'object_id': obj.id} 13 | 14 | def filter_for_related_object(self, related_obj): 15 | ctype_id = self.get_ctype_id_for_obj(related_obj) 16 | return {'related_object_ctype': ctype_id, 'related_object_id': related_obj.id} 17 | -------------------------------------------------------------------------------- /recommends/storages/redis/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | RECOMMENDS_STORAGE_REDIS_DATABASE_DEFAULT = { 4 | 'HOST': 'localhost', 5 | 'PORT': 6379, 6 | 'NAME': 0 7 | } 8 | 9 | RECOMMENDS_STORAGE_REDIS_DATABASE = getattr(settings, 'RECOMMENDS_STORAGE_REDIS_DATABASE', RECOMMENDS_STORAGE_REDIS_DATABASE_DEFAULT) 10 | -------------------------------------------------------------------------------- /recommends/storages/redis/storage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | import redis 4 | from recommends.models import MockModel, MockSimilarity 5 | from recommends.storages.base import BaseRecommendationStorage 6 | from recommends.settings import RECOMMENDS_LOGGER_NAME, RECOMMENDS_STORAGE_LOGGING_THRESHOLD 7 | from .settings import RECOMMENDS_STORAGE_REDIS_DATABASE 8 | from .managers import RedisStorageManager 9 | 10 | 11 | logger = logging.getLogger(RECOMMENDS_LOGGER_NAME) 12 | 13 | 14 | class RedisStorage(BaseRecommendationStorage): 15 | manager = RedisStorageManager() 16 | 17 | _redis = None 18 | 19 | can_lock = True 20 | LOCK_KEY = 'recommends-redis-lock-key' 21 | 22 | @property 23 | def redis(self): 24 | if self._redis is None: 25 | self._redis = redis.StrictRedis(host=RECOMMENDS_STORAGE_REDIS_DATABASE['HOST'], port=RECOMMENDS_STORAGE_REDIS_DATABASE['PORT'], db=RECOMMENDS_STORAGE_REDIS_DATABASE['NAME']) 26 | return self._redis 27 | 28 | def get_lock(self): 29 | return self.redis.setnx(self.LOCK_KEY, 1) 30 | 31 | def release_lock(self): 32 | return self.redis.delete(self.LOCK_KEY) 33 | 34 | def _get_mock_models(self, dicts, mock_class=MockModel): 35 | return map(lambda x: mock_class(**x), dicts) 36 | 37 | def get_similarities_for_object(self, obj, limit=10, raw_id=False): 38 | r = self.redis 39 | 40 | object_id = self.get_identifier(obj) 41 | key = 'recommends:similarity:%s' % object_id 42 | scores = r.zrevrangebyscore(key, min=0, max=1, num=limit, start=0, withscores=True) 43 | 44 | similarity_dicts = [] 45 | for identifier, score in scores: 46 | similarity_dict = self.identifier_manager.identifier_to_dict(object_id) 47 | similarity_dict.update(self.identifier_manager.identifier_to_dict(identifier, score, related=True)) 48 | similarity_dicts.append(similarity_dict) 49 | if raw_id: 50 | return [{ 51 | 'related_object_id': item['related_object_id'], 52 | 'contect_type_id': item['object_ctype']} 53 | for item in similarity_dicts][:limit] 54 | return self._get_mock_models(similarity_dicts, mock_class=MockSimilarity) 55 | 56 | def get_recommendations_for_user(self, user, limit=10, raw_id=False): 57 | r = self.redis 58 | key = 'recommends:recommendation:%s' % user.id 59 | scores = r.zrevrangebyscore(key, min=0, max=1, num=limit, start=0, withscores=True) 60 | 61 | recommendation_dicts = [self.identifier_manager.identifier_to_dict(object_id, score) for object_id, score in scores] 62 | if raw_id: 63 | return [{ 64 | 'object_id': item['object_id'], 65 | 'contect_type_id': item['object_ctype']} 66 | for item in recommendation_dicts][:limit] 67 | return self._get_mock_models(recommendation_dicts, mock_class=MockModel) 68 | 69 | def get_votes(self): 70 | pass 71 | 72 | def store_votes(self, iterable): 73 | pass 74 | 75 | def store_similarities(self, itemMatch): 76 | r = self.redis 77 | 78 | logger.info('saving similarities') 79 | count = 0 80 | 81 | for object_id, scores in itemMatch: 82 | object_target, object_target_site = self.resolve_identifier(object_id) 83 | 84 | for related_object_id, score in scores: 85 | if not math.isnan(score) and score > self.threshold_similarities: 86 | object_related, object_related_site = self.resolve_identifier(related_object_id) 87 | if object_target != object_related: 88 | key = 'recommends:similarity:%s' % object_id 89 | r.zadd(key, score, related_object_id) 90 | 91 | rev_key = 'recommends:similarity_reverse:%s' % related_object_id 92 | r.sadd(rev_key, object_id) 93 | 94 | index_key = 'recommends:similarity:index' 95 | r.sadd(index_key, object_id) 96 | 97 | count = count + 1 98 | 99 | if count % RECOMMENDS_STORAGE_LOGGING_THRESHOLD == 0: 100 | logger.debug('saved %s similarities...' % count) 101 | logger.info('saved %s similarities...' % count) 102 | 103 | def store_recommendations(self, recommendations): 104 | r = self.redis 105 | 106 | logger.info('saving recommendation') 107 | count = 0 108 | 109 | for (user, rankings) in recommendations: 110 | for object_id, score in rankings: 111 | if not math.isnan(score) and score > self.threshold_recommendations: 112 | key = 'recommends:recommendation:%s' % user.id 113 | r.zadd(key, score, object_id) 114 | 115 | rev_key = 'recommends:recommendation_reverse:%s' % object_id 116 | r.zadd(rev_key, score, user.id) 117 | 118 | index_key = 'recommends:recommendation:index' 119 | r.sadd(index_key, object_id) 120 | 121 | count = count + 1 122 | if count % RECOMMENDS_STORAGE_LOGGING_THRESHOLD == 0: 123 | logger.debug('saved %s recommendations...' % count) 124 | logger.info('saved %s recommendation...' % count) 125 | 126 | def remove_similarities(self, obj): 127 | r = self.redis 128 | object_id = self.get_identifier(obj) 129 | key = 'recommends:similarity:%s' % object_id 130 | r.delete(key) 131 | 132 | rev_key = 'recommends:similarity_reverse:%s' % object_id 133 | values = r.smembers(rev_key) 134 | for value in values: 135 | r.zrem('recommends:similarity:%s' % value, object_id) 136 | r.delete(rev_key) 137 | 138 | index_key = 'recommends:similarity:index' 139 | r.srem(index_key, object_id) 140 | 141 | def remove_recommendations(self, obj): 142 | r = self.redis 143 | object_id = self.get_identifier(obj) 144 | 145 | rev_key = 'recommends:recommendation_reverse:%s' % object_id 146 | user_ids = r.zrevrangebyscore(rev_key, min=0, max=1, start=0, num=r.zcount(rev_key, min=0, max=1)) 147 | for user_id in user_ids: 148 | r.zrem('recommends:recommendation:%s' % user_id, object_id) 149 | r.delete(rev_key) 150 | 151 | index_key = 'recommends:recommendation:index' 152 | r.srem(index_key, object_id) 153 | -------------------------------------------------------------------------------- /recommends/tasks.py: -------------------------------------------------------------------------------- 1 | from celery.task import task, periodic_task 2 | from celery.schedules import crontab 3 | from .utils import filelock 4 | 5 | from .settings import RECOMMENDS_TASK_RUN, RECOMMENDS_TASK_CRONTAB, RECOMMENDS_TASK_EXPIRES 6 | 7 | 8 | def recommends_precompute(): 9 | results = [] 10 | from .providers import recommendation_registry 11 | 12 | # I know this is weird, but it's faster (tested on CPyhton 2.6.5) 13 | def _precompute(provider_instance): 14 | results.append(provider_instance.precompute()) 15 | 16 | if recommendation_registry.storage.can_lock: 17 | locked = recommendation_registry.storage.get_lock() 18 | if locked: 19 | try: 20 | [_precompute(provider_instance) for provider_instance in recommendation_registry.get_vote_providers()] 21 | finally: 22 | recommendation_registry.storage.release_lock() 23 | else: 24 | with filelock('recommends_precompute.lock'): 25 | [_precompute(provider_instance) 26 | for provider_instance in recommendation_registry.get_vote_providers()] 27 | 28 | return results 29 | 30 | 31 | if RECOMMENDS_TASK_RUN: 32 | @periodic_task(name='recommends_precompute', run_every=crontab(**RECOMMENDS_TASK_CRONTAB), expires=RECOMMENDS_TASK_EXPIRES) 33 | def _recommends_precompute(): 34 | recommends_precompute() 35 | 36 | 37 | @task(name='remove_suggestions') 38 | def remove_suggestions(rated_model, object_id): 39 | from django.apps import apps 40 | from recommends.providers import recommendation_registry 41 | 42 | ObjectClass = apps.get_model(*rated_model.split('.')) 43 | provider_instance = recommendation_registry.get_provider_for_content( 44 | ObjectClass) 45 | obj = ObjectClass.objects.get(pk=object_id) 46 | 47 | provider_instance.storage.remove_recommendations(obj) 48 | 49 | 50 | @task(name='remove_similarities') 51 | def remove_similarities(rated_model, object_id): 52 | from django.apps import apps 53 | from recommends.providers import recommendation_registry 54 | 55 | ObjectClass = apps.get_model(*rated_model.split('.')) 56 | provider_instance = recommendation_registry.get_provider_for_content( 57 | ObjectClass) 58 | obj = ObjectClass.objects.get(pk=object_id) 59 | 60 | provider_instance.storage.remove_similarities(obj) 61 | -------------------------------------------------------------------------------- /recommends/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/templatetags/__init__.py -------------------------------------------------------------------------------- /recommends/templatetags/recommends.py: -------------------------------------------------------------------------------- 1 | from ..providers import recommendation_registry 2 | from ..settings import RECOMMENDS_CACHE_TEMPLATETAGS_TIMEOUT 3 | from django import template 4 | from django.core.cache import cache 5 | from django.conf import settings 6 | from django.db import models 7 | register = template.Library() 8 | 9 | 10 | @register.filter 11 | def similarities(obj, limit=5): 12 | """ 13 | Returns a list of Similarity objects, representing how much an object is similar to the given one. 14 | 15 | Usage: 16 | 17 | :: 18 | 19 | {% for similarity in myobj|similar:5 %} 20 | {{ similarity.related_object }} 21 | {% endfor %} 22 | """ 23 | if isinstance(obj, models.Model): 24 | cache_key = 'recommends:similarities:%s:%s.%s:%s:%s' % (settings.SITE_ID, obj._meta.app_label, obj._meta.object_name.lower(), obj.id, limit) 25 | similarities = cache.get(cache_key) 26 | if similarities is None: 27 | provider = recommendation_registry.get_provider_for_content(obj) 28 | similarities = provider.storage.get_similarities_for_object(obj, int(limit)) 29 | cache.set(cache_key, similarities, RECOMMENDS_CACHE_TEMPLATETAGS_TIMEOUT) 30 | return similarities 31 | 32 | 33 | class SuggestionNode(template.Node): 34 | def __init__(self, varname, limit): 35 | self.varname = varname 36 | self.limit = limit 37 | 38 | def render(self, context): 39 | user = context['user'] 40 | if user.is_authenticated(): # We need an id after all 41 | cache_key = 'recommends:recommendations:%s:%s:%s' % (settings.SITE_ID, user.id, self.limit) 42 | suggestions = cache.get(cache_key) 43 | if suggestions is None: 44 | suggestions = set() 45 | for provider in recommendation_registry.providers: 46 | suggestions.update(provider.storage.get_recommendations_for_user(user, int(self.limit))) 47 | cache.set(cache_key, suggestions, RECOMMENDS_CACHE_TEMPLATETAGS_TIMEOUT) 48 | context[self.varname] = suggestions 49 | return '' 50 | 51 | 52 | @register.tag() 53 | def suggested(parser, token): 54 | """ 55 | Returns a list of Recommendation (suggestions of objects) for the current user. 56 | 57 | Usage: 58 | 59 | :: 60 | 61 | {% suggested as suggestions [limit 5] %} 62 | {% for suggested in suggestions %} 63 | {{ suggested.object }} 64 | {% endfor %} 65 | """ 66 | bits = token.contents.split() 67 | varname = bits[2] 68 | try: 69 | limit = int(bits[4]) 70 | except IndexError: 71 | limit = 5 72 | return SuggestionNode(varname, limit) 73 | -------------------------------------------------------------------------------- /recommends/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-recsys/django-recommends/1e9ca31ee0ed7669fe5b6a26552fb5326a646ed1/recommends/tests/__init__.py -------------------------------------------------------------------------------- /recommends/tests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import RecProduct, RecVote 3 | 4 | admin.site.register(RecProduct) 5 | admin.site.register(RecVote) 6 | -------------------------------------------------------------------------------- /recommends/tests/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from django.db import models 3 | from django.contrib.sites.models import Site 4 | from django.contrib.auth import models as auth_models 5 | from django.utils.encoding import python_2_unicode_compatible 6 | 7 | 8 | @python_2_unicode_compatible 9 | class RecProduct(models.Model): 10 | """A generic Product""" 11 | name = models.CharField(blank=True, max_length=100) 12 | sites = models.ManyToManyField(Site) 13 | 14 | class Meta: 15 | app_label = 'recommends' 16 | 17 | def __str__(self): 18 | return self.name 19 | 20 | @models.permalink 21 | def get_absolute_url(self): 22 | return ('product_detail', [self.id]) 23 | 24 | def sites_str(self): 25 | return ', '.join([s.name for s in self.sites.all()]) 26 | sites_str.short_description = 'sites' 27 | 28 | 29 | @python_2_unicode_compatible 30 | class RecVote(models.Model): 31 | """A Vote on a Product""" 32 | user = models.ForeignKey(auth_models.User, related_name='rec_votes') 33 | product = models.ForeignKey(RecProduct) 34 | site = models.ForeignKey(Site) 35 | score = models.FloatField() 36 | 37 | class Meta: 38 | app_label = 'recommends' 39 | 40 | def __str__(self): 41 | return "Vote" 42 | -------------------------------------------------------------------------------- /recommends/tests/recommendations.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from recommends.providers import RecommendationProvider 4 | from recommends.providers import recommendation_registry 5 | try: 6 | from recommends.storages.mongodb.storage import MongoStorage 7 | except ImportError: 8 | MongoStorage = None 9 | try: 10 | from recommends.storages.redis.storage import RedisStorage 11 | except ImportError: 12 | RedisStorage = None 13 | try: 14 | from recommends.algorithms.pyrecsys import RecSysAlgorithm 15 | except ImportError: 16 | RecSysAlgorithm = None 17 | try: 18 | from recommends.tasks import recommends_precompute 19 | except ImportError: 20 | recommends_precompute = None 21 | 22 | if recommends_precompute is not None: 23 | from recommends.tests.tests import RecommendsTestCase 24 | 25 | from recommends.tests.models import RecProduct, RecVote 26 | 27 | 28 | class ProductRecommendationProvider(RecommendationProvider): 29 | 30 | def get_users(self): 31 | return User.objects.filter(is_active=True, rec_votes__isnull=False).distinct() 32 | 33 | def get_items(self): 34 | return RecProduct.objects.all() 35 | 36 | def get_ratings(self, obj): 37 | return RecVote.objects.filter(product=obj) 38 | 39 | def get_rating_score(self, rating): 40 | return rating.score 41 | 42 | def get_rating_site(self, rating): 43 | return rating.site 44 | 45 | def get_rating_user(self, rating): 46 | return rating.user 47 | 48 | def get_rating_item(self, rating): 49 | return rating.product 50 | 51 | recommendation_registry.register( 52 | RecVote, 53 | [RecProduct], 54 | ProductRecommendationProvider) 55 | 56 | 57 | class GhettoRecommendationProvider(RecommendationProvider): 58 | 59 | def get_users(self): 60 | return User.objects.filter(is_active=True, rec_votes__isnull=False).distinct() 61 | 62 | def get_items(self): 63 | return RecProduct.objects.all() 64 | 65 | def get_ratings(self, obj): 66 | return RecVote.objects.filter(product=obj) 67 | 68 | def get_rating_score(self, rating): 69 | return rating.score 70 | 71 | def get_rating_site(self, rating): 72 | return rating.site 73 | 74 | def get_rating_user(self, rating): 75 | return rating.user 76 | 77 | def get_rating_item(self, rating): 78 | return rating.product 79 | 80 | 81 | if recommends_precompute is not None and RecSysAlgorithm is not None and getattr(settings, 'RECOMMENDS_TEST_RECSYS', False): 82 | class RecSysRecommendationProvider(ProductRecommendationProvider): 83 | algorithm = RecSysAlgorithm() 84 | 85 | class RecSysAlgoTestCase(RecommendsTestCase): 86 | results = { 87 | 'len_recommended': 4, 88 | 'len_similar_to_mug': 5 89 | } 90 | 91 | def setUp(self): 92 | recommendation_registry.unregister( 93 | RecVote, 94 | [RecProduct], 95 | ProductRecommendationProvider) 96 | recommendation_registry.register( 97 | RecVote, 98 | [RecProduct], 99 | RecSysRecommendationProvider) 100 | super(RecSysAlgoTestCase, self).setUp() 101 | 102 | def tearDown(self): 103 | super(RecSysAlgoTestCase, self).tearDown() 104 | recommendation_registry.unregister( 105 | RecVote, 106 | [RecProduct], 107 | RecSysRecommendationProvider) 108 | recommendation_registry.register( 109 | RecVote, 110 | [RecProduct], 111 | ProductRecommendationProvider) 112 | 113 | if recommends_precompute is not None and RedisStorage is not None and getattr(settings, 'RECOMMENDS_TEST_REDIS', False): 114 | class RedisRecommendationProvider(GhettoRecommendationProvider): 115 | storage = RedisStorage(settings=settings) 116 | 117 | class RecommendsRedisStorageTestCase(RecommendsTestCase): 118 | 119 | def setUp(self): 120 | recommendation_registry.unregister( 121 | RecVote, 122 | [RecProduct], 123 | ProductRecommendationProvider) 124 | recommendation_registry.register( 125 | RecVote, 126 | [RecProduct], 127 | RedisRecommendationProvider) 128 | super(RecommendsRedisStorageTestCase, self).setUp() 129 | 130 | def tearDown(self): 131 | super(RecommendsRedisStorageTestCase, self).tearDown() 132 | recommendation_registry.unregister( 133 | RecVote, 134 | [RecProduct], 135 | RedisRecommendationProvider) 136 | recommendation_registry.register( 137 | RecVote, 138 | [RecProduct], 139 | ProductRecommendationProvider) 140 | 141 | if recommends_precompute is not None and MongoStorage is not None and getattr(settings, 'RECOMMENDS_TEST_MONGO', False): 142 | class MongoRecommendationProvider(GhettoRecommendationProvider): 143 | storage = MongoStorage(settings=settings) 144 | 145 | class RecommendsMongoStorageTestCase(RecommendsTestCase): 146 | 147 | def setUp(self): 148 | recommendation_registry.unregister( 149 | RecVote, 150 | [RecProduct], 151 | ProductRecommendationProvider) 152 | recommendation_registry.register( 153 | RecVote, 154 | [RecProduct], 155 | MongoRecommendationProvider) 156 | super(RecommendsMongoStorageTestCase, self).setUp() 157 | 158 | def tearDown(self): 159 | super(RecommendsMongoStorageTestCase, self).tearDown() 160 | recommendation_registry.unregister( 161 | RecVote, 162 | [RecProduct], 163 | MongoRecommendationProvider) 164 | recommendation_registry.register( 165 | RecVote, 166 | [RecProduct], 167 | ProductRecommendationProvider) 168 | -------------------------------------------------------------------------------- /recommends/tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | from os import path 4 | 5 | import django 6 | from django.test.utils import get_runner 7 | 8 | from django.conf import settings 9 | 10 | PROJECT_DIR = path.dirname(path.realpath(__file__)) 11 | 12 | 13 | settings.configure( 14 | DATABASES={ 15 | 'default': {'ENGINE': 'django.db.backends.sqlite3'} 16 | }, 17 | INSTALLED_APPS=[ 18 | 'django.contrib.auth', 19 | 'django.contrib.sessions', 20 | 'django.contrib.contenttypes', 21 | 'django.contrib.sites', 22 | 'recommends', 23 | 'recommends.storages.djangoorm', 24 | 'recommends.tests', 25 | ], 26 | ROOT_URLCONF='recommends.tests.urls', 27 | TEMPLATES=[ 28 | { 29 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 30 | 'APP_DIRS': True, 31 | 'DIRS': [ 32 | path.join(PROJECT_DIR, 'templates'), 33 | ], 34 | 'OPTIONS': { 35 | 'context_processors': [ 36 | 'django.contrib.auth.context_processors.auth', 37 | 'django.template.context_processors.debug', 38 | 'django.template.context_processors.i18n', 39 | 'django.template.context_processors.media', 40 | 'django.template.context_processors.static', 41 | 'django.template.context_processors.tz', 42 | 'django.contrib.messages.context_processors.messages', 43 | ], 44 | }, 45 | }, 46 | ], 47 | MIDDLEWARE_CLASSES=( 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | ), 54 | BROKER_URL='redis://localhost:6379/0', 55 | CELERY_ALWAYS_EAGER=True, 56 | ALLOWED_HOSTS=['*'], 57 | SITE_ID=1, 58 | RECOMMENDS_TEST_REDIS=True, 59 | RECOMMENDS_TEST_MONGO=True, 60 | RECOMMENDS_TEST_RECSYS=True, 61 | RECOMMENDS_STORAGE_MONGODB_FSYNC=True, 62 | RECOMMENDS_TASK_RUN=True, 63 | ) 64 | 65 | 66 | def runtests(*test_args): 67 | django.setup() 68 | runner_class = get_runner(settings) 69 | test_runner = runner_class(verbosity=1, interactive=True) 70 | failures = test_runner.run_tests(['recommends']) 71 | sys.exit(failures) 72 | 73 | if __name__ == '__main__': 74 | runtests() 75 | -------------------------------------------------------------------------------- /recommends/tests/settings.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | 5 | # Django settings for example_project project. 6 | PROJECT_DIR = path.dirname(path.realpath(__file__)) 7 | 8 | DEBUG = True 9 | TEMPLATE_DEBUG = DEBUG 10 | 11 | ADMINS = ( 12 | # ('Your Name', 'your_email@example.com'), 13 | ) 14 | 15 | MANAGERS = ADMINS 16 | 17 | # Local time zone for this installation. Choices can be found here: 18 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 19 | # although not all choices may be available on all operating systems. 20 | # On Unix systems, a value of None will cause Django to use the same 21 | # timezone as the operating system. 22 | # If running in a Windows environment this must be set to the same as your 23 | # system time zone. 24 | TIME_ZONE = 'America/Chicago' 25 | 26 | # Language code for this installation. All choices can be found here: 27 | # http://www.i18nguy.com/unicode/language-identifiers.html 28 | LANGUAGE_CODE = 'en-us' 29 | 30 | SITE_ID = 1 31 | 32 | # If you set this to False, Django will make some optimizations so as not 33 | # to load the internationalization machinery. 34 | USE_I18N = True 35 | 36 | # If you set this to False, Django will not format dates, numbers and 37 | # calendars according to the current locale 38 | USE_L10N = True 39 | 40 | # Absolute filesystem path to the directory that will hold user-uploaded files. 41 | # Example: "/home/media/media.lawrence.com/media/" 42 | MEDIA_ROOT = '' 43 | 44 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 45 | # trailing slash. 46 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 47 | MEDIA_URL = '' 48 | 49 | # Absolute path to the directory static files should be collected to. 50 | # Don't put anything in this directory yourself; store your static files 51 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 52 | # Example: "/home/media/media.lawrence.com/static/" 53 | STATIC_ROOT = '' 54 | 55 | # URL prefix for static files. 56 | # Example: "http://media.lawrence.com/static/" 57 | STATIC_URL = '/static/' 58 | 59 | # URL prefix for admin static files -- CSS, JavaScript and images. 60 | # Make sure to use a trailing slash. 61 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 62 | ADMIN_MEDIA_PREFIX = '/static/admin/' 63 | 64 | # Additional locations of static files 65 | STATICFILES_DIRS = ( 66 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 67 | # Always use forward slashes, even on Windows. 68 | # Don't forget to use absolute paths, not relative paths. 69 | ) 70 | 71 | # List of finder classes that know how to find static files in 72 | # various locations. 73 | STATICFILES_FINDERS = ( 74 | 'django.contrib.staticfiles.finders.FileSystemFinder', 75 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 76 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 77 | ) 78 | 79 | # Make this unique, and don't share it with anybody. 80 | SECRET_KEY = '91tj&kv4c5(8f1t_@c0hs-%bkp7r4e*bmjet3ph=8-6iw#v7kq' 81 | 82 | # List of callables that know how to import templates from various sources. 83 | TEMPLATE_LOADERS = ( 84 | 'django.template.loaders.filesystem.Loader', 85 | 'django.template.loaders.app_directories.Loader', 86 | # 'django.template.loaders.eggs.Loader', 87 | ) 88 | 89 | MIDDLEWARE_CLASSES = ( 90 | 'django.middleware.common.CommonMiddleware', 91 | 'django.contrib.sessions.middleware.SessionMiddleware', 92 | 'django.middleware.csrf.CsrfViewMiddleware', 93 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 94 | 'django.contrib.messages.middleware.MessageMiddleware', 95 | ) 96 | 97 | ROOT_URLCONF = 'recommends.tests.urls' 98 | 99 | TEMPLATE_DIRS = ( 100 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 101 | # Always use forward slashes, even on Windows. 102 | # Don't forget to use absolute paths, not relative paths. 103 | path.join(PROJECT_DIR, 'templates'), 104 | ) 105 | 106 | INSTALLED_APPS = ( 107 | 'django.contrib.auth', 108 | 'django.contrib.contenttypes', 109 | 'django.contrib.sessions', 110 | 'django.contrib.sites', 111 | 'django.contrib.messages', 112 | 'django.contrib.staticfiles', 113 | 114 | 'recommends', 115 | 'recommends.storages.djangoorm', 116 | 'recommends.tests', 117 | # Uncomment the next line to enable the admin: 118 | 'django.contrib.admin', 119 | # Uncomment the next line to enable admin documentation: 120 | # 'django.contrib.admindocs', 121 | ) 122 | 123 | # A sample logging configuration. The only tangible logging 124 | # performed by this configuration is to send an email to 125 | # the site admins on every HTTP 500 error. 126 | # See http://docs.djangoproject.com/en/dev/topics/logging for 127 | # more details on how to customize your logging configuration. 128 | LOGGING = { 129 | 'version': 1, 130 | 'disable_existing_loggers': False, 131 | 'handlers': { 132 | 'mail_admins': { 133 | 'level': 'ERROR', 134 | 'class': 'django.utils.log.AdminEmailHandler' 135 | } 136 | }, 137 | 'loggers': { 138 | 'django.request': { 139 | 'handlers': ['mail_admins'], 140 | 'level': 'ERROR', 141 | 'propagate': True, 142 | }, 143 | } 144 | } 145 | 146 | CACHES = { 147 | 'default': { 148 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 149 | } 150 | } 151 | 152 | CELERY_ALWAYS_EAGER = True 153 | ALLOWED_HOSTS = ['*'] 154 | 155 | BROKER_URL = 'redis://localhost:6379/0' 156 | 157 | DATABASES = { 158 | 'default': { 159 | 'ENGINE': 'django.db.backends.sqlite3', 160 | # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 161 | 'NAME': ':memory:', 162 | # Or path to database file if using sqlite3. 163 | 'USER': '', # Not used with sqlite3. 164 | 'PASSWORD': '', # Not used with sqlite3. 165 | 'HOST': '', 166 | # Set to empty string for localhost. Not used with sqlite3. 167 | 'PORT': '', 168 | # Set to empty string for default. Not used with sqlite3. 169 | } 170 | } 171 | 172 | RECOMMENDS_TEST_RECSYS = sys.version_info[0] == 2 173 | -------------------------------------------------------------------------------- /recommends/tests/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Django Recommends 6 | 7 | 8 | {% if user.is_anonymous %} 9 |
{% csrf_token %} 10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 | {% else %} 23 |

Welcome, {{user.username}}. Logout.

24 | {% endif %} 25 | {% block content %}{% endblock %} 26 | 27 | 28 | -------------------------------------------------------------------------------- /recommends/tests/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load recommends %} 3 | 4 | {% block content %} 5 | {% suggested as suggestions limit 5 %} 6 | {% if user.is_authenticated %} 7 | {% if suggestions %} 8 |

Suggestions for you:

9 | 14 | 15 | {% endif %} 16 | {% endif %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /recommends/tests/templates/recommends/recproduct_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load recommends %} 3 | 4 | {% block content %} 5 |

{{object.name}}

6 | {% with object|similarities as similarities %} 7 | {% if similarities %} 8 | 13 | {% endif %} 14 | {% endwith %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /recommends/tests/tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | import timeit 5 | 6 | from django.contrib.auth.models import User 7 | from django.core.urlresolvers import reverse 8 | from django.test import Client, TestCase 9 | from django.test.utils import override_settings 10 | 11 | from recommends.algorithms.ghetto import GhettoAlgorithm 12 | from recommends.providers import RecommendationProvider, recommendation_registry 13 | from recommends.tasks import recommends_precompute 14 | 15 | from .models import RecProduct, RecVote 16 | 17 | 18 | @override_settings(CELERY_DB_REUSE_MAX=200, LANGUAGES=( 19 | ('en', 'English'),), 20 | LANGUAGE_CODE='en', 21 | TEMPLATE_DIRS=( 22 | os.path.join(os.path.dirname(__file__), 'templates'),), 23 | TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',), 24 | USE_TZ=False, ) 25 | class RecommendsTestCase(TestCase): 26 | fixtures = ['products.json'] 27 | urls = 'recommends.tests.urls' 28 | 29 | results = { 30 | 'len_recommended': 2, 31 | 'len_similar_to_mug': 2 32 | } 33 | 34 | def setUp(self): 35 | self.client = Client() 36 | self.mug = RecProduct.objects.get(name='Coffee Mug') 37 | self.orange_juice = RecProduct.objects.get(name='Orange Juice') 38 | self.wine = RecProduct.objects.get(name='Bottle of Red Wine') 39 | RecProduct.objects.get(name='1lb Tenderloin Steak').delete() 40 | self.user1 = User.objects.get(username='user1') 41 | self.user1.set_password('user1') 42 | self.user1.save() 43 | 44 | from django.template import loader 45 | loader.template_source_loaders = None 46 | 47 | self.provider = recommendation_registry.get_provider_for_content(RecProduct) 48 | recommends_precompute() 49 | 50 | def tearDown(self): 51 | from django.template import loader 52 | loader.template_source_loaders = None 53 | super(RecommendsTestCase, self).tearDown() 54 | 55 | def isObjectWithIdExists(self, object_id): 56 | return RecProduct.objects.filter(id=object_id).exists() 57 | 58 | def test_similarities(self): 59 | # test similarities objects 60 | similar_to_mug = self.provider.storage.get_similarities_for_object(self.mug) 61 | self.assertNotEquals(len(similar_to_mug), 0) 62 | self.assertEquals(len(similar_to_mug), self.results['len_similar_to_mug']) 63 | self.assertTrue(self.wine in [s.related_object for s in similar_to_mug]) 64 | # Make sure we didn't get all 0s 65 | zero_scores = list(filter(lambda x: x.score == 0, similar_to_mug)) 66 | self.assertNotEquals(len(zero_scores), len(similar_to_mug)) 67 | 68 | def test_similarities_raw_ids(self): 69 | # test similarities raw ids 70 | similar_to_mug_ids = self.provider.storage.get_similarities_for_object(self.mug, raw_id=True) 71 | self.assertNotEquals(len(similar_to_mug_ids), 0) 72 | self.assertEquals(len(similar_to_mug_ids), self.results['len_similar_to_mug']) 73 | similar_to_mug_related_ids = [item['related_object_id'] for item in similar_to_mug_ids] 74 | self.assertTrue(self.wine.id in similar_to_mug_related_ids) 75 | self.assertTrue(all([self.isObjectWithIdExists(related_object_id) for related_object_id in similar_to_mug_related_ids])) 76 | 77 | def test_recommendation(self): 78 | # test recommendations 79 | recommendations = self.provider.storage.get_recommendations_for_user(self.user1) 80 | self.assertNotEquals(len(recommendations), 0) 81 | self.assertEquals(len(recommendations), self.results['len_recommended']) 82 | self.assertTrue(self.wine in [s.object for s in recommendations]) 83 | # Make sure we didn't get all 0s 84 | zero_scores = list(filter(lambda x: x.score == 0, recommendations)) 85 | self.assertNotEquals(len(zero_scores), len(recommendations)) 86 | # Make sure we don't recommend item that the user already have 87 | self.assertFalse(self.mug in [v.product for v in RecVote.objects.filter(user=self.user1)]) 88 | 89 | def test_recommendation_raw_ids(self): 90 | # test recommendation raw ids 91 | recommendation_ids = self.provider.storage.get_recommendations_for_user(self.user1, raw_id=True) 92 | self.assertNotEquals(len(recommendation_ids), 0) 93 | self.assertEquals(len(recommendation_ids), self.results['len_recommended']) 94 | recommendation_object_ids = [item['object_id'] for item in recommendation_ids] 95 | self.assertTrue(self.wine.id in recommendation_object_ids) 96 | self.assertTrue(all([self.isObjectWithIdExists(object_id) for object_id in recommendation_object_ids])) 97 | 98 | def test_views(self): 99 | self.client.login(username='user1', password='user1') 100 | 101 | response = self.client.get(self.mug.get_absolute_url()) 102 | self.assertContains(response, self.orange_juice.get_absolute_url()) 103 | 104 | def _test_performance(self): 105 | stmt = """recommends_precompute()""" 106 | setup = """from recommends.tasks import recommends_precompute""" 107 | print("timing...") 108 | times = timeit.repeat(stmt, setup, number=100) 109 | print(times) 110 | 111 | 112 | class RecommendsListenersTestCase(TestCase): 113 | fixtures = ['products.json'] 114 | 115 | def setUp(self): 116 | self.client = Client() 117 | self.mug = RecProduct.objects.get(name='Coffee Mug') 118 | self.orange_juice = RecProduct.objects.get(name='Orange Juice') 119 | self.wine = RecProduct.objects.get(name='Bottle of Red Wine') 120 | self.steak = RecProduct.objects.get(name='1lb Tenderloin Steak') 121 | self.user1 = User.objects.get(username='user1') 122 | self.user1.set_password('user1') 123 | self.user1.save() 124 | 125 | self.provider = recommendation_registry.get_provider_for_content(RecProduct) 126 | 127 | recommends_precompute() 128 | 129 | def test_listeners(self): 130 | self.client.login(username='user1', password='user1') 131 | response = self.client.get(reverse('home')) 132 | 133 | self.vote = RecVote.objects.create( 134 | user=self.user1, 135 | product=self.steak, 136 | site_id=1, 137 | score=1 138 | ) 139 | 140 | response = self.client.get(reverse('home')) 141 | steak_url = self.steak.get_absolute_url() 142 | self.assertContains(response, steak_url) 143 | recommendations = self.provider.storage.get_recommendations_for_user(self.user1) 144 | steak_recs = list(filter(lambda x: x.object_id == self.steak.id, recommendations)) 145 | self.assertEqual(1, len(steak_recs)) 146 | 147 | self.steak.delete() 148 | 149 | response = self.client.get(reverse('home')) 150 | self.assertNotContains(response, steak_url) 151 | recommendations = self.provider.storage.get_recommendations_for_user(self.user1) 152 | steak_recs = list(filter(lambda x: x.object_id == self.steak.id, recommendations)) 153 | self.assertEqual(0, len(steak_recs)) 154 | 155 | def tearDown(self): 156 | self.vote.delete() 157 | recommends_precompute() 158 | 159 | 160 | class GhettoAlgoTestCase(TestCase): 161 | def test_ghetto_algo_warning(self): 162 | with warnings.catch_warnings(record=True) as w: 163 | # Cause all warnings to always be triggered. 164 | warnings.simplefilter("always") 165 | 166 | class GhettAlgoRecommendationProvider(RecommendationProvider): 167 | algorithm = GhettoAlgorithm() 168 | 169 | self.assertEqual(len(w), 1) 170 | self.assertTrue(issubclass(w[-1].category, PendingDeprecationWarning)) 171 | -------------------------------------------------------------------------------- /recommends/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from django.views.generic import TemplateView 4 | 5 | from django.contrib import admin 6 | 7 | from .views import login, RecProductView 8 | 9 | urlpatterns = [ 10 | # Examples: 11 | # url(r'^$', 'example_project.views.home', name='home'), 12 | # url(r'^example_project/', 13 | # include('example_project.foo.urls')), 14 | 15 | # Uncomment the admin/doc line below to enable admin documentation: 16 | # url(r'^admin/doc/', 17 | # include('django.contrib.admindocs.urls')), 18 | 19 | # Uncomment the next line to enable the admin: 20 | url(r'^admin/', admin.site.urls), 21 | url(r'^login/', 22 | login, name='login'), 23 | url(r'^product/(?P\d+)/$', 24 | RecProductView.as_view(), name='product_detail'), 25 | url(r'^$', TemplateView.as_view( 26 | template_name='home.html'), name='home'), 27 | ] 28 | -------------------------------------------------------------------------------- /recommends/tests/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from django.contrib.auth import authenticate, login as _login 3 | from django.http import HttpResponseRedirect 4 | from django.views.generic import DetailView 5 | 6 | from .models import RecProduct 7 | 8 | 9 | def login(request): 10 | username = request.POST['username'] 11 | password = request.POST['password'] 12 | user = authenticate(username=username, password=password) 13 | if user is not None: 14 | if user.is_active: 15 | _login(request, user) 16 | # redirect to a success page. 17 | else: 18 | # Return a 'disabled account' error message 19 | pass 20 | else: 21 | pass 22 | # Return an 'invalid login' error message. 23 | return HttpResponseRedirect(request.POST.get('next', '/')) 24 | 25 | 26 | class RecProductView(DetailView): 27 | model = RecProduct 28 | -------------------------------------------------------------------------------- /recommends/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import errno 3 | import os 4 | import time 5 | import tempfile 6 | import importlib 7 | 8 | 9 | def import_from_classname(class_name_str): 10 | module, class_name = class_name_str.rsplit('.', 1) 11 | Class = getattr(importlib.import_module(module), class_name) 12 | return Class 13 | 14 | 15 | def ctypes_dict(): 16 | from django.contrib.contenttypes.models import ContentType 17 | 18 | values = ContentType.objects.values_list('app_label', 'model', 'id') 19 | ctypes = {} 20 | [ctypes.update({"%s.%s" % x[:2]: x[2]}) for x in values] 21 | return ctypes 22 | 23 | 24 | @contextlib.contextmanager 25 | def filelock(name, wait_delay=.1): 26 | path = os.path.join(tempfile.gettempdir(), name) 27 | while True: 28 | try: 29 | fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR) 30 | except OSError as e: 31 | if e.errno != errno.EEXIST: 32 | raise 33 | time.sleep(wait_delay) 34 | continue 35 | else: 36 | break 37 | try: 38 | yield fd 39 | finally: 40 | os.close(fd) 41 | os.unlink(path) 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.7 2 | celery>=3.0 3 | python-dateutil>=2.1 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-coverage = true 3 | cover-package = recommends 4 | cover-html = true 5 | cover-erase = true 6 | cover-inclusive = true 7 | 8 | [wheel] 9 | universal = 1 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | VERSION = "0.4.0" 5 | 6 | 7 | def read(fname): 8 | try: 9 | with open(os.path.join(os.path.dirname(__file__), fname)) as fh: 10 | return fh.read() 11 | except IOError: 12 | return '' 13 | 14 | 15 | requirements = read('requirements.txt').splitlines() 16 | 17 | setup( 18 | name="django-recommends", 19 | version=VERSION, 20 | description="A django app that builds item-based suggestions for users", 21 | long_description=read('README.rst'), 22 | url='https://github.com/python-recsys/django-recommends', 23 | license='MIT', 24 | author='Flavio Curella', 25 | author_email='flavio.curella@gmail.com', 26 | packages=find_packages(exclude=['tests']), 27 | classifiers=[ 28 | 'Development Status :: 4 - Beta', 29 | 'Environment :: Console', 30 | 'Intended Audience :: Developers', 31 | 'Intended Audience :: Information Technology', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Programming Language :: Python :: 3.3', 36 | 'Programming Language :: Python :: 3.4', 37 | 'Programming Language :: Python :: 3.5', 38 | 'Framework :: Django', 39 | 'Topic :: Scientific/Engineering :: Information Analysis', 40 | ], 41 | install_requires=requirements, 42 | ) 43 | -------------------------------------------------------------------------------- /test-requirements-py2.7.txt: -------------------------------------------------------------------------------- 1 | -r test-requirements.txt 2 | 3 | git+https://github.com/ocelma/python-recsys.git 4 | -------------------------------------------------------------------------------- /test-requirements-py3.3.txt: -------------------------------------------------------------------------------- 1 | -r test-requirements.txt -------------------------------------------------------------------------------- /test-requirements-py3.4.txt: -------------------------------------------------------------------------------- 1 | -r test-requirements.txt -------------------------------------------------------------------------------- /test-requirements-py3.5.txt: -------------------------------------------------------------------------------- 1 | -r test-requirements.txt -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | redis==2.7.4 2 | pymongo==2.5.2 3 | flake8>=2.0 4 | --------------------------------------------------------------------------------