Search multiple models (Elasticsearch) - python

i added a second model to my app and now i also want to search trough the fields for that model and return it in the same view as my first model.
views.py
#Elasticsearch
def globalsearch_elastic(request):
qs = request.GET.get('qs')
page = request.GET.get('page')
if qs:
qs = PostDocument.search().query("multi_match", query=qs, fields=["title", "content", "tag"])
qs = qs.to_queryset()
else:
qs = ''
paginator = Paginator(qs, 10) # Show 10 results per page
try:
qs = paginator.page(page)
except PageNotAnInteger:
qs = paginator.page(1)
except EmptyPage:
qs = paginator.page(paginator.num_pages)
return render(request, 'MyProject/search/search_results_elastic.html', {'object_list':qs})
currently only PostDocument get searched for a match, how do i add a second one here from documents.py in this function?
or to be more specific, what do i have to change in this line to Search multiple documents?
qs = PostDocument.search().query("multi_match", query=qs, fields=["title", "content", "tag"])
this is where "qs" comes from (base.html):
<div class="globalsearch">
<form id="searchform" action="{% url 'search' %}" method="get" accept-charset="utf-8">
<div class="form-row align-items-center">
<input class="class-search-input-fields" id="searchbox" name="qs" required="required" type="text" placeholder="Search ..."><a>in</a>
<div class="custom-dropdown">
<a>{{ categorysearch_form.category }}</a>
</div>
<button class="btn btn-dark" type="submit">
<i class="fa fa-search"></i>
</button>
</div>
</form>
</div>
documents.py:
Thanks in advance

Got it working like this:
def globalsearch_elastic(request):
qs = request.GET.get('qs')
page = request.GET.get('page')
if qs:
post = PostDocument.search().query("multi_match", query=qs, fields=["title", "content", "tag"]).to_queryset()
post_extra = PostExtraDocument.search().query("multi_match", query=qs, fields=["title", "content_preview", "tag"]).to_queryset()
qs = list(
sorted(
chain(post, post_extra),
key=lambda objects: objects.pk
))
else:
qs = ''
paginator = Paginator(qs, 10) # Show 10 results per page
try:
qs = paginator.page(page)
except PageNotAnInteger:
qs = paginator.page(1)
except EmptyPage:
qs = paginator.page(paginator.num_pages)
return render(request, 'MyProject/search/search_results_elastic.html', {'object_list': qs})
i know that this is maybe not "best-practice" because i always have to load all posts, sort them afterwards and display them by creation-date (pk).
Anyways, from my point of view it's fine if you don't have to search millions f posts ... Beside that you still have the power of elastic even if that does not sort my qs list of posts

The accepted answer is correct but not efficient because it fetches rows from the database to convert the results to a querysets and finally chain and sort...
I hope my answer will help others. I'm using django-oscar and dajngo-elasticsearch-dsl and I assume that you're at least using elasticsearch-dsl. So you can simply do
# *********************************************** documents.py in user module
from django_elasticsearch_dsl import Document
from django_elasticsearch_dsl.registries import registry
from oscar.core.compat import get_user_model
User = get_user_model()
#registry.register_document
class UserDocument(Document):
class Index:
name = 'users'
settings = {
'number_of_shards': 1,
'number_of_replicas': 0
}
class Django:
model = User # <-- User model class
fields = [
'first_name',
'last_name',
'username'
]
# *********************************************** documents.py in product module
from django_elasticsearch_dsl import Document, TextField
from django_elasticsearch_dsl.registries import registry
from oscar.core.loading import get_model
Product = get_model('catalogue', 'product')
#registry.register_document
class ProductDocument(Document):
upc = TextField(attr='upc', required=False)
class Index:
name = 'products'
settings = {
'number_of_shards': 1,
'number_of_replicas': 0
}
class Django:
model = Product # <-- Product model class
fields = [
'title',
'description'
]
# *********************************************** search method
from django.shortcuts import render
from elasticsearch_dsl import Search
def global_search(request):
q = request.GET.get('q')
objects = ''
if q:
search = Search(index=['users', 'products'])
objects = search.query("multi_match", query=q, fields=['first_name', 'last_name', 'username', 'title', 'description', 'upc'])
return render(request, 'oscar/search/search.html', {'objects': objects})

In a match query you already search all the tokens in the query as an OR query in SQL . In your case, if you have a multimatch query against 3 field means that you are searching for a match of any of your token in your query against any of that 3 fields - if you haven't assigned any specific mapping to ES, it means that your text fields have processed with a standard analyzer please read here that breaks your string in tokens on whitespace. So to add a new key to be queryied, just concatenate a new value to the string qs:
from document import DocClass
def globalsearch_elastic(request):
qs = request.GET.get('qs')
document = DocClass()
document_value = document.my_function() # you invoke a function in a class inside document.py that returns to you a string
page = request.GET.get('page')
if any([qs, document_value]):
queryable = " ".join([qs, document_value])
qs = PostDocument.search().query("multi_match", query=queryable, fields=["title", "content", "tag"])
qs = qs.to_queryset()
else:
return "No values to query"

Related

Is there an effcient way to perform search query in django?

I'm creating a blog in which i want to perform a search query based on ones rating (1-5). Here my search would be like query:"smart phone tech updates", rating:"3". Result should be list of post that contains query word(at least one word) which has rating 3, in a sorted way on val(for each query word, if found in title val+=1 if found in content val+=0.4).
My models.py file has the following :
class Post(models.Model):
title = models.CharField(max_length=50)
content = models.CharField(max_length=500)
rating = models.IntegerField(default=1)
enter code here
date = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
My view.py file has the following:
def search(request):
contents = Post.objects.all()
if request.method == 'GET':
query = request.GET['query']
rating = request.GET['rating']
# search function
# contents = InSearch.search_fun(contents,query,rating)
vector = SearchVector('title', weight='A') + SearchVector('content', weight='B')
qry = SearchQuery(str(query))
contents = Post.objects.annotate(rank=SearchRank(vector, qry)).order_by('-rank')
#print("---->\n\t"+query+ str(contents))
context = {
'contents': contents
}
else:
context = {
'contents': Post.objects.all()
}
return render(request, 'feed/home.html', context)
My urls.py is:
urlpatterns = [
#...
path('feed/search-result/', views.search, name='feed_search'),
]
I'm getting this error
django.db.utils.OperationalError: no such function: plainto_tsquery
You can try like this in your views for searching.
from django.db.models import Q
def search(request):
q = request.GET.get('q')
if q:
search_results = Post.objects.filter(Q(title__icontains=q)|Q(rating=q))
# if you want the exact then do Post.objects.filter(Q(title__iexact=q) & Q(rating=q))
return render(request, 'feed/home.html','search_results':search_results)
else:
messages.info(request,'no results found for {}',format(q))
If you want to sort search query result by number of matches then you can try like this:
search_results = Post.objects.filter(Q(title__icontains=q)|Q(rating=q)).annotate(title_counts=Count('title')).order_by('-title_counts')
And in your template give name='q' in the search form.
<form action="{% url 'your search action' %}">
<input type="text" name="q">
<input type='submit' value='Search'>
</form>

How to implement pagination in Search results in django?

I found a resource where it has code like this:
from django.contrib.auth.models import User
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
def index(request):
user_list = User.objects.all()
page = request.GET.get('page', 1)
paginator = Paginator(user_list, 10)
try:
users = paginator.page(page)
except PageNotAnInteger:
users = paginator.page(1)
except EmptyPage:
users = paginator.page(paginator.num_pages)
return render(request, 'core/user_list.html', { 'users': users })
I have a code like this:
def product_list(request, category_slug=None):
category = None
categories = Category.objects.all().order_by("-rating")
#paginator = Paginator(categories, 10)
products = Product.objects.all().order_by("-number")
users = User.objects.exclude(id=request.user.id)
query = request.GET.get('q')
if query=='':
return HttpResponseRedirect('/')
if query:
categories = Category.objects.filter(Q(slug__icontains=query)| Q(url__icontains=query)).order_by("-rating")
products = Product.objects.filter(Q(slug__icontains=query) | Q(name__icontains=query) | Q(description__icontains=query)).order_by("number")
if category_slug:
category = get_object_or_404(Category, slug=category_slug)
products = Product.objects.filter(category=category)
categories_counter = products.annotate(Count('id'))
categories_count = len(categories_counter)
#contacts = paginator.get_page(query)
context = {
'category': category,
'categories': categories,
'products': products,
'categories_count':categories_count,
'query':query,
'users':users,
#'contacts':contacts,
}
return render(request, 'shop/product/list.html', context)
I have retrieved objects from two models, Category, and Product. How do I implement the pagination code in this view? It's not a normal pagination but pagination in search results.
user_list = User.objects.all() # this is the full queryset, contains all objects
page = request.GET.get('page', 1) # this is the page number of page whose data you want to retrieve, you need to pass page value as query params
paginator = Paginator(user_list, 10) # this will paginate the full queryset in pages of 10 objects
try:
users = paginator.page(page) # this will return data of that particular page
except PageNotAnInteger:
users = paginator.page(1) # if the passed page is not Integer then first page is returned, you can customize this
except EmptyPage:
users = paginator.page(paginator.num_pages) # if that page contains no element, then last page is returned, you can customize this also
You can apply same logic to retrieve categories in category_page and other data.

Django filters not applying when going to next page

I am using Django filters and when I apply the filters they work but when I go to the next page of results they reset. I have looked at this post django-filter use paginations but they seem to be doing the same things that I am. What am I doing wrong?
Url
url(r'^relations/$', views.annotation_views.relations, name="relations")
returns a url like this when the filters are applied:
/relations/?createdBy=&occursIn=&createdAfter=&createdBefore=&terminal_nodes=&project=
Filters.py
class RelationSetFilter(django_filters.FilterSet):
occursIn = django_filters.CharFilter('occursIn__uri', method='filter_occursIn')
createdBefore = django_filters.DateTimeFilter('created', lookup_expr='lt')
createdAfter = django_filters.DateTimeFilter('created', lookup_expr='gt')
terminal_nodes = django_filters.CharFilter('terminal_nodes__uri')
def filter_occursIn(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(Q(occursIn__uri=value) | Q(occursIn__part_of__uri=value) | Q(occursIn__part_of__part_of__uri=value))
class Meta:
model = RelationSet
fields = ['createdBy', 'project', 'occursIn', 'terminal_nodes']
View
def relations(request):
from annotations.filters import RelationSetFilter
qs = RelationSet.objects.all()
filtered = RelationSetFilter(request.GET, queryset=qs)
qs = filtered.qs
for r in qs:
print r.__dict__
paginator = Paginator(qs, 40)
page = request.GET.get('page')
try:
relations = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
relations = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
relations = paginator.page(paginator.num_pages)
context = {
'paginator': paginator,
'relations': relations,
'params': request.GET.urlencode(),
'filter': filtered,
}
return render(request, 'annotations/relations.html', context)
I was able to solve this by copying the url and passing the params to the template. I had to delete the page param due to it being duplicated. In order to do this I added the following:
View
def relations(request):
from annotations.filters import RelationSetFilter
qs = RelationSet.objects.all()
filtered = RelationSetFilter(request.GET, queryset=qs)
qs = filtered.qs
for r in qs:
print r.__dict__
paginator = Paginator(qs, 40)
page = request.GET.get('page')
gt = request.GET.copy()
if 'page' in gt:
del gt['page']
try:
relations = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
relations = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
relations = paginator.page(paginator.num_pages)
context = {
'paginator': paginator,
'relations': relations,
'params': urlencode(gt),
'filter': filtered,
}
return render(request, 'annotations/relations.html', context)
Template
<div class="col-xs-4 clearfix text-center">
{% if relations.has_next %}
<div class="pull-right">
Next ยป
</div>
{% endif %}
</div>
Update
I found a more secure fix for this by getting the data directly for the filter object
project = filtered.data.get('project')
then adding this to the view
&project={{ project }}

How can I keep GET instance on template form after submission?

I have a simple search bar, I would like to keep the data the user submited and show it on the search bar after the form submission. How can I do that ?
I'm using GET for the search method, but I do not save any searched items on any model and I prefer not to, I was wondering if there was another way to show it without using the database storage.
Here is what my code looks like :
views.py
def index(request):
allGigs = Gig.objects.filter(status=True)
context = {'gigs': allGigs}
return render(request, 'index.html', context)
def search_gigs(request):
title = request.GET['title']
request.session['title'] = title #a try with session, but the data is kept once the user returns to front page...
gigs = Gig.objects.filter(title__contains=title)
return render(request, 'index.html', {"gigs": gigs})
models.py Gig Model has title CharField.
index.html
<form role="search" method="GET" action="{% url 'search' %}">
<input type="text" name="title">
<input type="submit" value="submit">
</form>
urls.py
url(r'^search/$', views.search_gigs, name='search'), #example : /search/?title=my_search_word
url(r'^$', views.index, name='index'),
I thought about using Django Sessions but the problem is that the user can only see what he searched after returning to the index page, any suggestion ?
You can use this sticky query method decorator on your view.
from urllib.parse import urlencode
try:
import urlparse
except ImportError:
from urllib import parse as urlparse
import wrapt
from django.http import HttpResponseRedirect
'''
Originally From:
https://www.snip2code.com/Snippet/430476/-refactor--Django--sticky-URL-query-para
'''
"""
File: decorators.py
Author: timfeirg
Email: kkcocogogo#gmail.com
Github: https://github.com/timfeirg/
Description: remember_last_query_params is from
http://chase-seibert.github.io/blog/2011/09/02/django-sticky-url-query-parameters-per-view.html
"""
class sticky_query(object):
"""Stores the specified list of query params from the last time this user
looked at this URL (by url_name). Stores the last values in the session.
If the view is subsequently rendered w/o specifying ANY of the query
params, it will redirect to the same URL with the last query params added
to the URL.
url_name is a unique identifier key for this view or view type if you want
to group multiple views together in terms of shared history
Example:
#remember_last_query_params("jobs", ["category", "location"])
def myview(request):
pass
"""
def __init__(self, views_name, query_params):
self._cookie_prefix = views_name + '_'
self._query_params = list(set(
query_params + ['page', 'paginate_by', 'order_by_fields']))
def _get_sticky_params(self, request):
"""
Are any of the query parameters we are interested in on this request
URL?
"""
gum = []
for current_param, v in request.GET.items():
if current_param in self._query_params:
gum.append(current_param)
return gum
def _get_last_used_params(self, session):
"""
Gets a dictionary of JUST the params from the last render with values
"""
litter = {}
for k in self._query_params:
last_value = session.get(self._cookie_prefix + k, None)
if last_value:
litter[k] = last_value
return litter
def _digest(self, current_url, litter):
"""
update an existing URL with or without paramters to include new
parameters from
http://stackoverflow.com/questions/2506379/add-params-to-given-url-in-python
"""
parse_res = urlparse.urlparse(current_url)
# part 4 == params
query = dict(urlparse.parse_qsl(parse_res[4]))
query.update(litter)
query = urlencode(query)
parse_res = urlparse.ParseResult(
parse_res[0], parse_res[1], parse_res[2], parse_res[3], query,
parse_res[5])
new_url = urlparse.urlunparse(parse_res)
return new_url
#wrapt.decorator
def __call__(self, wrapped, instance, args, kwargs):
request = args[0]
session = request.session
query = request.GET
gum = self._get_sticky_params(request)
if gum:
for k in gum:
sticky_key = self._cookie_prefix + k
session[sticky_key] = query[k]
else:
meta = request.META
litter = self._get_last_used_params(session)
if litter:
current_url = '{0}?{1}'.format(
meta['PATH_INFO'], meta['QUERY_STRING'])
new_url = self._digest(current_url, litter)
return HttpResponseRedirect(new_url)
return wrapped(*args, **kwargs)
Use this decorator on your view:
from django.utils.decorators import method_decorator
#method_decorator(sticky_query("search_page", ["title"]), name='dispatch')
There is a simple way to do so :
<input type="text" name="title" value="{{ request.POST.title }}">
After the form submit it will keep the POST title field value and use it as the input value.

how to set urls in django to prevent similar job?

I practice how to use django to write a website
I finished one with model name = Traveltime
But I still have many to do (like: aaaaa,bbbbb,ccccc below)
They are do similar job ,just the model name is different
I feel it duplicate and don't know how to do.
how can I edit my urls.py?? Please help me thank you!
urls.py:
urlpatterns = patterns('',
url(r'^travel/$', views.object_list, {'model': models.Traveltime}),
url(r'^travel/result/$', views.object_result, {'model': models.Traveltime}),
url(r'^travel/update/$', views.update),
#I have many urls to set (below)
url(r'^aaaaa/$', views.object_list, {'model': models.aaaaa}),
url(r'^aaaaa/result/$', views.object_result, {'model': models.aaaaa}),
url(r'^aaaaa/update/$', views.update),
url(r'^bbbbb/$', views.object_list, {'model': models.bbbbb}),
url(r'^bbbbb/result/$', views.object_result, {'model': models.bbbbb}),
url(r'^bbbbb/update/$', views.update),
url(r'^ccccc/$', views.object_list, {'model': models.ccccc}),
url(r'^ccccc/result/$', views.object_result, {'model': models.ccccc}),
url(r'^ccccc/update/$', views.ccccc),
views.py
def object_list(request, model):
obj_list = model.objects.filter(image_elect='')
paginator = Paginator(obj_list, 10)
page = request.GET.get('page')
try:
contacts = paginator.page(page)
except PageNotAnInteger:
contacts = paginator.page(1)
except EmptyPage:
contacts = paginator.page(paginator.num_pages)
template_name = 'filterimgs/%s_list.html' % model.__name__.lower()
return render_to_response(template_name, {'object_list': obj_list,"contacts": contacts},
context_instance=RequestContext(request))
def update(request):
travel = Traveltime.objects.filter(title=request.POST['title'])
# travel.update(image_elect='asd')
return redirect(object_result)
def object_result(request, model):
obj_list = model.objects.all()
paginator = Paginator(obj_list, 10)
page = request.GET.get('page')
try:
contacts = paginator.page(page)
except PageNotAnInteger:
contacts = paginator.page(1)
except EmptyPage:
contacts = paginator.page(paginator.num_pages)
template_name = 'filterimgs/%s_result.html' % model.__name__.lower()
return render_to_response(template_name, {'object_list': obj_list,"contacts": contacts},
context_instance=RequestContext(request))
Django url patterns are regular expressions, any grouped expression will be passed into the appropriate view as an additional parameter.
urlpatterns = patterns('',
url(r'^([a-zA-Z]+)/$', views.object_list),
url(r'^([a-zA-Z]+)/result/$', views.object_result),
url(r'^([a-zA-Z]+)/update/$', views.update),
)
Then in your request you can
import importlib
def object_list(request, model_type):
# use the model_type which is passed in from
# the URL to actually grab the proper model
found = importlib.import_module('models.{0}'.format(model_type))
Python is gracious enough to allow you to import modules using a string .
Also, found is a lame name, so you should name it accordingly depending on what it's purpose is.

Categories

Resources