Pregunta

Estoy tratando de construir la búsqueda de un sitio Django que estoy construyendo, y en la búsqueda estoy buscando en 3 modelos diferentes. Y para obtener la paginación en la lista de resultados de búsqueda, me gustaría usar una vista genérica de lista de objetos para mostrar los resultados. Pero para hacer eso tengo que fusionar 3 conjuntos de consultas en uno.

¿Cómo puedo hacer eso? He intentado esto:

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

Pero esto no funciona. Recibo un error cuando intento usar esa lista en la vista genérica. A la lista le falta el atributo de clonación.

¿Alguien sabe cómo puedo fusionar las tres listas, page_list , article_list y post_list ?

¿Fue útil?

Solución

Concatenar los conjuntos de consultas en una lista es el enfoque más simple. Si la base de datos se verá afectada por todos los conjuntos de consultas de todos modos (por ejemplo, porque el resultado necesita ser ordenado), esto no agregará más costos.

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

Usar itertools.chain es más rápido que recorrer cada lista y agregar elementos uno por uno, ya que itertools se implementa en C. También consume menos memoria que convertir cada conjunto de consultas en una lista antes de concatenar.

Ahora es posible ordenar la lista resultante, p. por fecha (según lo solicitado en el comentario de hasen j a otra respuesta). La función sorted () acepta convenientemente un generador y devuelve una lista:

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

Si está usando Python 2.4 o posterior, puede usar attrgetter en lugar de una lambda. Recuerdo haber leído que era más rápido, pero no vi una diferencia de velocidad notable para una lista de millones de artículos.

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

Otros consejos

Prueba esto:

matches = pages | articles | posts

Conserva todas las funciones de los conjuntos de consultas, lo cual es bueno si desea order_by o similar.

Tenga en cuenta: esto no funciona en conjuntos de consultas de dos modelos diferentes.

Relacionado, para mezclar conjuntos de consultas del mismo modelo, o para campos similares de algunos modelos, comenzando con Django 1.11 a qs.union () método también está disponible:

  

union()

union(*other_qs, all=False)
     

Nuevo en Django 1.11 . Utiliza el operador UNION de SQL para combinar los resultados de dos o más QuerySets. Por ejemplo:

>>> qs1.union(qs2, qs3)
     

El operador UNION selecciona solo valores distintos por defecto. Para permitir valores duplicados, use all = True   argumento.

     

union (), intersección () y diferencia () devuelven instancias de modelo de   el tipo del primer QuerySet incluso si los argumentos son QuerySets de   Otros modelos. Pasar diferentes modelos funciona siempre que SELECT   La lista es la misma en todos los QuerySets (al menos los tipos, los nombres don & # 8217; t   importa siempre que los tipos estén en el mismo orden).

     

Además, solo LIMIT, OFFSET y ORDER BY (es decir, rebanar y   order_by ()) están permitidos en el QuerySet resultante. Además, bases de datos   imponer restricciones sobre qué operaciones están permitidas en la combinación   consultas. Por ejemplo, la mayoría de las bases de datos no permiten LIMIT u OFFSET en   las consultas combinadas.

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

Puede usar la clase QuerySetChain a continuación. Cuando se usa con el paginador de Django, solo debe golpear la base de datos con consultas COUNT (*) para todos los conjuntos de consultas y consultas SELECT () solo para aquellos conjuntos de consultas cuyos registros se muestran en la página actual.

Tenga en cuenta que debe especificar template_name = si usa un QuerySetChain con vistas genéricas, incluso si los conjuntos de consultas encadenados usan el mismo 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()

En su ejemplo, el uso sería:

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)

Luego use coincide con el paginador como usó result_list en su ejemplo.

El módulo itertools se introdujo en Python 2.3, por lo que debería estar disponible en todas las versiones de Python en las que se ejecuta Django.

La gran desventaja de su enfoque actual es su ineficiencia con grandes conjuntos de resultados de búsqueda, ya que debe extraer todo el conjunto de resultados de la base de datos cada vez, aunque solo tenga la intención de mostrar una página de resultados.

Para extraer solo los objetos que realmente necesita de la base de datos, debe usar la paginación en un QuerySet, no en una lista. Si hace esto, Django en realidad corta el QuerySet antes de que se ejecute la consulta, por lo que la consulta SQL usará OFFSET y LIMIT para obtener solo los registros que realmente mostrará. Pero no puede hacer esto a menos que pueda agrupar su búsqueda en una sola consulta de alguna manera.

Dado que los tres modelos tienen campos de título y cuerpo, ¿por qué no usar modelo de herencia ? Simplemente haga que los tres modelos hereden de un ancestro común que tenga título y cuerpo, y realice la búsqueda como una sola consulta en el modelo de ancestro.

En caso de que desee encadenar muchos conjuntos de consultas, intente esto:

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

where: docs es una lista de conjuntos de consultas

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)

Citado de https://groups.google.com/forum/#!topic/django -users / 6wUNuJa4jVw . Ver Alex Gaynor

aquí hay una idea ... simplemente despliegue una página completa de resultados de cada uno de los tres y luego arroje los 20 menos útiles ... esto elimina los grandes conjuntos de consultas y de esa manera solo sacrifica un poco de rendimiento en lugar de mucho

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

En caso de que desee combinar querysets y aún así salir con un QuerySet , es posible que desee consultar django-queryset-secuencia .

Pero una nota al respecto. Solo se necesitan dos querysets como argumento. Pero con python reduce siempre puede aplicarlo a múltiples queryset s.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

Y eso es todo. A continuación hay una situación en la que me encontré y cómo empleé comprensión de la lista , reduce y 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})

Esto también se puede lograr de dos maneras.

Primera forma de hacer esto

Use el operador de unión para el conjunto de consultas | para tomar la unión de dos conjuntos de consultas. Si ambos conjuntos de consultas pertenecen al mismo modelo / modelo único, es posible combinar conjuntos de consultas utilizando el operador de unión.

Para una instancia

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 forma de hacer esto

Otra forma de lograr la operación combinada entre dos conjuntos de consultas es usar la función de cadena itertools .

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

Esta función recursiva concatena una matriz de conjuntos de consultas en un 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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top