Domanda

Sto cercando di costruire la ricerca di un sito Django che sto costruendo, e nella ricerca sto cercando in 3 diversi modelli. E per ottenere l'impaginazione nell'elenco dei risultati della ricerca, vorrei utilizzare una visualizzazione generica object_list per visualizzare i risultati. Ma per farlo devo unire 3 queryset in uno.

Come posso farlo? Ho provato questo:

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

Ma questo non funziona Ottengo un errore quando provo a usare quell'elenco nella vista generica. Nell'elenco manca l'attributo clone.

Qualcuno sa come posso unire le tre liste, page_list , article_list e post_list ?

È stato utile?

Soluzione

La concatenazione dei queryset in un elenco è l'approccio più semplice. Se il database verrà colpito per tutti i queryset (ad es. Perché il risultato deve essere ordinato), ciò non comporterà ulteriori costi.

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

L'uso di itertools.chain è più veloce del loop di ogni elenco e dell'aggiunta di elementi uno per uno, poiché itertools è implementato in C. Inoltre, consuma meno memoria rispetto alla conversione di ogni query in un elenco prima di concatenare.

Ora è possibile ordinare l'elenco risultante, ad es. per data (come richiesto nel commento di hasen j ad un'altra risposta). La funzione sort () accetta convenientemente un generatore e restituisce un elenco:

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

Se stai usando Python 2.4 o successivo, puoi usare attrgetter invece di un lambda. Ricordo di aver letto che era più veloce, ma non ho notato una notevole differenza di velocità per un milione di articoli.

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

Altri suggerimenti

Prova questo:

matches = pages | articles | posts

Mantiene tutte le funzioni dei queryset, il che è utile se si desidera order_by o simili.

Nota: questo non funziona su queryset di due diversi modelli.

Puoi utilizzare la classe QuerySetChain di seguito. Quando lo si utilizza con il paginator di Django, dovrebbe colpire il database solo con le query COUNT (*) per tutte le query e le query SELECT () solo per quelle query i cui record sono visualizzati su la pagina corrente.

Nota che devi specificare template_name = se usi un QuerySetChain con viste generiche, anche se tutti i queryset concatenati usano lo stesso modello.

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

Nel tuo esempio, l'utilizzo sarebbe:

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)

Quindi usa corrispondenze con il paginatore come hai usato result_list nel tuo esempio.

Il modulo itertools è stato introdotto in Python 2.3, quindi dovrebbe essere disponibile in tutte le versioni di Python su cui Django gira.

Il grande svantaggio del vostro attuale approccio è la sua inefficienza con grandi set di risultati di ricerca, poiché ogni volta è necessario rimuovere l'intero set di risultati dal database, anche se si intende visualizzare solo una pagina di risultati.

Per estrarre solo gli oggetti effettivamente necessari dal database, è necessario utilizzare l'impaginazione su un QuerySet, non su un elenco. Se lo fai, Django effettivamente suddivide il QuerySet prima dell'esecuzione della query, quindi la query SQL utilizzerà OFFSET e LIMIT per ottenere solo i record che verranno effettivamente visualizzati. Ma non puoi farlo a meno che tu non riesca a raggruppare la tua ricerca in una singola query in qualche modo.

Dato che tutti e tre i tuoi modelli hanno campi titolo e corpo, perché non utilizzare eredità del modello ? Basta avere tutti e tre i modelli ereditati da un antenato comune con titolo e corpo ed eseguire la ricerca come una singola query sul modello antenato.

Nel caso in cui desideri concatenare molti queryset, prova questo:

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

dove: docs è un elenco di queryset

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)

Citato da https://groups.google.com/forum/#!topic/django : agli utenti / 6wUNuJa4jVw . Vedi Alex Gaynor

ecco un'idea ... basta tirare giù una pagina intera di risultati da ciascuno dei tre e poi buttare via i 20 meno utili ... questo elimina i queryset di grandi dimensioni e in questo modo sacrifichi solo un po 'di prestazioni invece di molto

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

Nel caso in cui si desideri combinare queryset e uscire comunque con un QuerySet , si consiglia di controllare sequenza di django-queryset .

Ma una nota a riguardo. Ci vogliono solo due queryset come argomento. Ma con Python riduci puoi sempre applicarlo a più queryset .

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

E questo è tutto. Di seguito è una situazione in cui mi sono imbattuto e come ho impiegato comprensione delle liste , riduci 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})

Questo può essere ottenuto in due modi.

1o modo per farlo

Usa l'operatore union per il queryset | per prendere l'unione di due queryset. Se entrambi i queryset appartengono allo stesso modello / modello singolo di quanto sia possibile combinare i queryset utilizzando l'operatore union.

Per un'istanza

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

Secondo modo per farlo

Un altro modo per ottenere l'operazione di combinazione tra due queryset è utilizzare la funzione a catena itertools .

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

Questa funzione ricorsiva concatena la matrice di queryset in un solo 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
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top