¿Cómo combinar dos o más conjuntos de consultas en una vista de Django?

654

Estoy tratando de construir la búsqueda de un sitio Django que estoy construyendo, y en esa 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_listy post_list?

espenhogbakk
fuente
Parece que t_rybik ha creado una solución integral en djangosnippets.org/snippets/1933
akaihola
Para buscar es mejor usar soluciones dedicadas como Haystack , es muy flexible.
Minder
1
Usuarios de Django 1.11 y abv, vea esta respuesta - stackoverflow.com/a/42186970/6003362
Sahil Agarwal
nota : la pregunta se limita al caso muy raro cuando, después de fusionar 3 modelos diferentes, no necesita extraer modelos nuevamente en la lista para distinguir datos sobre tipos. Para la mayoría de los casos, si se espera una distinción, la interfaz será incorrecta. Para los mismos modelos: ver respuestas sobre union.
Sławomir Lenart

Respuestas:

1058

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.chaines más rápido que recorrer cada lista y agregar elementos uno por uno, ya que itertoolsse 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, por ejemplo, por fecha (como se solicitó en el comentario de Hasen J a otra respuesta). La sorted()función 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á utilizando Python 2.4 o posterior, puede usar en attrgetterlugar 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'))
akaihola
fuente
14
Si fusiona conjuntos de consultas de la misma tabla para realizar una consulta OR y tiene filas duplicadas, puede eliminarlas con la función 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, entonces nm sobre la función groupby en este contexto. Con la función Q, debería poder realizar cualquier consulta OR que necesite: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo
2
@apelliciari Chain usa significativamente menos memoria que list.extend, porque no necesita cargar ambas listas completamente en la memoria.
Dan Gayle
2
@AWrightIV Aquí está la nueva versión de ese enlace: docs.djangoproject.com/en/1.8/topics/db/queries/…
Josh Russo
1
probando esta aproximación pero tengo'list' object has no attribute 'complex_filter'
grillazz
466

Prueba esto:

matches = pages | articles | posts

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

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

Daniel Holmes
fuente
10
Sin embargo, no funciona en conjuntos de consultas en rodajas. ¿O me estoy perdiendo algo?
sthzg
1
Solía ​​unirme a los conjuntos de consultas usando "|" Pero no siempre funciona bien. Es mejor usar "Q": docs.djangoproject.com/en/dev/topics/db/queries/…
Ignacio Pérez
1
No parece crear duplicados, usando Django 1.6.
Teekin
15
Aquí |está el operador de unión de conjuntos, no bit a bit O.
e100
66
@ e100 no, no es el operador de unión establecida. django sobrecarga el operador OR bit a bit: github.com/django/django/blob/master/django/db/models/…
shangxiao
109

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

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 el argumento all = True.

union (), intersection (), y difference () devuelven instancias de modelo del tipo del primer QuerySet incluso si los argumentos son QuerySets de otros modelos. Pasar diferentes modelos funciona siempre que la lista SELECT sea la misma en todos los QuerySets (al menos los tipos, los nombres no importan siempre que los tipos estén en el mismo orden).

Además, solo LIMIT, OFFSET y ORDER BY (es decir, slicing y order_by ()) están permitidos en el QuerySet resultante. Además, las bases de datos imponen restricciones sobre qué operaciones están permitidas en las consultas combinadas. 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

Udi
fuente
Esta es una mejor solución para mi conjunto de problemas que necesita tener valores únicos.
Burning Crystals
No funciona para geometrías geodjango.
MarMat
¿De dónde importas la unión? ¿Tiene que venir de uno de los X conjuntos de consultas?
Jack
Sí, es un método de conjunto de consultas.
Udi
Creo que elimina los filtros de búsqueda
Pierre Cordier
76

Puedes usar la QuerySetChainclase a continuación. Cuando se usa con el paginador de Django, solo debe llegar a la base de datos con COUNT(*)consultas para todos los conjuntos de SELECT()consultas y consultas 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 QuerySetChaincon 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 utilícelo matchescon el paginador como usó result_listen su ejemplo.

El itertoolsmódulo 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.

akaihola
fuente
55
Buen enfoque, pero un problema que veo aquí es que los conjuntos de consultas se agregan "cabeza a cola". ¿Qué sucede si cada conjunto de consultas está ordenado por fecha y uno necesita que el conjunto combinado también se ordene por fecha?
hasen
Esto ciertamente parece prometedor, genial, tendré que intentarlo, pero no tengo tiempo hoy. Me pondré en contacto con usted si resuelve mi problema. Buen trabajo.
espenhogbakk
Ok, tuve que intentarlo hoy, pero no funcionó, primero se quejó de que no tenía que _ clonar el atributo, así que agregué ese, solo copié el _todo y funcionó, pero parece que el paginador tiene algún problema con este conjunto de consultas. Me sale este error de paginador: "len () de objeto sin tamaño"
espenhogbakk
1
Biblioteca @Espen Python: pdb, registro. Externo: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. Use declaraciones de impresión en código o use el módulo de registro. Sobre todo, aprende a introspectar en el caparazón. Google para publicaciones de blog sobre depuración de Django. ¡Encantado de ayudar!
akaihola
44
@patrick ver djangosnippets.org/snippets/1103 y djangosnippets.org/snippets/1933 - especialmente esta última es una solución muy completa
akaihola
27

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 la herencia del modelo ? 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.

Carl Meyer
fuente
23

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

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

donde: docs es una lista de conjuntos de consultas

vutran
fuente
8

Esto se puede lograr de dos maneras.

Primera forma de hacer esto

Utilice 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.

Por 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

2da 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))
Devang Padhiyar
fuente
7

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

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

Pero una nota al respecto. Solo se necesitan dos querysetscomo argumento. Pero con python reducesiempre puedes aplicarlo a múltiples querysets.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

Y eso es. A continuación hay una situación en la que me encontré y cómo empleé list comprehension, reduceydjango-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
fuente
1
¿ Book.objects.filter(owner__mentor=mentor)No hace lo mismo? No estoy seguro de que este sea un caso de uso válido. Creo que es Bookposible que deba tener varios correos ownerelectrónicos antes de que necesite comenzar a hacer algo así.
Will S
Sí, hace lo mismo. Lo intenté. De todos modos, quizás esto podría ser útil en alguna otra situación. Gracias por señalar eso. No comienzas exactamente conociendo todos los atajos como principiante. A veces hay que recorrer el camino sinuoso de carga para apreciar la mosca del cuervo
chidimo
6

He aquí 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

Jiaaro
fuente
1

Esto hará el trabajo sin usar ninguna otra biblioteca

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

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
Petr Dvořáček
fuente
1
Estoy literalmente perdido.
líquido
combinando el resultado de la consulta no se puede usar en tiempo de ejecución y esa es una muy mala idea para hacerlo. porque a veces se agrega duplicación sobre el resultado.
Devang Hingu