Como combinar dois ou mais conjuntos de consultas em uma exibição do Django?

654

Estou tentando criar a pesquisa para um site do Django que estou construindo e nessa pesquisa estou pesquisando em 3 modelos diferentes. E para obter a paginação na lista de resultados da pesquisa, eu gostaria de usar uma visualização genérica de object_list para exibir os resultados. Mas para fazer isso, eu tenho que mesclar 3 conjuntos de consultas em um.

Como eu 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 funciona. Eu recebo um erro quando tento usar essa lista na exibição genérica. A lista está ausente do atributo clone.

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

espenhogbakk
fonte
Parece que t_rybik criou uma solução abrangente em djangosnippets.org/snippets/1933
akaihola
Para pesquisar, é melhor usar soluções dedicadas como o Haystack - é muito flexível.
minder
1
Usuários Django 1.11 e ABV, consulte esta resposta - stackoverflow.com/a/42186970/6003362
Sahil Agarwal
nota : a pergunta é limitada ao caso muito raro quando, após a fusão de 3 modelos diferentes, você não precisa extrair modelos novamente na lista para distinguir dados sobre os tipos. Para a maioria dos casos - se houver distinção -, a interface será incorreta. Para os mesmos modelos: veja respostas sobre union.
Sławomir Lenart

Respostas:

1058

Concatenar os conjuntos de consultas em uma lista é a abordagem mais simples. Se o banco de dados for atingido para todos os conjuntos de consultas de qualquer maneira (por exemplo, porque o resultado precisa ser classificado), isso não aumentará o custo.

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

O uso itertools.chainé mais rápido do que repetir cada lista e acrescentar elementos um a um, uma vez que itertoolsé implementado em C. Ele também consome menos memória do que converter cada conjunto de consultas em uma lista antes de concatenar.

Agora é possível classificar a lista resultante, por exemplo, por data (conforme solicitado no comentário de hasen j para outra resposta). A sorted()função aceita convenientemente 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 o Python 2.4 ou posterior, poderá usar em attrgettervez de um lambda. Lembro-me de ler sobre ser mais rápido, mas não vi uma diferença de velocidade perceptível para um milhão de itens.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))
akaihola
fonte
14
Se a fusão querysets da mesma tabela para realizar uma ou consulta e tem linhas duplicadas você pode eliminá-los com a função groupby: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo
1
Ok, então nm sobre a função groupby neste contexto. Com a função Q, você poderá executar qualquer consulta OR necessária: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo
2
O @apelliciari Chain usa significativamente menos memória que list.extend, porque não precisa carregar as duas listas completamente na memória.
Dan Gayle
2
@AWrightIV Aqui está a nova versão desse link: docs.djangoproject.com/en/1.8/topics/db/queries/...
Josh Russo
1
tentar este approacg mas têm'list' object has no attribute 'complex_filter'
grillazz
466

Tente o seguinte:

matches = pages | articles | posts

Ele mantém todas as funções dos conjuntos de consultas, o que é bom se você quiser order_byou algo semelhante.

Observe: isso não funciona em conjuntos de consultas de dois modelos diferentes.

Daniel Holmes
fonte
10
Porém, não funciona em conjuntos de consultas fatiadas. Ou eu estou esquecendo de alguma coisa?
sthzg
1
Eu costumava ingressar nos conjuntos de consultas usando "|" mas nem sempre funciona bem. É melhor usar "Q": docs.djangoproject.com/en/dev/topics/db/queries/…
Ignacio Pérez
1
Não parece criar duplicatas, usando o Django 1.6.
Teekin
15
Aqui |está o operador de união de conjunto, não OR bit a bit.
E100
6
@ e100 não, não é o operador de união definido. Django sobrecarrega o operador binário OR: github.com/django/django/blob/master/django/db/models/...
shangxiao
109

Relacionado, para misturar conjuntos de consultas do mesmo modelo ou para campos semelhantes de alguns modelos, a partir do Django 1.11 qs.union(), também está disponível um método :

union()

union(*other_qs, all=False)

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

>>> qs1.union(qs2, qs3)

O operador UNION seleciona apenas valores distintos por padrão. Para permitir valores duplicados, use o argumento all = True.

union (), interseção () e diferença () retornam instâncias de modelo do tipo do primeiro QuerySet, mesmo que os argumentos sejam QuerySets de outros modelos. A passagem de modelos diferentes funciona desde que a lista SELECT seja a mesma em todos os QuerySets (pelo menos os tipos, os nomes não importam, desde que os tipos na mesma ordem).

Além disso, apenas LIMIT, OFFSET e ORDER BY (ou seja, corte e order_by ()) são permitidos no QuerySet resultante. Além disso, os bancos de dados impõem restrições sobre quais operações são permitidas nas consultas combinadas. Por exemplo, a maioria dos bancos de dados não permite LIMIT ou OFFSET nas consultas combinadas.

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

Udi
fonte
Esta é uma solução melhor para o meu conjunto de problemas que precisa ter valores exclusivos.
Burning Crystals
Não funciona para geometrias de geodjango.
MarMat
De onde você importa a união? Ele precisa vir de um dos números X de conjuntos de consultas?
19419 Jack
Sim, é um método de consulta.
Udi19:
Eu acho que remove filtros de pesquisa
Pierre Cordier
76

Você pode usar a QuerySetChainclasse abaixo. Ao usá-lo com o paginador do Django, ele só deve atingir o banco de dados com COUNT(*)consultas para todos os conjuntos de SELECT()consultas e consultas apenas para os conjuntos de consultas cujos registros são exibidos na página atual.

Observe que você precisa especificar template_name=se está usando a QuerySetChaincom visualizações genéricas, mesmo que todos os conjuntos de consultas em cadeia usem 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 matchescom o paginador como você usou result_listno seu exemplo.

O itertoolsmódulo foi introduzido no Python 2.3, portanto deve estar disponível em todas as versões do Python nas quais o Django roda.

akaihola
fonte
5
Boa abordagem, mas um problema que vejo aqui é que os conjuntos de consultas são anexados "da cabeça à cauda". E se cada conjunto de consultas for ordenado por data e for necessário que o conjunto combinado também seja ordenado por data?
hasen
Isso certamente parece promissor, ótimo, vou ter que tentar isso, mas hoje não tenho tempo. Voltarei a você se resolver o meu problema. Ótimo trabalho.
espenhogbakk
Ok, eu tive que tentar hoje, mas não funcionou, primeiro ele reclamou que não tinha que _clonar atributo, então eu adicionei aquele, apenas copiei o _tudo e funcionou, mas parece que o paginador tem algum problema com esse conjunto de consultas. Eu recebo este erro do paginador: "len () de objeto não
dimensionado
1
Biblioteca @Espen Python: pdb, log. Externo: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. Use instruções de impressão no código ou use o módulo de registro. Acima de tudo, aprenda a introspectar na concha. Google para postagens de blog sobre depuração do Django. Feliz em ajudar!
Akaihola
4
@patrick ver djangosnippets.org/snippets/1103 e djangosnippets.org/snippets/1933 - epecially o último é uma solução muito abrangente
akaihola
27

A grande desvantagem de sua abordagem atual é sua ineficiência com grandes conjuntos de resultados de pesquisa, pois você precisa extrair todo o conjunto de resultados do banco de dados a cada vez, mesmo que apenas pretenda exibir uma página de resultados.

Para retirar apenas os objetos que você realmente precisa do banco de dados, você deve usar a paginação em um QuerySet, não em uma lista. Se você fizer isso, o Django fatiará o QuerySet antes da execução da consulta, portanto a consulta SQL usará OFFSET e LIMIT para obter apenas os registros que você realmente exibirá. Mas você não pode fazer isso, a menos que possa agrupar sua pesquisa em uma única consulta de alguma forma.

Como todos os três modelos têm campos de título e corpo, por que não usar a herança de modelos ? Apenas tenha todos os três modelos herdados de um ancestral comum que possua título e corpo e execute a pesquisa como uma única consulta no modelo de ancestral.

Carl Meyer
fonte
23

Caso você queira encadear muitos conjuntos de consultas, tente o seguinte:

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

onde: docs é uma lista de conjuntos de consultas

vutran
fonte
8

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

1ª maneira de fazer isso

Use o operador union para o queryset |para obter a união de dois queryset. Se o conjunto de consultas pertencer ao mesmo modelo / modelo único, será possível combinar conjuntos de consultas usando o operador union.

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 maneira de obter uma operação combinada entre dois conjuntos de consultas é usar a função de cadeia itertools .

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Devang Padhiyar
fonte
7

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

Caso você deseje combinar querysetse ainda obter um a QuerySet, convém verificar django-queryset-sequence .

Mas uma nota sobre isso. São necessários apenas dois querysetscomo argumento. Mas com python reducevocê sempre pode aplicá-lo 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 situação em que me deparei e como eu empregava list comprehension, reduceedjango-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})
chidimo
fonte
1
Não Book.objects.filter(owner__mentor=mentor)faz a mesma coisa? Não tenho certeza se este é um caso de uso válido. Eu acho que um Bookpode precisar ter vários owners antes de você começar a fazer algo assim.
Will S
Sim, faz a mesma coisa. Eu tentei. De qualquer forma, talvez isso possa ser útil em alguma outra situação. Obrigado por apontar isso. Você não começa exatamente conhecendo todos os atalhos como iniciante. Às vezes você tem que viajar a carga sinuosa estrada para apreciar o corvo mosca
chidimo
6

aqui está uma idéia ... basta puxar uma página inteira de resultados de cada um dos três e depois jogar fora os 20 menos úteis ... isso elimina os grandes conjuntos de consultas e, dessa forma, você sacrifica apenas um pouco de desempenho em vez de muito

Jiaaro
fonte
1

Isso fará o trabalho sem usar outras bibliotecas

result_list = list(page_list) + list(article_list) + list(post_list)
Satyam Faujdar
fonte
-1

Essa função recursiva concatena a matriz de conjuntos de consultas em um único conjunto de consultas.

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
Petr Dvořáček
fonte
1
Eu estou literalmente perdido.
lycuid
combinando o resultado da consulta, ele não pode ser usado em tempo de execução e é uma péssima ideia fazer isso. porque em algum momento adiciona duplicação ao resultado.
Devang Hingu