Pergunta

Eu estou tentando construir a busca de um site Django Estou construindo, e na busca Estou procurando em 3 modelos diferentes. E para chegar a paginação na lista de resultados de pesquisa que eu gostaria de usar uma exibição object_list genérico para exibir os resultados. Mas para fazer isso eu tenho que fundir 3 querysets em um.

Como posso fazer isso? Eu tentei isso:

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")

Mas isso não funcionar eu recebo um erro quando tento usar essa lista na vista genérico. A lista está faltando o atributo clone.

Alguém sabe como posso mesclar as três listas, page_list, article_list e post_list?

Foi útil?

Solução

concatenando os querysets em uma lista é a abordagem mais simples. Se o banco de dados vai ser atingido por todos querysets qualquer maneira (por exemplo, porque as necessidades de resultados a serem classificados), isso não vai acrescentar ainda mais o custo.

from itertools import chain
result_list = list(chain(page_list, article_list, post_list))

Usando itertools.chain é mais rápido que looping cada lista e acrescentando elementos, um por um, desde itertools é implementado em C. Ele também consome menos memória do que convertendo cada queryset em uma lista antes de concatenação.

Agora é possível ordenar a lista resultante, por exemplo, por data (tal como solicitado no comentário hasen de j para outra resposta). A função sorted() convenientemente aceita um gerador e retorna uma lista:

result_list = sorted(
    chain(page_list, article_list, post_list),
    key=lambda instance: instance.date_created)

Se você estiver usando Python 2.4 ou posterior, você pode usar attrgetter em vez de um lambda. Lembro de ter lido sobre ele ser mais rápido, mas eu não vi uma diferença de velocidade perceptível para uma lista milhões item.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))

Outras dicas

Tente isto:

matches = pages | articles | posts

Ele mantém todas as funções dos querysets que é bom se você quiser order_by ou similar.

Por favor nota:. Isso não funciona em querysets de dois modelos diferentes

relacionada, para misturar querysets do mesmo modelo, ou para campos similares de alguns modelos, começando com Django 1.11 a método qs.union() também está disponível:

union()

union(*other_qs, all=False)

Novo no Django 1.11 . Utiliza operador UNION do SQL para combinar os resultados de duas ou mais QuerySets. Por exemplo:

>>> qs1.union(qs2, qs3)

Os seleciona operador de união apenas valores distintos por padrão. Para permitir valores duplicados, use a todos = true argumento.

sindicais (), interseção (), e diferença () instâncias do modelo de retorno de o tipo do primeiro QuerySet mesmo se os argumentos são QuerySets de outros modelos. Passando modelos diferentes funcionam desde que o SELECT lista é a mesma em todos os QuerySets (pelo menos os tipos, os nomes não fazer importa, desde que os tipos na mesma ordem).

Além disso, apenas para o limite, OFFSET, e ORDER BY (isto é, o corte ea order_by ()) são permitidos na QuerySet resultante. Além disso, bancos de dados colocam restrições sobre quais operações são permitidas no combinado consultas. Por exemplo, a maioria dos bancos de dados não permitem limitar ou compensados ??em as consultas combinadas.

https: / /docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union

Você pode usar a classe QuerySetChain abaixo. Quando usá-lo com paginator do Django, ele só deve bater o banco de dados com consultas COUNT(*) para todos os querysets e consultas SELECT() apenas para os querysets cujos registros são exibidos na página atual.

Note que você precisa especificar template_name= se estiver usando um QuerySetChain com vistas genéricas, mesmo que os querysets acorrentados todos usam o mesmo modelo.

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()

No seu exemplo, o uso seria:

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)

Em seguida, use matches com o paginador, como costumava result_list no seu exemplo.

O módulo itertools foi introduzido em Python 2.3, por isso deve estar disponível em todas as versões do Python Django é executado.

A grande desvantagem de sua abordagem atual é sua ineficiência com grandes conjuntos de resultados de pesquisa, como você tem que puxar para baixo todo o conjunto de resultados do banco de dados de cada vez, mesmo que você só pretende apresentar uma página de resultados.

A fim de só puxar para baixo os objetos que você realmente precisa do banco de dados, você tem que usar a paginação em um QuerySet, não uma lista. Se você fizer isso, Django realmente corta o QuerySet antes da consulta é executada, então a consulta SQL usará DESVIO e LIMIT para só obter os registros que você vai realmente mostrar. Mas você não pode fazer isso a menos que você pode empinar a sua pesquisa em uma única consulta de alguma forma.

Uma vez que todos os três de seus modelos têm campos de título e corpo, por que não usar modelo de herança ? Basta ter todos os três modelos herdar de um ancestral comum que tem título e no corpo, e realizar a pesquisa como uma única consulta no modelo ancestral.

No caso de você querer cadeia um monte de querysets, tente o seguinte:

from itertools import chain
result = list(chain(*docs))

em que: docs é uma lista de querysets

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 -Usuários / 6wUNuJa4jVw . Veja Alex Gaynor

aqui está uma ideia ... basta puxar para baixo uma página completa de resultados de cada um dos três e, em seguida, jogar fora os 20 os menos úteis ... Isso elimina a grandes querysets e dessa forma você só sacrificar um pouco de desempenho em vez de um monte

Requisitos: Django==2.0.2, django-querysetsequence==0.8

No caso de você querer combinar querysets e ainda sair com um QuerySet, você pode querer verificar para fora -sequência django-queryset .

Mas uma nota sobre o assunto. Leva apenas dois querysets como argumento. Mas com reduce python você sempre pode aplicá-la a vários querysets.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

E é isso. Abaixo está uma Corri situação em e como eu list comprehension empregada, reduce e 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})

Isto pode ser alcançado de duas maneiras também.

1 maneira de fazer isso

Use operador de união para | queryset tomar união de dois queryset. Se ambos queryset pertence mesmo modelo modelo / única do que é possível combinar querysets usando operador de união.

Para uma instância

pagelist1 = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
pagelist2 = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
combined_list = pagelist1 | pagelist2 # this would take union of two querysets

segunda maneira de fazer isso

Uma outra forma de conseguir combinar operação entre duas queryset é usar itertools função da cadeia.

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))

Esta função recursiva concatena variedade de querysets em um queryset.

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
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top