Django поиск в нескольких моделях и удаление дубликатов

Я пытаюсь создать функцию поиска для моего блога, которая ищет по двум моделям Articles и Spots. Эти две модели связаны через поворотную таблицу ArticleSpots. Мой блог структурирован таким образом, что в каждой статье есть несколько мест.

При поиске запроса я хочу, чтобы запрос искался в обеих моделях, но отображались только кликабельные статьи. У меня есть html-страница для каждой статьи, но не для каждого спота, поэтому все споты, полученные в результате поиска, должны быть показаны как статья, которая содержит спот. Надеюсь, это имеет смысл!

Вот код, который я придумал, но проблема в том, что я получаю много дубликатов в переменной results. Есть дубликаты внутри каждого articles_from_spots и articles_from_query, а также есть дубликаты между ними.

Правильно ли это? Как я могу удалить дубликаты из результатов? Любая помощь будет принята с благодарностью!

views.py

def search(request):

    query = request.GET.get("q")

    articles_from_query = Articles.objects.filter(
        Q(title__icontains=query) |
        Q(summary__icontains=query)
    )

    spots_from_query = Spots.objects.filter(
        Q(title__icontains=query) |
        Q(summary__icontains=query) |
        Q(content__icontains=query) 
    )

    articles_from_spots = []
    for x in spots_from_query:
        article = Articles.objects.filter(articlespots__spot=x)
        articles_from_spots.extend(article)

    results = chain(articles_from_spots, articles_from_query)

    context = {
        'results': results,
    }
    return render(request, "Search.html", context)

models.py

class Articles(models.Model):
    title = models.CharField(max_length=155)
    summary = models.TextField(blank=True, null=True)


class ArticleSpots(models.Model):
    article = models.ForeignKey('Articles', models.DO_NOTHING)
    spot = models.ForeignKey('Spots', models.DO_NOTHING)


class Spots(models.Model):
    title = models.CharField(max_length=155)
    summary = models.TextField(blank=True, null=True)
    content = models.TextField(blank=True, null=True)

Вы должны быть в состоянии сделать это в одном запросе, следуя взаимосвязи от статьи к месту

Articles.objects.filter(
    Q(title__icontains=query) |
    Q(summary__icontains=query) |
    Q(articlespots__spot__title__icontains=query) |
    Q(articlespots__spot__summary__icontains=query) |
    Q(articlespots__spot__content__icontains=query) 
).distinct()

Если бы вы добавили поле ManyToManyField из Article в Spots, это бы немного упростило дело и имело бы смысл с точки зрения дизайна

class Articles(models.Model):
    ...
    spots = models.ManyToManyField('Spots', through='ArticleSpots')


Articles.objects.filter(
    Q(title__icontains=query) |
    Q(summary__icontains=query) |
    Q(spots__title__icontains=query) |
    Q(spots__summary__icontains=query) |
    Q(spots__content__icontains=query) 
).distinct()

Основной проблемой является неэффективный цикл for, но сначала я должен предложить кое-что другое.

Я бы настоятельно рекомендовал изменить дизайн модели:

class Articles(models.Model):
    title = models.CharField(max_length=155)
    summary = models.TextField(blank=True, null=True)
    spots = models.ManyToManyField(Spot, blank=True, related_name='articles')


class Spots(models.Model):
    title = models.CharField(max_length=155)
    summary = models.TextField(blank=True, null=True)
    content = models.TextField(blank=True, null=True)

Функция точно такая же (плюс вы можете вызывать spot.articles.all() и article.spots.all()). Вы все еще можете получить доступ к вашей модели ArticleSpots как Article.spots.through, если вам нужно. В случае, если вам позже понадобится больше полей на соединение, вы можете сделать следующее (вместе с вашим оригинальным классом ArticleSpots, возможно, с on_delete=models.CASCADE там вместо этого):

    spots = models.ManyToManyField(Spot, blank=True, through= 'ArticleSpots')

Цикл for-loop неэффективен (подумайте о десятках секунд, когда вы дойдете до тысяч объектов или если произойдет объединение), потому что он запускает запрос для каждого элемента в результате. Вместо этого вы должны получить articles_from_spots с помощью прямого запроса. IE.:

article_ids = spots_from_query.values_list('articles__id', flat=True)
articles_from_spots = Article.objects.filter(id__in=article_ids)

Это гарантирует только 2 db-запроса за прогон. Затем вам нужно будет сделать что-то вроде того, чтобы превратить наборы запросов в списки перед их объединением:

results = chain(map(list, [articles_from_spots, articles_from_query]))

Возможно, все еще есть проблемы со смешиванием двух модельных кверисетов вместе, но все зависит от вашего шаблона. В целом это плохая практика, но острой проблемы, насколько вы знаете, нет.

Вернуться на верх