Changeset 410 in main


Ignore:
Timestamp:
10/25/11 09:56:50 (8 years ago)
Author:
pcosquer
Message:

Add search with haystack and xapian:

  • new search form (simple)
  • new dependencies:
    • django-haystack 1.2.x
    • python-xapian (a modified version of xapian-haystack is shipped)
    • celery (tested: 2.3.3)
    • a celery broker (tested with rabbitmq)
    • optional:
      • tracker (/usr/lib/tracker/tracker-extract is called by extractor.sh,

indexing is not required)

  • the search index must be build:

run ./manage.py syncdb && ./manage.py rebuild_index

  • you may need to configure your celery broker:

example for rabbitmq:

rabbitmqctl add_user openplm 'a secret password but not this one'
rabbitmqctl add_vhost openplm
rabbitmqctl set_permissions -p openplm openplm ".*" ".*" ".*"
and modify the settings.py file

  • celeryd must be run:

run ./manage.py celeryd (as the same user who runs the server)


Documentation on how to install/configure/deploy this will come later.

Location:
trunk/openPLM
Files:
16 added
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/openPLM/apache/django.wsgi

    r192 r410  
    33 
    44os.environ['DJANGO_SETTINGS_MODULE'] = 'openPLM.settings' 
     5os.environ["CELERY_LOADER"] = "django" 
    56 
    67sys.path.append('/var/django/openPLM/trunk/') 
  • trunk/openPLM/media/css/openplm.css

    r403 r410  
    682682} 
    683683 
     684/** simple search **/ 
     685li.Result div.summary { 
     686    padding-left: 1em; 
     687} 
  • trunk/openPLM/media/css/timeline.css

    r394 r410  
    6161    margin-bottom: 1em; 
    6262} 
     63 
     64dd p br { 
     65    clear:none !important; 
     66} 
  • trunk/openPLM/media/js/navigate.js

    r397 r410  
    223223                        accept:  
    224224                function (child_tr){ 
    225                     if (child_tr.is("tr.Content, tr.Content2")){ 
     225                    if (child_tr.is("li.Result")){ 
    226226                        return (can_add_child($(this), $("form", child_tr), cache1) || 
    227227                                can_attach($(this), $("form", child_tr), cache2) 
     
    368368 
    369369        // add on drag and drop 
    370         $("tr.Content").add("tr.Content2").css("z-index", "99"); 
    371         $("tr.Content").add("tr.Content2").draggable({ helper: 'clone' }); 
     370        $("li.Result").draggable({  
     371            zIndex: 2000, 
     372            cursor:"pointer", 
     373            cursorAt: { left: 16, top: -16} , 
     374            helper: function (){ 
     375                var div = $(this).find("div.reference").clone(); 
     376                div.addClass("dragged"); 
     377                return div; 
     378                }}); 
    372379        $("#navAddForm").dialog({ 
    373380            autoOpen: false, 
  • trunk/openPLM/plmapp/base_views.py

    r392 r410  
    4646from django.contrib.auth.decorators import login_required 
    4747 
     48from haystack.query import SearchQuerySet 
     49 
    4850import openPLM.plmapp.models as models 
    4951from openPLM.plmapp.controllers import get_controller, \ 
     
    5456from openPLM.plmapp.navigate import NavigationGraph 
    5557from openPLM.plmapp.forms import TypeForm, TypeFormWithoutUser, get_navigate_form, \ 
    56         get_search_form 
     58        SimpleSearchForm 
    5759 
    5860def get_obj(obj_type, obj_ref, obj_revi, user): 
     
    247249        'object_revision': init_revision, 
    248250        'object_type': init_type_, 
     251        'search_query' : "", 
    249252        'THUMBNAILS_URL' : settings.THUMBNAILS_URL, 
    250253        'LANGUAGES' : settings.LANGUAGES, 
     
    295298        if type_form.is_valid(): 
    296299            cls = models.get_all_users_and_plmobjects()[type_form.cleaned_data["type"]] 
    297         attributes_form = get_search_form(cls, request_dict.GET) 
     300        attributes_form = SimpleSearchForm(request_dict.GET) 
    298301        request_dict.session.update(request_dict.GET.items()) 
    299302        search_need = True 
     
    302305        type_form4creation = TypeFormWithoutUser(request_dict.session) 
    303306        cls = models.get_all_users_and_plmobjects()[request_dict.session["type"]] 
    304         attributes_form = get_search_form(cls, request_dict.session) 
     307        attributes_form = SimpleSearchForm(request_dict.session) 
    305308    else: 
    306309        type_form = TypeForm() 
    307310        type_form4creation = TypeFormWithoutUser() 
    308311        request_dict.session['type'] = 'Part' 
    309         attributes_form = get_search_form(models.Part) 
     312        attributes_form = SimpleSearchForm() 
    310313    if attributes_form.is_valid() and search_need: 
    311         qset = cls.objects.all() 
    312         qset = attributes_form.search(qset)[:30] 
     314        m = {} 
     315        models._get_all_subclasses(cls, m) 
     316        search_query = attributes_form.cleaned_data["q"] 
     317        mods = m.values() 
     318        if issubclass(cls, models.Document): 
     319            mods.append(models.DocumentFile) 
     320        qset = attributes_form.search(*mods)[:30] 
    313321        if qset is None: 
    314322            qset = [] 
    315         if issubclass(cls, User): 
    316             qset = [UserController(u, request_dict.user) for u in qset] 
     323        request_dict.session["search_query"] = search_query 
    317324        request_dict.session["results"] = qset 
    318325    else: 
    319326        qset = request_dict.session.get("results", []) 
     327        search_query = request_dict.session.get("search_query", "") 
    320328    ctx.update({'results' : qset,  
     329                'search_query' : search_query, 
    321330                'type_form' : type_form, 
    322331                'type_form4creation' : type_form4creation, 
     
    335344    else: 
    336345        ctx["is_readable"] = True 
     346 
     347    # little hack to avoid a KeyError 
     348    # see https://github.com/toastdriven/django-haystack/issues/404 
     349    from haystack import site 
     350    for r in request_dict.session.get("results", []): 
     351        r.searchsite = site 
     352 
    337353    return selected_object, ctx 
    338354 
     
    342358    obj, ctx = get_generic_data(request, obj_type, obj_ref, obj_revi) 
    343359    FilterForm = get_navigate_form(obj) 
     360 
    344361    has_session = any(field in request.session for field in FilterForm.base_fields) 
    345362    if request.method == 'POST' and request.POST: 
     
    356373    if not form.is_valid(): 
    357374        raise ValueError("Invalid form") 
    358     graph = NavigationGraph(obj, ctx["results"]) 
     375    graph = NavigationGraph(obj, [r.object for r in ctx.get("results", [])]) 
    359376    options = form.cleaned_data 
    360377    if options["update"]: 
  • trunk/openPLM/plmapp/forms.py

    r406 r410  
    4040from openPLM.plmapp.widgets import JQueryAutoComplete 
    4141from openPLM.plmapp.encoding import ENCODINGS 
    42 import openPLM.plmapp.csvimport as csvimport 
    4342 
    4443class PLMObjectForm(forms.Form): 
     
    218217        return Form(empty_permitted=True) 
    219218get_search_form.cache = {}     
    220        
     219 
     220import xapian 
     221from haystack.query import SearchQuerySet, EmptySearchQuerySet 
     222from haystack import backend as search_backend 
     223from haystack.constants import DJANGO_CT 
     224 
     225class SimpleSearchForm(forms.Form): 
     226    q = forms.CharField(label=_("Query"), required=False) 
     227 
     228    def search(self, *models): 
     229        if self.is_valid(): 
     230            query = self.cleaned_data["q"].strip() 
     231            sqs = SearchQuerySet().highlight().models(*models) 
     232            if not query or query == "*": 
     233                return sqs 
     234            if models: 
     235                models = sorted(['%s:%s.%s' % (DJANGO_CT, model._meta.app_label, model._meta.module_name) for model in models]) 
     236                models_clause = ' OR '.join(models) 
     237                final_query = '(%s) AND (%s)' % (query, models_clause) 
     238            else: 
     239                final_query = query 
     240 
     241            try: 
     242                sb = search_backend.SearchBackend() 
     243                xqr = sb.parse_query(final_query) 
     244                results = sqs.raw_search(xqr) 
     245            except xapian.QueryParserError: 
     246                results = sqs.auto_query(query) 
     247            return results 
     248        else: 
     249            return EmptySearchQuerySet() 
     250         
     251 
    221252class AddChildForm(PLMObjectForm): 
    222253    quantity = forms.FloatField() 
  • trunk/openPLM/plmapp/models.py

    r400 r410  
    234234        return True 
    235235 
     236    def get_attributes_and_values(self): 
     237        return [(attr, getattr(self, attr)) for attr in self.attributes] 
     238 
     239 
    236240# lifecycle stuff 
    237241 
     
    545549                fields.append(field) 
    546550        return fields 
     551 
     552    def get_attributes_and_values(self): 
     553        return [(attr, getattr(self, attr)) for attr in self.attributes] 
    547554 
    548555# parts stuff 
  • trunk/openPLM/plmapp/tests/views.py

    r392 r410  
    300300 
    301301 
     302from django.core.management import call_command 
    302303class SearchViewTest(CommonViewTest): 
    303304 
    304305    def search(self, request): 
    305         response = self.client.get("/user/user/attributes/", request)  
     306        # rebuild the index 
     307        call_command("rebuild_index", interactive=False) 
     308 
     309        query = u" ".join(u"%s:%s" % q for q in request.iteritems() if q[0] != "type") 
     310        query = query or "*" 
     311        response = self.client.get("/user/user/attributes/", {"type" : request["type"], 
     312                "q" : query})  
    306313        self.assertEqual(response.status_code, 200) 
    307314        results = list(response.context["results"]) 
    308         results.sort(key=lambda r:r.pk) 
    309         return results 
     315        results.sort(key=lambda r:r.object.pk) 
     316        return [r.object for r in results] 
    310317 
    311318    def test_forms(self): 
  • trunk/openPLM/settings.py

    r401 r410  
    9393    'django.contrib.comments', 
    9494    'django.contrib.humanize', 
     95    'djcelery', 
     96    'haystack', 
    9597    'south', 
    9698    'openPLM.plmapp', 
     
    138140MAX_FILE_SIZE = -1 
    139141 
    140 #: True if emails must have a domain of the list of sites 
    141 RESTRICT_EMAIL_TO_DOMAINS = False 
     142# search stuff 
     143HAYSTACK_SITECONF = 'openPLM.plmapp.search_sites' 
     144HAYSTACK_SEARCH_ENGINE = 'xapian' 
     145HAYSTACK_XAPIAN_PATH = "/var/openPLM/xapian_index/" 
     146HAYSTACK_INCLUDE_SPELLING = True 
     147EXTRACTOR = os.path.abspath(os.path.join(os.path.dirname(__file__), "bin", "extractor.sh")) 
     148 
     149# celery stuff 
     150import djcelery 
     151djcelery.setup_loader() 
     152 
     153BROKER_HOST = "localhost" 
     154BROKER_PORT = 5672 
     155BROKER_USER = "openplm" 
     156BROKER_PASSWORD = "secret" 
     157BROKER_VHOST = "openplm" 
    142158 
    143159COMPANY = "company" 
  • trunk/openPLM/settings_tests.py

    r391 r410  
    9494    'django.contrib.comments', 
    9595    'django.contrib.humanize', 
     96    'djcelery', 
     97    'haystack', 
    9698    'openPLM.plmapp', 
    9799    # you can add your application after this line 
     
    135137MAX_FILE_SIZE = -1 
    136138 
     139# search stuff 
     140HAYSTACK_SITECONF = 'openPLM.plmapp.search_sites' 
     141HAYSTACK_SEARCH_ENGINE = 'xapian' 
     142HAYSTACK_XAPIAN_PATH = "/tmp/xapian_index/" 
     143HAYSTACK_INCLUDE_SPELLING = True 
     144EXTRACTOR = os.path.abspath("bin/extractor.sh") 
     145 
     146# celery stuff 
     147import djcelery 
     148djcelery.setup_loader() 
     149 
     150BROKER_BACKEND = "memory" 
     151 
    137152COMPANY = "company" 
    138153 
  • trunk/openPLM/templates/BaseDisplayHomePage.htm

    r387 r410  
    1919    <script type="text/javascript" src="/media/js/jquery-ui-1.8.4.custom.min.js"></script> 
    2020    <script type="text/javascript" src="/media/js/jquery.cookie.js"></script> 
    21     <script type="text/javascript" src="/media/js/search_form.js"></script> 
    2221    <script type="text/javascript" src="/media/js/panels.js"></script> 
    2322    
  • trunk/openPLM/templates/DisplayHomePage.htm

    r405 r410  
    22{% load i18n %} 
    33{% load plmapp_tags %} 
     4{% load highlight %} 
    45 
    56<!-- Manage html display of the home page --> 
     
    3334{% block ResultBlock %} 
    3435    {% if link_creation %} 
    35         Results for link creation : 
     36        {% trans "Results for link creation:" %} 
    3637    {% endif %} 
    37     <table class="Result"> 
    38         {% for object in results %} 
    39             {% if forloop.first %} 
    40                 {% if not object.username %}                 
    41                     <col width="30%"/> 
    42                     <col width="30%"/> 
    43                     <col width="20%"/> 
    44                     <col width="20%"/> 
    45                 {% endif %} 
    46                 <tr class="Content"> 
    47                     {% if object.username %} 
    48                         <th class="Content">{% trans "Username" %}</th> 
    49                         <th class="Content">{% trans "Last name" %}</th> 
    50                     {% else %} 
    51                         <th class="Content">{% trans "Type" %}</th> 
    52                         <th class="Content">{% trans "Reference" %}</th> 
    53                         <th class="Content">{% trans "Rev." %}</th> 
    54                     {% endif %} 
    55                     {% if link_creation %} 
    56                         <th>{% trans "ADD" %}</th> 
    57                     {% else %} 
    58                         <th class="Content">{% trans "Name" %}</th> 
    59                     {% endif %} 
    60                 </tr> 
    61             {% endif %} 
    62  
    63             <tr class="{% cycle 'Content' 'Content2' as rowcolors %}"> 
    64                 {% if object.username %} 
    65                     <td class="Content"> 
    66                         <a href="{{object.plmobject_url}}"> 
    67                             {{object.username|escape}} 
    68                         </a> 
    69                     </td> 
    70                     <td class="Content">{{object.last_name|escape}}</td> 
    71                 {% else %} 
    72                     <td class="Content">{{object.type|escape}}</td> 
    73                     <td class="Content reference"> 
    74                         <a href="{{object.plmobject_url}}"> 
    75                             {{object.reference|escape}} 
    76                         </a> 
    77                     </td> 
    78                     <td class="Content">{{object.revision|escape}}</td> 
    79                     {% if not link_creation %} 
    80                         <td class="Content"> 
    81                             <span class="info"> 
    82                                 <span class="small">{{object.name|trunc:10}}</span> 
    83                                 <span class="long">{{object.name}}</span> 
    84                             </span> 
    85                         </td> 
    86                     {% endif %} 
    87                 {% endif %} 
    88  
     38    <ul class="Result"> 
     39        {% for result in results %} 
     40            <li class="Result"> 
     41            <div class="reference"> 
     42                {{result.rendered|safe}} 
     43            </div> 
     44            <div class="summary"> 
     45                {{ result.highlighted.text|safe }} 
     46            </div> 
     47            {% if link_creation or navigate_bool %} 
    8948                <form method="post" action="." enctype="multipart/form-data"> 
    90                     {% if object.username %} 
    91                         <input type="hidden" name="type" value="User"/> 
    92                         <input type="hidden" name="username" value="{{object.username|escape}}"/> 
    93  
    94                     {% else %} 
    95                         <input type="hidden" name="type" value="{{object.type|escape}}"/> 
    96                         <input type="hidden" name="reference" value="{{object.reference|escape}}"/> 
    97                         <input type="hidden" name="revision" value="{{object.revision|escape}}"/> 
    98  
    99                     {% endif %} 
    100                     {% if link_creation %} 
    101                         {% if object|can_add:attach %} 
    102                             <td class="Content"> 
    103                                 <input type="submit" value="{% trans "ADD" %}"/> 
    104                             </td> 
    105                         {% endif %} 
     49                    {{result.rendered_add|safe}} 
     50                    {% if result.object|can_add:attach %} 
     51                        <input type="submit" value="{% trans "ADD" %}"/> 
    10652                    {% endif %} 
    10753                </form> 
    108             </tr> 
     54 
     55            {% endif %} 
     56            </li> 
     57 
    10958        {% empty %} 
    110             <tr>{% trans "No result matching given query" %}</tr> 
     59            <li>{% trans "No result matching given query" %}</li> 
    11160        {% endfor %} 
    112     </table> 
     61    </ul> 
    11362{% endblock %} 
    11463 
     
    12170            </li> 
    12271            <li class="ui-button ui-button-text  ui-button-text-only ui-widget ui-state-default ui-corner-all"  id="ImportButton"> 
    123                 <a href="/import/csv/"> 
    124                     {% trans "Import" %} 
    125                 </a> 
    126              </li> 
     72            <a href="/import/csv/"> 
     73                {% trans "Import" %} 
     74            </a> 
     75            </li> 
    12776            <li class="ui-button ui-button-text  ui-button-text-only ui-widget ui-state-default ui-corner-all"  id="ImportBOMButton"> 
    128                 <a href="/import/bom/"> 
    129                     {% trans "Import a BOM" %} 
    130                 </a> 
    131              </li> 
     77            <a href="/import/bom/"> 
     78                {% trans "Import a BOM" %} 
     79            </a> 
     80            </li> 
    13281        </ul> 
    13382    </form> 
  • trunk/openPLM/urls.py

    r405 r410  
    197197) 
    198198 
    199  
Note: See TracChangeset for help on using the changeset viewer.