Я пытаюсь построить поиск для сайта Django, который я строю, и в поиске я ищу в 3 разных моделях. И чтобы получить нумерацию страниц в списке результатов поиска, я хотел бы использовать общее представление object_list для отображения результатов. Но для этого мне нужно объединить 3 набора запросов в один.
Как я могу это сделать? Я пробовал это:
result_list = []
page_list = Page.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
article_list = Article.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
post_list = Post.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
for x in page_list:
result_list.append(x)
for x in article_list:
result_list.append(x)
for x in post_list:
result_list.append(x)
return object_list(
request,
queryset=result_list,
template_object_name='result',
paginate_by=10,
extra_context={
'search_term': search_term},
template_name="search/result_list.html")
Но это не работает, я получаю сообщение об ошибке, когда пытаюсь использовать этот список в общем виде. В списке отсутствует атрибут clone.
Кто-нибудь знает, как я могу объединить три списка, page_list
, article_list
и post_list
?
Объединение запросов в список является самым простым подходом. Если база данных будет удалена для всех запросов в любом случае (например, потому что результат нужно сортировать), это не добавит дополнительных затрат.
from itertools import chain
result_list = list(chain(page_list, article_list, post_list))
Использование itertools.chain
выполняется быстрее, чем цикл каждого списка и добавление элементов один за другим, поскольку itertools
реализован в C. Он также потребляет меньше памяти, чем преобразование каждого набора запросов в список перед конкатенацией.
Теперь можно отсортировать полученный список, например. по дате (по запросу в jen j комментировать другой ответ). Функция sorted()
удобно принимает генератор и возвращает список:
result_list = sorted(
chain(page_list, article_list, post_list),
key=lambda instance: instance.date_created)
Если вы используете Python 2.4 или новее, вы можете использовать attrgetter
вместо лямбда. Я помню, как читал об этом быстрее, но я не видел заметной разницы в скорости для миллиона списков элементов.
from operator import attrgetter
result_list = sorted(
chain(page_list, article_list, post_list),
key=attrgetter('date_created'))
from itertools import groupby
unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Попробуйте следующее:
matches = pages | articles | posts
Сохраняет все функции запросов, которые хороши, если вы хотите order_by или подобное.
К сожалению, это не работает с запросами из двух разных моделей...
Связанный, для смешивания запросов с той же модели или для похожих полей из нескольких моделей, Начиная с Django 1.11 a qs.union()
:
union()
union(*other_qs, all=False)
Новое в Django 1.11. Использует оператор UNION SQLs для объединения результатов двух или более запросов QuerySets. Например:
>>> qs1.union(qs2, qs3)
Оператор UNION по умолчанию выбирает только разные значения. Чтобы разрешить повторяющиеся значения, используйте значение all = True аргумент.
union(), пересечение() и разность() возвращают экземпляры модели тип первого QuerySet, даже если аргументы QuerySets другие модели. Передача разных моделей работает до тех пор, пока SELECT список одинаковый во всех QuerySets (по крайней мере, типы, имена dont до тех пор, пока типы в том же порядке).
Кроме того, только LIMIT, OFFSET и ORDER BY (то есть нарезка и order_by()) разрешены на полученном QuerySet. Кроме того, базы данных устанавливать ограничения на то, какие операции разрешены в комбинированных запросов. Например, большинство баз данных не разрешают LIMIT или OFFSET в объединенные запросы.
https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union
Вы можете использовать класс QuerySetChain
ниже. При использовании его с paginator Django он должен только попадать в базу данных с запросами COUNT(*)
для всех запросов и запросов SELECT()
только для тех запросов, записи которых отображаются на текущей странице.
Обратите внимание, что вам нужно указать template_name=
, если использовать QuerySetChain
с общими представлениями, даже если цепочки запросов все используют одну и ту же модель.
from itertools import islice, chain
class QuerySetChain(object):
"""
Chains multiple subquerysets (possibly of different models) and behaves as
one queryset. Supports minimal methods needed for use with
django.core.paginator.
"""
def __init__(self, *subquerysets):
self.querysets = subquerysets
def count(self):
"""
Performs a .count() for all subquerysets and returns the number of
records as an integer.
"""
return sum(qs.count() for qs in self.querysets)
def _clone(self):
"Returns a clone of this queryset chain"
return self.__class__(*self.querysets)
def _all(self):
"Iterates records in all subquerysets"
return chain(*self.querysets)
def __getitem__(self, ndx):
"""
Retrieves an item or slice from the chained set of results from all
subquerysets.
"""
if type(ndx) is slice:
return list(islice(self._all(), ndx.start, ndx.stop, ndx.step or 1))
else:
return islice(self._all(), ndx, ndx+1).next()
В вашем примере использование будет:
pages = Page.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
articles = Article.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
posts = Post.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
matches = QuerySetChain(pages, articles, posts)
Затем используйте matches
с помощью paginator, как вы использовали result_list
в вашем примере.
Модуль itertools
был введен в Python 2.3, поэтому он должен быть доступен во всех версиях Django на Python.
Большой недостаток вашего нынешнего подхода - его неэффективность с большими наборами результатов поиска, так как каждый раз приходится вытаскивать весь результирующий набор из базы данных, хотя вы только собираетесь отображать одну страницу результатов.
Чтобы только вытащить нужные вам объекты из базы данных, вам нужно использовать разбиение на страницы в QuerySet, а не на список. Если вы это сделаете, Django на самом деле разрезает QuerySet до того, как запрос будет выполнен, поэтому SQL-запрос будет использовать OFFSET и LIMIT, чтобы получить только записи, которые вы фактически увидите. Но вы не можете этого сделать, если вы не сможете вкратце поиска в один запрос каким-то образом.
Учитывая, что у всех трех моделей есть поля заголовка и тела, почему бы не использовать наследование модели? Просто у всех трех моделей наследуется от общего предка, который имеет название и тело, и выполняет поиск как один запрос на модели предка.
Если вы хотите связать множество запросов, попробуйте следующее:
from itertools import chain
result = list(chain(*docs))
где: docs - это список запросов
DATE_FIELD_MAPPING = {
Model1: 'date',
Model2: 'pubdate',
}
def my_key_func(obj):
return getattr(obj, DATE_FIELD_MAPPING[type(obj)])
And then sorted(chain(Model1.objects.all(), Model2.objects.all()), key=my_key_func)
Цитата из https://groups.google.com/forum/#!topic/django-users/6wUNuJa4jVw. См. Алекс Гейнор
Для поиска лучше использовать специальные решения, такие как Haystack - он очень гибкий.
Похоже, t_rybik создал комплексное решение на http://www.djangosnippets.org/snippets/1933/
Требования: Django==2.0.2
, django-querysetsequence==0.8
Если вы хотите объединить querysets
и по-прежнему использовать QuerySet
, вы можете проверить django-queryset-sequence.
Но одна заметка об этом. Он принимает только два querysets
качестве аргумента. Но с помощью Python reduce
вы всегда можете применить его к нескольким queryset
.
from functools import reduce
from queryset_sequence import QuerySetSequence
combined_queryset = reduce(QuerySetSequence, list_of_queryset)
И это оно. Ниже приведена ситуация, с которой я столкнулся, и то, как я использовал list comprehension
, reduce
и django-queryset-sequence
from functools import reduce
from django.shortcuts import render
from queryset_sequence import QuerySetSequence
class People(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
mentor = models.ForeignKey('self', null=True, on_delete=models.SET_NULL, related_name='my_mentees')
class Book(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Student, on_delete=models.CASCADE)
# as a mentor, I want to see all the books owned by all my mentees in one view.
def mentee_books(request):
template = "my_mentee_books.html"
mentor = People.objects.get(user=request.user)
my_mentees = mentor.my_mentees.all() # returns QuerySet of all my mentees
mentee_books = reduce(QuerySetSequence, [each.book_set.all() for each in my_mentees])
return render(request, template, {'mentee_books' : mentee_books})
Book.objects.filter(owner__mentor=mentor)
не делает то же самое? Я не уверен, что это правильный вариант использования. Я думаю, что для Book
может потребоваться несколько owner
прежде чем вы начнете делать что-то подобное.
вот идея... просто вытащить одну полную страницу результатов из каждого из трех, а затем выбросить 20 наименее полезных... это устраняет большие запросы и таким образом вы жертвуете только небольшим исполнением, а не много
Эта рекурсивная функция объединяет массив наборов запросов в один набор запросов.
def merge_query(ar):
if len(ar) ==0:
return [ar]
while len(ar)>1:
tmp=ar[0] | ar[1]
ar[0]=tmp
ar.pop(1)
return ar